2 Комити 012d448732 ... 3f7311d70c

Аутор SHA1 Порука Датум
  zhuangyunsheng 3f7311d70c 计划 пре 4 недеља
  zhuangyunsheng 0f53c734f8 计划 пре 4 недеља
73 измењених фајлова са 2274 додато и 341 уклоњено
  1. 3 2
      .env.development
  2. 27 0
      src/api/model/basic.js
  3. 32 0
      src/api/model/equip.js
  4. 5 0
      src/api/model/process.js
  5. 36 0
      src/api/model/production.js
  6. 2 0
      src/components/scFormTable/detail.vue
  7. 13 6
      src/components/scFormTable/index.vue
  8. 3 3
      src/components/scTable/helper.js
  9. 20 22
      src/components/scTable/index.vue
  10. 8 4
      src/components/scTable/renderer/form-select.vue
  11. 1 1
      src/config/route.js
  12. 5 5
      src/config/select.js
  13. 1 1
      src/config/table.js
  14. 5 7
      src/directives/auth.js
  15. 7 13
      src/directives/auths.js
  16. 5 8
      src/directives/authsAll.js
  17. 10 12
      src/directives/copy.js
  18. 9 16
      src/directives/role.js
  19. 14 14
      src/directives/time.js
  20. 1 1
      src/locales/index.js
  21. 18 18
      src/main.js
  22. 2 2
      src/router/systemRouter.js
  23. 28 28
      src/scui.js
  24. 3 3
      src/store/index.js
  25. 1 1
      src/store/modules/global.js
  26. 3 3
      src/store/modules/iframe.js
  27. 1 1
      src/store/modules/keepAlive.js
  28. 4 3
      src/store/modules/tenant.js
  29. 3 1
      src/style/app.scss
  30. 25 2
      src/utils/basicDic.js
  31. 1 1
      src/utils/color.js
  32. 4 6
      src/views/basic/customer/detail.vue
  33. 5 5
      src/views/basic/customer/index.vue
  34. 1 1
      src/views/basic/material/index.vue
  35. 12 9
      src/views/basic/qualityPlan/desc.vue
  36. 2 4
      src/views/basic/qualityPlan/detail.vue
  37. 1 1
      src/views/basic/qualityPlan/index.vue
  38. 204 0
      src/views/basic/supplier/detail.vue
  39. 159 0
      src/views/basic/supplier/index.vue
  40. 84 0
      src/views/basic/warehouse/detail.vue
  41. 85 0
      src/views/basic/warehouse/index.vue
  42. 89 0
      src/views/equip/device/detail.vue
  43. 86 0
      src/views/equip/device/index.vue
  44. 1 1
      src/views/login/index.vue
  45. 8 5
      src/views/process/line/desc.vue
  46. 23 12
      src/views/process/line/detail.vue
  47. 13 3
      src/views/process/line/index.vue
  48. 12 7
      src/views/process/line/main.js
  49. 1 3
      src/views/process/stage/detail.vue
  50. 1 1
      src/views/process/stage/index.vue
  51. 12 9
      src/views/production/bom/desc.vue
  52. 29 8
      src/views/production/bom/detail.vue
  53. 56 16
      src/views/production/bom/index.vue
  54. 9 7
      src/views/production/bom/main.js
  55. 56 0
      src/views/production/order/index.vue
  56. 104 0
      src/views/production/plan/desc.vue
  57. 186 0
      src/views/production/plan/detail.vue
  58. 131 0
      src/views/production/plan/index.vue
  59. 34 0
      src/views/production/plan/main copy.js
  60. 28 0
      src/views/production/plan/main.js
  61. 236 0
      src/views/production/prePlan/detail.vue
  62. 45 0
      src/views/production/prePlan/drawer.vue
  63. 92 0
      src/views/production/prePlan/index.vue
  64. 103 0
      src/views/production/prePlan/main.js
  65. 17 14
      src/views/sales/order/desc.vue
  66. 2 4
      src/views/sales/order/detail.vue
  67. 2 2
      src/views/sales/order/index.vue
  68. 3 3
      src/views/sales/order/main.js
  69. 19 19
      src/views/sales/performance/main.js
  70. 1 1
      src/views/sales/plan/index.vue
  71. 1 1
      src/views/system/dept/index.vue
  72. 1 1
      src/views/system/tenant/index.vue
  73. 20 20
      src/vxeTable.js

+ 3 - 2
.env.development

@@ -4,8 +4,9 @@ NODE_ENV = development
 # 接口地址
 VUE_APP_ICONIFY_BASEURL = https://api.iconify.design
 VUE_APP_ZEROAPI_BASEURL = http://www.qdeasydo.com
-# VUE_APP_MES_BASEURL = http://192.168.101.93:10160
-VUE_APP_MES_BASEURL = http://192.168.101.135:10160
+# VUE_APP_MES_BASEURL = http://www.qdeasydo.com/mes
+VUE_APP_MES_BASEURL = http://192.168.101.230:10160
+# VUE_APP_MES_BASEURL = http://192.168.101.135:10160
 
 # 本地端口
 VUE_APP_PORT = 4400

+ 27 - 0
src/api/model/basic.js

@@ -25,6 +25,33 @@ export default {
         }
     },
 
