zhuangyunsheng 4 ngày trước cách đây
mục cha
commit
efbb6961d1
61 tập tin đã thay đổi với 3305 bổ sung687 xóa
  1. 2 2
      public/index.html
  2. 2 2
      src/App.vue
  3. 75 0
      src/api/model/basic.js
  4. 29 0
      src/api/model/production.js
  5. 25 0
      src/api/model/sales.js
  6. 1 1
      src/api/model/system.js
  7. 54 0
      src/components/scFormTable/detail.vue
  8. 59 20
      src/components/scFormTable/index.vue
  9. 2 2
      src/components/scIconSelect/index.vue
  10. 1 1
      src/components/scPageHeader/index.vue
  11. 65 39
      src/components/scTable/index.vue
  12. 8 1
      src/components/scTable/renderer/cell-tag.vue
  13. 9 4
      src/components/scTable/renderer/table-search.vue
  14. 54 0
      src/components/scTableInput/index.vue
  15. 112 223
      src/components/scTableSelect/index.vue
  16. 2 2
      src/layout/components/sideM.vue
  17. 9 6
      src/layout/index.vue
  18. 5 3
      src/router/index.js
  19. 7 3
      src/style/app.scss
  20. 5 1
      src/style/dark.scss
  21. 143 0
      src/utils/basicDic.js
  22. 1 1
      src/utils/request.js
  23. 175 0
      src/views/basic/customer/detail.vue
  24. 130 0
      src/views/basic/customer/index.vue
  25. 164 0
      src/views/basic/material/detail.vue
  26. 145 0
      src/views/basic/material/index.vue
  27. 149 0
      src/views/basic/qualityPlan/desc.vue
  28. 182 0
      src/views/basic/qualityPlan/detail.vue
  29. 144 0
      src/views/basic/qualityPlan/index.vue
  30. 61 0
      src/views/defaultVue/index.vue
  31. 3 2
      src/views/login/index.vue
  32. 84 0
      src/views/production/bom/desc.vue
  33. 155 0
      src/views/production/bom/detail.vue
  34. 175 0
      src/views/production/bom/index.vue
  35. 44 0
      src/views/production/bom/main.js
  36. 89 0
      src/views/sales/order/desc.vue
  37. 160 0
      src/views/sales/order/detail.vue
  38. 154 0
      src/views/sales/order/index.vue
  39. 68 0
      src/views/sales/order/main.js
  40. 208 0
      src/views/sales/plan/detail.vue
  41. 134 0
      src/views/sales/plan/index.vue
  42. 2 2
      src/views/system/dept/detail.vue
  43. 3 3
      src/views/system/dept/index.vue
  44. 28 15
      src/views/system/menu/detail.vue
  45. 3 3
      src/views/system/menu/index.vue
  46. 1 1
      src/views/system/role/bind.vue
  47. 2 2
      src/views/system/role/detail.vue
  48. 3 3
      src/views/system/role/index.vue
  49. 2 2
      src/views/system/user/detail.vue
  50. 4 4
      src/views/system/user/index.vue
  51. 1 1
      src/views/userCenter/index.vue
  52. 24 19
      src/views/workmanship/line/desc.vue
  53. 51 46
      src/views/workmanship/line/detail.vue
  54. 5 3
      src/views/workmanship/line/history.vue
  55. 22 34
      src/views/workmanship/line/index.vue
  56. 26 39
      src/views/workmanship/main.js
  57. 0 47
      src/views/workmanship/line/selectTable.vue
  58. 0 82
      src/views/workmanship/process/desc.vue
  59. 6 21
      src/views/workmanship/process/detail.vue
  60. 26 46
      src/views/workmanship/process/index.vue
  61. 2 1
      src/vxeTable.js

+ 2 - 2
public/index.html

@@ -10,7 +10,7 @@
         <%= VUE_APP_TITLE %>
     </title>
     <script type="text/javascript">
-        document.write("<script src='config.js?" + new Date().getTime() + "'><\/script>");
+        document.write("<script src='<%= BASE_URL %>config.js?" + new Date().getTime() + "'><\/script>");
     </script>
 </head>
 
@@ -25,7 +25,7 @@
     <div id="app" class="aminui">
         <div class="app-loading">
             <div class="app-loading__logo">
-                <img src="img/logo.png" />
+                <img src="<%= BASE_URL %>img/logo.png" />
             </div>
             <div class="app-loading__loader"></div>
             <div class="app-loading__title">

+ 2 - 2
src/App.vue

@@ -69,8 +69,8 @@ export default {
 
             // vxe-Table
             document.documentElement.style.setProperty("--vxe-ui-font-primary-color", app_color);
-            document.documentElement.style.setProperty("--vxe-ui-font-primary-darken-color", app_color);
-            document.documentElement.style.setProperty("--vxe-ui-font-primary-lighten-color", app_color);
+            document.documentElement.style.setProperty("--vxe-ui-font-primary-darken-color", colorTool.darken(app_color, 3 / 10));
+            document.documentElement.style.setProperty("--vxe-ui-font-primary-lighten-color", colorTool.lighten(app_color, 3 / 10));
         }
     }
 }

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

@@ -0,0 +1,75 @@
+import config from "@/config"
+import http from "@/utils/request"
+
+export default {
+    material: {
+        name: "工序管理",
+        url: `${config.API_URL}/mes/processMaterial`,
+        
+        get: async function (data = {}) {
+            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);
+        }
+    },
+
+    qualityPlan: {
+        name: "质检方案",
+        url: `${config.API_URL}/mes/qualityInspectProgram`,
+        
+        get: async function (data = {}) {
+            return await http.post(`${this.url}/getPage`, data);
+        },
+
+        all: async function (data = {}) {
+            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);
+        }
+    },
+
+    customer: {
+        name: "客户/供应商管理",
+        url: `${config.API_URL}/mes/customer`,
+        
+        get: async function (data = {}) {
+            return await http.post(`${this.url}/getPage`, data);
+        },
+
+        all: async function (data = {}) {
+            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);
+        }
+    }
+}

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

@@ -0,0 +1,29 @@
+import config from "@/config"
+import http from "@/utils/request"
+
+export default {
+    bom: {
+        name: "BOM管理",
+        url: `${config.API_URL}/mes/processBom`,
+        
+        get: async function (data = {}) {
+            return await http.post(`${this.url}/getPage`, data);
+        },
+
+        getChild: async function (data = {}) {
+            return await http.post(`${this.url}/getChildrenList`, 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);
+        }
+    }
+}

+ 25 - 0
src/api/model/sales.js

@@ -0,0 +1,25 @@
+import config from "@/config"
+import http from "@/utils/request"
+
+export default {
+    plan: {
+        name: "销售计划",
+        url: `${config.API_URL}/mes/salePlan`,
+        
+        get: async function (data = {}) {
+            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);
+        }
+    }
+}

+ 1 - 1
src/api/model/system.js

@@ -92,5 +92,5 @@ export default {
         del: async function (data = {}) {
             return await http.post(`${this.url}/remove`, data);
         }
-    },
+    }
 }

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

@@ -0,0 +1,54 @@
+<template>
+    <el-dialog v-model="visible" :title="compDic[tableKey].title" :width="1000" append-to-body :close-on-click-modal="false" @closed="$emit('closed')">
+        <component v-if="tableKey" :is="compDic[tableKey].compName" ref="tableRef" hidePageHeader hideHandler :hideCheckbox="!multiple" :options="tableOptions" />
+        
+        <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 processTable from "@/views/workmanship/process/index";
+import materialTable from "@/views/basic/material/index";
+
+const props = defineProps({
+    tableKey: { type: String, default: "" },
+    multiple: { type: Boolean, default: false },
+    options: { type: Object, default: () => {} }
+});
+const $emit = defineEmits(["success", "closed"]);
+const visible = ref(false);
+
+const compDic = reactive({
+    stage: { title: "工序选择", compName: processTable },
+    material: { title: "产品选择", compName: materialTable },
+});
+
+const tableRef = ref();
+const tableOptions = reactive({
+    checkedRows: [],
+    maxHeight: 1048,
+    toolbarConfig: { enabled: true, print: false, zoom: false },
+    ...props.options
+});
+
+const setData = data => {
+    visible.value = true;
+    tableOptions.checkedRows = data;
+}
+
+const submit = () => {
+    const selectedRows = tableRef.value?.getSelectRows() || [];
+    if (!props.multiple && selectedRows.length > 1) return ElMessage.warning("只能选择一条数据!");
+    
+    visible.value = false;
+    $emit("success", selectedRows);
+}
+
+defineExpose({
+    setData
+});
+</script>

+ 59 - 20
src/components/scFormTable/index.vue

@@ -6,7 +6,7 @@
 
 <template>
     <el-main class="sc-form-table">
-        <vxe-grid ref="xGrid" v-bind="gridOptions" @edit-activated="$emit('editActivated', $event)">
+        <vxe-grid ref="xGrid" v-bind="gridOptions">
             <template #empty>
                 <el-empty :image-size="100" description="您还没有添加任何数据"></el-empty>
             </template>
@@ -14,11 +14,17 @@
                 <sc-iconify icon="mingcute:move-line" size="1.1em"></sc-iconify>
             </template>
 
-            <template #seq_handler="{ seq, row }">
-                <span :class="['seq', disabled && 'is-disabled']">{{ seq }}</span>
-                <vxe-button-group v-if="!disabled" circle>
-                    <vxe-button v-if="!hideAdd" icon="vxe-icon-add" status="primary" @click="rowAdd"></vxe-button>
-                    <vxe-button icon="vxe-icon-minus" status="error" @click="rowDel(row)"></vxe-button>
+            <template #top>
+                <vxe-button v-if="!disabled" style="width: 140px;margin-bottom: 15px;" status="primary" :content="titleMap[tableKey]" @click="table_add"></vxe-button>
+            </template>
+
+            <template #seq_add>
+                <vxe-button v-if="!disabled" icon="vxe-icon-add" status="primary" circle @click="rowAdd"></vxe-button>
+            </template>
+            <template #seq_del="{ seq, row, column }">
+                <span :class="['seq', (disabled || column.params?.hide_del(row)) && 'is-disabled']">{{ seq }}</span>
+                <vxe-button-group v-if="!disabled && !column.params?.hide_del(row)" status="error" circle>
+                    <vxe-button icon="vxe-icon-minus" @click="rowDel(row)"></vxe-button>
                 </vxe-button-group>
             </template>
 
@@ -27,10 +33,12 @@
             </template>
 
             <template v-for="(_, slotName) in $slots" #[slotName]="context">
-                <slot :name="slotName" v-bind="{ ...context }"></slot>
+                <slot :name="slotName" v-bind="context"></slot>
             </template>
         </vxe-grid>
     </el-main>
+
+    <select-table v-if="dialog" ref="selectTableRef" v-bind="selectTableOptions" @success="selectChange" @closed="dialog = false"></select-table>
 </template>
 
 <script setup>
@@ -40,21 +48,30 @@ domZIndex.setCurrent(domZIndex.getMax() + 1);
 
 import XEUtils from "xe-utils";
 import scUploadFile from "@/components/scUpload/file";
+import selectTable from "@/components/scFormTable/detail";
 
-const $emit = defineEmits(["update:modelValue"]);
+const $emit = defineEmits(["update:modelValue", "success"]);
 const props = defineProps({
     modelValue: { type: Array, default: () => [] },
     disabled: { type: Boolean, default: false },
-    hideAdd: { type: Boolean, default: false },
+    tableKey: { type: String, default: "" },
     addTemplate: { type: Object, default: () => {} },
-
+    
     layouts: { type: Array, default: () => [["Top", "Form"], ["Toolbar", "Table", "Bottom", "Pager"]] },
     rowKey: { type: String, default: "id" },
     columns: { type: Array, default: () => [] },
     editRules: { type: Object, default: () => {} },
     footerField: { type: Array, default: () => [] },
-    mergeFooterItems: { type: Array, default: () => [] }
-})
+    mergeFooterItems: { type: Array, default: () => [] },
+
+    selectOptions: { type: Object, default: () => {} },
+    add_success: { type: Function, default: (oldValue, newValue) => newValue }
+});
+
+const titleMap = reactive({
+    stage: "工序选择",
+    material: "产品选择"
+});
 
 const gridOptions = reactive({
     id: "xGride-form-table",
@@ -90,11 +107,18 @@ const gridOptions = reactive({
             })
         ]
     }
-})
+});
+
+const selectTableOptions = reactive({
+    tableKey: props.tableKey,
+    multiple: true,
+    options: {
+        rowKey: props.rowKey,
+        ...props.selectOptions
+    }
+});
 
