|
|
@@ -0,0 +1,447 @@
|
|
|
+<template>
|
|
|
+ <div>
|
|
|
+ <layout>
|
|
|
+ <layout-header>
|
|
|
+ <div class="product-header">
|
|
|
+ <el-input
|
|
|
+ v-model="searchKeyword"
|
|
|
+ placeholder="搜索产品编号或名称"
|
|
|
+ clearable
|
|
|
+ style="width: 220px; margin: 5px"
|
|
|
+ @keyup.enter="onSearch"
|
|
|
+ @clear="onSearch"
|
|
|
+ />
|
|
|
+ <el-button type="primary" icon="Search" style="margin: 5px" @click="onSearch">查询</el-button>
|
|
|
+ </div>
|
|
|
+ </layout-header>
|
|
|
+
|
|
|
+ <layout>
|
|
|
+ <layout-sider :resize-directions="['right']" :width="240" style="margin-right: 10px">
|
|
|
+ <div class="product-menu-wrap">
|
|
|
+ <h3 class="product-menu-title">产品分类</h3>
|
|
|
+ <el-tree
|
|
|
+ ref="menuTreeRef"
|
|
|
+ :data="menuTreeData"
|
|
|
+ class="treecolor product-menu-tree"
|
|
|
+ :props="treeProps"
|
|
|
+ node-key="id"
|
|
|
+ default-expand-all
|
|
|
+ highlight-current
|
|
|
+ @node-click="handleNodeClick"
|
|
|
+ >
|
|
|
+ <template #default="{ data }">
|
|
|
+ <span class="tree-node-text">{{ data.label }}</span>
|
|
|
+ </template>
|
|
|
+ </el-tree>
|
|
|
+ </div>
|
|
|
+ </layout-sider>
|
|
|
+
|
|
|
+ <layout-content>
|
|
|
+ <el-main class="product-main">
|
|
|
+ <div class="gva-table-box product-split">
|
|
|
+ <div class="product-list-block">
|
|
|
+ <el-table
|
|
|
+ :data="tableData"
|
|
|
+ border
|
|
|
+ size="small"
|
|
|
+ style="width: 100%"
|
|
|
+ :height="340"
|
|
|
+ row-key="id"
|
|
|
+ :show-overflow-tooltip="true"
|
|
|
+ highlight-current-row
|
|
|
+ @row-click="onProductRowClick"
|
|
|
+ >
|
|
|
+ <el-table-column align="center" label="产品编号" prop="product_code" width="120" />
|
|
|
+ <el-table-column align="left" label="产品名称" prop="product_name" width="200" show-overflow-tooltip />
|
|
|
+ <el-table-column align="center" label="单位" prop="unit" width="72" />
|
|
|
+ <el-table-column align="center" label="产品类型" prop="product_type" width="100" />
|
|
|
+ <el-table-column align="center" label="操作人" prop="Sys_id" width="100" />
|
|
|
+ <el-table-column align="center" label="创建时间" prop="Sys_rq" width="160" />
|
|
|
+ <el-table-column align="center" label="修改时间" prop="mod_rq" width="160" />
|
|
|
+ </el-table>
|
|
|
+ <div class="gva-pagination">
|
|
|
+ <el-pagination
|
|
|
+ v-model:current-page="page"
|
|
|
+ v-model:page-size="pageSize"
|
|
|
+ :page-sizes="[10, 20, 50, 100]"
|
|
|
+ :total="total"
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
+ background
|
|
|
+ @current-change="fetchProductList"
|
|
|
+ @size-change="onPageSizeChange"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="product-detail-block">
|
|
|
+ <div v-if="!selectedRow" class="detail-empty">请在上方列表中点击一行产品,查看部件资料与工艺资料</div>
|
|
|
+ <el-tabs v-else v-model="detailTab" type="border-card" class="product-detail-tabs">
|
|
|
+ <el-tab-pane label="部件资料" name="part">
|
|
|
+ <el-table
|
|
|
+ :data="partList"
|
|
|
+ border
|
|
|
+ size="small"
|
|
|
+ max-height="260"
|
|
|
+ row-key="id"
|
|
|
+ :show-overflow-tooltip="true"
|
|
|
+ >
|
|
|
+ <el-table-column align="center" label="部件代号" prop="part_code" width="110" />
|
|
|
+ <el-table-column align="left" label="部件名称" prop="part_name" min-width="120" />
|
|
|
+ <el-table-column align="center" label="规格" prop="part_spec" width="100" />
|
|
|
+ <el-table-column align="center" label="数量" prop="qty" width="72" />
|
|
|
+ <el-table-column align="center" label="操作人" prop="Sys_id" width="100" />
|
|
|
+ <el-table-column align="center" label="创建时间" prop="Sys_rq" width="160" />
|
|
|
+ <el-table-column align="center" label="修改时间" prop="mod_rq" width="160" />
|
|
|
+ </el-table>
|
|
|
+ <div class="gva-pagination detail-pagination">
|
|
|
+ <el-pagination
|
|
|
+ v-model:current-page="partPage"
|
|
|
+ v-model:page-size="partPageSize"
|
|
|
+ :page-sizes="[10, 20, 30, 50]"
|
|
|
+ :total="partTotal"
|
|
|
+ small
|
|
|
+ background
|
|
|
+ layout="total, sizes, prev, pager, next"
|
|
|
+ @current-change="fetchPartList"
|
|
|
+ @size-change="onPartPageSizeChange"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </el-tab-pane>
|
|
|
+ <el-tab-pane label="工艺资料" name="gy">
|
|
|
+ <el-table
|
|
|
+ :data="gyList"
|
|
|
+ border
|
|
|
+ size="small"
|
|
|
+ max-height="260"
|
|
|
+ row-key="id"
|
|
|
+ :show-overflow-tooltip="true"
|
|
|
+ >
|
|
|
+ <el-table-column align="center" label="工艺代号" prop="gy_code" width="110" />
|
|
|
+ <el-table-column align="left" label="工艺名称" prop="gy_name" min-width="140" />
|
|
|
+ <el-table-column align="center" label="生产工序" prop="big_process" width="100" />
|
|
|
+ <el-table-column align="right" label="标准工时" prop="standard_hour" width="96" />
|
|
|
+ <el-table-column align="right" label="标准工分" prop="standard_score" width="96" />
|
|
|
+ <el-table-column align="center" label="排序" prop="sort" width="72" />
|
|
|
+ <el-table-column align="center" label="创建时间" prop="createtime" width="160" />
|
|
|
+ <el-table-column align="center" label="更新时间" prop="updatetime" width="160" />
|
|
|
+ </el-table>
|
|
|
+ <div class="gva-pagination detail-pagination">
|
|
|
+ <el-pagination
|
|
|
+ v-model:current-page="gyPage"
|
|
|
+ v-model:page-size="gyPageSize"
|
|
|
+ :page-sizes="[10, 20, 30, 50]"
|
|
|
+ :total="gyTotal"
|
|
|
+ small
|
|
|
+ background
|
|
|
+ layout="total, sizes, prev, pager, next"
|
|
|
+ @current-change="fetchGyList"
|
|
|
+ @size-change="onGyPageSizeChange"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </el-tab-pane>
|
|
|
+ </el-tabs>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-main>
|
|
|
+ </layout-content>
|
|
|
+ </layout>
|
|
|
+ </layout>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { Layout, LayoutSider, LayoutContent } from '@arco-design/web-vue'
|
|
|
+import { ref, computed, onMounted, nextTick } from 'vue'
|
|
|
+import { ProductTypeMenu, ProductList, ProductPartList, ProductGyList } from '@/api/yunyin/product'
|
|
|
+import { ElMessage } from 'element-plus'
|
|
|
+
|
|
|
+defineOptions({ name: 'ProductList' })
|
|
|
+
|
|
|
+const menuTreeRef = ref(null)
|
|
|
+const treeProps = { label: 'label', children: 'children' }
|
|
|
+const menuTreeData = ref([])
|
|
|
+const tableData = ref([])
|
|
|
+const total = ref(0)
|
|
|
+const page = ref(1)
|
|
|
+const pageSize = ref(30)
|
|
|
+const searchKeyword = ref('')
|
|
|
+
|
|
|
+/** 左侧选中:null 全部 | { type:'category', searchText } | { type:'product', searchText } —— 请求只带 search */
|
|
|
+const menuFilter = ref(null)
|
|
|
+
|
|
|
+const selectedRow = ref(null)
|
|
|
+const detailTab = ref('part')
|
|
|
+
|
|
|
+const partList = ref([])
|
|
|
+const partTotal = ref(0)
|
|
|
+const partPage = ref(1)
|
|
|
+const partPageSize = ref(30)
|
|
|
+
|
|
|
+const gyList = ref([])
|
|
|
+const gyTotal = ref(0)
|
|
|
+const gyPage = ref(1)
|
|
|
+const gyPageSize = ref(30)
|
|
|
+
|
|
|
+
|
|
|
+/** 接口 data: [{ name, list: [{ id, product_code, product_name }] }] */
|
|
|
+const buildMenuTree = (data) => {
|
|
|
+ const root = [
|
|
|
+ {
|
|
|
+ id: 'all-products',
|
|
|
+ label: '全部产品',
|
|
|
+ isAll: true,
|
|
|
+ },
|
|
|
+ ]
|
|
|
+ if (!Array.isArray(data)) return root
|
|
|
+ const cats = data.map((cat, idx) => ({
|
|
|
+ id: `cat-${idx}-${cat.name}`,
|
|
|
+ label: cat.name,
|
|
|
+ isCategory: true,
|
|
|
+ product_type: cat.name,
|
|
|
+ children: (cat.list || []).map((p) => ({
|
|
|
+ id: `prod-${p.id}`,
|
|
|
+ label: p.product_name,
|
|
|
+ isProduct: true,
|
|
|
+ product_name: p.product_name,
|
|
|
+ })),
|
|
|
+ }))
|
|
|
+ return [...root, ...cats]
|
|
|
+}
|
|
|
+
|
|
|
+const loadMenu = async () => {
|
|
|
+ try {
|
|
|
+ const res = await ProductTypeMenu()
|
|
|
+ if (res?.code !== 0) {
|
|
|
+ ElMessage.error(res?.msg || '获取产品分类失败')
|
|
|
+ menuTreeData.value = buildMenuTree([])
|
|
|
+ return
|
|
|
+ }
|
|
|
+ menuTreeData.value = buildMenuTree(res.data)
|
|
|
+ await nextTick()
|
|
|
+ menuTreeRef.value?.setCurrentKey('all-products')
|
|
|
+ } catch (e) {
|
|
|
+ console.error(e)
|
|
|
+ menuTreeData.value = buildMenuTree([])
|
|
|
+ ElMessage.error('获取产品分类失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const fetchProductList = async () => {
|
|
|
+ try {
|
|
|
+ const params = {
|
|
|
+ page: page.value,
|
|
|
+ limit: pageSize.value,
|
|
|
+ }
|
|
|
+ const kw = searchKeyword.value.trim()
|
|
|
+ const f = menuFilter.value
|
|
|
+
|
|
|
+ // 左侧与顶部筛选统一只传 search(不传 product_type)
|
|
|
+ if (f?.type === 'category' || f?.type === 'product') {
|
|
|
+ params.search = f.searchText ?? ''
|
|
|
+ } else if (kw) {
|
|
|
+ params.search = kw
|
|
|
+ }
|
|
|
+
|
|
|
+ const res = await ProductList(params)
|
|
|
+ if (res?.code !== 0) {
|
|
|
+ ElMessage.error(res?.msg || '获取产品列表失败')
|
|
|
+ tableData.value = []
|
|
|
+ total.value = 0
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const payload = res.data || {}
|
|
|
+ tableData.value = Array.isArray(payload.list) ? payload.list : []
|
|
|
+ const c = payload.count ?? payload.total
|
|
|
+ total.value = c != null ? Number(c) : tableData.value.length
|
|
|
+ } catch (e) {
|
|
|
+ console.error(e)
|
|
|
+ tableData.value = []
|
|
|
+ total.value = 0
|
|
|
+ ElMessage.error('获取产品列表失败')
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleNodeClick = (data) => {
|
|
|
+ if (data.isAll) {
|
|
|
+ menuFilter.value = null
|
|
|
+ } else if (data.isCategory) {
|
|
|
+ menuFilter.value = { type: 'category', searchText: data.product_type }
|
|
|
+ } else if (data.isProduct) {
|
|
|
+ menuFilter.value = {
|
|
|
+ type: 'product',
|
|
|
+ searchText: data.product_name ?? data.label,
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ menuFilter.value = null
|
|
|
+ }
|
|
|
+
|
|
|
+ page.value = 1
|
|
|
+ fetchProductList()
|
|
|
+}
|
|
|
+
|
|
|
+const onSearch = () => {
|
|
|
+ page.value = 1
|
|
|
+ fetchProductList()
|
|
|
+}
|
|
|
+
|
|
|
+const onPageSizeChange = () => {
|
|
|
+ page.value = 1
|
|
|
+ fetchProductList()
|
|
|
+}
|
|
|
+
|
|
|
+const fetchPartList = async () => {
|
|
|
+ const code = selectedRow.value?.product_code
|
|
|
+ if (!code) {
|
|
|
+ partList.value = []
|
|
|
+ partTotal.value = 0
|
|
|
+ return
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const res = await ProductPartList({
|
|
|
+ product_code: code,
|
|
|
+ page: partPage.value,
|
|
|
+ limit: partPageSize.value,
|
|
|
+ })
|
|
|
+ if (res?.code !== 0) {
|
|
|
+ partList.value = []
|
|
|
+ partTotal.value = 0
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const payload = res.data || {}
|
|
|
+ partList.value = Array.isArray(payload.list) ? payload.list : []
|
|
|
+ const c = payload.count ?? payload.total
|
|
|
+ partTotal.value = c != null ? Number(c) : partList.value.length
|
|
|
+ } catch (e) {
|
|
|
+ console.error(e)
|
|
|
+ partList.value = []
|
|
|
+ partTotal.value = 0
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const fetchGyList = async () => {
|
|
|
+ const code = selectedRow.value?.product_code
|
|
|
+ if (!code) {
|
|
|
+ gyList.value = []
|
|
|
+ gyTotal.value = 0
|
|
|
+ return
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ const res = await ProductGyList({
|
|
|
+ product_code: code,
|
|
|
+ page: gyPage.value,
|
|
|
+ limit: gyPageSize.value,
|
|
|
+ })
|
|
|
+ if (res?.code !== 0) {
|
|
|
+ gyList.value = []
|
|
|
+ gyTotal.value = 0
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const payload = res.data || {}
|
|
|
+ gyList.value = Array.isArray(payload.list) ? payload.list : []
|
|
|
+ const c = payload.count ?? payload.total
|
|
|
+ gyTotal.value = c != null ? Number(c) : gyList.value.length
|
|
|
+ } catch (e) {
|
|
|
+ console.error(e)
|
|
|
+ gyList.value = []
|
|
|
+ gyTotal.value = 0
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const onProductRowClick = (row) => {
|
|
|
+ selectedRow.value = row
|
|
|
+ partPage.value = 1
|
|
|
+ gyPage.value = 1
|
|
|
+ detailTab.value = 'part'
|
|
|
+ fetchPartList()
|
|
|
+ fetchGyList()
|
|
|
+}
|
|
|
+
|
|
|
+const onPartPageSizeChange = () => {
|
|
|
+ partPage.value = 1
|
|
|
+ fetchPartList()
|
|
|
+}
|
|
|
+
|
|
|
+const onGyPageSizeChange = () => {
|
|
|
+ gyPage.value = 1
|
|
|
+ fetchGyList()
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(async () => {
|
|
|
+ await loadMenu()
|
|
|
+ menuFilter.value = null
|
|
|
+ await fetchProductList()
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.product-header {
|
|
|
+ padding: 4px 0;
|
|
|
+}
|
|
|
+.product-menu-wrap {
|
|
|
+ background: #fff;
|
|
|
+ padding: 10px;
|
|
|
+ min-height: 260px;
|
|
|
+ max-height: calc(100vh - 140px);
|
|
|
+ overflow: auto;
|
|
|
+}
|
|
|
+.product-menu-title {
|
|
|
+ font-size: 15px;
|
|
|
+ font-weight: 700;
|
|
|
+ margin: 0 0 10px;
|
|
|
+}
|
|
|
+.tree-node-text {
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+/* 自定义插槽时无 .el-tree-node__label,用 is-current + .tree-node-text;勿用 DOM 改色 */
|
|
|
+.product-menu-wrap :deep(.product-menu-tree.el-tree .el-tree-node.is-current > .el-tree-node__content) {
|
|
|
+ color: red;
|
|
|
+}
|
|
|
+.product-menu-wrap :deep(.product-menu-tree.el-tree .el-tree-node.is-current > .el-tree-node__content .tree-node-text) {
|
|
|
+ color: red;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+.filter-hint {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #606266;
|
|
|
+ margin-bottom: 10px;
|
|
|
+}
|
|
|
+.gva-pagination {
|
|
|
+ margin-top: 12px;
|
|
|
+}
|
|
|
+.product-main {
|
|
|
+ padding-bottom: 8px;
|
|
|
+}
|
|
|
+.product-split {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+ min-height: 0;
|
|
|
+}
|
|
|
+.product-list-block {
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+.product-detail-block {
|
|
|
+ flex: 1;
|
|
|
+ min-height: 200px;
|
|
|
+}
|
|
|
+.detail-empty {
|
|
|
+ padding: 24px;
|
|
|
+ text-align: center;
|
|
|
+ color: #909399;
|
|
|
+ font-size: 14px;
|
|
|
+ background: #fafafa;
|
|
|
+ border: 1px dashed #dcdfe6;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+.product-detail-tabs {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+.product-detail-tabs :deep(.el-tabs__content) {
|
|
|
+ padding-top: 8px;
|
|
|
+}
|
|
|
+.detail-pagination {
|
|
|
+ margin-top: 10px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+}
|
|
|
+</style>
|