Bladeren bron

feat: 添加文件上传功能,支持购机者照片、证件附件和发票附件的上传与删除,优化附件展示逻辑

laiqi 5 maanden geleden
bovenliggende
commit
10eaf415f0
3 gewijzigde bestanden met toevoegingen van 516 en 77 verwijderingen
  1. 1 0
      .serena/.gitignore
  2. 84 0
      .serena/project.yml
  3. 431 77
      apps/web-ele/src/views/order-manage/detail.vue

+ 1 - 0
.serena/.gitignore

@@ -0,0 +1 @@
+/cache

+ 84 - 0
.serena/project.yml

@@ -0,0 +1,84 @@
+# list of languages for which language servers are started; choose from:
+#   al               bash             clojure          cpp              csharp           csharp_omnisharp
+#   dart             elixir           elm              erlang           fortran          go
+#   haskell          java             julia            kotlin           lua              markdown
+#   nix              perl             php              python           python_jedi      r
+#   rego             ruby             ruby_solargraph  rust             scala            swift
+#   terraform        typescript       typescript_vts   yaml             zig
+# Note:
+#   - For C, use cpp
+#   - For JavaScript, use typescript
+# Special requirements:
+#   - csharp: Requires the presence of a .sln file in the project folder.
+# When using multiple languages, the first language server that supports a given file will be used for that file.
+# The first language is the default language and the respective language server will be used as a fallback.
+# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored.
+languages:
+- typescript
+
+# the encoding used by text files in the project
+# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
+encoding: "utf-8"
+
+# whether to use the project's gitignore file to ignore files
+# Added on 2025-04-07
+ignore_all_files_in_gitignore: true
+
+# list of additional paths to ignore
+# same syntax as gitignore, so you can use * and **
+# Was previously called `ignored_dirs`, please update your config if you are using that.
+# Added (renamed) on 2025-04-07
+ignored_paths: []
+
+# whether the project is in read-only mode
+# If set to true, all editing tools will be disabled and attempts to use them will result in an error
+# Added on 2025-04-18
+read_only: false
+
+# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
+# Below is the complete list of tools for convenience.
+# To make sure you have the latest list of tools, and to view their descriptions, 
+# execute `uv run scripts/print_tool_overview.py`.
+#
+#  * `activate_project`: Activates a project by name.
+#  * `check_onboarding_performed`: Checks whether project onboarding was already performed.
+#  * `create_text_file`: Creates/overwrites a file in the project directory.
+#  * `delete_lines`: Deletes a range of lines within a file.
+#  * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
+#  * `execute_shell_command`: Executes a shell command.
+#  * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
+#  * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
+#  * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
+#  * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
+#  * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
+#  * `initial_instructions`: Gets the initial instructions for the current project.
+#     Should only be used in settings where the system prompt cannot be set,
+#     e.g. in clients you have no control over, like Claude Desktop.
+#  * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
+#  * `insert_at_line`: Inserts content at a given line in a file.
+#  * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
+#  * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
+#  * `list_memories`: Lists memories in Serena's project-specific memory store.
+#  * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
+#  * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
+#  * `read_file`: Reads a file within the project directory.
+#  * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
+#  * `remove_project`: Removes a project from the Serena configuration.
+#  * `replace_lines`: Replaces a range of lines within a file with new content.
+#  * `replace_symbol_body`: Replaces the full definition of a symbol.
+#  * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
+#  * `search_for_pattern`: Performs a search for a pattern in the project.
+#  * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
+#  * `switch_modes`: Activates modes by providing a list of their names
+#  * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
+#  * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
+#  * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
+#  * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
+excluded_tools: []
+
+# initial prompt for the project. It will always be given to the LLM upon activating the project
+# (contrary to the memories, which are loaded on demand).
+initial_prompt: ""
+
+project_name: "DZ-MXWPP-WEB"
+included_optional_tools: []

+ 431 - 77
apps/web-ele/src/views/order-manage/detail.vue

@@ -1,18 +1,28 @@
 <script lang="ts" setup>
-import { h, ref } from 'vue';
+import { ref } from 'vue';
 
 import { useVbenModal } from '@vben/common-ui';
+import { X } from '@vben/icons';
 
 import {
+  ElButton,
   ElCard,
   ElDescriptions,
   ElDescriptionsItem,
+  ElDialog,
   ElImage,
+  ElMessage,
+  ElMessageBox,
   ElTag,
+  ElUpload,
 } from 'element-plus';
 
 import { getDictListApi } from '#/api/dict';
