feat: add full-column sorting and advanced filtering to semi module
Co-authored-by: aider (openai/DeepSeek-V3.2-Thinking) <aider@aider.chat>
This commit is contained in:
@ -75,7 +75,38 @@ def get_list():
|
|||||||
keyword = request.args.get('keyword', '')
|
keyword = request.args.get('keyword', '')
|
||||||
statuses_str = request.args.get('statuses', '')
|
statuses_str = request.args.get('statuses', '')
|
||||||
statuses = statuses_str.split(',') if statuses_str else []
|
statuses = statuses_str.split(',') if statuses_str else []
|
||||||
result = SemiInboundService.get_list(page, limit, keyword, statuses)
|
company = request.args.get('company', '')
|
||||||
|
category = request.args.get('category', '')
|
||||||
|
material_type = request.args.get('material_type', '')
|
||||||
|
order_by_column = request.args.get('orderByColumn', '')
|
||||||
|
is_asc = request.args.get('isAsc', '')
|
||||||
|
advanced_filters_str = request.args.get('advancedFilters', '')
|
||||||
|
|
||||||
|
# 准备额外筛选字典
|
||||||
|
extra_filters = {}
|
||||||
|
if company:
|
||||||
|
extra_filters['company'] = company
|
||||||
|
if category:
|
||||||
|
extra_filters['category'] = category
|
||||||
|
if material_type:
|
||||||
|
extra_filters['material_type'] = material_type
|
||||||
|
if order_by_column:
|
||||||
|
extra_filters['order_by_column'] = order_by_column
|
||||||
|
if is_asc:
|
||||||
|
extra_filters['is_asc'] = is_asc
|
||||||
|
if advanced_filters_str:
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
advanced_filters = json.loads(advanced_filters_str)
|
||||||
|
extra_filters['advanced_filters'] = advanced_filters
|
||||||
|
except Exception:
|
||||||
|
extra_filters['advanced_filters'] = []
|
||||||
|
|
||||||
|
# 调用服务,传入所有参数
|
||||||
|
result = SemiInboundService.get_list(
|
||||||
|
page, limit, keyword, statuses,
|
||||||
|
**extra_filters
|
||||||
|
)
|
||||||
user_permissions = get_current_user_permissions()
|
user_permissions = get_current_user_permissions()
|
||||||
if result.get('items'):
|
if result.get('items'):
|
||||||
result['items'] = [filter_item_by_permissions(item, user_permissions) for item in result['items']]
|
result['items'] = [filter_item_by_permissions(item, user_permissions) for item in result['items']]
|
||||||
|
|||||||
@ -343,7 +343,8 @@ class SemiInboundService:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_list(page, limit, keyword=None, statuses=None, category=None, material_type=None, company=None):
|
def get_list(page, limit, keyword=None, statuses=None, category=None, material_type=None, company=None,
|
||||||
|
order_by_column=None, is_asc=None, advanced_filters=None):
|
||||||
from app.models.inbound.semi import StockSemi
|
from app.models.inbound.semi import StockSemi
|
||||||
try:
|
try:
|
||||||
query = db.session.query(StockSemi).outerjoin(MaterialBase, StockSemi.base_id == MaterialBase.id)
|
query = db.session.query(StockSemi).outerjoin(MaterialBase, StockSemi.base_id == MaterialBase.id)
|
||||||
@ -380,11 +381,108 @@ class SemiInboundService:
|
|||||||
StockSemi.stock_quantity > 0
|
StockSemi.stock_quantity > 0
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
pagination = query.order_by(StockSemi.production_date.desc()).paginate(page=page, per_page=limit,
|
|
||||||
error_out=False)
|
# 动态高级筛选
|
||||||
|
if advanced_filters:
|
||||||
|
if isinstance(advanced_filters, str):
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
advanced_filters = json.loads(advanced_filters)
|
||||||
|
except:
|
||||||
|
advanced_filters = []
|
||||||
|
if isinstance(advanced_filters, list):
|
||||||
|
field_mapping = {
|
||||||
|
'company_name': MaterialBase.company_name,
|
||||||
|
'material_name': MaterialBase.name,
|
||||||
|
'spec_model': MaterialBase.spec_model,
|
||||||
|
'category': MaterialBase.category,
|
||||||
|
'material_type': MaterialBase.material_type,
|
||||||
|
'status': StockSemi.status,
|
||||||
|
'quality_status': StockSemi.quality_status,
|
||||||
|
'warehouse_location': StockSemi.warehouse_location,
|
||||||
|
'bom_code': StockSemi.bom_code,
|
||||||
|
'work_order_code': StockSemi.work_order_code,
|
||||||
|
'qty_stock': StockSemi.stock_quantity,
|
||||||
|
'qty_available': StockSemi.available_quantity,
|
||||||
|
'unit_total_cost': StockSemi.manual_cost,
|
||||||
|
'raw_material_cost': StockSemi.raw_material_cost,
|
||||||
|
}
|
||||||
|
for cond in advanced_filters:
|
||||||
|
field = cond.get('field')
|
||||||
|
operator = cond.get('operator')
|
||||||
|
value = cond.get('value')
|
||||||
|
if not field or not operator:
|
||||||
|
continue
|
||||||
|
model_field = field_mapping.get(field)
|
||||||
|
if model_field is None:
|
||||||
|
continue
|
||||||
|
# 防止 SQL 注入,只允许映射的字段
|
||||||
|
if operator == '=':
|
||||||
|
query = query.filter(model_field == value)
|
||||||
|
elif operator == '!=':
|
||||||
|
query = query.filter(model_field != value)
|
||||||
|
elif operator == 'like':
|
||||||
|
query = query.filter(model_field.ilike(f'%{value}%'))
|
||||||
|
elif operator == 'not_like':
|
||||||
|
query = query.filter(~model_field.ilike(f'%{value}%'))
|
||||||
|
elif operator == '>':
|
||||||
|
if value.replace('.', '', 1).isdigit():
|
||||||
|
query = query.filter(model_field > float(value))
|
||||||
|
elif operator == '<':
|
||||||
|
if value.replace('.', '', 1).isdigit():
|
||||||
|
query = query.filter(model_field < float(value))
|
||||||
|
elif operator == '>=':
|
||||||
|
if value.replace('.', '', 1).isdigit():
|
||||||
|
query = query.filter(model_field >= float(value))
|
||||||
|
elif operator == '<=':
|
||||||
|
if value.replace('.', '', 1).isdigit():
|
||||||
|
query = query.filter(model_field <= float(value))
|
||||||
|
|
||||||
|
# 动态排序
|
||||||
|
order_field = None
|
||||||
|
if order_by_column and is_asc is not None:
|
||||||
|
order_mapping = {
|
||||||
|
'id': StockSemi.id,
|
||||||
|
'base_id': StockSemi.base_id,
|
||||||
|
'company_name': MaterialBase.company_name,
|
||||||
|
'material_name': MaterialBase.name,
|
||||||
|
'category': MaterialBase.category,
|
||||||
|
'material_type': MaterialBase.material_type,
|
||||||
|
'spec_model': MaterialBase.spec_model,
|
||||||
|
'unit': MaterialBase.unit,
|
||||||
|
'sku': StockSemi.sku,
|
||||||
|
'inbound_date': StockSemi.production_date,
|
||||||
|
'barcode': StockSemi.barcode,
|
||||||
|
'serial_number': StockSemi.serial_number,
|
||||||
|
'batch_number': StockSemi.batch_number,
|
||||||
|
'status': StockSemi.status,
|
||||||
|
'quality_status': StockSemi.quality_status,
|
||||||
|
'qty_inbound': StockSemi.in_quantity,
|
||||||
|
'qty_stock': StockSemi.stock_quantity,
|
||||||
|
'qty_available': StockSemi.available_quantity,
|
||||||
|
'warehouse_loc': StockSemi.warehouse_location,
|
||||||
|
'bom_code': StockSemi.bom_code,
|
||||||
|
'bom_version': StockSemi.bom_version,
|
||||||
|
'work_order_code': StockSemi.work_order_code,
|
||||||
|
'raw_material_cost': StockSemi.raw_material_cost,
|
||||||
|
'unit_total_cost': StockSemi.manual_cost,
|
||||||
|
'total_price': StockSemi.total_price,
|
||||||
|
'production_manager': StockSemi.production_manager,
|
||||||
|
'production_start_time': StockSemi.production_start_time,
|
||||||
|
'production_end_time': StockSemi.production_end_time,
|
||||||
|
}
|
||||||
|
order_field = order_mapping.get(order_by_column)
|
||||||
|
if order_field is not None:
|
||||||
|
if is_asc == 'true' or is_asc == True:
|
||||||
|
query = query.order_by(order_field.asc())
|
||||||
|
else:
|
||||||
|
query = query.order_by(order_field.desc())
|
||||||
|
if order_field is None:
|
||||||
|
query = query.order_by(StockSemi.production_date.desc())
|
||||||
|
|
||||||
|
pagination = query.paginate(page=page, per_page=limit, error_out=False)
|
||||||
items = []
|
items = []
|
||||||
for item in pagination.items:
|
for item in pagination.items:
|
||||||
# 把 manual_cost 伪装成 unit_total_cost 返回给前端
|
|
||||||
item_dict = item.to_dict()
|
item_dict = item.to_dict()
|
||||||
item_dict['unit_total_cost'] = float(item.manual_cost or 0)
|
item_dict['unit_total_cost'] = float(item.manual_cost or 0)
|
||||||
items.append(item_dict)
|
items.append(item_dict)
|
||||||
|
|||||||
@ -54,6 +54,36 @@
|
|||||||
<el-button type="primary" plain class="search-btn" @click="fetchData">搜索</el-button>
|
<el-button type="primary" plain class="search-btn" @click="fetchData">搜索</el-button>
|
||||||
<el-button class="reset-btn" @click="resetQuery">重置</el-button>
|
<el-button class="reset-btn" @click="resetQuery">重置</el-button>
|
||||||
|
|
||||||
|
<el-popover
|
||||||
|
placement="bottom"
|
||||||
|
width="600"
|
||||||
|
trigger="click"
|
||||||
|
v-model:visible="advancedFilterVisible"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<el-button type="primary" plain>高级筛选</el-button>
|
||||||
|
</template>
|
||||||
|
<div style="padding: 10px;">
|
||||||
|
<div v-for="(cond, idx) in advancedConditions" :key="idx" style="display: flex; gap: 10px; margin-bottom: 10px; align-items: center;">
|
||||||
|
<el-select v-model="cond.field" placeholder="字段" style="width: 150px;">
|
||||||
|
<el-option v-for="opt in fieldOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||||
|
</el-select>
|
||||||
|
<el-select v-model="cond.operator" placeholder="操作符" style="width: 120px;">
|
||||||
|
<el-option v-for="opt in operatorOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
||||||
|
</el-select>
|
||||||
|
<el-input v-model="cond.value" placeholder="值" style="flex: 1;" />
|
||||||
|
<el-button type="danger" size="small" @click="removeCondition(idx)" :disabled="advancedConditions.length === 1">删除</el-button>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; justify-content: space-between; margin-top: 10px;">
|
||||||
|
<el-button type="primary" size="small" @click="addCondition">添加条件</el-button>
|
||||||
|
<div>
|
||||||
|
<el-button size="small" @click="resetAdvancedFilter">重置</el-button>
|
||||||
|
<el-button type="primary" size="small" @click="applyAdvancedFilter">应用</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-popover>
|
||||||
|
|
||||||
<el-select
|
<el-select
|
||||||
v-model="queryParams.statuses"
|
v-model="queryParams.statuses"
|
||||||
multiple
|
multiple
|
||||||
@ -103,6 +133,7 @@
|
|||||||
class="modern-table"
|
class="modern-table"
|
||||||
highlight-current-row
|
highlight-current-row
|
||||||
header-cell-class-name="table-header-gray"
|
header-cell-class-name="table-header-gray"
|
||||||
|
@sort-change="handleSortChange"
|
||||||
>
|
>
|
||||||
<template v-for="col in allColumns" :key="col.prop">
|
<template v-for="col in allColumns" :key="col.prop">
|
||||||
<el-table-column
|
<el-table-column
|
||||||
@ -110,6 +141,7 @@
|
|||||||
:prop="col.prop"
|
:prop="col.prop"
|
||||||
:label="col.label"
|
:label="col.label"
|
||||||
:min-width="col.minWidth || '140'"
|
:min-width="col.minWidth || '140'"
|
||||||
|
:sortable="col.sortable ? 'custom' : false"
|
||||||
show-overflow-tooltip
|
show-overflow-tooltip
|
||||||
>
|
>
|
||||||
<template #default="scope" v-if="col.prop === 'material_name'">
|
<template #default="scope" v-if="col.prop === 'material_name'">
|
||||||
@ -586,6 +618,30 @@ const queryParams = reactive({ page: 1, pageSize: 100, keyword: '', category: ''
|
|||||||
const categoryOptions = ref<string[]>([])
|
const categoryOptions = ref<string[]>([])
|
||||||
const typeOptions = ref<string[]>([])
|
const typeOptions = ref<string[]>([])
|
||||||
const companyOptions = ref<string[]>([]) // [新增]
|
const companyOptions = ref<string[]>([]) // [新增]
|
||||||
|
const advancedFilterVisible = ref(false)
|
||||||
|
const advancedConditions = ref([{ field: '', operator: '', value: '' }])
|
||||||
|
const fieldOptions = ref([
|
||||||
|
{ label: '所属公司', value: 'company_name' },
|
||||||
|
{ label: '名称', value: 'material_name' },
|
||||||
|
{ label: '规格型号', value: 'spec_model' },
|
||||||
|
{ label: '类别', value: 'category' },
|
||||||
|
{ label: '类型', value: 'material_type' },
|
||||||
|
{ label: '状态', value: 'status' },
|
||||||
|
{ label: '质量状态', value: 'quality_status' },
|
||||||
|
{ label: '库位', value: 'warehouse_location' },
|
||||||
|
{ label: 'BOM编号', value: 'bom_code' },
|
||||||
|
{ label: '工单号', value: 'work_order_code' },
|
||||||
|
])
|
||||||
|
const operatorOptions = ref([
|
||||||
|
{ label: '等于', value: '=' },
|
||||||
|
{ label: '不等于', value: '!=' },
|
||||||
|
{ label: '包含', value: 'like' },
|
||||||
|
{ label: '不包含', value: 'not_like' },
|
||||||
|
{ label: '大于', value: '>' },
|
||||||
|
{ label: '小于', value: '<' },
|
||||||
|
{ label: '大于等于', value: '>=' },
|
||||||
|
{ label: '小于等于', value: '<=' },
|
||||||
|
])
|
||||||
const materialOptions = ref<any[]>([])
|
const materialOptions = ref<any[]>([])
|
||||||
const searchPage = ref(1)
|
const searchPage = ref(1)
|
||||||
const searchKeyword = ref('')
|
const searchKeyword = ref('')
|
||||||
@ -618,39 +674,39 @@ const modeLocked = ref(false)
|
|||||||
|
|
||||||
// 列定义
|
// 列定义
|
||||||
const baseColumns = [
|
const baseColumns = [
|
||||||
{prop: 'company_name', label: '所属公司'}, // [新增]
|
{prop: 'company_name', label: '所属公司', sortable: true}, // [新增]
|
||||||
{prop: 'material_name', label: '名称'},
|
{prop: 'material_name', label: '名称', sortable: true},
|
||||||
{prop: 'category', label: '类别'},
|
{prop: 'category', label: '类别', sortable: true},
|
||||||
{prop: 'material_type', label: '类型'},
|
{prop: 'material_type', label: '类型', sortable: true},
|
||||||
{prop: 'spec_model', label: '规格型号'},
|
{prop: 'spec_model', label: '规格型号', sortable: true},
|
||||||
{prop: 'unit', label: '单位'},
|
{prop: 'unit', label: '单位', sortable: true},
|
||||||
]
|
]
|
||||||
|
|
||||||
const stockColumns = [
|
const stockColumns = [
|
||||||
{prop: 'id', label: 'ID', minWidth: '60'},
|
{prop: 'id', label: 'ID', minWidth: '60', sortable: true},
|
||||||
{prop: 'base_id', label: 'BaseID', minWidth: '80'},
|
{prop: 'base_id', label: 'BaseID', minWidth: '80', sortable: true},
|
||||||
{prop: 'sku', label: 'SKU', minWidth: '120'},
|
{prop: 'sku', label: 'SKU', minWidth: '120', sortable: true},
|
||||||
{prop: 'inbound_date', label: '入库日期', minWidth: '120'},
|
{prop: 'inbound_date', label: '入库日期', minWidth: '120', sortable: true},
|
||||||
{prop: 'barcode', label: '条码', minWidth: '120'},
|
{prop: 'barcode', label: '条码', minWidth: '120', sortable: true},
|
||||||
{prop: 'sn_bn', label: '序列号/批号', minWidth: '160'},
|
{prop: 'sn_bn', label: '序列号/批号', minWidth: '160', sortable: false},
|
||||||
{prop: 'status', label: '状态', minWidth: '100'},
|
{prop: 'status', label: '状态', minWidth: '100', sortable: true},
|
||||||
{prop: 'quality_status', label: '质量状态', minWidth: '100'},
|
{prop: 'quality_status', label: '质量状态', minWidth: '100', sortable: true},
|
||||||
{prop: 'qty_inbound', label: '入库量', minWidth: '100'},
|
{prop: 'qty_inbound', label: '入库量', minWidth: '100', sortable: true},
|
||||||
{prop: 'qty_stock', label: '库存数', minWidth: '100'},
|
{prop: 'qty_stock', label: '库存数', minWidth: '100', sortable: true},
|
||||||
{prop: 'qty_available', label: '可用数', minWidth: '100'},
|
{prop: 'qty_available', label: '可用数', minWidth: '100', sortable: true},
|
||||||
{prop: 'warehouse_loc', label: '库位', minWidth: '120'},
|
{prop: 'warehouse_loc', label: '库位', minWidth: '120', sortable: true},
|
||||||
{prop: 'bom_code', label: 'BOM编号', minWidth: '120'},
|
{prop: 'bom_code', label: 'BOM编号', minWidth: '120', sortable: true},
|
||||||
{prop: 'bom_version', label: 'BOM版本', minWidth: '90'},
|
{prop: 'bom_version', label: 'BOM版本', minWidth: '90', sortable: true},
|
||||||
{prop: 'work_order_code', label: '工单号', minWidth: '120'},
|
{prop: 'work_order_code', label: '工单号', minWidth: '120', sortable: true},
|
||||||
{prop: 'raw_material_cost', label: '原料成本', minWidth: '100'},
|
{prop: 'raw_material_cost', label: '原料成本', minWidth: '100', sortable: true},
|
||||||
{prop: 'unit_total_cost', label: '单件成本', minWidth: '100'},
|
{prop: 'unit_total_cost', label: '单件成本', minWidth: '100', sortable: true},
|
||||||
{prop: 'total_price', label: '总成本', minWidth: '100'},
|
{prop: 'total_price', label: '总成本', minWidth: '100', sortable: true},
|
||||||
{prop: 'production_manager', label: '生产负责人', minWidth: '100'},
|
{prop: 'production_manager', label: '生产负责人', minWidth: '100', sortable: true},
|
||||||
{prop: 'production_start_time', label: '生产开始', minWidth: '160'},
|
{prop: 'production_start_time', label: '生产开始', minWidth: '160', sortable: true},
|
||||||
{prop: 'production_end_time', label: '生产结束', minWidth: '160'},
|
{prop: 'production_end_time', label: '生产结束', minWidth: '160', sortable: true},
|
||||||
{prop: 'arrival_photo', label: '到货图', minWidth: '100'},
|
{prop: 'arrival_photo', label: '到货图', minWidth: '100', sortable: false},
|
||||||
{prop: 'quality_report_link', label: '质量报告', minWidth: '100'},
|
{prop: 'quality_report_link', label: '质量报告', minWidth: '100', sortable: false},
|
||||||
{prop: 'detail_link', label: '详情链接', minWidth: '100'},
|
{prop: 'detail_link', label: '详情链接', minWidth: '100', sortable: false},
|
||||||
]
|
]
|
||||||
const allColumns = [...baseColumns, ...stockColumns]
|
const allColumns = [...baseColumns, ...stockColumns]
|
||||||
|
|
||||||
@ -943,10 +999,31 @@ const handleEntryModeChange = (val: string) => {
|
|||||||
else { form.batch_number = ''; if(formRef.value) formRef.value.clearValidate('batch_number') }
|
else { form.batch_number = ''; if(formRef.value) formRef.value.clearValidate('batch_number') }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleSortChange = ({ column, prop, order }: any) => {
|
||||||
|
if (order === 'ascending') {
|
||||||
|
queryParams.orderByColumn = prop
|
||||||
|
queryParams.isAsc = 'true'
|
||||||
|
} else if (order === 'descending') {
|
||||||
|
queryParams.orderByColumn = prop
|
||||||
|
queryParams.isAsc = 'false'
|
||||||
|
} else {
|
||||||
|
queryParams.orderByColumn = ''
|
||||||
|
queryParams.isAsc = ''
|
||||||
|
}
|
||||||
|
queryParams.page = 1
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const params = { ...queryParams, statuses: queryParams.statuses.join(',') }
|
const params = {
|
||||||
|
...queryParams,
|
||||||
|
statuses: queryParams.statuses.join(','),
|
||||||
|
orderByColumn: queryParams.orderByColumn,
|
||||||
|
isAsc: queryParams.isAsc,
|
||||||
|
advancedFilters: queryParams.advancedFilters.length > 0 ? JSON.stringify(queryParams.advancedFilters) : ''
|
||||||
|
}
|
||||||
const res: any = await getSemiList(params)
|
const res: any = await getSemiList(params)
|
||||||
tableData.value = res.data.items || []
|
tableData.value = res.data.items || []
|
||||||
total.value = res.data.total || 0
|
total.value = res.data.total || 0
|
||||||
@ -975,6 +1052,27 @@ const resetQuery = () => {
|
|||||||
fetchData()
|
fetchData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addCondition = () => {
|
||||||
|
advancedConditions.value.push({ field: '', operator: '', value: '' })
|
||||||
|
}
|
||||||
|
const removeCondition = (index: number) => {
|
||||||
|
advancedConditions.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
const applyAdvancedFilter = () => {
|
||||||
|
const validConditions = advancedConditions.value.filter(c => c.field && c.operator && c.value !== '')
|
||||||
|
queryParams.advancedFilters = validConditions
|
||||||
|
advancedFilterVisible.value = false
|
||||||
|
queryParams.page = 1
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
const resetAdvancedFilter = () => {
|
||||||
|
advancedConditions.value = [{ field: '', operator: '', value: '' }]
|
||||||
|
queryParams.advancedFilters = []
|
||||||
|
advancedFilterVisible.value = false
|
||||||
|
queryParams.page = 1
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
const handleCreate = () => {
|
const handleCreate = () => {
|
||||||
dialogStatus.value = 'create'
|
dialogStatus.value = 'create'
|
||||||
resetForm()
|
resetForm()
|
||||||
|
|||||||
Reference in New Issue
Block a user