sgc 2 месяцев назад
Родитель
Сommit
335780ab0e

+ 96 - 0
src/api/modules/contractManagement.ts

@@ -0,0 +1,96 @@
+/**
+ * 合同管理相关接口
+ * 使用独立的 axios 实例,端口为 33333
+ */
+import axios from "axios";
+import { ResultEnum } from "@/enums/httpEnum";
+import { useUserStore } from "@/stores/modules/user";
+import { ElMessage } from "element-plus";
+import { LOGIN_URL } from "@/config";
+import router from "@/routers";
+
+// 合同接口专用配置 - 使用不同的端口
+const CONTRACT_BASE_URL = "http://120.26.186.130:33333";
+
+// 创建专门用于合同接口的 axios 实例
+const contractAxios = axios.create({
+  baseURL: CONTRACT_BASE_URL,
+  timeout: ResultEnum.TIMEOUT as number,
+  withCredentials: true
+});
+
+// 请求拦截:补充 token
+contractAxios.interceptors.request.use(
+  config => {
+    const userStore = useUserStore();
+    if (config.headers) {
+      (config.headers as any).Authorization = userStore.token;
+    }
+    return config;
+  },
+  error => Promise.reject(error)
+);
+
+// 响应拦截:与平台 http 保持一致的基础处理
+contractAxios.interceptors.response.use(
+  response => {
+    const data = response.data;
+    console.log(3333, data);
+    const userStore = useUserStore();
+    if (data.code == 200) {
+      userStore.setToken("");
+      router.replace(LOGIN_URL);
+      ElMessage.error(data.msg);
+      return Promise.reject(data);
+    }
+    if (data.code && data.code !== ResultEnum.SUCCESS) {
+      ElMessage.error(data.msg);
+      return Promise.reject(data);
+    }
+    return data;
+  },
+  error => {
+    ElMessage.error("请求失败,请稍后重试");
+    return Promise.reject(error);
+  }
+);
+
+/**
+ * 获取合同列表
+ * @param {string} storeId - 店铺ID
+ * @param {object} params - 请求参数 { page, page_size, status, file_name }
+ * @returns {Promise}
+ */
+export const getContractList = (storeId: string | number, params: any = {}) => {
+  return contractAxios.get(`/api/store/contracts/${storeId}`, { params });
+};
+
+/**
+ * 获取合同详情
+ * @param {string} storeId - 店铺ID
+ * @param {string} contractId - 合同ID
+ * @returns {Promise}
+ */
+export const getContractDetail = (storeId: string | number, contractId: string | number) => {
+  return contractAxios.get(`/api/store/contarcts/${storeId}/${contractId}`);
+};
+
+/**
+ * 签署合同
+ * @param {string} storeId - 店铺ID
+ * @param {string} contractId - 合同ID
+ * @param {object} data - 签署数据
+ * @returns {Promise}
+ */
+export const signContract = (storeId: string | number, contractId: string | number, data: any = {}) => {
+  return contractAxios.post(`/api/store/contarcts/${storeId}/${contractId}/sign`, data);
+};
+
+/**
+ * 获取合同签署链接(查看合同)
+ * @param {object} data - 请求参数 { sign_flow_id, contact_phone }
+ * @returns {Promise}
+ */
+export const getContractSignUrl = (data: any = {}) => {
+  return contractAxios.post(`/api/store/esign/signurl`, data);
+};

+ 31 - 0
src/assets/json/authMenuList.json

@@ -1079,6 +1079,37 @@
         }
       ]
     },