-import { getAttachmentListApi } from '#/api/file';
+import {
+  addAttachmentApi,
+  deleteAttachmentApi,
+  getAttachmentListApi,
+} from '#/api/file';
 import { getOrdersDetailApi } from '#/api/orders';
 
 const data = ref();
@@ -23,6 +33,11 @@ const certificateDataList = ref<any[]>([]);
 const buyerPhotoDataList = ref<any[]>([]);
 const coupon2sid = ref(''); // 优惠券id
 
+// 上传对话框相关状态
+const uploadDialogVisible = ref(false);
+const currentUploadType = ref('');
+const uploadLoading = ref(false);
+
 const [Modal, modalApi] = useVbenModal({
   class: 'w-2/3',
   onCancel() {
@@ -143,6 +158,210 @@ const getFilePrefix = async () => {
   return '';
 };
 
+// 处理删除附件
+const handleDeleteAttachment = async (attid: string, type: string) => {
+  try {
+    // 显示确认对话框
+    await ElMessageBox.confirm(
+      '确定要删除这个附件吗?删除后无法恢复。',
+      '删除确认',
+      {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+      }
+    );
+
+    // 用户确认后执行删除
+    await deleteAttachmentApi({ attid });
+    ElMessage.success('删除成功');
+
+    // 根据类型刷新对应的附件列表
+    const filePrefix = await getFilePrefix();
+
+    switch (type) {
+      case 'buyer': {
+        await getOrderBuyerPhoto(coupon2sid.value, filePrefix);
+        break;
+      }
+      case 'certificate': {
+        await getOrderCertificate(coupon2sid.value, filePrefix);
+        break;
+      }
+      case 'invoice': {
+        await getOrderInvoice(data.value.row.ordersid, filePrefix);
+        break;
+      }
+    }
+  } catch (error) {
+    // 如果用户取消操作,error会包含'cancel'字符串,我们不显示错误消息
+    if (error !== 'cancel') {
+      console.error('删除附件失败:', error);
+      ElMessage.error('删除失败,请重试');
+    }
+  }
+};
+
+// 处理上传附件点击
+const handleUploadClick = (type: string) => {
+  currentUploadType.value = type;
+  uploadDialogVisible.value = true;
+};
+
+// 处理文件上传前的钩子
+const beforeUpload = (file: File) => {
+  // 根据上传类型设置不同的文件类型验证
+  let isValidType = false;
+  const isLt10M = file.size / 1024 / 1024 < 10;
+
+  switch (currentUploadType.value) {
+    case 'buyer':
+    case 'certificate': {
+      // 购机者照片和证件照片只能上传图片
+      isValidType = [
+        'image/bmp',
+        'image/gif',
+        'image/jpeg',
+        'image/png',
+      ].includes(file.type);
+      if (!isValidType) {
+        ElMessage.error('只能上传JPG/PNG/GIF/BMP格式的图片文件!');
+        return false;
+      }
+      break;
+    }
+    case 'invoice': {
+      // 发票附件可以上传图片或PDF
+      isValidType = [
+        'application/pdf',
+        'image/bmp',
+        'image/gif',
+        'image/jpeg',
+        'image/png',
+      ].includes(file.type);
+      if (!isValidType) {
+        ElMessage.error('只能上传JPG/PNG/GIF/BMP/PDF格式的文件!');
+        return false;
+      }
+      break;
+    }
+  }
+
+  if (!isLt10M) {
+    ElMessage.error('上传文件大小不能超过10MB!');
+    return false;
+  }
+
+  return true;
+};
+
+// 处理文件上传
+const handleFileUpload = async (options: any) => {
+  const { file } = options;
+  uploadLoading.value = true;
+
+  try {
+    // 检查是否已有文件,每种类型只能上传一个文件
+    let hasExistingFile = false;
+    let existingFileName = '';
+
+    switch (currentUploadType.value) {
+      case 'buyer': {
+        if (buyerPhotoDataList.value.length > 0) {
+          hasExistingFile = true;
+          existingFileName =
+            buyerPhotoDataList.value[0].attorginname ||
+            `${buyerPhotoDataList.value[0].attname}.${buyerPhotoDataList.value[0].atttype}`;
+        }
+        break;
+      }
+      case 'certificate': {
+        if (certificateDataList.value.length > 0) {
+          hasExistingFile = true;
+          existingFileName =
+            certificateDataList.value[0].attorginname ||
+            `${certificateDataList.value[0].attname}.${certificateDataList.value[0].atttype}`;
+        }
+        break;
+      }
+      case 'invoice': {
+        if (invoiceDataList.value.length > 0) {
+          hasExistingFile = true;
+          existingFileName =
+            invoiceDataList.value[0].attorginname ||
+            `${invoiceDataList.value[0].attname}.${invoiceDataList.value[0].atttype}`;
+        }
+        break;
+      }
+    }
+
+    if (hasExistingFile) {
+      ElMessage.warning(
+        `已存在文件"${existingFileName}",请先删除现有文件再上传新文件`,
+      );
+      uploadDialogVisible.value = false;
+      return;
+    }
+
+    const formData = new FormData();
+    formData.append('file', file);
+
+    // 根据上传类型设置不同的参数
+    let attlsh = '';
+    let attmodel = '';
+
+    switch (currentUploadType.value) {
+      case 'buyer': {
+        attlsh = coupon2sid.value;
+        attmodel = 'coupon_buyer';
+        break;
+      }
+      case 'certificate': {
+        attlsh = coupon2sid.value;
+        attmodel = 'coupon_identity';
+        break;
+      }
+      case 'invoice': {
+        attlsh = data.value.row.ordersid;
+        attmodel = 'coupon_invoice';
+        break;
+      }
+    }
+
+    formData.append('attlsh', attlsh);
+    formData.append('attmodel', attmodel);
+
+    // 调用上传API
+    await addAttachmentApi(formData);
+
+    ElMessage.success('上传成功!');
+    uploadDialogVisible.value = false;
+
+    // 刷新对应的附件列表
+    const filePrefix = await getFilePrefix();
+
+    switch (currentUploadType.value) {
+      case 'buyer': {
+        await getOrderBuyerPhoto(coupon2sid.value, filePrefix);
+        break;
+      }
+      case 'certificate': {
+        await getOrderCertificate(coupon2sid.value, filePrefix);
+        break;
+      }
+      case 'invoice': {
+        await getOrderInvoice(data.value.row.ordersid, filePrefix);
+        break;
+      }
+    }
+  } catch (error) {
+    console.error('上传失败:', error);
+    ElMessage.error('上传失败,请重试!');
+  } finally {
+    uploadLoading.value = false;
+  }
+};
+
 // 订单状态映射
 const getOrderStatusTag = (status: number) => {
   const statusMap = {
@@ -159,72 +378,10 @@ const getOrderStatusTag = (status: number) => {
   );
 };
 
-// 渲染附件
-const renderAttachments = (attachments: any[], emptyText: string) => {
-  if (attachments.length === 0) {
-    return h('div', { class: 'text-gray-500 text-sm' }, emptyText);
-  }
-
-  return h(
-    'div',
-    { class: 'flex flex-wrap gap-3' },
-    attachments.map((item) => {
-      // 判断文件类型,根据atttype进行不同展示
-      if (item.atttype === 'pdf') {
-        // PDF文件显示为链接
-        return h(
-          'a',
-          {
-            href: item.url,
-            target: '_blank',
-            class:
-              'inline-flex items-center px-3 py-2 text-sm font-medium text-blue-600 bg-blue-50 border border-blue-200 rounded-lg hover:bg-blue-100 hover:text-blue-700 transition-colors',
-            title: item.attorginname || item.attname,
-          },
-          [
-            h('i', { class: 'fas fa-file-pdf mr-2 text-red-500' }),
-            item.attorginname || `${item.attname}.${item.atttype}`,
-          ],
-        );
-      } else {
-        // 图片文件使用ElImage展示
-        const imageExtensions = new Set([
-          'bmp',
-          'gif',
-          'jpeg',
-          'jpg',
-          'png',
-          'webp',
-        ]);
-        return imageExtensions.has(item.atttype.toLowerCase())
-          ? h(ElImage, {
-              src: item.url,
-              previewSrcList: attachments
-                .filter((i) => imageExtensions.has(i.atttype.toLowerCase()))
-                .map((i) => i.url),
-              fit: 'cover',
-              style: 'width: 100px; height: 100px;',
-              class: 'border rounded',
-              previewTeleported: true,
-            })
-          : // 其他文件类型显示为普通链接
-            h(
-              'a',
-              {
-                href: item.url,
-                target: '_blank',
-                class:
-                  'inline-flex items-center px-3 py-2 text-sm font-medium text-gray-600 bg-gray-50 border border-gray-200 rounded-lg hover:bg-gray-100 hover:text-gray-700 transition-colors',
-                title: item.attorginname || item.attname,
-              },
-              [
-                h('i', { class: 'fas fa-file mr-2 text-gray-500' }),
-                item.attorginname || `${item.attname}.${item.atttype}`,
-              ],
-            );
-      }
-    }),
-  );
+// 判断是否为图片文件
+const isImageFile = (atttype: string) => {
+  const imageExtensions = new Set(['bmp', 'gif', 'jpeg', 'jpg', 'png', 'webp']);
+  return imageExtensions.has(atttype.toLowerCase());
 };
 </script>
 
@@ -325,9 +482,56 @@ const renderAttachments = (attachments: any[], emptyText: string) => {
           <div>
             <h4 class="mb-2 text-sm font-medium text-gray-700">购机者照片</h4>
             <div class="min-h-[60px] rounded-lg bg-gray-50 p-3">
-              <component
-                :is="renderAttachments(buyerPhotoDataList, '暂无购机者照片')"
-              />
+              <div
+                v-if="buyerPhotoDataList.length === 0"
+                class="flex h-20 flex-col items-center justify-center space-y-2"
+              >
+                <span class="text-sm text-gray-500">暂无购机者照片</span>
+                <ElButton
+                  size="small"
+                  type="primary"
+                  @click="handleUploadClick('buyer')"
+                >
+                  上传附件
+                </ElButton>
+              </div>
+              <div v-else class="flex flex-wrap gap-3">
+                <div
+                  v-for="item in buyerPhotoDataList"
+                  :key="item.attid"
+                  class="group relative"
+                >
+                  <ElImage
+                    v-if="isImageFile(item.atttype)"
+                    :src="item.url"
+                    :preview-src-list="
+                      buyerPhotoDataList
+                        .filter((i) => isImageFile(i.atttype))
+                        .map((i) => i.url)
+                    "
+                    fit="cover"
+                    style="width: 100px; height: 100px"
+                    class="rounded border"
+                    preview-teleported
+                  />
+                  <a
+                    v-else
+                    :href="item.url"
+                    target="_blank"
+                    class="inline-flex items-center rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-700"
+                    :title="item.attorginname || item.attname"
+                  >
+                    <i class="fas fa-file mr-2 text-gray-500"></i>
+                    {{ item.attorginname || `${item.attname}.${item.atttype}` }}
+                  </a>
+                  <button
+                    class="absolute right-1 top-1 flex h-6 w-6 items-center justify-center rounded-full bg-red-500 text-white opacity-0 transition-opacity duration-200 group-hover:opacity-100"
+                    @click.stop="handleDeleteAttachment(item.attid, 'buyer')"
+                  >
+                    <X class="w-3 h-3" />
+                  </button>
+                </div>
+              </div>
             </div>
           </div>
 
@@ -335,9 +539,58 @@ const renderAttachments = (attachments: any[], emptyText: string) => {
           <div>
             <h4 class="mb-2 text-sm font-medium text-gray-700">证件附件</h4>
             <div class="min-h-[60px] rounded-lg bg-gray-50 p-3">
-              <component
-                :is="renderAttachments(certificateDataList, '暂无证件附件')"
-              />
+              <div
+                v-if="certificateDataList.length === 0"
+                class="flex h-20 flex-col items-center justify-center space-y-2"
+              >
+                <span class="text-sm text-gray-500">暂无证件附件</span>
+                <ElButton
+                  size="small"
+                  type="primary"
+                  @click="handleUploadClick('certificate')"
+                >
+                  上传附件
+                </ElButton>
+              </div>
+              <div v-else class="flex flex-wrap gap-3">
+                <div
+                  v-for="item in certificateDataList"
+                  :key="item.attid"
+                  class="group relative"
+                >
+                  <ElImage
+                    v-if="isImageFile(item.atttype)"
+                    :src="item.url"
+                    :preview-src-list="
+                      certificateDataList
+                        .filter((i) => isImageFile(i.atttype))
+                        .map((i) => i.url)
+                    "
+                    fit="cover"
+                    style="width: 100px; height: 100px"
+                    class="rounded border"
+                    preview-teleported
+                  />
+                  <a
+                    v-else
+                    :href="item.url"
+                    target="_blank"
+                    class="inline-flex items-center rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-700"
+                    :title="item.attorginname || item.attname"
+                  >
+                    <i class="fas fa-file mr-2 text-gray-500"></i>
+                    {{ item.attorginname || `${item.attname}.${item.atttype}` }}
+                  </a>
+                  <button
+                    class="absolute right-1 top-1 flex h-6 w-6 items-center justify-center rounded-full bg-red-500 text-white opacity-0 transition-opacity duration-200 group-hover:opacity-100"
+                    @click.stop="
+                      handleDeleteAttachment(item.attid, 'certificate')
+                    "
+                  >
+                    <X class="w-3 h-3" />
+                  </button>
+                </div>
+              </div>
             </div>
           </div>
 
@@ -345,15 +598,116 @@ const renderAttachments = (attachments: any[], emptyText: string) => {
           <div>
             <h4 class="mb-2 text-sm font-medium text-gray-700">发票附件</h4>
             <div class="min-h-[60px] rounded-lg bg-gray-50 p-3">
-              <component
-                :is="renderAttachments(invoiceDataList, '暂无发票附件')"
-              />
+              <div
+                v-if="invoiceDataList.length === 0"
+                class="flex h-20 flex-col items-center justify-center space-y-2"
+              >
+                <span class="text-sm text-gray-500">暂无发票附件</span>
+                <ElButton
+                  size="small"
+                  type="primary"
+                  @click="handleUploadClick('invoice')"
+                >
+                  上传附件
+                </ElButton>
+              </div>
+              <div v-else class="flex flex-wrap gap-3">
+                <div
+                  v-for="item in invoiceDataList"
+                  :key="item.attid"
+                  class="group relative"
+                >
+                  <a
+                    v-if="item.atttype === 'pdf'"
+                    :href="item.url"
+                    target="_blank"
+                    class="inline-flex items-center rounded-lg border border-blue-200 bg-blue-50 px-3 py-2 text-sm font-medium text-blue-600 transition-colors hover:bg-blue-100 hover:text-blue-700"
+                    :title="item.attorginname || item.attname"
+                  >
+                    <i class="fas fa-file-pdf mr-2 text-red-500"></i>
+                    {{ item.attorginname || `${item.attname}.${item.atttype}` }}
+                  </a>
+                  <ElImage
+                    v-else-if="isImageFile(item.atttype)"
+                    :src="item.url"
+                    :preview-src-list="
+                      invoiceDataList
+                        .filter((i) => isImageFile(i.atttype))
+                        .map((i) => i.url)
+                    "
+                    fit="cover"
+                    style="width: 100px; height: 100px"
+                    class="rounded border"
+                    preview-teleported
+                  />
+                  <a
+                    v-else
+                    :href="item.url"
+                    target="_blank"
+                    class="inline-flex items-center rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-700"
+                    :title="item.attorginname || item.attname"
+                  >
+                    <i class="fas fa-file mr-2 text-gray-500"></i>
+                    {{ item.attorginname || `${item.attname}.${item.atttype}` }}
+                  </a>
+                  <button
+                    class="absolute right-1 top-1 flex h-6 w-6 items-center justify-center rounded-full bg-red-500 text-white opacity-0 transition-opacity duration-200 group-hover:opacity-100"
+                    @click.stop="handleDeleteAttachment(item.attid, 'invoice')"
+                  >
+                    <X class="w-3 h-3" />
+                  </button>
+                </div>
+              </div>
             </div>
           </div>
         </div>
       </ElCard>
     </div>
   </Modal>
+
+  <!-- 上传对话框 -->
+  <ElDialog
+    v-model="uploadDialogVisible"
+    title="上传附件"
+    width="500px"
+    :close-on-click-modal="false"
+  >
+    <div class="py-4">
+      <ElUpload
+        class="w-full"
+        drag
+        :auto-upload="false"
+        :show-file-list="false"
+        :before-upload="beforeUpload"
+        :on-change="handleFileUpload"
+        :accept="currentUploadType === 'invoice' ? 'image/*,.pdf' : 'image/*'"
+      >
+        <div class="flex flex-col items-center justify-center py-6">
+          <i class="fas fa-cloud-upload-alt mb-4 text-4xl text-gray-400"></i>
+          <div class="mb-2 text-sm text-gray-600">
+            {{
+              currentUploadType === 'invoice'
+                ? '点击或拖拽文件到此区域上传'
+                : '点击或拖拽图片到此区域上传'
+            }}
+          </div>
+          <div class="text-xs text-gray-400">
+            {{
+              currentUploadType === 'invoice'
+                ? '支持JPG/PNG/GIF/BMP/PDF格式,文件大小不超过10MB'
+                : '支持JPG/PNG/GIF/BMP格式,文件大小不超过10MB'
+            }}
+          </div>
+        </div>
+      </ElUpload>
+    </div>
+
+    <template #footer>
+      <div class="flex justify-end">
+        <ElButton @click="uploadDialogVisible = false">取消</ElButton>
+      </div>
+    </template>
+  </ElDialog>
 </template>
 
 <style scoped>