feat(material): add global floating helper to track latest specification codes with smart grouping

This commit is contained in:
DXC
2026-04-13 08:28:27 +08:00
parent e23e8c6a9e
commit c7ac092be4
5 changed files with 298 additions and 1 deletions

View File

@ -455,3 +455,21 @@ def batch_set_inspection():
db.session.rollback() db.session.rollback()
current_app.logger.error(f"批量设置强制质检失败: {str(e)}") current_app.logger.error(f"批量设置强制质检失败: {str(e)}")
return jsonify({"code": 500, "msg": f"批量设置强制质检失败: {str(e)}"}), 500 return jsonify({"code": 500, "msg": f"批量设置强制质检失败: {str(e)}"}), 500
# ==============================================================================
# 2.7 智能分组求最大连号 API (GET /api/v1/inbound/base/spec-latest)
# ==============================================================================
@inbound_base_bp.route('/spec-latest', methods=['GET'])
@permission_required('material_list')
def get_spec_latest():
"""
获取所有规格型号的最大连号,按智能分组返回
返回格式: [{"group": "S", "latest": "S0115/S0115"}, {"group": "Opt4xxx", "latest": "Opt4018/Opt4018"}, ...]
"""
try:
data = MaterialBaseService.get_latest_specs()
return jsonify({"code": 200, "msg": "success", "data": data})
except Exception as e:
traceback.print_exc()
return jsonify({"code": 500, "msg": str(e)}), 500

View File

@ -955,3 +955,70 @@ class MaterialBaseService:
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
raise e raise e
@staticmethod
def get_latest_specs():
"""
获取所有规格型号的最大连号,按智能分组返回
返回格式: [{"group": "S", "latest": "S0115/S0115"}, {"group": "Opt4xxx", "latest": "Opt4018/Opt4018"}, ...]
"""
import re
# 1. 查询所有不为空的规格型号
specs = MaterialBase.query.filter(
MaterialBase.spec_model.isnot(None),
MaterialBase.spec_model != ''
).all()
# 2. 数据结构:{分组名: (原始规格, 数字部分)}
groups = {}
def parse_spec(spec_full):
"""
解析规格型号
返回: (prefix, num, group_name, original_spec)
"""
# 取斜杠前的部分作为基准
base_spec = spec_full.split('/')[0]
# 使用正则解析:字母前缀 + 数字
match = re.match(r'^([A-Za-z]+)(\d+)$', base_spec)
if not match:
return (base_spec, 0, base_spec, spec_full)
prefix, num_str = match.groups()
num = int(num_str)
# 智能分组逻辑
if prefix == 'Opt':
# Opt 按千位段分组
thousand = num // 1000
group = f"Opt{thousand}xxx"
else:
# 常规前缀按原值分组
group = prefix
return (prefix, num, group, spec_full)
# 3. 遍历并分组
for material in specs:
spec = material.spec_model
if not spec:
continue
prefix, num, group, original_spec = parse_spec(spec)
if group not in groups:
groups[group] = (original_spec, num)
else:
_, existing_num = groups[group]
if num > existing_num:
groups[group] = (original_spec, num)
# 4. 构建返回结果,按分组名排序
result = [
{"group": group, "latest": spec}
for group, (spec, _) in sorted(groups.items())
]
return result

View File

@ -70,3 +70,11 @@ export function batchSetInspection(data: { ids: number[], isInspectionRequired:
data data
}) })
} }
// 7. 获取智能分组规格最大连号
export function getLatestSpecs() {
return request({
url: '/inbound/base/spec-latest',
method: 'get'
})
}

View File

@ -0,0 +1,200 @@
<template>
<div class="spec-helper" :class="{ expanded }">
<!-- 触发按钮 -->
<div class="trigger-btn" @click="toggle">
<span class="arrow">{{ expanded ? '>' : '<' }}</span>
</div>
<!-- 面板内容 -->
<div class="panel">
<div class="panel-header">
<span class="title">规格连号助手</span>
<el-input
v-model="filterText"
placeholder="搜索分组或规格..."
clearable
size="small"
class="search-input"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</div>
<el-scrollbar class="data-list">
<div
v-for="item in filteredData"
:key="item.group"
class="data-item"
>
<span class="group-tag">{{ item.group }}</span>
<span class="latest-spec">{{ item.latest }}</span>
</div>
<el-empty v-if="filteredData.length === 0" description="暂无数据" :image-size="60" />
</el-scrollbar>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { Search } from '@element-plus/icons-vue'
import { getLatestSpecs } from '@/api/material_base'
import { ElMessage } from 'element-plus'
interface SpecItem {
group: string
latest: string
}
const expanded = ref(false)
const filterText = ref('')
const specData = ref<SpecItem[]([])
const toggle = () => {
expanded.value = !expanded.value
if (expanded.value && specData.value.length === 0) {
fetchData()
}
}
const fetchData = async () => {
try {
const res = await getLatestSpecs()
if (res.data.code === 200) {
specData.value = res.data.data || []
} else {
ElMessage.error(res.data.msg || '获取规格数据失败')
}
} catch (error) {
console.error('获取规格数据失败:', error)
ElMessage.error('获取规格数据失败')
}
}
const filteredData = computed(() => {
if (!filterText.value) {
return specData.value
}
const keyword = filterText.value.toLowerCase()
return specData.value.filter(
item =>
item.group.toLowerCase().includes(keyword) ||
item.latest.toLowerCase().includes(keyword)
)
})
onMounted(() => {
// 默认不加载,展开时再加载
})
</script>
<style scoped>
.spec-helper {
position: fixed;
right: 0;
top: 50%;
transform: translateY(-50%);
z-index: 9999;
display: flex;
align-items: center;
transition: transform 0.3s ease;
}
.spec-helper:not(.expanded) {
transform: translateY(-50%) translateX(calc(100% - 24px));
}
.trigger-btn {
width: 24px;
height: 60px;
background: #409eff;
color: white;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 4px 0 0 4px;
flex-shrink: 0;
}
.trigger-btn:hover {
background: #66b1ff;
}
.arrow {
font-size: 14px;
font-weight: bold;
}
.panel {
width: 280px;
height: 400px;
background: white;
border-radius: 4px 0 0 4px;
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
overflow: hidden;
}
.panel-header {
padding: 12px;
border-bottom: 1px solid #ebeef5;
display: flex;
flex-direction: column;
gap: 8px;
}
.title {
font-size: 14px;
font-weight: 600;
color: #303133;
}
.search-input {
width: 100%;
}
.data-list {
flex: 1;
overflow-y: auto;
}
.data-item {
padding: 8px 12px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #f5f7fa;
cursor: default;
}
.data-item:hover {
background: #f5f7fa;
}
.group-tag {
font-size: 12px;
font-weight: 500;
color: #409eff;
background: #ecf5ff;
padding: 2px 6px;
border-radius: 3px;
max-width: 80px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.latest-spec {
font-size: 13px;
color: #303133;
font-family: monospace;
max-width: 140px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>

View File

@ -5,12 +5,16 @@
<div class="main-container"> <div class="main-container">
<AppMain /> <AppMain />
</div> </div>
<!-- 全局规格连号助手 -->
<SpecHelper />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import Sidebar from './components/Sidebar/index.vue' import Sidebar from './components/Sidebar/index.vue'
import AppMain from './components/AppMain.vue' import AppMain from './components/AppMain.vue'
import SpecHelper from '@/components/SpecHelper/index.vue'
</script> </script>
<style scoped> <style scoped>