+    warehouse: {
+        name: "仓库管理",
+        url: "/mes/warehouse",
+        
+        get: async function (data = {}) {
+            if (store.state.tenant.tenantId !== "0") data.tenantId = store.state.tenant.tenantId
+            return await http.post(`${this.url}/getPage`, data)
+        },
+
+        all: async function (data = {}) {
+            if (store.state.tenant.tenantId !== "0") data.tenantId = store.state.tenant.tenantId
+            return await http.post(`${this.url}/getList`, data)
+        },
+
+        add: async function (data = {}) {
+            return await http.post(`${this.url}/save`, data)
+        },
+
+        edit: async function (data = {}) {
+            return await http.post(`${this.url}/update`, data)
+        },
+
+        del: async function (data = {}) {
+            return await http.post(`${this.url}/remove`, data)
+        }
+    },
+
     qualityPlan: {
         name: "质检方案",
         url: "/mes/qualityInspectProgram",

+ 32 - 0
src/api/model/equip.js

@@ -0,0 +1,32 @@
+import store from "@/store"
+import config from "@/config"
+import http from "@/utils/request"
+
+export default {
+    device: {
+        name: "设备列表",
+        url: "/mes/device",
+        
+        get: async function (data = {}) {
+            if (store.state.tenant.tenantId !== "0") data.tenantId = store.state.tenant.tenantId
+            return await http.post(`${this.url}/getPage`, data)
+        },
+
+        all: async function (data = {}) {
+            if (store.state.tenant.tenantId !== "0") data.tenantId = store.state.tenant.tenantId
+            return await http.post(`${this.url}/getList`, data)
+        },
+
+        add: async function (data = {}) {
+            return await http.post(`${this.url}/save`, data)
+        },
+
+        edit: async function (data = {}) {
+            return await http.post(`${this.url}/update`, data)
+        },
+
+        del: async function (data = {}) {
+            return await http.post(`${this.url}/remove`, data)
+        }
+    },
+}

+ 5 - 0
src/api/model/process.js

@@ -34,6 +34,11 @@ export default {
 			return await http.post(`${this.url}/getPage`, data)
 		},
 
+        all: async function (data = {}) {
+            if (store.state.tenant.tenantId !== "0") data.tenantId = store.state.tenant.tenantId
+			return await http.post(`${this.url}/getList`, data)
+		},
+
         add: async function (data = {}) {
             return await http.post(`${this.url}/save`, data)
         },

+ 36 - 0
src/api/model/production.js

@@ -25,6 +25,42 @@ export default {
             return await http.post(`${this.url}/update`, data)
         },
 
+        del: async function (data = {}) {
+            return await http.post(`${this.url}/remove`, data)
+        }
+    },
+
+    prePlan: {
+        name: "预生产计划",
+        url: "/mes/productPrePlan",
+        
+        get: async function (data = {}) {
+            if (store.state.tenant.tenantId !== "0") data.tenantId = store.state.tenant.tenantId
+            return await http.post(`${this.url}/getPage`, data)
+        },
+
+        allocate: async function (data = {}) {
+            return await http.post(`${this.url}/save`, data)
+        }
+    },
+
+    plan: {
+        name: "生产计划",
+        url: "/mes/productPlan",
+        
+        get: async function (data = {}) {
+            if (store.state.tenant.tenantId !== "0") data.tenantId = store.state.tenant.tenantId
+            return await http.post(`${this.url}/getPage`, data)
+        },
+
+        add: async function (data = {}) {
+            return await http.post(`${this.url}/save`, data)
+        },
+
+        edit: async function (data = {}) {
+            return await http.post(`${this.url}/update`, data)
+        },
+
         del: async function (data = {}) {
             return await http.post(`${this.url}/remove`, data)
         }

+ 2 - 0
src/components/scFormTable/detail.vue

@@ -13,6 +13,7 @@
 import XEUtils from "xe-utils";
 import stageTable from "@/views/process/stage/index";
 import materialTable from "@/views/basic/material/index";
+import bomTable from "@/views/production/bom/index";
 import customerTable from "@/views/basic/customer/index";
 
 const $emit = defineEmits(["success", "closed"]);
@@ -26,6 +27,7 @@ const props = defineProps({
 const compDic = reactive({
     stage: { title: "工序选择", compName: stageTable },
     material: { title: "产品选择", compName: materialTable },
+    bom: { title: "产品选择", compName: bomTable },
     customer: { title: "客户选择", compName: customerTable },
 });
 

+ 13 - 6
src/components/scFormTable/index.vue

@@ -6,7 +6,7 @@
 
 <template>
     <el-main class="sc-form-table">
-        <vxe-grid ref="xGrid" v-bind="gridOptions">
+        <vxe-grid ref="xGrid" v-bind="gridOptions" @edit-activated="$emit('editActivated', $event)">
             <template #empty>
                 <el-empty :image-size="100" description="您还没有添加任何数据"></el-empty>
             </template>
@@ -15,7 +15,7 @@
             </template>
 
             <template #top>
-                <vxe-button v-if="!disabled" style="width: 140px;margin-bottom: 15px;" status="primary" :content="titleMap[tableKey]" @click="table_add"></vxe-button>
+                <vxe-button v-if="tableKey && !disabled" style="width: 140px;margin-bottom: 15px;" status="primary" :content="titleMap[tableKey]" @click="table_add"></vxe-button>
             </template>
 
             <template #seq_add>
@@ -50,17 +50,20 @@ import XEUtils from "xe-utils";
 import scUploadFile from "@/components/scUpload/file";
 import selectTable from "@/components/scFormTable/detail";
 
-const $emit = defineEmits(["update:modelValue", "success"]);
+const $emit = defineEmits(["update:modelValue"]);
 const props = defineProps({
     modelValue: { type: Array, default: () => [] },
     disabled: { type: Boolean, default: false },
     tableKey: { type: String, default: "" },
     addTemplate: { type: Object, default: () => ({}) },
     
+    minHeight: { type: [String, Number], default: 108 },
     layouts: { type: Array, default: () => [["Top", "Form"], ["Toolbar", "Table", "Bottom", "Pager"]] },
     rowKey: { type: String, default: "id" },
     columns: { type: Array, default: () => [] },
+    treeConfig: { type: Object, default: null },
     editRules: { type: Object, default: () => ({}) },
+    editDiasbled: { type: Object, default: () => ({}) }, // 每个field是否禁用方法
     footerField: { type: Array, default: () => [] },
     footerTitle: { type: Array, default: () => [] },
     footerMethod: { type: Array, default: () => [] },
@@ -72,11 +75,13 @@ const props = defineProps({
 
 const titleMap = reactive({
     stage: "工序选择",
-    material: "产品选择"
+    material: "产品选择",
+    bom: "产品选择"
 });
 
 const gridOptions = reactive({
     id: "xGride-form-table",
+    minHeight: props.minHeight,
     maxHeight: 1048,
     border: "full",
     size: "mini",
@@ -90,9 +95,10 @@ const gridOptions = reactive({
     toolbarConfig: { enabled: false },
     formConfig: { enabled: false },
     pagerConfig: { enabled: false },
-    rowConfig: { keyField: props.rowKey, drag: !props.disabled, resizable: true, useKey: true, isHover: true },
+    treeConfig: props.treeConfig,
+    rowConfig: { keyField: props.rowKey, drag: !props.treeConfig && !props.disabled, resizable: true, useKey: true, isHover: true },
     editRules: props.editRules,
-    editConfig: { enabled: !props.disabled, mode: "cell", trigger: "click", showStatus: false, showIcon: false },
+    editConfig: { mode: "cell", trigger: "click", showStatus: false, showIcon: false, beforeEditMethod: ({ row, column }) => !props.disabled && !row.disabled && (!XEUtils.get(props.editDiasbled, column.field) || !XEUtils.get(props.editDiasbled, column.field)(row)) },
     columnConfig: { useKey: true, resizable: true },
     resizableConfig: { isAllRowDrag: !props.disabled },
     tooltipConfig: { enterable: true },
@@ -183,4 +189,5 @@ defineExpose({
 
 <style scoped>
 .sc-form-table.el-main {padding: 0;background: var(--el-bg-color);}
+.sc-form-table.el-main .vxe-grid :deep(.vxe-table--body-inner-wrapper) {min-height: 36px!important;}
 </style>

+ 3 - 3
src/components/scTable/helper.js

@@ -1,5 +1,5 @@
-import XEUtils from "xe-utils";
-import store from "@/store";
+import XEUtils from "xe-utils"
+import store from "@/store"
 
 /**
  * 输入框配置
@@ -87,7 +87,7 @@ export const mapFormItemDatePicker = (field, title, config = {}) => ({
  * @param config 其他配置
  */
 export const mapFormItemTenant = (config = {}) => ({
-    visible: computed(() => store.state.tenant.tenantId === "0").value,
+    visible: computed(() => store.state.tenant.tenantId === "0"),
     field: "tenantId",
     title: "所属租户",
     titlePrefix: { content: "所属租户", icon: "vxe-icon-question-circle-fill" },

+ 20 - 22
src/components/scTable/index.vue

@@ -6,7 +6,7 @@
 
 <template>
     <el-main>
-        <vxe-grid ref="xGrid" v-bind="gridOptions" @form-collapse="formCollapseEvent" @edit-activated="$emit('editActivated', $event)" @page-change="pageChangeEvent">
+        <vxe-grid ref="xGrid" v-bind="gridOptions" v-on="gridEvents">
             <template #queryAction>
                 <el-button type="primary" auto-insert-space @click="searchData">查询</el-button>
                 <el-button auto-insert-space @click="resetData">重置</el-button>
@@ -31,6 +31,7 @@ import store from "@/store";
 import config from "@/config/table";
 import pagerBatchDel from "./renderer/pager-batch-del";
 
+const $emit = defineEmits(["tableSearch", "tableReset", "editActivated"]);
 const props = defineProps({
     apiObj: { type: Object, default: () => ({}) },
     apiKey: { type: String, default: () => "get" },
@@ -262,15 +263,11 @@ const getAllData = () => {
     });
 }
 
-const pageChangeEvent = ({ pageSize, currentPage }) => {
-    gridOptions.pagerConfig.currentPage = currentPage;
-    gridOptions.pagerConfig.pageSize = pageSize;
-    getData();
-}
-
 const searchData = (mode = "add") => {
     if (mode == "add") gridOptions.pagerConfig.currentPage = 1;
     gridOptions.pagerConfig.pageSize = config.pageSize;
+
+    $emit("tableSearch", config.queryData(gridOptions, props.paramsColums));
     getData();
 }
 
@@ -278,7 +275,8 @@ const resetData = () => {
     gridOptions.pagerConfig.currentPage = 1;
     gridOptions.pagerConfig.pageSize = config.pageSize;
     xGrid.value.resetForm();
-    
+
+    $emit("tableReset", config.queryData(gridOptions, props.paramsColums));
     getData();
 }
 
@@ -296,16 +294,6 @@ const getSelectRows = () => {
     return [];
 }
 
-const formCollapseEvent = ({ collapse }) => collapseStatus.value = collapse;
-
-const toggleTableExpand = () => xGrid.value.getTreeExpandRecords().length && xGrid.value.clearTreeExpand() || xGrid.value.setAllTreeExpand(true);
-
-const reloadColumn = columns => xGrid.value.reloadColumn(columns);
-
-const getTableData = () => xGrid.value.getTableData();
-
-const reloadData = data => xGrid.value.reloadData(data);
-
 const table_batch_del = () => {
     const deleteIdList = XEUtils.map(getSelectRows(), item => item.id);
     ElMessageBox.confirm("是否确认删除所有已选数据(不局限本页)?", "删除警告", {
@@ -320,12 +308,22 @@ const table_batch_del = () => {
     }).catch(() => {});
 }
 
+const gridEvents = {
+    formCollapse: ({ collapse }) => collapseStatus.value = collapse,
+    editActivated: params => $emit("editActivated", params),
+    pageChange: ({ pageSize, currentPage }) => {
+        gridOptions.pagerConfig.currentPage = currentPage;
+        gridOptions.pagerConfig.pageSize = pageSize;
+        getData();
+    }
+}
+
 defineExpose({
     getSelectRows,
-    toggleTableExpand,
-    reloadColumn,
-    getTableData,
-    reloadData,
+    toggleTableExpand: () => xGrid.value.getTreeExpandRecords().length && xGrid.value.clearTreeExpand() || xGrid.value.setAllTreeExpand(true),
+    reloadColumn: columns => xGrid.value.reloadColumn(columns),
+    getTableData: () => xGrid.value.getTableData(),
+    reloadData: data => xGrid.value.reloadData(data),
     searchData,
     resetData
 })

+ 8 - 4
src/components/scTable/renderer/form-select.vue

@@ -1,6 +1,6 @@
 <template>
     <el-select v-if="renderOpts.props.type == 'select'" :loading="loading" v-model="modelValue" v-bind="renderOpts.props" @focus="refreshOptions" @change="compChange">
-        <el-option v-for="(item, index) in options" :key="index" :label="XEUtils.get(item, optionProps.label, item)" :value="XEUtils.get(item, optionProps.value, index)"></el-option>
+        <el-option v-for="(item, index) in formatOptions" :key="index" :label="XEUtils.get(item, optionProps.label, item)" :value="XEUtils.get(item, optionProps.value, index)"></el-option>
     </el-select>
     
     <el-tree-select v-if="renderOpts.props.type == 'treeSelect'" :loading="loading" v-model="modelValue" :nodeKey="optionProps.value" :data="options" :props="optionProps" checkStrictly v-bind="renderOpts.props" @focus="refreshOptions" @change="compChange"></el-tree-select>
@@ -20,15 +20,19 @@ const loading = ref(false);
 const modelValue = ref(XEUtils.get(props.params.data, props.params.field, null));
 const options = ref(props.renderOpts.options || []);
 const optionProps = reactive(props.renderOpts.optionProps || config.props);
+const formatOptions = ref(props.renderOpts.options || []);
 
-watch(() => props.params.data, value => {
-    if (modelValue.value != XEUtils.get(value, props.params.field, null)) modelValue.value = XEUtils.get(value, props.params.field, null);
-}, { deep: true });
+watch(() => XEUtils.get(props.params.data, props.params.field), (newVal, oldVal) => newVal !== oldVal && (modelValue.value = newVal));
+watch(() => XEUtils.get(props.params.data, "tenantId"), (newVal, oldVal) => {
+    if (props.params.field !== "tenantId" && props.renderOpts.api && newVal) formatOptions.value = XEUtils.filter(options.value, item => item.tenantId == newVal);
+    else formatOptions.value = options.value;
+});
 
 const getRemoteData = async () => {
     if (props.renderOpts.api) {
         loading.value = true;
         options.value = await config.queryData(props.renderOpts.api);
+        formatOptions.value = options.value;
         loading.value = false;
     }
 }

+ 1 - 1
src/config/route.js

@@ -19,4 +19,4 @@ const routes = [
     }
 ]
 
-export default routes;
+export default routes

+ 5 - 5
src/config/select.js

@@ -1,14 +1,14 @@
 // 选择器配置
-import API from "@/api";
-import config from "@/config/table";
-import XEUtils from "xe-utils";
+import XEUtils from "xe-utils"
+import API from "@/api"
+import config from "@/config/table"
 
 export default {
     queryData: function ({ key = "", objKey = "all", query = {} }) {
         return new Promise(resolve => {
             if (!key || !XEUtils.has(XEUtils.get(API, key), objKey)) resolve([])
-
-            XEUtils.get(API, key)[objKey](query).then(res => {
+            
+                XEUtils.get(API, key)[objKey](query).then(res => {
                 const response = config.parseData(res)
                 resolve(response.data)
             }).catch(() => resolve([]))

+ 1 - 1
src/config/table.js

@@ -1,5 +1,5 @@
 // 数据表格配置
-const XEUtils = require("xe-utils");
+const XEUtils = require("xe-utils")
 
 export default {
 	pageSize: 20,													                                            // 表格每一页条数

+ 5 - 7
src/directives/auth.js

@@ -1,5 +1,5 @@
-import { permissionAll } from '@/utils/permission'
-import tool from '@/utils/tool';
+import { permissionAll } from "@/utils/permission"
+import tool from "@/utils/tool"
 
 /**
  * 用户权限指令
@@ -9,10 +9,8 @@ import tool from '@/utils/tool';
  */
 export default {
 	mounted (el, binding) {
-		if(permissionAll()){
-			return
-		}
-		let permissions = tool.data.get("PERMISSIONS");
-		if (!permissions.some((v) => v === binding.value)) el.parentNode.removeChild(el);
+		if (permissionAll()) return
+
+		!tool.data.get("PERMISSIONS").some(v => v === binding.value) && el.parentNode.removeChild(el)
 	}
 }

+ 7 - 13
src/directives/auths.js

@@ -1,5 +1,5 @@
-import { permissionAll } from '@/utils/permission'
-import tool from '@/utils/tool';
+import { permissionAll } from "@/utils/permission"
+import tool from "@/utils/tool"
 
 /**
  * 用户权限指令
@@ -9,16 +9,10 @@ import tool from '@/utils/tool';
  */
 export default {
 	mounted (el, binding) {
-		if(permissionAll()){
-			return
-		}
-		let permissions = tool.data.get("PERMISSIONS");
-		let flag = false;
-		permissions.map((val) => {
-			binding.value.map((v) => {
-				if (val === v) flag = true;
-			});
-		});
-		if (!flag) el.parentNode.removeChild(el);
+		if (permissionAll()) return
+		
+		let flag = false
+		tool.data.get("PERMISSIONS").forEach(val => binding.value.map(v => val === v && (flag = true)))
+		!flag && el.parentNode.removeChild(el)
 	}
 }

+ 5 - 8
src/directives/authsAll.js

@@ -1,5 +1,5 @@
-import { permissionAll, judementSameArr } from '@/utils/permission'
-import tool from '@/utils/tool';
+import { permissionAll, judementSameArr } from "@/utils/permission"
+import tool from "@/utils/tool"
 
 /**
  * 用户权限指令
@@ -9,11 +9,8 @@ import tool from '@/utils/tool';
  */
 export default {
 	mounted (el, binding) {
-		if(permissionAll()){
-			return
-		}
-		let permissions = tool.data.get("PERMISSIONS");
-		const flag = judementSameArr(binding.value, permissions);
-		if (!flag) el.parentNode.removeChild(el);
+		if (permissionAll()) return
+
+		!judementSameArr(binding.value, tool.data.get("PERMISSIONS")) && el.parentNode.removeChild(el)
 	}
 }

+ 10 - 12
src/directives/copy.js

@@ -2,26 +2,24 @@ export default {
 	mounted(el, binding) {
 		el.$value = binding.value
 		el.handler = () => {
-			const textarea = document.createElement('textarea')
-			textarea.readOnly = 'readonly'
-			textarea.style.position = 'absolute'
-			textarea.style.left = '-9999px'
+			const textarea = document.createElement("textarea")
+			textarea.readOnly = "readonly"
+			textarea.style.position = "absolute"
+			textarea.style.left = "-9999px"
 			textarea.value = el.$value
 			document.body.appendChild(textarea)
 			textarea.select()
 			textarea.setSelectionRange(0, textarea.value.length)
-			const result = document.execCommand('Copy')
-			if (result) {
-				ElMessage.success("复制成功")
-			}
+			const result = document.execCommand("Copy")
+			if (result) ElMessage.success("复制成功")
 			document.body.removeChild(textarea)
 		}
-		el.addEventListener('click', el.handler)
+		el.addEventListener("click", el.handler)
 	},
-	updated(el, binding){
+	updated(el, binding) {
 		el.$value = binding.value
 	},
-	unmounted(el){
-		el.removeEventListener('click', el.handler)
+	unmounted(el) {
+		el.removeEventListener("click", el.handler)
 	}
 }

+ 9 - 16
src/directives/role.js

@@ -1,22 +1,15 @@
-import { rolePermission } from '@/utils/permission'
+import { rolePermission } from "@/utils/permission"
 
 export default {
 	mounted(el, binding) {
 		const { value } = binding
-		if(Array.isArray(value)){
-			let ishas = false;
-			value.forEach(item => {
-				if(rolePermission(item)){
-					ishas = true;
-				}
-			})
-			if (!ishas){
-				el.parentNode.removeChild(el)
-			}
-		}else{
-			if(!rolePermission(value)){
-				el.parentNode.removeChild(el);
-			}
+		if (Array.isArray(value)) {
+			let ishas = false
+            
+			value.forEach(item => rolePermission(item) && (ishas = true))
+            !ishas && el.parentNode.removeChild(el)
+		} else {
+			!rolePermission(value) && el.parentNode.removeChild(el)
 		}
 	}
-};
+}

+ 14 - 14
src/directives/time.js

@@ -1,29 +1,29 @@
-import moment from "moment";
-import tool from "@/utils/tool";
+import moment from "moment"
+import tool from "@/utils/tool"
 
 let Time = {
 	getFormateTime: function (date) {
-		let timestamp = moment(date).valueOf();
-		let now = moment().valueOf();
-		let today = moment().startOf("day").valueOf();
+		let timestamp = moment(date).valueOf()
+		let now = moment().valueOf()
+		let today = moment().startOf("day").valueOf()
 
-		let timer = (now - timestamp) / 1000;
-		let tip = "";
+		let timer = (now - timestamp) / 1000
+		let tip = ""
 
 		if (timer <= 0) {
-			tip = "刚刚";
+			tip = "刚刚"
 		} else if (Math.floor(timer / 60) <= 0) {
-			tip = "刚刚";
+			tip = "刚刚"
 		} else if (timer < 3600) {
-			tip = Math.floor(timer / 60) + "分钟前";
+			tip = Math.floor(timer / 60) + "分钟前"
 		} else if (timer >= 3600 && (timestamp - today >= 0)) {
-			tip = Math.floor(timer / 3600) + "小时前";
+			tip = Math.floor(timer / 3600) + "小时前"
 		} else if (timer / 86400 <= 31) {
-			tip = Math.ceil(timer / 86400) + "天前";
+			tip = Math.ceil(timer / 86400) + "天前"
 		} else {
-			tip = tool.dateFormat(date, "YYYY-MM-DD");
+			tip = tool.dateFormat(date, "YYYY-MM-DD")
 		}
-		return tip;
+		return tip
 	}
 }
 

+ 1 - 1
src/locales/index.js

@@ -25,4 +25,4 @@ const i18n = createI18n({
 	messages
 })
 
-export default i18n;
+export default i18n

+ 18 - 18
src/main.js

@@ -1,23 +1,23 @@
-import ElementPlus from "element-plus";
-import "element-plus/dist/index.css";
-import "element-plus/theme-chalk/display.css";
-import "element-plus/theme-chalk/dark/css-vars.css";
+import ElementPlus from "element-plus"
+import "element-plus/dist/index.css"
+import "element-plus/theme-chalk/display.css"
+import "element-plus/theme-chalk/dark/css-vars.css"
 
-import vxeTable from "./vxeTable";
-import scui from "./scui";
-import i18n from "./locales";
-import router from "./router";
-import store from "./store";
-import App from "./App.vue";
+import vxeTable from "./vxeTable"
+import scui from "./scui"
+import i18n from "./locales"
+import router from "./router"
+import store from "./store"
+import App from "./App.vue"
 
-const app = createApp(App);
+const app = createApp(App)
 
-app.use(store);
-app.use(router);
-app.use(ElementPlus);
-app.use(i18n);
-app.use(scui);
-app.use(vxeTable);
+app.use(store)
+app.use(router)
+app.use(ElementPlus)
+app.use(i18n)
+app.use(scui)
+app.use(vxeTable)
 
 //挂载app
-app.mount("#app");
+app.mount("#app")

+ 2 - 2
src/router/systemRouter.js

@@ -1,4 +1,4 @@
-import config from "@/config";
+import config from "@/config"
 
 //系统路由
 const routes = [
@@ -16,4 +16,4 @@ const routes = [
 	}
 ]
 
-export default routes;
+export default routes

+ 28 - 28
src/scui.js

@@ -1,37 +1,37 @@
-import config from "./config";
-import api from "./api";
-import tool from "./utils/tool";
-import http from "./utils/request";
-import { permission, rolePermission } from "./utils/permission";
+import config from "./config"
+import api from "./api"
+import tool from "./utils/tool"
+import http from "./utils/request"
+import { permission, rolePermission } from "./utils/permission"
 
-import auth from "./directives/auth";
-import auths from "./directives/auths";
-import authsAll from "./directives/authsAll";
-import role from "./directives/role";
-import time from "./directives/time";
-import copy from "./directives/copy";
-import errorHandler from "./utils/errorHandler";
+import auth from "./directives/auth"
+import auths from "./directives/auths"
+import authsAll from "./directives/authsAll"
+import role from "./directives/role"
+import time from "./directives/time"
+import copy from "./directives/copy"
+import errorHandler from "./utils/errorHandler"
 
-import * as elIcons from "@element-plus/icons-vue";
-import * as scIcons from "./assets/icons";
+import * as elIcons from "@element-plus/icons-vue"
+import * as scIcons from "./assets/icons"
 
 export default {
 	install(app) {
 		//挂载全局对象
-		app.config.globalProperties.$CONFIG = config;
-		app.config.globalProperties.$TOOL = tool;
-		app.config.globalProperties.$HTTP = http;
-		app.config.globalProperties.$API = api;
-		app.config.globalProperties.$AUTH = permission;
-		app.config.globalProperties.$ROLE = rolePermission;
+		app.config.globalProperties.$CONFIG = config
+		app.config.globalProperties.$TOOL = tool
+		app.config.globalProperties.$HTTP = http
+		app.config.globalProperties.$API = api
+		app.config.globalProperties.$AUTH = permission
+		app.config.globalProperties.$ROLE = rolePermission
 
 		//注册全局指令
-		app.directive("auth", auth);
-		app.directive("auths", auths);
-		app.directive("auths-all", authsAll);
-		app.directive("role", role);
-		app.directive("time", time);
-		app.directive("copy", copy);
+		app.directive("auth", auth)
+		app.directive("auths", auths)
+		app.directive("auths-all", authsAll)
+		app.directive("role", role)
+		app.directive("time", time)
+		app.directive("copy", copy)
 
         // 统一注册el-icon图标
 		for (let icon in elIcons) {
@@ -43,9 +43,9 @@ export default {
         }
 
 		//关闭async-validator全局控制台警告
-		window.ASYNC_VALIDATOR_NO_WARNING = 1;
+		window.ASYNC_VALIDATOR_NO_WARNING = 1
 
 		//全局代码错误捕捉
-		app.config.errorHandler = errorHandler;
+		app.config.errorHandler = errorHandler
 	}
 }

+ 3 - 3
src/store/index.js

@@ -2,9 +2,9 @@
  * @description 自动import导入所有 vuex 模块
  */
 
-import { createStore } from 'vuex';
+import { createStore } from 'vuex'
 
-const files = require.context('./modules', false, /\.js$/);
+const files = require.context('./modules', false, /\.js$/)
 const modules = {}
 files.keys().forEach((key) => {
 	modules[key.replace(/(\.\/|\.js)/g, '')] = files(key).default
@@ -12,4 +12,4 @@ files.keys().forEach((key) => {
 
 export default createStore({
 	modules
-});
+})

+ 1 - 1
src/store/modules/global.js

@@ -1,4 +1,4 @@
-import config from "@/config";
+import config from "@/config"
 
 export default {
 	state: {

+ 3 - 3
src/store/modules/iframe.js

@@ -23,11 +23,11 @@ export default {
 		refreshIframe(state, route){
 			state.iframeList.forEach((item) => {
 				if (item.path == route.path){
-					var url = route.meta.url;
-					item.meta.url = '';
+					var url = route.meta.url
+					item.meta.url = ""
 					setTimeout(function() {
 						item.meta.url = url
-					}, 200);
+					}, 200)
 				}
 			})
 		},

+ 1 - 1
src/store/modules/keepAlive.js

@@ -28,7 +28,7 @@ export default {
 	},
 	actions: {
 		setRouteKey({ commit }, key) {
-			commit("setRouteKey", key);
+			commit("setRouteKey", key)
 		}
 	}
 }

+ 4 - 3
src/store/modules/tenant.js

@@ -1,6 +1,6 @@
-import XEUtils from "xe-utils";
-import API from "@/api";
-import TOOL from "@/utils/tool";
+import XEUtils from "xe-utils"
+import API from "@/api"
+import TOOL from "@/utils/tool"
 
 export default {
 	state: {
@@ -27,6 +27,7 @@ export default {
             const query = { orderBy: "id_asc" }
             XEUtils.get(TOOL.data.get("USER_INFO"), "tenantId", "0") !== "0" && XEUtils.set(query, "id", TOOL.data.get("USER_INFO").tenantId)
 
+            commit("setRouteShow", false)
             API.system.tenant.all(query).then(res => {
 				commit("SET_tenants", res)
 				commit("SET_tenantId")

+ 3 - 1
src/style/app.scss

@@ -131,6 +131,7 @@ a,button,input,textarea{-webkit-tap-highlight-color:rgba(0,0,0,0);box-sizing: bo
         .vxe-form-table-row.row--hover .vxe-table-seq-cell__handler .vxe-button-group {display: inline-block;}
         .vxe-form-table-row.row--hover .vxe-table-seq-cell__handler .seq {display: none;}
         .vxe-form-table-row.row--hover .vxe-table-seq-cell__handler .seq.is-disabled {display: inline-block;}
+        .vxe-table-edit-handler-cell .el-button {margin-left: .5em;padding-top: 0;}
         /******************************** form-table ********************************/
         .vxe-table-link-cell .vxe-cell .vxe-text {cursor: pointer;}
         .vxe-table-link-cell .vxe-cell .vxe-text:hover {text-decoration: underline;}
@@ -150,4 +151,5 @@ a,button,input,textarea{-webkit-tap-highlight-color:rgba(0,0,0,0);box-sizing: bo
 
 .el-dialog .vxe-grid.size--mini .vxe-table-query {background-color: unset;}
 
-.vxe-modal--wrapper.vxe-image-preview-popup-wrapper.is--visible.is--active {z-index: 9999 !important;}
+.vxe-modal--wrapper.vxe-image-preview-popup-wrapper.is--visible.is--active {z-index: 9999 !important;}
+.vxe-select--panel .vxe-select--body .vxe-select-option {height: 34px;padding-left: 10px;line-height: 34px;}

+ 25 - 2
src/utils/basicDic.js

@@ -1,4 +1,4 @@
-import XEUtils from "xe-utils";
+import XEUtils from "xe-utils"
 
 export const statusDic = {
     enable: "启用",
@@ -97,7 +97,6 @@ export const qualityPlanTypeDic = {
 }
 
 export const customerDic = {
-    // customer/supplier/outsourcing
     type: {
         enterprise: "企业",
         individual: "个人/个体",
@@ -130,6 +129,30 @@ export const customerDic = {
     }
 }
 
+export const supplierDic = {
+    type: {
+        raw_material: "原材料/包材供应商", // 基础物料
+        component: "零部件供应商", // 可装配的成品组件
+        outsourcing: "委外供应商",
+        mro: "MRO/耗材供应商", // 如工具、劳保用品、备件。(维护维修)
+        service: "服务供应商",
+        capital: "资产供应商"
+    },
+
+    tier: {
+        strategic: "战略供应商",
+        core: "核心供应商",
+        normal: "一般供应商"
+    },
+
+    rating: {
+        A: "A类",
+        B: "B类",
+        C: "C类",
+        D: "D类"
+    }
+}
+
 export const salesDic = {
     planType: {
         year: "年度",

+ 1 - 1
src/utils/color.js

@@ -12,7 +12,7 @@ export default {
 		for (var i = 0; i < 3; i++) {
 			if (hexs[i].length == 1) hexs[i] = "0" + hexs[i]
 		}
-		return "#" + hexs.join("");
+		return "#" + hexs.join("")
 	},
 	//加深
 	darken(color, level) {

+ 4 - 6
src/views/basic/customer/detail.vue

@@ -23,8 +23,8 @@
                             </el-form-item>
                         </el-col>
                         <el-col :md="8" :xs="24">
-                            <el-form-item label="客户类" prop="type">
-                                <el-select v-model="form.type" placeholder="请选择客户类">
+                            <el-form-item label="客户类" prop="type">
+                                <el-select v-model="form.type" placeholder="请选择客户类">
                                     <el-option v-for="(label, key) in customerDic.type" :key="key" :label="label" :value="key" />
                                 </el-select>
                             </el-form-item>
@@ -130,7 +130,7 @@ const form = ref({
 const rules = reactive({
     tenantId: [{ required: true, message: "请选择所属租户" }],
     name: [{ required: true, message: "请输入客户名称" }],
-    type: [{ required: true, message: "请选择客户类" }],
+    type: [{ required: true, message: "请选择客户类" }],
     managerName: [{ required: true, message: "请输入联系人姓名" }],
     creditNo: computed(() => [
         { required: true, validator: form.value.type == "individual" ? verifyIdCard : (rule, value, callback) => {
@@ -153,10 +153,8 @@ const formRef = ref();
 const submit = () => {
     formRef.value.validate(valid => {
         if (valid) {
-            const data = XEUtils.omit(form.value);
-
             isSaving.value = true;
-            API.basic.customer[mode.value](data).then(res => {
+            API.basic.customer[mode.value](form.value).then(res => {
                 ElMessage.success("操作成功");
                 isSaving.value = false;
                 visible.value = false;

+ 5 - 5
src/views/basic/customer/index.vue

@@ -54,18 +54,18 @@ const formConfig = reactive({
     items: computed(() => props.selectable ? [
         mapFormItemInput("nameLike", "客户名称"),
         mapFormItemInput("codeLike", "客户编号"),
-        mapFormItemSelect("type", "客户类", { ...selectConfig, options: customerDic.type })
+        mapFormItemSelect("type", "客户类", { ...selectConfig, options: customerDic.type })
     ] : [
         mapFormItemTenant({ events: { change: data => XEUtils.merge(formConfig.data, data) } }),
         mapFormItemInput("nameLike", "客户名称"),
         mapFormItemInput("codeLike", "客户编号"),
         mapFormItemSelect("status", "客户状态", selectConfig),
-        mapFormItemSelect("type", "客户类", { ...selectConfig, options: customerDic.type })
+        mapFormItemSelect("type", "客户类", { ...selectConfig, options: customerDic.type })
     ])
 });
 
 const paramsColums = reactive([
-    { column: "orderBy", defaultValue: "code_asc" },
+    { column: "orderBy", defaultValue: "createTime_desc" },
     { column: "customerType", defaultValue: "customer" },
     { column: "tenantId" },
     { column: "nameLike" },
@@ -79,7 +79,7 @@ const columns = computed(() => props.selectable ? [
     { type: props.multiple && "checkbox" || "radio", fixed: "left", width: 40 },
     { type: "html", field: "name", title: "客户名称", fixed: "left", minWidth: 150, sortable: true },
     { type: "html", field: "code", title: "客户编号", fixed: "left", minWidth: 150, sortable: true },
-    { type: "html", field: "type", title: "客户类", minWidth: 120, sortable: true, formatter: ({ cellValue }) => XEUtils.get(customerDic.type, cellValue, cellValue) },
+    { type: "html", field: "type", title: "客户类", minWidth: 120, sortable: true, formatter: ({ cellValue }) => XEUtils.get(customerDic.type, cellValue, cellValue) },
     { type: "html", field: "valueLevel", title: "客户层级", minWidth: 120, sortable: true, formatter: ({ cellValue }) => XEUtils.get(customerDic.tier, cellValue, cellValue) },
     { type: "html", field: "creditLevel", title: "信用等级", minWidth: 120, sortable: true, formatter: ({ cellValue }) => XEUtils.get(customerDic.rating, cellValue, cellValue) },
     { type: "html", field: "managerName", title: "联系人", minWidth: 120, sortable: true },
@@ -90,7 +90,7 @@ const columns = computed(() => props.selectable ? [
     { type: "html", field: "name", title: "客户名称", fixed: "left", minWidth: 150, sortable: true },
     { type: "html", field: "code", title: "客户编号", fixed: "left", minWidth: 150, sortable: true },
     { field: "status", title: "客户状态", minWidth: 100, editRender: { name: "$cell-tag", options: statusDic } },
-    { type: "html", field: "type", title: "客户类", minWidth: 120, sortable: true, formatter: ({ cellValue }) => XEUtils.get(customerDic.type, cellValue, cellValue) },
+    { type: "html", field: "type", title: "客户类", minWidth: 120, sortable: true, formatter: ({ cellValue }) => XEUtils.get(customerDic.type, cellValue, cellValue) },
     { visible: false, type: "html", field: "valueLevel", title: "客户层级", minWidth: 120, sortable: true, formatter: ({ cellValue }) => XEUtils.get(customerDic.tier, cellValue, cellValue) },
     { visible: false, type: "html", field: "creditLevel", title: "信用等级", minWidth: 120, sortable: true, formatter: ({ cellValue }) => XEUtils.get(customerDic.rating, cellValue, cellValue) },
     { type: "html", field: "managerName", title: "联系人", minWidth: 120, sortable: true },

+ 1 - 1
src/views/basic/material/index.vue

@@ -66,7 +66,7 @@ const formConfig = reactive({
 });
 
 const paramsColums = reactive([
-    { column: "orderBy", defaultValue: "code_asc" },
+    { column: "orderBy", defaultValue: "createTime_desc" },
     { column: "tenantId" },
     { column: "nameLike" },
     { column: "codeLike" },

+ 12 - 9
src/views/basic/qualityPlan/desc.vue

@@ -3,13 +3,15 @@
         <el-main>
             <el-collapse v-model="activeNames">
                 <el-collapse-item title="基本信息" name="basic">
+                    <el-descriptions v-if="$store.state.tenant.tenantId === '0'" :column="1" label-width="140" border>
+                        <el-descriptions-item label="所属租户" label-align="right">{{ tenantName }}</el-descriptions-item>
+                    </el-descriptions>
                     <el-descriptions :column="3" label-width="140" border>
-                        <el-descriptions-item v-if="$store.state.tenant.tenantId === '0'" label="所属租户" :span="ismobile ? 3 : 1" label-align="right" min-width="120">{{ tenantName }}</el-descriptions-item>
-                        <el-descriptions-item label="方案名称" :span="ismobile ? 3 : 1" label-align="right" min-width="120">{{ descData.name }}</el-descriptions-item>
-                        <el-descriptions-item label="方案编号" :span="ismobile ? 3 : 1" label-align="right" min-width="120">{{ descData.code }}</el-descriptions-item>
-                        <el-descriptions-item label="添加时间" :span="ismobile ? 3 : 1" label-align="right" min-width="120">{{ descData.createTime }}</el-descriptions-item>
-                        <el-descriptions-item v-if="!descData.reviewUserName" label="审批状态" :span="ismobile ? 3 : 1" label-align="right" min-width="120">{{ XEUtils.get(reviewStatusDic, descData.reviewStatus, descData.reviewStatus) }}</el-descriptions-item>
-                        <el-descriptions-item v-if="descData.reviewUserName && mode == 'approval'" label="审批人员" :span="ismobile ? 3 : 1" label-align="right" min-width="120">{{ descData.reviewUserName }}</el-descriptions-item>
+                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="方案名称" :span="ismobile ? 3 : 1" label-align="right">{{ descData.name }}</el-descriptions-item>
+                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="方案编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.code }}</el-descriptions-item>
+                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="添加时间" :span="ismobile ? 3 : 1" label-align="right">{{ descData.createTime }}</el-descriptions-item>
+                        <el-descriptions-item v-if="!descData.reviewUserName" label="审批状态" :span="ismobile ? 3 : 1" label-align="right">{{ XEUtils.get(reviewStatusDic, descData.reviewStatus, descData.reviewStatus) }}</el-descriptions-item>
+                        <el-descriptions-item v-if="descData.reviewUserName && mode == 'approval'" label="审批人员" :span="ismobile ? 3 : 1" label-align="right">{{ descData.reviewUserName }}</el-descriptions-item>
                         <el-descriptions-item label="质检人员" :span="ismobile ? 3 : 1" label-align="right">{{ descData.inspectUserName }}</el-descriptions-item>
                         <el-descriptions-item label="方案类型" :span="ismobile ? 3 : 1" label-align="right">{{ XEUtils.get(qualityPlanTypeDic, descData.type, descData.type) }}</el-descriptions-item>
                         <template v-if="descData.type == 'sampling'">
@@ -27,9 +29,9 @@
 
                 <el-collapse-item v-if="descData.reviewUserName" title="审批信息" name="approval">
                     <el-descriptions v-if="mode == 'detail'" :column="3" label-width="140" border>
-                        <el-descriptions-item label="审批状态" :span="ismobile ? 3 : 1" label-align="right" min-width="120">{{ XEUtils.get(reviewStatusDic, descData.reviewStatus, descData.reviewStatus) }}</el-descriptions-item>
-                        <el-descriptions-item label="审批人员" :span="ismobile ? 3 : 1" label-align="right" min-width="120">{{ descData.reviewUserName }}</el-descriptions-item>
-                        <el-descriptions-item label="审批时间" :span="ismobile ? 3 : 1" label-align="right" min-width="120">{{ descData.reviewTime }}</el-descriptions-item>
+                        <el-descriptions-item label="审批状态" :span="ismobile ? 3 : 1" label-align="right">{{ XEUtils.get(reviewStatusDic, descData.reviewStatus, descData.reviewStatus) }}</el-descriptions-item>
+                        <el-descriptions-item label="审批人员" :span="ismobile ? 3 : 1" label-align="right">{{ descData.reviewUserName }}</el-descriptions-item>
+                        <el-descriptions-item label="审批时间" :span="ismobile ? 3 : 1" label-align="right">{{ descData.reviewTime }}</el-descriptions-item>
                         <el-descriptions-item label="审批意见" :span="3" label-align="right">{{ descData.reviewReason }}</el-descriptions-item>
                     </el-descriptions>
 
@@ -152,4 +154,5 @@ defineExpose({
 .el-collapse-item :deep(.el-collapse-item__header.is-active) {border-bottom: 1px solid var(--el-border-color-lighter);}
 .el-collapse-item :deep(.el-collapse-item__wrap) {border: none;}
 .el-collapse-item :deep(.el-collapse-item__content) {padding: 20px 0;}
+.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions__content {min-width: 120px;}
 </style>

+ 2 - 4
src/views/basic/qualityPlan/detail.vue

@@ -6,7 +6,7 @@
                     <el-row>
                         <el-col v-if="$store.state.tenant.tenantId === '0'" :md="8" :xs="24">
                             <el-form-item label="所属租户" prop="tenantId">
-                                <el-select v-if="!form.id" v-model="form.tenantId" filterable placeholder="请选择所属租户">
+                                <el-select v-if="!form.id" v-model="form.tenantId" filterable placeholder="请选择所属租户" @change="form.inspectUserId = null, form.reviewUserId = null">
                                     <el-option v-for="item in $store.state.tenant.tenants" :key="item.id" :label="item.name" :value="item.id"></el-option>
                                 </el-select>
                                 <el-input v-else v-model="tenantName" readonly></el-input>
@@ -166,9 +166,7 @@ const submit = () => {
     });
 }
 
-const removeSuccess = () => {
-    if (form.value.id) isDel.value = true;
-}
+const removeSuccess = () => form.value.id && (isDel.value = true);
 
 const fetchUser = () => API.auth.user.all({ orderBy: "id_desc" }).then(res => users.value = res).catch(() => users.value = []);
 fetchUser();

+ 1 - 1
src/views/basic/qualityPlan/index.vue

@@ -71,7 +71,7 @@ const formConfig = reactive({
 });
 
 const paramsColums = reactive([
-    { column: "orderBy", defaultValue: "code_desc" },
+    { column: "orderBy", defaultValue: "createTime_desc" },
     { column: "tenantId" },
     { column: "nameLike" },
     { column: "codeLike" },

+ 204 - 0
src/views/basic/supplier/detail.vue

@@ -0,0 +1,204 @@
+<template>
+    <el-dialog v-model="visible" :title="titleMap[mode]" fullscreen :close-on-click-modal="false" @closed="$emit('closed', isDel)">
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="120">
+            <el-collapse v-model="activeNames">
+                <el-collapse-item title="基本信息" name="basic">
+                    <el-row>
+                        <el-col v-if="$store.state.tenant.tenantId === '0'" :md="8" :xs="24">
+                            <el-form-item label="所属租户" prop="tenantId">
+                                <el-select v-if="!form.id" v-model="form.tenantId" filterable placeholder="请选择所属租户">
+                                    <el-option v-for="item in $store.state.tenant.tenants" :key="item.id" :label="item.name" :value="item.id"></el-option>
+                                </el-select>
+                                <el-input v-else v-model="tenantName" readonly></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="供应商名称" prop="name">
+                                <el-input v-model="form.name" placeholder="请输入供应商名称"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="供应商编号" required>
+                                <el-input v-model="form.code" :readonly="!!form.id" maxlength="50" show-word-limit clearable placeholder="不填将自动生成"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="供应商类型" prop="type">
+                                <el-select v-model="form.type" placeholder="请选择供应商类型">
+                                    <el-option v-for="(label, key) in supplierDic.type" :key="key" :label="label" :value="key" />
+                                </el-select>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="供应商等级">
+                                <el-select v-model="form.valueLevel" clearable placeholder="请选择供应商等级">
+                                    <el-option v-for="(label, key) in supplierDic.tier" :key="key" :label="label" :value="key" />
+                                </el-select>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="绩效等级">
+                                <el-select v-model="form.creditLevel" clearable placeholder="请选择绩效等级">
+                                    <el-option v-for="(label, key) in supplierDic.rating" :key="key" :label="label" :value="key" />
+                                </el-select>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :xs="24">
+                            <el-form-item label="经营地址">
+                                <el-input v-model="form.address" type="textarea" maxlength="200" placeholder="请输入经营地址"></el-input>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+                
+                <el-collapse-item title="联系信息" name="contact">
+                    <el-row>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="联系人姓名" prop="managerName">
+                                <el-input v-model="form.managerName" placeholder="请输入联系人姓名"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="联系方式" prop="managerPhone">
+                                <el-input v-model="form.managerPhone" clearable placeholder="请输入联系方式"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="办公电话">
+                                <el-input v-model="form.phone" clearable placeholder="请输入办公电话"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="统一社会信用代码" prop="creditNo" label-width="140">
+                                <el-input v-model="form.creditNo" placeholder="请输入统一社会信用代码"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="邮箱">
+                                <el-input v-model="form.email" clearable placeholder="请输入邮箱"></el-input>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+
+                <el-collapse-item title="资质信息" name="credit">
+                    <el-row>
+                        <el-col :xs="24">
+                            <el-form-item label="附件" label-width="100">
+                                <sc-upload-file v-model="form.fileList" @removeSuccess="removeSuccess">
+                                    <el-button type="primary" size="small">上传附件</el-button>
+                                </sc-upload-file>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+            </el-collapse>
+        </el-form>
+
+        <template #footer>
+            <el-button auto-insert-space @click="visible = false">取消</el-button>
+            <el-button :loading="isSaving" type="primary" auto-insert-space @click="submit">保存</el-button>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup>
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import store from "@/store";
+import { supplierDic } from "@/utils/basicDic";
+import scUploadFile from "@/components/scUpload/file";
+
+const $emit = defineEmits(["success", "closed"]);
+const visible = ref(false);
+const isSaving = ref(false);
+const isDel = ref(false);
+
+const activeNames = ref(["basic", "contact", "credit"]);
+const mode = ref("add");
+const titleMap = reactive({
+    add: "新增供应商",
+    edit: "修改供应商"
+});
+
+const tenantName = computed(() => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == form.value.tenantId), "name"));
+const form = ref({
+    id: null,
+    tenantId: store.state.tenant.tenantId,
+    name: null,
+    code: null,
+    customerType: "supplier",
+    type: null,
+    creditNo: null,
+    valueLevel: null,
+    creditLevel: null,
+    managerName: null,
+    managerPhone: null,
+    phone: null,
+    email: null,
+    address: null,
+    fileList: []
+});
+const rules = reactive({
+    tenantId: [{ required: true, message: "请选择所属租户" }],
+    name: [{ required: true, message: "请输入供应商名称" }],
+    type: [{ required: true, message: "请选择供应商类型" }],
+    managerName: [{ required: true, message: "请输入联系人姓名" }],
+    creditNo: [
+        { required: true, message: "请输入统一社会信用代码" },
+        { len: 18, message: "请输入18位统一社会信用代码" }
+    ],
+    managerPhone: [{ pattern: /^\d{11}$/, message: "请输入11位手机号码" }]
+});
+
+const open = () => visible.value = true;
+const setData = data => {
+    open();
+    mode.value = "edit";
+    XEUtils.objectEach(form.value, (_, key) => {
+        if (key == "fileList") XEUtils.set(form.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item, name: item.fileName })));
+        else XEUtils.set(form.value, key, XEUtils.get(data, key));
+    });
+}
+
+const formRef = ref();
+const submit = () => {
+    formRef.value.validate(valid => {
+        if (valid) {
+            const data = XEUtils.omit(form.value, "fileList");
+            const fileList = XEUtils.map(XEUtils.filter(form.value.fileList, item => !item.id), item => ({ ...XEUtils.omit(item, "id", "name"), fileName: item.name, fileType: "supplierAttach" }));
+            fileList.length > 0 && XEUtils.set(data, "fileList", fileList);
+
+            isSaving.value = true;
+            API.basic.customer[mode.value](data).then(res => {
+                ElMessage.success("操作成功");
+                isSaving.value = false;
+                isDel.value = false;
+                visible.value = false;
+                $emit("success", mode.value);
+            }).catch(() => isSaving.value = false);
+        } else {
+            return false;
+        }
+    });
+}
+
+const removeSuccess = () => form.value.id && (isDel.value = true);
+
+defineExpose({
+    open,
+    setData
+});
+</script>
+
+<style scoped>
+.el-form {padding-left: 16px;padding-right: 22px;}
+
+.el-collapse {border: none;}
+.el-collapse-item {margin-top: 15px;padding: 0 24px;background-color: var(--el-fill-color-blank);border: 1px solid var(--el-border-color-light);border-radius: 4px;color: var(--el-text-color-primary);box-shadow: var(--el-box-shadow-light);transition: var(--el-transition-duration);}
+.el-collapse-item :deep(.el-collapse-item__header) {border-bottom-color: transparent;line-height: 55px;font-size: 16px;font-weight: bold;}
+.el-collapse-item :deep(.el-collapse-item__header.is-active) {border-bottom: 1px solid var(--el-border-color-lighter);}
+.el-collapse-item :deep(.el-collapse-item__wrap) {border: none;}
+.el-collapse-item :deep(.el-collapse-item__content) {padding: 20px 28px 20px 0;}
+</style>

+ 159 - 0
src/views/basic/supplier/index.vue

@@ -0,0 +1,159 @@
+<template>
+	<el-container class="is-vertical">
+        <sc-page-header v-if="!selectable" @add="table_add"></sc-page-header>
+
+        <scTable ref="xGridTable" :apiObj="$API.basic.customer" :formConfig="formConfig" :paramsColums="paramsColums" :columns="columns" v-bind="options">
+            <template #action="{ row }">
+                <el-button type="primary" link @click="table_edit(row)">
+                    <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>修改
+                </el-button>
+                <el-button v-if="row.status == 'enable'" type="primary" link @click="table_change(row)">
+                    <template #icon><sc-iconify icon="material-symbols:lock-outline"></sc-iconify></template>停用
+                </el-button>
+                <el-button v-else type="primary" link @click="table_change(row)">
+                    <template #icon><sc-iconify icon="material-symbols:lock-open-outline"></sc-iconify></template>启用
+                </el-button>
+                <el-button type="primary" link @click="table_del(row)">
+                    <template #icon><sc-iconify icon="ant-design:delete-outlined"></sc-iconify></template>删除
+                </el-button>
+            </template>
+        </scTable>
+	</el-container>
+
+    <supplier-detail v-if="dialog" ref="supplierRef" @success="refreshTable" @closed="dialogClose"></supplier-detail>
+</template>
+
+<script setup>
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import TOOL from "@/utils/tool";
+import { statusDic, supplierDic } from "@/utils/basicDic";
+import { mapFormItemInput, mapFormItemSelect, mapFormItemTenant } from "@/components/scTable/helper";
+import supplierDetail from "./detail";
+
+import store from "@/store";
+watch(() => store.state.tenant.tenantId, () => refreshTable());
+
+const props = defineProps({
+    selectable: { type: Boolean, default: false },
+    multiple: { type: Boolean, default: false },
+    paramsColums: { type: Array, default: () => [] },
+    options: { type: Object, default: () => ({}) }
+});
+
+const selectConfig = reactive({
+    options: statusDic,
+    events: {
+        change: data => XEUtils.merge(formConfig.data, data)
+    }
+});
+
+const formConfig = reactive({
+    data: {},
+    items: computed(() => props.selectable ? [
+        mapFormItemInput("nameLike", "供应商名称"),
+        mapFormItemInput("codeLike", "供应商编号"),
+        mapFormItemSelect("type", "供应商类型", { ...selectConfig, options: supplierDic.type })
+    ] : [
+        mapFormItemTenant({ events: { change: data => XEUtils.merge(formConfig.data, data) } }),
+        mapFormItemInput("nameLike", "供应商名称"),
+        mapFormItemInput("codeLike", "供应商编号"),
+        mapFormItemSelect("status", "供应商状态", selectConfig),
+        mapFormItemSelect("type", "供应商类型", { ...selectConfig, options: supplierDic.type })
+    ])
+});
+
+const paramsColums = reactive([
+    { column: "orderBy", defaultValue: "createTime_desc" },
+    { column: "customerType", defaultValue: "supplier" },
+    { column: "tenantId" },
+    { column: "nameLike" },
+    { column: "codeLike" },
+    { column: "status" },
+    { column: "type" },
+    ...props.paramsColums
+]);
+
+const columns = computed(() => props.selectable ? [
+    { type: props.multiple && "checkbox" || "radio", fixed: "left", width: 40 },
+    { type: "html", field: "name", title: "供应商名称", fixed: "left", minWidth: 150, sortable: true },
+    { type: "html", field: "code", title: "供应商编号", fixed: "left", minWidth: 150, sortable: true },
+    { type: "html", field: "type", title: "供应商类型", minWidth: 120, sortable: true, formatter: ({ cellValue }) => XEUtils.get(supplierDic.type, cellValue, cellValue) },
+    { type: "html", field: "valueLevel", title: "供应商等级", minWidth: 120, sortable: true, formatter: ({ cellValue }) => XEUtils.get(supplierDic.tier, cellValue, cellValue) },
+    { type: "html", field: "creditLevel", title: "绩效等级", minWidth: 120, sortable: true, formatter: ({ cellValue }) => XEUtils.get(supplierDic.rating, cellValue, cellValue) },
+    { type: "html", field: "managerName", title: "联系人", minWidth: 120, sortable: true },
+    { type: "html", field: "managerPhone", title: "联系方式", minWidth: 120, sortable: true }
+] : [
+    { type: "seq", fixed: "left", width: 60 },
+    { visible: store.state.tenant.tenantId === "0", type: "html", field: "tenantName", title: "所属租户", fixed: "left", minWidth: 200, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == row.tenantId), "name") },
+    { type: "html", field: "name", title: "供应商名称", fixed: "left", minWidth: 150, sortable: true },
+    { type: "html", field: "code", title: "供应商编号", fixed: "left", minWidth: 150, sortable: true },
+    { field: "status", title: "供应商状态", minWidth: 100, editRender: { name: "$cell-tag", options: statusDic } },
+    { type: "html", field: "type", title: "供应商类型", minWidth: 140, sortable: true, formatter: ({ cellValue }) => XEUtils.get(supplierDic.type, cellValue, cellValue) },
+    { visible: false, type: "html", field: "valueLevel", title: "供应商等级", minWidth: 120, sortable: true, formatter: ({ cellValue }) => XEUtils.get(supplierDic.tier, cellValue, cellValue) },
+    { visible: false, type: "html", field: "creditLevel", title: "绩效等级", minWidth: 120, sortable: true, formatter: ({ cellValue }) => XEUtils.get(supplierDic.rating, cellValue, cellValue) },
+    { visible: false, type: "html", field: "creditNo", title: "统一社会信用代码", minWidth: 120, sortable: true, formatter: ({ cellValue }) => XEUtils.get(supplierDic.tier, cellValue, cellValue) },
+    { type: "html", field: "managerName", title: "联系人", minWidth: 120, sortable: true },
+    { type: "html", field: "managerPhone", title: "联系方式", minWidth: 120, sortable: true },
+    { visible: false, type: "html", field: "email", title: "邮箱", minWidth: 120, sortable: true },
+    { type: "html", field: "createTime", title: "创建日期", minWidth: 120, sortable: true, formatter: ({ cellValue }) => TOOL.dateFormat(cellValue, "YYYY-MM-DD") || cellValue },
+    { visible: false, type: "html", field: "address", title: "经营地址", minWidth: 300, sortable: true },
+    { title: "操作", fixed: "right", width: 220, slots: { default: "action" } }
+]);
+
+// 显示隐藏 筛选表单
+const xGridTable = ref();
+const refreshTable = (mode = "add") => (xGridTable.value.searchData(mode), xGridTable.value.reloadColumn(columns.value));
+
+const supplierRef = ref();
+const dialog = ref(false);
+
+const table_add = () => {
+    dialog.value = true;
+    nextTick(() => supplierRef.value?.open());
+}
+
+const table_edit = row => {
+    dialog.value = true;
+    nextTick(() => supplierRef.value?.setData(row));
+}
+
+const table_del = ({ id }) => {
+    ElMessageBox.confirm("是否确认删除该供应商?", "删除警告", {
+        type: "warning",
+        confirmButtonText: "确定",
+        cancelButtonText: "取消"
+    }).then(() => {
+        API.basic.customer.del({ id }).then(() => {
+            ElMessage.success("操作成功");
+            refreshTable();
+        });
+    }).catch(() => {});
+}
+
+const table_change = row => {
+    const status = row.status == "enable" && "disable" || "enable";
+    const msg = row.status == "enable" && "停用" || "启用";
+
+    ElMessageBox.confirm(`是否确认${msg}该供应商?`, `${msg}警告`, {
+        type: "warning",
+        confirmButtonText: "确定",
+        cancelButtonText: "取消"
+    }).then(() => {
+        API.basic.customer.edit({ id: row.id, status }).then(() => {
+            ElMessage.success("操作成功");
+            refreshTable();
+        });
+    }).catch(() => {});
+}
+
+const dialogClose = isDel => {
+    dialog.value = false;
+    isDel && refreshTable();
+}
+
+defineExpose({
+    getSelectRows: () => xGridTable.value.getSelectRows()
+});
+</script>

+ 84 - 0
src/views/basic/warehouse/detail.vue

@@ -0,0 +1,84 @@
+<template>
+    <el-dialog v-model="visible" :title="titleMap[mode]" width="480" :close-on-click-modal="false" @closed="$emit('closed')">
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="120">
+            <el-form-item v-if="$store.state.tenant.tenantId === '0'" label="所属租户" prop="tenantId">
+                <el-select v-if="!form.id" v-model="form.tenantId" filterable placeholder="请选择所属租户">
+                    <el-option v-for="item in $store.state.tenant.tenants" :key="item.id" :label="item.name" :value="item.id"></el-option>
+                </el-select>
+                <el-input v-else v-model="tenantName" readonly></el-input>
+			</el-form-item>
+            <el-form-item label="仓库名称" prop="name">
+                <el-input v-model="form.name" placeholder="请输入仓库名称"></el-input>
+            </el-form-item>
+            <el-form-item label="仓库编号" required>
+                <el-input v-model="form.code" :readonly="!!form.id" maxlength="50" show-word-limit clearable placeholder="不填将自动生成"></el-input>
+            </el-form-item>
+        </el-form>
+
+        <template #footer>
+            <el-button auto-insert-space @click="visible = false">取消</el-button>
+            <el-button :loading="isSaving" type="primary" auto-insert-space @click="submit">保存</el-button>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup>
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import store from "@/store";
+const $emit = defineEmits(["success", "closed"]);
+const visible = ref(false);
+const isSaving = ref(false);
+
+const mode = ref("add");
+const titleMap = reactive({
+    add: "新增仓库",
+    edit: "修改仓库"
+});
+
+const tenantName = computed(() => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == form.value.tenantId), "name"));
+const form = ref({
+    id: null,
+    tenantId: store.state.tenant.tenantId,
+    name: null,
+    code: null
+});
+const rules = reactive({
+    tenantId: [{ required: true, message: "请选择所属租户" }],
+    name: [{ required: true, message: "请输入仓库名称" }]
+});
+
+const open = () => visible.value = true;
+const setData = data => {
+    open();
+    mode.value = "edit";
+    XEUtils.objectEach(form.value, (_, key) => XEUtils.set(form.value, key, XEUtils.get(data, key)));
+}
+
+const formRef = ref();
+const submit = () => {
+    formRef.value.validate(valid => {
+        if (valid) {
+            isSaving.value = true;
+            API.basic.warehouse[mode.value](form.value).then(res => {
+                isSaving.value = false;
+                ElMessage.success("操作成功");
+                visible.value = false;
+                $emit("success", mode.value);
+            }).catch(() => isSaving.value = false);
+        } else {
+            return false;
+        }
+    });
+}
+
+defineExpose({
+    open,
+    setData
+});
+</script>
+
+<style scoped>
+.el-form {padding-right: 34px;}
+</style>

+ 85 - 0
src/views/basic/warehouse/index.vue

@@ -0,0 +1,85 @@
+<template>
+	<el-container class="is-vertical">
+        <sc-page-header @add="table_add"></sc-page-header>
+
+        <scTable ref="xGridTable" :apiObj="$API.basic.warehouse" :formConfig="formConfig" :paramsColums="paramsColums" :columns="columns">
+            <template #action="{ row }">
+                <el-button type="primary" link @click="table_edit(row)">
+                    <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>修改
+                </el-button>
+                <el-button type="primary" link @click="table_del(row)">
+                    <template #icon><sc-iconify icon="ant-design:delete-outlined"></sc-iconify></template>删除
+                </el-button>
+            </template>
+        </scTable>
+	</el-container>
+
+    <warehouse-detail v-if="dialog" ref="warehouseRef" @success="refreshTable" @closed="dialog = false"></warehouse-detail>
+</template>
+
+<script setup>
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import TOOL from "@/utils/tool";
+import { mapFormItemInput, mapFormItemTenant } from "@/components/scTable/helper";
+import warehouseDetail from "./detail";
+
+import store from "@/store";
+watch(() => store.state.tenant.tenantId, () => refreshTable());
+
+const formConfig = reactive({
+    data: {},
+    items: [
+        mapFormItemTenant({ events: { change: data => XEUtils.merge(formConfig.data, data) } }),
+        mapFormItemInput("nameLike", "仓库名称"),
+        mapFormItemInput("codeLike", "仓库编号")
+    ]
+});
+
+const paramsColums = reactive([
+    { column: "orderBy", defaultValue: "createTime_desc" },
+    { column: "tenantId" },
+    { column: "nameLike" },
+    { column: "codeLike" }
+]);
+
+const columns = reactive([
+    { type: "seq", width: 60 },
+    { visible: computed(() => store.state.tenant.tenantId === "0"), type: "html", field: "tenantName", title: "所属租户", minWidth: 200, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == row.tenantId), "name") },
+    { type: "html", field: "name", title: "仓库名称", minWidth: 150, sortable: true },
+    { type: "html", field: "code", title: "仓库编号", minWidth: 150, sortable: true },
+    { type: "html", field: "createTime", title: "创建日期", minWidth: 120, sortable: true, formatter: ({ cellValue }) => TOOL.dateFormat(cellValue, "YYYY-MM-DD") || cellValue },
+    { title: "操作", fixed: "right", width: 220, slots: { default: "action" } }
+]);
+
+// 显示隐藏 筛选表单
+const xGridTable = ref();
+const refreshTable = (mode = "add") => (xGridTable.value.searchData(mode), xGridTable.value.reloadColumn(columns));
+
+const warehouseRef = ref();
+const dialog = ref(false);
+
+const table_add = () => {
+    dialog.value = true;
+    nextTick(() => warehouseRef.value?.open());
+}
+
+const table_edit = row => {
+    dialog.value = true;
+    nextTick(() => warehouseRef.value?.setData(row));
+}
+
+const table_del = ({ id }) => {
+    ElMessageBox.confirm("是否确认删除该仓库?", "删除警告", {
+        type: "warning",
+        confirmButtonText: "确定",
+        cancelButtonText: "取消"
+    }).then(() => {
+        API.basic.warehouse.del(({ id })).then(() => {
+            ElMessage.success("操作成功");
+            refreshTable();
+        });
+    }).catch(() => {});
+}
+</script>

+ 89 - 0
src/views/equip/device/detail.vue

@@ -0,0 +1,89 @@
+<template>
+    <el-dialog v-model="visible" :title="titleMap[mode]" width="480" :close-on-click-modal="false" @closed="$emit('closed')">
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="120">
+            <el-form-item v-if="$store.state.tenant.tenantId === '0'" label="所属租户" prop="tenantId">
+                <el-select v-if="!form.id" v-model="form.tenantId" filterable placeholder="请选择所属租户">
+                    <el-option v-for="item in $store.state.tenant.tenants" :key="item.id" :label="item.name" :value="item.id"></el-option>
+                </el-select>
+                <el-input v-else v-model="tenantName" readonly></el-input>
+			</el-form-item>
+            <el-form-item label="设备名称" prop="name">
+                <el-input v-model="form.name" placeholder="请输入设备名称"></el-input>
+            </el-form-item>
+            <el-form-item label="设备编号" prop="code">
+                <el-input v-model="form.code" placeholder="请输入设备编号"></el-input>
+            </el-form-item>
+            <el-form-item label="描述信息">
+                <el-input v-model="form.remark" type="textarea" :rows="4" placeholder="请输入内容"></el-input>
+            </el-form-item>
+        </el-form>
+
+        <template #footer>
+            <el-button auto-insert-space @click="visible = false">取消</el-button>
+            <el-button :loading="isSaving" type="primary" auto-insert-space @click="submit">保存</el-button>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup>
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import store from "@/store";
+const $emit = defineEmits(["success", "closed"]);
+const visible = ref(false);
+const isSaving = ref(false);
+
+const mode = ref("add");
+const titleMap = reactive({
+    add: "新增设备",
+    edit: "修改设备"
+});
+
+const tenantName = computed(() => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == form.value.tenantId), "name"));
+const form = ref({
+    id: null,
+    tenantId: store.state.tenant.tenantId,
+    name: null,
+    code: null,
+    remark: null
+});
+const rules = reactive({
+    tenantId: [{ required: true, message: "请选择所属租户" }],
+    name: [{ required: true, message: "请输入设备名称" }],
+    code: [{ required: true, message: "请输入设备编号" }]
+});
+
+const open = () => visible.value = true;
+const setData = data => {
+    open();
+    mode.value = "edit";
+    XEUtils.objectEach(form.value, (_, key) => XEUtils.set(form.value, key, XEUtils.get(data, key)));
+}
+
+const formRef = ref();
+const submit = () => {
+    formRef.value.validate(valid => {
+        if (valid) {
+            isSaving.value = true;
+            API.equip.device[mode.value](form.value).then(res => {
+                isSaving.value = false;
+                ElMessage.success("操作成功");
+                visible.value = false;
+                $emit("success", mode.value);
+            }).catch(() => isSaving.value = false);
+        } else {
+            return false;
+        }
+    });
+}
+
+defineExpose({
+    open,
+    setData
+});
+</script>
+
+<style scoped>
+.el-form {padding-right: 34px;}
+</style>

+ 86 - 0
src/views/equip/device/index.vue

@@ -0,0 +1,86 @@
+<template>
+	<el-container class="is-vertical">
+        <sc-page-header @add="table_add"></sc-page-header>
+
+        <scTable ref="xGridTable" :apiObj="$API.equip.device" :formConfig="formConfig" :paramsColums="paramsColums" :columns="columns">
+            <template #action="{ row }">
+                <el-button type="primary" link @click="table_edit(row)">
+                    <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>修改
+                </el-button>
+                <el-button type="primary" link @click="table_del(row)">
+                    <template #icon><sc-iconify icon="ant-design:delete-outlined"></sc-iconify></template>删除
+                </el-button>
+            </template>
+        </scTable>
+	</el-container>
+
+    <device-detail v-if="dialog" ref="deviceRef" @success="refreshTable" @closed="dialog = false"></device-detail>
+</template>
+
+<script setup>
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import TOOL from "@/utils/tool";
+import { mapFormItemInput, mapFormItemTenant } from "@/components/scTable/helper";
+import deviceDetail from "./detail";
+
+import store from "@/store";
+watch(() => store.state.tenant.tenantId, () => refreshTable());
+
+const formConfig = reactive({
+    data: {},
+    items: [
+        mapFormItemTenant({ events: { change: data => XEUtils.merge(formConfig.data, data) } }),
+        mapFormItemInput("nameLike", "设备名称"),
+        mapFormItemInput("codeLike", "设备编号")
+    ]
+});
+
+const paramsColums = reactive([
+    { column: "orderBy", defaultValue: "createTime_desc" },
+    { column: "tenantId" },
+    { column: "nameLike" },
+    { column: "codeLike" }
+]);
+
+const columns = reactive([
+    { type: "seq", width: 60 },
+    { visible: computed(() => store.state.tenant.tenantId === "0"), type: "html", field: "tenantName", title: "所属租户", minWidth: 200, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == row.tenantId), "name") },
+    { type: "html", field: "name", title: "设备名称", minWidth: 150, sortable: true },
+    { type: "html", field: "code", title: "设备编号", minWidth: 150, sortable: true },
+    { type: "html", field: "createTime", title: "创建日期", minWidth: 120, sortable: true, formatter: ({ cellValue }) => TOOL.dateFormat(cellValue, "YYYY-MM-DD") || cellValue },
+    { type: "html", field: "remark", title: "描述信息", minWidth: 300, sortable: true },
+    { title: "操作", fixed: "right", width: 220, slots: { default: "action" } }
+]);
+
+// 显示隐藏 筛选表单
+const xGridTable = ref();
+const refreshTable = (mode = "add") => (xGridTable.value.searchData(mode), xGridTable.value.reloadColumn(columns));
+
+const deviceRef = ref();
+const dialog = ref(false);
+
+const table_add = () => {
+    dialog.value = true;
+    nextTick(() => deviceRef.value?.open());
+}
+
+const table_edit = row => {
+    dialog.value = true;
+    nextTick(() => deviceRef.value?.setData(row));
+}
+
+const table_del = ({ id }) => {
+    ElMessageBox.confirm("是否确认删除该设备?", "删除警告", {
+        type: "warning",
+        confirmButtonText: "确定",
+        cancelButtonText: "取消"
+    }).then(() => {
+        API.equip.device.del(({ id })).then(() => {
+            ElMessage.success("操作成功");
+            refreshTable();
+        });
+    }).catch(() => {});
+}
+</script>

+ 1 - 1
src/views/login/index.vue

@@ -117,7 +117,7 @@ export default {
                         this.$TOOL.cookie.set("MES_TOKEN", res.token, { expires: this.form.rememberMe ? 24 * 60 * 60 : 2 * 60 * 60 });
                         this.$TOOL.data.set("USER_INFO", res.user.user || res.user);
                         this.$router.replace({ path: XEUtils.get(this.$route, "redirectedFrom.fullPath", "/") });
-                        this.getCode();
+                        setTimeout(() => this.getCode(), 0);
                     }).catch(() => {
                         this.islogin = false;
                         this.getCode();

+ 8 - 5
src/views/process/line/desc.vue

@@ -3,12 +3,14 @@
         <el-main>
             <el-collapse v-model="activeNames">
                 <el-collapse-item title="基本信息" name="basic">
+                    <el-descriptions v-if="$store.state.tenant.tenantId === '0'" :column="1" label-width="140" border>
+                        <el-descriptions-item label="所属租户" label-align="right">{{ tenantName }}</el-descriptions-item>
+                    </el-descriptions>
                     <el-descriptions :column="3" label-width="140" border>
-                        <el-descriptions-item v-if="$store.state.tenant.tenantId === '0'" label="所属租户" :span="ismobile ? 3 : 1" label-align="right" min-width="120">{{ tenantName }}</el-descriptions-item>
-                        <el-descriptions-item label="工艺路线名称" :span="ismobile ? 3 : 1" label-align="right" min-width="120">{{ descData.name }}</el-descriptions-item>
-                        <el-descriptions-item label="工艺路线编号" :span="ismobile ? 3 : 1" label-align="right" min-width="120">{{ descData.code }}</el-descriptions-item>
-                        <el-descriptions-item label="添加时间" :span="ismobile ? 3 : 1" label-align="right" min-width="120">{{ descData.createTime }}</el-descriptions-item>
-                        <el-descriptions-item label="工艺路线状态" :span="ismobile ? 3 : 1" label-align="right" min-width="120">{{ XEUtils.get(statusDic, descData.status, descData.status) }}</el-descriptions-item>
+                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="工艺路线名称" :span="ismobile ? 3 : 1" label-align="right">{{ descData.name }}</el-descriptions-item>
+                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="工艺路线编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.code }}</el-descriptions-item>
+                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="添加时间" :span="ismobile ? 3 : 1" label-align="right">{{ descData.createTime }}</el-descriptions-item>
+                        <el-descriptions-item label="工艺路线状态" :span="ismobile ? 3 : 1" label-align="right">{{ XEUtils.get(statusDic, descData.status, descData.status) }}</el-descriptions-item>
                         <el-descriptions-item label="时间单位" :span="ismobile ? 3 : 1" label-align="right">{{ XEUtils.get(processDic.timeUnit, descData.timeUnit, descData.timeUnit) }}</el-descriptions-item>
                         <el-descriptions-item label="版本号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.version }}</el-descriptions-item>
                     </el-descriptions>
@@ -90,4 +92,5 @@ defineExpose({
 .el-collapse-item :deep(.el-collapse-item__header.is-active) {border-bottom: 1px solid var(--el-border-color-lighter);}
 .el-collapse-item :deep(.el-collapse-item__wrap) {border: none;}
 .el-collapse-item :deep(.el-collapse-item__content) {padding: 20px 0;}
+.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions__content {min-width: 120px;}
 </style>

+ 23 - 12
src/views/process/line/detail.vue

@@ -6,7 +6,7 @@
                     <el-row>
                         <el-col v-if="$store.state.tenant.tenantId === '0'" :md="8" :xs="24">
                             <el-form-item label="所属租户" prop="tenantId">
-                                <el-select v-if="!form.id" v-model="form.tenantId" filterable placeholder="请选择所属租户">
+                                <el-select v-if="!form.id" v-model="form.tenantId" filterable placeholder="请选择所属租户" @change="form.inspectProgramId = null">
                                     <el-option v-for="item in $store.state.tenant.tenants" :key="item.id" :label="item.name" :value="item.id"></el-option>
                                 </el-select>
                                 <el-input v-else v-model="tenantName" readonly></el-input>
@@ -29,6 +29,13 @@
                                 </el-radio-group>
                             </el-form-item>
                         </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="质检方案">
+                                <el-select v-model="form.inspectProgramId" filterable clearable placeholder="请选择质检方案">
+                                    <el-option v-for="item in XEUtils.filter(plans, item => item.tenantId == form.tenantId)" :key="item.id" :label="item.name" :value="item.id"></el-option>
+                                </el-select>
+                            </el-form-item>
+                        </el-col>
                         <el-col :md="8" :xs="24">
                             <el-form-item label="版本号" prop="version">
                                 <el-input v-model="form.version" :readonly="mode == 'edit'" placeholder="v1.0.0"></el-input>
@@ -38,13 +45,9 @@
                 </el-collapse-item>
 
                 <el-collapse-item title="加工路线" name="line">
-                    <sc-form-table ref="formTableRef" v-model="form.detailList" v-bind="tableOptions"></sc-form-table>
+                    <sc-form-table ref="formTableRef" v-model="form.detailList" v-bind="tableOptions" @editActivated="editActivated"></sc-form-table>
                 </el-collapse-item>
 
-                <!-- <el-collapse-item title="质检方案" name="plan">
-                    inspectProgramId
-                </el-collapse-item> -->
-
                 <el-collapse-item title="其他说明" name="other">
                     <el-row>
                         <el-col :xs="24">
@@ -100,6 +103,8 @@ const titleMap = reactive({
     upgrade: "发布新版本"
 });
 
+const plans = ref([]);
+const devices = ref([]);
 provide("tenantId", computed(() => form.value.tenantId));
 const tenantName = computed(() => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == form.value.tenantId), "name"));
 const oldData = reactive({
@@ -113,6 +118,7 @@ const form = ref({
     tenantId: store.state.tenant.tenantId,
     name: null,
     code: null,
+    inspectProgramId: null,
     timeUnit: "minute",
     version: "v1.0.0",
     detailList: [],
@@ -148,13 +154,14 @@ const setData = (data, modeKey = "edit") => {
                 XEUtils.set(oldData, key, keyValue);
                 XEUtils.set(form.value, key, key == "fileList" ? [] : null);
             } else XEUtils.set(form.value, key, keyValue);
-        } else if (key == "detailList") XEUtils.set(form.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item.stage, ...XEUtils.omit(item, "id", "stage") })));
+        } else if (key == "detailList") XEUtils.set(form.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item.stage, ...XEUtils.omit(item, "id", "stage"), deviceList: item.deviceList ? XEUtils.toStringJSON(item.deviceList) : null })));
         else XEUtils.set(form.value, key, XEUtils.get(data, key));
     });
 }
 
 const formRef = ref();
 const formTableRef = ref();
+const editActivated = ({ column }) => column.field == "deviceList" && (column.editRender.options = XEUtils.filter(devices.value, item => item.tenantId == form.value.tenantId));
 const submit = () => {
     formRef.value.validate(async valid => {
         if (valid) {
@@ -162,13 +169,13 @@ const submit = () => {
             
             if (await formTableRef.value.validateFormTable()) {
                 const data = XEUtils.omit(form.value, "detailList", "fileList");
-                const detailList = XEUtils.map(form.value.detailList, item => XEUtils.omit(item, "id", "name", "code", "processType"));
+                const detailList = XEUtils.map(form.value.detailList, item => ({ ...XEUtils.omit(item, "id", "name", "code", "processType"), deviceList: item.deviceList ? XEUtils.toJSONString(item.deviceList) : null }));
                 XEUtils.set(data, "detailList", detailList);
                 
                 let fileList = XEUtils.map(XEUtils.filter(form.value.fileList, item => !item.id), item => ({ ...XEUtils.omit(item, "id", "name"), fileName: item.name, fileType: "processLineAttach" }));
                 if (mode.value == "upgrade") fileList = fileList.concat(XEUtils.map(oldData.fileList, item => ({ ...XEUtils.omit(item, "id", "name"), fileName: item.name, fileType: "processLineAttach" })));
                 fileList.length > 0 && XEUtils.set(data, "fileList", fileList);
-
+                
                 isSaving.value = true;
                 API.process.line[mode.value](data).then(res => {
                     ElMessage.success("操作成功");
@@ -184,9 +191,13 @@ const submit = () => {
     });
 }
 
-const removeSuccess = () => {
-    if (form.value.id) isDel.value = true;
-}
+const removeSuccess = () => form.value.id && (isDel.value = true);
+
+const fetchQualityPlan = () => API.basic.qualityPlan.all({ orderBy: "createTime_desc", reviewStatus: "approved" }).then(res => plans.value = res).catch(() => plans.value = []);
+fetchQualityPlan();
+
+const fetchDevice = () => API.equip.device.all({ orderBy: "createTime_desc" }).then(res => devices.value = res).catch(() => devices.value = []);
+fetchDevice();
 
 defineExpose({
     open,

+ 13 - 3
src/views/process/line/index.vue

@@ -62,6 +62,14 @@ const props = defineProps({
     options: { type: Object, default: () => ({}) }
 });
 
+const qualityPlanConfig = reactive({
+    api: { key: "basic.qualityPlan", query: { orderBy: "code_desc", reviewStatus: "approved" } },
+    optionProps: { label: "name", value: "id" },
+    events: {
+        change: data => XEUtils.merge(formConfig.data, data)
+    }
+});
+
 const selectConfig = reactive({
     visible: !props.hideHandler,
     options: statusDic,
@@ -88,20 +96,22 @@ const formConfig = reactive({
         mapFormItemSelect("status", "工艺路线状态", selectConfig),
         mapFormItemDatePicker("createTime", "创建日期", daterangeConfig)
     ] : [
-        mapFormItemTenant({ events: { change: data => XEUtils.merge(formConfig.data, data) } }),
+        mapFormItemTenant({ events: { change: data => XEUtils.merge(formConfig.data, { ...data, inspectProgramId: undefined }) } }),
         mapFormItemInput("nameLike", "工艺路线名称"),
         mapFormItemInput("codeLike", "工艺路线编号"),
+        mapFormItemSelect("inspectProgramId", "质检方案", qualityPlanConfig),
         mapFormItemSelect("status", "工艺路线状态", selectConfig),
         mapFormItemDatePicker("createTime", "创建日期", daterangeConfig)
     ])
 });
 
 const paramsColums = reactive([
-    { column: "orderBy", defaultValue: "code_asc" },
+    { column: "orderBy", defaultValue: "createTime_desc" },
     { column: "parentId", defaultValue: "0" },
     { column: "tenantId" },
     { column: "nameLike" },
     { column: "codeLike" },
+    { column: "inspectProgramId" },
     { column: "status" },
     { column: "createTimeBegin", field: "createTime[0]" },
     { column: "createTimeEnd", field: "createTime[1]" },
@@ -115,7 +125,7 @@ const columns = reactive([
     { field: "code", title: "工艺路线编号", fixed: "left", minWidth: 150, sortable: true, className: "vxe-table-link-cell", slots: { default: "code_link" } },
     { visible: !props.hideHandler, field: "status", title: "工艺路线状态", minWidth: 120, editRender: { name: "$cell-tag", options: statusDic } },
     { visible: false, type: "html", field: "timeUnit", title: "时间单位", minWidth: 100, sortable: true, formatter: ({ cellValue }) => XEUtils.get(processDic.timeUnit, cellValue, cellValue) },
-    { type: "html", field: "inspectProgramName", title: "质检方案", minWidth: 160, sortable: true },
+    { type: "html", field: "inspectProgram.name", title: "质检方案", minWidth: 160, sortable: true },
     { type: "html", field: "createTime", title: "创建日期", minWidth: 120, sortable: true, formatter: ({ cellValue }) => TOOL.dateFormat(cellValue, "YYYY-MM-DD") || cellValue },
     { type: "html", field: "version", title: "版本号", minWidth: 120, sortable: true, className: "vxe-table-link-cell", slots: { default: "version_link" } },
     { visible: false, type: "html", field: "remark", title: "概要", minWidth: 300, sortable: true },

+ 12 - 7
src/views/process/line/main.js

@@ -1,6 +1,6 @@
-import XEUtils from "xe-utils";
-import { mapFormItemInput } from "@/components/scTable/helper";
-import { processDic } from "@/utils/basicDic";
+import XEUtils from "xe-utils"
+import { mapFormItemInput } from "@/components/scTable/helper"
+import { processDic } from "@/utils/basicDic"
 
 export const tableOptions = reactive({
     tableKey: "stage",
@@ -11,9 +11,14 @@ export const tableOptions = reactive({
         { field: "name", title: "加工工序", fixed: "left", minWidth: 150 },
         { field: "code", title: "工序编号", fixed: "left", minWidth: 150 },
         { field: "processType", title: "加工类型", fixed: "left", minWidth: 140, formatter: ({ cellValue }) => XEUtils.get(processDic.type, cellValue, cellValue) },
-        // { field: "", title: "工作中心", minWidth: 150 }, // select 车间
+        { field: "deviceList", title: "工序设备", fixed: "left", minWidth: 150, editRender: { name: "VxeSelect", props: { filterable: true, multiple: true, multiCharOverflow: 1, clearable: true }, optionProps: { label: "name", value: "id" } }, formatter: ({ cellValue, row, column }) => 
+            { 
+            console.log('cellValue',cellValue)
+                return cellValue && cellValue.length ? XEUtils.map(cellValue, id => XEUtils.get(XEUtils.find(column.editRender.options, item => item.id == id), "name")).join() || "测试" : ""
+            }
+        },
         { field: "readyTimeHour", title: "准备时间", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 0, type: "float", controlConfig: { enabled: false } }, defaultValue: 0 } },
-        { title:  "定额工时", headerAlign: "center",
+        { title: "定额工时", headerAlign: "center",
             children: [
                 { field: "processNum", title: "加工批量", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 0, controlConfig: { enabled: false } }, defaultValue: 1 } },
                 { field: "processTimeHour", minWidth: 100, title: "加工工时", editRender: { name: "VxeNumberInput", props: { min: 0, type: "float", controlConfig: { enabled: false } }, defaultValue: 1 } },
@@ -24,7 +29,7 @@ export const tableOptions = reactive({
         { field: "isReport", title: "是否汇报", minWidth: 100, cellRender: { name: "VxeCheckbox" } },
         { field: "isRound", title: "汇报取整", minWidth: 100, cellRender: { name: "VxeCheckbox" } },
         { field: "isReview", title: "汇报审核", minWidth: 100, cellRender: { name: "VxeCheckbox" } },
-        { field: "remark", title: "备注", minWidth: 200, editRender: { name: "ElInput", props: { type: "textarea", autosize: true } } }
+        { field: "remark", title: "备注", minWidth: 200, editRender: { name: "VxeInput", props: { clearable: true, placeholder: "" } } }
     ],
     editRules: {
         orderNum: [{ required: true, message: "必须填写" }],
@@ -35,7 +40,7 @@ export const tableOptions = reactive({
         moveTimeHour: [{ required: true, message: "必须填写" }]
     },
     footerField: [["readyTimeHour", "processNum", "processTimeHour", "moveNum", "moveTimeHour"]],
-    mergeFooterItems: [{ row: 0, col: 0, rowspan: 1, colspan: 5 }],
+    mergeFooterItems: [{ row: 0, col: 0, rowspan: 1, colspan: 6 }],
 
     selectOptions: {
         paramsColums: [{ column: "status", defaultValue: "enable" }]

+ 1 - 3
src/views/process/stage/detail.vue

@@ -154,9 +154,7 @@ const submit = () => {
     });
 }
 
-const removeSuccess = () => {
-    if (form.value.id) isDel.value = true;
-}
+const removeSuccess = () => form.value.id && (isDel.value = true);
 
 defineExpose({
     open,

+ 1 - 1
src/views/process/stage/index.vue

@@ -76,7 +76,7 @@ const formConfig = reactive({
 });
 
 const paramsColums = reactive([
-    { column: "orderBy", defaultValue: "code_asc" },
+    { column: "orderBy", defaultValue: "createTime_desc" },
     { column: "tenantId" },
     { column: "nameLike" },
     { column: "codeLike" },

+ 12 - 9
src/views/production/bom/desc.vue

@@ -3,21 +3,23 @@
         <el-main>
             <el-collapse v-model="activeNames">
                 <el-collapse-item title="基本信息" name="basic">
+                    <el-descriptions v-if="$store.state.tenant.tenantId === '0'" :column="1" label-width="140" border>
+                        <el-descriptions-item label="所属租户" label-align="right">{{ tenantName }}</el-descriptions-item>
+                    </el-descriptions>
                     <el-descriptions :column="3" label-width="140" border>
-                        <el-descriptions-item v-if="$store.state.tenant.tenantId === '0'" label="所属租户" :span="ismobile ? 3 : 1" label-align="right" min-width="120">{{ tenantName }}</el-descriptions-item>
-                        <el-descriptions-item label="BOM单编号" :span="ismobile ? 3 : 1" label-align="right" min-width="120">{{ descData.bomCode }}</el-descriptions-item>
-                        <el-descriptions-item label="BOM单状态" :span="ismobile ? 3 : 1" label-align="right" min-width="120">{{ XEUtils.get(statusDic, descData.status, descData.status) }}</el-descriptions-item>
-                        <el-descriptions-item label="添加时间" :span="ismobile ? 3 : 1" label-align="right" min-width="120">{{ descData.createTime }}</el-descriptions-item>
+                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="BOM单编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.bomCode }}</el-descriptions-item>
+                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="BOM单状态" :span="ismobile ? 3 : 1" label-align="right">{{ XEUtils.get(statusDic, descData.status, descData.status) }}</el-descriptions-item>
+                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="添加时间" :span="ismobile ? 3 : 1" label-align="right">{{ descData.createTime }}</el-descriptions-item>
                         <el-descriptions-item label="概要" :span="3" label-align="right">{{ descData.remark }}</el-descriptions-item>
                     </el-descriptions>
                 </el-collapse-item>
 
                 <el-collapse-item title="产品信息" name="material">
                     <el-descriptions :column="3" label-width="140" border>
-                        <el-descriptions-item label="产品编号" :span="ismobile ? 3 : 1" label-align="right" min-width="120">{{ descData.material.code }}</el-descriptions-item>
-                        <el-descriptions-item label="产品名称" :span="ismobile ? 3 : 1" label-align="right" min-width="120">{{ descData.material.name }}</el-descriptions-item>
-                        <el-descriptions-item label="规格型号" :span="ismobile ? 3 : 1" label-align="right" min-width="120">{{ descData.material.specification }}</el-descriptions-item>
-                        <el-descriptions-item label="单位" :span="3" label-align="right" min-width="120">{{ descData.material.unit }}</el-descriptions-item>
+                        <el-descriptions-item label="产品编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.material.code }}</el-descriptions-item>
+                        <el-descriptions-item label="产品名称" :span="ismobile ? 3 : 1" label-align="right">{{ descData.material.name }}</el-descriptions-item>
+                        <el-descriptions-item label="规格型号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.material.specification }}</el-descriptions-item>
+                        <el-descriptions-item label="单位" :span="3" label-align="right">{{ descData.material.unit }}</el-descriptions-item>
                     </el-descriptions>
                 </el-collapse-item>
 
@@ -61,7 +63,7 @@ const descData = ref({
 });
 
 const setData = async data => {
-    const res = await API.production.bom.getChild({ id: data.id });
+    const res = await API.production.bom.getChild({ parentId: data.id });
     XEUtils.objectEach(descData.value, (_, key) => {
         if (key == "childrenList") XEUtils.set(descData.value, key, XEUtils.map(res, item => ({ ...item.material, ...XEUtils.omit(item, "id", "material") })));
         else XEUtils.set(descData.value, key, XEUtils.get(data, key));
@@ -84,4 +86,5 @@ defineExpose({
 .el-collapse-item :deep(.el-collapse-item__header.is-active) {border-bottom: 1px solid var(--el-border-color-lighter);}
 .el-collapse-item :deep(.el-collapse-item__wrap) {border: none;}
 .el-collapse-item :deep(.el-collapse-item__content) {padding: 20px 0;}
+.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions__content {min-width: 120px;}
 </style>

+ 29 - 8
src/views/production/bom/detail.vue

@@ -6,7 +6,7 @@
                     <el-row>
                         <el-col v-if="$store.state.tenant.tenantId === '0'" :md="8" :xs="24">
                             <el-form-item label="所属租户" prop="tenantId">
-                                <el-select v-if="!form.id" v-model="form.tenantId" filterable placeholder="请选择所属租户">
+                                <el-select v-if="!form.id" v-model="form.tenantId" filterable placeholder="请选择所属租户" @change="form.routeId = null">
                                     <el-option v-for="item in $store.state.tenant.tenants" :key="item.id" :label="item.name" :value="item.id"></el-option>
                                 </el-select>
                                 <el-input v-else v-model="tenantName" readonly></el-input>
@@ -49,6 +49,13 @@
                                 <el-input v-model="form.material.unit" readonly placeholder="选择产品"></el-input>
                             </el-form-item>
                         </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="工艺路线" prop="routeId">
+                                <el-select v-model="form.routeId" filterable clearable placeholder="请选择工艺路线">
+                                    <el-option v-for="item in XEUtils.filter(lines, item => item.tenantId == form.tenantId)" :key="item.id" :label="item.name" :value="item.id"></el-option>
+                                </el-select>
+                            </el-form-item>
+                        </el-col>
                         <el-col v-if="form.parentId != 0" :md="16" :xs="24">
                             <el-form-item label="备注">
                                 <el-input v-model="form.remark" type="textarea" maxlength="200" :rows="1" placeholder="请输入内容"></el-input>
@@ -58,7 +65,7 @@
                 </el-collapse-item>
 
                 <el-collapse-item title="子件信息" name="children">
-                    <sc-form-table ref="formTableRef" v-model="form.childrenList" v-bind="tableOptions"></sc-form-table>
+                    <sc-form-table ref="formTableRef" v-model="form.childrenList" v-bind="tableOptions" @editActivated="editActivated"></sc-form-table>
                 </el-collapse-item>
             </el-collapse>
         </el-form>
@@ -90,6 +97,7 @@ const titleMap = reactive({
     edit: "修改BOM单"
 });
 
+const lines = ref([]);
 provide("tenantId", computed(() => form.value.tenantId));
 const tenantName = computed(() => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == form.value.tenantId), "name"));
 const form = ref({
@@ -100,20 +108,28 @@ const form = ref({
     material: {
         code: null,
         name: null,
+        needType: null,
         specification: null,
         unit: null
     },
+    routeId: null,
     childrenList: [],
     remark: null
 });
 const rules = reactive({
     tenantId: [{ required: true, message: "请选择所属租户" }],
-    "material.code": [{ required: true, message: "请选择产品" }]
+    "material.code": [{ required: true, message: "请选择产品" }],
+    routeId: [{ required: computed(() => form.value.material.needType == "self_made"),
+        validator: (rule, value, callback) => {
+            if (form.value.material.needType == "self_made" && !value) return callback(new Error("请选择工艺路线"));
+            callback();
+        }
+    }]
 });
 
 const setData = async (data = {}, model = "add") => {
     if (data.id) {
-        const res = await API.production.bom.getChild({ id: data.id });
+        const res = await API.production.bom.getChild({ parentId: data.id });
         XEUtils.objectEach(form.value, (_, key) => {
             if (key == "childrenList") XEUtils.set(form.value, key, XEUtils.map(res, item => ({ ...item.material, childrenId: item.id, ...XEUtils.omit(item, "id", "material") })));
             else XEUtils.set(form.value, key, XEUtils.get(data, key));
@@ -126,19 +142,21 @@ const setData = async (data = {}, model = "add") => {
 
 const formRef = ref();
 const formTableRef = ref();
+const editActivated = ({ column }) => column.field == "routeId" && (column.editRender.options = XEUtils.filter(lines.value, item => item.tenantId == form.value.tenantId));
 const submit = () => {
     formRef.value.validate(async valid => {
         if (valid) {
-            if (form.value.parentId == 0 && !form.value.childrenList.length) return ElMessage.warning("请添加子件信息后再保存");
+            if (form.value.parentId === "0" && !form.value.childrenList.length) return ElMessage.warning("请添加子件信息后再保存");
 
             if (await formTableRef.value.validateFormTable()) {
                 const data = XEUtils.omit(form.value, "material", "childrenList");
-                const childrenList = XEUtils.map(form.value.childrenList, item => ({ id: XEUtils.get(item, "childrenId", null), emptyField: item.remark ? []: ["remark"], materialCode: item.code, materialName: item.name, ...XEUtils.pick(item, "tenantId", "quantity", "remark") }));
+                const childrenList = XEUtils.map(form.value.childrenList, item => ({ id: XEUtils.get(item, "childrenId", null), emptyField: XEUtils.filter(["routeId", "remark"], key => !item[key]), materialCode: item.code, materialName: item.name, ...XEUtils.pick(item, "tenantId", "quantity", "routeId", "remark") }));
                 XEUtils.set(data, "materialName", form.value.material.name);
                 XEUtils.set(data, "materialCode", form.value.material.code);
                 XEUtils.set(data, "childrenList", childrenList);
-                form.value.parentId !== 0 && mode.value == "add" && (data.remark === "" || XEUtils.isNull(data.remark) || XEUtils.isUndefined(data.remark)) && XEUtils.set(data, "emptyField", ["remark"]);
-
+                form.value.parentId === "0" && mode.value == "add" && XEUtils.set(data, "quantity", 1);
+                form.value.parentId !== "0" && mode.value == "add" && (data.remark === "" || XEUtils.isNull(data.remark) || XEUtils.isUndefined(data.remark)) && XEUtils.set(data, "emptyField", ["remark"]);
+                
                 isSaving.value = true;
                 API.production.bom[mode.value](data).then(res => {
                     ElMessage.success("操作成功");
@@ -153,6 +171,9 @@ const submit = () => {
     });
 }
 
+const fetchProcessLine = () => API.process.line.all({ orderBy: "createTime_desc", parentId: "0", status: "enable" }).then(res => lines.value = res).catch(() => lines.value = []);
+fetchProcessLine();
+
 defineExpose({
     setData
 });

+ 56 - 16
src/views/production/bom/index.vue

@@ -1,10 +1,10 @@
 <template>
 	<el-container class="is-vertical">
-        <sc-page-header @add="table_add()"></sc-page-header>
+        <sc-page-header v-if="!selectable" @add="table_add()"></sc-page-header>
         
-        <scTable ref="xGridTable" :apiObj="$API.production.bom" :formConfig="formConfig" :paramsColums="paramsColums" :columns="columns" :treeConfig="treeConfig">
+        <scTable ref="xGridTable" :apiObj="$API.production.bom" :formConfig="formConfig" :paramsColums="paramsColums" :columns="columns" :treeConfig="treeConfig" v-bind="options" v-on="tableEvents">
             <template #code_link="{ row }">
-                <vxe-text status="primary" @click="table_detail(row)">{{ row.bomCode }}</vxe-text>
+                <vxe-text v-if="row.parentId === '0'" status="primary" @click="table_detail(row)">{{ row.bomCode }}</vxe-text>
             </template>
 
             <template #action="{ row }">
@@ -49,11 +49,28 @@ import bomDesc from "./desc";
 import store from "@/store";
 watch(() => store.state.tenant.tenantId, () => refreshTable());
 
+const props = defineProps({
+    selectable: { type: Boolean, default: false },
+    multiple: { type: Boolean, default: false },
+    paramsColums: { type: Array, default: () => [] },
+    options: { type: Object, default: () => ({}) }
+});
+
+const formQueryData = reactive({});
+
 const treeConfig = reactive({
     lazy: true,
     hasChildField: "isHaveChildren",
-    loadMethod: ({ row }) => API.production.bom.getChild({ id: row.id })
-})
+    loadMethod: ({ row }) => API.production.bom.getChild({ parentId: row.id, ...XEUtils.pick(formQueryData, (_, key) => XEUtils.includes(["materialCodeLike", "materialNameLike", "routeId"], key)) })
+});
+
+const routeConfig = reactive({
+    api: { key: "process.line", query: { orderBy: "createTime_desc", parentId: "0", status: "enable" } },
+    optionProps: { label: "name", value: "id" },
+    events: {
+        change: data => XEUtils.merge(formConfig.data, data)
+    }
+});
 
 const selectConfig = reactive({
     options: statusDic,
@@ -74,35 +91,49 @@ const daterangeConfig = reactive({
 
 const formConfig = reactive({
     data: {},
-    items: [
-        mapFormItemTenant({ events: { change: data => XEUtils.merge(formConfig.data, data) } }),
-        mapFormItemInput("bomCodeLike", "BOM单编号"),
+    items: computed(() => props.selectable ? [
+        mapFormItemInput("materialCodeLike", "产品编码"),
+        mapFormItemInput("materialNameLike", "产品名称")
+    ]: [
+        mapFormItemTenant({ events: { change: data => XEUtils.merge(formConfig.data, { ...data, routeId: undefined }) } }),
         mapFormItemInput("materialCodeLike", "产品编码"),
         mapFormItemInput("materialNameLike", "产品名称"),
+        mapFormItemInput("bomCodeLike", "BOM单编号"),
         mapFormItemSelect("status", "BOM单状态", selectConfig),
+        mapFormItemSelect("routeId", "工艺路线", routeConfig),
         mapFormItemDatePicker("createTime", "创建日期", daterangeConfig)
-    ]
+    ])
 });
 
 const paramsColums = reactive([
-    { column: "orderBy", defaultValue: "bomCode_asc" },
+    { column: "orderBy", defaultValue: "createTime_desc" },
     { column: "parentId", defaultValue: "0" },
     { column: "tenantId" },
-    { column: "bomCodeLike" },
     { column: "materialCodeLike" },
     { column: "materialNameLike" },
+    { column: "bomCodeLike" },
     { column: "status" },
+    { column: "routeId" },
     { column: "createTimeBegin", field: "createTime[0]" },
-    { column: "createTimeEnd", field: "createTime[1]" }
+    { column: "createTimeEnd", field: "createTime[1]" },
+    ...props.paramsColums
 ]);
 
-const columns = reactive([
+const columns = computed(() => props.selectable ? [
+    { type: props.multiple && "checkbox" || "radio", fixed: "left", width: 40 },
+    { type: "html", field: "materialCode", title: "产品编码", fixed: "left", minWidth: 200, treeNode: true, sortable: true },
+    { type: "html", field: "materialName", title: "产品名称", fixed: "left", minWidth: 150, sortable: true },
+    { field: "bomCode", title: "BOM单编号", minWidth: 150, sortable: true },
+    { type: "html", field: "quantity", title: "标准用量", minWidth: 100, sortable: true },
+    { type: "html", field: "material.unit", title: "单位", minWidth: 100, sortable: true }
+] : [
     { type: "seq", fixed: "left", width: 80 },
-    { visible: computed(() => store.state.tenant.tenantId === "0"), type: "html", field: "tenantName", title: "所属租户", fixed: "left", minWidth: 200, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == row.tenantId), "name") },
-    { type: "html", field: "materialCode", title: "产品编码", fixed: "left", minWidth: 150, treeNode: true, headerAlign: "center", align: "left", sortable: true },
+    { visible: computed(() => store.state.tenant.tenantId === "0"), type: "html", field: "tenantName", title: "所属租户", fixed: "left", minWidth: 200, sortable: true, formatter: ({ cellValue, row }) => row.parentId === "0" ? cellValue || XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == row.tenantId), "name") : "" },
+    { type: "html", field: "materialCode", title: "产品编码", fixed: "left", minWidth: 200, treeNode: true, headerAlign: "center", align: "left", sortable: true },
     { type: "html", field: "materialName", title: "产品名称", fixed: "left", minWidth: 150, sortable: true },
     { field: "bomCode", title: "BOM单编号", minWidth: 150, sortable: true, className: "vxe-table-link-cell", slots: { default: "code_link" } },
     { field: "status", title: "BOM单状态", minWidth: 120, editRender: { name: "$cell-tag", options: statusDic } },
+    { visible: true, type: "html", field: "routeName", title: "工艺路线", minWidth: 150, sortable: true },
     { type: "html", field: "createTime", title: "创建日期", minWidth: 120, sortable: true, formatter: ({ cellValue }) => TOOL.dateFormat(cellValue, "YYYY-MM-DD") || cellValue },
     { visible: false, type: "html", field: "remark", title: "概要", minWidth: 300, sortable: true },
     { title: "操作", fixed: "right", width: 220, slots: { default: "action" } }
@@ -110,7 +141,7 @@ const columns = reactive([
 
 // 显示隐藏 筛选表单
 const xGridTable = ref();
-const refreshTable = (mode = "add") => (xGridTable.value.searchData(mode), xGridTable.value.reloadColumn(columns));
+const refreshTable = (mode = "add") => (xGridTable.value.searchData(mode), xGridTable.value.reloadColumn(columns.value));
 
 const bomRef = ref();
 const bomDescRef = ref();
@@ -162,4 +193,13 @@ const table_change = row => {
         });
     }).catch(() => {});
 }
+
+const tableEvents = {
+    tableSearch: data => (XEUtils.clear(formQueryData), XEUtils.merge(formQueryData, data)),
+    tableReset: data => (XEUtils.clear(formQueryData), XEUtils.merge(formQueryData, data))
+}
+
+defineExpose({
+    getSelectRows: () => xGridTable.value.getSelectRows()
+});
 </script>

+ 9 - 7
src/views/production/bom/main.js

@@ -1,6 +1,6 @@
-import XEUtils from "xe-utils";
-import { materialDic } from "@/utils/basicDic";
-import { mapFormItemInput } from "@/components/scTable/helper";
+import XEUtils from "xe-utils"
+import { materialDic } from "@/utils/basicDic"
+import { mapFormItemInput } from "@/components/scTable/helper"
 
 export const tableOptions = reactive({
     tableKey: "material",
@@ -11,11 +11,13 @@ export const tableOptions = reactive({
         { field: "name", title: "物料名称", fixed: "left", minWidth: 150 },
         { field: "specification", title: "规格型号", minWidth: 150 },
         { field: "unit", title: "单位", minWidth: 150 },
-        { field: "quantity", title: "材料用量", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 1, controlConfig: { enabled: false } }, defaultValue: 1 } },
-        { field: "remark", title: "备注", minWidth: 200, editRender: { name: "ElInput", props: { type: "textarea", autosize: true } } }
+        { field: "quantity", title: "标准用量", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 1, controlConfig: { enabled: false } }, defaultValue: 1 } },
+        { field: "routeId", title: "质检方案", minWidth: 150, editRender: { name: "VxeSelect", props: { filterable: true, clearable: true }, optionProps: { label: "name", value: "id" } }, formatter: ({ cellValue, row, column }) => cellValue ? XEUtils.get(XEUtils.find(column.editRender.options, item => item.id == cellValue), "name", row.routeName) : "" },
+        { field: "remark", title: "备注", minWidth: 200, editRender: { name: "VxeInput", props: { clearable: true, placeholder: "" } } }
     ],
     editRules: {
-        quantity: [{ required: true, message: "必须填写" }]
+        quantity: [{ required: true, message: "必须填写" }],
+        routeId: [{ validator: ({ cellValue, row }) => row.needType == "self_made" && !cellValue && new Error("必须填写") }]
     },
     footerField: [["quantity"]],
     mergeFooterItems: [{ row: 0, col: 0, rowspan: 1, colspan: 5 }],
@@ -27,7 +29,7 @@ export const tableOptions = reactive({
         ]
     },
 
-    add_success: (oldValue, newValue) => XEUtils.map(newValue, (item, index) => XEUtils.pick(item, "id", "tenantId", "code", "name", "specification", "unit"))
+    add_success: (oldValue, newValue) => XEUtils.map(newValue, (item, index) => XEUtils.pick(item, "id", "tenantId", "code", "name", "needType", "specification", "unit"))
 })
 
 export const selectOptions = reactive({

+ 56 - 0
src/views/production/order/index.vue

@@ -0,0 +1,56 @@
+<template>
+	<el-container class="is-vertical">
+        <sc-page-header @add="table_add"></sc-page-header>
+
+        <scTable ref="xGridTable" :formConfig="formConfig" :columns="columns" :options="options">
+            <template #action>
+                <el-button type="primary" link>
+                    <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>修改
+                </el-button>
+                <el-button type="primary" link>
+                    <template #icon><sc-iconify icon="ant-design:delete-outlined"></sc-iconify></template>删除
+                </el-button>
+            </template>
+        </scTable>
+	</el-container>
+</template>
+
+<script setup>
+import XEUtils from "xe-utils";
+import { mapFormItemInput } from "@/components/scTable/helper";
+
+const formConfig = reactive({
+    data: {},
+    items: [
+        mapFormItemInput("name", "Name"),
+        mapFormItemInput("role", "Role"),
+        mapFormItemInput("sex", "Sex"),
+        mapFormItemInput("num", "Num")
+    ]
+});
+
+const columns = reactive([
+    { type: "seq", fixed: "left", width: 60 },
+    { type: "html", field: "name", title: "Name", sortable: true },
+    { type: "html", field: "role", title: "Role", sortable: true },
+    { type: "html", field: "sex", title: "Sex", sortable: true },
+    { type: "html", field: "num", title: "Num", sortable: true },
+    { type: "html", field: "address", title: "Address", sortable: true },
+    { title: "操作", fixed: "right", width: 160, slots: { default: "action" } }
+]);
+
+const options = reactive({
+    data: [
+        { id: '10001', name: 'Test1', role: 'Develop', sex: 'Man', num: '28', address: 'test abc' },
+        { id: '10002', name: 'Test2', role: 'Test', sex: 'Women', num: '22', address: 'Guangzhou' },
+        { id: '10003', name: 'Test3', role: 'PM', sex: 'Man', num: '32', address: 'Shanghai' },
+        { id: '10004', name: 'Test4', role: 'Designer', sex: 'Women', num: '24', address: 'Shanghai' },
+        { id: '10005', name: 'Test5', role: 'Develop', sex: 'Man', num: '42', address: 'Guangzhou' },
+        { id: '10006', name: 'Test6', role: 'Test', sex: 'Women', num: '39', address: 'Shanghai' },
+        { id: '10007', name: 'Test7', role: 'Develop', sex: 'Man', num: '46', address: 'Shanghai' },
+        { id: '10008', name: 'Test8', role: 'PM', sex: 'Women', num: '49', address: 'Guangzhou' }
+    ]
+});
+
+const table_add = () => {};
+</script>

+ 104 - 0
src/views/production/plan/desc.vue

@@ -0,0 +1,104 @@
+<template>
+    <el-dialog v-model="visible" title="销售订单详情" fullscreen :close-on-click-modal="false" @closed="$emit('closed')">
+        <el-main>
+            <el-collapse v-model="activeNames">
+                <el-collapse-item title="基本信息" name="basic">
+                    <el-descriptions v-if="$store.state.tenant.tenantId === '0'" :column="1" label-width="140" border>
+                        <el-descriptions-item label="所属租户" label-align="right">{{ tenantName }}</el-descriptions-item>
+                    </el-descriptions>
+                    <el-descriptions :column="3" label-width="140" border>
+                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="单据编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.code }}</el-descriptions-item>
+                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="合同编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.contractNo }}</el-descriptions-item>
+                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="单据日期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.orderDate }}</el-descriptions-item>
+                        <el-descriptions-item label="单据状态" :span="ismobile ? 3 : 1" label-align="right">{{ XEUtils.get(salesDic.orderStatus, descData.status, descData.status) }}</el-descriptions-item>
+                        <el-descriptions-item label="客户名称" :span="ismobile ? 3 : 1" label-align="right">{{ descData.customerName }}</el-descriptions-item>
+                        <el-descriptions-item label="添加时间" :span="ismobile ? 3 : 1" label-align="right">{{ descData.createTime }}</el-descriptions-item>
+                        <el-descriptions-item label="预计交期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.planReceiveDate }}</el-descriptions-item>
+                        <el-descriptions-item label="实际交期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.deliveryDate }}</el-descriptions-item>
+                        <el-descriptions-item label="业务员" :span="ismobile ? 3 : 1" label-align="right">{{ descData.managerName }}</el-descriptions-item>
+                        <el-descriptions-item label="收货日期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.actualReceiveDate }}</el-descriptions-item>
+                        <el-descriptions-item label="客户收货地址" label-align="right" :span="ismobile ? 3 : 1">{{ descData.deliveryAddress }}</el-descriptions-item>
+                    </el-descriptions>
+                    <el-descriptions :column="1" label-width="140" border>
+                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="概要" label-align="right">{{ descData.remark }}</el-descriptions-item>
+                        <el-descriptions-item label="附件" label-align="right">
+                            <sc-upload-file v-model="descData.fileList" hideAdd disabled></sc-upload-file>
+                        </el-descriptions-item>
+                    </el-descriptions>
+                </el-collapse-item>
+
+                <el-collapse-item title="产品信息" name="material">
+                    <sc-form-table v-model="descData.childrenList" v-bind="tableOptions" disabled></sc-form-table>
+                </el-collapse-item>
+
+                <el-collapse-item title="金额信息" name="amount">
+                    <el-descriptions :column="2" label-width="140" border>
+                        <el-descriptions-item label="整单折扣额" label-align="right">{{ descData.freePrice }}</el-descriptions-item>
+                        <el-descriptions-item label="成交金额" label-align="right">{{ descData.actualPrice }}</el-descriptions-item>
+                    </el-descriptions>
+                </el-collapse-item>
+            </el-collapse>
+        </el-main>
+    </el-dialog>
+</template>
+
+<script setup>
+import XEUtils from "xe-utils";
+import { salesDic } from "@/utils/basicDic";
+import { tableOptions } from "./main";
+import scUploadFile from "@/components/scUpload/file";
+
+const $emit = defineEmits(["closed"]);
+const visible = ref(false);
+
+import store from "@/store";
+const ismobile = computed(() => store.state.global.ismobile);
+const tenantName = computed(() => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == descData.value.tenantId), "name"));
+
+const activeNames = ref(["basic", "material", "amount"]);
+const descData = ref({
+    id: null,
+    tenantId: store.state.tenant.tenantId,
+    code: null,
+    orderDate: null,
+    customerName: null,
+    contractNo: null,
+    planReceiveDate: null,
+    actualReceiveDate: null,
+    deliveryDate: null,
+    managerName: null,
+    deliveryAddress: null,
+    childrenList: [],
+    freePrice: null,
+    actualPrice: null,
+    remark: null,
+    fileList: [],
+    status: "pending",
+    createTime: null
+});
+
+const setData = data => {
+    visible.value = true;
+    XEUtils.objectEach(descData.value, (_, key) => {
+        if (key == "fileList") XEUtils.set(descData.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item, name: item.fileName })));
+        else if (key == "childrenList") XEUtils.set(descData.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item.material, quantity: item.materialQuantity, price: item.materialPrice  })));
+        else XEUtils.set(descData.value, key, XEUtils.get(data, key));
+    });
+}
+
+defineExpose({
+    setData
+})
+</script>
+
+<style scoped>
+.el-main {padding-top: 0;}
+
+.el-collapse {border: none;}
+.el-collapse-item {margin-top: 15px;padding: 0 24px;background-color: var(--el-fill-color-blank);border: 1px solid var(--el-border-color-light);border-radius: 4px;color: var(--el-text-color-primary);box-shadow: var(--el-box-shadow-light);transition: var(--el-transition-duration);}
+.el-collapse-item :deep(.el-collapse-item__header) {border-bottom-color: transparent;line-height: 55px;font-size: 16px;font-weight: bold;}
+.el-collapse-item :deep(.el-collapse-item__header.is-active) {border-bottom: 1px solid var(--el-border-color-lighter);}
+.el-collapse-item :deep(.el-collapse-item__wrap) {border: none;}
+.el-collapse-item :deep(.el-collapse-item__content) {padding: 20px 0;}
+.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions__content {min-width: 120px;}
+</style>

+ 186 - 0
src/views/production/plan/detail.vue

@@ -0,0 +1,186 @@
+<template>
+    <el-dialog v-model="visible" :title="titleMap[mode]" fullscreen :close-on-click-modal="false" @closed="$emit('closed')">
+        <el-form ref="formRef" :model="form" :rules="rules" label-width="120">
+            <el-collapse v-model="activeNames">
+                <el-collapse-item title="基本信息" name="basic">
+                    <el-row>
+                        <el-col v-if="$store.state.tenant.tenantId === '0'" :md="8" :xs="24">
+                            <el-form-item label="所属租户" prop="tenantId">
+                                <el-select v-if="!form.id" v-model="form.tenantId" filterable placeholder="请选择所属租户">
+                                    <el-option v-for="item in $store.state.tenant.tenants" :key="item.id" :label="item.name" :value="item.id"></el-option>
+                                </el-select>
+                                <el-input v-else v-model="tenantName" readonly></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="计划主题" prop="name">
+                                <el-input v-model="form.name" placeholder="请输入计划主题"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="计划编号" required>
+                                <el-input v-model="form.code" :readonly="!!form.id" maxlength="50" show-word-limit clearable placeholder="不填将自动生成"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="负责人" prop="managerUserId">
+                                <el-select v-model="form.managerUserId" placeholder="请选择负责人">
+                                    <el-option v-for="item in users.filter(r => r.tenantId == form.tenantId)" :key="item.id" :label="item.nickName" :value="item.id" />
+                                </el-select>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="计划周期" prop="beginDate">
+                                <vxe-date-range-picker v-model:start-value="form.beginDate" v-model:end-value="form.endDate" value-format="yyyy-MM-dd" transfer placeholder="请选择计划周期"></vxe-date-range-picker>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+
+                <el-collapse-item title="产品信息" name="material">
+                    <sc-form-table ref="formTableRef" v-model="form.childrenList" v-bind="tableOptions">
+                        <template #bom="{ row }">
+                            <el-button type="primary" link @click="show_bom(row)">查看BOM清单</el-button>
+                        </template>
+                        <template #stock="{ row }">{{row}}
+                            <!-- <template v-if="!row.disabled">
+                                <template v-if="!row.warehouse.length">无库存</template>
+                                <template v-else>
+                                    <span>{{ formatWarehouseCount(row.warehouse, "number") }}</span>/
+                                    <span>{{ formatWarehouseCount(row.warehouse, "normalNumber") }}</span>/
+                                    <span>{{ formatWarehouseCount(row.warehouse, "lockedNumber") }}</span>
+                                    <el-button type="primary" link @click="table_stock(name, row)">查看</el-button>
+                                </template>
+                            </template> -->
+                        </template>
+                        <template #stockUse="{ row }">
+                            <el-button type="primary" link @click="table_stock(name, row, 'stockUse')">分配</el-button>
+                            <el-button type="info" link :disabled="!row.stockUseNum || row.stockUseNum == 0" @click="table_clear(row)">清空</el-button>
+                        </template>
+                    </sc-form-table>
+                </el-collapse-item>
+            </el-collapse>
+        </el-form>
+
+        <template #footer>
+            <el-button auto-insert-space @click="visible = false">取消</el-button>
+            <el-button :loading="isSaving" type="primary" auto-insert-space @click="submit">保存</el-button>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import store from "@/store";
+import { tableOptions } from "./main";
+
+const $emit = defineEmits(["success", "closed"]);
+const visible = ref(false);
+const isSaving = ref(false);
+
+const activeNames = ref(["basic", "material", "other"]);
+const mode = ref("add");
+const titleMap = reactive({
+    add: "新增生产计划",
+    edit: "修改生产计划"
+});
+
+const users = ref([]);
+provide("tenantId", computed(() => form.value.tenantId));
+const tenantName = computed(() => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == form.value.tenantId), "name"));
+const form = ref({
+    id: null,
+    tenantId: store.state.tenant.tenantId,
+    name: null,
+    code: null,
+    managerUserId: null,
+    beginDate: null,
+    endDate: null,
+    childrenList: []
+});
+const rules = reactive({
+    tenantId: [{ required: true, message: "请选择所属租户" }],
+    name: [{ required: true, message: "请输入计划主题" }],
+    managerUserId: [{ required: true, message: "请选择负责人" }],
+    beginDate: [{ required: true, message: "请选择计划周期" }],
+});
+
+const open = () => visible.value = true;
+const setData = data => {
+    open();
+    mode.value = "edit";
+    XEUtils.objectEach(form.value, (_, key) => {
+        if (key == "childrenList") XEUtils.set(form.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item.material, quantity: item.materialQuantity, price: item.materialPrice })));
+        else XEUtils.set(form.value, key, XEUtils.get(data, key));
+    });
+}
+
+const show_bom = row => {
+
+    API.production.bom.getChild({ materialCode: row.code })
+    API.production.bom.get({materialCode: row.code})
+    // const bomTree = XEUtils.toArrayTree(bomList);
+    // const tableData = XEUtils.mapTree(bomTree, item => ({ ...item, total: XEUtils.reduce(XEUtils.map(XEUtils.get(XEUtils.findTree(bomTree, b => b.id == item.id), "nodes", []), b => b.parentId === "0" ? materialQuantity : b.quantity), (p, v) => XEUtils.multiply(p, v)) }));
+
+    // drawer.value = true;
+    // nextTick(() => drawerRef.value?.setData({ tableData }));
+}
+
+
+const formRef = ref();
+const formTableRef = ref();
+const submit = () => {
+    formRef.value.validate(async valid => {
+        if (valid) {
+            // if (!form.value.childrenList.length) return ElMessage.warning("请添加产品信息后再保存");
+            
+            // if (await formTableRef.value.validateFormTable()) {
+            //     const data = XEUtils.omit(form.value, "customer", "childrenList", "fileList");
+            //     const childrenList = XEUtils.map(form.value.childrenList, item => ({ materialCode: item.code, materialQuantity: item.quantity, materialPrice: item.price }));
+            //     const fileList = XEUtils.map(XEUtils.filter(form.value.fileList, item => !item.id), item => ({ ...XEUtils.omit(item, "id", "name"), fileName: item.name, fileType: "saleOrderAttach" }));
+            //     XEUtils.set(data, "customerId", form.value.customer.id);
+            //     XEUtils.set(data, "childrenList", childrenList);
+            //     fileList.length > 0 && XEUtils.set(data, "fileList", fileList);
+
+            //     isSaving.value = true;
+            //     API.production.plan[mode.value](data).then(res => {
+            //         ElMessage.success("操作成功");
+            //         isSaving.value = false;
+            //         visible.value = false;
+            //         $emit("success", mode.value);
+            //     }).catch(() => isSaving.value = false);
+            // }
+        } else {
+            return false;
+        }
+    });
+}
+
+const fetchUser = () => API.auth.user.all({ orderBy: "id_desc" }).then(res => users.value = res).catch(() => users.value = []);
+fetchUser();
+
+defineExpose({
+    open,
+    setData
+});
+</script>
+
+<style scoped>
+.el-form {padding-left: 16px;padding-right: 22px;}
+.el-form .el-input-number {width: 100%;}
+.el-form .el-input-number :deep(.el-input__inner) {text-align: unset;}
+.el-form .vxe-date-range-picker {flex-direction: row-reverse;width: 100%;}
+.el-form .vxe-date-range-picker :deep(.vxe-date-range-picker--suffix) {border-radius: var(--vxe-ui-base-border-radius) 0 0 var(--vxe-ui-base-border-radius);}
+.el-form .vxe-date-range-picker :deep(.vxe-date-range-picker--control-icon) {padding-left: .5em;padding-right: 0;}
+
+.el-collapse {border: none;}
+.el-collapse-item {margin-top: 15px;padding: 0 24px;background-color: var(--el-fill-color-blank);border: 1px solid var(--el-border-color-light);border-radius: 4px;color: var(--el-text-color-primary);box-shadow: var(--el-box-shadow-light);transition: var(--el-transition-duration);}
+.el-collapse-item :deep(.el-collapse-item__header) {border-bottom-color: transparent;line-height: 55px;font-size: 16px;font-weight: bold;}
+.el-collapse-item :deep(.el-collapse-item__header.is-active) {border-bottom: 1px solid var(--el-border-color-lighter);}
+.el-collapse-item :deep(.el-collapse-item__wrap) {border: none;}
+.el-collapse-item :deep(.el-collapse-item__content) {padding: 20px 28px 20px 0;}
+.el-collapse-item:nth-child(2) :deep(.el-collapse-item__content) {padding-right: 0;}
+</style>

+ 131 - 0
src/views/production/plan/index.vue

@@ -0,0 +1,131 @@
+<template>
+	<el-container class="is-vertical">
+        <sc-page-header @add="table_add"></sc-page-header>
+
+        <scTable ref="xGridTable" :apiObj="$API.production.plan" :formConfig="formConfig" :paramsColums="paramsColums" :columns="columns">
+            <template #code_link="{ row }">
+                <vxe-text status="primary" @click="table_detail(row)">{{ row.code }}</vxe-text>
+            </template>
+            <template #order_link="{ row }">
+                <vxe-text status="primary" @click="table_order(row)">{{ row.saleOrder.code }}</vxe-text>
+            </template>
+
+            <template #action="{ row }">
+                <el-button type="primary" link @click="table_edit(row)">
+                    <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>修改
+                </el-button>
+                <el-button type="primary" link @click="table_del(row)">
+                    <template #icon><sc-iconify icon="ant-design:delete-outlined"></sc-iconify></template>删除
+                </el-button>
+            </template>
+        </scTable>
+	</el-container>
+
+    <plan-detail v-if="dialog.detail" ref="planRef" @success="refreshTable" @closed="dialog.detail = false"></plan-detail>
+    <plan-desc v-if="dialog.desc" ref="planDescRef" @closed="dialog.desc = false"></plan-desc>
+    <order-desc v-if="dialog.orderDesc" ref="orderDescRef" @closed="dialog.orderDesc = false"></order-desc>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import TOOL from "@/utils/tool";
+import { salesDic } from "@/utils/basicDic";
+import { mapFormItemInput, mapFormItemSelect, mapFormItemDatePicker, mapFormItemTenant } from "@/components/scTable/helper";
+import planDetail from "./detail";
+import planDesc from "./desc";
+import orderDesc from "@/views/sales/order/desc";
+
+import store from "@/store";
+watch(() => store.state.tenant.tenantId, () => refreshTable());
+
+const daterangeConfig = reactive({
+    resetValue: () => [],
+    props: {
+        type: "daterange",
+        startPlaceholder: "开始日期",
+        endPlaceholder: "结束日期",
+        format: "YYYY-MM-DD"
+    }
+});
+
+const formConfig = reactive({
+    data: {},
+    items: [
+        mapFormItemTenant({ events: { change: data => XEUtils.merge(formConfig.data, data) } }),
+        mapFormItemInput("nameLike", "计划主题"),
+        mapFormItemInput("codeLike", "计划编号"),
+        mapFormItemDatePicker("createTime", "创建日期", daterangeConfig)
+    ]
+});
+
+const paramsColums = reactive([
+    { column: "orderBy", defaultValue: "createTime_desc" },
+    { column: "tenantId" },
+    { column: "nameLike" },
+    { column: "codeLike" },
+    { column: "createTimeBegin", field: "createTime[0]" },
+    { column: "createTimeEnd", field: "createTime[1]" }
+]);
+
+const columns = reactive([
+    { type: "seq", fixed: "left", width: 60 },
+    { visible: computed(() => store.state.tenant.tenantId === "0"), type: "html", field: "tenantName", title: "所属租户", fixed: "left", minWidth: 200, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == row.tenantId), "name") },
+    { type: "html", field: "name", title: "计划主题", fixed: "left", minWidth: 150, sortable: true },
+    { field: "code", title: "计划编号", fixed: "left", minWidth: 150, sortable: true, className: "vxe-table-link-cell", slots: { default: "code_link" } },
+    { field: "orderCode", title: "来源单据", fixed: "left", minWidth: 150, sortable: true, className: "vxe-table-link-cell", slots: { default: "order_link" } },
+    { field: "status", title: "计划状态", minWidth: 120, editRender: { name: "$cell-tag", options: salesDic.planStatus } },
+    { type: "html", field: "beginDate", title: "计划开始日期", minWidth: 150, sortable: true },
+    { type: "html", field: "endDate", title: "计划结束日期", minWidth: 150, sortable: true },
+    { type: "html", field: "createTime", title: "创建日期", minWidth: 120, sortable: true, formatter: ({ cellValue }) => TOOL.dateFormat(cellValue, "YYYY-MM-DD") || cellValue },
+    { title: "操作", fixed: "right", width: 140, slots: { default: "action" } }
+]);
+
+// 显示隐藏 筛选表单
+const xGridTable = ref();
+const refreshTable = (mode = "add") => (xGridTable.value.searchData(mode), xGridTable.value.reloadColumn(columns));
+
+const planRef = ref();
+const planDescRef = ref();
+const orderDescRef = ref();
+const dialog = reactive({
+    detail: false,
+    desc: false,
+    orderDesc: false
+});
+
+const table_add = () => {
+    dialog.detail = true;
+    nextTick(() => planRef.value?.open());
+}
+
+const table_edit = row => {
+    dialog.detail = true;
+    nextTick(() => planRef.value?.setData(row));
+}
+
+const table_detail = row => {
+    dialog.desc = true;
+    nextTick(() => planDescRef.value?.setData(row));
+}
+
+const table_order = row => {
+    dialog.orderDesc = true;
+    nextTick(() => orderDescRef.value?.setData(row.saleOrder));
+}
+
+const table_del = ({ id }) => {
+    ElMessageBox.confirm("是否确认删除该生产计划?", "删除警告", {
+        type: "warning",
+        confirmButtonText: "确定",
+        cancelButtonText: "取消"
+    }).then(() => {
+        API.production.plan.del({ id }).then(() => {
+            ElMessage.success("操作成功");
+            refreshTable();
+        });
+    }).catch(() => {});
+}
+</script>

+ 34 - 0
src/views/production/plan/main copy.js

@@ -0,0 +1,34 @@
+import XEUtils from "xe-utils"
+import { materialDic } from "@/utils/basicDic"
+
+export const tableOptions = reactive({
+    tableKey: "material",
+
+    // editDiasbled: { quantity: row => row.parentId !== "0" },
+    columns: [
+        { type: "seq", fixed: "left", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false, slots: { default: "seq_del" } },
+        // { type: "seq", fixed: "left", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false, params: { hide_del: row => !!row.children.length }, slots: { default: "seq_del" } },
+        { field: "code", title: "产品编码", fixed: "left", minWidth: 200 },
+        { field: "name", title: "产品名称", fixed: "left", minWidth: 150 },
+        { field: "bomList", title: "BOM清单", fixed: "left", minWidth: 150, slots: { default: "bom" } },
+        { field: "needType", title: "需求类型", fixed: "left", minWidth: 120, formatter: ({ cellValue }) => XEUtils.get(materialDic.needType, cellValue, cellValue) },
+        { field: "unit", title: "单位", minWidth: 120 },
+        { field: "quantity", title: "标准用量", minWidth: 100 },
+        { field: "stockNum", title: "库存明细(总/可用/锁定)", minWidth: 160, className: "vxe-table-edit-handler-cell", slots: { default: "stock" } },
+        { field: "stockUseNum", title: "仓库领用数量", minWidth: 120, editRender: { name: "VxeNumberInput", props: { min: 0, controlConfig: { enabled: false } }, defaultValue: 0 }, slots: { edit: "stockUse" } },
+        { field: "allocateNum", title: "生产数量", minWidth: 120, editRender: { enabled: false, name: "VxeNumberInput", props: { controlConfig: { enabled: false } } } }
+    ],
+    editRules: {
+        stockUseNum: [{ required: true, message: "必须填写" }],
+        allocateNum: [{ required: true, message: "必须填写" }]
+    },
+
+    selectOptions: {
+        paramsColums: [
+            { column: "status", defaultValue: "enable" },
+            { column: "materialTypeIn", defaultValue: materialDic.typeRelation["outsourcing"] }
+        ]
+    },
+
+    add_success: (oldValue, newValue) => XEUtils.map(newValue, (item, index) => XEUtils.pick(item, "id", "tenantId", "code", "name", "needType", "quantity", "unit"))
+})

+ 28 - 0
src/views/production/plan/main.js

@@ -0,0 +1,28 @@
+import XEUtils from "xe-utils"
+import { materialDic } from "@/utils/basicDic"
+
+export const tableOptions = reactive({
+    tableKey: "bom",
+
+    treeConfig: { transform: true, expandAll: true },
+    // editDiasbled: { quantity: row => row.parentId !== "0" },
+    columns: [
+        { type: "seq", fixed: "left", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false, params: { hide_del: row => !!row.children.length }, slots: { default: "seq_del" } },
+        { field: "material.code", title: "产品编码", fixed: "left", minWidth: 200, treeNode: true, headerAlign: "center", align: "left" },
+        { field: "material.name", title: "产品名称", fixed: "left", minWidth: 150 },
+        { field: "material.unit", title: "单位", minWidth: 120 },
+        { field: "material.needType", title: "需求类型", minWidth: 120, formatter: ({ cellValue }) => XEUtils.get(materialDic.needType, cellValue, cellValue) },
+        { field: "quantity", title: "单用量", minWidth: 100 }
+    ],
+    editRules: {
+        quantity: [{ required: true, message: "必须填写" }]
+    },
+
+    selectOptions: {
+        paramsColums: [
+            { column: "status", defaultValue: "enable" }
+        ]
+    },
+
+    add_success: (oldValue, newValue) => XEUtils.map(newValue, (item, index) => XEUtils.pick(item, "id", "parentId", "material", "quantity"))
+})

+ 236 - 0
src/views/production/prePlan/detail.vue

@@ -0,0 +1,236 @@
+<template>
+    <el-dialog v-model="visible" title="物料分配" fullscreen :close-on-click-modal="false" @closed="$emit('closed')">
+        <el-form ref="formRef" :model="form" label-width="120">
+            <el-collapse v-model="activeNames">
+                <el-collapse-item title="订单信息" name="order">
+                    <el-descriptions v-if="$store.state.tenant.tenantId === '0'" :column="1" label-width="140" border>
+                        <el-descriptions-item label="所属租户" label-align="right">{{ tenantName }}</el-descriptions-item>
+                    </el-descriptions>
+                    <el-descriptions :column="2" label-width="140" border>
+                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="单据编号" :span="ismobile ? 2 : 1" label-align="right">{{ orderData.code }}</el-descriptions-item>
+                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="单据日期" :span="ismobile ? 2 : 1" label-align="right">{{ orderData.orderDate }}</el-descriptions-item>
+                        <el-descriptions-item label="客户名称" :span="ismobile ? 2 : 1" label-align="right">{{ orderData.customerName }}</el-descriptions-item>
+                        <el-descriptions-item label="预计交期" :span="ismobile ? 2 : 1" label-align="right">{{ orderData.planReceiveDate }}</el-descriptions-item>
+                        <el-descriptions-item label="订单产品" :span="ismobile ? 2 : 1" label-align="right">
+                            <div v-for="item in orderData.childrenList" :key="item.id" class="order-product">{{ item.material.name }}({{ item.materialCode }})
+                                <template v-if="item.material.needType == 'self_made'">
+                                    <el-tag v-if="!item.bomList" type="warning">请维护BOM清单</el-tag>
+                                    <el-button v-else type="primary" link @click="show_bom(item)">查看BOM清单</el-button>
+                                </template>
+                            </div>
+                        </el-descriptions-item>
+                        <el-descriptions-item label="需求数量" :span="ismobile ? 2 : 1" label-align="right">
+                            <div v-for="item in orderData.childrenList" :key="item.id" class="order-product">{{ item.materialQuantity }}({{ item.material.unit }})</div>
+                        </el-descriptions-item>
+                    </el-descriptions>
+
+                    <el-row>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="计划周期" prop="planBeginDate" label-width="140" :rules="{ required: true, message: '请选择计划周期' }">
+                                <vxe-date-range-picker v-model:start-value="form.planBeginDate" v-model:end-value="form.planEndDate" value-format="yyyy-MM-dd" transfer placeholder="请选择计划周期"></vxe-date-range-picker>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+                
+                <el-collapse-item v-for="(item, name) in XEUtils.omit(cardDic, (_, key) => !form[key].length)" v-bind:key="name" :name="name">
+                    <template #title="{ isActive }">
+                        <div :class="['title-wrapper', { 'is-active': isActive }]">{{ item.title }}<span>(共{{ formatEditableCount(name) }}项)</span></div>
+                    </template>
+                    <el-row>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item :label="`${item.title}主题`" required>
+                                <el-input v-model="form[`${item.fieldPrefix}Name`]" clearable placeholder="不填将自动生成"></el-input>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                    <sc-form-table v-model="form[name]" v-bind="item.tableOptions">
+                        <template #stock="{ row }">
+                            <template v-if="!row.disabled">
+                                <template v-if="!row.warehouse.length">无库存</template>
+                                <template v-else>
+                                    <span>{{ formatWarehouseCount(row.warehouse, "number") }}</span>/
+                                    <span>{{ formatWarehouseCount(row.warehouse, "normalNumber") }}</span>/
+                                    <span>{{ formatWarehouseCount(row.warehouse, "lockedNumber") }}</span>
+                                    <el-button type="primary" link @click="table_stock(name, row)">查看</el-button>
+                                </template>
+                            </template>
+                        </template>
+                        <template #stockUse="{ row }">
+                            <el-button type="primary" link @click="table_stock(name, row, 'stockUse')">分配</el-button>
+                            <el-button type="info" link :disabled="!row.stockUseNum || row.stockUseNum == 0" @click="table_clear(row)">清空</el-button>
+                        </template>
+                    </sc-form-table>
+                </el-collapse-item>
+            </el-collapse>
+        </el-form>
+
+        <template #footer>
+            <el-button auto-insert-space @click="visible = false">取消</el-button>
+            <el-button :loading="isSaving" type="primary" auto-insert-space @click="submit">保存</el-button>
+        </template>
+    </el-dialog>
+
+    <multiple-drawer v-if="drawer" ref="drawerRef" @success="editSuccess" @closed="drawer = false"></multiple-drawer>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import store from "@/store";
+import { cardDic } from "./main";
+import multipleDrawer from "./drawer";
+
+const $emit = defineEmits(["success", "closed"]);
+const visible = ref(false);
+const drawer = ref(false);
+const isSaving = ref(false);
+
+const ismobile = computed(() => store.state.global.ismobile);
+const tenantName = computed(() => XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == orderData.tenantId), "name"));
+
+const activeNames = ref(["order", "self_made", "out_purchase", "outsourcing"]);
+const orderData = reactive({
+    id: null,
+    tenantId: store.state.tenant.tenantId,
+    code: null,
+    orderDate: null,
+    customerName: null,
+    planReceiveDate: null,
+    childrenList: []
+});
+
+const form = reactive({
+    productPlanName: null,
+    purchasePlanName: null,
+    outsourcingPlanName: null,
+    planBeginDate: null,
+    planEndDate: null,
+    self_made: [],
+    out_purchase: [],
+    outsourcing: []
+});
+
+const formatEditableCount = name => XEUtils.filter(XEUtils.toTreeArray(form[name]), item => item.material.needType == name).length;
+const formatWarehouseCount = (array, key) => XEUtils.sum(XEUtils.map(array, item => XEUtils.get(item, key)));
+const setData = data => {
+    visible.value = true;
+    XEUtils.objectEach(orderData, (_, key) => {
+        if (key == "childrenList") XEUtils.set(orderData, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item, bomList: item.bomList || [{ parentId: "0", ...XEUtils.pick(item, "material", "warehouseMaterialList") }] })));
+        else XEUtils.set(orderData, key, XEUtils.get(data, key));
+    });
+
+    XEUtils.objectEach(form, (_, key) => {
+        if (XEUtils.has(cardDic, key)) {
+            const tableData = XEUtils.map(orderData.childrenList, product => {
+                // 三种类型 bomtree
+                const bomTree = XEUtils.searchTree(XEUtils.toArrayTree(product.bomList), item => item.material.needType == key, { isEvery: true });
+                
+                return XEUtils.mapTree(bomTree, item => {
+                    // 获取父节点quantity 求积
+                    const quantity = XEUtils.reduce(XEUtils.map(XEUtils.get(XEUtils.findTree(bomTree, b => b.id == item.id), "nodes", []), b => b.parentId === "0" ? product.materialQuantity : b.quantity), (p, v) => XEUtils.multiply(p, v));
+                    // 控制冗余节点(展示层级关系) 不可编辑
+                    const disabled = item.material.needType != key;
+                    return {
+                        disabled,
+                        bomId: item.id,
+                        routeId: item.routeId,
+                        material: item.material,
+                        warehouse: item.warehouseMaterialList || [],
+                        quantity,
+                        stockUseNum: disabled ? undefined: 0,
+                        allocateNum: disabled ? undefined: quantity
+                    }
+                });
+            });
+
+            XEUtils.set(form, key, XEUtils.flatten(tableData));
+        }
+    });
+}
+
+const drawerRef = ref();
+const show_bom = ({ materialQuantity, bomList }) => {
+    const bomTree = XEUtils.toArrayTree(bomList);
+    const tableData = XEUtils.mapTree(bomTree, item => ({ ...item, total: XEUtils.reduce(XEUtils.map(XEUtils.get(XEUtils.findTree(bomTree, b => b.id == item.id), "nodes", []), b => b.parentId === "0" ? materialQuantity : b.quantity), (p, v) => XEUtils.multiply(p, v)) }));
+
+    drawer.value = true;
+    nextTick(() => drawerRef.value?.setData({ tableData }));
+}
+
+const table_stock = (table_Key, row, mode = "stock") => {
+    drawer.value = true;
+    nextTick(() => drawerRef.value?.setData({ table_Key, ...XEUtils.omit(row, "warehouse"), tableData: row.warehouse }, mode));
+}
+
+const editSuccess = ({ table_Key, id, tableData }) => {
+    const treeItem = XEUtils.get(XEUtils.findTree(form[table_Key], item => item.id == id), "item");
+    treeItem.stockUseNum = XEUtils.sum(tableData, item => item.useNum);
+    treeItem.allocateNum = XEUtils.subtract(treeItem.quantity, XEUtils.sum(tableData, item => item.useNum));
+    treeItem.warehouse = tableData;
+}
+
+const table_clear = row => {
+    row.stockUseNum = 0;
+    row.allocateNum = row.quantity;
+    XEUtils.arrayEach(row.warehouse, item => item.useNum = undefined);
+}
+
+const formRef = ref();
+const submit = () => {
+    formRef.value.validate(async valid => {
+        if (valid) {
+            const data = XEUtils.omit(form, (_, key) => XEUtils.has(cardDic, key));
+            XEUtils.set(data, "saleOrderId", orderData.id);
+            XEUtils.objectEach(cardDic, ({ fieldPrefix }, key) => {
+                XEUtils.set(data,
+                    `${fieldPrefix}VoList`,
+                    XEUtils.map(XEUtils.filter(XEUtils.toTreeArray(form[key]), item => item.material.needType == key), item => XEUtils.omit({
+                        bomId: item.bomId,
+                        routeId: item.routeId,
+                        allocateNum: item.allocateNum,
+                        materialCode: item.material.code,
+                        warehouseMaterialVoList: XEUtils.map(item.warehouse, w => ({ warehouseId: w.warehouseId, lockedNumber: w.useNum || 0 }))
+                    }, key != "self_made" && ["bomId", "routeId"]))
+                );
+            });
+            
+            isSaving.value = true;
+            API.production.prePlan.allocate(data).then(res => {
+                ElMessage.success("操作成功");
+                isSaving.value = false;
+                visible.value = false;
+                $emit("success");
+            }).catch(() => isSaving.value = false);
+        } else {
+            return false;
+        }
+    });
+}
+
+defineExpose({
+    setData
+});
+</script>
+
+<style scoped>
+.el-form {padding-left: 16px;padding-right: 22px;}
+
+.el-collapse {border: none;}
+.el-collapse-item {margin-top: 15px;padding: 0 24px;background-color: var(--el-fill-color-blank);border: 1px solid var(--el-border-color-light);border-radius: 4px;color: var(--el-text-color-primary);box-shadow: var(--el-box-shadow-light);transition: var(--el-transition-duration);}
+.el-collapse-item :deep(.el-collapse-item__header) {border-bottom-color: transparent;line-height: 55px;font-size: 16px;font-weight: bold;}
+.el-collapse-item :deep(.el-collapse-item__header.is-active) {border-bottom: 1px solid var(--el-border-color-lighter);}
+.el-collapse-item :deep(.el-collapse-item__header) .title-wrapper {display: flex;align-items: center;gap: 4px;}
+.el-collapse-item :deep(.el-collapse-item__header) .title-wrapper span {font-size: var(--el-collapse-header-font-size);color: var(--el-text-color-secondary);}
+.el-collapse-item :deep(.el-collapse-item__wrap) {border: none;}
+.el-collapse-item :deep(.el-collapse-item__content) {padding: 20px 0;}
+.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions + .el-row {margin-top: 20px;}
+.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions + .el-row .el-form-item {margin-bottom: 0;}
+.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions + .el-row .el-form-item .vxe-date-range-picker {flex-direction: row-reverse;width: 100%;}
+.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions + .el-row .el-form-item .vxe-date-range-picker .vxe-date-range-picker--suffix {border-radius: var(--vxe-ui-base-border-radius) 0 0 var(--vxe-ui-base-border-radius);}
+.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions + .el-row .el-form-item .vxe-date-range-picker .vxe-date-range-picker--control-icon {padding-left: .5em;padding-right: 0;}
+.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions__content {min-width: 260px;}
+.el-collapse-item :deep(.el-collapse-item__content) .order-product + .order-product {margin-top: 4px;padding-top: 4px;border-top: var(--el-descriptions-table-border);}
+</style>

+ 45 - 0
src/views/production/prePlan/drawer.vue

@@ -0,0 +1,45 @@
+<template>
+    <el-drawer v-model="visible" :title="drawerDic[mode].title(drawerData)" size="80%" @closed="$emit('closed')">
+        <el-container>
+            <sc-form-table v-model="drawerData.tableData" v-bind="drawerDic[mode].options" @edit-activated="editActivated"></sc-form-table>
+        </el-container>
+
+        <template v-if="!drawerDic[mode].options.disabled" #footer>
+            <el-button auto-insert-space @click="visible = false">取消</el-button>
+            <el-button type="primary" @click="$emit('success', drawerData), visible = false">确定分配</el-button>
+        </template>
+	</el-drawer>
+</template>
+
+<script setup>
+import XEUtils from "xe-utils";
+import { drawerDic } from "./main";
+
+const $emit = defineEmits(["success", "closed"]);
+const visible = ref(false);
+const mode = ref("bom");
+const drawerData = reactive({
+    tableData: []
+});
+
+const setData = (data, model = "bom") => {
+    visible.value = true;
+    mode.value = model;
+    XEUtils.merge(drawerData, data);
+}
+
+const editActivated = ({ row, rowIndex, column }) => {
+    // 其他行仓库的领用数量
+    const excludeSum = XEUtils.sum(XEUtils.omit(drawerData.tableData, rowIndex), item => item[column.field]);
+    // (需求数 - 其他行仓库的领用数量) 和 当前仓库可用库存 的最小值
+    column.editRender.props.max = XEUtils.min([XEUtils.subtract(drawerData.quantity, excludeSum), row.normalNumber]);
+}
+
+defineExpose({
+    setData
+});
+</script>
+
+<style scoped>
+.el-container {padding: 0 20px;}
+</style>

+ 92 - 0
src/views/production/prePlan/index.vue

@@ -0,0 +1,92 @@
+<template>
+	<el-container class="is-vertical">
+        <sc-page-header></sc-page-header>
+
+        <scTable ref="xGridTable" :apiObj="$API.production.prePlan" :formConfig="formConfig" :paramsColums="paramsColums" :toolbarConfig="toolbarConfig" :columns="columns">
+            <template #action="{ row }">
+                <el-button type="primary" link @click="table_bind(row)">
+                    <template #icon><sc-iconify icon="material-symbols:arrow-split"></sc-iconify></template>物料分配
+                </el-button>
+            </template>
+        </scTable>
+	</el-container>
+
+    <plan-detail v-if="dialog" ref="planRef" @success="refreshTable" @closed="dialog = false"></plan-detail>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import TOOL from "@/utils/tool";
+import { mapFormItemInput, mapFormItemSelect, mapFormItemDatePicker, mapFormItemTenant } from "@/components/scTable/helper";
+import planDetail from "./detail";
+
+import store from "@/store";
+watch(() => store.state.tenant.tenantId, () => refreshTable());
+
+const customerConfig = reactive({
+    api: { key: "basic.customer", query: { orderBy: "createTime_desc", status: "enable" } },
+    optionProps: { label: "name", value: "id" },
+    events: {
+        change: data => XEUtils.merge(formConfig.data, data)
+    }
+});
+
+const daterangeConfig = reactive({
+    resetValue: () => [],
+    props: {
+        type: "daterange",
+        startPlaceholder: "开始日期",
+        endPlaceholder: "结束日期",
+        format: "YYYY-MM-DD"
+    }
+});
+    
+const toolbarConfig = reactive({ export: false });
+const formConfig = reactive({
+    data: {},
+    items: [
+        mapFormItemTenant({ events: { change: data => XEUtils.merge(formConfig.data, { ...data, customerId: undefined }) } }),
+        mapFormItemInput("codeLike", "单据编号"),
+        mapFormItemSelect("customerId", "客户", customerConfig),
+        mapFormItemInput("contractNoLike", "合同编号"),
+        mapFormItemDatePicker("orderDate", "单据日期", daterangeConfig)
+    ]
+});
+
+const paramsColums = reactive([
+    { column: "orderBy", defaultValue: "orderDate_asc" },
+    { column: "status", defaultValue: "pending" },
+    { column: "tenantId" },
+    { column: "codeLike" },
+    { column: "customerId" },
+    { column: "contractNoLike" },
+    { column: "orderDateBegin", field: "orderDate[0]" },
+    { column: "orderDateEnd", field: "orderDate[1]" }
+]);
+
+const columns = reactive([
+    { type: "seq", fixed: "left", width: 60 },
+    { visible: computed(() => store.state.tenant.tenantId === "0"), type: "html", field: "tenantName", title: "所属租户", fixed: "left", minWidth: 200, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == row.tenantId), "name") },
+    { field: "code", title: "单据编号", fixed: "left", minWidth: 150, sortable: true },
+    { type: "html", field: "orderDate", title: "单据日期", fixed: "left", minWidth: 120, sortable: true },
+    { type: "html", field: "customerName", title: "客户", minWidth: 150, sortable: true },
+    { type: "html", field: "contractNo", title: "合同编号", minWidth: 150, sortable: true },
+    { type: "html", field: "actualPrice", title: "成交金额", minWidth: 120, sortable: true, formatter: ({ cellValue }) => TOOL.amountFormat(cellValue) || cellValue },
+    { type: "html", field: "planReceiveDate", title: "预计交期", minWidth: 120, sortable: true },
+    { title: "操作", fixed: "right", width: 140, slots: { default: "action" } }
+]);
+
+// 显示隐藏 筛选表单
+const xGridTable = ref();
+const refreshTable = () => (xGridTable.value.searchData(), xGridTable.value.reloadColumn(columns));
+
+const planRef = ref();
+const dialog = ref(false);
+const table_bind = row => {
+    dialog.value = true;
+    nextTick(() => planRef.value?.setData(row));
+}
+</script>

+ 103 - 0
src/views/production/prePlan/main.js

@@ -0,0 +1,103 @@
+import XEUtils from "xe-utils"
+import { materialDic } from "@/utils/basicDic"
+
+const defaultColumns = [
+    { type: "seq", fixed: "left", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false },
+    { field: "material.code", title: "产品编码", fixed: "left", minWidth: 200, treeNode: true, headerAlign: "center", align: "left" },
+    { field: "material.name", title: "产品名称", fixed: "left", minWidth: 150 },
+    { field: "material.unit", title: "单位", minWidth: 150 },
+    { field: "quantity", title: "需求数量", minWidth: 100 },
+    { field: "stockNum", title: "库存明细(总/可用/锁定)", minWidth: 160, className: "vxe-table-edit-handler-cell", slots: { default: "stock" } },
+    { field: "stockUseNum", title: "仓库领用数量", minWidth: 120, editRender: { name: "VxeNumberInput", props: { min: 0, controlConfig: { enabled: false } }, defaultValue: 0 }, slots: { edit: "stockUse" } }
+]
+
+const tableOptions = {
+    treeConfig: { expandAll: true },
+    editDiasbled: { stockUseNum: row => !row.warehouse.length },
+    editRules: {
+        stockUseNum: [{ required: true, message: "必须填写" }],
+        allocateNum: [{ required: true, message: "必须填写" }]
+    }
+}
+
+export const cardDic = reactive({
+    self_made: {
+        title: "生产计划",
+        fieldPrefix: "productPlan",
+        tableOptions: {
+            columns: [
+                ...defaultColumns,
+                { field: "allocateNum", title: "自主生产数量", minWidth: 120, editRender: { enabled: false, name: "VxeNumberInput", props: { controlConfig: { enabled: false } } } }
+            ],
+            ...tableOptions
+        }
+    },
+    out_purchase: {
+        title: "采购计划",
+        fieldPrefix: "purchasePlan",
+        tableOptions: {
+            columns: [
+                ...defaultColumns,
+                { field: "allocateNum", title: "外部采购数量", minWidth: 120, editRender: { enabled: false, name: "VxeNumberInput", props: { controlConfig: { enabled: false } } } }
+            ],
+            ...tableOptions
+        }
+    },
+    outsourcing: {
+        title: "委外计划",
+        fieldPrefix: "outsourcingPlan",
+        tableOptions: {
+            columns: [
+                ...defaultColumns,
+                { field: "allocateNum", title: "委外加工数量", minWidth: 120, editRender: { enabled: false, name: "VxeNumberInput", props: { controlConfig: { enabled: false } } } }
+            ],
+            ...tableOptions
+        }
+    },
+})
+
+export const drawerDic = reactive({
+    bom: {
+        title: data => `【${XEUtils.get(XEUtils.first(data.tableData), "bomCode")}】- BOM清单`,
+        options: {
+            disabled: true,
+            treeConfig: { expandAll: true },
+            columns: [
+                { field: "material.code", title: "产品编码", fixed: "left", minWidth: 200, treeNode: true, headerAlign: "center", align: "left" },
+                { field: "material.name", title: "产品名称", fixed: "left", minWidth: 150 },
+                { field: "material.specification", title: "规格型号", minWidth: 150 },
+                { field: "material.unit", title: "单位", minWidth: 120 },
+                { field: "material.needType", title: "需求类型", minWidth: 120, formatter: ({ cellValue }) => XEUtils.get(materialDic.needType, cellValue, cellValue) },
+                { field: "quantity", title: "标准用量", minWidth: 100 },
+                { field: "total", title: "需求数量", minWidth: 100 }
+            ]
+        }
+    },
+
+    stock: {
+        title: data => `【${data.material.name}(${data.material.code})】- 库存明细`,
+        options: {
+            disabled: true,
+            columns: [
+                { field: "warehouse.name", title: "仓库名称", minWidth: 150 },
+                { field: "number", title: "总库存", minWidth: 100 },
+                { field: "normalNumber", title: "可用库存", minWidth: 100 },
+                { field: "lockedNumber", title: "锁定库存", minWidth: 100 }
+            ],
+            footerField: [["warehouse.name", "number", "normalNumber", "lockedNumber"]]
+        }
+    },
+
+    stockUse: {
+        title: data => `【${data.material.name}(需求 ${data.quantity + data.material.unit})】- 仓库领用分配`,
+        options: {
+            columns: [
+                { field: "warehouse.name", title: "仓库名称", minWidth: 150, footerAlign: "right" },
+                { field: "normalNumber", title: "可用库存", minWidth: 100 },
+                { field: "useNum", title: "本次领用", minWidth: 120, editRender: { name: "VxeNumberInput", props: { min: 0, clearable: true, controlConfig: { enabled: false } }, defaultValue: 0 } }
+            ],
+            footerField: [["warehouse.name", "useNum"]],
+            mergeFooterItems: [{ row: 0, col: 0, rowspan: 1, colspan: 2 }]
+        }
+    }
+})

+ 17 - 14
src/views/sales/order/desc.vue

@@ -3,22 +3,24 @@
         <el-main>
             <el-collapse v-model="activeNames">
                 <el-collapse-item title="基本信息" name="basic">
+                    <el-descriptions v-if="$store.state.tenant.tenantId === '0'" :column="1" label-width="140" border>
+                        <el-descriptions-item label="所属租户" label-align="right">{{ tenantName }}</el-descriptions-item>
+                    </el-descriptions>
                     <el-descriptions :column="3" label-width="140" border>
-                        <el-descriptions-item v-if="$store.state.tenant.tenantId === '0'" label="所属租户" :span="ismobile ? 3 : 1" label-align="right" min-width="120">{{ tenantName }}</el-descriptions-item>
-                        <el-descriptions-item label="单据编号" :span="ismobile ? 3 : 1" label-align="right" min-width="120">{{ descData.code }}</el-descriptions-item>
-                        <el-descriptions-item label="合同编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.contractNo }}</el-descriptions-item>
-                        <el-descriptions-item label="单据日期" :span="ismobile ? 3 : 1" label-align="right" min-width="120">{{ descData.orderDate }}</el-descriptions-item>
-                        <el-descriptions-item label="单据状态" :span="ismobile ? 3 : 1" label-align="right" min-width="120">{{ XEUtils.get(salesDic.orderStatus, descData.status, descData.status) }}</el-descriptions-item>
-                        <el-descriptions-item label="客户" :span="ismobile ? 3 : 1" label-align="right">{{ descData.customerName }}</el-descriptions-item>
-                        <el-descriptions-item label="添加时间" :span="ismobile ? 3 : 1" label-align="right" min-width="120">{{ descData.createTime }}</el-descriptions-item>
-                        <el-descriptions-item label="预计交期" :span="ismobile ? 3 : 1" label-align="right" min-width="120">{{ descData.planReceiveDate }}</el-descriptions-item>
-                        <el-descriptions-item label="实际交期" :span="ismobile ? 3 : 1" label-align="right" min-width="120">{{ descData.deliveryDate }}</el-descriptions-item>
+                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="单据编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.code }}</el-descriptions-item>
+                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="合同编号" :span="ismobile ? 3 : 1" label-align="right">{{ descData.contractNo }}</el-descriptions-item>
+                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="单据日期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.orderDate }}</el-descriptions-item>
+                        <el-descriptions-item label="单据状态" :span="ismobile ? 3 : 1" label-align="right">{{ XEUtils.get(salesDic.orderStatus, descData.status, descData.status) }}</el-descriptions-item>
+                        <el-descriptions-item label="客户名称" :span="ismobile ? 3 : 1" label-align="right">{{ descData.customerName }}</el-descriptions-item>
+                        <el-descriptions-item label="添加时间" :span="ismobile ? 3 : 1" label-align="right">{{ descData.createTime }}</el-descriptions-item>
+                        <el-descriptions-item label="预计交期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.planReceiveDate }}</el-descriptions-item>
+                        <el-descriptions-item label="实际交期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.deliveryDate }}</el-descriptions-item>
                         <el-descriptions-item label="业务员" :span="ismobile ? 3 : 1" label-align="right">{{ descData.managerName }}</el-descriptions-item>
-                        <el-descriptions-item label="收货日期" :span="ismobile ? 3 : 1" label-align="right" min-width="120">{{ descData.actualReceiveDate }}</el-descriptions-item>
+                        <el-descriptions-item label="收货日期" :span="ismobile ? 3 : 1" label-align="right">{{ descData.actualReceiveDate }}</el-descriptions-item>
+                        <el-descriptions-item label="客户收货地址" label-align="right" :span="ismobile ? 3 : 1">{{ descData.deliveryAddress }}</el-descriptions-item>
                     </el-descriptions>
                     <el-descriptions :column="1" label-width="140" border>
-                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="客户收货地址" label-align="right">{{ descData.deliveryAddress }}</el-descriptions-item>
-                        <el-descriptions-item label="概要" label-align="right">{{ descData.remark }}</el-descriptions-item>
+                        <el-descriptions-item label-class-name="no-border-top" class-name="no-border-top" label="概要" label-align="right">{{ descData.remark }}</el-descriptions-item>
                         <el-descriptions-item label="附件" label-align="right">
                             <sc-upload-file v-model="descData.fileList" hideAdd disabled></sc-upload-file>
                         </el-descriptions-item>
@@ -31,8 +33,8 @@
 
                 <el-collapse-item title="金额信息" name="amount">
                     <el-descriptions :column="2" label-width="140" border>
-                        <el-descriptions-item label="整单折扣额" label-align="right" min-width="120">{{ descData.freePrice }}</el-descriptions-item>
-                        <el-descriptions-item label="成交金额" label-align="right" min-width="120">{{ descData.actualPrice }}</el-descriptions-item>
+                        <el-descriptions-item label="整单折扣额" label-align="right">{{ descData.freePrice }}</el-descriptions-item>
+                        <el-descriptions-item label="成交金额" label-align="right">{{ descData.actualPrice }}</el-descriptions-item>
                     </el-descriptions>
                 </el-collapse-item>
             </el-collapse>
@@ -98,4 +100,5 @@ defineExpose({
 .el-collapse-item :deep(.el-collapse-item__header.is-active) {border-bottom: 1px solid var(--el-border-color-lighter);}
 .el-collapse-item :deep(.el-collapse-item__wrap) {border: none;}
 .el-collapse-item :deep(.el-collapse-item__content) {padding: 20px 0;}
+.el-collapse-item :deep(.el-collapse-item__content) .el-descriptions__content {min-width: 120px;}
 </style>

+ 2 - 4
src/views/sales/order/detail.vue

@@ -6,7 +6,7 @@
                     <el-row>
                         <el-col v-if="$store.state.tenant.tenantId === '0'" :md="8" :xs="24">
                             <el-form-item label="所属租户" prop="tenantId">
-                                <el-select v-if="!form.id" v-model="form.tenantId" filterable placeholder="请选择所属租户">
+                                <el-select v-if="!form.id" v-model="form.tenantId" filterable placeholder="请选择所属租户" @change="form.managerId = null">
                                     <el-option v-for="item in $store.state.tenant.tenants" :key="item.id" :label="item.name" :value="item.id"></el-option>
                                 </el-select>
                                 <el-input v-else v-model="tenantName" readonly></el-input>
@@ -200,9 +200,7 @@ const submit = () => {
     });
 }
 
-const removeSuccess = () => {
-    if (form.value.id) isDel.value = true;
-}
+const removeSuccess = () => form.value.id && (isDel.value = true);
 
 const fetchUser = () => API.auth.user.all({ orderBy: "id_desc" }).then(res => users.value = res).catch(() => users.value = []);
 fetchUser();

+ 2 - 2
src/views/sales/order/index.vue

@@ -44,7 +44,7 @@ const selectConfig = reactive({
 });
 
 const customerConfig = reactive({
-    api: { key: "basic.customer", query: { orderBy: "id_desc" } },
+    api: { key: "basic.customer", query: { orderBy: "createTime_desc", status: "enable" } },
     optionProps: { label: "name", value: "id" },
     events: {
         change: data => XEUtils.merge(formConfig.data, data)
@@ -64,7 +64,7 @@ const daterangeConfig = reactive({
 const formConfig = reactive({
     data: {},
     items: [
-        mapFormItemTenant({ events: { change: data => XEUtils.merge(formConfig.data, data) } }),
+        mapFormItemTenant({ events: { change: data => XEUtils.merge(formConfig.data, { ...data, customerId: undefined }) } }),
         mapFormItemInput("codeLike", "单据编号"),
         mapFormItemSelect("status", "单据状态", selectConfig),
         mapFormItemSelect("customerId", "客户", customerConfig),

+ 3 - 3
src/views/sales/order/main.js

@@ -1,6 +1,6 @@
-import XEUtils from "xe-utils";
-import { materialDic } from "@/utils/basicDic";
-import { mapFormItemInput } from "@/components/scTable/helper";
+import XEUtils from "xe-utils"
+import { materialDic } from "@/utils/basicDic"
+import { mapFormItemInput } from "@/components/scTable/helper"
 
 export const tableOptions = reactive({
     tableKey: "material",

+ 19 - 19
src/views/sales/performance/main.js

@@ -1,5 +1,5 @@
-import moment from "moment";
-import XEUtils from "xe-utils";
+import moment from "moment"
+import XEUtils from "xe-utils"
 
 export const radioDic = {
     year: {
@@ -8,48 +8,48 @@ export const radioDic = {
         generateXAxis: (begin, end) => {
             const list = [];
             for (let year = moment(begin).year(); year <= moment(end).year(); year++) {
-                list.push(year);
+                list.push(year)
             }
-            return list;
+            return list
         }
     },
     quarter: {
         label: "季度",
         style: { width: "200px" },
         generateXAxis: (begin, end) => {
-            const list = [];
-            let current = moment(begin);
+            const list = []
+            let current = moment(begin)
             while (current.isSameOrBefore(end)) {
-                list.push(`${current.year()}-Q${current.quarter()}`);
-                current.add(1, "quarter");
+                list.push(`${current.year()}-Q${current.quarter()}`)
+                current.add(1, "quarter")
             }
-            return list;
+            return list
         }
     },
     month: {
         label: "月度",
         style: { width: "190px" },
         generateXAxis: (begin, end) => {
-            const list = [];
-            let current = moment(begin);
+            const list = []
+            let current = moment(begin)
             while (current.isSameOrBefore(end)) {
-                list.push(current.format("YYYY-MM"));
-                current.add(1, "month");
+                list.push(current.format("YYYY-MM"))
+                current.add(1, "month")
             }
-            return list;
+            return list
         }
     },
     date: {
         label: "天数",
         style: { width: "240px" },
         generateXAxis: (begin, end) => {
-            const list = [];
-            let current = moment(begin);
+            const list = []
+            let current = moment(begin)
             while (current.isSameOrBefore(end)) {
-                list.push(current.format("YYYY-MM-DD"));
-                current.add(1, "day");
+                list.push(current.format("YYYY-MM-DD"))
+                current.add(1, "day")
             }
-            return list;
+            return list
         }
     }
 }

+ 1 - 1
src/views/sales/plan/index.vue

@@ -81,7 +81,7 @@ const options = reactive({
     pagerConfig : { enabled: false },
     columns: [
         { type: "seq", fixed: "left", width: 80 },
-        { visible: computed(() => store.state.tenant.tenantId === "0"), type: "html", field: "tenantName", title: "所属租户", fixed: "left", minWidth: 200, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == row.tenantId), "name") },
+        { visible: computed(() => store.state.tenant.tenantId === "0"), type: "html", field: "tenantName", title: "所属租户", fixed: "left", minWidth: 200, sortable: true, formatter: ({ cellValue, row }) => row.parentId === "0" ? cellValue || XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == row.tenantId), "name") : "" },
         { type: "html", field: "name", title: "计划名称", fixed: "left", minWidth: 200, treeNode: true, headerAlign: "center", align: "left", sortable: true },
         { type: "html", field: "code", title: "计划编号", fixed: "left", minWidth: 150, sortable: true },
         { type: "html", field: "type", title: "计划类型", minWidth: 120, sortable: true, formatter: ({ cellValue }) => XEUtils.get(salesDic.planType, cellValue, cellValue) },

+ 1 - 1
src/views/system/dept/index.vue

@@ -45,8 +45,8 @@ const options = reactive({
     treeConfig: { transform: true, parentField: "pid" },
     columns: [
         { type: "seq", width: 60 },
-        { type: "html", field: "name", title: "部门名称", minWidth: 200, treeNode: true, sortable: true, headerAlign: "center", align: "left" },
         { visible: computed(() => store.state.tenant.tenantId === "0"), type: "html", field: "tenantName", title: "所属租户", minWidth: 200, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.get(XEUtils.find(store.state.tenant.tenants, item => item.id == row.tenantId), "name") },
+        { type: "html", field: "name", title: "部门名称", minWidth: 200, treeNode: true, sortable: true, headerAlign: "center", align: "left" },
         { type: "html", field: "firmFunctionary", title: "负责人", minWidth: 160, sortable: true },
         { type: "html", field: "firmFunctionaryPhone", title: "负责人电话", minWidth: 160, sortable: true },
         { type: "html", field: "remark", title: "备注", minWidth: 300, sortable: true },

+ 1 - 1
src/views/system/tenant/index.vue

@@ -48,7 +48,7 @@ const formConfig = reactive({
 });
 
 const paramsColums = reactive([
-    { column: "orderBy", defaultValue: "id_asc" },
+    { column: "orderBy", defaultValue: "createTime_desc" },
     { column: "nameLike" },
     { column: "status" }
 ]);

+ 20 - 20
src/vxeTable.js

@@ -1,38 +1,38 @@
-import XEUtils from "xe-utils";
-import VxeUI from "vxe-pc-ui";
-import VxeUITable from "vxe-table";
-import VxeUIPluginRenderElement from "@vxe-ui/plugin-render-element";
-import VxeUIPluginExportXLSX from "@vxe-ui/plugin-export-xlsx";
-import ExcelJS from "exceljs";
+import XEUtils from "xe-utils"
+import VxeUI from "vxe-pc-ui"
+import VxeUITable from "vxe-table"
+import VxeUIPluginRenderElement from "@vxe-ui/plugin-render-element"
+import VxeUIPluginExportXLSX from "@vxe-ui/plugin-export-xlsx"
+import ExcelJS from "exceljs"
 
-import "vxe-pc-ui/lib/style.css";
-import "vxe-table/lib/style.css";
-import "@vxe-ui/plugin-render-element/dist/style.css";
+import "vxe-pc-ui/lib/style.css"
+import "vxe-table/lib/style.css"
+import "@vxe-ui/plugin-render-element/dist/style.css"
 
-VxeUI.use(VxeUIPluginRenderElement).use(VxeUIPluginExportXLSX, { ExcelJS });
+VxeUI.use(VxeUIPluginRenderElement).use(VxeUIPluginExportXLSX, { ExcelJS })
 
 // 自定义renderer
-import tableSearch from "@/components/scTable/renderer/table-search";
-import formSelect from "@/components/scTable/renderer/form-select";
-import formRadio from "@/components/scTable/renderer/form-radio";
-import cellTag from "@/components/scTable/renderer/cell-tag";
+import tableSearch from "@/components/scTable/renderer/table-search"
+import formSelect from "@/components/scTable/renderer/form-select"
+import formRadio from "@/components/scTable/renderer/form-radio"
+import cellTag from "@/components/scTable/renderer/cell-tag"
 
 VxeUI.renderer.mixin({
     "$table-search": {
         renderToolbarButton(renderOpts, params) {
-            return h(tableSearch, { renderOpts, params });
+            return h(tableSearch, { renderOpts, params })
         }
     },
 
     "$form-select": {
         renderFormItemContent(renderOpts, params) {
-            return h(formSelect, { renderOpts, params });
+            return h(formSelect, { renderOpts, params })
         }
     },
 
     "$form-radio": {
         renderFormItemContent(renderOpts, params) {
-            return h(formRadio, { renderOpts, params });
+            return h(formRadio, { renderOpts, params })
         }
     },
 
@@ -44,16 +44,16 @@ VxeUI.renderer.mixin({
 
             const props = XEUtils.get(renderOpts, "props", {})
             const options = XEUtils.get(renderOpts, "options", {})
-            return h(cellTag, { renderOpts: { defaultValue, props, options }, params });
+            return h(cellTag, { renderOpts: { defaultValue, props, options }, params })
         }
     }
 })
 
 export default {
 	install(app) {
-        app.use(VxeUI).use(VxeUITable);
+        app.use(VxeUI).use(VxeUITable)
 
         // 黑夜模式
-        localStorage.getItem("APP_DARK") && VxeUITable.VxeUI.setTheme("dark");
+        localStorage.getItem("APP_DARK") && VxeUITable.VxeUI.setTheme("dark")
 	}
 }