+    {
+      "path": "/contractManagement",
+      "name": "contractManagement",
+      "component": "/contractManagement/index",
+      "meta": {
+        "icon": "Menu",
+        "title": "合同管理",
+        "isLink": "",
+        "isHide": false,
+        "isFull": false,
+        "isAffix": false,
+        "isKeepAlive": false
+      },
+      "children": [
+        {
+          "path": "/contractManagement/detail",
+          "name": "contractManagementDetail",
+          "component": "/contractManagement/detail",
+          "meta": {
+            "icon": "Menu",
+            "title": "合同详情",
+            "activeMenu": "/contractManagement",
+            "isLink": "",
+            "isHide": true,
+            "isFull": false,
+            "isAffix": false,
+            "isKeepAlive": false
+          }
+        }
+      ]
+    },
 
     {
       "path": "/accountRoleManagement",

+ 152 - 0
src/views/contractManagement/detail.vue

@@ -0,0 +1,152 @@
+<template>
+  <div class="contract-detail-page">
+    <div class="page-header">
+      <el-button @click="goBack"> 返回 </el-button>
+      <h2 class="page-title">
+        {{ isSigned ? "合同" : "合同签署" }}
+      </h2>
+    </div>
+
+    <div class="content-wrapper">
+      <div v-if="loading" class="loading-container">
+        <el-loading :loading="loading" text="加载中..." />
+      </div>
+      <div v-else-if="contractUrl" class="contract-content">
+        <iframe :src="contractUrl" class="contract-iframe" frameborder="0" />
+      </div>
+      <div v-else class="error-container">
+        <el-empty description="合同内容加载失败" />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts" name="contractDetail">
+import { ref, onMounted } from "vue";
+import { useRoute, useRouter } from "vue-router";
+import { ElMessage } from "element-plus";
+import { getContractSignUrl } from "@/api/modules/contractManagement";
+
+const route = useRoute();
+const router = useRouter();
+
+// 合同URL
+const contractUrl = ref<string>("");
+// 加载状态
+const loading = ref<boolean>(true);
+// 是否已签署
+const isSigned = ref<boolean>(false);
+
+// 加载合同详情
+const loadContractDetail = async () => {
+  try {
+    loading.value = true;
+    const signFlowId = route.query.signFlowId as string;
+    const contactPhone = route.query.contactPhone as string;
+    const signed = route.query.signed as string;
+
+    // 判断是否已签署
+    isSigned.value = signed === "true" || signed === "1";
+
+    if (!signFlowId) {
+      ElMessage.error("缺少合同ID参数");
+      return;
+    }
+
+    // 调用获取合同签署链接的接口
+    const res: any = await getContractSignUrl({
+      sign_flow_id: signFlowId,
+      contact_phone: contactPhone || ""
+    });
+
+    if (res && res.code === 200 && res.data) {
+      // 如果返回的是URL字符串,直接使用
+      if (typeof res.data === "string") {
+        contractUrl.value = res.data;
+      } else if (res.data.url) {
+        // 如果返回的是对象,取url字段
+        contractUrl.value = res.data.url;
+      } else {
+        ElMessage.error("合同链接格式错误");
+      }
+    } else {
+      ElMessage.error(res?.msg || "获取合同详情失败");
+    }
+  } catch (error: any) {
+    console.error("加载合同详情失败:", error);
+    ElMessage.error(error?.message || "加载合同详情失败,请重试");
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 返回
+const goBack = () => {
+  router.go(-1);
+};
+
+// 初始化
+onMounted(() => {
+  loadContractDetail();
+});
+</script>
+
+<style lang="scss" scoped>
+.contract-detail-page {
+  display: flex;
+  flex-direction: column;
+  width: 100%;
+  min-height: 100%;
+  background-color: #ffffff;
+  .page-header {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    padding: 20px 24px;
+    background-color: #ffffff;
+    border-bottom: 1px solid #e4e7ed;
+    box-shadow: 0 2px 4px rgb(0 0 0 / 2%);
+    .page-title {
+      flex: 1;
+      margin: 0;
+      font-size: 18px;
+      font-weight: 600;
+      color: #303133;
+      text-align: center;
+    }
+  }
+  .content-wrapper {
+    position: relative;
+    flex: 1;
+    padding: 20px;
+    background: #f5f7fa;
+    .loading-container {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: 100%;
+      height: 600px;
+    }
+    .contract-content {
+      width: 100%;
+      height: calc(100vh - 120px);
+      overflow: hidden;
+      background: #ffffff;
+      border-radius: 8px;
+      box-shadow: 0 2px 8px rgb(0 0 0 / 10%);
+      .contract-iframe {
+        width: 100%;
+        height: 100%;
+        border: none;
+      }
+    }
+    .error-container {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: 100%;
+      height: 600px;
+    }
+  }
+}
+</style>

+ 277 - 0
src/views/contractManagement/index.vue

@@ -0,0 +1,277 @@
+<template>
+  <div class="contract-management-page">
+    <!-- Tab切换 -->
+    <div class="tab-container">
+      <div class="tab-item" :class="{ active: activeTab === 'unsigned' }" @click="switchTab('unsigned')">
+        <span>未签署</span>
+      </div>
+      <div class="tab-item" :class="{ active: activeTab === 'signed' }" @click="switchTab('signed')">
+        <span>已签署</span>
+      </div>
+    </div>
+
+    <!-- 表格 -->
+    <div class="table-container">
+      <ProTable
+        ref="proTable"
+        :columns="columns"
+        :request-api="getTableList"
+        :init-param="initParam"
+        :data-callback="dataCallback"
+        :refresh-reset-page="true"
+      >
+        <template #contractName="scope">
+          <div class="contract-row">
+            <div class="contract-indicator" />
+            <span>{{ scope.row.file_name || "—" }}</span>
+          </div>
+        </template>
+        <template #operation="scope">
+          <template v-if="activeTab === 'unsigned'">
+            <el-button type="primary" link @click="handleViewDetail(scope.row, false)"> 签署 </el-button>
+          </template>
+          <template v-else>
+            <el-button type="primary" link @click="handleViewDetail(scope.row, true)"> 查看 </el-button>
+            <el-button type="primary" link @click="handleDownload(scope.row)"> 下载 </el-button>
+          </template>
+        </template>
+      </ProTable>
+    </div>
+  </div>
+</template>
+
+<script setup lang="tsx" name="contractManagement">
+import { reactive, ref, computed } from "vue";
+import { useRouter } from "vue-router";
+import { ElMessage } from "element-plus";
+import ProTable from "@/components/ProTable/index.vue";
+import { ColumnProps, ProTableInstance } from "@/components/ProTable/interface";
+import { getContractList, getContractSignUrl } from "@/api/modules/contractManagement";
+import { localGet } from "@/utils";
+
+const router = useRouter();
+const proTable = ref<ProTableInstance>();
+
+// 当前选中的tab
+const activeTab = ref<"unsigned" | "signed">("unsigned");
+
+// 初始化请求参数
+const initParam = reactive({
+  storeId: localGet("createdId") || ""
+});
+
+// 表格列配置
+const columns = reactive<ColumnProps<any>[]>([
+  {
+    prop: "file_name",
+    label: "合同名称",
+    minWidth: 200,
+    search: {
+      el: "input",
+      props: { placeholder: "请输入" }
+    }
+  },
+  {
+    prop: "counterparty",
+    label: "对方单位",
+    minWidth: 250,
+    render: (scope: any) => {
+      return scope.row.counterparty || "—";
+    }
+  },
+  {
+    prop: "validity_period",
+    label: "有效期",
+    width: 300,
+    render: (scope: any) => {
+      if (scope.row.effective_time && scope.row.expiry_time) {
+        const start = formatDate(scope.row.effective_time);
+        const end = formatDate(scope.row.expiry_time);
+        return `${start}-${end}`;
+      }
+      return "—";
+    }
+  },
+  { prop: "operation", label: "操作", fixed: "right", width: 200 }
+]);
+
+// 格式化日期
+const formatDate = (dateStr: string) => {
+  if (!dateStr) return "";
+  const date = new Date(dateStr);
+  const year = date.getFullYear();
+  const month = String(date.getMonth() + 1).padStart(2, "0");
+  const day = String(date.getDate()).padStart(2, "0");
+  return `${year}/${month}/${day}`;
+};
+
+// 数据回调处理
+const dataCallback = (data: any) => {
+  console.log(4444, data);
+  // 根据实际接口返回格式处理
+  if (data) {
+    return {
+      list: data.items || [],
+      total: data.total || 0
+    };
+  }
+  return {
+    list: [],
+    total: 0
+  };
+};
+
+// 获取表格列表
+const getTableList = async (params: any) => {
+  const storeId = params.storeId || localGet("createdId");
+  // 根据选中的tab设置状态参数
+  const status = activeTab.value === "signed" ? 1 : 0; // 1-已签署, 0-未签署
+
+  // 构建请求参数
+  const requestParams: any = {
+    page: params.pageNum || params.page || 1,
+    page_size: params.pageSize || params.size || 10,
+    status: status
+  };
+
+  // 添加搜索条件(从 ProTable 的 params 中获取)
+  if (params.file_name) {
+    requestParams.file_name = params.file_name;
+  }
+
+  const res = await getContractList(storeId, requestParams);
+  return res;
+};
+
+// 切换tab
+const switchTab = (tab: "unsigned" | "signed") => {
+  if (activeTab.value === tab) return;
+  activeTab.value = tab;
+  // 刷新表格
+  proTable.value?.reset();
+};
+
+// 查看合同详情(统一入口,未签署和已签署都跳转到详情页)
+const handleViewDetail = (row: any, isSigned: boolean) => {
+  const signFlowId = row.sign_flow_id || row.id;
+  const contactPhone = row.contact_phone || "";
+
+  if (!signFlowId) {
+    ElMessage.error("缺少合同ID");
+    return;
+  }
+
+  // 跳转到合同详情页
+  router.push({
+    path: "/contractManagement/detail",
+    query: {
+      signFlowId: signFlowId,
+      contactPhone: contactPhone,
+      signed: isSigned ? "true" : "false"
+    }
+  });
+};
+
+// 下载合同
+const handleDownload = async (row: any) => {
+  try {
+    // 如果合同有文件URL,直接下载
+    if (row.file_url || row.download_url || row.url) {
+      const downloadUrl = row.file_url || row.download_url || row.url;
+      // 创建临时链接下载
+      const link = document.createElement("a");
+      link.href = downloadUrl;
+      link.download = row.file_name || "合同文件";
+      link.target = "_blank";
+      document.body.appendChild(link);
+      link.click();
+      document.body.removeChild(link);
+      ElMessage.success("下载开始");
+      return;
+    }
+
+    // 如果没有直接的文件URL,尝试通过接口获取下载链接
+    const res: any = await getContractSignUrl({
+      sign_flow_id: row.sign_flow_id || row.id,
+      contact_phone: row.contact_phone || ""
+    });
+
+    if (res && res.code === 200 && res.data) {
+      // 如果返回的是文件URL,直接下载
+      const link = document.createElement("a");
+      link.href = res.data;
+      link.download = row.file_name || "合同文件";
+      link.target = "_blank";
+      document.body.appendChild(link);
+      link.click();
+      document.body.removeChild(link);
+      ElMessage.success("下载开始");
+    } else {
+      ElMessage.error(res?.msg || "获取下载链接失败");
+    }
+  } catch (error: any) {
+    console.error("下载合同失败:", error);
+    ElMessage.error(error?.message || "下载合同失败,请重试");
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.contract-management-page {
+  min-height: 100%;
+  padding: 20px;
+  background: #ffffff;
+  .tab-container {
+    display: flex;
+    gap: 0;
+    margin-bottom: 20px;
+    border-bottom: 1px solid #e4e7ed;
+    .tab-item {
+      position: relative;
+      padding: 12px 24px;
+      font-size: 14px;
+      color: #666666;
+      cursor: pointer;
+      transition: all 0.3s;
+      &:hover {
+        color: #409eff;
+      }
+      &.active {
+        font-weight: bold;
+        color: #409eff;
+        &::after {
+          position: absolute;
+          right: 0;
+          bottom: -1px;
+          left: 0;
+          height: 2px;
+          content: "";
+          background: #409eff;
+        }
+      }
+    }
+  }
+  .table-container {
+    :deep(.el-table) {
+      .contract-row {
+        position: relative;
+        display: flex;
+        align-items: center;
+        padding-left: 8px;
+        .contract-indicator {
+          position: absolute;
+          top: 0;
+          bottom: 0;
+          left: 0;
+          width: 4px;
+          background: linear-gradient(180deg, #ff6b35 0%, #ff8c5a 100%);
+          border-radius: 2px;
+        }
+        span {
+          margin-left: 12px;
+        }
+      }
+    }
+  }
+}
+</style>

+ 1 - 1
src/views/operationManagement/activityDetail.vue

@@ -99,7 +99,7 @@
           <div class="detail-item">
             <div class="detail-label">审核状态</div>
             <div class="detail-value">
-              {{ getAuditStatusLabel(activityModel.status) }}
+              {{ activityModel.auditStatus == 1 ? "审核通过" : "审核驳回" }}
             </div>
           </div>
           <!-- 审核时间 -->

+ 2 - 2
src/views/operationManagement/caseDetail.vue

@@ -11,13 +11,13 @@
             <div class="detail-label">所属活动 : {{ detail.activityName || detail.activityTitle || "-" }}</div>
           </div>
           <div class="detail-item">
-            <div class="detail-label">用户昵称 : {{ detail.nickName || detail.nickname || "-" }}</div>
+            <div class="detail-label">用户昵称 : {{ detail.signupName || "-" }}</div>
           </div>
           <div class="detail-item">
             <div class="detail-label">姓名 : {{ detail.userName || "-" }}</div>
           </div>
           <div class="detail-item">
-            <div class="detail-label">联系方式 : {{ detail.phone || "-" }}</div>
+            <div class="detail-label">联系方式 : {{ detail.signupPhone || "-" }}</div>
           </div>
           <div class="detail-item">
             <div class="detail-label">报名时间 : {{ formatTime(detail.createdTime) }}</div>

+ 2 - 2
src/views/operationManagement/cases.vue

@@ -28,8 +28,8 @@ const router = useRouter();
 const proTable = ref<ProTableInstance>();
 
 const columns = reactive<ColumnProps<any>[]>([
-  { prop: "nickName", label: "姓名", minWidth: 300 },
-  { prop: "phone", label: "联系方式", width: 300 },
+  { prop: "signupName", label: "姓名", minWidth: 300 },
+  { prop: "signupPhone", label: "联系方式", width: 300 },
   {
     prop: "activityName",
     label: "所属活动",

+ 2 - 2
src/views/operationManagement/personnel.vue

@@ -40,8 +40,8 @@ const registrationStatusEnum = [
 ];
 
 const columns = reactive<ColumnProps<any>[]>([
-  { prop: "userName", label: "姓名", minWidth: 220 },
-  { prop: "phone", label: "联系方式", width: 300 },
+  { prop: "signupName", label: "姓名", minWidth: 220 },
+  { prop: "signupPhone", label: "联系方式", width: 300 },
   {
     prop: "activityName",
     label: "所属活动",

+ 3 - 9
src/views/operationManagement/personnelDetail.vue

@@ -11,13 +11,13 @@
             <div class="detail-label">所属活动 : {{ detail.activityName || "-" }}</div>
           </div>
           <div class="detail-item">
-            <div class="detail-label">用户昵称 : {{ detail.nickName || detail.nickname || "-" }}</div>
+            <div class="detail-label">用户昵称 : {{ detail.signUpName || "-" }}</div>
           </div>
           <div class="detail-item">
             <div class="detail-label">姓名 : {{ detail.userName || "-" }}</div>
           </div>
           <div class="detail-item">
-            <div class="detail-label">联系方式 : {{ detail.phone || "-" }}</div>
+            <div class="detail-label">联系方式 : {{ detail.signUpPhone || "-" }}</div>
           </div>
           <div class="detail-item">
             <div class="detail-label">报名时间 : {{ formatTime(detail.signupTime) }}</div>
@@ -112,38 +112,32 @@ onMounted(async () => {
   padding: 16px;
   background: #ffffff;
 }
-
 .header {
   display: flex;
   gap: 16px;
   align-items: center;
   margin-bottom: 16px;
 }
-
 .title {
   margin: 0;
   font-size: 18px;
   font-weight: 600;
 }
-
 .detail-list {
   display: flex;
   flex-direction: column;
   gap: 16px;
 }
-
 .detail-item {
   display: flex;
   flex-direction: column;
   gap: 8px;
 }
-
 .detail-label {
   font-size: 14px;
-  color: #606266;
   font-weight: 500;
+  color: #606266;
 }
-
 .detail-value {
   font-size: 14px;
   color: #303133;