From bb201f4cf6bbe1b3669bffdb3aae2c7514cc32ea Mon Sep 17 00:00:00 2001
From: huminmin <mac@MacBook-Pro.local>
Date: 星期三, 21 一月 2026 13:04:33 +0800
Subject: [PATCH] 将军泰伟业生产管控页面迁移到双奇点

---
 src/views/basicData/product/ProductSelectDialog.vue |  211 ++++++++++++++++++++++++++++++
 src/api/basicData/productModel.js                   |    9 +
 src/components/Dialog/ImportDialog.vue              |  172 ++++++++++++++++++++++++
 3 files changed, 392 insertions(+), 0 deletions(-)

diff --git a/src/api/basicData/productModel.js b/src/api/basicData/productModel.js
new file mode 100644
index 0000000..2e1ed01
--- /dev/null
+++ b/src/api/basicData/productModel.js
@@ -0,0 +1,9 @@
+import request from "@/utils/request.js";
+
+export function productModelList(query) {
+    return request({
+        url: '/basic/product/pageModel',
+        method: 'get',
+        params: query
+    })
+}
diff --git a/src/components/Dialog/ImportDialog.vue b/src/components/Dialog/ImportDialog.vue
new file mode 100644
index 0000000..5b126dc
--- /dev/null
+++ b/src/components/Dialog/ImportDialog.vue
@@ -0,0 +1,172 @@
+<template>
+  <el-dialog
+    :title="title"
+    v-model="dialogVisible"
+    :width="width"
+    :append-to-body="appendToBody"
+    @close="handleClose"
+  >
+    <el-upload
+      ref="uploadRef"
+      :limit="limit"
+      :accept="accept"
+      :headers="headers"
+      :action="action"
+      :disabled="disabled"
+      :before-upload="beforeUpload"
+      :on-progress="onProgress"
+      :on-success="onSuccess"
+      :on-error="onError"
+      :on-change="onChange"
+      :auto-upload="autoUpload"
+      drag
+    >
+      <el-icon class="el-icon--upload"><UploadFilled /></el-icon>
+      <div class="el-upload__text">灏嗘枃浠舵嫋鍒版澶勶紝鎴�<em>鐐瑰嚮涓婁紶</em></div>
+      <template #tip>
+        <div class="el-upload__tip text-center">
+          <span>{{ tipText }}</span>
+          <el-link
+            v-if="showDownloadTemplate"
+            type="primary"
+            :underline="false"
+            style="font-size: 12px; vertical-align: baseline; margin-left: 5px;"
+            @click="handleDownloadTemplate"
+            >涓嬭浇妯℃澘</el-link
+          >
+        </div>
+      </template>
+    </el-upload>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button type="primary" @click="handleConfirm">纭� 瀹�</el-button>
+        <el-button @click="handleCancel">鍙� 娑�</el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup>
+import { computed, ref } from 'vue'
+import { UploadFilled } from '@element-plus/icons-vue'
+
+const props = defineProps({
+  modelValue: {
+    type: Boolean,
+    default: false
+  },
+  title: {
+    type: String,
+    default: '瀵煎叆'
+  },
+  width: {
+    type: String,
+    default: '400px'
+  },
+  appendToBody: {
+    type: Boolean,
+    default: true
+  },
+  limit: {
+    type: Number,
+    default: 1
+  },
+  accept: {
+    type: String,
+    default: '.xlsx, .xls'
+  },
+  headers: {
+    type: Object,
+    default: () => ({})
+  },
+  action: {
+    type: String,
+    required: true
+  },
+  disabled: {
+    type: Boolean,
+    default: false
+  },
+  autoUpload: {
+    type: Boolean,
+    default: false
+  },
+  tipText: {
+    type: String,
+    default: '浠呭厑璁稿鍏ls銆亁lsx鏍煎紡鏂囦欢銆�'
+  },
+  showDownloadTemplate: {
+    type: Boolean,
+    default: true
+  },
+  beforeUpload: {
+    type: Function,
+    default: null
+  },
+  onProgress: {
+    type: Function,
+    default: null
+  },
+  onSuccess: {
+    type: Function,
+    default: null
+  },
+  onError: {
+    type: Function,
+    default: null
+  },
+  onChange: {
+    type: Function,
+    default: null
+  }
+})
+
+const emit = defineEmits(['update:modelValue', 'close', 'confirm', 'cancel', 'download-template'])
+
+const dialogVisible = computed({
+  get: () => props.modelValue,
+  set: (val) => emit('update:modelValue', val)
+})
+
+const uploadRef = ref(null)
+
+const handleClose = () => {
+  emit('close')
+}
+
+const handleConfirm = () => {
+  emit('confirm')
+}
+
+const submit = () => {
+  if (uploadRef.value) {
+    uploadRef.value.submit()
+  }
+}
+
+const handleCancel = () => {
+  emit('cancel')
+  dialogVisible.value = false
+}
+
+const handleDownloadTemplate = () => {
+  emit('download-template')
+}
+
+defineExpose({
+  uploadRef,
+  submit,
+  clearFiles: () => {
+    if (uploadRef.value) {
+      uploadRef.value.clearFiles()
+    }
+  }
+})
+</script>
+
+<style scoped>
+.dialog-footer {
+  text-align: center;
+}
+</style>
+
diff --git a/src/views/basicData/product/ProductSelectDialog.vue b/src/views/basicData/product/ProductSelectDialog.vue
new file mode 100644
index 0000000..9f85785
--- /dev/null
+++ b/src/views/basicData/product/ProductSelectDialog.vue
@@ -0,0 +1,211 @@
+<template>
+  <el-dialog
+      v-model="visible"
+      title="閫夋嫨浜у搧"
+      width="900px"
+      destroy-on-close
+      :close-on-click-modal="false"
+  >
+    <el-form :inline="true" :model="query" class="mb-2">
+      <el-form-item label="浜у搧澶х被">
+        <el-input
+            v-model="query.productName"
+            placeholder="杈撳叆浜у搧澶х被"
+            clearable
+            @keyup.enter="onSearch"
+        />
+      </el-form-item>
+
+      <el-form-item label="鍨嬪彿鍚嶇О">
+        <el-input
+            v-model="query.model"
+            placeholder="杈撳叆鍨嬪彿鍚嶇О"
+            clearable
+            @keyup.enter="onSearch"
+        />
+      </el-form-item>
+
+      <el-form-item>
+        <el-button type="primary" @click="onSearch">鎼滅储</el-button>
+        <el-button @click="onReset">閲嶇疆</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 鍒楄〃 -->
+    <el-table
+        ref="tableRef"
+        v-loading="loading"
+        :data="tableData"
+        height="420"
+        highlight-current-row
+        row-key="id"
+        @selection-change="handleSelectionChange"
+        @select="handleSelect"
+    >
+      <el-table-column type="selection" width="55" />
+      <el-table-column type="index" label="#" width="60"/>
+      <el-table-column prop="productName" label="浜у搧澶х被" min-width="160"/>
+      <el-table-column prop="model" label="鍨嬪彿鍚嶇О" min-width="200"/>
+      <el-table-column prop="unit" label="鍗曚綅" min-width="160"/>
+    </el-table>
+
+    <div class="mt-3 flex justify-end">
+      <el-pagination
+          background
+          layout="total, sizes, prev, pager, next, jumper"
+          :total="total"
+          v-model:page-size="page.pageSize"
+          v-model:current-page="page.pageNum"
+          :page-sizes="[10, 20, 50, 100]"
+          @size-change="onPageChange"
+          @current-change="onPageChange"
+      />
+    </div>
+
+    <template #footer>
+      <el-button @click="close()">鍙栨秷</el-button>
+      <el-button type="primary" :disabled="multipleSelection.length === 0" @click="onConfirm">
+        纭畾
+      </el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+import {computed, onMounted, reactive, ref, watch, nextTick} from "vue";
+import {ElMessage} from "element-plus";
+import {productModelList} from '@/api/basicData/productModel'
+
+export type ProductRow = {
+  id: number;
+  productName: string;
+  model: string;
+  unit?: string;
+};
+
+const props = defineProps<{
+  modelValue: boolean;
+  single?: boolean; // 鏄惁鍙兘閫夋嫨涓�涓紝榛樿false锛堝彲閫夋嫨澶氫釜锛�
+}>();
+
+const emit = defineEmits(['update:modelValue', 'confirm']);
+
+const visible = computed({
+  get: () => props.modelValue,
+  set: (v) => emit("update:modelValue", v),
+});
+
+const query = reactive({
+  productName: "",
+  model: "",
+});
+
+const page = reactive({
+  pageNum: 1,
+  pageSize: 10,
+});
+
+const loading = ref(false);
+const tableData = ref<ProductRow[]>([]);
+const total = ref(0);
+const multipleSelection = ref<ProductRow[]>([]);
+const tableRef = ref();
+
+function close() {
+  visible.value = false;
+}
+
+const handleSelectionChange = (val: ProductRow[]) => {
+  if (props.single && val.length > 1) {
+    // 濡傛灉闄愬埗涓哄崟涓�夋嫨锛屽彧淇濈暀鏈�鍚庝竴涓�変腑鐨�
+    const lastSelected = val[val.length - 1];
+    multipleSelection.value = [lastSelected];
+    // 娓呯┖琛ㄦ牸閫変腑鐘舵�侊紝鐒跺悗閲嶆柊閫変腑鏈�鍚庝竴涓�
+    nextTick(() => {
+      if (tableRef.value) {
+        tableRef.value.clearSelection();
+        tableRef.value.toggleRowSelection(lastSelected, true);
+      }
+    });
+  } else {
+    multipleSelection.value = val;
+  }
+}
+
+// 澶勭悊鍗曚釜閫夋嫨
+const handleSelect = (selection: ProductRow[], row: ProductRow) => {
+  if (props.single) {
+    // 濡傛灉闄愬埗涓哄崟涓紝娓呯┖鍏朵粬閫夋嫨锛屽彧淇濈暀褰撳墠琛�
+    if (selection.includes(row)) {
+      // 閫変腑褰撳墠琛屾椂锛屾竻绌哄叾浠栭�変腑
+      multipleSelection.value = [row];
+      nextTick(() => {
+        if (tableRef.value) {
+          tableData.value.forEach((item) => {
+            if (item.id !== row.id) {
+              tableRef.value.toggleRowSelection(item, false);
+            }
+          });
+        }
+      });
+    }
+  }
+}
+
+function onSearch() {
+  page.pageNum = 1;
+  loadData();
+}
+
+function onReset() {
+  query.productName = "";
+  query.model = "";
+  page.pageNum = 1;
+  loadData();
+}
+
+function onPageChange() {
+  loadData();
+}
+
+function onConfirm() {
+  if (multipleSelection.value.length === 0) {
+    ElMessage.warning("璇烽�夋嫨涓�鏉′骇鍝�");
+    return;
+  }
+  if (props.single && multipleSelection.value.length > 1) {
+    ElMessage.warning("鍙兘閫夋嫨涓�涓骇鍝�");
+    return;
+  }
+  emit("confirm", props.single ? [multipleSelection.value[0]] : multipleSelection.value);
+  close();
+}
+
+async function loadData() {
+  loading.value = true;
+  try {
+    multipleSelection.value = []; // 缈婚〉/鎼滅储鍚庢竻绌洪�夋嫨鏇寸鍚堥鏈�
+    const res = await productModelList({
+      productName: query.productName.trim(),
+      model: query.model.trim(),
+      current: page.pageNum,
+      size: page.pageSize,
+    });
+    tableData.value = res.records;
+    total.value = res.total;
+  } finally {
+    loading.value = false;
+  }
+}
+
+// 鐩戝惉寮圭獥鎵撳紑锛岄噸缃�夋嫨
+watch(() => props.modelValue, (visible) => {
+  if (visible) {
+    multipleSelection.value = [];
+  }
+});
+
+onMounted(() => {
+  loadData()
+})
+</script>

--
Gitblit v1.9.3