فهرست منبع

feat: 优化优惠券相关API,新增优惠券领取和下单功能,更新优惠券审核功能,调整相关组件和表单逻辑

laiqi 11 ماه پیش
والد
کامیت
a74e716753

+ 31 - 5
apps/web-ele/src/api/coupon/coupon1.ts

@@ -12,16 +12,17 @@ interface Coupon1PartialEntity
 interface Coupon1QueryParams extends Coupon1PartialEntity, PageConfig {}
 
 /**
- * 生产企业信息_列表
+ * 优惠劵主券_列表
  */
 export async function getCoupon1ListApi(params: Coupon1QueryParams) {
   return requestClient.post<any>('/api/query/list?pagevalue=33', {
     ...params,
+    'couponproductids.like': params.couponproductids || '',
   });
 }
 
 /**
- * 优惠券信息_详情
+ * 优惠劵主券_详情
  */
 export async function getCoupon1DetailApi(data: { couponid: string }) {
   return requestClient.post<any>(
@@ -34,22 +35,47 @@ export async function getCoupon1DetailApi(data: { couponid: string }) {
 }
 
 /**
- * 优惠券信息_新增
+ * 优惠劵主券_新增
  */
 export async function addCoupon1Api(data: Coupon1Entity) {
   return requestClient.post<any>('/api/add?pagevalue=35', { ...data });
 }
 
 /**
- * 优惠券信息_编辑
+ * 优惠劵主券_编辑
  */
 export async function editCoupon1Api(data: Coupon1Entity) {
   return requestClient.post<any>('/api/up?pagevalue=36', { ...data });
 }
 
 /**
- * 优惠券信息_删除
+ * 优惠劵主券_删除
  */
 export async function deleteCoupon1Api(data: { 'couponid.value': string }) {
   return requestClient.post<any>('/api/del?pagevalue=37', { ...data });
 }
+
+/**
+ * 优惠劵领劵
+ */
+export async function receiveCouponApi(data: { couponid: string }) {
+  return requestClient.post<any>('/api/nlua/call?pagevalue=115', {
+    ...data,
+  });
+}
+
+/**
+ * 优惠劵下单
+ */
+export async function useCouponApi(data: {
+  filesid: string; // 发票id
+  orderscouponid: string; // 优惠码
+  ordersphone: string; // 手机号
+  ordersproductid: string; // 产品id
+  ordersproductsn: string; // SN号
+  ordersuserid1: string; // 销售网点id
+}) {
+  return requestClient.post<any>('/api/nlua/call?pagevalue=116', {
+    ...data,
+  });
+}

+ 24 - 0
apps/web-ele/src/api/coupon/coupon2.ts

@@ -50,6 +50,30 @@ export async function editCoupon2Api(data: Coupon2Entity) {
 }
 
 /**
+ * 我的优惠券_审核_列表
+ */
+export async function getCoupon2AuditListApi(data: Coupon2QueryParams) {
+  return requestClient.post<any>('/api/query/list?pagevalue=117', {
+    pageindex: data.pageindex,
+    rows: data.rows,
+    ...parseQueryValues(data),
+  });
+}
+
+/**
+ * 我的优惠券_审核_详情
+ */
+export async function getCoupon2AuditDetailApi(data: any) {
+  return requestClient.post<any>(
+    '/api/query/view?pagevalue=118',
+    {
+      ...parseQueryValues(data),
+    },
+    { formatData: true },
+  );
+}
+
+/**
  * 我的优惠券信息_审核
  */
 export async function auditCoupon2Api(data: Coupon2Entity) {

+ 3 - 1
apps/web-ele/src/api/file/index.ts

@@ -32,7 +32,7 @@ export async function getAttachmentListApi(params: AttachmentQueryParams) {
 export async function addImageApi(data: FormData) {
   const token = useAccessStore().accessToken;
   return baseRequestClient.post<any>(
-    '/api/attachment/addtopath?pagevalue=93',
+    '/api/attachment/addimg?pagevalue=93',
     data,
     {
       headers: {
@@ -47,12 +47,14 @@ export async function addImageApi(data: FormData) {
  * 添加附件记录并返回路径
  */
 export async function addAttachmentApi(data: FormData) {
+  const token = useAccessStore().accessToken;
   return baseRequestClient.post<any>(
     '/api/attachment/addtopath?pagevalue=94',
     data,
     {
       headers: {
         'Content-Type': 'multipart/form-data',
+        token,
       },
     },
   );

+ 1 - 0
apps/web-ele/src/api/product/index.ts

@@ -17,6 +17,7 @@ interface ProductQueryParams extends PageConfig, ProductPartialEntity {}
 export async function getProductListApi(params: ProductQueryParams) {
   return requestClient.post<any>('/api/query/list?pagevalue=23', {
     ...params,
+    'productsmerchantid.like': params.productsmerchantid || '',
   });
 }
 

+ 125 - 0
apps/web-ele/src/components/upload-file/README.md

@@ -0,0 +1,125 @@
+# UploadFile 组件使用说明
+
+## 功能特性
+- 支持多种文件上传(图片、PDF等)
+- 自动生成文件预览URL
+- 支持文件删除和预览
+- 兼容多种v-model绑定方式
+- 严格的文件数量限制控制
+- 自动内存管理(blob URL清理)
+
+## Props 属性
+
+| 属性名 | 类型 | 默认值 | 必需 | 说明 |
+|--------|------|--------|------|------|
+| modelValue | Array | [] | 否 | v-model默认绑定的文件列表 |
+| fileIds | Array | [] | 否 | v-model:fileIds绑定的文件ID数组 |
+| limit | Number | 1 | 否 | 上传文件数量限制,limit=1时会严格替换现有文件 |
+| attmodel | String | - | 是 | 关联模块名称,用于文件分类存储 |
+| attpath | String | '' | 否 | 自定义路径,默认为`/${attmodel}/` |
+| attId | String | '' | 否 | 单个文件ID,用于初始化单文件场景 |
+| message | String | '' | 否 | 提示消息,与tips作用相同,优先显示tips |
+| tips | String | '' | 否 | 提示信息,显示在上传组件下方 |
+| listType | String | 'text' | 否 | 列表类型,支持 'text'、'picture'、'picture-card' |
+
+## Events 事件
+
+| 事件名 | 参数 | 说明 |
+|--------|------|------|
+| update:modelValue | Array | 更新文件列表 |
+| update:fileIds | Array | 更新文件ID数组 |
+| update:fileID | String | 更新文件ID字符串(逗号分隔,兼容旧版本) |
+
+## 使用方式
+
+### 方式一:单文件上传(推荐)
+```vue
+<UploadFile
+  v-model:fileIds="formData.avatarIds"
+  attmodel="user_avatar"
+  :limit="1"
+  listType="picture-card"
+  tips="请上传头像,支持jpg、png格式"
+/>
+```
+
+### 方式二:多文件上传
+```vue
+<UploadFile
+  v-model="formData.attachmentFiles"
+  v-model:fileIds="formData.attachmentIds"
+  attmodel="project_attachments"
+  :limit="5"
+  tips="最多上传5个文件,支持图片和PDF格式"
+/>
+```
+
+### 方式三:自定义路径
+```vue
+<UploadFile
+  v-model:fileIds="formData.documentIds"
+  attmodel="contract"
+  attpath="/contracts/2024/"
+  :limit="3"
+  tips="上传合同文档"
+/>
+```
+
+### 方式四:初始化单个文件
+```vue
+<UploadFile
+  v-model:fileIds="formData.certIds"
+  attmodel="certification"
+  :attId="existingFileId"
+  :limit="1"
+  tips="营业执照"
+/>
+```
+
+## 重要特性说明
+
+### 严格的数量限制
+- 当 `limit=1` 时,上传新文件会自动替换现有文件
+- 超出数量限制时会显示警告并阻止上传
+- 自动清理被替换文件的内存占用
+
+### 双向绑定支持
+- `v-model`: 绑定完整的文件对象数组
+- `v-model:fileIds`: 绑定文件ID数组
+- `@update:fileID`: 兼容旧版本的字符串格式ID
+
+### 自动内存管理
+- 组件会自动清理不再使用的blob URL
+- 文件删除时自动释放相关内存
+- 组件销毁时清理所有资源
+
+## 暴露的方法
+
+### clearFiles()
+清空所有已上传的文件
+
+```vue
+<script setup>
+const uploadRef = ref(null);
+
+const handleClear = () => {
+  uploadRef.value?.clearFiles();
+};
+</script>
+
+<template>
+  <UploadFile 
+    ref="uploadRef" 
+    v-model:fileIds="fileIds" 
+    attmodel="demo" 
+  />
+  <button @click="handleClear">清空文件</button>
+</template>
+```
+
+## 注意事项
+
+1. **attmodel是必需的**:用于服务端文件分类和路径管理
+2. **limit=1时的行为**:会严格替换现有文件,而不是添加新文件
+3. **内存管理**:组件会自动管理blob URL,无需手动清理
+4. **事件兼容性**:同时支持新的fileIds数组格式和旧的fileID字符串格式

+ 512 - 0
apps/web-ele/src/components/upload-file/index.vue

@@ -0,0 +1,512 @@
+<script setup>
+/**
+ * 上传图片,并返回id,多个以,号分割
+ */
+import { computed, defineExpose, nextTick, onMounted, ref, watch } from 'vue';
+
+import { MdiPlus } from '@vben/icons';
+
+import { ElMessage, genFileId } from 'element-plus';
+
+import { addAttachmentApi, addImageApi } from '#/api/file';
+
+const props = defineProps({
+  // v-model 默认绑定的值 (fileList)
+  modelValue: {
+    type: Array,
+    default: () => [],
+  },
+  // v-model:fileIds 绑定的文件ID数组
+  fileIds: {
+    type: Array,
+    default: () => [],
+  },
+  limit: {
+    type: Number,
+    default: 1,
+  },
+  attmodel: {
+    type: String,
+    required: true,
+  },
+  // 支持自定义路径
+  attpath: {
+    type: String,
+    default: '',
+  },
+  attId: {
+    type: String,
+    default: '',
+  },
+  message: {
+    type: String,
+    default: '',
+  },
+  tips: {
+    type: String,
+    default: '',
+  },
+  listType: {
+    type: String,
+    default: 'text',
+  },
+  // 新增accept属性
+  accept: {
+    type: String,
+    default: '',
+  },
+});
+
+const emits = defineEmits([
+  'update:modelValue',
+  'update:fileIds',
+  'update:fileID',
+]);
+
+const upload = ref(null);
+const fileList = ref([]);
+const fileIdsInternal = ref([]); // 上传后返回的id集合
+
+// 计算实际使用的路径
+const actualAttpath = computed(() => {
+  return props.attpath || `/${props.attmodel}/`;
+});
+
+// 内部状态标记,防止循环更新
+const isInternalUpdate = ref(false);
+
+// 安全的数组比较函数
+const arraysEqual = (a, b) => {
+  if (!a && !b) return true;
+  if (!a || !b) return false;
+  if (a.length !== b.length) return false;
+  return JSON.stringify(a) === JSON.stringify(b);
+};
+
+// 监听外部modelValue变化
+watch(
+  () => props.modelValue,
+  async (newVal) => {
+    if (isInternalUpdate.value) return;
+    if (!arraysEqual(newVal, fileList.value)) {
+      isInternalUpdate.value = true;
+      fileList.value = newVal ? [...newVal] : [];
+      await nextTick();
+      isInternalUpdate.value = false;
+    }
+  },
+  { deep: true, immediate: true },
+);
+
+// 监听外部fileIds变化
+watch(
+  () => props.fileIds,
+  async (newVal) => {
+    if (isInternalUpdate.value) return;
+    if (!arraysEqual(newVal, fileIdsInternal.value)) {
+      isInternalUpdate.value = true;
+      fileIdsInternal.value = newVal ? [...newVal] : [];
+      await nextTick();
+      isInternalUpdate.value = false;
+    }
+  },
+  { deep: true, immediate: true },
+);
+
+// 监听内部fileList变化,同步到外部
+watch(
+  fileList,
+  async (newVal) => {
+    if (isInternalUpdate.value) return;
+    if (!arraysEqual(newVal, props.modelValue)) {
+      isInternalUpdate.value = true;
+      emits('update:modelValue', [...newVal]);
+      await nextTick();
+      isInternalUpdate.value = false;
+    }
+  },
+  { deep: true },
+);
+
+// 监听内部fileIds变化,同步到外部
+watch(
+  fileIdsInternal,
+  async (newVal) => {
+    if (isInternalUpdate.value) return;
+    if (!arraysEqual(newVal, props.fileIds)) {
+      isInternalUpdate.value = true;
+      emits('update:fileIds', [...newVal]);
+      // 兼容原有的字符串格式
+      const idStr = newVal
+        .map((item) => (typeof item === 'object' ? item.id : item))
+        .join(',');
+      emits('update:fileID', idStr);
+      await nextTick();
+      isInternalUpdate.value = false;
+    }
+  },
+  { deep: true },
+);
+
+const handleRemove = async (uploadFile, _uploadFiles) => {
+  isInternalUpdate.value = true;
+  fileIdsInternal.value = fileIdsInternal.value.filter(
+    (item) => item.uid !== uploadFile.uid,
+  );
+  // 更新 fileList,移除对应文件
+  const removedFileIndex = fileList.value.findIndex(
+    (f) => f.uid === uploadFile.uid,
+  );
+  if (removedFileIndex !== -1) {
+    const removedF = fileList.value.splice(removedFileIndex, 1)[0];
+    // 如果是本地创建的 blob URL,则释放它
+    if (removedF && removedF.url && removedF.url.startsWith('blob:')) {
+      URL.revokeObjectURL(removedF.url);
+    }
+  }
+
+  const idStr = fileIdsInternal.value.map((item) => item.id).join(',');
+  emits('update:fileID', idStr);
+  await nextTick();
+  isInternalUpdate.value = false;
+};
+
+// 超出文件数量限制处理
+const handleExceed = (files) => {
+  // 当limit=1时,替换现有文件而不是添加新文件
+  if (props.limit === 1) {
+    ElMessage.warning('只能上传一个文件,将替换当前文件');
+    // 清理现有文件和blob URL
+    fileList.value.forEach((file) => {
+      if (file.url && file.url.startsWith('blob:')) {
+        URL.revokeObjectURL(file.url);
+      }
+    });
+    // 清空所有相关状态
+    fileList.value = [];
+    fileIdsInternal.value = [];
+    emits('update:fileIds', []);
+    emits('update:fileID', '');
+
+    // 处理新文件
+    const file = files[0];
+    file.uid = genFileId();
+    if (upload.value) {
+      upload.value.clearFiles();
+      upload.value.handleStart(file);
+    }
+    handleUploadFile({ file });
+  } else {
+    // 多文件上传时的原有逻辑
+    ElMessage.warning(`最多只能上传${props.limit}个文件`);
+  }
+};
+
+// 添加文件选择前的检查
+const beforeUpload = (_file) => {
+  // 当limit=1且已有文件时,先清理现有文件
+  if (props.limit === 1 && fileList.value.length > 0) {
+    // 清理现有文件的blob URL
+    fileList.value.forEach((existingFile) => {
+      if (existingFile.url && existingFile.url.startsWith('blob:')) {
+        URL.revokeObjectURL(existingFile.url);
+      }
+    });
+    // 清空状态
+    fileList.value = [];
+    fileIdsInternal.value = [];
+    emits('update:fileIds', []);
+    emits('update:fileID', '');
+  }
+  return true;
+};
+
+// 预览图片(功能已移除)
+const handlePictureCardPreview = (_uploadFile) => {
+  // 预览功能已移除,因为使用本地blob URL预览
+};
+
+// 判断文件是否为图片类型
+const isImageFile = (file) => {
+  // 通过文件类型判断
+  if (file.type && file.type.startsWith('image/')) {
+    return true;
+  }
+
+  // 通过文件扩展名判断(作为备用方案)
+  const imageExtensions = [
+    '.jpg',
+    '.jpeg',
+    '.png',
+    '.gif',
+    '.bmp',
+    '.webp',
+    '.svg',
+    '.pdf',
+  ];
+  const fileName = file.name.toLowerCase();
+  return imageExtensions.some((ext) => fileName.endsWith(ext));
+};
+
+// 自定义上传文件
+const handleUploadFile = async (option) => {
+  const { file } = option;
+  const localPreviewUrl = URL.createObjectURL(file); // 生成本地预览URL
+
+  // 更新fileList以进行即时预览
+  const existingFileIndex = fileList.value.findIndex((f) => f.uid === file.uid);
+  if (existingFileIndex === -1) {
+    // 查找el-upload可能已添加的文件条目
+    const newFileInList = fileList.value.find((f) => f.uid === file.uid);
+    if (newFileInList) {
+      newFileInList.url = localPreviewUrl;
+      newFileInList.status = 'uploading';
+    } else {
+      // 防御性添加(通常不需要,因为el-upload会自动管理)
+      const newFileItem = {
+        name: file.name,
+        url: localPreviewUrl,
+        uid: file.uid,
+        status: 'uploading',
+        raw: file,
+      };
+
+      // 当limit=1时,严格替换现有文件
+      if (props.limit === 1) {
+        // 清理现有文件的blob URL
+        fileList.value.forEach((existingFile) => {
+          if (existingFile.url && existingFile.url.startsWith('blob:')) {
+            URL.revokeObjectURL(existingFile.url);
+          }
+        });
+        // 清空并替换为新文件
+        fileList.value = [newFileItem];
+        // 同时清空fileIds,上传成功后会重新添加
+        fileIdsInternal.value = [];
+      } else if (fileList.value.length < props.limit) {
+        fileList.value.push(newFileItem);
+      }
+    }
+  } else {
+    // 更新现有文件的预览URL
+    fileList.value[existingFileIndex].url = localPreviewUrl;
+    fileList.value[existingFileIndex].status = 'uploading';
+    fileList.value[existingFileIndex].name = file.name;
+  }
+
+  const formData = new FormData();
+  formData.append('attmodel', props.attmodel);
+  formData.append('attpath', actualAttpath.value);
+  formData.append('file', file);
+
+  try {
+    // 根据文件类型选择不同的上传接口
+    const isImage = isImageFile(file);
+    const res = isImage
+      ? await addImageApi(formData)
+      : await addAttachmentApi(formData);
+
+    if (res.data && res.data.Status === 0) {
+      const fileId = res.data.Data;
+      const serverUrl = res.data.OtherData;
+
+      // 更新fileIdsInternal并直接发送事件
+      const newFileEntry = {
+        uid: file.uid,
+        id: fileId,
+        name: file.name,
+        url: serverUrl || '',
+      };
+
+      // 当limit=1时,替换所有现有文件ID
+      if (props.limit === 1) {
+        fileIdsInternal.value = [newFileEntry];
+      } else {
+        fileIdsInternal.value.push(newFileEntry);
+      }
+
+      // 直接发送事件,确保父组件及时接收
+      emits('update:fileIds', [...fileIdsInternal.value]);
+      const idStr = fileIdsInternal.value.map((item) => item.id).join(',');
+      emits('update:fileID', idStr);
+
+      // 更新fileList状态和URL
+      await nextTick();
+      isInternalUpdate.value = true;
+      const uploadedFileEntry = fileList.value.find((f) => f.uid === file.uid);
+      if (uploadedFileEntry) {
+        // 释放blob URL并使用服务器URL
+        if (
+          uploadedFileEntry.url &&
+          uploadedFileEntry.url.startsWith('blob:')
+        ) {
+          URL.revokeObjectURL(uploadedFileEntry.url);
+        }
+        uploadedFileEntry.attId = fileId;
+        uploadedFileEntry.status = 'success';
+        uploadedFileEntry.url = serverUrl || '';
+      }
+      await nextTick();
+      isInternalUpdate.value = false;
+    } else {
+      ElMessage.error(res.data?.Message || '上传失败');
+      updateFileStatusAndCleanup(file.uid, 'fail', localPreviewUrl);
+    }
+  } catch (error) {
+    console.error('上传错误:', error);
+    ElMessage.error('上传失败,请重试');
+    updateFileStatusAndCleanup(file.uid, 'fail', localPreviewUrl);
+  }
+};
+
+// 更新文件状态并清理blob URL的辅助函数
+const updateFileStatusAndCleanup = (uid, status, _blobUrl) => {
+  const fileEntry = fileList.value.find((f) => f.uid === uid);
+  if (fileEntry) {
+    fileEntry.status = status;
+    if (fileEntry.url && fileEntry.url.startsWith('blob:')) {
+      URL.revokeObjectURL(fileEntry.url);
+    }
+  }
+};
+
+// 根据 fileIds 初始化 fileList
+const initializeFileList = async () => {
+  if (props.fileIds && props.fileIds.length > 0) {
+    isInternalUpdate.value = true;
+    const newFileList = [];
+
+    for (const fileIdObj of props.fileIds) {
+      const attId = typeof fileIdObj === 'object' ? fileIdObj.id : fileIdObj;
+      const uid = typeof fileIdObj === 'object' ? fileIdObj.uid : genFileId();
+      if (!attId) continue;
+
+      // 优先从外部传入的 modelValue 中查找对应的文件信息
+      const modelFile = props.modelValue.find(
+        (f) =>
+          f.attId === attId ||
+          (f.uid === uid && f.name) ||
+          (typeof f.attId === 'string' && f.attId === attId),
+      );
+
+      let fileName = `File_${attId}`;
+      let fileUrl = '';
+
+      if (modelFile) {
+        fileName = modelFile.name || fileName;
+        fileUrl = modelFile.url || '';
+      }
+
+      newFileList.push({
+        name: fileName,
+        url: fileUrl,
+        attId,
+        uid,
+      });
+    }
+
+    fileList.value = newFileList;
+    await nextTick();
+    isInternalUpdate.value = false;
+  } else if (
+    (!props.fileIds || props.fileIds.length === 0) &&
+    props.modelValue.length === 0
+  ) {
+    // 清空状态
+    isInternalUpdate.value = true;
+    fileList.value = [];
+    fileIdsInternal.value = [];
+    emits('update:fileID', '');
+    await nextTick();
+    isInternalUpdate.value = false;
+  }
+};
+
+onMounted(async () => {
+  await initializeFileList();
+});
+
+// 当 attId prop 变化时处理单个文件ID
+watch(
+  () => props.attId,
+  async (newAttId) => {
+    if (
+      newAttId &&
+      typeof newAttId === 'string' &&
+      props.fileIds.length === 0
+    ) {
+      const pseudoFileId = {
+        id: newAttId,
+        uid: genFileId(),
+        name: `File_${newAttId}`,
+        url: '',
+      };
+      isInternalUpdate.value = true;
+      fileIdsInternal.value = [pseudoFileId];
+      emits('update:fileIds', [pseudoFileId]);
+      await nextTick();
+      isInternalUpdate.value = false;
+      await initializeFileList();
+    }
+  },
+  { immediate: true },
+);
+
+// 监听 fileIds 变化,重新初始化文件列表
+watch(() => props.fileIds, initializeFileList, { deep: true });
+
+// 暴露 clearFiles 方法
+defineExpose({
+  clearFiles: () => {
+    if (upload.value) {
+      upload.value.clearFiles();
+    }
+  },
+});
+</script>
+
+<template>
+  <div class="upload-file-container">
+    <ElUpload
+      ref="upload"
+      v-model:file-list="fileList"
+      action="#"
+      :http-request="handleUploadFile"
+      :limit="limit"
+      :multiple="limit > 1"
+      :on-exceed="handleExceed"
+      :on-preview="handlePictureCardPreview"
+      :on-remove="handleRemove"
+      :list-type="listType"
+      :before-upload="beforeUpload"
+      :accept="accept"
+    >
+      <slot name="trigger">
+        <MdiPlus />
+      </slot>
+      <template #tip>
+        <div v-if="tips || message" class="el-upload__tip">
+          {{ tips || message }}
+        </div>
+      </template>
+    </ElUpload>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.upload-file-container {
+  :deep(.el-upload--picture-card) {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 100px; /* 设置默认宽度 */
+    height: 100px; /* 设置默认高度 */
+  }
+
+  :deep(.el-upload-list--picture-card .el-upload-list__item) {
+    width: 100px; /* 设置默认宽度 */
+    height: 100px; /* 设置默认高度 */
+    margin: 0 8px 8px 0; /* 统一间距 */
+  }
+}
+</style>

+ 19 - 0
apps/web-ele/src/components/upload-file/index.vue.d.ts

@@ -0,0 +1,19 @@
+declare module '#/components/upload-file/index.vue' {
+  import type { DefineComponent } from 'vue';
+
+  export interface UploadFileProps {
+    modelValue?: any[];
+    fileIds?: { id: string; name?: string; url: string }[];
+    limit?: number;
+    attmodel: string;
+    attpath?: string;
+    attId?: string;
+    message?: string;
+    tips?: string;
+    listType?: string;
+    accept?: string;
+  }
+
+  const UploadFile: DefineComponent<UploadFileProps, any, any>;
+  export default UploadFile;
+}

+ 5 - 2
apps/web-ele/src/views/examine-manage/examine-coupon/audit-form.vue

@@ -10,7 +10,10 @@ import { useVbenModal } from '@vben/common-ui';
 import { ElMessage } from 'element-plus';
 
 import { useVbenForm, z } from '#/adapter/form';
-import { auditCoupon2Api, getCoupon2DetailApi } from '#/api/coupon/coupon2';
+import {
+  auditCoupon2Api,
+  getCoupon2AuditDetailApi,
+} from '#/api/coupon/coupon2';
 
 interface Emits {
   (event: 'finish'): void;
@@ -138,7 +141,7 @@ const [Modal, modalApi] = useVbenModal({
 
       if (auditData.value?.coupon2sid) {
         try {
-          const detailData = await getCoupon2DetailApi({
+          const detailData = await getCoupon2AuditDetailApi({
             coupon2sid: auditData.value.coupon2sid,
           });
           const initialValues = { ...auditData.value, ...detailData };

+ 2 - 2
apps/web-ele/src/views/examine-manage/examine-coupon/detail.vue

@@ -5,7 +5,7 @@ import { useVbenModal } from '@vben/common-ui';
 
 import { ElTag } from 'element-plus';
 
-import { getCoupon2DetailApi } from '#/api/coupon/coupon2';
+import { getCoupon2AuditDetailApi } from '#/api/coupon/coupon2';
 
 const data = ref<any>(null);
 const loading = ref(false);
@@ -154,7 +154,7 @@ const [Modal, modalApi] = useVbenModal({
       if (modalData?.row?.coupon2sid) {
         try {
           loading.value = true;
-          const detailData = await getCoupon2DetailApi({
+          const detailData = await getCoupon2AuditDetailApi({
             coupon2sid: modalData.row.coupon2sid,
           });
           data.value = detailData;

+ 72 - 32
apps/web-ele/src/views/examine-manage/examine-coupon/index.vue

@@ -5,25 +5,30 @@ import type { VxeGridListeners, VxeGridProps } from '#/adapter/vxe-table';
 
 import { h, ref } from 'vue';
 
-import { Page, useVbenModal } from '@vben/common-ui';
-import { MdiCheckboxMultipleMarked, MdiDetail, MdiEdit } from '@vben/icons';
+import { Page } from '@vben/common-ui';
+import { MdiCheckboxMultipleMarked, MdiDetail, MdiPlus } from '@vben/icons';
 
-import { ElTag } from 'element-plus';
+import { ElMessage, ElMessageBox, ElTag } from 'element-plus';
 
 import { useVbenVxeGrid } from '#/adapter/vxe-table';
-import { getCoupon2ListApi } from '#/api/coupon/coupon2';
+import { getCoupon2AuditListApi } from '#/api/coupon/coupon2';
 import { $t } from '#/locales';
 
 import AuditForm from './audit-form.vue';
 import DetailForm from './detail.vue';
-import Coupon1Form from './form.vue';
+import OrderCouponForm from './order-coupon-form.vue';
+import ReceiveCouponForm from './receive-coupon-form.vue';
+
+const receiveCouponFormRef = ref<InstanceType<typeof ReceiveCouponForm> | null>(
+  null,
+);
+const orderCouponFormRef = ref<InstanceType<typeof OrderCouponForm> | null>(
+  null,
+);
 
 const formOptions: VbenFormProps = {
-  // 默认展开
   collapsed: true,
-  // 控制表单是否显示折叠按钮
   showCollapseButton: true,
-  // 按下回车时是否提交表单
   submitOnEnter: true,
   schema: [
     {
@@ -58,7 +63,6 @@ const gridOptions: VxeGridProps<any> = {
   toolbarConfig: {
     custom: true,
     export: true,
-    // import: true,
     refresh: true,
     zoom: true,
   },
@@ -77,7 +81,7 @@ const gridOptions: VxeGridProps<any> = {
     },
     ajax: {
       query: async ({ page }, formValues) => {
-        return await getCoupon2ListApi({
+        return await getCoupon2AuditListApi({
           pageindex: page.currentPage,
           rows: page.pageSize,
           ...formValues,
@@ -92,7 +96,6 @@ const gridOptions: VxeGridProps<any> = {
     { title: '优惠券名称', field: 'coupon2mc', sortable: true },
     { title: '可用产品', field: 'productsname', sortable: true },
     { title: '购机者', field: 'usersname', sortable: true },
-    // { title: '申请人ID', field: 'coupon2userid', sortable: true },
     {
       title: '优惠券状态',
       field: 'coupon2sype',
@@ -132,7 +135,6 @@ const gridOptions: VxeGridProps<any> = {
         },
       },
     },
-    // { title: '关联主券ID', field: 'coupon2coupon1id', sortable: true },
     { title: '申请时间', field: 'coupon2adddatetime', sortable: true },
     { title: '审核时间', field: 'coupon2reviewdatetime', sortable: true },
     {
@@ -140,7 +142,7 @@ const gridOptions: VxeGridProps<any> = {
       field: 'action',
       fixed: 'right',
       slots: { default: 'action' },
-      width: 160,
+      width: 200,
     },
   ],
 };
@@ -157,24 +159,9 @@ const [Grid, gridApi] = useVbenVxeGrid({
   formOptions,
 });
 
-const [Modal, modalApi] = useVbenModal({
-  fullscreenButton: false,
-  closeOnClickModal: false,
-  closeOnPressEscape: false,
-  connectedComponent: Coupon1Form,
-});
-
-// Add ref for audit-form.vue
 const auditFormRef = ref<any>(null);
-// Add ref for detail-form.vue
 const detailFormRef = ref<any>(null);
 
-/* 编辑 */
-function handleEdit(row: any) {
-  modalApi.setState({ showCancelButton: true });
-  modalApi.setData({ formType: 'edit', row }).open();
-}
-
 /* 详情 */
 function handleDetail(row: any) {
   detailFormRef.value?.openDetailModal(row);
@@ -185,13 +172,48 @@ function handleAudit(row: any) {
   auditFormRef.value?.openAuditModal(row);
 }
 
+/* 下单 */
+function handleOrder(row: any) {
+  orderCouponFormRef.value?.openOrderCouponModal({
+    coupon2sid: row.coupon2sid,
+  });
+}
+
 function handleFinish() {
   gridApi.reload();
 }
 
 // 领取优惠劵
 function handleCreate() {
-  console.log('领取优惠劵');
+  receiveCouponFormRef.value?.openReceiveCouponModal({});
+}
+
+// 处理领取优惠券成功后的逻辑
+function handleReceiveSuccess({
+  coupon2sid: _coupon2sid,
+}: {
+  coupon2sid: string;
+}) {
+  gridApi.reload();
+  ElMessageBox.confirm('优惠券领取成功!', '提示', {
+    // confirmButtonText: '优惠劵下单',
+    confirmButtonText: '确定',
+    cancelButtonText: '关闭',
+    type: 'success',
+  })
+    .then(() => {
+      // // 用户选择使用优惠券下单,打开下单弹窗
+      // orderCouponFormRef.value?.openOrderCouponModal({ coupon2sid });
+    })
+    .catch(() => {
+      // 用户关闭了确认对话框
+    });
+}
+
+// 处理下单成功后的逻辑
+function handleOrderSuccess({ orderId }: { orderId: string }) {
+  gridApi.reload();
+  ElMessage.success(`下单成功!订单号:${orderId}`);
 }
 </script>
 
@@ -215,7 +237,7 @@ function handleCreate() {
             class="!p-2"
           />
         </el-tooltip>
-        <el-tooltip
+        <!-- <el-tooltip
           class="box-item"
           effect="dark"
           content="编辑"
@@ -227,7 +249,7 @@ function handleCreate() {
             :icon="MdiEdit"
             class="!p-2"
           />
-        </el-tooltip>
+        </el-tooltip> -->
         <el-tooltip
           class="box-item"
           effect="dark"
@@ -242,10 +264,28 @@ function handleCreate() {
             :disabled="row.coupon2sype !== 0"
           />
         </el-tooltip>
+        <el-tooltip
+          class="box-item"
+          effect="dark"
+          content="下单"
+          placement="top"
+        >
+          <el-button
+            round
+            @click="() => handleOrder(row)"
+            class="!p-2"
+            :icon="MdiPlus"
+            :disabled="row.coupon2sype !== 1 || row.coupon2isused === 1"
+          />
+        </el-tooltip>
       </template>
     </Grid>
-    <Modal @finish="handleFinish" />
     <AuditForm ref="auditFormRef" @finish="handleFinish" />
     <DetailForm ref="detailFormRef" />
+    <ReceiveCouponForm
+      ref="receiveCouponFormRef"
+      @finish="handleReceiveSuccess"
+    />
+    <OrderCouponForm ref="orderCouponFormRef" @finish="handleOrderSuccess" />
   </Page>
 </template>

+ 296 - 0
apps/web-ele/src/views/examine-manage/examine-coupon/order-coupon-form.vue

@@ -0,0 +1,296 @@
+<script lang="ts" setup>
+import type { FormInstance, FormRules } from 'element-plus';
+
+import { defineEmits, defineExpose, reactive, ref } from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { ElMessage } from 'element-plus';
+
+import { useCouponApi } from '#/api/coupon/coupon1';
+import { getCoupon2AuditDetailApi } from '#/api/coupon/coupon2';
+import { getCustomerDetailApi } from '#/api/user';
+import UploadFile from '#/components/upload-file/index.vue';
+
+const emit = defineEmits(['finish', 'cancel']);
+
+const localCoupon2sid = ref('');
+const couponDetailData = ref<any>(null);
+const merchantDetailData = ref<any>(null);
+const loading = ref(false);
+
+const formRef = ref<FormInstance>();
+const formData = reactive({
+  serialNumber: '', // SN号
+  invoiceEpFiles: [] as any[], // 发票文件列表
+  invoiceIds: [] as { id: string; name?: string; url: string }[], // 发票文件ID列表
+});
+
+const submitLoading = ref(false);
+
+const formRules: FormRules = {
+  serialNumber: [
+    { required: true, message: '请输入SN号', trigger: 'blur' },
+    { min: 3, message: 'SN号至少3位', trigger: 'blur' },
+  ],
+  invoiceIds: [
+    {
+      required: true,
+      validator: (_rule, value, callback) => {
+        if (!value || value.length === 0) {
+          callback(new Error('请上传发票信息'));
+        } else {
+          callback();
+        }
+      },
+      trigger: 'blur',
+    },
+  ],
+};
+
+// 获取优惠券详情信息
+async function fetchCouponDetail(coupon2sid: string) {
+  if (!coupon2sid) {
+    couponDetailData.value = null;
+    merchantDetailData.value = null;
+    return;
+  }
+
+  try {
+    loading.value = true;
+    const detailData = await getCoupon2AuditDetailApi({ coupon2sid });
+    couponDetailData.value = detailData;
+
+    // 获取渠道商详情
+    if (detailData && detailData.coupon2merchantid) {
+      try {
+        const merchantData = await getCustomerDetailApi({
+          usersid: detailData.coupon2merchantid,
+        });
+        merchantDetailData.value = merchantData;
+      } catch (error) {
+        console.error('获取渠道商详情失败:', error);
+        merchantDetailData.value = null;
+      }
+    }
+  } catch (error) {
+    console.error('获取优惠券详情失败:', error);
+    ElMessage.error('获取优惠券详情失败');
+    couponDetailData.value = null;
+    merchantDetailData.value = null;
+  } finally {
+    loading.value = false;
+  }
+}
+
+async function handleSubmit(): Promise<boolean> {
+  if (!formRef.value) return false;
+
+  try {
+    await formRef.value.validate();
+
+    if (!couponDetailData.value) {
+      ElMessage.error('优惠券信息未加载完成,请稍后重试');
+      return false;
+    }
+
+    submitLoading.value = true;
+
+    // 获取发票文件ID字符串
+    const invoiceFileIdsString = formData.invoiceIds
+      .map((f) => f.id)
+      .filter(Boolean)
+      .join(',');
+
+    // 构建下单接口参数
+    const orderParams = {
+      filesid: invoiceFileIdsString, // 发票id
+      orderscouponid: couponDetailData.value.coupon2code, // 优惠码
+      ordersphone: couponDetailData.value.usersphone || '', // 手机号
+      ordersproductid: couponDetailData.value.coupon2productids || '', // 产品id
+      ordersproductsn: formData.serialNumber, // SN号
+      ordersuserid1: couponDetailData.value.coupon2merchantid || '', // 销售网点id
+    };
+
+    // 调用真实的下单接口
+    await useCouponApi(orderParams);
+
+    ElMessage.success('下单提交成功');
+
+    return true;
+  } catch (error) {
+    ElMessage.error(error || '下单失败,请重试');
+    return false;
+  } finally {
+    submitLoading.value = false;
+  }
+}
+
+const [Modal, modalApi] = useVbenModal({
+  class: 'w-[700px]',
+  title: '优惠券下单',
+  showCancelButton: true,
+  fullscreenButton: false,
+  closeOnClickModal: false,
+  closeOnPressEscape: false,
+  async onConfirm() {
+    const success = await handleSubmit();
+    if (success) {
+      modalApi.close();
+      emit('finish', { orderId: `ORDER_${Date.now()}` });
+    }
+  },
+  onCancel() {
+    modalApi.close();
+    emit('cancel');
+  },
+  async onOpenChange(isOpen: boolean) {
+    if (isOpen) {
+      const modalData = modalApi.getData() as {
+        coupon2sid?: string;
+      };
+      localCoupon2sid.value = modalData?.coupon2sid || '';
+
+      // 重置表单数据
+      formData.serialNumber = '';
+      formData.invoiceEpFiles = [];
+      formData.invoiceIds = [];
+      couponDetailData.value = null;
+      merchantDetailData.value = null;
+
+      formRef.value?.resetFields();
+      submitLoading.value = false;
+
+      // 获取优惠券详情
+      if (localCoupon2sid.value) {
+        await fetchCouponDetail(localCoupon2sid.value);
+      }
+    }
+  },
+});
+
+function openOrderCouponModal(data?: { coupon2sid?: string }) {
+  modalApi.setData(data || {});
+  modalApi.open();
+}
+
+defineExpose({
+  openOrderCouponModal,
+});
+</script>
+
+<template>
+  <Modal>
+    <div v-loading="loading" class="p-4">
+      <!-- 优惠券详情信息展示 -->
+      <div
+        v-if="couponDetailData"
+        class="mb-4 rounded-md border border-blue-600 bg-blue-50 p-4"
+      >
+        <div class="mb-2">
+          <span class="text-lg font-semibold text-blue-800">
+            {{ couponDetailData.coupon2mc }}
+          </span>
+        </div>
+        <div class="grid grid-cols-2 gap-4">
+          <div class="space-y-2">
+            <div class="flex">
+              <span class="w-24 text-sm font-medium text-gray-600">
+                优惠券代码:
+              </span>
+              <span class="text-sm text-gray-800">
+                {{ couponDetailData.coupon2code }}
+              </span>
+            </div>
+            <div class="flex">
+              <span class="w-24 text-sm font-medium text-gray-600">
+                产品名称:
+              </span>
+              <span class="text-sm text-gray-800">
+                {{ couponDetailData.productsname }}
+              </span>
+            </div>
+            <div class="flex">
+              <span class="w-24 text-sm font-medium text-gray-600">
+                经销商:
+              </span>
+              <span class="text-sm text-gray-800">
+                {{
+                  merchantDetailData?.usersname ||
+                  couponDetailData.coupon2merchantid ||
+                  '未知'
+                }}
+              </span>
+            </div>
+          </div>
+          <div class="space-y-2">
+            <div class="flex">
+              <span class="w-24 text-sm font-medium text-gray-600">
+                购机者:
+              </span>
+              <span class="text-sm text-gray-800">
+                {{ couponDetailData.usersname }}
+              </span>
+            </div>
+            <div class="flex">
+              <span class="w-24 text-sm font-medium text-gray-600">
+                联系电话:
+              </span>
+              <span class="text-sm text-gray-800">
+                {{ couponDetailData.usersphone }}
+              </span>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div
+        class="rounded-md border border-gray-200 bg-white px-4 py-4 shadow-sm"
+      >
+        <el-form
+          ref="formRef"
+          :model="formData"
+          :rules="formRules"
+          label-width="120px"
+        >
+          <el-form-item label="优惠券ID">
+            <el-input
+              :value="localCoupon2sid"
+              placeholder="优惠券ID"
+              disabled
+            />
+          </el-form-item>
+
+          <el-form-item label="SN号" prop="serialNumber">
+            <el-input
+              v-model="formData.serialNumber"
+              placeholder="请输入产品SN号"
+              clearable
+            />
+          </el-form-item>
+
+          <el-form-item label="发票信息" prop="invoiceIds">
+            <UploadFile
+              v-model:file-list="formData.invoiceEpFiles"
+              v-model:file-ids="formData.invoiceIds"
+              attmodel="coupon_invoice"
+              attpath="/coupon_invoice/"
+              :limit="3"
+              list-type="text"
+              tips="支持上传jpg、png、pdf格式,最多3个文件"
+              accept="image/*,.pdf"
+            />
+          </el-form-item>
+        </el-form>
+      </div>
+    </div>
+  </Modal>
+</template>
+
+<style lang="scss" scoped>
+.el-form {
+  .el-form-item {
+    margin-bottom: 20px;
+  }
+}
+</style>

+ 745 - 0
apps/web-ele/src/views/examine-manage/examine-coupon/receive-coupon-form.vue

@@ -0,0 +1,745 @@
+<script lang="ts" setup>
+import type { FormInstance, FormRules } from 'element-plus';
+
+import {
+  computed,
+  defineEmits,
+  defineExpose,
+  onMounted,
+  reactive,
+  ref,
+} from 'vue';
+
+import { useVbenModal } from '@vben/common-ui';
+
+import { ElMessage } from 'element-plus';
+
+import { getCoupon1ListApi, receiveCouponApi } from '#/api/coupon/coupon1';
+import { getProductListApi } from '#/api/product';
+import { getCustomerListApi } from '#/api/user';
+import UploadFile from '#/components/upload-file/index.vue';
+
+const emit = defineEmits(['finish', 'cancel']);
+
+const localCouponId = ref('');
+const localProductId = ref('');
+const localMerchantId = ref('');
+
+// 添加经销商和产品选择相关数据
+const merchantOptions = ref<{ label: string; value: string }[]>([]);
+const productOptions = ref<{ label: string; value: string }[]>([]);
+const merchantLoading = ref(false);
+const productLoading = ref(false);
+
+// 添加优惠券选择相关数据
+const couponOptions = ref<{ label: string; value: string }[]>([]);
+const couponLoading = ref(false);
+
+// 添加产品详细信息状态
+const selectedProductInfo = ref<any>(null);
+const productInfoLoading = ref(false);
+
+// 添加优惠券详细信息状态
+const selectedCouponInfo = ref<any>(null);
+const couponInfoLoading = ref(false);
+
+const formRef = ref<FormInstance>();
+const formData = reactive({
+  merchantId: '', // 经销商ID
+  productId: '', // 产品ID
+  couponId: '', // 优惠券ID
+  userstype: '个人',
+  usersname: '',
+  idNumber: '',
+  idCardEpFiles: [] as any[],
+  businessLicenseEpFiles: [] as any[],
+  buyerPhotoEpFiles: [] as any[],
+  idCardIds: [] as { id: string; name?: string; url: string }[],
+  businessLicenseIds: [] as { id: string; name?: string; url: string }[],
+  buyerPhotoIds: [] as { id: string; name?: string; url: string }[],
+  phone: '',
+});
+
+const usersnameLabel = computed(() =>
+  formData.userstype === '个人' ? '姓名' : '企业名称',
+);
+const idNumberLabel = computed(() =>
+  formData.userstype === '个人' ? '身份证号' : '统一社会信用代码',
+);
+const idPhotoLabel = computed(() =>
+  formData.userstype === '个人' ? '身份证正面' : '营业执照',
+);
+
+const submitLoading = ref(false);
+
+const formRules = computed<FormRules>(() => ({
+  merchantId: [{ required: true, message: '请选择经销商', trigger: 'blur' }],
+  productId: [{ required: true, message: '请选择产品', trigger: 'blur' }],
+  couponId: [{ required: true, message: '请选择优惠券', trigger: 'blur' }],
+  userstype: [{ required: true, message: '请选择用户类型', trigger: 'blur' }],
+  usersname: [
+    {
+      required: true,
+      message: `请输入${usersnameLabel.value}`,
+      trigger: 'blur',
+    },
+  ],
+  idNumber: [
+    {
+      required: true,
+      message: `请输入${idNumberLabel.value}`,
+      trigger: 'blur',
+    },
+  ],
+  idCardIds: [
+    {
+      required: true,
+      validator: (_rule, value, callback) => {
+        if (formData.userstype === '个人' && (!value || value.length === 0)) {
+          callback(new Error('请上传身份证正面照片'));
+        } else {
+          callback();
+        }
+      },
+      trigger: 'blur',
+    },
+  ],
+  businessLicenseIds: [
+    {
+      required: true,
+      validator: (_rule, value, callback) => {
+        if (formData.userstype === '企业' && (!value || value.length === 0)) {
+          callback(new Error('请上传营业执照照片'));
+        } else {
+          callback();
+        }
+      },
+      trigger: 'blur',
+    },
+  ],
+  phone: [
+    { required: true, message: '请输入手机号码', trigger: 'blur' },
+    {
+      pattern: /^1[3-9]\d{9}$/,
+      message: '请输入正确的手机号码',
+      trigger: 'blur',
+    },
+  ],
+}));
+
+// 获取经销商列表
+async function fetchMerchantOptions() {
+  try {
+    merchantLoading.value = true;
+    const response = await getCustomerListApi({
+      'usersnature.value': '经销商',
+      pageindex: 1,
+      rows: 100,
+    });
+
+    if (response && response.Data && Array.isArray(response.Data)) {
+      merchantOptions.value = response.Data.map((item: any) => ({
+        label: item.usersname || '未命名经销商',
+        value: item.usersid || '',
+      })).filter((item: any) => item.value);
+    }
+  } catch (error) {
+    console.error('获取经销商列表失败', error);
+    ElMessage.error('获取经销商列表失败');
+  } finally {
+    merchantLoading.value = false;
+  }
+}
+
+// 获取产品列表
+async function fetchProductOptions(merchantId: string) {
+  if (!merchantId) {
+    productOptions.value = [];
+    return;
+  }
+
+  try {
+    productLoading.value = true;
+    const response = await getProductListApi({
+      productsmerchantid: merchantId,
+      pageindex: 1,
+      rows: 100,
+    });
+
+    if (response && response.Data && Array.isArray(response.Data)) {
+      productOptions.value = response.Data.map((item: any) => ({
+        label: item.productsname || '未命名产品',
+        value: item.productsid || '',
+      })).filter((item: any) => item.value);
+    }
+  } catch (error) {
+    console.error('获取产品列表失败', error);
+    ElMessage.error('获取产品列表失败');
+  } finally {
+    productLoading.value = false;
+  }
+}
+
+// 获取优惠券列表
+async function fetchCouponOptions(productId: string) {
+  if (!productId) {
+    couponOptions.value = [];
+    return;
+  }
+
+  try {
+    couponLoading.value = true;
+    const response = await getCoupon1ListApi({
+      couponproductids: productId,
+      couponsfky: 1, // 只获取可用的优惠券
+      pageindex: 1,
+      rows: 100,
+    });
+
+    if (response && response.Data && Array.isArray(response.Data)) {
+      couponOptions.value = response.Data.map((item: any) => ({
+        label: item.couponmc || '未命名优惠券',
+        value: item.couponid || '',
+      })).filter((item: any) => item.value);
+    }
+  } catch (error) {
+    console.error('获取优惠券列表失败', error);
+    ElMessage.error('获取优惠券列表失败');
+  } finally {
+    couponLoading.value = false;
+  }
+}
+
+// 经销商选择变化时的处理
+function handleMerchantChange(merchantId: string) {
+  formData.productId = ''; // 清空产品选择
+  localMerchantId.value = merchantId;
+  if (merchantId) {
+    fetchProductOptions(merchantId);
+  } else {
+    productOptions.value = [];
+  }
+}
+
+// 产品选择变化时的处理
+function handleProductChange(productId: string) {
+  localProductId.value = productId;
+  formData.couponId = ''; // 清空优惠券选择
+  selectedCouponInfo.value = null; // 清空优惠券信息
+
+  if (productId) {
+    fetchProductInfo(productId);
+    fetchCouponOptions(productId);
+  } else {
+    selectedProductInfo.value = null;
+    couponOptions.value = [];
+  }
+}
+
+// 获取产品详细信息
+async function fetchProductInfo(productId: string) {
+  if (!productId) {
+    selectedProductInfo.value = null;
+    return;
+  }
+
+  try {
+    productInfoLoading.value = true;
+    const response = await getProductListApi({
+      productsid: productId,
+      pageindex: 1,
+      rows: 1,
+    });
+
+    selectedProductInfo.value =
+      response &&
+      response.Data &&
+      Array.isArray(response.Data) &&
+      response.Data.length > 0
+        ? response.Data[0]
+        : null;
+  } catch (error) {
+    console.error('获取产品详细信息失败', error);
+    ElMessage.error('获取产品详细信息失败');
+    selectedProductInfo.value = null;
+  } finally {
+    productInfoLoading.value = false;
+  }
+}
+
+// 优惠券选择变化时的处理
+function handleCouponChange(couponId: string) {
+  localCouponId.value = couponId;
+  if (couponId) {
+    fetchCouponInfo(couponId);
+  } else {
+    selectedCouponInfo.value = null;
+  }
+}
+
+// 获取优惠券详细信息
+async function fetchCouponInfo(couponId: string) {
+  if (!couponId) {
+    selectedCouponInfo.value = null;
+    return;
+  }
+
+  try {
+    couponInfoLoading.value = true;
+    const response = await getCoupon1ListApi({
+      couponid: couponId,
+      pageindex: 1,
+      rows: 1,
+    });
+
+    selectedCouponInfo.value =
+      response &&
+      response.Data &&
+      Array.isArray(response.Data) &&
+      response.Data.length > 0
+        ? response.Data[0]
+        : null;
+  } catch (error) {
+    console.error('获取优惠券详细信息失败', error);
+    ElMessage.error('获取优惠券详细信息失败');
+    selectedCouponInfo.value = null;
+  } finally {
+    couponInfoLoading.value = false;
+  }
+}
+
+function handleUserTypeChange() {
+  formData.usersname = '';
+  formData.idNumber = '';
+  formData.idCardEpFiles = [];
+  formData.idCardIds = [];
+  formData.businessLicenseEpFiles = [];
+  formData.businessLicenseIds = [];
+  formData.buyerPhotoEpFiles = [];
+  formData.buyerPhotoIds = [];
+  formRef.value?.clearValidate();
+}
+
+async function handleSubmit(): Promise<boolean> {
+  if (!formRef.value) return false;
+  try {
+    await formRef.value.validate();
+    submitLoading.value = true;
+
+    let combinedFileIds: string[] = [];
+    combinedFileIds =
+      formData.userstype === '个人'
+        ? [...combinedFileIds, ...formData.idCardIds.map((f) => f.id)]
+        : [...combinedFileIds, ...formData.businessLicenseIds.map((f) => f.id)];
+    combinedFileIds = [
+      ...combinedFileIds,
+      ...formData.buyerPhotoIds.map((f) => f.id),
+    ];
+
+    const finalFileIdsString = combinedFileIds.filter(Boolean).join(',');
+
+    const params = {
+      couponid: formData.couponId,
+      userstype: formData.userstype,
+      usersshtyxydm: formData.userstype === '企业' ? formData.idNumber : '',
+      usersidcardnumber: formData.userstype === '个人' ? formData.idNumber : '',
+      usersname: formData.usersname,
+      coupon2productids: formData.productId,
+      coupon2phone: formData.phone,
+      filesid: finalFileIdsString,
+      coupon2merchantid: formData.merchantId,
+    };
+
+    // 调用领取优惠券API
+    const response = await receiveCouponApi(params);
+
+    if (response && response.success !== false) {
+      ElMessage.success('优惠券领取申请提交成功');
+      return response;
+    } else {
+      ElMessage.error(response?.message || '提交失败,请重试');
+      return response;
+    }
+  } catch (error) {
+    console.error('表单验证失败或提交出错:', error);
+    ElMessage.error('请检查表单填写是否正确或联系管理员');
+    return false;
+  } finally {
+    submitLoading.value = false;
+  }
+}
+
+const [Modal, modalApi] = useVbenModal({
+  class: 'w-[1000px]',
+  title: '领取优惠券',
+  showCancelButton: true,
+  fullscreenButton: false,
+  closeOnClickModal: false,
+  closeOnPressEscape: false,
+  async onConfirm() {
+    const coupon2sid = await handleSubmit();
+    if (coupon2sid) {
+      modalApi.close();
+      emit('finish', { coupon2sid });
+    }
+  },
+  onCancel() {
+    modalApi.close();
+    emit('cancel');
+  },
+  onOpenChange(isOpen: boolean) {
+    if (isOpen) {
+      const modalData = modalApi.getData() as {
+        couponId?: string;
+        merchantId?: string;
+        productId?: string;
+      };
+      localCouponId.value = modalData?.couponId || '';
+      localProductId.value = modalData?.productId || '';
+      localMerchantId.value = modalData?.merchantId || '';
+
+      // 重置表单数据
+      formData.merchantId = '';
+      formData.productId = '';
+      formData.couponId = '';
+      formData.userstype = '个人';
+      formData.usersname = '';
+      formData.idNumber = '';
+      formData.idCardEpFiles = [];
+      formData.idCardIds = [];
+      formData.businessLicenseEpFiles = [];
+      formData.businessLicenseIds = [];
+      formData.buyerPhotoEpFiles = [];
+      formData.buyerPhotoIds = [];
+      formData.phone = '';
+
+      // 重置选项数据
+      productOptions.value = [];
+      couponOptions.value = [];
+      selectedProductInfo.value = null;
+      selectedCouponInfo.value = null;
+
+      formRef.value?.resetFields();
+      submitLoading.value = false;
+
+      // 初始化经销商选项
+      fetchMerchantOptions();
+    }
+  },
+});
+
+function openReceiveCouponModal(data?: {
+  couponId?: string;
+  merchantId?: string;
+  productId?: string;
+}) {
+  modalApi.setData(data || {});
+  modalApi.open();
+}
+
+defineExpose({
+  openReceiveCouponModal,
+});
+
+onMounted(() => {});
+</script>
+
+<template>
+  <Modal>
+    <div class="p-4">
+      <div
+        class="rounded-md border border-gray-200 bg-white px-4 py-4 shadow-sm"
+      >
+        <el-form
+          ref="formRef"
+          :model="formData"
+          :rules="formRules"
+          label-width="140px"
+        >
+          <el-form-item label="经销商" prop="merchantId">
+            <el-select
+              v-model="formData.merchantId"
+              placeholder="请选择经销商"
+              :loading="merchantLoading"
+              clearable
+              @change="handleMerchantChange"
+            >
+              <el-option
+                v-for="option in merchantOptions"
+                :key="option.value"
+                :label="option.label"
+                :value="option.value"
+              />
+            </el-select>
+          </el-form-item>
+
+          <el-form-item label="产品" prop="productId">
+            <el-select
+              v-model="formData.productId"
+              placeholder="请先选择经销商"
+              :loading="productLoading"
+              :disabled="!formData.merchantId"
+              clearable
+              @change="handleProductChange"
+            >
+              <el-option
+                v-for="option in productOptions"
+                :key="option.value"
+                :label="option.label"
+                :value="option.value"
+              />
+            </el-select>
+          </el-form-item>
+
+          <el-form-item label="优惠券" prop="couponId">
+            <el-select
+              v-model="formData.couponId"
+              placeholder="请先选择产品"
+              :loading="couponLoading"
+              :disabled="!formData.productId"
+              clearable
+              @change="handleCouponChange"
+            >
+              <el-option
+                v-for="option in couponOptions"
+                :key="option.value"
+                :label="option.label"
+                :value="option.value"
+              />
+            </el-select>
+          </el-form-item>
+
+          <!-- 产品详细信息展示 -->
+          <el-form-item v-if="selectedProductInfo" label="" label-width="0">
+            <div
+              class="w-full rounded-md border border-green-600 bg-green-200 p-4"
+            >
+              <div v-if="productInfoLoading" class="text-center">
+                <span class="text-gray-500">加载产品信息中...</span>
+              </div>
+              <div v-else class="space-y-2">
+                <div class="mb-2">
+                  <span class="text-lg font-semibold text-green-800">
+                    {{ selectedProductInfo.productsname }}
+                  </span>
+                </div>
+                <div class="grid grid-cols-2 gap-4">
+                  <div class="space-y-2">
+                    <div class="flex">
+                      <span class="w-20 text-sm font-medium text-gray-600">
+                        产品型号:
+                      </span>
+                      <span class="text-sm text-gray-800">
+                        {{ selectedProductInfo.productsmodel }}
+                      </span>
+                    </div>
+                    <div class="flex">
+                      <span class="w-20 text-sm font-medium text-gray-600">
+                        机具类型:
+                      </span>
+                      <span class="text-sm text-gray-800">
+                        {{ selectedProductInfo.productsjjlx }}
+                      </span>
+                    </div>
+                    <div class="flex">
+                      <span class="w-20 text-sm font-medium text-gray-600">
+                        产品分类:
+                      </span>
+                      <span class="text-sm text-gray-800">
+                        {{ selectedProductInfo.productscategory }}
+                      </span>
+                    </div>
+                  </div>
+                  <div class="space-y-2">
+                    <div class="flex">
+                      <span class="w-20 text-sm font-medium text-gray-600">
+                        产品价格:
+                      </span>
+                      <span class="text-sm font-semibold text-red-600">
+                        ¥{{
+                          selectedProductInfo.productsprice?.toLocaleString()
+                        }}
+                      </span>
+                    </div>
+                    <div class="flex">
+                      <span class="w-20 text-sm font-medium text-gray-600">
+                        中央补贴:
+                      </span>
+                      <span class="text-sm text-green-600">
+                        ¥{{
+                          selectedProductInfo.productszybt?.toLocaleString()
+                        }}
+                      </span>
+                    </div>
+                    <div class="flex">
+                      <span class="w-20 text-sm font-medium text-gray-600">
+                        特殊补贴:
+                      </span>
+                      <span class="text-sm text-green-600">
+                        ¥{{
+                          selectedProductInfo.productstsxzybt?.toLocaleString()
+                        }}
+                      </span>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </el-form-item>
+
+          <!-- 优惠券详细信息展示 -->
+          <el-form-item v-if="selectedCouponInfo" label="" label-width="0">
+            <div
+              class="w-full rounded-md border border-blue-600 bg-blue-200 p-4"
+            >
+              <div v-if="couponInfoLoading" class="text-center">
+                <span class="text-gray-500">加载优惠券信息中...</span>
+              </div>
+              <div v-else class="space-y-2">
+                <div class="mb-2">
+                  <span class="text-lg font-semibold text-blue-800">
+                    {{ selectedCouponInfo.couponmc }}
+                  </span>
+                </div>
+                <div class="grid grid-cols-2 gap-4">
+                  <div class="space-y-2">
+                    <div class="flex">
+                      <span class="w-24 text-sm font-medium text-gray-600">
+                        优惠券ID:
+                      </span>
+                      <span class="text-sm text-gray-800">
+                        {{ selectedCouponInfo.couponid }}
+                      </span>
+                    </div>
+                    <div class="flex">
+                      <span class="w-24 text-sm font-medium text-gray-600">
+                        创建时间:
+                      </span>
+                      <span class="text-sm text-gray-800">
+                        {{ selectedCouponInfo.couponcreatedate }}
+                      </span>
+                    </div>
+                  </div>
+                  <div class="space-y-2">
+                    <div class="flex">
+                      <span class="w-24 text-sm font-medium text-gray-600">
+                        状态:
+                      </span>
+                      <span
+                        :class="
+                          selectedCouponInfo.couponsfky === 1
+                            ? 'text-green-600'
+                            : 'text-red-600'
+                        "
+                        class="text-sm font-semibold"
+                      >
+                        {{
+                          selectedCouponInfo.couponsfky === 1
+                            ? '可用'
+                            : '不可用'
+                        }}
+                      </span>
+                    </div>
+                    <div class="flex">
+                      <span class="w-24 text-sm font-medium text-gray-600">
+                        创建人:
+                      </span>
+                      <span class="text-sm text-gray-800">
+                        {{ selectedCouponInfo.couponcreateuser }}
+                      </span>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </el-form-item>
+
+          <el-form-item label="用户类型" prop="userstype">
+            <el-radio-group
+              v-model="formData.userstype"
+              @change="handleUserTypeChange"
+            >
+              <el-radio value="个人">个人</el-radio>
+              <el-radio value="企业">企业</el-radio>
+            </el-radio-group>
+          </el-form-item>
+
+          <el-form-item :label="usersnameLabel" prop="usersname">
+            <el-input
+              v-model="formData.usersname"
+              :placeholder="`请输入${usersnameLabel}`"
+              clearable
+            />
+          </el-form-item>
+
+          <el-form-item :label="idNumberLabel" prop="idNumber">
+            <el-input
+              v-model="formData.idNumber"
+              :placeholder="`请输入${idNumberLabel}`"
+              clearable
+            />
+          </el-form-item>
+
+          <el-form-item
+            :label="idPhotoLabel"
+            :prop="
+              formData.userstype === '个人' ? 'idCardIds' : 'businessLicenseIds'
+            "
+          >
+            <template v-if="formData.userstype === '个人'">
+              <UploadFile
+                v-model:file-list="formData.idCardEpFiles"
+                v-model:file-ids="formData.idCardIds"
+                attmodel="coupon_identity"
+                attpath="/coupon_identity/"
+                :limit="1"
+                list-type="picture-card"
+                tips=""
+                accept="image/*,.pdf"
+              />
+            </template>
+            <template v-else>
+              <UploadFile
+                v-model:file-list="formData.businessLicenseEpFiles"
+                v-model:file-ids="formData.businessLicenseIds"
+                attmodel="coupon_identity"
+                attpath="/coupon_identity/"
+                :limit="1"
+                list-type="picture-card"
+                tips=""
+                accept="image/*,.pdf"
+              />
+            </template>
+          </el-form-item>
+
+          <el-form-item label="购机者照片(可选)" prop="buyerPhotoIds">
+            <UploadFile
+              v-model:file-list="formData.buyerPhotoEpFiles"
+              v-model:file-ids="formData.buyerPhotoIds"
+              attmodel="coupon_buyer"
+              attpath="/coupon_buyer/"
+              :limit="1"
+              list-type="picture-card"
+              tips=""
+              accept="image/*,.pdf"
+            />
+          </el-form-item>
+
+          <el-form-item label="手机号" prop="phone">
+            <el-input
+              v-model="formData.phone"
+              placeholder="请输入您的手机号码"
+              clearable
+            />
+          </el-form-item>
+        </el-form>
+      </div>
+    </div>
+  </Modal>
+</template>
+
+<style lang="scss" scoped>
+.rules-text p {
+  margin-bottom: 0.25rem;
+}
+</style>

+ 80 - 156
apps/web-ele/src/views/order-manage/form.vue

@@ -10,7 +10,7 @@ import { ElImage, ElMessage } from 'element-plus';
 
 import { useVbenForm, z } from '#/adapter/form';
 import { getDictListApi } from '#/api/dict';
-import { addImageApi, getAttachmentListApi } from '#/api/file';
+import { getAttachmentListApi } from '#/api/file';
 import { addOrdersApi, editOrdersApi, getOrdersDetailApi } from '#/api/orders';
 
 const emit = defineEmits(['finish']);
@@ -380,168 +380,92 @@ const [Modal, modalApi] = useVbenModal({
           });
         }
 
-        // 最后添加发票附件
+        // 最后添加发票附件展示
         // 如果不存在,则添加发票附件字段
-        if (!hasInvoiceAttachments && invoiceDataList.value.length > 0) {
+        if (!hasInvoiceAttachments) {
           baseFormApi.setState({
             schema: [
               ...(baseFormApi.getState()?.schema ?? []),
               {
                 component: h(
                   'div',
-                  { class: 'grid-cols-1 lg:grid-cols-2 flex flex-wrap gap-3' },
-                  invoiceDataList.value.map((item) => {
-                    return h(ElImage, {
-                      src: item.url,
-                      previewSrcList: invoiceDataList.value.map((i) => i.url),
-                      fit: 'cover',
-                      style: 'width: 100px; height: 100px;',
-                      class: 'border rounded',
-                      previewTeleported: true,
-                    });
-                  }),
-                ),
-                fieldName: 'invoiceAttachments',
-                label: '发票附件',
-                // formItemClass: 'lg:col-span-2',
-              },
-            ],
-          });
-        } else {
-          // 设置空数据提示
-          baseFormApi.setState({
-            schema: [
-              ...(baseFormApi.getState()?.schema ?? []),
-              {
-                component: h(
-                  'div',
-                  {
-                    class:
-                      'grid-cols-1 lg:grid-cols-2 flex flex-wrap gap-3 text-sm',
-                  },
-                  formType.value === 'edit'
-                    ? h(
-                        'button',
-                        {
-                          class:
-                            'mt-1 px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600',
-                          onClick: () => {
-                            // 处理上传发票的逻辑
-                            const fileInput = document.createElement('input');
-                            fileInput.type = 'file';
-                            fileInput.accept = 'image/*';
-                            fileInput.multiple = false; // 限制上传数量为1
-
-                            fileInput.addEventListener(
-                              'change',
-                              async (event) => {
-                                const target = event.target as HTMLInputElement;
-                                const files = target.files;
-
-                                if (files && files.length > 0) {
-                                  try {
-                                    // 开始上传
-                                    modalApi.setState({ loading: true });
-
-                                    const file = files[0];
-                                    const formData = new FormData();
-
-                                    // 添加必要参数
-                                    if (file) {
-                                      formData.append('file', file);
-                                    }
-                                    formData.append(
-                                      'attlsh',
-                                      data.value.row.ordersid,
-                                    );
-                                    formData.append(
-                                      'attmodel',
-                                      'orders_invoice',
-                                    );
-                                    formData.append(
-                                      'attpath',
-                                      '/orders_invoice/',
-                                    );
-
-                                    // 调用上传接口
-                                    const result = await addImageApi(
-                                      formData as any,
-                                    );
-
-                                    if (result) {
-                                      ElMessage.success('发票上传成功');
-
-                                      // 重新获取发票附件列表
-                                      const filePrefix = await getFilePrefix();
-                                      await getOrderInvoice(
-                                        data.value.row.ordersid,
-                                        filePrefix,
-                                      );
-
-                                      // 更新发票附件显示
-                                      if (invoiceDataList.value.length > 0) {
-                                        baseFormApi.setState({
-                                          schema:
-                                            baseFormApi
-                                              .getState()
-                                              ?.schema?.map((item) => {
-                                                if (
-                                                  item.fieldName ===
-                                                  'invoiceAttachments'
-                                                ) {
-                                                  return {
-                                                    ...item,
-                                                    component: h(
-                                                      'div',
-                                                      {
-                                                        class:
-                                                          'grid-cols-1 lg:grid-cols-2 flex flex-wrap gap-3',
-                                                      },
-                                                      invoiceDataList.value.map(
-                                                        (imgItem) => {
-                                                          return h(ElImage, {
-                                                            src: imgItem.url,
-                                                            previewSrcList:
-                                                              invoiceDataList.value.map(
-                                                                (i) => i.url,
-                                                              ),
-                                                            fit: 'cover',
-                                                            style:
-                                                              'width: 100px; height: 100px;',
-                                                            class:
-                                                              'border rounded',
-                                                            previewTeleported:
-                                                              true,
-                                                          });
-                                                        },
-                                                      ),
-                                                    ),
-                                                  };
-                                                }
-                                                return item;
-                                              }) || [],
-                                        });
-                                      }
-                                    } else {
-                                      ElMessage.error('发票上传失败');
-                                    }
-                                  } catch (error) {
-                                    console.error(error);
-                                    ElMessage.error('发票上传过程中发生错误');
-                                  } finally {
-                                    modalApi.setState({ loading: false });
-                                  }
-                                }
-                              },
-                            );
-
-                            // 触发文件选择对话框
-                            fileInput.click();
+                  { class: 'flex flex-wrap gap-3' },
+                  invoiceDataList.value.length > 0
+                    ? invoiceDataList.value.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: invoiceDataList.value
+                                  .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}`,
+                                ],
+                              );
+                        }
+                      })
+                    : [
+                        h(
+                          'span',
+                          {
+                            class: 'text-sm text-gray-500',
                           },
-                        },
-                        '上传发票',
-                      )
-                    : '暂无',
+                          '暂无发票附件',
+                        ),
+                      ],
                 ),
                 fieldName: 'invoiceAttachments',
                 label: '发票附件',
@@ -581,7 +505,7 @@ const getOrderDetail = async (ordersid: string, formApi: ExtendedFormApi) => {
 const getOrderInvoice = async (ordersid: string, filePrefix: string) => {
   const invoiceData = await getAttachmentListApi({
     attlsh: ordersid,
-    attmodel: 'orders_invoice',
+    attmodel: 'coupon_invoice',
     pageindex: 1,
     rows: 10,
   });

+ 3 - 1
packages/types/src/coupon2.ts

@@ -6,7 +6,7 @@
 //   `coupon2reviewdatetime` datetime DEFAULT NULL COMMENT '优惠券审核时间',
 //   `coupon2userid` varchar(50) DEFAULT NULL COMMENT '关联申请人id',
 //   `coupon2openid` varchar(50) DEFAULT NULL COMMENT '关联申请人openid',
-//   `coupon2sype` tinyint(2) DEFAULT NULL COMMENT '优惠券(0:申请中,1:审核通过,2:审核不通过)',
+//   `coupon2sype` tinyint(2) DEFAULT NULL COMMENT '优惠券(0:申请中,1:审核通过,2:审核失败)',
 //   `coupon2isused` tinyint(2) DEFAULT NULL COMMENT '是否已使用(0:未使用,1:已使用)',
 //   `coupon2merchantid` varchar(50) DEFAULT NULL COMMENT '对应渠道id',
 //   `coupon2productids` varchar(5000) DEFAULT NULL COMMENT '可用产品id',
@@ -49,6 +49,8 @@ interface Coupon2Entity {
   coupon2shbz: string;
   /** 优惠券名称 */
   coupon2mc: string;
+  /** 对应渠道id */
+  coupon2merchantid: string;
 }
 
 export type { Coupon2Entity };