-watch(() => gridOptions.data, val => {
-    xGrid.value?.recalculate()
-    $emit("update:modelValue", val)}, { deep: true });
+watch(() => gridOptions.data, val => $emit("update:modelValue", val), { deep: true });
 
 const xGrid = ref();
 const rowAdd = async () => {
@@ -103,7 +127,14 @@ const rowAdd = async () => {
 }
 const rowDel = row => gridOptions.data = XEUtils.filter(gridOptions.data, item => item[props.rowKey] !== row[props.rowKey]);
 
-const selectChange = async records => {
+const selectTableRef = ref();
+const dialog = ref(false);
+const table_add = () => {
+    dialog.value = true;
+    nextTick(() => selectTableRef.value?.setData(gridOptions.data));
+}
+const selectChange = async array => {
+    const records = props.add_success(gridOptions.data, array);
     gridOptions.data = XEUtils.filter(gridOptions.data, item => XEUtils.find(records, row => row[props.rowKey] == item[props.rowKey]));
     
     const newRecords = XEUtils.filter(records, item => !XEUtils.find(gridOptions.data, row => row[props.rowKey] == item[props.rowKey]));
@@ -111,11 +142,19 @@ const selectChange = async records => {
     gridOptions.data = gridOptions.data.concat(newRows);
 }
 
-const hhhhh = (e) => {console.log(e)} 
+const validateFormTable = async () => {
+    const errMap = await xGrid.value.validate(true)
+    if (errMap) {
+        ElMessage.warning(`请维护${XEUtils.last(XEUtils.objectMap(errMap, item => XEUtils.first(item).column.title))}`);
+        return false;
+    } else {
+        return true;
+    }
+}
 
 defineExpose({
-    selectChange
-})
+    validateFormTable
+});
 </script>
 
 <style scoped>

+ 2 - 2
src/components/scIconSelect/index.vue

@@ -92,7 +92,7 @@ export default {
             if (this.disabled) return false;
             this.dialogVisible = true;
             this.searchText = "";
-            if (!config.selectIcon(this.value).prefix) this.tabsData[2].icons = XEUtils.uniq([...this.tabsData[2].icons, this.value]);
+            if (this.value && !config.selectIcon(this.value).prefix) this.tabsData[2].icons = XEUtils.uniq([...this.tabsData[2].icons, this.value]);
             nextTick(() => document.querySelector(".select-icon")?.scrollIntoView({ block: "center" }));
         },
 
@@ -109,7 +109,7 @@ export default {
 
         search(text) {
             let filterData = XEUtils.clone(config.icons, true);
-            if (!config.selectIcon(this.value).prefix) filterData[2].icons = XEUtils.uniq([...filterData[2].icons, this.value]);
+            if (this.value && !config.selectIcon(this.value).prefix) filterData[2].icons = XEUtils.uniq([...filterData[2].icons, this.value]);
 
             if (text) {
                 XEUtils.arrayEach(filterData, item => {

+ 1 - 1
src/components/scPageHeader/index.vue

@@ -30,7 +30,7 @@
             </template>
 
             <template v-for="(_, slotName) in $slots" #[slotName]="context">
-                <slot :name="slotName" v-bind="{ ...context }"></slot>
+                <slot :name="slotName" v-bind="context"></slot>
             </template>
         </el-page-header>
 	</div>

+ 65 - 39
src/components/scTable/index.vue

@@ -1,6 +1,7 @@
 <!--
  * @Descripttion: 数据表格组件
  * @version: 1.10
+ * 待重构
 -->
 
 <template>
@@ -13,7 +14,7 @@
 
             <!-- table-column / 操作 -->
             <template v-for="(_, slotName) in $slots" #[slotName]="context">
-                <slot :name="slotName" v-bind="{ ...context, row: XEUtils.get(context, '$grid', XEUtils.get(context, '$table'))?.getData(context.rowIndex) || context.row }"></slot>
+                <slot :name="slotName" v-bind="formatScoped(context)"></slot>
             </template>
         </vxe-grid>
     </el-main>
@@ -24,17 +25,17 @@
 import domZIndex from "dom-zindex";
 domZIndex.setCurrent(domZIndex.getMax() + 1);
 
+import VxeUI from "vxe-pc-ui";
 import XEUtils from "xe-utils";
 import store from "@/store";
 import config from "@/config/table";
 import pagerBatchDel from "./renderer/pager-batch-del";
-import { nextTick } from "vue";
 
 const props = defineProps({
     apiObj: { type: Object, default: () => {} },
     apiKey: { type: String, default: () => "get" },
     rowKey: { type: String, default: "id" },
-    minHeight: { type: [String, Number], default: 144 },
+    minHeight: { type: [String, Number], default: VxeUI.getConfig().table.minHeight || 144 },
     maxHeight: { type: [String, Number] },
     layouts: { type: Array, default: () => [["Top", "Form"], ["Toolbar", "Table", "Bottom", "Pager"]] },
     checkedRows: { type: Array, default: () => [] },
@@ -53,8 +54,7 @@ const props = defineProps({
 })
 
 const xGrid = ref();
-const selectedRows = ref([]);
-const gridOptions = ref({
+const gridOptions = reactive({
     id: "xGride-table",
     loading: false,
     minHeight: props.minHeight,
@@ -62,7 +62,7 @@ const gridOptions = ref({
     size: "mini",
     align: "center",
     data: [],
-    columns: XEUtils.map(props.columns, item => ({ ...item, [item.type != "seq" && "exportMethod"]: ({ row, column }) => row[column?.field] || "" })),
+    columns: props.columns,
     showOverflow: true,
     keepSource: true,
     layouts: [...props.layouts],
@@ -89,7 +89,7 @@ const gridOptions = ref({
                 slots: { default: "queryAction" },
                 className: ({ $grid, item, data }) => {
                     const showItems = XEUtils.filter(XEUtils.orderBy(XEUtils.get(props, "formConfig.items", []), "orderBy"), formItem => (XEUtils.isUndefined(formItem.visible) || formItem.visible) && (XEUtils.isUndefined(formItem.visibleMethod) || formItem.visibleMethod({ data })));
-                    const spanItems = (!gridOptions.value.formConfig.collapseStatus && showItems) || XEUtils.filter(showItems, f_item => !f_item.folding);
+                    const spanItems = (!gridOptions.formConfig.collapseStatus && showItems) || XEUtils.filter(showItems, f_item => !f_item.folding);
 
                     let spanItemsSum = 0;
                     XEUtils.arrayEach(spanItems, s_item => {
@@ -140,7 +140,8 @@ const gridOptions = ref({
     },
     exportConfig: {
         types: ["xlsx"],
-        modes: XEUtils.find(props.columns, item => XEUtils.includes(config.exportExcludeFields, item.type)) && ["current", "selected", "all"] || ["current", "all"],
+        modes: XEUtils.find(props.columns, item => XEUtils.get(item, "visible", true) && XEUtils.includes(config.exportExcludeFields, item.type)) && ["current", "selected", "all"] || ["current", "all"],
+        columns: XEUtils.filter(props.columns, item => !(XEUtils.includes(config.exportExcludeFields, item.type)) && XEUtils.get(item, "visible", true)),
     },
     rowConfig: {
         keyField: props.rowKey,
@@ -157,10 +158,16 @@ const gridOptions = ref({
     cellConfig: {
         height: 36
     },
+    radioConfig: {
+        reserve: true, // 是否保留勾选状态
+        highlight: true,
+    },
     checkboxConfig: {
+        reserve: true, // 是否保留勾选状态
         highlight: true,
-        range: true, // 鼠标在复选框的列内滑动选中或取消指定行
-        isShiftKey: true // 鼠标点击和 shift 键选取指定范围的行
+        // 树结构有冲突
+        range: computed(() => !XEUtils.has(props.options, "treeConfig")), // 鼠标在复选框的列内滑动选中或取消指定行
+        isShiftKey: computed(() => !XEUtils.has(props.options, "treeConfig")) // 鼠标点击和 shift 键选取指定范围的行
     },
     tooltipConfig: {
         enterable: true
@@ -189,14 +196,19 @@ const gridOptions = ref({
     ...props.options
 })
 
-watch(() => xGrid.value?.getCheckboxRecords(), val => selectedRows.value = val);
-watch(() => props.options, val => XEUtils.merge(gridOptions.value, val), { deep: true });
-watch(() => gridOptions.value.data, val => {
+watch(() => props.options, val => XEUtils.merge(gridOptions, val), { deep: true });
+watch(() => gridOptions.data, val => {
     if (props.columns.find(item => item.type == "checkbox" && XEUtils.get(item, "visible", true)) && props.checkedRows.length) {
         xGrid.value?.setCheckboxRow(props.checkedRows, true);
     }
+
+    if (props.columns.find(item => item.type == "radio" && XEUtils.get(item, "visible", true)) && props.checkedRows.length) {
+        xGrid.value?.setRadioRow(XEUtils.first(props.checkedRows));
+    }
 }, { deep: true });
 
+const formatScoped = context => ({ ...context, row: XEUtils.get(XEUtils.findTree(XEUtils.get(context, "$grid", XEUtils.get(context, "$table"))?.getData(), item => item.id == context.rowid), "item") || context.row });
+
 addEventListener("resize", () => resizeTable());
 onMounted(() => {
     resizeTable();
@@ -205,8 +217,8 @@ onMounted(() => {
 
 const resizeTable = () => {
     nextTick(() => {
-        if (store.state.global.ismobile) XEUtils.set(gridOptions.value, "maxHeight", 1048);
-        else XEUtils.set(gridOptions.value, "maxHeight", (props.maxHeight || xGrid.value?.$el.parentElement.offsetHeight));
+        if (store.state.global.ismobile) XEUtils.set(gridOptions, "maxHeight", 1048);
+        else XEUtils.set(gridOptions, "maxHeight", (props.maxHeight || xGrid.value?.$el.parentElement.offsetHeight) - 12);
     });
 }
 
@@ -215,26 +227,26 @@ const getData = () => {
     nextTick(() => {
         if (!props.apiObj) {
             if (props.options.data && props.options.data.length > 0) {
-                gridOptions.value.pagerConfig.total = props.options.data.length;
+                gridOptions.pagerConfig.total = props.options.data.length;
                 return;
             }
 
-            gridOptions.value.data = [];
-            gridOptions.value.pagerConfig.total = 0;
+            gridOptions.data = [];
+            gridOptions.pagerConfig.total = 0;
             return;
         }
 
-        gridOptions.value.loading = true;
-        const reqData = config.queryData(gridOptions.value, props.paramsColums);
+        gridOptions.loading = true;
+        const reqData = config.queryData(gridOptions, props.paramsColums);
         props.apiObj[props.apiKey](reqData).then(res => {
             const response = config.parseData(res);
-            gridOptions.value.data = response.data || [];
-            gridOptions.value.pagerConfig.total = response.total || 0;
-            gridOptions.value.loading = false;
+            gridOptions.data = response.data || [];
+            gridOptions.pagerConfig.total = response.total || 0;
+            gridOptions.loading = false;
         }).catch(error => {
-            gridOptions.value.loading = false;
-            gridOptions.value.data = [];
-            gridOptions.value.pagerConfig.total = 0;
+            gridOptions.loading = false;
+            gridOptions.data = [];
+            gridOptions.pagerConfig.total = 0;
         });
     });
 }
@@ -243,7 +255,7 @@ const getAllData = () => {
     return new Promise((resolve, reject) => {
         if (!props.apiObj) resolve([]);
         
-        const reqData = config.queryExport(gridOptions.value, props.paramsColums);
+        const reqData = config.queryExport(gridOptions, props.paramsColums);
         props.apiObj[props.apiKey](reqData).then(res => {
             const response = config.parseData(res);
             resolve(response.data || [])
@@ -252,34 +264,48 @@ const getAllData = () => {
 }
 
 const pageChangeEvent = ({ pageSize, currentPage }) => {
-    gridOptions.value.pagerConfig.currentPage = currentPage;
-    gridOptions.value.pagerConfig.pageSize = pageSize;
+    gridOptions.pagerConfig.currentPage = currentPage;
+    gridOptions.pagerConfig.pageSize = pageSize;
     getData();
 }
 
 const searchData = (mode = "add") => {
-    if (mode == "add") gridOptions.value.pagerConfig.currentPage = 1;
-    gridOptions.value.pagerConfig.pageSize = config.pageSize;
+    if (mode == "add") gridOptions.pagerConfig.currentPage = 1;
+    gridOptions.pagerConfig.pageSize = config.pageSize;
     getData();
 }
 
 const resetData = () => {
-    gridOptions.value.pagerConfig.currentPage = 1;
-    gridOptions.value.pagerConfig.pageSize = config.pageSize;
+    gridOptions.pagerConfig.currentPage = 1;
+    gridOptions.pagerConfig.pageSize = config.pageSize;
     xGrid.value.resetForm();
     
     getData();
 }
 
-const formCollapseEvent = ({ collapse }) => gridOptions.value.formConfig.collapseStatus = collapse;
+const getSelectRows = () => {
+    if (props.columns.find(item => item.type == "checkbox" && XEUtils.get(item, "visible", true))) {
+        const selectRecords = xGrid.value?.getCheckboxRecords();
+        const selectReserveRecords = xGrid.value?.getCheckboxReserveRecords();
+        return selectRecords.concat(selectReserveRecords);
+    }
+    if (props.columns.find(item => item.type == "radio" && XEUtils.get(item, "visible", true))) {
+        const currRow = xGrid.value?.getRadioRecord();
+        const currReserveRow = xGrid.value?.getRadioReserveRecord();
+        return [currRow || currReserveRow];
+    }
+    return [];
+}
 
-const toggleTableLoading = value => gridOptions.value.loading = value;
+const formCollapseEvent = ({ collapse }) => gridOptions.formConfig.collapseStatus = collapse;
 
-const toggleFormEnabled = () => gridOptions.value.formConfig.enabled = !gridOptions.value.formConfig.enabled;
+const toggleTableLoading = value => gridOptions.loading = value;
+
+const toggleFormEnabled = () => gridOptions.formConfig.enabled = !gridOptions.formConfig.enabled;
 
 const toggleTableExpand = () => xGrid.value.getTreeExpandRecords().length && xGrid.value.clearTreeExpand() || xGrid.value.setAllTreeExpand(true);
 
-const toggleToolbarProps = obj => XEUtils.objectEach(obj, (value, key) => XEUtils.set(gridOptions.value.toolbarConfig, key, value));
+const toggleToolbarProps = obj => XEUtils.objectEach(obj, (value, key) => XEUtils.set(gridOptions.toolbarConfig, key, value));
 
 const reloadColumn = columns => xGrid.value.reloadColumn(columns);
 
@@ -297,11 +323,11 @@ const table_batch_del = ids => {
             ElMessage.success("操作成功");
             getData();
         });
-    });
+    }).catch(() => {});
 }
 
 defineExpose({
-    selectedRows,
+    getSelectRows,
     toggleTableLoading,
     toggleFormEnabled,
     toggleTableExpand,

+ 8 - 1
src/components/scTable/renderer/cell-tag.vue

@@ -7,7 +7,14 @@ import XEUtils from "xe-utils";
 
 const colorDic = {
     enable: "success",
-    disable: "red"
+    disable: "danger",
+
+    pending: "processing",
+    approved: "success",
+    rejected: "red",
+    
+    executing: "warning",
+    finished: "default",
 }
 
 const props = defineProps({

+ 9 - 4
src/components/scTable/renderer/table-search.vue

@@ -22,15 +22,18 @@ const searchInTable = () => {
     if (filterName) {
         const filterRE = new RegExp(filterName.replace(/([.?*+^$[\]\\(){}|-])/gi, "\\$1"), "gi");
         const searchColumns = props.params.$grid?.getColumns()?.filter(col => col.field);
-
-        const data = tableData?.map(item => {
+        
+        const mapTree = XEUtils.mapTree(tableData, item => {
             searchColumns?.forEach(col => XEUtils.set(
                 item,
                 col.field,
                 props.params.$grid?.getCellLabel(item, col.field)
             ));
             return item;
-        })?.filter(item => searchColumns?.some(col => XEUtils.toValueString(XEUtils.get(item, col.field)).toLowerCase().includes(filterName)))?.map(row => {
+        });
+        XEUtils.searchTree(mapTree, item => searchColumns?.some(col => XEUtils.toValueString(XEUtils.get(item, col.field)).toLowerCase().includes(filterName)), { isEvery: true });
+        
+        const data = XEUtils.mapTree(mapTree, row => {
             const item = Object.assign({}, row);
             searchColumns?.filter(col => col.type === "html").forEach(col => {
                 XEUtils.set(
@@ -40,8 +43,10 @@ const searchInTable = () => {
                 )
             });
             return item;
-        })
+        });
+       
         props.params.$grid?.reloadData(data);
+        props.params.$grid?.setAllTreeExpand(true);
     } else props.params.$grid?.reloadData(tableData);
 }
 </script>

+ 54 - 0
src/components/scTableInput/index.vue

@@ -0,0 +1,54 @@
+<!--
+ * @Descripttion: form-input-table
+ * @version: 1.1
+ * @Date: 2025年11月17日12:10:06
+-->
+
+<template>
+    <div class="sc-table-input" @click="show">
+        <el-input v-model="defaultValue" readonly v-bind="$attrs">
+            <template #suffix><sc-iconify icon="mingcute:more-3-line"></sc-iconify></template>
+
+            <!-- table-column / 操作 -->
+            <template v-for="(_, slotName) in $slots" #[slotName]="context">
+                <slot :name="slotName" v-bind="context"></slot>
+            </template>
+        </el-input>
+    </div>
+
+    <select-table v-if="dialog" ref="selectTableRef" v-bind="{ tableKey, options }" @success="success" @closed="dialog = false"></select-table>
+</template>
+
+<script setup>
+import XEUtils from "xe-utils";
+import selectTable from "@/components/scFormTable/detail";
+
+const $emit = defineEmits(["update:modelValue"]);
+const props = defineProps({
+    modelValue: { type: Object, default: () => {} },
+    valueKey: { type: String, default: () => "id" },
+    tableKey: { type: String, default: () => "" },
+    hideShow: { type: Boolean, default: () => false },
+    options: { type: Object, default: () => {} }
+});
+
+const dialog = ref(false);
+const defaultValue = ref(XEUtils.get(props.modelValue, props.valueKey, null));
+
+const selectTableRef = ref();
+const show = () => {
+    if (!props.hideShow) {
+        dialog.value = true;
+        nextTick(() => selectTableRef.value?.setData(defaultValue.value ? [props.modelValue] : []));
+    }
+}
+
+const success = array => {
+    defaultValue.value = XEUtils.get(XEUtils.first(array) || {}, props.valueKey, null);
+    $emit("update:modelValue", XEUtils.first(array) || {});
+}
+</script>
+
+<style scoped>
+.sc-table-input {width: 100%;}
+</style>

+ 112 - 223
src/components/scTableSelect/index.vue

@@ -1,234 +1,123 @@
 <!--
- * @Descripttion: 表格选择器组件
- * @version: 1.3
- * @Author: sakuya
- * @Date: 2021年6月10日10:04:07
- * @LastEditors: sakuya
- * @LastEditTime: 2022年6月6日21:50:36
+ * @Descripttion: form-input-table
+ * @version: 1.1
+ * @Date: 2025年12月02日10:00:06
+ * ******未完成******
 -->
 
 <template>
-	<el-select ref="select" v-model="defaultValue" :size="size" :clearable="clearable" :multiple="multiple" :collapse-tags="collapseTags" :collapse-tags-tooltip="collapseTagsTooltip" :filterable="filterable" :placeholder="placeholder" :disabled="disabled" :filter-method="filterMethod" @remove-tag="removeTag" @visible-change="visibleChange" @clear="clear">
-		<template #empty>
-			<div class="sc-table-select__table" :style="{width: tableWidth+'px'}" v-loading="loading">
-				<div class="sc-table-select__header">
-					<slot name="header" :form="formData" :submit="formSubmit"></slot>
-				</div>
-				<el-table ref="table" :data="tableData" :height="245" :highlight-current-row="!multiple" @row-click="click" @select="select" @select-all="selectAll">
-					<el-table-column v-if="multiple" type="selection" width="45"></el-table-column>
-					<el-table-column v-else type="index" width="45">
-						<template #default="scope"><span>{{scope.$index+(currentPage - 1) * pageSize + 1}}</span></template>
-					</el-table-column>
-					<slot></slot>
-				</el-table>
-				<div class="sc-table-select__page">
-					<el-pagination small background layout="prev, pager, next" :total="total" :page-size="pageSize" v-model:currentPage="currentPage" @current-change="reload"></el-pagination>
-				</div>
-			</div>
-		</template>
-	</el-select>
+    <div class="sc-table-select">
+        <vxe-table-select v-model="defaultValue" size="medium" :columns="columns" :options="tableData" :grid-config="gridConfig" transfer :placeholder="placeholder" @change="change">
+            <template #prefix><sc-iconify icon="mingcute:more-3-line"></sc-iconify></template>
+            
+            <template #queryAction>
+                <el-button type="primary" auto-insert-space @click="searchData">查询</el-button>
+                <el-button auto-insert-space @click="resetData">重置</el-button>
+            </template>
+        </vxe-table-select>
+    </div>
 </template>
 
-<script>
-	import config from "@/config/tableSelect";
+<script setup>
+import XEUtils from "xe-utils";
+import config from "@/config/table";
+import configSelect from "@/config/tableSelect";
 
-	export default {
-		props: {
-			modelValue: null,
-			apiObj: { type: Object, default: () => {} },
-			params: { type: Object, default: () => {} },
-			placeholder: { type: String, default: "请选择" },
-			size: { type: String, default: "default" },
-			clearable: { type: Boolean, default: false },
-			multiple: { type: Boolean, default: false },
-			filterable: { type: Boolean, default: false },
-			collapseTags: { type: Boolean, default: false },
-			collapseTagsTooltip: { type: Boolean, default: false },
-			disabled: { type: Boolean, default: false },
-			tableWidth: {type: Number, default: 400},
-			mode: { type: String, default: "popover" },
-			props: { type: Object, default: () => {} }
-		},
-		data() {
-			return {
-				loading: false,
-				keyword: null,
-				defaultValue: [],
-				tableData: [],
-				pageSize: config.pageSize,
-				total: 0,
-				currentPage: 1,
-				defaultProps: {
-					label: config.props.label,
-					value: config.props.value,
-					page: config.request.page,
-					pageSize: config.request.pageSize,
-					keyword: config.request.keyword
-				},
-				formData: {}
-			}
-		},
-		computed: {
+const $emit = defineEmits(["update:modelValue"]);
+const props = defineProps({
+    modelValue: { type: Object, default: () => {} },
+    apiObj: { type: Object, default: () => {} },
+    apiKey: { type: String, default: () => "all" },
+    optionsProps: { type: Object, default: () => configSelect.props },
+    placeholder: { type: String, default: "请选择" },
+    formConfig: { type: Object, default: () => {} },
+    paramsColums: { type: Array, default: () => [] },
+    columns: { type: Array, default: () => [] },
+    pagerConfig: { type: Object, default: () => {} },
+    options: { type: Object, default: () => {} }
+});
 
-		},
-		watch: {
-			modelValue:{
-				handler(){
-					this.defaultValue = this.modelValue
-					this.autoCurrentLabel()
-				},
-				deep: true
-			}
-		},
-		mounted() {
-			this.defaultProps = Object.assign(this.defaultProps, this.props);
-			this.defaultValue =  this.modelValue
-			this.autoCurrentLabel()
-		},
-		methods: {
-			//表格显示隐藏回调
-			visibleChange(visible){
-				if(visible){
-					this.currentPage = 1
-					this.keyword = null
-					this.formData = {}
-					this.getData()
-				}else{
-					this.autoCurrentLabel()
-				}
-			},
-			//获取表格数据
-			async getData(){
-				this.loading = true;
-				var reqData = {
-					[this.defaultProps.page]: this.currentPage,
-					[this.defaultProps.pageSize]: this.pageSize,
-					[this.defaultProps.keyword]: this.keyword
-				}
-				Object.assign(reqData, this.params, this.formData)
-				var res = await this.apiObj.get(reqData);
-				var parseData = config.parseData(res)
-				this.tableData = parseData.rows;
-				this.total = parseData.total;
-				this.loading = false;
-				//表格默认赋值
-				this.$nextTick(() => {
-					if(this.multiple){
-						this.defaultValue.forEach(row => {
-							var setrow = this.tableData.filter(item => item[this.defaultProps.value]===row[this.defaultProps.value])
-							if(setrow.length > 0){
-								this.$refs.table.toggleRowSelection(setrow[0], true);
-							}
-						})
-					}else{
-						var setrow = this.tableData.filter(item => item[this.defaultProps.value]===this.defaultValue[this.defaultProps.value])
-						this.$refs.table.setCurrentRow(setrow[0]);
-					}
-					this.$refs.table.setScrollTop(0)
-				})
-			},
-			//插糟表单提交
-			formSubmit(){
-				this.currentPage = 1
-				this.keyword = null
-				this.getData()
-			},
-			//分页刷新表格
-			reload(){
-				this.getData()
-			},
-			//自动模拟options赋值
-			autoCurrentLabel(){
-				this.$nextTick(() => {
-					if(this.multiple){
-						this.$refs.select.selected.forEach(item => {
-							item.currentLabel = item.value[this.defaultProps.label]
-						})
-					}else{
-						this.$refs.select.selectedLabel = this.defaultValue[this.defaultProps.label]
-					}
-				})
-			},
-			//表格勾选事件
-			select(rows, row){
-				var isSelect = rows.length && rows.indexOf(row) !== -1
-				if(isSelect){
-					this.defaultValue.push(row)
-				}else{
-					this.defaultValue.splice(this.defaultValue.findIndex(item => item[this.defaultProps.value] == row[this.defaultProps.value]), 1)
-				}
-				this.autoCurrentLabel()
-				this.$emit('update:modelValue', this.defaultValue);
-				this.$emit('change', this.defaultValue);
-			},
-			//表格全选事件
-			selectAll(rows){
-				var isAllSelect = rows.length > 0
-				if(isAllSelect){
-					rows.forEach(row => {
-						var isHas = this.defaultValue.find(item => item[this.defaultProps.value] == row[this.defaultProps.value])
-						if(!isHas){
-							this.defaultValue.push(row)
-						}
-					})
-				}else{
-					this.tableData.forEach(row => {
-						var isHas = this.defaultValue.find(item => item[this.defaultProps.value] == row[this.defaultProps.value])
-						if(isHas){
-							this.defaultValue.splice(this.defaultValue.findIndex(item => item[this.defaultProps.value] == row[this.defaultProps.value]), 1)
-						}
-					})
-				}
-				this.autoCurrentLabel()
-				this.$emit('update:modelValue', this.defaultValue);
-				this.$emit('change', this.defaultValue);
-			},
-			click(row){
-				if(this.multiple){
-					//处理多选点击行
-				}else{
-					this.defaultValue = row
-					this.$refs.select.blur()
-					this.autoCurrentLabel()
-					this.$emit('update:modelValue', this.defaultValue);
-					this.$emit('change', this.defaultValue);
-				}
-			},
-			//tags删除后回调
-			removeTag(tag){
-				var row = this.findRowByKey(tag[this.defaultProps.value])
-				this.$refs.table.toggleRowSelection(row, false);
-				this.$emit('update:modelValue', this.defaultValue);
-			},
-			//清空后的回调
-			clear(){
-				this.$emit('update:modelValue', this.defaultValue);
-			},
-			// 关键值查询表格数据行
-			findRowByKey (value) {
-				return this.tableData.find(item => item[this.defaultProps.value] === value)
-			},
-			filterMethod(keyword){
-				if(!keyword){
-					this.keyword = null;
-					return false;
-				}
-				this.keyword = keyword;
-				this.getData()
-			},
-			// 触发select隐藏
-			blur(){
-				this.$refs.select.blur();
-			},
-			// 触发select显示
-			focus(){
-				this.$refs.select.focus();
-			}
-		}
-	}
+const defaultValue = ref(XEUtils.get(props.modelValue, props.optionsProps.value, null));
+
+const tableData = ref([]);
+
+const gridConfig = reactive({
+    border: "full",
+    size: "mini",
+    showOverflow: true,
+    keepSource: true,
+    formConfig: {
+        enabled: true,
+        className: "vxe-table-query",
+        titleAlign: "right",
+        collapseStatus: true,
+        span: 12,
+        items: [
+            ...XEUtils.map(XEUtils.orderBy(XEUtils.get(props, "formConfig.items", []), "orderBy"), (formItem, formIndex) => {
+                return {
+                    ...formItem,
+                    className: ({ $grid, item, data }) => {
+                        const showItems = XEUtils.filter(XEUtils.orderBy(XEUtils.get(props, "formConfig.items", []), "orderBy"), f_item => (XEUtils.isUndefined(f_item.visible) || f_item.visible) && (XEUtils.isUndefined(f_item.visibleMethod) || f_item.visibleMethod({ data })));
+                        const index = XEUtils.findIndexOf(showItems, f_item => f_item.field == item.field);
+                        item.folding = index > 2;
+                        XEUtils.set(formItem, "folding", index > 2);
+                        return "";
+                    }
+                }
+            })
+        ],
+        ...XEUtils.omit(XEUtils.get(props, "formConfig", {}), "items")
+    },
+    rowConfig: {
+        keyField: props.optionsProps.value,
+        useKey: true,
+        isHover: true
+    },
+    columnConfig: {
+        useKey: true,
+        resizable: true // 列宽拖动功能
+    },
+    headerCellConfig: {
+        height: 36
+    },
+    cellConfig: {
+        height: 36
+    },
+    tooltipConfig: {
+        enterable: true
+    },
+    pagerConfig: {
+        pageSizes: config.pageSizes,
+        layouts: config.layouts,
+        currentPage: 1,
+        pageSize: config.pageSize,
+        total: 0
+    },
+})
+
+// 获取数据
+const getData = () => {
+    return new Promise((resolve, reject) => {
+        if (!props.apiObj) tableData.value = [];
+        
+        const reqData = config.queryData(gridConfig, props.paramsColums);
+        props.apiObj[props.apiKey](reqData).then(res => {
+            const response = config.parseData(res);
+            tableData.value = XEUtils.map(response.data || [], item => ({ ...item, label: item.name,value: item.id }));
+        }).catch(error => {
+            tableData.value = [];
+        });
+    });
+}
+getData();
+
+const change = ({ row }) => $emit("update:modelValue", row);
 </script>
 
 <style scoped>
-	.sc-table-select__table {padding:12px;}
-	.sc-table-select__page {padding-top: 12px;}
-</style>
+.sc-table-select, .sc-table-select .vxe-table-select {width: 100%;}
+.sc-table-select :deep(.vxe-input) {flex-direction: row-reverse;}
+.sc-table-select :deep(.vxe-input) .vxe-input--suffix {display: none;}
+.sc-table-select :deep(.vxe-input) .vxe-input--prefix {border-radius: 0 var(--vxe-ui-base-border-radius) var(--vxe-ui-base-border-radius) 0;}
+.sc-table-select .el-input__inner::placeholder {color: var(--vxe-ui-input-placeholder-color);}
+</style>

+ 2 - 2
src/layout/components/sideM.vue

@@ -4,8 +4,8 @@
 	<el-drawer ref="mobileNavBox" title="移动端菜单" :size="240" v-model="nav" direction="ltr" :with-header="false" destroy-on-close>
 		<el-container class="mobile-nav">
             <el-header class="logo-bar">
-				<img class="logo" src="img/logo.png">
-                <span>{{ $CONFIG.APP_NAME }}</span>
+				<img class="logo" :src="require('/public/img/logo.png')">
+                <vxe-text-ellipsis :title="$CONFIG.APP_NAME" :content="$CONFIG.APP_NAME"></vxe-text-ellipsis>
 			</el-header>
             <el-main>
                 <el-scrollbar>

+ 9 - 6
src/layout/index.vue

@@ -4,7 +4,7 @@
         <header class="aminui-header">
             <div class="aminui-header-left">
                 <div class="logo-bar">
-                    <img class="logo" src="img/logo.png">
+                    <img class="logo" :src="require('/public/img/logo.png')">
                     <span>{{ $CONFIG.APP_NAME }}</span>
                 </div>
                 <el-tabs v-if="!ismobile" ref="menuTabs" class="nav-menu" v-model="tabsName" @tab-change="showMenu">
@@ -32,10 +32,13 @@
                         </el-menu>
                     </el-scrollbar>
                 </div>
+
+                <div class="aminui-side-collapse" @click="$store.commit('TOGGLE_menuIsCollapse')">
+                    <sc-iconify :icon="menuIsCollapse ? 'ep:arrow-right': 'ep:arrow-left'" size="14"></sc-iconify>
+                </div>
             </div>
             <Side-m v-if="ismobile"></Side-m>
             <div class="aminui-body el-container">
-				<Topbar v-if="!ismobile && currentMenu?.children?.length"></Topbar>
                 <Tags v-if="!ismobile && layoutTags"></Tags>
                 <div class="aminui-main" id="aminui-main">
                     <router-view v-slot="{ Component }">
@@ -54,8 +57,8 @@
         <header class="aminui-header">
 			<div class="aminui-header-left">
 				<div class="logo-bar">
-					<img class="logo" src="img/logo.png">
-					<span>{{ $CONFIG.APP_NAME }}</span>
+					<img class="logo" :src="require('/public/img/logo.png')">
+                    <span>{{ $CONFIG.APP_NAME }}</span>
 				</div>
 			</div>
 			<div class="aminui-header-right">
@@ -94,8 +97,8 @@
             <div v-if="!ismobile" :class="['aminui-side hasMenu', menuIsCollapse && 'isCollapse']">
                 <div class="aminui-side-top">
                     <div class="logo-bar">
-                        <img class="logo" src="img/logo.png">
-                        <span v-if="!menuIsCollapse">{{ $CONFIG.APP_NAME }}</span>
+                        <img class="logo" :src="require('/public/img/logo.png')">
+                        <vxe-text-ellipsis v-if="!menuIsCollapse" :title="$CONFIG.APP_NAME" :content="$CONFIG.APP_NAME"></vxe-text-ellipsis>
                     </div>
                 </div>
                 <div class="aminui-side-scroll">

+ 5 - 3
src/router/index.js

@@ -87,14 +87,15 @@ router.beforeEach(async (to, from, next) => {
                 }).then(() => {
                     tool.cookie.remove("MES_TOKEN")
                     location.reload() // 为了重新实例化vue-router对象 避免bug
-                })
+                }).catch(() => {})
             }
 
             routes_404_r = router.addRoute(routes_empty);
         } else {
-            tool.data.set("MENU", [...userRoutes, ...mapAsyncMenu(apiMenu)])
+            const zeroMenu = XEUtils.filter(mapAsyncMenu(apiMenu), item => item.type == 0)
+            tool.data.set("MENU", [...userRoutes, ...zeroMenu])
 
-            const menuRouter = XEUtils.mapTree([...userRoutes, ...mapAsyncMenu(apiMenu)], item => {
+            const menuRouter = XEUtils.mapTree([...userRoutes, ...zeroMenu], item => {
                 return {
                     ...XEUtils.omit(item, "component"),
                     [item.component ? "component" : "redirect"]: item.component ? loadComponent(item.component) : XEUtils.get(XEUtils.first(item.children), "path", "")
@@ -142,6 +143,7 @@ function mapAsyncMenu(menus) {
     return XEUtils.mapTree(XEUtils.toArrayTree(menus, { parentKey: "pid", sortKey: "menuSort" }), item => {
         return {
             name: item.title,
+            type: item.type,
             path: XEUtils.map(XEUtils.get(XEUtils.findTree(menus, parent => parent.id == item.pid), "nodes", []), node => XEUtils.get(node, "path", "")).join("") + item.path,
             iframe: item.iframe,
             meta: { title: item.title, icon: item.icon, hidden: item.hidden },

+ 7 - 3
src/style/app.scss

@@ -47,12 +47,15 @@ a,button,input,textarea{-webkit-tap-highlight-color:rgba(0,0,0,0);box-sizing: bo
 .aminui-header .nav-menu .el-tabs__item.is-active {background: rgba(255, 255, 255, 0.1);color: #fff;}
 
 /* 左侧菜单 */
-.aminui-side {display: flex;flex-flow: column;flex-shrink:0;width:210px;background: #fff;box-shadow: 2px 0 8px 0 rgba(29,35,41,.05);border-right: 1px solid #e6e6e6;transition:width 0.3s;}
+.aminui-side {position: relative;display: flex;flex-flow: column;flex-shrink:0;width:210px;background: #fff;box-shadow: 2px 0 8px 0 rgba(29, 35, 41, .05);border-right: 1px solid #e6e6e6;transition:width 0.3s;}
 .aminui-side-top {border-bottom: 1px solid #ebeef5;height:50px;line-height: 50px;}
 .aminui-side-top h2 {height: 100%;padding:0 20px;font-size: 17px;color: #3c4a54;}
 .aminui-side-scroll {overflow: auto;overflow-x:hidden;flex: 1;}
 .aminui-side.isCollapse {width: 65px;}
 
+.aminui-side-collapse {position: absolute;right: -12px;top: 40px;z-index: 101;cursor: pointer;display: flex;align-items: center;justify-content: center;width: 24px;height: 24px;background-color: #fff;border-radius: 50%;color: rgba(0, 0, 0, .25);box-shadow: 0 2px 8px -2px #0000000d,0 1px 4px -1px #190f0f12,0 0 1px #00000014;transition: all .3s;}
+.aminui-side-collapse:hover {color: rgba(0, 0, 0, .95);}
+
 /* 智慧工地layout */
 .aminui-side.hasMenu {background: #304156;color: #fff;}
 .aminui-side.hasMenu .aminui-side-top {border-bottom: none;}
@@ -65,9 +68,10 @@ a,button,input,textarea{-webkit-tap-highlight-color:rgba(0,0,0,0);box-sizing: bo
 .aminui-side.hasMenu.isCollapse .logo-bar {justify-content: center;margin-left: 0;}
 
 /* 移动端菜单 */
-.mobile-nav .logo-bar {justify-content: unset;background: #222b45;border: none;font-size: 17px;font-weight: bold;color: #fff;}
+.mobile-nav {background: #222b45;}
+.mobile-nav .logo-bar {justify-content: unset;background: transparent!important;border: none;font-size: 17px;font-weight: bold;color: #fff;}
 .mobile-nav .logo-bar .logo {width: 30px;margin-right: 10px;vertical-align: bottom;}
-.mobile-nav .el-main {background: #222b45;padding: 0;}
+.mobile-nav .el-main {padding: 0;}
 .mobile-nav .el-menu {--el-menu-text-color: #fff;--el-menu-hover-text-color: #fff;--el-menu-bg-color: #222b45;--el-menu-hover-bg-color: rgb(26, 36, 49);--el-menu-active-color: var(--el-color-primary);}
 
 /* 右侧内容 */

+ 5 - 1
src/style/dark.scss

@@ -7,6 +7,8 @@ html.dark {
 	--el-color-primary-light-7: var(--el-color-primary-dark-6)!important;
 	--el-color-primary-light-5: var(--el-color-primary-dark-4)!important;
 	--el-color-primary-light-3: var(--el-color-primary-dark-3)!important;
+    --vxe-ui-font-primary-lighten-color: var(--el-color-primary-light-3)!important;
+    .vxe-button {transition: .1s;}
 
 	//背景
 	#app {background: var(--el-bg-color);}
@@ -25,8 +27,10 @@ html.dark {
     .aminui-header .nav-menu {background: var(--el-bg-color-overlay);} // width: 0 解决flex: 1 子元素超出父元素
 	.aminui-side {background: var(--el-bg-color-overlay);border-color: var(--el-border-color-light);}
     .aminui-side.hasMenu .el-menu, .side-menu-popper .el-menu {--el-menu-hover-bg-color: var(--el-color-primary-light-9);}
-	.aminui-side-top, .aminui-side-bottom {border-color: var(--el-border-color-light);}
+	.aminui-side-top {border-color: var(--el-border-color-light);}
 	.aminui-side-top h2 {color: var(--el-text-color-primary);}
+    .aminui-side-collapse {background-color: var(--el-bg-color-overlay);color: var(--el-border-color-light);}
+    .aminui-side-collapse:hover {color: rgba(255, 255, 255, .95);}
 	.aminui-topbar, .aminui-tags {background: var(--el-bg-color-overlay);border-color: var(--el-border-color-light);}
     .aminui-topbar .user-bar .panel-item:hover {background: rgba(255, 255, 255, 0.1);}
 

+ 143 - 0
src/utils/basicDic.js

@@ -1,5 +1,148 @@
 import XEUtils from "xe-utils";
 
+export const statusDic = {
+    enable: "启用",
+    disable: "停用"
+}
+
+export const reviewStatusDic = {
+    pending: "待审批",
+    approved: "审批通过",
+    rejected: "审批拒绝"
+}
+
+export const materialDic = {
+    type: {
+        raw_material: "原材料",
+        semi_finished: "半成品",
+        finished_product: "产成品",
+        trade_goods: "贸易品",
+        packaging_material: "包装材料"
+    },
+
+    needType: {
+        self_made: "自制",
+        out_purchase: "外购",
+        outsourcing: "委外"
+    },
+
+    typeRelation: {
+        self_made: ["semi_finished", "finished_product"],
+        out_purchase: ["raw_material", "trade_goods", "packaging_material"],
+        outsourcing: ["semi_finished", "finished_product"]
+    },
+
+    unit: {
+        PC: "个/件(其他小型独立单元)",
+        LOT: "批",
+        SHT: "张",
+        SET: "台",
+        KIT: "套",
+        CASE: "箱",
+        BAG: "包/袋",
+        RL: "卷",
+        DRUM: "桶",
+        BTL: "瓶",
+        CAN: "罐",
+        BOX: "盒",
+        PLT: "板/托",
+        M: "米",
+        CM: "厘米",
+        MM: "毫米",
+        IN: "英寸",
+        SQM: "平方米",
+        CBM: "立方米",
+        G: "克",
+        KG: "千克",
+        T: "吨",
+        LB: "磅",
+        OZ: "盎司",
+        L: "升",
+        ML: "毫升",
+        GAL: "加仑"
+    }
+}
+
+export const workmanshipDic = {
+    category: {
+        preparation: "准备工序",
+        processing: "加工工序",
+        inspection: "检验工序",
+        auxiliary: "辅助工序"
+    },
+
+    type: {
+        self_made: "自制",
+        outsourcing: "委外"
+    },
+
+    calcMethod: {
+        both_rates: "计件+计时都支持",
+        piece_rate: "计件",
+        time_rate: "计时",
+        non_prod_pay: "不计生产工资"
+    },
+
+    timeUnit: {
+        hour: "时",
+        minute: "分",
+        second: "秒"
+    }
+}
+
+export const qualityPlanTypeDic = {
+    full: "全检",
+    sampling: "抽检"
+}
+
+export const customerDic = {
+    // customer/supplier/outsourcing
+    type: {
+        enterprise: "企业",
+        individual: "个人/个体",
+        government: "政府机构",
+        institution: "事业单位",
+        other: "其他组织"
+    },
+
+    creditNo: {
+        enterprise: "统一社会信用代码",
+        individual: "身份证号",
+        government: "统一社会信用代码",
+        institution: "统一社会信用代码",
+        other: "其他身份标识"
+    },
+
+    tier: {
+        strategic: "战略客户",
+        VIP: "VIP客户",
+        core: "核心客户",
+        normal: "一般客户",
+        small: "小客户"
+    },
+
+    rating: {
+        A: "优秀",
+        B: "良好",
+        C: "一般/关注",
+        D: "限制/高风险"
+    }
+}
+
+export const planDic = {
+    type: {
+        year: "年度",
+        quarter: "季度",
+        month: "月度"
+    },
+
+    status: {
+        pending: "未开始",
+        executing: "进行中",
+        finished: "已结束"
+    }
+}
+
 export function objectToArray(obj) {
     return XEUtils.map(XEUtils.keys(obj), value => ({ label: XEUtils.get(obj, value), value }))
 }

+ 1 - 1
src/utils/request.js

@@ -54,7 +54,7 @@ axios.interceptors.response.use(
 					title: "请求错误",
 					message: error.response.data.message || "Status:500,服务器发生错误!"
 				});
-			} else if (error.response.status == 401) {
+			} else if (error.response.status == 401 || error.response.data.message == "找不到当前登录的信息") {
 				if (!MessageBox_401_show) {
 					MessageBox_401_show = true
 					ElMessageBox.confirm("当前用户已被登出或无权限访问当前资源,请尝试重新登录后再操作。", "无权限访问", {

+ 175 - 0
src/views/basic/customer/detail.vue

@@ -0,0 +1,175 @@
+<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 :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 customerDic.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 customerDic.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 customerDic.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="creditNoLabel" prop="creditNo" label-width="140">
+                                <el-input v-model="form.creditNo" :placeholder="`请输入${creditNoLabel}`"></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>
+        </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 { customerDic } from "@/utils/basicDic";
+import { verifyIdCard } from "@/utils/verificate";
+
+const $emit = defineEmits(["success", "closed"]);
+const visible = ref(false);
+const isSaving = ref(false);
+
+const activeNames = ref(["basic", "contact"]);
+const creditNoLabel = computed(() => XEUtils.get(customerDic.creditNo, form.value.type) || "统一社会信用代码");
+const mode = ref("add");
+const titleMap = reactive({
+    add: "新增客户",
+    edit: "修改客户"
+});
+
+const form = ref({
+    id: null,
+    name: null,
+    code: null,
+    customerType: "customer",
+    type: null,
+    creditNo: null,
+    valueLevel: null,
+    creditLevel: null,
+    managerName: null,
+    managerPhone: null,
+    phone: null,
+    email: null,
+    address: null,
+    remark: null
+});
+const rules = reactive({
+    name: [{ required: true, message: "请输入客户名称" }],
+    type: [{ required: true, message: "请选择客户分类" }],
+    managerName: [{ required: true, message: "请输入联系人姓名" }],
+    creditNo: computed(() => [
+        { required: true, validator: form.value.type == "individual" ? verifyIdCard : (rule, value, callback) => {
+            if (!value) return callback(new Error(`请输入${creditNoLabel.value}`));
+            callback();
+        } },
+        { len: 18, message: `请输入18位${creditNoLabel.value}` }
+    ]),
+    managerPhone: [{ pattern: /^\d{11}$/, message: "请输入11位手机号码" }]
+});
+
+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) {
+            const data = XEUtils.omit(form.value);
+
+            isSaving.value = true;
+            API.basic.customer[mode.value](data).then(res => {
+                ElMessage.success("操作成功");
+                isSaving.value = false;
+                visible.value = false;
+                $emit("success", mode.value);
+            }).catch(() => isSaving.value = false);
+        } else {
+            return false;
+        }
+    });
+}
+
+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>

+ 130 - 0
src/views/basic/customer/index.vue

@@ -0,0 +1,130 @@
+<template>
+	<el-container class="is-vertical">
+        <sc-page-header @add="table_add"></sc-page-header>
+
+        <scTable ref="xGridTable" :apiObj="$API.basic.customer" :formConfig="formConfig" :paramsColums="paramsColums" :toolbarConfig="toolbarConfig" :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 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>
+
+    <customer-detail v-if="dialog" ref="customerRef" @success="refreshTable" @closed="dialog = false"></customer-detail>
+</template>
+
+<script setup>
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import TOOL from "@/utils/tool";
+import { statusDic, customerDic, objectToArray } from "@/utils/basicDic";
+import { mapFormItemInput, mapFormItemSelect } from "@/components/scTable/helper";
+import customerDetail from "./detail";
+
+const toolbarConfig = reactive({
+    enabled: true,
+    export: true
+});
+
+const selectConfig = reactive({
+    options: objectToArray(statusDic),
+    events: {
+        change: data => XEUtils.merge(formConfig.data, data)
+    }
+});
+
+const formConfig = reactive({
+    data: {},
+    items: [
+        mapFormItemInput("nameLike", "客户名称"),
+        mapFormItemInput("codeLike", "客户编号"),
+        mapFormItemSelect("status", "客户状态", selectConfig),
+        mapFormItemSelect("type", "客户分类", { ...selectConfig, options: objectToArray(customerDic.type) })
+    ]
+});
+
+const paramsColums = reactive([
+    { column: "orderBy", defaultValue: "code_asc" },
+    { column: "customerType", defaultValue: "customer" },
+    { column: "nameLike" },
+    { column: "codeLike" },
+    { column: "status" },
+    { column: "type" }
+]);
+
+const columns = reactive([
+    { type: "seq", fixed: "left", width: 60 },
+    { 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) },
+    { 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 },
+    { 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.reloadColumn(columns);
+    xGridTable.value.searchData(mode);
+}
+
+const customerRef = ref();
+const dialog = ref(false);
+
+const table_add = () => {
+    dialog.value = true;
+    nextTick(() => customerRef.value?.open());
+}
+
+const table_edit = row => {
+    dialog.value = true;
+    nextTick(() => customerRef.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(() => {});
+}
+</script>

+ 164 - 0
src/views/basic/material/detail.vue

@@ -0,0 +1,164 @@
+<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 :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="materialType">
+                                <el-select v-model="form.materialType" placeholder="请选择物料类型">
+                                    <el-option v-for="(label, key) in materialDic.type" :key="key" :label="label" :value="key" :disabled="materialOptionDisabled(key)" />
+                                </el-select>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="物料来源" prop="needType">
+                                <el-select v-model="form.needType" placeholder="请选择物料来源">
+                                    <el-option v-for="(label, key) in materialDic.needType" :key="key" :label="label" :value="key" :disabled="needTypeOptionDisabled(key)" />
+                                </el-select>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="规格型号">
+                                <el-input v-model="form.specification" clearable placeholder="请输入规格型号"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="单位" prop="unit">
+                                <el-select v-model.trim="form.unit" filterable :filter-method="filterMethod" placeholder="请选择物料单位">
+                                    <el-option v-for="(label, key) in filterOptions" :key="key" :label="label" :value="key">
+                                        <span style="float: left;">{{ label }}</span>
+                                        <span style="float: right;padding-left: 18px;font-size: 13px;color: var(--el-text-color-secondary);">{{ key }}</span>
+                                    </el-option>
+                                </el-select>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="标准售价" prop="price">
+                                <el-input-number v-model="form.price" :min="0" :precision="2" :controls="false" placeholder="请输入标准售价">
+                                    <template #suffix>元</template>
+                                </el-input-number>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+                
+                <el-collapse-item title="其他说明" name="other">
+                    <el-row>
+                        <el-col :xs="24">
+                            <el-form-item label="备注" label-width="100">
+                                <el-input v-model="form.remark" type="textarea" maxlength="200" :rows="4" placeholder="请输入内容"></el-input>
+                            </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 { statusDic, materialDic } from "@/utils/basicDic";
+
+const $emit = defineEmits(["success", "closed"]);
+const visible = ref(false);
+const isSaving = ref(false);
+
+const activeNames = ref(["basic", "other"]);
+const filterOptions = ref(XEUtils.clone(materialDic.unit, true));
+const mode = ref("add");
+const titleMap = reactive({
+    add: "新增物料",
+    edit: "修改物料"
+});
+
+const form = ref({
+    id: null,
+    name: null,
+    code: null,
+    materialType: null,
+    needType: null,
+    specification: null,
+    unit: null,
+    price: null,
+    remark: null
+});
+const rules = reactive({
+    name: [{ required: true, message: "请输入物料名称" }],
+    code: [{ required: true, message: "请输入物料编码" }],
+    materialType: [{ required: true, message: "请选择物料类型" }],
+    unit: [{ required: true, message: "请输入单位" }],
+    price: [{ required: true, message: "请输入标准售价" }],
+    needType: [{ required: true, message: "请选择物料来源" }]
+});
+
+const needTypeOptionDisabled = key => form.value.materialType && !XEUtils.includes(materialDic.typeRelation[key], form.value.materialType);
+const materialOptionDisabled = key => form.value.needType && !XEUtils.includes(XEUtils.get(materialDic.typeRelation, form.value.needType, []), key);
+
+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 filterMethod = query => {
+    const fields = [];
+    XEUtils.objectEach(materialDic.unit, (label, key) => (label.toLowerCase().includes(query.toLowerCase()) || key.toLowerCase().includes(query.toLowerCase())) && fields.push(key));
+    filterOptions.value = XEUtils.pick(materialDic.unit, fields);
+};
+
+const formRef = ref();
+const submit = () => {
+    formRef.value.validate(valid => {
+        if (valid) {
+            isSaving.value = true;
+            API.basic.material[mode.value](form.value).then(res => {
+                ElMessage.success("操作成功");
+                isSaving.value = false;
+                visible.value = false;
+                $emit("success", mode.value);
+            }).catch(() => isSaving.value = false);
+        } else {
+            return false;
+        }
+    });
+}
+
+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 .el-input-number :deep(.el-input__suffix) {font-size: 12px;}
+
+.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>

+ 145 - 0
src/views/basic/material/index.vue

@@ -0,0 +1,145 @@
+<template>
+	<el-container class="is-vertical">
+        <sc-page-header v-if="!hidePageHeader" @add="table_add"></sc-page-header>
+
+        <scTable ref="xGridTable" :apiObj="$API.basic.material" :formConfig="formConfig" :paramsColums="paramsColums" :toolbarConfig="toolbarConfig" :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>
+
+    <material-detail v-if="dialog" ref="materialRef" @success="refreshTable" @closed="dialog = false"></material-detail>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import TOOL from "@/utils/tool";
+import { statusDic, materialDic, objectToArray } from "@/utils/basicDic";
+import { mapFormItemInput, mapFormItemSelect } from "@/components/scTable/helper";
+import materialDetail from "./detail";
+
+const props = defineProps({
+    options: { type: Object, default: () => {} },
+    hidePageHeader: { type: Boolean, default: false },
+    hideHandler: { type: Boolean, default: false },
+    hideCheckbox: { type: Boolean, default: false }
+});
+
+const toolbarConfig = reactive({
+    enabled: true,
+    export: true
+});
+
+const selectConfig = reactive({
+    options: objectToArray(materialDic.type),
+    events: {
+        change: data => XEUtils.merge(formConfig.data, data)
+    }
+});
+
+const formConfig = reactive({
+    data: {},
+    items: [
+        mapFormItemInput("nameLike", "物料名称"),
+        mapFormItemInput("codeLike", "物料编码"),
+        mapFormItemSelect("materialType", "物料类型", selectConfig),
+        mapFormItemSelect("status", "物料状态", { ...selectConfig, options: objectToArray(statusDic) }),
+        mapFormItemSelect("needType", "需求类型", { ...selectConfig, options: objectToArray(materialDic.needType) })
+    ]
+});
+
+const paramsColums = reactive([
+    { column: "orderBy", defaultValue: "code_asc" },
+    { column: "nameLike" },
+    { column: "codeLike" },
+    { column: "materialType" },
+    { column: "materialTypeIn" },
+    { column: "status" },
+    { column: "needType" }
+]);
+
+const columns = reactive([
+    { visible: props.hideHandler, type: props.hideCheckbox && "radio" || "checkbox", fixed: "left", width: 40 },
+    { type: "seq", fixed: "left", width: 60 },
+    { 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: "materialType", title: "物料类型", minWidth: 120, sortable: true, formatter: ({ cellValue }) => XEUtils.get(materialDic.type, cellValue, cellValue) },
+    { type: "html", field: "needType", title: "物料来源", minWidth: 120, sortable: true, formatter: ({ cellValue }) => XEUtils.get(materialDic.needType, cellValue, cellValue) },
+    { type: "html", field: "specification", title: "规格型号", minWidth: 120, sortable: true },
+    { type: "html", field: "unit", title: "单位", minWidth: 100, sortable: true },
+    { visible: false, type: "html", field: "price", title: "标准售价", minWidth: 100, sortable: true },
+    { field: "status", title: "物料状态", minWidth: 100, editRender: { name: "$cell-tag", options: statusDic } },
+    { 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 },
+    { visible: !props.hideHandler, title: "操作", fixed: "right", width: 220, slots: { default: "action" } }
+]);
+
+// 显示隐藏 筛选表单
+const xGridTable = ref();
+const refreshTable = (mode = "add") => {
+    xGridTable.value.reloadColumn(columns);
+    xGridTable.value.searchData(mode);
+}
+const getSelectRows = () => xGridTable.value.getSelectRows();
+
+const materialRef = ref();
+const dialog = ref(false);
+
+const table_add = () => {
+    dialog.value = true;
+    nextTick(() => materialRef.value?.open());
+}
+
+const table_edit = row => {
+    dialog.value = true;
+    nextTick(() => materialRef.value?.setData(row));
+}
+
+const table_del = ({ id }) => {
+    ElMessageBox.confirm("是否确认删除该物料?", "删除警告", {
+        type: "warning",
+        confirmButtonText: "确定",
+        cancelButtonText: "取消"
+    }).then(() => {
+        API.basic.material.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.material.edit({ id: row.id, status }).then(() => {
+            ElMessage.success("操作成功");
+            refreshTable();
+        });
+    }).catch(() => {});
+}
+
+defineExpose({
+    getSelectRows
+});
+</script>

+ 149 - 0
src/views/basic/qualityPlan/desc.vue

@@ -0,0 +1,149 @@
+<template>
+    <el-dialog v-model="visible" :title="titleMap[mode]" fullscreen :close-on-click-modal="false" @closed="$emit('closed')">
+        <el-main>
+            <el-collapse v-model="activeNames">
+                <el-collapse-item title="基本信息" name="basic">
+                    <el-descriptions :column="3" label-width="140" border>
+                        <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="质检人员" :span="ismobile ? 3 : 1" label-align="right">{{ descData.inspectUserName }}</el-descriptions-item>
+                        <el-descriptions-item label="方案类型" :span="ismobile ? 3 : descData.type == 'full' ? 2 : 1" label-align="right">{{ XEUtils.get(qualityPlanTypeDic, descData.type, descData.type) }}</el-descriptions-item>
+                        <template v-if="descData.type == 'sampling'">
+                            <el-descriptions-item label="抽检比例" :span="ismobile ? 3 : 1" label-align="right">{{ descData.sampleRate }}%</el-descriptions-item>
+                            <el-descriptions-item label="合格率" :span="3" label-align="right">{{ descData.passedRate }}%</el-descriptions-item>
+                        </template>
+                        <el-descriptions-item label="概要" :span="3" label-align="right">{{ descData.remark }}</el-descriptions-item>
+                        <el-descriptions-item label="附件" :span="3" 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 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="3" label-align="right">{{ descData.reviewReason }}</el-descriptions-item>
+                    </el-descriptions>
+
+                    <el-form v-if="mode == 'approval'" ref="formRef" :model="descData" :rules="rules" label-width="120">
+                        <el-form-item label="审批结果" prop="reviewStatus">
+                            <el-radio-group v-model="descData.reviewStatus" @change="radioChange">
+                                <el-radio label="同意" value="approved"></el-radio>
+                                <el-radio label="拒绝" value="rejected"></el-radio>
+                            </el-radio-group>
+                        </el-form-item>
+                        <el-form-item label="审批意见" prop="reviewReason" label-width="100">
+                            <el-input v-model="descData.reviewReason" type="textarea" maxlength="200" :rows="4" placeholder="请输入内容"></el-input>
+                    </el-form-item>
+                    </el-form>
+                </el-collapse-item>
+            </el-collapse>
+        </el-main>
+
+        <template v-if="mode == 'approval'" #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 { reviewStatusDic, qualityPlanTypeDic } from "@/utils/basicDic";
+import scUploadFile from "@/components/scUpload/file";
+
+const $emit = defineEmits(["closed"]);
+const visible = ref(false);
+const isSaving = ref(false);
+
+const store = useStore();
+const ismobile = computed(() => store.state.global.ismobile);
+
+const activeNames = ref(["basic", "approval"]);
+const mode = ref("detail");
+const titleMap = reactive({
+    approval: "质检方案审批",
+    detail: "质检方案详情"
+});
+
+const descData = ref({
+    id: null,
+    name: null,
+    code: null,
+    inspectUserName: null,
+    type: "full",
+    sampleRate: null,
+    passedRate: null,
+    remark: null,
+    fileList: [],
+    createTime: null,
+    reviewStatus: "pending",
+    reviewUserName: null,
+    reviewTime: null,
+    reviewReason: null
+});
+
+const rules = reactive({
+    reviewStatus: [{ required: true, validator: (rule, value, callback) => {
+        if (value === "pending") return callback(new Error("请选择审批结果"));
+        callback();
+    }}],
+    reviewReason: [{ validator: (rule, value, callback) => {
+        if (descData.value.reviewStatus === "rejected" && !value) return callback(new Error("请输入审批意见"));
+        callback();
+    }}]
+});
+
+const setData = (data, model = "detail") => {
+    visible.value = true;
+    mode.value = model;
+    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 XEUtils.set(descData.value, key, XEUtils.get(data, key));
+    });
+}
+
+const formRef = ref();
+const radioChange = () => formRef.value.validateField("reviewReason", () => {});
+const submit = () => {
+    formRef.value.validate(valid => {
+        if (valid) {
+            const data = XEUtils.pick(descData.value, "id", "reviewStatus", "reviewReason");
+            XEUtils.set(data, "reviewTime", moment().format("YYYY-MM-DD HH:mm:ss"));
+            
+            isSaving.value = true;
+            API.basic.qualityPlan.edit(data).then(res => {
+                ElMessage.success("操作成功");
+                isSaving.value = false;
+                visible.value = false;
+                $emit("success", mode.value);
+            }).catch(() => isSaving.value = false);
+        } else {
+            return false;
+        }
+    });
+}
+
+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;}
+</style>

+ 182 - 0
src/views/basic/qualityPlan/detail.vue

@@ -0,0 +1,182 @@
+<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 :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="inspectUserId">
+                                <el-select v-model="form.inspectUserId" placeholder="请选择质检人员">
+                                    <el-option v-for="item in users" :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="方案类型" required>
+                                <el-radio-group v-model="form.type">
+                                    <el-radio v-for="(label, key) in qualityPlanTypeDic" :key="key" :label="label" :value="key"></el-radio>
+                                </el-radio-group>
+                            </el-form-item>
+                        </el-col>
+                        <template v-if="form.type == 'sampling'">
+                            <el-col :md="8" :xs="24">
+                                <el-form-item label="抽检比例" prop="sampleRate">
+                                    <el-input-number v-model="form.sampleRate" :min="1" :max="100" :controls="false" placeholder="请输入抽检比例">
+                                        <template #suffix>%</template>
+                                    </el-input-number>
+                                </el-form-item>
+                            </el-col>
+                            <el-col :md="8" :xs="24">
+                                <el-form-item label="合格率" prop="passedRate">
+                                    <el-input-number v-model="form.passedRate" :min="0" :max="100" :controls="false" placeholder="请输入抽检比例">
+                                        <template #suffix>%</template>
+                                    </el-input-number>
+                                </el-form-item>
+                            </el-col>
+                        </template>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="审批人员">
+                                <el-select v-model="form.reviewUserId" clearable placeholder="请选择审批人员">
+                                    <el-option v-for="item in users" :key="item.id" :label="item.nickName" :value="item.id" />
+                                </el-select>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+                
+                <el-collapse-item title="其他说明" name="other">
+                    <el-row>
+                        <el-col :xs="24">
+                            <el-form-item label="概要" label-width="100">
+                                <el-input v-model="form.remark" type="textarea" maxlength="200" :rows="4" placeholder="请输入内容"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <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 { qualityPlanTypeDic } 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", "other"]);
+const mode = ref("add");
+const titleMap = reactive({
+    add: "新增质检方案",
+    edit: "修改质检方案"
+});
+
+const users = ref([]);
+const form = ref({
+    id: null,
+    name: null,
+    code: null,
+    inspectUserId: null,
+    type: "full",
+    sampleRate: null,
+    passedRate: null,
+    reviewUserId: null,
+    reviewStatus: "pending",
+    remark: null,
+    fileList: []
+});
+const rules = reactive({
+    name: [{ required: true, message: "请输入方案名称" }],
+    inspectUserId: [{ required: true, message: "请选择质检人员" }],
+    sampleRate: [{ required: true, message: "请输入抽检比例" }],
+    passedRate: [{ required: true, message: "请输入合格率" }]
+});
+
+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: "qualityPlanAttach" }));
+            fileList.length > 0 && XEUtils.set(data, "fileList", fileList);
+            form.value.type == "full" && (XEUtils.set(data, "sampleRate", 100), XEUtils.set(data, "passedRate", null));
+            !form.value.reviewUserId && XEUtils.set(data, "reviewStatus", "approved");
+
+            isSaving.value = true;
+            API.basic.qualityPlan[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 = () => {
+    if (form.value.id) isDel.value = true;
+}
+
+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 .el-input-number :deep(.el-input__suffix) {font-size: 12px;}
+
+.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>

+ 144 - 0
src/views/basic/qualityPlan/index.vue

@@ -0,0 +1,144 @@
+<template>
+	<el-container class="is-vertical">
+        <sc-page-header @add="table_add"></sc-page-header>
+
+        <scTable ref="xGridTable" :apiObj="$API.basic.qualityPlan" :formConfig="formConfig" :paramsColums="paramsColums" :toolbarConfig="toolbarConfig" :columns="columns">
+            <template #code_link="{ row }">
+                <vxe-text status="primary" @click="table_detail(row)">{{ row.code }}</vxe-text>
+            </template>
+
+            <template #action="{ row }">
+                <template v-if="row.reviewStatus == 'pending'">
+                    <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="$TOOL.data.get('USER_INFO').id == row.reviewUserId" type="primary" link @click="table_detail(row, 'approval')">
+                        <template #icon><sc-iconify icon="material-symbols:approval-outline"></sc-iconify></template>审批
+                    </el-button>
+                </template>
+                <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="dialogClose"></plan-detail>
+    <plan-desc v-if="dialog.desc" ref="planDescRef" @success="refreshTable" @closed="dialog.desc = false"></plan-desc>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import TOOL from "@/utils/tool";
+import { reviewStatusDic, qualityPlanTypeDic, objectToArray } from "@/utils/basicDic";
+import { mapFormItemInput, mapFormItemSelect, mapFormItemDatePicker } from "@/components/scTable/helper";
+import planDetail from "./detail";
+import planDesc from "./desc";
+
+const toolbarConfig = reactive({
+    enabled: true,
+    export: true
+});
+
+const selectConfig = reactive({
+    options: objectToArray(reviewStatusDic),
+    events: {
+        change: data => XEUtils.merge(formConfig.data, data)
+    }
+});
+
+const daterangeConfig = reactive({
+    resetValue: () => [],
+    props: {
+        type: "daterange",
+        startPlaceholder: "开始日期",
+        endPlaceholder: "结束日期",
+        format: "YYYY-MM-DD"
+    }
+});
+
+const formConfig = reactive({
+    data: {},
+    items: [
+        mapFormItemInput("nameLike", "方案名称"),
+        mapFormItemInput("codeLike", "方案编号"),
+        mapFormItemSelect("reviewStatus", "审批状态", selectConfig),
+        mapFormItemSelect("type", "方案类型", { ...selectConfig, options: objectToArray(qualityPlanTypeDic) }),
+        mapFormItemDatePicker("createTime", "创建日期", daterangeConfig)
+    ]
+});
+
+const paramsColums = reactive([
+    { column: "orderBy", defaultValue: "code_desc" },
+    { column: "nameLike" },
+    { column: "codeLike" },
+    { column: "reviewStatus" },
+    { column: "type" },
+    { column: "createTimeBegin", field: "createTime[0]" },
+    { column: "createTimeEnd", field: "createTime[1]" }
+]);
+
+const columns = reactive([
+    { type: "seq", fixed: "left", width: 60 },
+    { 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" } },
+    { visible: false, type: "html", field: "inspectUserName", title: "质检人员", minWidth: 120, sortable: true },
+    { type: "html", field: "type", title: "方案类型", minWidth: 100, sortable: true, formatter: ({ cellValue }) => XEUtils.get(qualityPlanTypeDic, cellValue, cellValue) },
+    { visible: false, type: "html", field: "sampleRate", title: "抽检比例", minWidth: 120, sortable: true },
+    { visible: false, type: "html", field: "passedRate", title: "合格率", minWidth: 120, sortable: true },
+    { visible: false, type: "html", field: "reviewUserName", title: "审批人员", minWidth: 120, sortable: true },
+    { field: "reviewStatus", title: "审批状态", minWidth: 100, editRender: { name: "$cell-tag", options: reviewStatusDic } },
+    { 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" } }
+]);
+
+// 显示隐藏 筛选表单
+const xGridTable = ref();
+const refreshTable = (mode = "add") => {
+    xGridTable.value.reloadColumn(columns);
+    xGridTable.value.searchData(mode);
+}
+
+const planRef = ref();
+const planDescRef = ref();
+const dialog = reactive({
+    detail: false,
+    desc: 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, mode = "detail") => {
+    dialog.desc = true;
+    nextTick(() => planDescRef.value?.setData(row, mode));
+}
+
+const table_del = ({ id }) => {
+    ElMessageBox.confirm("是否确认删除该质检方案?", "删除警告", {
+        type: "warning",
+        confirmButtonText: "确定",
+        cancelButtonText: "取消"
+    }).then(() => {
+        API.basic.qualityPlan.del({ id }).then(() => {
+            ElMessage.success("操作成功");
+            refreshTable();
+        });
+    }).catch(() => {});
+}
+
+const dialogClose = isDel => {
+    dialog.detail = false;
+    isDel && refreshTable();
+}
+</script>

+ 61 - 0
src/views/defaultVue/index.vue

@@ -0,0 +1,61 @@
+<template>
+	<el-container class="is-vertical">
+        <sc-page-header @add="table_add"></sc-page-header>
+
+        <scTable ref="xGridTable" :formConfig="formConfig" :toolbarConfig="toolbarConfig" :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 toolbarConfig = reactive({
+    enabled: true,
+    export: true
+});
+
+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>

+ 3 - 2
src/views/login/index.vue

@@ -1,8 +1,8 @@
 <template>
-	<div style="background-image: url(img/background.png);" class="login-bg">
+	<div :style="{ backgroundImage: `url(${backgroundImage})` }" class="login-bg">
         <div class="login-top">
             <div class="login-top__header">
-                <div class="logo"><img :alt="$CONFIG.APP_NAME" src="img/logo.png"></div>
+                <div class="logo"><img :alt="$CONFIG.APP_NAME" :src="require('/public/img/logo.png')"></div>
                 <label>{{ $CONFIG.APP_NAME }}</label>
             </div>
             
@@ -49,6 +49,7 @@ import { VxeUI } from "vxe-table";
 export default {
     data() {
         return {
+            backgroundImage: require("/public/img/background.png"),
             config: {
                 dark: this.$TOOL.data.get("APP_DARK") || false
             },

+ 84 - 0
src/views/production/bom/desc.vue

@@ -0,0 +1,84 @@
+<template>
+    <el-dialog v-model="visible" title="BOM单详情" fullscreen :close-on-click-modal="false" @closed="$emit('closed')">
+        <el-main>
+            <el-collapse v-model="activeNames">
+                <el-collapse-item title="基本信息" name="basic">
+                    <el-descriptions :column="3" label-width="140" border>
+                        <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="概要" :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>
+                </el-collapse-item>
+
+                <el-collapse-item title="子件信息" name="children">
+                    <sc-form-table v-model="descData.childrenList" v-bind="tableOptions" disabled></sc-form-table>
+                </el-collapse-item>
+            </el-collapse>
+        </el-main>
+    </el-dialog>
+</template>
+
+<script setup>
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import { statusDic, workmanshipDic } from "@/utils/basicDic";
+import { tableOptions } from "./main";
+
+const $emit = defineEmits(["closed"]);
+const visible = ref(false);
+
+const store = useStore();
+const ismobile = computed(() => store.state.global.ismobile);
+
+const activeNames = ref(["basic", "material", "children"]);
+const descData = ref({
+    id: null,
+    bomCode: null,
+    material: {
+        code: null,
+        name: null,
+        specification: null,
+        unit: null
+    },
+    childrenList: [],
+    remark: null,
+    status: "enable",
+    createTime: null
+});
+
+const setData = async data => {
+    const res = await API.production.bom.getChild({ id: 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));
+    });
+
+    visible.value = true;
+}
+
+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;}
+</style>

+ 155 - 0
src/views/production/bom/detail.vue

@@ -0,0 +1,155 @@
+<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="100">
+            <el-collapse v-model="activeNames">
+                <el-collapse-item v-if="form.parentId == 0" title="基本信息" name="basic">
+                    <el-row>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="BOM单编号" required>
+                                <el-input v-model="form.bomCode" :readonly="!!form.id" maxlength="50" show-word-limit clearable placeholder="不填将自动生成"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="16" :xs="24">
+                            <el-form-item label="概要">
+                                <el-input v-model="form.remark" type="textarea" maxlength="200" :rows="1" placeholder="请输入内容"></el-input>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+
+                <el-collapse-item title="产品信息" name="material">
+                    <el-row>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="产品编号" prop="material.code">
+                                <sc-table-input v-model="form.material" :hideShow="!!form.id" placeholder="选择产品" valueKey="code" tableKey="material" :options="selectOptions"></sc-table-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="产品名称" required>
+                                <el-input v-model="form.material.name" readonly placeholder="选择产品"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="规格型号">
+                                <el-input v-model="form.material.specification" readonly placeholder="选择产品"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="单位" required>
+                                <el-input v-model="form.material.unit" readonly placeholder="选择产品"></el-input>
+                            </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>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+
+                <el-collapse-item title="子件信息" name="children">
+                    <sc-form-table ref="formTableRef" v-model="form.childrenList" v-bind="tableOptions"></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 XEUtils from "xe-utils";
+
+import API from "@/api";
+import { mapFormItemInput } from "@/components/scTable/helper";
+import { materialDic } from "@/utils/basicDic";
+import { tableOptions, selectOptions } from "./main";
+
+const $emit = defineEmits(["success", "closed"]);
+const visible = ref(false);
+const isSaving = ref(false);
+
+const activeNames = ref(["basic", "material", "children"]);
+const mode = ref("add");
+const titleMap = reactive({
+    add: "新增BOM单",
+    edit: "修改BOM单"
+});
+
+const form = ref({
+    id: null,
+    parentId: "0",
+    bomCode: null,
+    material: {
+        code: null,
+        name: null,
+        specification: null,
+        unit: null
+    },
+    childrenList: [],
+    remark: null
+});
+const rules = reactive({
+    "material.code": [{ required: true, message: "请选择产品" }]
+});
+
+const setData = async (data = {}, model = "add") => {
+    if (data.id) {
+        const res = await API.production.bom.getChild({ id: 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));
+        });
+    }
+
+    mode.value = model;
+    visible.value = true;
+}
+
+const formRef = ref();
+const formTableRef = ref();
+const submit = () => {
+    formRef.value.validate(async valid => {
+        if (valid) {
+            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, "quantity", "remark") }));
+                XEUtils.set(data, "materialName", form.value.material.name);
+                XEUtils.set(data, "materialCode", form.value.material.code);
+                XEUtils.set(data, "childrenList", childrenList);
+
+                isSaving.value = true;
+                API.production.bom[mode.value](data).then(res => {
+                    ElMessage.success("操作成功");
+                    isSaving.value = false;
+                    visible.value = false;
+                    $emit("success", mode.value);
+                }).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__wrap) {border: none;}
+.el-collapse-item :deep(.el-collapse-item__content) {padding: 20px 28px 20px 0;}
+.el-collapse-item:last-child :deep(.el-collapse-item__content) {padding-right: 0;}
+</style>

+ 175 - 0
src/views/production/bom/index.vue

@@ -0,0 +1,175 @@
+<template>
+	<el-container class="is-vertical">
+        <sc-page-header @add="table_add()"></sc-page-header>
+        
+        <scTable ref="xGridTable" :apiObj="$API.production.bom" :formConfig="formConfig" :paramsColums="paramsColums" :toolbarConfig="toolbarConfig" :columns="columns" v-bind="expandOptions">
+            <template #code_link="{ row }">
+                <vxe-text status="primary" @click="table_detail(row)">{{ row.bomCode }}</vxe-text>
+            </template>
+
+            <template #action="{ row }">
+                <el-button v-if="!row.isHaveChildren" type="primary" link @click="table_add(row)">
+                    <template #icon><sc-iconify icon="ant-design:cloud-upload-outlined"></sc-iconify></template>新增
+                </el-button>
+                <el-button v-else type="primary" link @click="table_edit(row)">
+                    <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>修改
+                </el-button>
+
+                <template v-if="row.parentId == 0">
+                    <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>
+                </template>
+
+                <el-button v-if="!row.isHaveChildren" 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>
+
+    <bom-detail v-if="dialog.detail" ref="bomRef" @success="refreshTable" @closed="dialog.detail = false"></bom-detail>
+    <bom-desc v-if="dialog.desc" ref="bomDescRef" @closed="dialog.desc = false"></bom-desc>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import TOOL from "@/utils/tool";
+import { statusDic, objectToArray } from "@/utils/basicDic";
+import { mapFormItemInput, mapFormItemSelect, mapFormItemDatePicker } from "@/components/scTable/helper";
+import bomDetail from "./detail";
+import bomDesc from "./desc";
+
+const toolbarConfig = reactive({
+    enabled: true,
+    export: true
+});
+
+const expandOptions = reactive({
+    options: {
+        treeConfig: {
+            lazy: true,
+            rowField: "id",
+            parentField: "parentId",
+            childrenField: "children",
+            hasChildField: "isHaveChildren",
+            loadMethod: ({ row }) => API.production.bom.getChild({ id: row.id })
+        }
+    }
+})
+
+const selectConfig = reactive({
+    options: objectToArray(statusDic),
+    events: {
+        change: data => XEUtils.merge(formConfig.data, data)
+    }
+});
+
+const daterangeConfig = reactive({
+    resetValue: () => [],
+    props: {
+        type: "daterange",
+        startPlaceholder: "开始日期",
+        endPlaceholder: "结束日期",
+        format: "YYYY-MM-DD"
+    }
+});
+
+const formConfig = reactive({
+    data: {},
+    items: [
+        mapFormItemInput("bomCodeLike", "BOM单编号"),
+        mapFormItemInput("materialCodeLike", "产品编码"),
+        mapFormItemInput("materialNameLike", "产品名称"),
+        mapFormItemSelect("status", "BOM单状态", selectConfig),
+        mapFormItemDatePicker("createTime", "创建日期", daterangeConfig)
+    ]
+});
+
+const paramsColums = reactive([
+    { column: "orderBy", defaultValue: "bomCode_asc" },
+    { column: "parentId", defaultValue: "0" },
+    { column: "bomCodeLike" },
+    { column: "materialCodeLike" },
+    { column: "materialNameLike" },
+    { column: "status" },
+    { column: "createTimeBegin", field: "createTime[0]" },
+    { column: "createTimeEnd", field: "createTime[1]" }
+]);
+
+const columns = reactive([
+    { type: "checkbox", fixed: "left", width: 40 },
+    { type: "seq", fixed: "left", width: 80 },
+    { type: "html", field: "materialCode", title: "产品编码", fixed: "left", minWidth: 150, 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 } },
+    { 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" } }
+]);
+
+// 显示隐藏 筛选表单
+const xGridTable = ref();
+const refreshTable = (mode = "add") => {
+    xGridTable.value.reloadColumn(columns);
+    xGridTable.value.searchData(mode);
+}
+
+const bomRef = ref();
+const bomDescRef = ref();
+const dialog = reactive({
+    detail: false,
+    desc: false
+});
+
+const table_add = (row = {}) => {
+    dialog.detail = true;
+    nextTick(() => bomRef.value?.setData(row));
+}
+
+const table_edit = row => {
+    dialog.detail = true;
+    nextTick(() => bomRef.value?.setData(row, "edit"));
+}
+
+const table_detail = row => {
+    dialog.desc = true;
+    nextTick(() => bomDescRef.value?.setData(row));
+}
+
+const table_del = ({ id }) => {
+    ElMessageBox.confirm("是否确认删除该BOM清单?", "删除警告", {
+        type: "warning",
+        confirmButtonText: "确定",
+        cancelButtonText: "取消"
+    }).then(() => {
+        API.production.bom.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}该BOM清单?`, `${msg}警告`, {
+        type: "warning",
+        confirmButtonText: "确定",
+        cancelButtonText: "取消"
+    }).then(() => {
+        API.production.bom.edit({ id: row.id, status }).then(() => {
+            ElMessage.success("操作成功");
+            refreshTable();
+        });
+    }).catch(() => {});
+}
+</script>

+ 44 - 0
src/views/production/bom/main.js

@@ -0,0 +1,44 @@
+import XEUtils from "xe-utils";
+import { materialDic } from "@/utils/basicDic";
+import { mapFormItemInput } from "@/components/scTable/helper";
+
+export const tableOptions = reactive({
+    tableKey: "material",
+
+    columns: [
+        { type: "seq", fixed: "left", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false, params: { hide_del: row => XEUtils.get(row, "isHaveChildren", false) }, slots: { default: "seq_del" } },
+        { field: "code", title: "物料编码", fixed: "left", minWidth: 150 },
+        { 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: 0, type: "float", controlConfig: { enabled: false } }, defaultValue: 1 } },
+        { field: "remark", title: "备注", minWidth: 200, editRender: { name: "ElInput", props: { type: "textarea", autosize: true } } }
+    ],
+    editRules: {
+        quantity: [{ required: true, message: "必须填写" }]
+    },
+    footerField: ["quantity"],
+    mergeFooterItems: [{ row: 0, col: 0, rowspan: 1, colspan: 5 }],
+
+    selectOptions: {
+        formConfig: {
+            data: { status: "enable", materialTypeIn: XEUtils.keys(XEUtils.omit(materialDic.type, "finished_product")) },
+            items: [
+                mapFormItemInput("nameLike", "物料名称"),
+                mapFormItemInput("codeLike", "物料编码")
+            ]
+        }
+    },
+
+    add_success: (oldValue, newValue) => XEUtils.map(newValue, (item, index) => XEUtils.pick(item, "id", "code", "name", "specification", "unit"))
+})
+
+export const selectOptions = reactive({
+    formConfig: {
+        data: { status: "enable", materialTypeIn: materialDic.typeRelation["self_made"] },
+        items: [
+            mapFormItemInput("nameLike", "物料名称"),
+            mapFormItemInput("codeLike", "物料编码")
+        ]
+    }
+})

+ 89 - 0
src/views/sales/order/desc.vue

@@ -0,0 +1,89 @@
+<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 :column="3" label-width="140" border>
+                        <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="时间单位" :span="ismobile ? 3 : 1" label-align="right">{{ XEUtils.get(workmanshipDic.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-item label="适用产品" :span="3" label-align="right">所有产品</el-descriptions-item>
+                        <el-descriptions-item label="概要" :span="3" label-align="right">{{ descData.remark }}</el-descriptions-item>
+                        <el-descriptions-item label="附件" :span="3" 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="route">
+                    <sc-form-table v-model="descData.detailList" v-bind="options" ></sc-form-table>
+                </el-collapse-item>
+
+                <el-collapse-item title="质检方案" name="plan">
+                </el-collapse-item>
+            </el-collapse>
+        </el-main>
+    </el-dialog>
+</template>
+
+<script setup>
+import XEUtils from "xe-utils";
+import { statusDic, workmanshipDic } from "@/utils/basicDic";
+import { tableOptions } from "./main";
+import scUploadFile from "@/components/scUpload/file";
+
+const $emit = defineEmits(["closed"]);
+const visible = ref(false);
+
+const store = useStore();
+const ismobile = computed(() => store.state.global.ismobile);
+
+const options = reactive({
+    disabled: true,
+    ...tableOptions,
+    columns: tableOptions.columns.slice(1),
+    mergeFooterItems: [{ row: 0, col: 0, rowspan: 1, colspan: 4 }]
+});
+
+const activeNames = ref(["basic", "route", "plan"]);
+const descData = ref({
+    id: null,
+    parentId: null,
+    name: null,
+    code: null,
+    timeUnit: "minute",
+    version: "v1.0.0",
+    detailList: [],
+    remark: null,
+    fileList: [],
+    status: "enable",
+    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 == "detailList") XEUtils.set(descData.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item.stage, ...XEUtils.omit(item, "id", "stage") })));
+        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;}
+</style>

+ 160 - 0
src/views/sales/order/detail.vue

@@ -0,0 +1,160 @@
+<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 :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="orderTime">
+                                <el-date-picker v-model="form.orderTime" :clearable="false" value-format="YYYY-MM-DD" :default-time="new Date()" placeholder="请选择单据日期"></el-date-picker>
+                            </el-form-item>
+                        </el-col>
+
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="客户" prop="customer.id">
+                                <sc-table-input v-model="form.customer" :hideShow="!!form.id" placeholder="选择客户" valueKey="name" tableKey="customer" :options="selectOptions"></sc-table-input>
+                            </el-form-item>
+                        </el-col>
+
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="预计交期" prop="confirmedDeliveryDate">
+                                <el-date-picker v-model="form.confirmedDeliveryDate" :clearable="false" value-format="YYYY-MM-DD" :default-time="new Date()" placeholder="请选择预计交期"></el-date-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"></sc-form-table>
+                </el-collapse-item>
+
+                <el-collapse-item title="其他说明" name="other">
+                    <el-row>
+                        <el-col :xs="24">
+                            <el-form-item label="概要" label-width="100">
+                                <el-input v-model="form.remark" type="textarea" maxlength="200" :rows="4" placeholder="请输入内容"></el-input>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :xs="24">
+                            <el-form-item label="附件" label-width="100">
+                                <sc-upload-file v-model="form.fileList" @removeSuccess="removeSuccess">
+                                    <vxe-button status="primary" size="mini" content="上传附件"></vxe-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">{{ mode == "upgrade" ? "发布" : "保存" }}</el-button>
+        </template>
+    </el-dialog>
+</template>
+
+<script setup>
+import moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import { workmanshipDic } from "@/utils/basicDic";
+import { tableOptions, selectOptions } from "./main";
+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", "material", "other"]);
+const mode = ref("add");
+const titleMap = reactive({
+    add: "新增销售订单",
+    edit: "修改销售订单"
+});
+
+const form = ref({
+    id: null,
+    code: null,
+    orderTime: moment().format("YYYY-MM-DD HH:mm:ss"),
+    confirmedDeliveryDate: null,
+    customer: {
+        id: null,
+        label: null
+    },
+    childrenList: [],
+    remark: null,
+    fileList: []
+});
+const rules = reactive({
+});
+
+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 if (key == "childrenList") XEUtils.set(form.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item.stage, ...XEUtils.omit(item, "id", "stage") })));
+        else XEUtils.set(form.value, key, XEUtils.get(data, key));
+    });
+}
+
+const formRef = ref();
+const formTableRef = ref();
+const submit = () => {
+    console.log('submit called', form.value.customerId);
+    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, "fileList", "childrenList");
+                // const childrenList = XEUtils.map(form.value.childrenList, item => XEUtils.omit(item, "id", "name", "code", "processType"));
+                // const fileList = XEUtils.map(XEUtils.filter(form.value.fileList, item => !item.id), item => ({ ...XEUtils.omit(item, "id", "name"), fileName: item.name, fileType: "processRouteFile" }));
+                // XEUtils.set(data, "childrenList", childrenList);
+                // fileList.length > 0 && XEUtils.set(data, "fileList", fileList);
+    
+                // isSaving.value = true;
+                // API.workmanship.route[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 = () => {
+    if (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;}
+.el-collapse-item:nth-child(2) :deep(.el-collapse-item__content) {padding-right: 0;}
+</style>

+ 154 - 0
src/views/sales/order/index.vue

@@ -0,0 +1,154 @@
+<template>
+	<el-container class="is-vertical">
+        <sc-page-header @add="table_add"></sc-page-header>
+
+        <scTable ref="xGridTable" :apiObj="$API.workmanship.route" :formConfig="formConfig" :paramsColums="paramsColums" :toolbarConfig="toolbarConfig" :columns="columns">
+            <template #code_link="{ row }">
+                <vxe-text status="primary" @click="table_detail(row)">{{ row.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 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 v-if="!row.isHaveHistory" 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>
+
+    <order-detail v-if="dialog.detail" ref="orderRef" @success="refreshTable" @closed="dialogClose"></order-detail>
+    <order-desc v-if="dialog.desc" ref="orderDescRef" @closed="dialog.desc = 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 { statusDic, workmanshipDic, objectToArray } from "@/utils/basicDic";
+import { mapFormItemInput, mapFormItemSelect, mapFormItemDatePicker } from "@/components/scTable/helper";
+import orderDetail from "./detail";
+import orderDesc from "./desc";
+
+const toolbarConfig = reactive({
+    enabled: true,
+    export: true
+});
+
+const selectConfig = reactive({
+    options: objectToArray(statusDic),
+    events: {
+        change: data => XEUtils.merge(formConfig.data, data)
+    }
+});
+
+const daterangeConfig = reactive({
+    resetValue: () => [],
+    props: {
+        type: "daterange",
+        startPlaceholder: "开始日期",
+        endPlaceholder: "结束日期",
+        format: "YYYY-MM-DD"
+    }
+});
+
+const formConfig = reactive({
+    data: {},
+    items: [
+        mapFormItemInput("codeLike", "单据编号"),
+        mapFormItemSelect("status", "工艺路线状态", selectConfig),
+        mapFormItemDatePicker("createTime", "创建日期", daterangeConfig)
+    ]
+});
+
+const paramsColums = reactive([
+    { column: "orderBy", defaultValue: "code_asc" },
+    { column: "codeLike" },
+    { column: "status" },
+    { column: "createTimeBegin", field: "createTime[0]" },
+    { column: "createTimeEnd", field: "createTime[1]" }
+]);
+
+const columns = reactive([
+    { type: "checkbox", fixed: "left", width: 40 },
+    { type: "seq", fixed: "left", width: 60 },
+    { field: "code", title: "单据编号", fixed: "left", minWidth: 150, sortable: true, className: "vxe-table-link-cell", slots: { default: "code_link" } },
+    { field: "status", title: "工艺路线状态", minWidth: 120, editRender: { name: "$cell-tag", options: statusDic } },
+    { type: "html", field: "", title: "质检方案", minWidth: 160, 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: 320, slots: { default: "action" } }
+]);
+
+// 显示隐藏 筛选表单
+const xGridTable = ref();
+const refreshTable = (mode = "add") => {
+    xGridTable.value.reloadColumn(columns);
+    xGridTable.value.searchData(mode);
+}
+
+const orderRef = ref();
+const orderDescRef = ref();
+const dialog = reactive({
+    detail: false,
+    desc: false
+});
+
+const table_add = () => {
+    dialog.detail = true;
+    nextTick(() => orderRef.value?.open());
+}
+
+const table_edit = row => {
+    dialog.detail = true;
+    nextTick(() => orderRef.value?.setData(row));
+}
+
+const table_detail = row => {
+    dialog.desc = true;
+    nextTick(() => orderDescRef.value?.setData(row));
+}
+
+const table_del = ({ id }) => {
+    ElMessageBox.confirm("是否确认删除该销售订单?", "删除警告", {
+        type: "warning",
+        confirmButtonText: "确定",
+        cancelButtonText: "取消"
+    }).then(() => {
+        API.workmanship.route.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.workmanship.route.edit({ id: row.id, status }).then(() => {
+            ElMessage.success("操作成功");
+            refreshTable();
+        });
+    }).catch(() => {});
+}
+
+const dialogClose = isDel => {
+    dialog.detail = false;
+    isDel && refreshTable();
+}
+</script>

+ 68 - 0
src/views/sales/order/main.js

@@ -0,0 +1,68 @@
+import XEUtils from "xe-utils";
+import TOOL from "@/utils/tool";
+import { materialDic, customerDic } from "@/utils/basicDic";
+import { mapFormItemInput } from "@/components/scTable/helper";
+
+export const tableOptions = reactive({
+    tableKey: "material",
+
+    columns: [
+        { type: "seq", fixed: "left", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false, params: { hide_del: row => XEUtils.get(row, "isHaveChildren", false) }, slots: { default: "seq_del" } },
+        { field: "code", title: "物料编码", fixed: "left", minWidth: 150 },
+        { 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: 0, type: "float", controlConfig: { enabled: false } }, defaultValue: 1 } },
+        { field: "remark", title: "备注", minWidth: 200, editRender: { name: "ElInput", props: { type: "textarea", autosize: true } } }
+    ],
+    editRules: {
+        quantity: [{ required: true, message: "必须填写" }]
+    },
+    footerField: ["quantity"],
+    mergeFooterItems: [{ row: 0, col: 0, rowspan: 1, colspan: 5 }],
+
+    selectOptions: {
+        formConfig: {
+            data: { status: "enable", materialTypeIn: ["finished_product", "trade_goods"] },
+            items: [
+                mapFormItemInput("nameLike", "物料名称"),
+                mapFormItemInput("codeLike", "物料编码")
+            ]
+        }
+    },
+
+    add_success: (oldValue, newValue) => XEUtils.map(newValue, (item, index) => XEUtils.pick(item, "id", "code", "name", "specification", "unit"))
+})
+
+export const selectOptions = reactive({
+    optionsProps: { label: "name", value: "id" },
+    placeholder: "请选择客户",
+    formConfig: {
+        data: { status: "enable", customerType: "customer" },
+        items: [
+            mapFormItemInput("nameLike", "客户名称"),
+            mapFormItemInput("codeLike", "客户编号")
+        ]
+    },
+    paramsColums: [
+        { column: "orderBy", defaultValue: "code_asc" },
+        { column: "nameLike" },
+        { column: "codeLike" },
+        { column: "status" },
+        { column: "type" }
+    ],
+
+    columns: [
+        { type: "radio", fixed: "left", width: 40 },
+        { type: "html", field: "name", title: "客户名称", minWidth: 150, sortable: true },
+        { type: "html", field: "code", title: "客户编号", minWidth: 150, sortable: true },
+        { 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 },
+        { 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 },
+    ]
+});

+ 208 - 0
src/views/sales/plan/detail.vue

@@ -0,0 +1,208 @@
+<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="100">
+            <el-collapse v-model="activeNames">
+                <el-collapse-item title="基本信息" name="basic">
+                    <el-row>
+                        <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-radio-group v-model="form.type">
+                                    <el-radio v-for="(label, key) in mapRadio" :key="key" :label="label" :value="key"></el-radio>
+                                </el-radio-group>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="年份" prop="year">
+                                <el-date-picker v-model="form.year" type="year" :readonly="!!form.children.length || form.parentId != 0" :clearable="false" placeholder="请选择计划年份" />
+                            </el-form-item>
+                        </el-col>
+                        <el-col v-if="form.type == 'quarter'" :md="8" :xs="24">
+                            <el-form-item label="季度" prop="quarter">
+                                <el-select v-model="form.quarter" :clearable="false" placeholder="请选择计划季度">
+                                    <el-option v-for="item in 4" :key="item" :label="`第${item}季度`" :value="item" :disabled="quarterDisabled(item)"></el-option>
+                                </el-select>
+                            </el-form-item>
+                        </el-col>
+                        <el-col v-if="form.type == 'month'" :md="8" :xs="24">
+                            <el-form-item label="月份" prop="month">
+                                <el-select v-model="form.month" :clearable="false" placeholder="请选择计划月份">
+                                    <el-option v-for="item in 12" :key="item" :label="`${item}月`" :value="item" :disabled="monthDisabled(item)"></el-option>
+                                </el-select>
+                            </el-form-item>
+                        </el-col>
+                        <el-col :md="8" :xs="24">
+                            <el-form-item label="销售金额" prop="saleAmount">
+                                <el-input-number v-model="form.saleAmount" :min="1" :precision="2" :controls="false" placeholder="请输入计划销售金额">
+                                    <template #suffix>万</template>
+                                </el-input-number>
+                            </el-form-item>
+                        </el-col>
+                    </el-row>
+                </el-collapse-item>
+
+                <el-collapse-item title="其他说明" name="other">
+                    <el-row>
+                        <el-col :xs="24">
+                            <el-form-item label="概要" label-width="100">
+                                <el-input v-model="form.remark" type="textarea" maxlength="200" :rows="4" placeholder="请输入内容"></el-input>
+                            </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 moment from "moment";
+import XEUtils from "xe-utils";
+
+import API from "@/api";
+import { planDic } from "@/utils/basicDic";
+
+const $emit = defineEmits(["success", "closed"]);
+const visible = ref(false);
+const isSaving = ref(false);
+
+const activeNames = ref(["basic", "other"]);
+const mode = ref("add");
+const titleMap = reactive({
+    add: "新增销售计划",
+    edit: "修改销售计划"
+});
+
+const parentType = ref(null);
+
+const form = ref({
+    id: null,
+    parentId: "0",
+    name: null,
+    code: null,
+    type: "year",
+    year: moment().format("YYYY"),
+    quarter: moment().quarter(),
+    month: moment().month() + 1,
+    saleAmount: null,
+    remark: null,
+    children: []
+});
+const rules = reactive({
+    name: [{ required: true, message: "请输入计划名称" }],
+    type: [{ required: true, message: "请选择计划类型" }],
+    year: [{ required: true, message: "请选择计划年份" }],
+    quarter: [{ required: true, message: "请选择计划季度" }],
+    month: [{ required: true, message: "请选择计划月份" }],
+    saleAmount: [{ required: true, message: "请选择计划销售金额" }]
+});
+
+const setData = (parent, data, model = "add") => {
+    visible.value = true;
+    mode.value = model;
+    if (parent && !XEUtils.isEmpty(parent)) {
+        parentType.value = parent.type;
+
+        XEUtils.set(form.value, "parentId", XEUtils.get(parent, "id"));
+        XEUtils.set(form.value, "type", parent.type == "year" ? "quarter" : "month");
+        XEUtils.set(form.value, "year", moment(XEUtils.get(parent, "beginDate")).format("YYYY"));
+        XEUtils.set(form.value, "quarter", moment(XEUtils.get(parent, "beginDate")).quarter());
+        XEUtils.set(form.value, "month", moment().quarter(form.value.quarter).startOf("quarter").month() + 1);
+    }
+
+    if (model == "edit") {
+        XEUtils.objectEach(form.value, (_, key) => {
+            if (key == "year") XEUtils.set(form.value, key, moment(XEUtils.get(data, "beginDate")).format("YYYY"));
+            else if (key == "quarter") XEUtils.set(form.value, "quarter", moment(XEUtils.get(data, "beginDate")).quarter());
+            else if (key == "month") XEUtils.set(form.value, "month", moment(XEUtils.get(data, "beginDate")).month() + 1);
+            else XEUtils.set(form.value, key, XEUtils.get(data, key));
+        });
+    }
+}
+
+const mapRadio = computed((() => {
+    if (form.value.children.length) return XEUtils.pick(planDic.type, form.value.type);
+
+    if (parentType.value == "year") return XEUtils.omit(planDic.type, "year");
+    if (parentType.value == "quarter") return XEUtils.pick(planDic.type, "month");
+    return planDic.type;
+}));
+
+const monthDisabled = month => {
+    if (parentType.value == "quarter") return month - 1 < moment().quarter(form.value.quarter).startOf("quarter").month() || month - 1 > moment().quarter(form.value.quarter).endOf("quarter").month()
+    return false;
+};
+
+const quarterDisabled = quarter => {
+    if (form.value.children.length) return form.value.quarter !== quarter;
+    return false;
+};
+
+const formRef = ref();
+const submit = () => {
+    formRef.value.validate(async valid => {
+        if (valid) {
+            const data = XEUtils.omit(form.value, "year", "quarter", "month");
+            let beginDate = moment(form.value.year).startOf("year").format("YYYY-MM-DD");
+            let endDate = moment(form.value.year).endOf("year").format("YYYY-MM-DD");
+            if (form.value.type == "quarter") {
+                beginDate = moment(form.value.year).quarter(form.value.quarter).startOf("quarter").format("YYYY-MM-DD");
+                endDate = moment(form.value.year).quarter(form.value.quarter).endOf("quarter").format("YYYY-MM-DD");
+            } else if (form.value.type == "month") {
+                beginDate = moment(form.value.year).month(form.value.month - 1).startOf("month").format("YYYY-MM-DD");
+                endDate = moment(form.value.year).month(form.value.month - 1).endOf("month").format("YYYY-MM-DD");
+            }
+            
+            XEUtils.set(data, "beginDate", beginDate);
+            XEUtils.set(data, "endDate", endDate);
+
+            isSaving.value = true;
+            API.sales.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;
+        }
+    });
+}
+
+defineExpose({
+    setData
+});
+</script>
+
+<style scoped>
+.el-form {padding-left: 16px;padding-right: 22px;}
+.el-form .el-radio-group {flex-wrap: nowrap;}
+.el-form .el-select :deep(.el-select__wrapper.is-disabled) {background-color: var(--el-fill-color-blank);box-shadow: 0 0 0 1px var(--el-border-color) inset;cursor: pointer;}
+.el-form .el-select :deep(.el-select__wrapper.is-disabled) .el-select__selected-item {color: var(--el-text-color-regular);}
+    
+.el-form .el-input-number {width: 100%;}
+.el-form .el-input-number :deep(.el-input__inner) {text-align: unset;}
+.el-form .el-input-number :deep(.el-input__suffix) {font-size: 12px;}
+
+.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:last-child :deep(.el-collapse-item__content) {padding-right: 0;}
+</style>

+ 134 - 0
src/views/sales/plan/index.vue

@@ -0,0 +1,134 @@
+<template>
+	<el-container class="is-vertical">
+        <sc-page-header @add="table_add()"></sc-page-header>
+        
+        <scTable ref="xGridTable" :apiObj="$API.sales.plan" :formConfig="formConfig" :paramsColums="paramsColums" :toolbarConfig="toolbarConfig" :columns="columns" :options="options">
+            <template #action="scoped">
+                <el-button v-if="XEUtils.includes(['year', 'quarter'], scoped.row.type)" type="primary" link @click="table_add(scoped.row)">
+                    <template #icon><sc-iconify icon="ant-design:cloud-upload-outlined"></sc-iconify></template>新增
+                </el-button>
+                <el-button type="primary" link @click="table_edit(scoped)">
+                    <template #icon><sc-iconify icon="ant-design:edit-outlined"></sc-iconify></template>修改
+                </el-button>
+                <el-button v-if="!scoped.row.children.length" type="primary" link @click="table_del(scoped.row)">
+                    <template #icon><sc-iconify icon="ant-design:delete-outlined"></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 { planDic, objectToArray } from "@/utils/basicDic";
+import { mapFormItemInput, mapFormItemSelect, mapFormItemDatePicker } from "@/components/scTable/helper";
+import planDetail from "./detail";
+
+const formatStatus = row => {
+    if (moment().isBefore(row.beginDate)) return "pending";
+    if (moment().isAfter(row.endDate)) return "finished";
+    return "executing";
+}
+
+const toolbarConfig = reactive({
+    enabled: true,
+    export: true
+});
+
+const options = reactive({
+    treeConfig: { transform: true }
+})
+
+const selectConfig = reactive({
+    options: objectToArray(planDic.type),
+    events: {
+        change: data => XEUtils.merge(formConfig.data, data)
+    }
+});
+
+const daterangeConfig = reactive({
+    resetValue: () => [moment().startOf("year").format("YYYY-MM-DD HH:mm:ss"), moment().endOf("day").format("YYYY-MM-DD HH:mm:ss")],
+    props: {
+        type: "daterange",
+        startPlaceholder: "开始日期",
+        endPlaceholder: "结束日期",
+        format: "YYYY-MM-DD"
+    }
+});
+
+const formConfig = reactive({
+    data: {
+        createTime: [moment().startOf("year").format("YYYY-MM-DD HH:mm:ss"), moment().endOf("day").format("YYYY-MM-DD HH:mm:ss")]
+    },
+    items: [
+        mapFormItemInput("nameLike", "计划名称"),
+        mapFormItemInput("codeLike", "计划编号"),
+        mapFormItemSelect("type", "计划类型", selectConfig),
+        mapFormItemDatePicker("createTime", "创建日期", daterangeConfig)
+    ]
+});
+
+const paramsColums = reactive([
+    { column: "orderBy", defaultValue: "code_asc" },
+    { column: "nameLike" },
+    { column: "codeLike" },
+    { column: "type" },
+    { column: "createTimeBegin", field: "createTime[0]" },
+    { column: "createTimeEnd", field: "createTime[1]" }
+]);
+
+const columns = reactive([
+    { type: "seq", fixed: "left", width: 80 },
+    { type: "html", field: "name", title: "计划名称", fixed: "left", minWidth: 160, 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(planDic.type, cellValue, cellValue) },
+    { field: "status", title: "计划状态", minWidth: 120, editRender: { name: "$cell-tag", options: planDic.status, formatter: row => formatStatus(row) } },
+    { type: "html", field: "beginDate", title: "计划开始日期", minWidth: 150, sortable: true },
+    { type: "html", field: "endDate", title: "计划结束日期", minWidth: 150, sortable: true },
+    { type: "html", field: "saleAmount", 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" } }
+]);
+
+// 显示隐藏 筛选表单
+const xGridTable = ref();
+const refreshTable = (mode = "add") => {
+    xGridTable.value.reloadColumn(columns);
+    xGridTable.value.searchData(mode);
+}
+
+const planRef = ref();
+const dialog = ref(false);
+
+const table_add = (parent = {}) => {
+    dialog.value = true;
+    nextTick(() => planRef.value?.setData(parent));
+}
+
+const table_edit = ({ $grid, row }) => {
+    const parent = $grid.getTreeParentRow(row);
+
+    dialog.value = true;
+    nextTick(() => planRef.value?.setData(parent, row, "edit"));
+}
+
+const table_del = ({ id }) => {
+    ElMessageBox.confirm("是否确认删除该销售计划?", "删除警告", {
+        type: "warning",
+        confirmButtonText: "确定",
+        cancelButtonText: "取消"
+    }).then(() => {
+        API.sales.plan.del({ id }).then(() => {
+            ElMessage.success("操作成功");
+            refreshTable();
+        });
+    }).catch(() => {});
+}
+</script>

+ 2 - 2
src/views/system/dept/detail.vue

@@ -67,7 +67,7 @@ const rules = reactive({
     name: [{ required: true, message: "请输入部门名称" }],
     firmFunctionaryPhone: [{ pattern: /^\d{11}$/, message: "请输入11位手机号码" }],
     deptSort: [{ required: true, message: "请输入部门排序" }]
-})
+});
 
 const open = () => visible.value = true;
 const setData = data => {
@@ -99,7 +99,7 @@ fetchDept();
 defineExpose({
     open,
     setData
-})
+});
 </script>
 
 <style scoped>

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

@@ -56,7 +56,7 @@ const formConfig = reactive({
 const paramsColums = reactive([
     { column: "orderBy", defaultValue: "deptSort_asc" },
     { column: "nameLike" }
-])
+]);
 
 const columns = reactive([
     { type: "seq", width: 60 },
@@ -65,7 +65,7 @@ const columns = reactive([
     { type: "html", field: "firmFunctionaryPhone", title: "负责人电话", minWidth: 160, sortable: true },
     { type: "html", field: "remark", title: "备注", minWidth: 300, sortable: true },
     { title: "操作", fixed: "right", width: 140, align: "center", slots: { default: "action" } }
-])
+]);
 
 // 显示隐藏 筛选表单
 const xGridTable = ref();
@@ -99,6 +99,6 @@ const table_del = ({ id }) => {
             ElMessage.success("操作成功");
             refreshTable();
         });
-    });
+    }).catch(() => {});
 }
 </script>

+ 28 - 15
src/views/system/menu/detail.vue

@@ -3,8 +3,8 @@
 
     <el-scrollbar>
         <el-form ref="formRef" :model="form" :rules="rules" label-width="120">
-            <el-form-item label="上级菜单">
-                <el-input v-model="pName" readonly placeholder="智能生产运管平台"></el-input>
+            <el-form-item label="上级菜单" required>
+				<el-tree-select v-model="form.pid" :data="treeSelect" :props="treeProps" node-key="id" :default-expanded-keys="expandedKeys" check-strictly placeholder="智能生产运管平台"></el-tree-select>
             </el-form-item>
             <el-form-item prop="title">
                 <template #label>
@@ -13,8 +13,11 @@
                 </template>
                 <el-input v-model="form.title" placeholder="菜单标题"></el-input>
             </el-form-item>
-            <el-form-item label="菜单图标" prop="icon">
-                <sc-icon-select v-model="form.icon" clearable></sc-icon-select>
+            <el-form-item label="菜单类型" prop="type">
+                <el-radio-group v-model="form.type">
+                    <el-radio-button label="目录" :value="0"></el-radio-button>
+                    <el-radio-button label="菜单" :value="1"></el-radio-button>
+                </el-radio-group>
             </el-form-item>
             <el-form-item label="菜单可见" prop="hidden">
                 <el-radio-group v-model="form.hidden">
@@ -28,6 +31,9 @@
                     <el-radio-button label="否" :value="false"></el-radio-button>
                 </el-radio-group>
             </el-form-item>
+            <el-form-item label="菜单图标" prop="icon">
+                <sc-icon-select v-model="form.icon" clearable></sc-icon-select>
+            </el-form-item>
             <el-form-item label="路由地址" prop="path">
                 <el-input v-model="form.path" placeholder="路由地址"></el-input>
             </el-form-item>
@@ -54,6 +60,7 @@
 import XEUtils from "xe-utils";
 
 import API from "@/api";
+import { computed } from "vue";
 const $emit = defineEmits(["success", "closed"]);
 const props = defineProps({
     menuTree: { type: Array, default: () => [] }
@@ -67,7 +74,14 @@ const titleMap = reactive({
     edit: "编辑菜单"
 });
 
-const pName = ref(null);
+const treeSelect = computed(() => [{ id: 0, title: "智能生产运管平台", children: props.menuTree }]);
+const expandedKeys = computed(() => XEUtils.map(XEUtils.get(XEUtils.findTree(treeSelect.value, item => item.id == form.value.pid), "nodes", []), item => item.id).slice(0, -1));
+const treeProps = reactive({
+    label: "title",
+    value: "id",
+    disabled: data => data.id == form.value.id || (form.value.id && data.pid == form.value.id)
+});
+
 const form = ref({
     id: null,
     pid: 0,
@@ -86,31 +100,31 @@ const rules = reactive({
     path: [{ required: true, message: "请输入路由地址" }],
     component: [{ required: true, message: "请输入组件路径" }],
     menuSort: [{ required: true, message: "请输入菜单排序" }]
-})
+});
 
 const formRef = ref();
-const setData = (data, model = "add") => {
+const setData = (data = {}, model = "add") => {
     mode.value = model;
     if (model == "add") {
         formRef.value?.resetFields();
-        pName.value = XEUtils.get(data, "title");
         form.value.id = null;
         form.value.pid = XEUtils.get(data, "id", 0);
         form.value.type = form.value.pid == 0 ? 0 : 1;
         form.value.component = null;
-    } else {
-        pName.value = XEUtils.get(XEUtils.findTree(props.menuTree, item => item.id == data.pid), "item.title");
-        XEUtils.objectEach(form.value, (_, key) => XEUtils.set(form.value, key, XEUtils.get(data, key)));
-    }
+    } else XEUtils.objectEach(form.value, (_, key) => XEUtils.set(form.value, key, XEUtils.get(data, key)));
 }
 
 const submit = () => {
     formRef.value.validate(valid => {
         if (valid) {
+            const data = XEUtils.omit(form.value, "emptyField");
+            if (data.type == 0) XEUtils.set(data, "component", null);
+
             isSaving.value = true;
-            API.system.menu[mode.value](form.value).then(res => {
+            API.system.menu[mode.value](data).then(res => {
                 isSaving.value = false;
                 ElMessage.success("操作成功");
+                mode.value == "add" && (mode.value = "edit", form.value.id = res.id);
                 $emit("success", form.value.id || res.id);
             }).catch(() => isSaving.value = false);
         } else {
@@ -121,8 +135,7 @@ const submit = () => {
 
 defineExpose({
     setData
-})
-
+});
 </script>
 
 <style lang="scss" scoped>

+ 3 - 3
src/views/system/menu/index.vue

@@ -1,6 +1,6 @@
 <template>
 	<el-container class="is-vertical">
-        <sc-page-header @add="table_add({})"></sc-page-header>
+        <sc-page-header @add="table_add()"></sc-page-header>
 
         <el-container class="menu-container">
             <el-aside width="310px">
@@ -65,7 +65,7 @@ const nodeClick = data => {
     nextTick(() => menuFormRef.value?.setData(data, "edit"));
 }
 
-const table_add = data => {
+const table_add = (data = {}) => {
     dialog.value = true;
     nextTick(() => menuFormRef.value?.setData(data));
 }
@@ -84,7 +84,7 @@ const table_del = ({ id }) => {
             }
             fetchMenu();
         });
-    });
+    }).catch(() => {});
 }
 
 const menuSuccess = id => {

+ 1 - 1
src/views/system/role/bind.vue

@@ -72,7 +72,7 @@ const submit = () => {
 
 defineExpose({
     setData
-})
+});
 </script>
 
 <style scoped>

+ 2 - 2
src/views/system/role/detail.vue

@@ -42,7 +42,7 @@ const form = ref({
 const rules = reactive({
     name: [{ required: true, message: "请输入角色名称" }],
     level: [{ required: true, message: "请输入角色级别" }]
-})
+});
 
 const open = () => visible.value = true;
 const setData = data => {
@@ -71,7 +71,7 @@ const submit = () => {
 defineExpose({
     open,
     setData
-})
+});
 </script>
 
 <style scoped>

+ 3 - 3
src/views/system/role/index.vue

@@ -44,7 +44,7 @@ const formConfig = reactive({
 const paramsColums = reactive([
     { column: "orderBy", defaultValue: "id_desc" },
     { column: "nameLike" }
-])
+]);
 
 const columns = reactive([
     { type: "seq", width: 60 },
@@ -52,7 +52,7 @@ const columns = reactive([
     { type: "html", field: "level", title: "角色级别", minWidth: 160, sortable: true },
     { type: "html", field: "description", title: "描述信息", minWidth: 300, sortable: true },
     { title: "操作", fixed: "right", width: 220, align: "center", slots: { default: "action" } }
-])
+]);
 
 // 显示隐藏 筛选表单
 const xGridTable = ref();
@@ -86,7 +86,7 @@ const table_del = ({ id }) => {
             ElMessage.success("操作成功");
             refreshTable();
         });
-    });
+    }).catch(() => {});
 }
 
 const bindRef = ref();

+ 2 - 2
src/views/system/user/detail.vue

@@ -86,7 +86,7 @@ const rules = reactive({
     ],
     phone: [{ pattern: /^\d{11}$/, message: "请输入11位手机号码" }],
     deptId: [{ required: true, message: "请选择所属部门" }]
-})
+});
 
 const open = () => visible.value = true;
 const setData = data => {
@@ -129,7 +129,7 @@ fetchRole();
 defineExpose({
     open,
     setData
-})
+});
 </script>
 
 <style scoped>

+ 4 - 4
src/views/system/user/index.vue

@@ -89,7 +89,7 @@ const paramsColums = reactive([
     { column: "usernameLike" },
     { column: "nickNameLike" },
     { column: "phoneLike" }
-])
+]);
 
 const columns = reactive([
     { type: "seq", width: 60 },
@@ -101,7 +101,7 @@ const columns = reactive([
     { type: "html", field: "email", title: "邮箱", minWidth: 160, sortable: true },
     { visible: false, type: "html", field: "idcard", title: "身份证号", minWidth: 160, sortable: true, formatter: ({ cellValue, row }) => cellValue || XEUtils.get(XEUtils.toStringJSON(row.features), "idcard") },
     { title: "操作", fixed: "right", width: 220, align: "center", slots: { default: "action" } }
-])
+]);
 
 // 显示隐藏 筛选表单
 const xGridTable = ref();
@@ -135,7 +135,7 @@ const table_del = ({ id }) => {
             ElMessage.success("操作成功");
             refreshTable();
         });
-    });
+    }).catch(() => {});
 }
 
 const password_rest = ({ id, username }) => {
@@ -151,7 +151,7 @@ const password_rest = ({ id, username }) => {
                 duration: 1500
             });
         });
-    });
+    }).catch(() => {});
 }
 
 const fetchDept = () => API.system.dept.get({ orderBy: "deptSort_asc" }).then(res => deptTree.value = XEUtils.toArrayTree(res, { parentKey: "pid" })).catch(() => deptTree.value = []);

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

@@ -69,7 +69,7 @@ const form = ref(XEUtils.clone(TOOL.data.get("USER_INFO"), true));
 const rules = reactive({
     nickName: [{ required: true, message: "用户昵称不能为空" }],
     phone: [{ pattern: /^\d{11}$/, message: "请输入11位手机号码" }]
-})
+});
 
 const formRef = ref();
 const submit = () => {

+ 24 - 19
src/views/workmanship/line/desc.vue

@@ -4,25 +4,22 @@
             <el-collapse v-model="activeNames">
                 <el-collapse-item title="基本信息" name="basic">
                     <el-descriptions :column="3" label-width="140" border>
-                        <el-descriptions-item label="工艺路线名称" :span="ismobile ? 3 : 1" label-align="right" min-width="160">{{ routeData.name }}</el-descriptions-item>
-                        <el-descriptions-item label="工艺路线编号" :span="ismobile ? 3 : 1" label-align="right" min-width="160">{{ routeData.code }}</el-descriptions-item>
-                        <el-descriptions-item label="添加时间" :span="ismobile ? 3 : 1" label-align="right" min-width="160">{{ routeData.createTime }}</el-descriptions-item>
-                        <el-descriptions-item label="工艺路线状态" :span="ismobile ? 3 : 1" label-align="right" min-width="160">{{ XEUtils.get(statusDic, routeData.status, routeData.status) }}</el-descriptions-item>
-                        <el-descriptions-item label="时间单位" :span="ismobile ? 3 : 1" label-align="right" min-width="160">{{ XEUtils.get(timeUnitDic, routeData.timeUnit, routeData.timeUnit) }}</el-descriptions-item>
-                        <el-descriptions-item label="版本号" :span="ismobile ? 3 : 1" label-align="right" min-width="160">{{ routeData.version }}</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="时间单位" :span="ismobile ? 3 : 1" label-align="right">{{ XEUtils.get(workmanshipDic.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-item label="适用产品" :span="3" label-align="right">所有产品</el-descriptions-item>
-                        <el-descriptions-item label="概要" :span="3" label-align="right">{{ routeData.remark }}</el-descriptions-item>
+                        <el-descriptions-item label="概要" :span="3" label-align="right">{{ descData.remark }}</el-descriptions-item>
                         <el-descriptions-item label="附件" :span="3" label-align="right">
-                            <sc-upload-file v-model="routeData.fileList" hideAdd disabled></sc-upload-file>
+                            <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="route">
-                    <sc-form-table v-model="routeData.detailList" v-bind="{ ...tableOptions }" disabled></sc-form-table>
-                </el-collapse-item>
-
-                <el-collapse-item title="通知信息" name="notice">
+                    <sc-form-table v-model="descData.detailList" v-bind="options" ></sc-form-table>
                 </el-collapse-item>
 
                 <el-collapse-item title="质检方案" name="plan">
@@ -34,7 +31,8 @@
 
 <script setup>
 import XEUtils from "xe-utils";
-import { statusDic, timeUnitDic, tableOptions } from "../main";
+import { statusDic, workmanshipDic } from "@/utils/basicDic";
+import { tableOptions } from "./main";
 import scUploadFile from "@/components/scUpload/file";
 
 const $emit = defineEmits(["closed"]);
@@ -43,8 +41,15 @@ const visible = ref(false);
 const store = useStore();
 const ismobile = computed(() => store.state.global.ismobile);
 
-const activeNames = ref(["basic", "route", "notice", "plan"]);
-const routeData = ref({
+const options = reactive({
+    disabled: true,
+    ...tableOptions,
+    columns: tableOptions.columns.slice(1),
+    mergeFooterItems: [{ row: 0, col: 0, rowspan: 1, colspan: 4 }]
+});
+
+const activeNames = ref(["basic", "route", "plan"]);
+const descData = ref({
     id: null,
     parentId: null,
     name: null,
@@ -60,10 +65,10 @@ const routeData = ref({
 
 const setData = data => {
     visible.value = true;
-    XEUtils.objectEach(routeData.value, (_, key) => {
-        if (key == "fileList") XEUtils.set(routeData.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item, name: item.fileName })));
-        else if (key == "detailList") XEUtils.set(routeData.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item.stage, ...XEUtils.omit(item, "id", "stage") })));
-        else XEUtils.set(routeData.value, key, XEUtils.get(data, key));
+    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 == "detailList") XEUtils.set(descData.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item.stage, ...XEUtils.omit(item, "id", "stage") })));
+        else XEUtils.set(descData.value, key, XEUtils.get(data, key));
     });
 }
 

+ 51 - 46
src/views/workmanship/line/detail.vue

@@ -10,14 +10,14 @@
                             </el-form-item>
                         </el-col>
                         <el-col :md="8" :xs="24">
-                            <el-form-item label="工艺路线编号" required>
-                                <el-input v-model="form.code" :readonly="mode == 'edit'" maxlength="50" show-word-limit placeholder="不填将自动生成"></el-input>
+                            <el-form-item label="工艺路线编号" prop="code">
+                                <el-input v-model="form.code" :readonly="mode == 'edit'" maxlength="50" show-word-limit clearable placeholder="不填将自动生成"></el-input>
                             </el-form-item>
                         </el-col>
                         <el-col :md="8" :xs="24">
                             <el-form-item label="时间单位">
                                 <el-radio-group v-model="form.timeUnit">
-                                    <el-radio v-for="(label, key) in timeUnitDic" :key="key" :label="label" :value="key"></el-radio>
+                                    <el-radio v-for="(label, key) in workmanshipDic.timeUnit" :key="key" :label="label" :value="key"></el-radio>
                                 </el-radio-group>
                             </el-form-item>
                         </el-col>
@@ -38,13 +38,12 @@
                 </el-collapse-item>
 
                 <el-collapse-item title="加工路线" name="route">
-                    <sc-form-table ref="formTableRef" v-model="form.detailList" v-bind="tableOptions">
-                        <template #top>
-                            <el-button style="width: 140px;margin-bottom: 15px;" type="primary" @click="table_add">工序选择</el-button>
-                        </template>
-                    </sc-form-table>
+                    <sc-form-table ref="formTableRef" v-model="form.detailList" v-bind="tableOptions"></sc-form-table>
                 </el-collapse-item>
 
+                <!-- <el-collapse-item title="质检方案" name="plan">
+                </el-collapse-item> -->
+
                 <!-- <el-collapse-item title="适用产品" name="product">
                 </el-collapse-item> -->
 
@@ -72,17 +71,15 @@
             <el-button :loading="isSaving" type="primary" auto-insert-space @click="submit">{{ mode == "upgrade" ? "发布" : "保存" }}</el-button>
         </template>
     </el-dialog>
-
-    <select-table v-if="dialog" ref="selectTableRef" @success="dialogSuccess" @closed="dialog = false"></select-table>
 </template>
 
 <script setup>
 import XEUtils from "xe-utils";
 
 import API from "@/api";
-import { timeUnitDic, tableOptions } from "../main";
+import { workmanshipDic } from "@/utils/basicDic";
+import { tableOptions } from "./main";
 import scUploadFile from "@/components/scUpload/file";
-import selectTable from "./selectTable";
 
 const $emit = defineEmits(["success", "closed"]);
 const visible = ref(false);
@@ -97,6 +94,10 @@ const titleMap = reactive({
     upgrade: "发布新版本"
 });
 
+const oldData = reactive({
+    code: null,
+    version: null
+});
 const form = ref({
     id: null,
     parentId: null,
@@ -109,8 +110,18 @@ const form = ref({
     fileList: []
 });
 const rules = reactive({
+    code: [{ required: true, validator: (rule, value, callback) => {
+        if (mode.value === "upgrade" && value === oldData.code) return callback(new Error("请清空或更新工艺路线编号"));
+        callback();
+    }}],
     name: [{ required: true, message: "请输入工艺路线名称" }],
-    version: [{ required: true, message: "请输入版本号" }]
+    version: [
+        { required: true, message: "请输入版本号" },
+        { validator: (rule, value, callback) => {
+            if (mode.value === "upgrade" && value === oldData.version) return callback(new Error("请更新版本号"));
+            callback();
+        }}
+    ]
 });
 
 const open = () => visible.value = true;
@@ -118,46 +129,40 @@ const setData = (data, modeKey = "edit") => {
     open();
     mode.value = modeKey;
     XEUtils.objectEach(form.value, (_, key) => {
-        if (key == "fileList") XEUtils.set(form.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item, name: item.fileName })));
+        if (key == "code" || key == "version") {
+            if (modeKey === "upgrade") {
+                XEUtils.set(oldData, key, XEUtils.get(data, key));
+                XEUtils.set(form.value, key, null);
+            } else XEUtils.set(form.value, key, XEUtils.get(data, key));
+        } else if (key == "fileList") XEUtils.set(form.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item, name: item.fileName })));
         else if (key == "detailList") XEUtils.set(form.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item.stage, ...XEUtils.omit(item, "id", "stage") })));
         else XEUtils.set(form.value, key, XEUtils.get(data, key));
     });
 }
 
-const selectTableRef = ref();
-const dialog = ref(false);
-const table_add = () => {
-    dialog.value = true;
-    nextTick(() => selectTableRef.value?.setData(form.value.detailList));
-}
-
-const formTableRef = ref();
-const dialogSuccess = array => {
-    const baseNum = XEUtils.get(XEUtils.max(form.value.detailList, item => item.orderNum), "orderNum", 0);
-    const addRecords = XEUtils.map(array, (item, index) => ({ ...XEUtils.pick(item, "id", "name", "code", "processType"), stageId: item.id, orderNum: baseNum + 1 + index, isReport: true }));
-    formTableRef.value.selectChange(addRecords);
-}
-
 const formRef = ref();
+const formTableRef = ref();
 const submit = () => {
-    formRef.value.validate(valid => {
+    formRef.value.validate(async valid => {
         if (valid) {
-            if (!form.value.detailList.length) return ElMessage.warning("请维护加工路线");
-
-            const data = XEUtils.omit(form.value, "fileList", "detailList");
-            const detailList = XEUtils.map(form.value.detailList, item => XEUtils.omit(item, "id", "name", "code", "processType"));
-            const fileList = XEUtils.map(XEUtils.filter(form.value.fileList, item => !item.id), item => ({ ...XEUtils.omit(item, "id", "name"), fileName: item.name, fileType: "processRouteFile" }));
-            XEUtils.set(data, "detailList", detailList);
-            fileList.length > 0 && XEUtils.set(data, "fileList", fileList);
-
-            isSaving.value = true;
-            API.workmanship.route[mode.value](data).then(res => {
-                ElMessage.success("操作成功");
-                isSaving.value = false;
-                isDel.value = false;
-                visible.value = false;
-                $emit("success", mode.value);
-            }).catch(() => isSaving.value = false);
+            if (!form.value.detailList.length) return ElMessage.warning("请添加加工路线信息后再保存");
+            
+            if (await formTableRef.value.validateFormTable()) {
+                const data = XEUtils.omit(form.value, "fileList", "detailList");
+                const detailList = XEUtils.map(form.value.detailList, item => XEUtils.omit(item, "id", "name", "code", "processType"));
+                const fileList = XEUtils.map(XEUtils.filter(form.value.fileList, item => !item.id), item => ({ ...XEUtils.omit(item, "id", "name"), fileName: item.name, fileType: "processRouteFile" }));
+                XEUtils.set(data, "detailList", detailList);
+                fileList.length > 0 && XEUtils.set(data, "fileList", fileList);
+    
+                isSaving.value = true;
+                API.workmanship.route[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;
         }
@@ -171,7 +176,7 @@ const removeSuccess = () => {
 defineExpose({
     open,
     setData
-})
+});
 </script>
 
 <style scoped>

+ 5 - 3
src/views/workmanship/line/history.vue

@@ -20,7 +20,7 @@ const visible = ref(false);
 
 const daterangeConfig = reactive({
     span: 7,
-    resetValue: () => [moment().startOf("month").format("YYYY-MM-DD"), moment().format("YYYY-MM-DD")],
+    resetValue: () => [moment().startOf("month").format("YYYY-MM-DD HH:mm:ss"), moment().endOf("day").format("YYYY-MM-DD HH:mm:ss")],
     props: {
         type: "daterange",
         startPlaceholder: "开始日期",
@@ -34,7 +34,9 @@ const tableOptions = reactive({
     maxHeight: 1048,
     toolbarConfig: { enabled: true, print: false, zoom: false },
     formConfig: {
-        data: {},
+        data: {
+            createTime: [moment().startOf("month").format("YYYY-MM-DD HH:mm:ss"), moment().endOf("day").format("YYYY-MM-DD HH:mm:ss")]
+        },
         items: [
             mapFormItemInput("nameLike", "工艺路线名称"),
             mapFormItemInput("codeLike", "工艺路线编号"),
@@ -50,5 +52,5 @@ const setData = data => {
 
 defineExpose({
     setData
-})
+});
 </script>

+ 22 - 34
src/views/workmanship/line/index.vue

@@ -1,11 +1,15 @@
 <template>
 	<el-container class="is-vertical">
-        <sc-page-header v-if="!hidePageHeader" @add="table_add"></sc-page-header>
+        <sc-page-header @add="table_add"></sc-page-header>
 
-        <scTable ref="xGridTable" :apiObj="$API.workmanship.route" :formConfig="formConfig" :paramsColums="paramsColums" :toolbarConfig="toolbarConfig" :columns="columns" v-bind="options">
-            <template #version_link="{ row }">
+        <scTable ref="xGridTable" :apiObj="$API.workmanship.route" :formConfig="formConfig" :paramsColums="paramsColums" :toolbarConfig="toolbarConfig" :columns="columns">
+            <template #code_link="{ row }">
+                <vxe-text status="primary" @click="table_detail(row)">{{ row.code }}</vxe-text>
+            </template>
+            
+            <template #version_link="{ items, row }">
                 <vxe-text v-if="row.isHaveHistory" status="primary" @click="table_history(row)">{{ row.version }}</vxe-text>
-                <template v-else>{{ row.version }}</template>
+                <span v-else v-html="XEUtils.first(items).version" class="vxe-cell--html"></span>
             </template>
             
             <template #action="{ row }">
@@ -42,19 +46,12 @@ import XEUtils from "xe-utils";
 
 import API from "@/api";
 import TOOL from "@/utils/tool";
-import { objectToArray } from "@/utils/basicDic";
+import { statusDic, workmanshipDic, objectToArray } from "@/utils/basicDic";
 import { mapFormItemInput, mapFormItemSelect, mapFormItemDatePicker } from "@/components/scTable/helper";
-import { statusDic, timeUnitDic } from "../main";
 import processDetail from "./detail";
 import processDesc from "./desc";
 import versionHistory from "./history";
 
-const props = defineProps({
-    options: { type: Object, default: () => {} },
-    hidePageHeader: { type: Boolean, default: false },
-    hideHandler: { type: Boolean, default: false }
-});
-
 const toolbarConfig = reactive({
     enabled: true,
     export: true
@@ -65,11 +62,10 @@ const selectConfig = reactive({
     events: {
         change: data => XEUtils.merge(formConfig.data, data)
     }
-})
+});
 
 const daterangeConfig = reactive({
-    span: 7,
-    resetValue: () => [moment().startOf("month").format("YYYY-MM-DD"), moment().format("YYYY-MM-DD")],
+    resetValue: () => [],
     props: {
         type: "daterange",
         startPlaceholder: "开始日期",
@@ -78,26 +74,18 @@ const daterangeConfig = reactive({
     }
 });
 
-const versionClassName = ({ row, column }) => {
-    if (!row.isHaveHistory) {
-        console.log(row)
-        XEUtils.set(column.cellRender.props, "event", {});
-        return ""
-    } else return "";
-}
-
 const formConfig = reactive({
     data: {},
     items: [
         mapFormItemInput("nameLike", "工艺路线名称"),
         mapFormItemInput("codeLike", "工艺路线编号"),
         mapFormItemSelect("status", "工艺路线状态", selectConfig),
-        mapFormItemDatePicker("createTime", "创建日期", daterangeConfig),
+        mapFormItemDatePicker("createTime", "创建日期", daterangeConfig)
     ]
 });
 
 const paramsColums = reactive([
-    { column: "orderBy", defaultValue: "createTime_desc" },
+    { column: "orderBy", defaultValue: "code_asc" },
     { column: "parentId", defaultValue: "0" },
     { column: "nameLike" },
     { column: "codeLike" },
@@ -107,16 +95,16 @@ const paramsColums = reactive([
 ]);
 
 const columns = reactive([
-    { type: "seq", width: 60 },
-    { field: "name", title: "工艺路线名称", minWidth: 150, sortable: true, className: "vxe-table-link-cell", cellRender: { name: "VxeText", props: { status: "primary" }, events: { click: ({ row }) => table_detail(row) } } },
-    { type: "html", field: "code", title: "工艺路线编号", minWidth: 150, sortable: true },
+    { type: "seq", fixed: "left", width: 60 },
+    { 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: "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(timeUnitDic, cellValue, cellValue) },
+    { visible: false, type: "html", field: "timeUnit", title: "时间单位", minWidth: 100, sortable: true, formatter: ({ cellValue }) => XEUtils.get(workmanshipDic.timeUnit, cellValue, cellValue) },
     { type: "html", field: "", title: "质检方案", minWidth: 160, sortable: true },
-    { type: "html", field: "createTime", title: "创建日期", minWidth: 120, sortable: true, formatter: ({ cellValue }) => TOOL.dateFormat(cellValue, "YYYY-MM-DD") },
+    { 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 },
-    { visible: !props.hideHandler, title: "操作", fixed: "right", width: 320, slots: { default: "action" } }
+    { title: "操作", fixed: "right", width: 320, slots: { default: "action" } }
 ]);
 
 // 显示隐藏 筛选表单
@@ -165,7 +153,7 @@ const table_del = ({ id }) => {
             ElMessage.success("操作成功");
             refreshTable();
         });
-    });
+    }).catch(() => {});
 }
 
 const table_regrade = ({ id }) => {
@@ -178,7 +166,7 @@ const table_regrade = ({ id }) => {
             ElMessage.success("操作成功");
             refreshTable();
         });
-    });
+    }).catch(() => {});
 }
 
 const table_change = row => {
@@ -194,7 +182,7 @@ const table_change = row => {
             ElMessage.success("操作成功");
             refreshTable();
         });
-    });
+    }).catch(() => {});
 }
 
 const dialogClose = isDel => {

+ 26 - 39
src/views/workmanship/main.js

@@ -1,46 +1,17 @@
 import XEUtils from "xe-utils";
-
-export const processCategoryDic = {
-    preparation: "准备工序",
-    processing: "加工工序",
-    inspection: "检验工序",
-    auxiliary: "辅助工序"
-}
-
-export const processTypeDic = {
-    self_made: "自制",
-    outsourcing: "委外"
-}
-
-export const calcMethodDic = {
-    both_rates: "计件+计时都支持",
-    piece_rate: "计件",
-    time_rate: "计时",
-    non_prod_pay: "不计生产工资"
-}
-
-export const timeUnitDic = {
-    hour: "时",
-    minute: "分",
-    second: "秒"
-}
-
-export const statusDic = {
-    enable: "启用",
-    disable: "停用"
-}
+import { mapFormItemInput } from "@/components/scTable/helper";
+import { workmanshipDic } from "@/utils/basicDic";
 
 export const tableOptions = reactive({
-    hideAdd: true,
-    rowKey: "stageId",
+    tableKey: "stage",
+
     columns: [
-        { type: "seq", title: " ", fixed: "left", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false, slots: { default: "seq_handler" } },
-        { field: "orderNum", title: "加工顺序", fixed: "left", minWidth: 100, dragSort: true, editRender: { name: "VxeNumberInput", props: { min: 0, controlConfig: { enabled: false } } } },
+        { type: "seq", title: " ", fixed: "left", width: 80, className: "vxe-table-seq-cell__handler", footerAlign: "right", showOverflow: false, slots: { default: "seq_del" } },
+        { field: "orderNum", title: "加工顺序", fixed: "left", minWidth: 100, footerAlign: "right", dragSort: true, editRender: { name: "VxeNumberInput", props: { min: 0, controlConfig: { enabled: false } } } },
         { 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(processTypeDic, cellValue, cellValue) },
-        // { field: "", title: "工作中心", minWidth: 150 }, // select
-        // { field: "", title: "资源名称", minWidth: 150 }, // disabled
+        { field: "processType", title: "加工类型", fixed: "left", minWidth: 140, formatter: ({ cellValue }) => XEUtils.get(workmanshipDic.type, cellValue, cellValue) },
+        // { field: "", title: "工作中心", minWidth: 150 }, // select 车间
         { field: "readyTimeHour", title: "准备时间", minWidth: 100, editRender: { name: "VxeNumberInput", props: { min: 0, type: "float", controlConfig: { enabled: false } }, defaultValue: 0 } },
         { title:  "定额工时", headerAlign: "center",
             children: [
@@ -61,8 +32,24 @@ export const tableOptions = reactive({
         processNum: [{ required: true, message: "必须填写" }],
         processTimeHour: [{ required: true, message: "必须填写" }],
         moveNum: [{ required: true, message: "必须填写" }],
-        moveTimeHour: [{ required: true, message: "必须填写" }],
+        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: 5 }],
+
+    selectOptions: {
+        formConfig: {
+            data: { status: "enable" },
+            items: [
+                mapFormItemInput("nameLike", "工序名称"),
+                mapFormItemInput("codeLike", "工序编号")
+            ]
+        }
+    },
+
+    add_success: (oldValue, newValue) => {
+        const baseNum = XEUtils.get(XEUtils.max(oldValue, item => item.orderNum), "orderNum", 0);
+        const addRecords = XEUtils.map(newValue, (item, index) => ({ ...XEUtils.pick(item, "id", "name", "code", "processType"), stageId: item.id, orderNum: baseNum + 1 + index, isReport: true }));
+        return addRecords
+    }
 })

+ 0 - 47
src/views/workmanship/line/selectTable.vue

@@ -1,47 +0,0 @@
-<template>
-    <el-dialog v-model="visible" title="工序选择" :width="1000" :close-on-click-modal="false" @closed="$emit('closed')">
-        <data-table ref="tableRef" hidePageHeader hideNameLink hideHandler :options="tableOptions"></data-table>
-
-        <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 { mapFormItemInput } from "@/components/scTable/helper";
-import dataTable from "@/views/workmanship/process/index";
-
-const $emit = defineEmits(["success", "closed"]);
-const visible = ref(false);
-
-const tableRef = ref();
-const tableOptions = reactive({
-    checkedRows: [],
-    maxHeight: 1048,
-    toolbarConfig: { enabled: true, print: false, zoom: false },
-    formConfig: {
-        data: { status: "enable" },
-        items: [
-            mapFormItemInput("nameLike", "工序名称"),
-            mapFormItemInput("codeLike", "工序编号")
-        ]
-    }
-});
-
-const setData = data => {
-    visible.value = true;
-    tableOptions.checkedRows = data;
-}
-
-const submit = () => {
-    visible.value = false;
-    $emit("success", tableRef.value?.getSelectRows() || []);
-}
-
-defineExpose({
-    setData
-})
-</script>

+ 0 - 82
src/views/workmanship/process/desc.vue

@@ -1,82 +0,0 @@
-<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 :column="3" label-width="140" border>
-                        <el-descriptions-item label="工序名称" :span="ismobile ? 3 : 1" label-align="right" min-width="160">{{ processData.name }}</el-descriptions-item>
-                        <el-descriptions-item label="工序编号" :span="ismobile ? 3 : 1" label-align="right" min-width="160">{{ processData.code }}</el-descriptions-item>
-                        <el-descriptions-item label="添加时间" :span="ismobile ? 3 : 1" label-align="right" min-width="160">{{ processData.createTime }}</el-descriptions-item>
-                        <el-descriptions-item label="工序状态" :span="ismobile ? 3 : 1" label-align="right" min-width="160">{{ XEUtils.get(statusDic, processData.status, processData.status) }}</el-descriptions-item>
-                        <el-descriptions-item label="工序负责人" :span="ismobile ? 3 : 1" label-align="right" min-width="160">{{ processData.directorName }}</el-descriptions-item>
-                        <el-descriptions-item label="联系方式" :span="ismobile ? 3 : 1" label-align="right" min-width="160">{{ processData.directorPhone }}</el-descriptions-item>
-                        <el-descriptions-item label="工序分类" :span="ismobile ? 3 : 1" label-align="right" min-width="160">{{ XEUtils.get(processCategoryDic, processData.category, processData.category) }}</el-descriptions-item>
-                        <el-descriptions-item label="加工类型" :span="ismobile ? 3 : 1" label-align="right" min-width="160">{{ XEUtils.get(processTypeDic, processData.processType, processData.processType) }}</el-descriptions-item>
-                        <el-descriptions-item label="工资计算方式" :span="ismobile ? 3 : 1" label-align="right" min-width="160">{{ XEUtils.get(calcMethodDic, processData.calculateMethod, processData.calculateMethod) }}</el-descriptions-item>
-                        <el-descriptions-item label="概要" :span="3" label-align="right">{{ processData.remark }}</el-descriptions-item>
-                        <el-descriptions-item label="附件" :span="3" label-align="right">
-                            <sc-upload-file v-model="processData.fileList" hideAdd disabled></sc-upload-file>
-                        </el-descriptions-item>
-                    </el-descriptions>
-                </el-collapse-item>
-                    
-                <el-collapse-item title="通知信息" name="notice">
-                </el-collapse-item>
-
-                <el-collapse-item title="质检方案" name="plan">
-                </el-collapse-item>
-            </el-collapse>
-        </el-main>
-    </el-dialog>
-</template>
-
-<script setup>
-import XEUtils from "xe-utils";
-import { processCategoryDic, processTypeDic, calcMethodDic, statusDic } from "../main";
-import scUploadFile from "@/components/scUpload/file";
-
-const $emit = defineEmits(["closed"]);
-const visible = ref(false);
-
-const store = useStore();
-const ismobile = computed(() => store.state.global.ismobile);
-
-const activeNames = ref(["basic", "notice", "plan"]);
-const processData = ref({
-    id: null,
-    name: null,
-    code: null,
-    category: null,
-    directorName: null,
-    directorPhone: null,
-    processType: "self_made",
-    calculateMethod: "both_rates",
-    remark: null,
-    fileList: [],
-    status: "enable",
-    createTime: null
-});
-
-const setData = data => {
-    visible.value = true;
-    XEUtils.objectEach(processData.value, (_, key) => {
-        if (key == "fileList") XEUtils.set(processData.value, key, XEUtils.map(XEUtils.get(data, key), item => ({ ...item, name: item.fileName })));
-        else XEUtils.set(processData.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;}
-</style>

+ 6 - 21
src/views/workmanship/process/detail.vue

@@ -11,13 +11,13 @@
                         </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 placeholder="不填将自动生成"></el-input>
+                                <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="工序分类">
                                 <el-select v-model="form.category" clearable placeholder="请选择工序分类">
-                                    <el-option v-for="(label, key) in processCategoryDic" :key="key" :label="label" :value="key" />
+                                    <el-option v-for="(label, key) in workmanshipDic.category" :key="key" :label="label" :value="key" />
                                 </el-select>
                             </el-form-item>
                         </el-col>
@@ -31,32 +31,17 @@
                                 <el-input v-model="form.directorPhone" clearable placeholder="请输入负责人联系方式"></el-input>
                             </el-form-item>
                         </el-col>
-                        <!-- <el-col :md="8" :xs="24">
-                            <el-form-item label="质检方案">
-                                <el-select v-model="form.category" clearable placeholder="请选择质检方案类">
-                                    <el-option v-for="(label, key) in processCategoryDic" :key="key" :label="label" :value="key" />
-                                </el-select>
-                            </el-form-item>
-                        </el-col> -->
-                        <!-- <el-col :md="8" :xs="24">
-                            <el-form-item label="设置工序替代">
-                                <el-radio-group v-model="form.isReplace">
-                                    <el-radio label="是" :value="true"></el-radio>
-                                    <el-radio label="否" :value="false"></el-radio>
-                                </el-radio-group>
-                            </el-form-item>
-                        </el-col> -->
                         <el-col :md="8" :xs="24">
                             <el-form-item label="默认加工类型">
                                 <el-radio-group v-model="form.processType">
-                                    <el-radio v-for="(label, key) in processTypeDic" :key="key" :label="label" :value="key"></el-radio>
+                                    <el-radio v-for="(label, key) in workmanshipDic.type" :key="key" :label="label" :value="key"></el-radio>
                                 </el-radio-group>
                             </el-form-item>
                         </el-col>
                         <el-col :xs="24">
                             <el-form-item label="工资默认计算方式" label-width="150" required>
                                 <el-radio-group v-model="form.calculateMethod">
-                                    <el-radio v-for="(label, key) in calcMethodDic" :key="key" :label="label" :value="key"></el-radio>
+                                    <el-radio v-for="(label, key) in workmanshipDic.calcMethod" :key="key" :label="label" :value="key"></el-radio>
                                 </el-radio-group>
                             </el-form-item>
                         </el-col>
@@ -93,7 +78,7 @@
 import XEUtils from "xe-utils";
 
 import API from "@/api";
-import { processCategoryDic, processTypeDic, calcMethodDic } from "../main";
+import { workmanshipDic } from "@/utils/basicDic";
 import scUploadFile from "@/components/scUpload/file";
 
 const $emit = defineEmits(["success", "closed"]);
@@ -164,7 +149,7 @@ const removeSuccess = () => {
 defineExpose({
     open,
     setData
-})
+});
 </script>
 
 <style scoped>

+ 26 - 46
src/views/workmanship/process/index.vue

@@ -3,11 +3,6 @@
         <sc-page-header v-if="!hidePageHeader" @add="table_add"></sc-page-header>
 
         <scTable ref="xGridTable" :apiObj="$API.workmanship.process" :formConfig="formConfig" :paramsColums="paramsColums" :toolbarConfig="toolbarConfig" :columns="columns" v-bind="options">
-            <template #name_link="{ row }">
-                <vxe-text v-if="!hideNameLink" status="primary" @click="table_detail(row)">{{ row.name }}</vxe-text>
-                <template v-else>{{ row.name }}</template>
-            </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>修改
@@ -25,8 +20,7 @@
         </scTable>
 	</el-container>
 
-    <process-detail v-if="dialog.detail" ref="processRef" @success="refreshTable" @closed="dialogClose"></process-detail>
-    <process-desc v-if="dialog.desc" ref="processDescRef" @closed="dialog.desc = false"></process-desc>
+    <process-detail v-if="dialog" ref="processRef" @success="refreshTable" @closed="dialogClose"></process-detail>
 </template>
 
 <script setup>
@@ -35,17 +29,15 @@ import XEUtils from "xe-utils";
 
 import API from "@/api";
 import TOOL from "@/utils/tool";
-import { objectToArray } from "@/utils/basicDic";
+import { statusDic, workmanshipDic, objectToArray } from "@/utils/basicDic";
 import { mapFormItemInput, mapFormItemSelect, mapFormItemDatePicker } from "@/components/scTable/helper";
-import { statusDic, processCategoryDic, processTypeDic, calcMethodDic } from "../main";
 import processDetail from "./detail";
-import processDesc from "./desc";
 
 const props = defineProps({
     options: { type: Object, default: () => {} },
     hidePageHeader: { type: Boolean, default: false },
-    hideNameLink: { type: Boolean, default: false },
-    hideHandler: { type: Boolean, default: false }
+    hideHandler: { type: Boolean, default: false },
+    hideCheckbox: { type: Boolean, default: false }
 });
 
 const toolbarConfig = reactive({
@@ -58,18 +50,17 @@ const selectConfig = reactive({
     events: {
         change: data => XEUtils.merge(formConfig.data, data)
     }
-})
+});
 
 const daterangeConfig = reactive({
-    span: 7,
-    resetValue: () => [moment().startOf("month").format("YYYY-MM-DD"), moment().format("YYYY-MM-DD")],
+    resetValue: () => [],
     props: {
         type: "daterange",
         startPlaceholder: "开始日期",
         endPlaceholder: "结束日期",
         format: "YYYY-MM-DD"
     }
-})
+});
 
 const formConfig = reactive({
     data: {},
@@ -77,13 +68,13 @@ const formConfig = reactive({
         mapFormItemInput("nameLike", "工序名称"),
         mapFormItemInput("codeLike", "工序编号"),
         mapFormItemSelect("status", "工序状态", selectConfig),
-        mapFormItemSelect("category", "工序分类", { ...selectConfig, options: objectToArray(processCategoryDic) }),
-        mapFormItemDatePicker("createTime", "创建日期", daterangeConfig),
+        mapFormItemSelect("category", "工序分类", { ...selectConfig, options: objectToArray(workmanshipDic.category) }),
+        mapFormItemDatePicker("createTime", "创建日期", daterangeConfig)
     ]
 });
 
 const paramsColums = reactive([
-    { column: "orderBy", defaultValue: "createTime_desc" },
+    { column: "orderBy", defaultValue: "code_asc" },
     { column: "nameLike" },
     { column: "codeLike" },
     { column: "status" },
@@ -93,19 +84,17 @@ const paramsColums = reactive([
 ]);
 
 const columns = reactive([
-    { visible: props.hideHandler, type: "checkbox", fixed: "left", width: 40 },
-    { type: "seq", width: 60 },
-    { type: "html", field: "name", title: "工序名称", minWidth: 150, sortable: true, className: "vxe-table-link-cell", slots: { default: "name_link" } },
-    { type: "html", field: "version", title: "版本号", minWidth: 120, sortable: true },
-    { type: "html", field: "code", title: "工序编号", minWidth: 150, sortable: true },
+    { visible: props.hideHandler, type: props.hideCheckbox && "radio" || "checkbox", fixed: "left", width: 40 },
+    { type: "seq", fixed: "left", width: 60 },
+    { 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: "category", title: "工序分类", minWidth: 100, sortable: true, formatter: ({ cellValue }) => XEUtils.get(processCategoryDic, cellValue, cellValue) },
+    { type: "html", field: "category", title: "工序分类", minWidth: 100, sortable: true, formatter: ({ cellValue }) => XEUtils.get(workmanshipDic.category, cellValue, cellValue) },
     { type: "html", field: "directorName", title: "工序负责人", minWidth: 120, sortable: true },
     { visible: false, type: "html", field: "directorPhone", title: "联系方式", minWidth: 120, sortable: true },
-    { visible: false, type: "html", field: "processType", title: "默认加工类型", minWidth: 140, sortable: true, formatter: ({ cellValue }) => XEUtils.get(processTypeDic, cellValue, cellValue) },
-    { visible: false, type: "html", field: "calculateMethod", title: "工资默认计算方式", minWidth: 140, sortable: true, formatter: ({ cellValue }) => XEUtils.get(calcMethodDic, cellValue, cellValue) },
-    { type: "html", field: "", title: "质检方案", minWidth: 160, sortable: true },
-    { type: "html", field: "createTime", title: "创建日期", minWidth: 120, sortable: true, formatter: ({ cellValue }) => TOOL.dateFormat(cellValue, "YYYY-MM-DD") },
+    { visible: false, type: "html", field: "processType", title: "默认加工类型", minWidth: 140, sortable: true, formatter: ({ cellValue }) => XEUtils.get(workmanshipDic.type, cellValue, cellValue) },
+    { visible: false, type: "html", field: "calculateMethod", title: "工资默认计算方式", minWidth: 140, sortable: true, formatter: ({ cellValue }) => XEUtils.get(workmanshipDic.calcMethod, cellValue, cellValue) },
+    { 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 },
     { visible: !props.hideHandler, title: "操作", fixed: "right", width: 220, slots: { default: "action" } }
 ]);
@@ -116,30 +105,21 @@ const refreshTable = (mode = "add") => {
     xGridTable.value.reloadColumn(columns);
     xGridTable.value.searchData(mode);
 }
-const getSelectRows = () => xGridTable.value.selectedRows;
+const getSelectRows = () => xGridTable.value.getSelectRows();
 
 const processRef = ref();
-const processDescRef = ref();
-const dialog = reactive({
-    detail: false,
-    desc: false
-});
+const dialog = ref(false);
 
 const table_add = () => {
-    dialog.detail = true;
+    dialog.value = true;
     nextTick(() => processRef.value?.open());
 }
 
 const table_edit = row => {
-    dialog.detail = true;
+    dialog.value = true;
     nextTick(() => processRef.value?.setData(row));
 }
 
-const table_detail = row => {
-    dialog.desc = true;
-    nextTick(() => processDescRef.value?.setData(row));
-}
-
 const table_del = ({ id }) => {
     ElMessageBox.confirm("是否确认删除该工序?", "删除警告", {
         type: "warning",
@@ -150,7 +130,7 @@ const table_del = ({ id }) => {
             ElMessage.success("操作成功");
             refreshTable();
         });
-    });
+    }).catch(() => {});
 }
 
 const table_change = row => {
@@ -166,15 +146,15 @@ const table_change = row => {
             ElMessage.success("操作成功");
             refreshTable();
         });
-    });
+    }).catch(() => {});
 }
 
 const dialogClose = isDel => {
-    dialog.detail = false;
+    dialog.value = false;
     isDel && refreshTable();
 }
 
 defineExpose({
     getSelectRows
-})
+});
 </script>

+ 2 - 1
src/vxeTable.js

@@ -51,7 +51,8 @@ VxeUI.renderer.mixin({
     "$cell-tag": {
         renderTableCell(renderOpts, params) {
             const field = XEUtils.get(params, "column.field")
-            const defaultValue = params.$grid.getCellLabel(params.row, field)
+            const row = XEUtils.get(XEUtils.findTree(params.$grid.getData(), item => item.id == params.rowid), "item") || params.row
+            const defaultValue = XEUtils.has(renderOpts, "formatter") && renderOpts.formatter(row) || params.$grid.getCellLabel(row, field)
 
             const props = XEUtils.get(renderOpts, "props", {})
             const options = XEUtils.get(renderOpts, "options", {})