feat(material): add global floating helper to track latest specification codes with smart grouping
This commit is contained in:
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
@ -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'
|
||||||
|
})
|
||||||
|
}
|
||||||
200
inventory-web/src/components/SpecHelper/index.vue
Normal file
200
inventory-web/src/components/SpecHelper/index.vue
Normal 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>
|
||||||
@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user