From eef2bdb568e03f1cfb1deba99fb36c85f2086e42 Mon Sep 17 00:00:00 2001
From: yyb <995253665@qq.com>
Date: 星期五, 08 五月 2026 13:55:50 +0800
Subject: [PATCH] Merge branch 'dev_浪潮' of http://114.132.189.42:9002/r/product-inventory-management into dev_浪潮

---
 src/views/basicData/customerFile/index.vue                   |    4 
 src/api/collaborativeApproval/journal.js                     |   62 +
 src/views/salesManagement/opportunityManagement/index.vue    | 1122 +++++++++++++++++++++++++
 src/api/salesManagement/opportunityManagement.js             |   61 +
 src/views/salesManagement/opportunityManagement/fileList.vue |   77 +
 src/views/basicData/contact/index.vue                        |  514 +++++++++++
 src/views/basicData/customerFileOpenSea/index.vue            |    4 
 src/api/basicData/contact.js                                 |   53 +
 src/components/filePreview/index.vue                         |    2 
 src/views/collaborativeApproval/journal/index.vue            |  703 +++++++++++++++
 10 files changed, 2,601 insertions(+), 1 deletions(-)

diff --git a/src/api/basicData/contact.js b/src/api/basicData/contact.js
new file mode 100644
index 0000000..d0ae294
--- /dev/null
+++ b/src/api/basicData/contact.js
@@ -0,0 +1,53 @@
+import request from '@/utils/request'
+
+// 鍒嗛〉鏌ヨ鑱旂郴浜哄垪琛�
+export function listContact(query) {
+  return request({
+    url: '/customerContact/listPage',
+    method: 'get',
+    params: query
+  })
+}
+
+// 鏍规嵁ID鏌ヨ鑱旂郴浜鸿鎯�
+export function getContactById(id) {
+  return request({
+    url: `/customerContact/getById/${id}`,
+    method: 'get'
+  })
+}
+
+// 鏂板鑱旂郴浜�
+export function addContact(data) {
+  return request({
+    url: '/customerContact/add',
+    method: 'post',
+    data: data
+  })
+}
+
+// 淇敼鑱旂郴浜�
+export function updateContact(data) {
+  return request({
+    url: '/customerContact/update',
+    method: 'put',
+    data: data
+  })
+}
+
+// 鍒犻櫎鑱旂郴浜�
+export function delContact(id) {
+  return request({
+    url: `/customerContact/delete/${id}`,
+    method: 'delete'
+  })
+}
+
+// 鎵归噺鍒犻櫎鑱旂郴浜�
+export function delContacts(ids) {
+  return request({
+    url: '/customerContact/delete',
+    method: 'delete',
+    data: ids
+  })
+}
diff --git a/src/api/collaborativeApproval/journal.js b/src/api/collaborativeApproval/journal.js
new file mode 100644
index 0000000..ac0bad3
--- /dev/null
+++ b/src/api/collaborativeApproval/journal.js
@@ -0,0 +1,62 @@
+import request from "@/utils/request";
+
+// 鏌ヨ鏃ュ織鍒楄〃
+export function listJournal(query) {
+  return request({
+    url: "/collaborativeApproval/journal/page",
+    method: "get",
+    params: query,
+  });
+}
+
+// 鏌ヨ鏃ュ織璇︾粏
+export function getJournal(journalId) {
+  return request({
+    url: "/collaborativeApproval/journal/" + journalId,
+    method: "get",
+  });
+}
+
+// 鏂板鏃ュ織
+export function addJournal(data) {
+  return request({
+    url: "/collaborativeApproval/journal/add",
+    method: "post",
+    data: data,
+  });
+}
+
+// 淇敼鏃ュ織
+export function updateJournal(data) {
+  return request({
+    url: "/collaborativeApproval/journal/update",
+    method: "put",
+    data: data,
+  });
+}
+
+// 鍒犻櫎鏃ュ織
+export function delJournal(ids) {
+  return request({
+    url: "/collaborativeApproval/journal/" + ids,
+    method: "delete",
+  });
+}
+
+// 鎺ㄩ�佹棩蹇�
+export function pushJournal(data) {
+  return request({
+    url: "/collaborativeApproval/journal/push",
+    method: "post",
+    data: data,
+  });
+}
+
+// 鑾峰彇鐢ㄦ埛鍒楄〃锛堢敤浜庢帹閫侀�夋嫨锛�
+export function listUser(query) {
+  return request({
+    url: "/system/user/list",
+    method: "get",
+    params: query,
+  });
+}
diff --git a/src/api/salesManagement/opportunityManagement.js b/src/api/salesManagement/opportunityManagement.js
new file mode 100644
index 0000000..0294c00
--- /dev/null
+++ b/src/api/salesManagement/opportunityManagement.js
@@ -0,0 +1,61 @@
+// 鍟嗘満绠$悊鎺ュ彛
+import request from "@/utils/request";
+
+// 鍒嗛〉鏌ヨ鍟嗘満鍒楄〃
+export function opportunityListPage(query) {
+  return request({
+    url: "/businessOpportunity/listPage",
+    method: "get",
+    params: query,
+  });
+}
+
+// 鏂板鍟嗘満
+export function addOpportunity(data) {
+  return request({
+    url: "/businessOpportunity/add",
+    method: "post",
+    data: data,
+  });
+}
+
+// 淇敼鍟嗘満
+export function updateOpportunity(data) {
+  return request({
+    url: "/businessOpportunity/update",
+    method: "put",
+    data: data,
+  });
+}
+// 娣诲姞鍟嗘満
+export function addDescription(data) {
+  return request({
+    url: "/businessOpportunity/addDescription",
+    method: "post",
+    data: data,
+  });
+}
+
+// 鍒犻櫎鍟嗘満
+export function delOpportunity(ids) {
+  return request({
+    url: "/businessOpportunity/delete",
+    method: "delete",
+    data: ids,
+  });
+}
+// 鏌ヨ鐪�
+export function getProvinceList() {
+  return request({
+    url: "/businessOpportunity/getProvinceList",
+    method: "get",
+  });
+}
+// 鏌ヨ甯�
+export function getCityList(id) {
+  return request({
+    url: "/businessOpportunity/getCityList",
+    method: "get",
+    params: id,
+  });
+}
diff --git a/src/components/filePreview/index.vue b/src/components/filePreview/index.vue
index cda5b56..ddc8429 100644
--- a/src/components/filePreview/index.vue
+++ b/src/components/filePreview/index.vue
@@ -164,7 +164,7 @@
 };
 
 const open = (url) => {
-  fileUrl.value = window.location.protocol+'//'+window.location.host+ url;
+  fileUrl.value = url;
   dialogVisible.value = true;
 };
 const handleClose = () => {
diff --git a/src/views/basicData/contact/index.vue b/src/views/basicData/contact/index.vue
new file mode 100644
index 0000000..07edce3
--- /dev/null
+++ b/src/views/basicData/contact/index.vue
@@ -0,0 +1,514 @@
+<template>
+  <div class="app-container">
+    <div class="search_form" style="margin-bottom: 20px;">
+      <div>
+        <span class="search_title">鑱旂郴浜哄鍚嶏細</span>
+        <el-input v-model="searchForm.contactPerson"
+                  style="width: 240px;margin-right: 10px"
+                  placeholder="璇疯緭鍏�"
+                  @keyup.enter="handleQuery"
+                  clearable
+                  :prefix-icon="Search" />
+        <span class="search_title">鑱旂郴鐢佃瘽锛�</span>
+        <el-input v-model="searchForm.contactPhone"
+                  style="width: 240px;margin-right: 10px"
+                  placeholder="璇疯緭鍏�"
+                  @keyup.enter="handleQuery"
+                  clearable />
+        <span class="search_title">鎵�灞炲鎴凤細</span>
+        <el-select v-model="searchForm.customerId"
+                   placeholder="璇烽�夋嫨"
+                   style="width: 240px"
+                   clearable
+                   filterable
+                   @change="handleQuery">
+          <el-option v-for="item in customerList"
+                     :key="item.id"
+                     :label="item.customerName"
+                     :value="item.id" />
+        </el-select>
+        <el-button type="primary"
+                   @click="handleQuery"
+                   style="margin-left: 10px">鎼滅储</el-button>
+        <el-button @click="resetSearch">閲嶇疆</el-button>
+      </div>
+      <div>
+        <el-button type="primary"
+                   @click="openForm('add')">鏂板鑱旂郴浜�</el-button>
+        <el-button type="danger"
+                   plain
+                   @click="handleDelete">鍒犻櫎</el-button>
+      </div>
+    </div>
+    <div class="table_list">
+      <PIMTable rowKey="id"
+                :column="tableColumn"
+                :tableData="tableData"
+                :page="page"
+                :isSelection="true"
+                @selection-change="handleSelectionChange"
+                :tableLoading="tableLoading"
+                @pagination="pagination">
+        <template #customerName="{ row }">
+          <div class="customer-tags">
+            <el-tag v-for="(name, index) in formatCustomerNames(row.customerNames)"
+                    :key="index"
+                    size="small"
+                    type="info"
+                    class="customer-tag">
+              {{ name }}
+            </el-tag>
+          </div>
+        </template>
+      </PIMTable>
+    </div>
+
+    <!-- 鑱旂郴浜鸿〃鍗曞璇濇 -->
+    <FormDialog v-model="dialogFormVisible"
+                :title="dialogTitle"
+                :operation-type="operationType"
+                width="600px"
+                @close="closeDia"
+                @confirm="submitForm"
+                @cancel="closeDia">
+      <el-form :model="form"
+               label-width="100px"
+               :rules="rules"
+               ref="formRef">
+        <el-form-item label="鑱旂郴浜猴細"
+                      prop="contactPerson">
+          <el-input v-model="form.contactPerson"
+                    placeholder="璇疯緭鍏ヨ仈绯讳汉濮撳悕"
+                    clearable />
+        </el-form-item>
+        <el-form-item label="鑱旂郴鐢佃瘽锛�"
+                      prop="contactPhone">
+          <el-input v-model="form.contactPhone"
+                    placeholder="璇疯緭鍏ヨ仈绯荤數璇�"
+                    clearable />
+        </el-form-item>
+        <el-form-item label="鎵�灞炲鎴凤細"
+                      prop="customerIdList">
+          <el-select v-model="form.customerIdList"
+                     placeholder="璇烽�夋嫨鎵�灞炲鎴凤紙鍙閫夛級"
+                     style="width: 100%"
+                     filterable
+                     clearable
+                     multiple>
+            <el-option v-for="item in customerList"
+                       :key="item.id"
+                       :label="item.customerName"
+                       :value="item.id" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="澶囨敞锛�"
+                      prop="remark">
+          <el-input v-model="form.remark"
+                    type="textarea"
+                    :rows="3"
+                    placeholder="璇疯緭鍏ュ娉�"
+                    clearable />
+        </el-form-item>
+      </el-form>
+    </FormDialog>
+
+    <!-- 瀹㈡埛璇︽儏瀵硅瘽妗� -->
+    <FormDialog v-model="customerDetailVisible"
+                title="缁戝畾瀹㈡埛璇︽儏"
+                operation-type="detail"
+                width="700px"
+                @close="closeCustomerDetail"
+                @cancel="closeCustomerDetail">
+      <div class="customer-detail">
+        <el-table :data="currentCustomerList"
+                  border
+                  style="width: 100%">
+          <el-table-column prop="customerName"
+                           label="瀹㈡埛鍚嶇О"
+                           min-width="150" />
+          <el-table-column prop="customerType"
+                           label="瀹㈡埛鍒嗙被"
+                           width="120" />
+          <el-table-column prop="companyPhone"
+                           label="鍏徃鐢佃瘽"
+                           width="150" />
+          <el-table-column prop="companyAddress"
+                           label="鍏徃鍦板潃"
+                           min-width="200"
+                           show-overflow-tooltip />
+        </el-table>
+      </div>
+    </FormDialog>
+  </div>
+</template>
+
+<script setup>
+import { onMounted, ref, reactive, getCurrentInstance, computed } from "vue";
+import { Search } from "@element-plus/icons-vue";
+import { listContact, getContactById, addContact, updateContact, delContacts } from "@/api/basicData/contact.js";
+import { listCustomer } from "@/api/basicData/customer.js";
+import { ElMessageBox } from "element-plus";
+import FormDialog from "@/components/Dialog/FormDialog.vue";
+
+const { proxy } = getCurrentInstance();
+
+// 瀵硅瘽妗嗘爣棰�
+const dialogTitle = computed(() => {
+  return operationType.value === "add" ? "鏂板鑱旂郴浜�" : "缂栬緫鑱旂郴浜�";
+});
+
+// 琛ㄦ牸鍒楀畾涔�
+const tableColumn = ref([
+  {
+    label: "鑱旂郴浜哄鍚�",
+    prop: "contactPerson",
+  },
+  {
+    label: "鑱旂郴鐢佃瘽",
+    prop: "contactPhone",
+  },
+  {
+    label: "鎵�灞炲鎴�",
+    prop: "customerNames",
+    dataType: "slot",
+    slot: "customerName",
+  },
+  {
+    label: "澶囨敞",
+    prop: "remark",
+    showOverflowTooltip: true,
+  },
+  {
+    dataType: "action",
+    label: "鎿嶄綔",
+    align: "center",
+    fixed: "right",
+    width: 200,
+    operation: [
+      {
+        name: "缂栬緫",
+        type: "text",
+        clickFun: row => {
+          openForm("edit", row);
+        },
+      },
+      {
+        name: "鏌ョ湅瀹㈡埛",
+        type: "text",
+        clickFun: row => {
+          viewCustomerDetail(row);
+        },
+      },
+      {
+        name: "鍒犻櫎",
+        type: "text",
+        style: "color: #f56c6c",
+        clickFun: row => {
+          handleDeleteRow(row);
+        },
+      },
+    ],
+  },
+]);
+
+const tableData = ref([]);
+const selectedRows = ref([]);
+const customerList = ref([]);
+const customerMap = ref(new Map());
+const tableLoading = ref(false);
+const page = reactive({
+  current: 1,
+  size: 10,
+  total: 0,
+});
+
+// 鎼滅储琛ㄥ崟
+const searchForm = reactive({
+  contactPerson: "",
+  contactPhone: "",
+  customerId: "",
+});
+
+// 瀵硅瘽妗嗙浉鍏�
+const operationType = ref("");
+const dialogFormVisible = ref(false);
+const formRef = ref();
+
+// 琛ㄥ崟鏁版嵁
+const form = reactive({
+  id: null,
+  contactPerson: "",
+  contactPhone: "",
+  customerIdList: [],
+  remark: "",
+});
+
+// 琛ㄥ崟楠岃瘉瑙勫垯
+const rules = {
+  contactPerson: [{ required: true, message: "璇疯緭鍏ヨ仈绯讳汉濮撳悕", trigger: "blur" }],
+  contactPhone: [{ required: true, message: "璇疯緭鍏ヨ仈绯荤數璇�", trigger: "blur" }],
+  customerIdList: [{ required: true, message: "璇烽�夋嫨鎵�灞炲鎴�", trigger: "change", type: "array" }],
+};
+
+// 瀹㈡埛璇︽儏鐩稿叧
+const customerDetailVisible = ref(false);
+const currentCustomerList = ref([]);
+
+// 鑾峰彇瀹㈡埛鍒楄〃锛堢敤浜庝笅鎷夐�夋嫨锛�
+const getCustomerList = () => {
+  listCustomer({ current: -1, size: -1 }).then(res => {
+    if (res.data && res.data.records) {
+      customerList.value = res.data.records;
+      // 鏋勫缓瀹㈡埛ID鍒板鎴蜂俊鎭殑鏄犲皠
+      customerMap.value = new Map();
+      res.data.records.forEach(item => {
+        customerMap.value.set(item.id, item);
+      });
+    }
+  });
+};
+
+// 鏍煎紡鍖栧鎴峰悕绉帮紙澶勭悊瀛楃涓叉垨鏁扮粍鏍煎紡锛�
+const formatCustomerNames = (customerNames) => {
+  if (!customerNames) return [];
+  if (Array.isArray(customerNames)) return customerNames;
+  if (typeof customerNames === 'string') {
+    return customerNames.split(',').map(s => s.trim()).filter(s => s);
+  }
+  return [];
+};
+
+// 閲嶇疆鎼滅储琛ㄥ崟
+const resetSearch = () => {
+  searchForm.contactPerson = "";
+  searchForm.contactPhone = "";
+  searchForm.customerId = "";
+  handleQuery();
+};
+
+// 鏌ヨ鍒楄〃
+const handleQuery = () => {
+  page.current = 1;
+  getList();
+};
+
+const pagination = obj => {
+  page.current = obj.page;
+  page.size = obj.limit;
+  getList();
+};
+
+// 鑾峰彇鍒楄〃鏁版嵁
+const getList = () => {
+  tableLoading.value = true;
+  const params = {
+    ...searchForm,
+    current: page.current,
+    size: page.size
+  };
+  listContact(params).then(res => {
+    tableLoading.value = false;
+    if (res.data) {
+      tableData.value = res.data.records || [];
+      page.total = res.data.total || 0;
+    }
+  }).catch(() => {
+    tableLoading.value = false;
+  });
+};
+
+// 琛ㄦ牸閫夋嫨鏁版嵁
+const handleSelectionChange = selection => {
+  selectedRows.value = selection;
+};
+
+// 鎵撳紑琛ㄥ崟瀵硅瘽妗�
+const openForm = (type, row) => {
+  operationType.value = type;
+  resetForm();
+  if (type === "edit" && row) {
+    form.id = row.id;
+    form.contactPerson = row.contactPerson;
+    form.contactPhone = row.contactPhone;
+    form.customerIdList = row.customerId ? row.customerId.toString().split(',').map(id => Number(id.trim())) : [];
+    form.remark = row.remark || "";
+  }
+  dialogFormVisible.value = true;
+};
+
+// 閲嶇疆琛ㄥ崟
+const resetForm = () => {
+  form.id = null;
+  form.contactPerson = "";
+  form.contactPhone = "";
+  form.customerIdList = [];
+  form.remark = "";
+  if (formRef.value) {
+    formRef.value.resetFields();
+  }
+};
+
+// 鍏抽棴瀵硅瘽妗�
+const closeDia = () => {
+  dialogFormVisible.value = false;
+  resetForm();
+};
+
+// 鎻愪氦琛ㄥ崟
+const submitForm = () => {
+  formRef.value.validate(valid => {
+    if (valid) {
+      if (operationType.value === "edit") {
+        submitEdit();
+      } else {
+        submitAdd();
+      }
+    }
+  });
+};
+
+// 鎻愪氦鏂板
+const submitAdd = () => {
+  const submitData = {
+    contactPerson: form.contactPerson,
+    contactPhone: form.contactPhone,
+    customerIdList: form.customerIdList,
+    remark: form.remark,
+  };
+  
+  addContact(submitData).then(res => {
+    if (res.code === 200) {
+      proxy.$modal.msgSuccess("娣诲姞鎴愬姛");
+      closeDia();
+      getList();
+    }
+  });
+};
+
+// 鎻愪氦缂栬緫
+const submitEdit = () => {
+  const submitData = {
+    id: form.id,
+    contactPerson: form.contactPerson,
+    contactPhone: form.contactPhone,
+    customerIdList: form.customerIdList,
+    remark: form.remark,
+  };
+  
+  updateContact(submitData).then(res => {
+    if (res.code === 200) {
+      proxy.$modal.msgSuccess("淇敼鎴愬姛");
+      closeDia();
+      getList();
+    }
+  });
+};
+
+// 鍒犻櫎鍗曡
+const handleDeleteRow = (row) => {
+  ElMessageBox.confirm(
+    `纭畾瑕佸垹闄よ仈绯讳汉"${row.contactPerson}"鍚楋紵`,
+    "鎻愮ず",
+    {
+      confirmButtonText: "纭畾",
+      cancelButtonText: "鍙栨秷",
+      type: "warning",
+    }
+  ).then(() => {
+    delContacts([row.id]).then(res => {
+      if (res.code === 200) {
+        proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+        getList();
+      }
+    });
+  });
+};
+
+// 鎵归噺鍒犻櫎
+const handleDelete = () => {
+  if (selectedRows.value.length === 0) {
+    proxy.$modal.msgWarning("璇烽�夋嫨瑕佸垹闄ょ殑鏁版嵁");
+    return;
+  }
+  
+  ElMessageBox.confirm(
+    `纭畾瑕佸垹闄ら�変腑鐨� ${selectedRows.value.length} 涓仈绯讳汉鍚楋紵`,
+    "鎻愮ず",
+    {
+      confirmButtonText: "纭畾",
+      cancelButtonText: "鍙栨秷",
+      type: "warning",
+    }
+  ).then(() => {
+    const ids = selectedRows.value.map(row => row.id);
+    delContacts(ids).then(res => {
+      if (res.code === 200) {
+        proxy.$modal.msgSuccess("鎵归噺鍒犻櫎鎴愬姛");
+        getList();
+      }
+    });
+  });
+};
+
+// 鏌ョ湅瀹㈡埛璇︽儏
+const viewCustomerDetail = (row) => {
+  const customerIdStr = row.customerId;
+  if (!customerIdStr) {
+    proxy.$modal.msgError("璇ヨ仈绯讳汉鏈粦瀹氬鎴�");
+    return;
+  }
+  // 鑾峰彇鎵�鏈夌粦瀹氱殑瀹㈡埛
+  const customerIds = customerIdStr.toString().split(',').map(id => Number(id.trim()));
+  const customers = customerIds.map(id => customerMap.value.get(id)).filter(item => item);
+  if (customers.length > 0) {
+    currentCustomerList.value = customers;
+    customerDetailVisible.value = true;
+  } else {
+    proxy.$modal.msgError("瀹㈡埛淇℃伅涓嶅瓨鍦�");
+  }
+};
+
+// 鍏抽棴瀹㈡埛璇︽儏
+const closeCustomerDetail = () => {
+  customerDetailVisible.value = false;
+  currentCustomerList.value = [];
+};
+
+onMounted(() => {
+  getList();
+  getCustomerList();
+});
+</script>
+
+<style scoped>
+.search_form {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.search_title {
+  font-size: 14px;
+  color: #606266;
+  margin-right: 5px;
+}
+
+.customer-detail {
+  padding: 10px;
+}
+
+:deep(.el-descriptions__label) {
+  width: 120px;
+  font-weight: bold;
+}
+
+.customer-tags {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 5px;
+}
+
+.customer-tag {
+  margin-right: 0;
+}
+</style>
diff --git a/src/views/basicData/customerFile/index.vue b/src/views/basicData/customerFile/index.vue
index a080bd9..104a4ff 100644
--- a/src/views/basicData/customerFile/index.vue
+++ b/src/views/basicData/customerFile/index.vue
@@ -133,6 +133,7 @@
             </el-form-item>
           </el-col>
         </el-row>
+        <!-- 鑱旂郴浜哄姛鑳藉凡杩佺Щ鍒拌仈绯讳汉绠$悊椤甸潰
         <el-row :gutter="30"
                 v-for="(contact, index) in formYYs.contactList"
                 :key="index">
@@ -165,6 +166,7 @@
         </el-row>
         <el-button @click="addNewContact"
                    style="margin-bottom: 10px;">+ 鏂板鑱旂郴浜�</el-button>
+        -->
         <el-row :gutter="30">
           <el-col :span="12">
             <el-form-item label="缁存姢浜猴細"
@@ -430,6 +432,7 @@
               </div>
             </el-col>
           </el-row>
+          <!-- 鑱旂郴浜轰俊鎭凡杩佺Щ鍒拌仈绯讳汉绠$悊椤甸潰
           <el-row :gutter="20">
             <el-col :span="12">
               <div class="info-item">
@@ -444,6 +447,7 @@
               </div>
             </el-col>
           </el-row>
+          -->
           <el-row :gutter="20">
             <el-col :span="12">
               <div class="info-item">
diff --git a/src/views/basicData/customerFileOpenSea/index.vue b/src/views/basicData/customerFileOpenSea/index.vue
index 2598f48..89262a6 100644
--- a/src/views/basicData/customerFileOpenSea/index.vue
+++ b/src/views/basicData/customerFileOpenSea/index.vue
@@ -133,6 +133,7 @@
             </el-form-item>
           </el-col>
         </el-row>
+        <!-- 鑱旂郴浜哄姛鑳藉凡杩佺Щ鍒拌仈绯讳汉绠$悊椤甸潰
         <el-row :gutter="30"
                 v-for="(contact, index) in formYYs.contactList"
                 :key="index">
@@ -165,6 +166,7 @@
         </el-row>
         <el-button @click="addNewContact"
                    style="margin-bottom: 10px;">+ 鏂板鑱旂郴浜�</el-button>
+        -->
         <el-row :gutter="30">
           <el-col :span="12">
             <el-form-item label="缁存姢浜猴細"
@@ -499,6 +501,7 @@
               </div>
             </el-col>
           </el-row>
+          <!-- 鑱旂郴浜轰俊鎭凡杩佺Щ鍒拌仈绯讳汉绠$悊椤甸潰
           <el-row :gutter="20">
             <el-col :span="12">
               <div class="info-item">
@@ -513,6 +516,7 @@
               </div>
             </el-col>
           </el-row>
+          -->
           <el-row :gutter="20">
             <el-col :span="12">
               <div class="info-item">
diff --git a/src/views/collaborativeApproval/journal/index.vue b/src/views/collaborativeApproval/journal/index.vue
new file mode 100644
index 0000000..ff65c41
--- /dev/null
+++ b/src/views/collaborativeApproval/journal/index.vue
@@ -0,0 +1,703 @@
+<template>
+  <div class="app-container">
+    <!-- 鎼滅储琛ㄥ崟 -->
+    <div class="search_form">
+      <el-form :model="searchForm" inline>
+        <el-form-item label="鏃ュ織鏍囬">
+          <el-input v-model="searchForm.title" placeholder="璇疯緭鍏ユ棩蹇楁爣棰�" clearable />
+        </el-form-item>
+        <el-form-item label="鍒涘缓浜�">
+          <el-input v-model="searchForm.createUserName" placeholder="璇疯緭鍏ュ垱寤轰汉" clearable />
+        </el-form-item>
+        <el-form-item label="鍒涘缓鏃堕棿">
+          <el-date-picker
+            v-model="searchForm.dateRange"
+            type="daterange"
+            range-separator="鑷�"
+            start-placeholder="寮�濮嬫棩鏈�"
+            end-placeholder="缁撴潫鏃ユ湡"
+            value-format="YYYY-MM-DD"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="handleQuery">鎼滅储</el-button>
+          <el-button @click="resetQuery">閲嶇疆</el-button>
+        </el-form-item>
+      </el-form>
+      <div>
+        <el-button type="primary" @click="openForm('add')">缂栧啓鏃ュ織</el-button>
+      </div>
+    </div>
+
+    <!-- 鏃ュ織绠$悊Tab -->
+    <div class="journal-board">
+      <el-tabs v-model="activeJournalType" @tab-change="handleTabChange">
+        <el-tab-pane label="鏃ユ姤" name="daily">
+          <template #label>
+            <span>
+              <el-icon><Calendar /></el-icon>
+              鏃ユ姤
+              <span class="tab-count" v-if="journalCount.daily > 0">({{ journalCount.daily }})</span>
+            </span>
+          </template>
+        </el-tab-pane>
+        <el-tab-pane label="鍛ㄦ姤" name="weekly">
+          <template #label>
+            <span>
+              <el-icon><Document /></el-icon>
+              鍛ㄦ姤
+              <span class="tab-count" v-if="journalCount.weekly > 0">({{ journalCount.weekly }})</span>
+            </span>
+          </template>
+        </el-tab-pane>
+        <el-tab-pane label="鏈堟姤" name="monthly">
+          <template #label>
+            <span>
+              <el-icon><DataAnalysis /></el-icon>
+              鏈堟姤
+              <span class="tab-count" v-if="journalCount.monthly > 0">({{ journalCount.monthly }})</span>
+            </span>
+          </template>
+        </el-tab-pane>
+      </el-tabs>
+
+      <!-- 鏃ュ織琛ㄦ牸 -->
+      <el-table :data="journalList" v-loading="loading" border style="width: 100%">
+        <el-table-column type="index" label="搴忓彿" width="60" align="center" />
+        <el-table-column prop="title" label="鏃ュ織鏍囬" min-width="200" show-overflow-tooltip>
+          <template #default="scope">
+            <el-link type="primary" @click="handleView(scope.row)">{{ scope.row.title }}</el-link>
+          </template>
+        </el-table-column>
+        <el-table-column prop="type" label="鏃ュ織绫诲瀷" width="100" align="center">
+          <template #default="scope">
+            <el-tag :type="getTypeTagType(scope.row.type)">{{ getTypeText(scope.row.type) }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="content" label="鏃ュ織鍐呭" min-width="300" show-overflow-tooltip />
+        <el-table-column prop="createUserName" label="鍒涘缓浜�" width="120" align="center" />
+        <el-table-column prop="createTime" label="鍒涘缓鏃堕棿" width="160" align="center" />
+        <el-table-column prop="pushStatus" label="鎺ㄩ�佺姸鎬�" width="100" align="center">
+          <template #default="scope">
+            <el-tag :type="scope.row.pushStatus === 1 ? 'success' : 'info'">
+              {{ scope.row.pushStatus === 1 ? '宸叉帹閫�' : '鏈帹閫�' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="鎿嶄綔" width="250" align="center" fixed="right">
+          <template #default="scope">
+            <el-button link type="primary" @click="handleView(scope.row)">鏌ョ湅</el-button>
+            <el-button link type="primary" @click="handleEdit(scope.row)">缂栬緫</el-button>
+            <el-button link type="success" @click="handlePush(scope.row)" v-if="scope.row.pushStatus !== 1">鎺ㄩ��</el-button>
+            <el-button link type="danger" @click="handleDelete(scope.row.id)">鍒犻櫎</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 鍒嗛〉 -->
+      <pagination
+        v-show="total > 0"
+        :total="total"
+        v-model:page="queryParams.pageNum"
+        v-model:limit="queryParams.pageSize"
+        @pagination="getList"
+      />
+
+      <!-- 绌虹姸鎬� -->
+      <div class="empty-state" v-if="journalList.length === 0 && !loading">
+        <el-empty description="鏆傛棤鏃ュ織鏁版嵁" />
+      </div>
+    </div>
+
+    <!-- 缂栧啓/缂栬緫鏃ュ織瀵硅瘽妗� -->
+    <el-dialog
+      :title="dialogTitle"
+      v-model="dialogVisible"
+      width="900px"
+      append-to-body
+      @close="resetForm"
+    >
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
+        <el-row>
+          <el-col :span="12">
+            <el-form-item label="鏃ュ織绫诲瀷" prop="type">
+              <el-select v-model="form.type" placeholder="璇烽�夋嫨鏃ュ織绫诲瀷" style="width: 100%">
+                <el-option label="鏃ユ姤" value="daily" />
+                <el-option label="鍛ㄦ姤" value="weekly" />
+                <el-option label="鏈堟姤" value="monthly" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="鏃ュ織鏍囬" prop="title">
+              <el-input v-model="form.title" placeholder="璇疯緭鍏ユ棩蹇楁爣棰�" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="24">
+            <el-form-item label="鏃ュ織鍐呭" prop="content">
+              <el-input
+                v-model="form.content"
+                type="textarea"
+                :rows="10"
+                placeholder="璇疯緭鍏ユ棩蹇楀唴瀹�"
+                maxlength="2000"
+                show-word-limit
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row>
+          <el-col :span="24">
+            <el-form-item label="澶囨敞">
+              <el-input
+                v-model="form.remark"
+                type="textarea"
+                :rows="3"
+                placeholder="璇疯緭鍏ュ娉ㄤ俊鎭�"
+                maxlength="500"
+                show-word-limit
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitForm">淇� 瀛�</el-button>
+          <el-button type="success" @click="submitAndPushForm">淇濆瓨骞舵帹閫�</el-button>
+          <el-button @click="dialogVisible = false">鍙� 娑�</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 鏌ョ湅鏃ュ織瀵硅瘽妗� -->
+    <el-dialog
+      title="鏌ョ湅鏃ュ織"
+      v-model="viewDialogVisible"
+      width="800px"
+      append-to-body
+    >
+      <div class="journal-detail">
+        <div class="detail-header">
+          <h3 class="detail-title">{{ currentJournal.title }}</h3>
+          <div class="detail-meta">
+            <el-tag :type="getTypeTagType(currentJournal.type)">{{ getTypeText(currentJournal.type) }}</el-tag>
+            <el-tag :type="currentJournal.pushStatus === 1 ? 'success' : 'info'">
+              {{ currentJournal.pushStatus === 1 ? '宸叉帹閫�' : '鏈帹閫�' }}
+            </el-tag>
+          </div>
+        </div>
+        <div class="detail-info">
+          <span><el-icon><User /></el-icon> 鍒涘缓浜猴細{{ currentJournal.createUserName }}</span>
+          <span><el-icon><Timer /></el-icon> 鍒涘缓鏃堕棿锛歿{ currentJournal.createTime }}</span>
+        </div>
+        <el-divider />
+        <div class="detail-content">
+          <div class="content-label">鏃ュ織鍐呭锛�</div>
+          <div class="content-text">{{ currentJournal.content }}</div>
+        </div>
+        <div class="detail-remark" v-if="currentJournal.remark">
+          <div class="content-label">澶囨敞锛�</div>
+          <div class="content-text">{{ currentJournal.remark }}</div>
+        </div>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="handleEditFromView" v-if="currentJournal.pushStatus !== 1">缂� 杈�</el-button>
+          <el-button type="success" @click="handlePushFromView" v-if="currentJournal.pushStatus !== 1">鎺� 閫�</el-button>
+          <el-button @click="viewDialogVisible = false">鍏� 闂�</el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 鎺ㄩ�佹棩蹇楀璇濇 -->
+    <el-dialog
+      title="鎺ㄩ�佹棩蹇�"
+      v-model="pushDialogVisible"
+      width="600px"
+      append-to-body
+    >
+      <div class="push-info">
+        <p><strong>鏃ュ織鏍囬锛�</strong>{{ currentJournal.title }}</p>
+        <p><strong>鏃ュ織绫诲瀷锛�</strong>{{ getTypeText(currentJournal.type) }}</p>
+      </div>
+      <el-form ref="pushFormRef" :model="pushForm" :rules="pushRules" label-width="100px">
+        <el-form-item label="鎺ㄩ�佷汉鍛�" prop="userIds">
+          <el-select
+            v-model="pushForm.userIds"
+            multiple
+            filterable
+            remote
+            reserve-keyword
+            placeholder="璇疯緭鍏ョ敤鎴峰悕鎼滅储"
+            :remote-method="remoteSearchUser"
+            :loading="userLoading"
+            style="width: 100%"
+          >
+            <el-option
+              v-for="item in userOptions"
+              :key="item.userId"
+              :label="item.userName"
+              :value="item.userId"
+            >
+              <span>{{ item.userName }}</span>
+              <span style="float: right; color: #8492a6; font-size: 13px">{{ item.deptName }}</span>
+            </el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="鎺ㄩ�佸娉�">
+          <el-input
+            v-model="pushForm.remark"
+            type="textarea"
+            :rows="3"
+            placeholder="璇疯緭鍏ユ帹閫佸娉ㄤ俊鎭�"
+          />
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button type="primary" @click="submitPush">纭� 瀹�</el-button>
+          <el-button @click="pushDialogVisible = false">鍙� 娑�</el-button>
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { Calendar, Document, DataAnalysis, User, Timer } from "@element-plus/icons-vue";
+import { onMounted, ref, reactive, toRefs } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import useUserStore from "@/store/modules/user";
+import {
+  listJournal,
+  getJournal,
+  addJournal,
+  updateJournal,
+  delJournal,
+  pushJournal,
+  listUser
+} from "@/api/collaborativeApproval/journal.js";
+import Pagination from "@/components/Pagination/index.vue";
+
+const userStore = useUserStore();
+
+// 鍝嶅簲寮忔暟鎹�
+const data = reactive({
+  searchForm: {
+    title: "",
+    createUserName: "",
+    dateRange: []
+  },
+  form: {
+    id: undefined,
+    type: "daily",
+    title: "",
+    content: "",
+    remark: "",
+    pushStatus: 0
+  },
+  rules: {
+    type: [
+      { required: true, message: "璇烽�夋嫨鏃ュ織绫诲瀷", trigger: "change" }
+    ],
+    title: [
+      { required: true, message: "鏃ュ織鏍囬涓嶈兘涓虹┖", trigger: "blur" }
+    ],
+    content: [
+      { required: true, message: "鏃ュ織鍐呭涓嶈兘涓虹┖", trigger: "blur" }
+    ]
+  }
+});
+
+const { searchForm, form, rules } = toRefs(data);
+
+// 椤甸潰鐘舵��
+const loading = ref(false);
+const journalList = ref([]);
+const total = ref(0);
+const activeJournalType = ref("daily");
+const dialogVisible = ref(false);
+const dialogTitle = ref("");
+const formRef = ref();
+const viewDialogVisible = ref(false);
+const pushDialogVisible = ref(false);
+const pushFormRef = ref();
+const currentJournal = ref({});
+const userLoading = ref(false);
+const userOptions = ref([]);
+
+// 鏌ヨ鍙傛暟
+const queryParams = reactive({
+  pageNum: 1,
+  pageSize: 10
+});
+
+// 鎺ㄩ�佽〃鍗�
+const pushForm = reactive({
+  journalId: undefined,
+  userIds: [],
+  remark: ""
+});
+
+const pushRules = {
+  userIds: [
+    { required: true, message: "璇烽�夋嫨鎺ㄩ�佷汉鍛�", trigger: "change", type: "array" }
+  ]
+};
+
+// 鏃ュ織鏁伴噺缁熻
+const journalCount = reactive({
+  daily: 0,
+  weekly: 0,
+  monthly: 0
+});
+
+// 鑾峰彇鏃ュ織鍒楄〃
+const getList = () => {
+  loading.value = true;
+  const params = {
+    pageNum: queryParams.pageNum,
+    pageSize: queryParams.pageSize,
+    type: activeJournalType.value
+  };
+  
+  if (searchForm.value.title) {
+    params.title = searchForm.value.title;
+  }
+  if (searchForm.value.createUserName) {
+    params.createUserName = searchForm.value.createUserName;
+  }
+  if (searchForm.value.dateRange && searchForm.value.dateRange.length === 2) {
+    params.startTime = searchForm.value.dateRange[0];
+    params.endTime = searchForm.value.dateRange[1];
+  }
+  
+  listJournal(params).then(res => {
+    if (res.code === 200) {
+      journalList.value = res.data.records || [];
+      total.value = res.data.total || 0;
+      // 鏇存柊褰撳墠绫诲瀷鐨勬暟閲�
+      journalCount[activeJournalType.value] = res.data.total || 0;
+    }
+    loading.value = false;
+  }).catch(() => {
+    loading.value = false;
+  });
+};
+
+// 鑾峰彇鎵�鏈夌被鍨嬫暟閲�
+const getAllTypeCounts = () => {
+  ['daily', 'weekly', 'monthly'].forEach(type => {
+    listJournal({ pageNum: 1, pageSize: 1, type }).then(res => {
+      if (res.code === 200) {
+        journalCount[type] = res.data.total || 0;
+      }
+    });
+  });
+};
+
+// Tab鍒囨崲
+const handleTabChange = (tabName) => {
+  activeJournalType.value = tabName;
+  queryParams.pageNum = 1;
+  getList();
+};
+
+// 鎼滅储
+const handleQuery = () => {
+  queryParams.pageNum = 1;
+  getList();
+};
+
+// 閲嶇疆鎼滅储
+const resetQuery = () => {
+  searchForm.value = {
+    title: "",
+    createUserName: "",
+    dateRange: []
+  };
+  handleQuery();
+};
+
+// 鑾峰彇绫诲瀷鏂囨湰
+const getTypeText = (type) => {
+  const typeMap = { daily: "鏃ユ姤", weekly: "鍛ㄦ姤", monthly: "鏈堟姤" };
+  return typeMap[type] || type;
+};
+
+// 鑾峰彇绫诲瀷鏍囩鏍峰紡
+const getTypeTagType = (type) => {
+  const typeMap = { daily: "primary", weekly: "success", monthly: "warning" };
+  return typeMap[type] || "";
+};
+
+// 鎵撳紑琛ㄥ崟
+const openForm = (type) => {
+  if (type === 'add') {
+    dialogTitle.value = "缂栧啓鏃ュ織";
+    form.value = {
+      id: undefined,
+      type: activeJournalType.value,
+      title: "",
+      content: "",
+      remark: "",
+      pushStatus: 0
+    };
+  }
+  dialogVisible.value = true;
+};
+
+// 缂栬緫鏃ュ織
+const handleEdit = (row) => {
+  if (row.pushStatus === 1) {
+    ElMessage.warning("宸叉帹閫佺殑鏃ュ織涓嶅彲缂栬緫");
+    return;
+  }
+  dialogTitle.value = "缂栬緫鏃ュ織";
+  form.value = { ...row };
+  dialogVisible.value = true;
+};
+
+// 浠庢煡鐪嬮〉闈㈢紪杈�
+const handleEditFromView = () => {
+  viewDialogVisible.value = false;
+  handleEdit(currentJournal.value);
+};
+
+// 鏌ョ湅鏃ュ織
+const handleView = (row) => {
+  getJournal(row.id).then(res => {
+    if (res.code === 200) {
+      currentJournal.value = res.data;
+      viewDialogVisible.value = true;
+    }
+  });
+};
+
+// 鍒犻櫎鏃ュ織
+const handleDelete = (id) => {
+  ElMessageBox.confirm(
+    "纭鍒犻櫎杩欐潯鏃ュ織鍚楋紵",
+    "鎻愮ず",
+    {
+      confirmButtonText: "纭畾",
+      cancelButtonText: "鍙栨秷",
+      type: "warning"
+    }
+  ).then(() => {
+    delJournal(id).then(res => {
+      if (res.code === 200) {
+        ElMessage.success("鍒犻櫎鎴愬姛");
+        getList();
+        getAllTypeCounts();
+      }
+    });
+  });
+};
+
+// 鎵撳紑鎺ㄩ�佸脊绐�
+const handlePush = (row) => {
+  currentJournal.value = row;
+  pushForm.journalId = row.id;
+  pushForm.userIds = [];
+  pushForm.remark = "";
+  pushDialogVisible.value = true;
+  // 鍔犺浇鐢ㄦ埛鍒楄〃
+  loadUserOptions();
+};
+
+// 浠庢煡鐪嬮〉闈㈡帹閫�
+const handlePushFromView = () => {
+  viewDialogVisible.value = false;
+  handlePush(currentJournal.value);
+};
+
+// 鍔犺浇鐢ㄦ埛閫夐」
+const loadUserOptions = () => {
+  listUser({ pageNum: 1, pageSize: 100 }).then(res => {
+    if (res.code === 200) {
+      userOptions.value = res.data.records || res.data || [];
+    }
+  });
+};
+
+// 杩滅▼鎼滅储鐢ㄦ埛
+const remoteSearchUser = (query) => {
+  if (query) {
+    userLoading.value = true;
+    listUser({ userName: query, pageNum: 1, pageSize: 50 }).then(res => {
+      if (res.code === 200) {
+        userOptions.value = res.data.records || res.data || [];
+      }
+      userLoading.value = false;
+    }).catch(() => {
+      userLoading.value = false;
+    });
+  }
+};
+
+// 鎻愪氦琛ㄥ崟
+const submitForm = () => {
+  formRef.value.validate((valid) => {
+    if (valid) {
+      const api = form.value.id ? updateJournal : addJournal;
+      api(form.value).then(res => {
+        if (res.code === 200) {
+          ElMessage.success(form.value.id ? "淇敼鎴愬姛" : "鏂板鎴愬姛");
+          dialogVisible.value = false;
+          getList();
+          getAllTypeCounts();
+        }
+      });
+    }
+  });
+};
+
+// 鎻愪氦骞舵帹閫�
+const submitAndPushForm = () => {
+  formRef.value.validate((valid) => {
+    if (valid) {
+      const api = form.value.id ? updateJournal : addJournal;
+      api(form.value).then(res => {
+        if (res.code === 200) {
+          const journalId = form.value.id || res.data;
+          dialogVisible.value = false;
+          // 鎵撳紑鎺ㄩ�佸脊绐�
+          currentJournal.value = { ...form.value, id: journalId };
+          pushForm.journalId = journalId;
+          pushForm.userIds = [];
+          pushForm.remark = "";
+          pushDialogVisible.value = true;
+          loadUserOptions();
+          getList();
+          getAllTypeCounts();
+        }
+      });
+    }
+  });
+};
+
+// 鎻愪氦鎺ㄩ��
+const submitPush = () => {
+  pushFormRef.value.validate((valid) => {
+    if (valid) {
+      pushJournal(pushForm).then(res => {
+        if (res.code === 200) {
+          ElMessage.success("鎺ㄩ�佹垚鍔�");
+          pushDialogVisible.value = false;
+          getList();
+          getAllTypeCounts();
+        }
+      });
+    }
+  });
+};
+
+// 閲嶇疆琛ㄥ崟
+const resetForm = () => {
+  formRef.value?.resetFields();
+};
+
+onMounted(() => {
+  getList();
+  getAllTypeCounts();
+});
+</script>
+
+<style scoped>
+.search_form {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+}
+
+.journal-board {
+  background: #fff;
+  padding: 20px;
+  border-radius: 4px;
+}
+
+.tab-count {
+  color: #f56c6c;
+  font-size: 12px;
+}
+
+.empty-state {
+  padding: 40px 0;
+}
+
+.journal-detail {
+  padding: 20px;
+}
+
+.detail-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 16px;
+}
+
+.detail-title {
+  margin: 0;
+  font-size: 20px;
+  color: #303133;
+}
+
+.detail-meta {
+  display: flex;
+  gap: 8px;
+}
+
+.detail-info {
+  display: flex;
+  gap: 24px;
+  color: #606266;
+  font-size: 14px;
+}
+
+.detail-info span {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+
+.detail-content,
+.detail-remark {
+  margin-top: 16px;
+}
+
+.content-label {
+  font-weight: bold;
+  color: #303133;
+  margin-bottom: 8px;
+}
+
+.content-text {
+  color: #606266;
+  line-height: 1.8;
+  white-space: pre-wrap;
+  word-wrap: break-word;
+  background: #f5f7fa;
+  padding: 16px;
+  border-radius: 4px;
+  min-height: 60px;
+}
+
+.push-info {
+  padding: 0 20px 20px;
+  color: #606266;
+}
+
+.push-info p {
+  margin: 8px 0;
+}
+
+.dialog-footer {
+  display: flex;
+  justify-content: flex-end;
+  gap: 10px;
+}
+</style>
diff --git a/src/views/salesManagement/opportunityManagement/fileList.vue b/src/views/salesManagement/opportunityManagement/fileList.vue
new file mode 100644
index 0000000..6ed631a
--- /dev/null
+++ b/src/views/salesManagement/opportunityManagement/fileList.vue
@@ -0,0 +1,77 @@
+<template>
+  <el-dialog v-model="dialogVisible" title="闄勪欢" width="40%" :before-close="handleClose" draggable>
+    <el-table :data="tableData" border height="40vh">
+      <el-table-column label="闄勪欢鍚嶇О" prop="originalFilename" min-width="400" show-overflow-tooltip />
+      <el-table-column fixed="right" label="鎿嶄綔" width="150" align="center">
+        <template #default="scope">
+          <el-button link type="primary" size="small" @click="downLoadFile(scope.row)">涓嬭浇</el-button>
+          <el-button link type="primary" size="small" @click="lookFile(scope.row)">棰勮</el-button>
+          <el-button link type="danger" size="small" @click="handleDelete(scope.row)">鍒犻櫎</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+  </el-dialog>
+  <filePreview ref="filePreviewRef" />
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import filePreview from '@/components/filePreview/index.vue'
+import { ElMessageBox, ElMessage } from 'element-plus'
+import { deleteAttachment } from '@/api/basicData/storageAttachment.js'
+
+const dialogVisible = ref(false)
+const tableData = ref([])
+const currentRowId = ref(null)
+const { proxy } = getCurrentInstance();
+const filePreviewRef = ref()
+const emit = defineEmits(['refresh'])
+
+const handleClose = () => {
+  dialogVisible.value = false
+}
+
+const open = (list, rowId) => {
+  dialogVisible.value = true
+  tableData.value = list || []
+  currentRowId.value = rowId
+}
+
+const downLoadFile = (row) => {
+  proxy.$download.name(row.url);
+}
+
+const lookFile = (row) => {
+  filePreviewRef.value.open(row.url)
+}
+
+// 鍒犻櫎闄勪欢
+const handleDelete = (row) => {
+  ElMessageBox.confirm(`纭鍒犻櫎闄勪欢"${row.originalFilename}"鍚楋紵`, '鎻愮ず', {
+    confirmButtonText: '纭畾',
+    cancelButtonText: '鍙栨秷',
+    type: 'warning'
+  }).then(() => {
+    deleteAttachment([row.storageAttachmentId]).then(() => {
+      ElMessage.success('鍒犻櫎鎴愬姛')
+      // 浠庡垪琛ㄤ腑绉婚櫎宸插垹闄ょ殑闄勪欢
+      const index = tableData.value.findIndex(item => item.id === row.id)
+      if (index !== -1) {
+        tableData.value.splice(index, 1)
+      }
+      // 瑙﹀彂鍒锋柊浜嬩欢
+      emit('refresh', currentRowId.value)
+    }).catch(() => {
+      ElMessage.error('鍒犻櫎澶辫触')
+    })
+  }).catch(() => {
+    ElMessage.info('宸插彇娑堝垹闄�')
+  })
+}
+
+defineExpose({
+  open
+})
+</script>
+
+<style scoped></style>
diff --git a/src/views/salesManagement/opportunityManagement/index.vue b/src/views/salesManagement/opportunityManagement/index.vue
new file mode 100644
index 0000000..0e4ecd4
--- /dev/null
+++ b/src/views/salesManagement/opportunityManagement/index.vue
@@ -0,0 +1,1122 @@
+<template>
+  <div class="app-container">
+    <!-- 鎼滅储鍖哄煙 -->
+    <div class="search_form">
+      <el-form :model="searchForm" :inline="true">
+        <el-form-item label="瀹㈡埛鍚嶇О">
+          <el-input
+            v-model="searchForm.customerName"
+            placeholder="璇疯緭鍏ュ鎴峰悕绉�"
+            clearable
+            prefix-icon="Search"
+            style="width: 200px;"
+            @change="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item label="鍩庡競">
+          <el-input
+              v-model="searchForm.city"
+              placeholder="璇疯緭鍏ュ煄甯傚悕绉�"
+              clearable
+              prefix-icon="Search"
+              style="width: 200px"
+              @change="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item label="鐘舵��">
+          <el-select
+            v-model="searchForm.status"
+            placeholder="璇烽�夋嫨鐘舵��"
+            clearable
+            style="width: 160px"
+            @change="handleQuery"
+          >
+            <el-option
+              v-for="item in statusOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="褰曞叆浜�">
+          <el-select
+            v-model="searchForm.entryPerson"
+            placeholder="璇烽�夋嫨褰曞叆浜�"
+            clearable
+            filterable
+            style="width: 200px"
+            @change="handleQuery"
+          >
+            <el-option
+              v-for="item in userList"
+              :key="item.nickName"
+              :label="item.nickName"
+              :value="item.nickName"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="褰曞叆鏃ユ湡锛�">
+          <el-date-picker v-model="searchForm.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" type="daterange"
+            placeholder="璇烽�夋嫨" clearable @change="changeDaterange" />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="handleQuery">鎼滅储</el-button>
+          <el-button @click="resetQuery">閲嶇疆</el-button>
+          <el-button type="primary" @click="handleAdd">鏂板缓</el-button>
+			<el-button type="danger" plain @click="handleDelete">鍒犻櫎</el-button>
+        </el-form-item>
+      </el-form>
+    </div>
+    <!-- 琛ㄦ牸鍖哄煙 -->
+    <div class="table_list">
+      <el-table
+        :data="tableData"
+        border
+        v-loading="tableLoading"
+        @selection-change="handleSelectionChange"
+        :row-key="(row) => row.id"
+        height="calc(100vh - 18.5em)"
+        stripe
+        show-summary
+        :summary-method="contractAmountSummaryMethod"
+      >
+        <el-table-column align="center" type="selection" width="55" fixed="left"/>
+        <el-table-column align="center" label="搴忓彿" type="index" width="60" />
+        <el-table-column label="鐘舵��" prop="status" width="120">
+          <template #default="{ row }">
+            <el-tag
+              :type="getStatusTagType(row.status)"
+              effect="light"
+            >
+              {{ getStatusText(row.status) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="鐪佷唤" prop="province" show-overflow-tooltip />
+        <el-table-column label="甯�" prop="city" show-overflow-tooltip/>
+        <el-table-column label="瀹㈡埛鍚嶇О" prop="customerName" show-overflow-tooltip />
+        <el-table-column label="琛屼笟" prop="industry" show-overflow-tooltip />
+        <el-table-column label="鍟嗘満鏉ユ簮" prop="businessSource" show-overflow-tooltip />
+        <el-table-column label="绛剧害閲戦" prop="contractAmount" show-overflow-tooltip width="150" />
+        <!-- <el-table-column label="瀹㈡埛鎻忚堪" prop="description" show-overflow-tooltip min-width="200" /> -->
+        <el-table-column label="褰曞叆浜�" prop="entryPerson" show-overflow-tooltip width="120" />
+        <el-table-column label="鏇存柊鏃ユ湡" prop="updateTime" width="120">
+          <template #default="{ row }">
+            {{ formatDate(row.updateTime) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="鎿嶄綔" fixed="right" width="240" align="center">
+          <template #default="{ row }">
+            <el-button
+              link
+              type="primary"
+              size="small"
+              @click="handleEdit(row)"
+            >
+              缂栬緫
+            </el-button>
+            <el-button
+              link
+              type="primary"
+              size="small"
+              @click="handleAddOperation(row)"
+            >
+              娣诲姞鎷滆璁板綍
+            </el-button>
+            <el-button
+              link
+              type="primary"
+              size="small"
+              @click="handleDetail(row)"
+            >
+              璇︽儏
+            </el-button>
+            <el-button
+              link
+              type="primary"
+              size="small"
+              @click="handleAttachment(row)"
+            >
+              闄勪欢
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 鍒嗛〉缁勪欢 -->
+      <pagination
+        v-show="total > 0"
+        :total="total"
+        layout="total, sizes, prev, pager, next, jumper"
+        :page="page.current"
+        :limit="page.size"
+        @pagination="paginationChange"
+      />
+    </div>
+
+    <!-- 鏂板/缂栬緫瀵硅瘽妗� -->
+    <el-dialog
+      v-model="dialogFormVisible"
+      :title="operationType === 'add' ? '鏂板缓鍟嗘満' : operationType === 'edit' ? '缂栬緫鍟嗘満' : operationType === 'addOperation' ? '娣诲姞鍟嗘満' : '鍟嗘満璇︽儏'"
+      width="1000px"
+      @close="closeDialog"
+    >
+      <el-form
+        :model="form"
+        :rules="rules"
+        ref="formRef"
+        label-width="100px"
+        label-position="left"
+      >
+        <el-form-item label="鐘舵��" prop="status">
+          <el-select v-model="form.status" placeholder="璇烽�夋嫨鐘舵��" style="width: 100%" :disabled="operationType === 'detail'">
+            <el-option
+              v-for="item in statusOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </el-form-item>
+        
+        <el-form-item label="鐪佷唤" prop="province">
+          <el-select v-model="form.province" filterable placeholder="璇烽�夋嫨鐪佷唤"
+										 @change="getCityListChange"
+										 style="width: 100%" :disabled="operationType === 'detail' || operationType === 'addOperation'">
+            <el-option
+              v-for="item in provinceOptions"
+              :key="item.id"
+              :label="item.name"
+              :value="item.id"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="甯�" prop="city">
+          <el-select v-model="form.city" filterable placeholder="璇烽�夋嫨甯�"
+										 style="width: 100%" :disabled="operationType === 'detail' || operationType === 'addOperation'">
+            <el-option
+              v-for="item in cityOptions"
+              :key="item.id"
+              :label="item.name"
+              :value="item.id"
+            />
+          </el-select>
+        </el-form-item>
+        
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="瀹㈡埛鍚嶇О" prop="customerName">
+              <el-select
+                v-model="form.customerName"
+                placeholder="璇烽�夋嫨"
+                clearable
+                style="width: 100%"
+                :disabled="operationType === 'detail' || operationType === 'addOperation'"
+              >
+                <el-option
+                  v-for="item in customerOption"
+                  :key="item.customerName"
+                  :label="item.customerName"
+                  :value="item.customerName"
+                >
+                  {{
+                    item.customerName + "鈥斺��" + item.taxpayerIdentificationNumber
+                  }}
+                </el-option>
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="鍟嗘満鏉ユ簮" prop="businessSource">
+              <el-input
+                v-model="form.businessSource"
+                placeholder="璇疯緭鍏ュ晢鏈烘潵婧�"
+                clearable
+                :disabled="operationType === 'detail' || operationType === 'addOperation'"
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="琛屼笟">
+              <el-input v-model="form.industry" placeholder="璇疯緭鍏ヨ涓�" clearable :disabled="operationType === 'detail' || operationType === 'addOperation'" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="涓昏惀浜у搧">
+              <el-input v-model="form.mainProducts" placeholder="璇疯緭鍏ヤ富钀ヤ骇鍝�" clearable :disabled="operationType === 'detail' || operationType === 'addOperation'" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="涓昏惀涓氬姟鏀跺叆">
+              <el-input v-model="form.mainBusinessRevenue" placeholder="璇疯緭鍏ヤ富钀ヤ笟鍔℃敹鍏�" clearable :disabled="operationType === 'detail' || operationType === 'addOperation'" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="瀹㈡埛瑙勬ā">
+              <el-input v-model="form.customerScale" placeholder="璇疯緭鍏ュ鎴疯妯�" clearable :disabled="operationType === 'detail' || operationType === 'addOperation'" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        
+        <el-row :gutter="20">
+          <el-col :span="24">
+            <el-form-item label="淇℃伅鍖栫幇鐘�">
+              <el-input v-model="form.informationState" placeholder="璇疯緭鍏ヤ俊鎭寲鐜扮姸" clearable :disabled="operationType === 'detail' || operationType === 'addOperation'" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        
+        <el-form-item label="绛剧害閲戦" prop="contractAmount">
+          <el-input
+            v-model="form.contractAmount"
+            placeholder="璇疯緭鍏ョ绾﹂噾棰�"
+            clearable
+            :disabled="operationType === 'detail' || operationType === 'addOperation'"
+          >
+            <template #append>鍏�</template>
+          </el-input>
+        </el-form-item>
+        
+        <el-form-item label="鎷滆璁板綍" prop="description">
+          <el-input
+            v-model="form.description"
+            type="textarea"
+            :autosize="{ minRows: 4, maxRows: 10 }"
+            placeholder="璇疯緭鍏ユ嫓璁胯褰�"
+            show-word-limit
+            :disabled="operationType === 'detail'"
+          />
+        </el-form-item>
+
+        <el-form-item label="鏀归�犲唴瀹�" prop="renContent">
+          <el-input
+            v-model="form.renContent"
+            type="textarea"
+            :autosize="{ minRows: 3, maxRows: 8 }"
+            :placeholder="renovationPlaceholder"
+            show-word-limit
+            :disabled="operationType === 'detail' || operationType === 'addOperation'"
+          />
+        </el-form-item>
+
+        <el-form-item label="浠樻鎻忚堪" prop="paymentDescription">
+          <el-input
+            v-model="form.paymentDescription"
+            type="textarea"
+            :autosize="{ minRows: 3, maxRows: 10 }"
+            placeholder="鏄惁鍨祫锛熶紒涓氭槸鍚﹀紑绁紵浼佷笟鏄惁鍒嗚ˉ璐存垨棰濆鍑洪挶锛�"
+            show-word-limit
+            :disabled="operationType === 'detail' || operationType === 'addOperation'"
+          />
+        </el-form-item>
+        
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="褰曞叆浜�" prop="entryPerson">
+              <el-select v-model="form.entryPerson" placeholder="璇烽�夋嫨" clearable @change="changs" :disabled="operationType === 'detail' || operationType === 'addOperation'">
+                <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName" :value="item.nickName" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="12"> 
+             <el-form-item label="褰曞叆鏃ユ湡" prop="entryDate"> 
+               <el-date-picker style="width: 100%" v-model="form.entryDate" value-format="YYYY-MM-DD" format="YYYY-MM-DD" 
+                 type="date" placeholder="璇烽�夋嫨" clearable :disabled="operationType === 'detail' || operationType === 'addOperation'" /> 
+             </el-form-item> 
+           </el-col>
+        </el-row>
+        
+        <!-- 闄勪欢涓婁紶锛堥潪璇︽儏妯″紡涓嬫樉绀猴級 -->
+        <el-row :gutter="30" v-if="operationType !== 'detail'">
+          <el-col :span="24">
+            <el-form-item label="闄勪欢鏉愭枡锛�">
+              <FileUpload v-model:file-list="fileList" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+      
+      <!-- 闄勪欢鏌ョ湅锛堜粎鍦ㄨ鎯呮ā寮忎笅鏄剧ず锛� -->
+      <div v-if="operationType === 'detail'" class="attachment-section">
+        <el-divider content-position="left">闄勪欢鏉愭枡</el-divider>
+        <div v-if="form.businessCommonFiles && form.businessCommonFiles.length > 0">
+          <el-table :data="form.businessCommonFiles" border stripe style="width: 100%">
+            <el-table-column label="闄勪欢鍚嶇О" prop="originalFilename" min-width="400" show-overflow-tooltip />
+            <el-table-column fixed="right" label="鎿嶄綔" width="150" align="center">
+              <template #default="scope">
+                <el-button link type="primary" size="small" @click="downloadAttachment(scope.row)">涓嬭浇</el-button>
+                <el-button link type="primary" size="small" @click="previewAttachment(scope.row)">棰勮</el-button>
+              </template>
+            </el-table-column>
+          </el-table>
+        </div>
+        <div v-else style="text-align: center; padding: 20px; color: #999;">
+          鏆傛棤闄勪欢
+        </div>
+      </div>
+      
+      <!-- 鍙樻洿璁板綍鏃堕棿绾匡紙浠呭湪璇︽儏妯″紡涓嬫樉绀猴級 -->
+      <div v-if="operationType === 'detail'" class="change-history-section">
+        <el-divider content-position="left">鍙樻洿璁板綍</el-divider>
+        <el-timeline>
+          <el-timeline-item
+            v-for="record in changeHistory"
+            :key="record.id"
+            :timestamp="record.timestamp"
+            :type="record.type === 'current' ? 'primary' : record.type === 'update' ? 'success' : 'info'"
+            :hollow="record.type === 'current'"
+            placement="top"
+          >
+            <el-card shadow="hover" class="timeline-card">
+              <template #header>
+                <div class="card-header">
+                  <span class="action-type">{{ record.action }}</span>
+                  <span class="operator">鎿嶄綔浜猴細{{ record.operator }}</span>
+                </div>
+              </template>
+              <div class="change-content">
+                <div class="status-change" v-if="record.status">
+                  <span class="label">鐘舵�侊細</span>
+                  <el-tag :type="record.type === 'current' ? 'primary' : 'info'" size="small">
+                    {{ getStatusLabel(record.status) }}
+                  </el-tag>
+                </div>
+                <div class="description-change" v-if="record.description">
+                  <span class="label">鎷滆璁板綍锛�</span>
+                  <span class="description-text">{{ record.description }}</span>
+                </div>
+              </div>
+            </el-card>
+          </el-timeline-item>
+        </el-timeline>
+      </div>
+      
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button @click="closeDialog">鍙栨秷</el-button>
+          <el-button type="primary" @click="submitForm" v-if="operationType !== 'detail'">
+            纭畾
+          </el-button>
+        </div>
+      </template>
+    </el-dialog>
+
+    <!-- 闄勪欢鍒楄〃瀵硅瘽妗� -->
+    <FileList ref="fileListRef" @refresh="handleFileListRefresh" />
+    <!-- 鏂囦欢棰勮缁勪欢 -->
+    <filePreview ref="filePreviewRef" />
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, getCurrentInstance } from 'vue'
+import { Search } from '@element-plus/icons-vue'
+import { ElMessageBox, ElMessage } from 'element-plus'
+import pagination from '@/components/PIMTable/Pagination.vue'
+import useUserStore from '@/store/modules/user'
+import dayjs from 'dayjs'
+
+import {
+	opportunityListPage,
+	addOpportunity,
+	updateOpportunity,
+	delOpportunity,
+	addDescription, getProvinceList, getCityList
+} from '@/api/salesManagement/opportunityManagement.js'
+import { userListNoPage } from '@/api/system/user.js'
+import {customerList, getSalesLedgerWithProducts} from '@/api/salesManagement/salesLedger.js'
+import FileList from './fileList.vue'
+import filePreview from '@/components/filePreview/index.vue'
+
+const { proxy } = getCurrentInstance()
+const userStore = useUserStore()
+
+// 琛ㄦ牸鏁版嵁
+const tableData = ref([])
+const selectedRows = ref([])
+const tableLoading = ref(false)
+const userList = ref([])
+const customerOption = ref([])
+const DEFAULT_USER_QUERY = { postCode: 'Market_Sales' }
+const DEFAULT_CUSTOMER_QUERY = { customerType: 2 }
+let userListPromise = null
+let customerListPromise = null
+
+const loadUserList = async (query = DEFAULT_USER_QUERY) => {
+  if (userListPromise) return userListPromise
+  userListPromise = (async () => {
+    try {
+      const res = await userListNoPage(query)
+      userList.value = res?.data || []
+      return userList.value
+    } catch (err) {
+      console.error('鑾峰彇鐢ㄦ埛鍒楄〃澶辫触:', err)
+      userList.value = []
+      userListPromise = null
+      throw err
+    }
+  })()
+  return userListPromise
+}
+
+const loadCustomerList = async (query = DEFAULT_CUSTOMER_QUERY) => {
+  if (customerListPromise) return customerListPromise
+  customerListPromise = (async () => {
+    try {
+      const res = await customerList(query)
+      customerOption.value = res || []
+      return customerOption.value
+    } catch (err) {
+      console.error('鑾峰彇瀹㈡埛鍒楄〃澶辫触:', err)
+      customerOption.value = []
+      customerListPromise = null
+      throw err
+    }
+  })()
+  return customerListPromise
+}
+
+// 鍒嗛〉閰嶇疆
+const page = reactive({
+  current: 1,
+  size: 100,
+})
+const total = ref(0)
+
+// 鎼滅储琛ㄥ崟
+const searchForm = reactive({
+  customerName: '',
+  city: '',
+  status: '',
+  entryPerson: '',
+  entryDate: [],
+  entryDateStart: '',
+  entryDateEnd: ''
+})
+
+// 瀵硅瘽妗嗙浉鍏�
+const dialogFormVisible = ref(false)
+const operationType = ref('') // add, detail
+const formRef = ref()
+const form = reactive({
+  id: undefined,
+  status: undefined,
+  province: '',
+	city: '',
+  customerName: '',
+  industry: '',
+  informationState: '',
+  mainBusinessRevenue: '',
+  customerScale: '',
+  mainProducts: '',
+  businessSource: '',
+  contractAmount: '',
+  description: '',
+  renContent: '',
+  paymentDescription: '',
+  entryPerson: userStore.nickName,
+  entryDate: dayjs().format('YYYY-MM-DD')
+})
+
+const renovationPlaceholder = '1.鏍囧噯鍖栵細\n2.瀹氬埗鍖栵細\n3.澶栭噰锛�'
+
+// 鍙樻洿璁板綍鏁版嵁锛堟ā鎷熸暟鎹級
+const changeHistory = ref([])
+
+// 鏂囦欢鍒楄〃
+const fileList = ref([])
+
+// FileList缁勪欢寮曠敤
+const fileListRef = ref(null)
+const currentAttachmentRow = ref(null)
+const filePreviewRef = ref(null)
+
+// 鑾峰彇鐘舵�佹爣绛�
+const getStatusLabel = (statusValue) => {
+  const status = statusOptions.find(item => item.value === statusValue)
+  return status ? status.label : statusValue
+}
+
+// 琛ㄥ崟楠岃瘉瑙勫垯
+const rules = reactive({
+  customerName: [
+    { required: true, message: '璇烽�夋嫨瀹㈡埛', trigger: 'change' }
+  ],
+  status: [
+    { required: true, message: '璇烽�夋嫨鐘舵��', trigger: 'change' }
+  ],
+  entryPerson: [
+    { required: true, message: '璇烽�夋嫨褰曞叆浜�', trigger: 'change' }
+  ],
+  entryDate: [
+    { required: true, message: '璇烽�夋嫨褰曞叆鏃ユ湡', trigger: 'change' }
+  ]
+})
+
+// 鐘舵�侀�夐」
+const statusOptions = [
+  { value: '鏂板缓', label: '鏂板缓' },
+  { value: '椤圭洰璺熻釜', label: '椤圭洰璺熻釜' },
+  { value: '鏀惧純', label: '鏀惧純' },
+  { value: '鍚堝悓绛剧害', label: '鍚堝悓绛剧害' },
+  { value: '澶囨鐢虫姤', label: '澶囨鐢虫姤' },
+  { value: '椤圭洰浜や粯', label: '椤圭洰浜や粯' },
+  { value: '椤圭洰楠屾敹', label: '椤圭洰楠屾敹' },
+  { value: '椤圭洰鍥炴', label: '椤圭洰鍥炴' },
+  { value: '鍥炶ˉ璐�', label: '鍥炶ˉ璐�' }
+]
+
+// 鐪佷唤閫夐」
+const provinceOptions = ref([])
+const cityOptions = ref([])
+
+// 鑾峰彇鐘舵�佹爣绛剧被鍨� 
+const getStatusTagType = (status) => { 
+  const typeMap = { 
+    '鏂板缓': 'info', 
+    '椤圭洰璺熻釜': 'primary', 
+    '鏀惧純': 'danger',
+    '鍚堝悓绛剧害': 'warning', 
+    '澶囨鐢虫姤': 'primary',
+    '椤圭洰浜や粯': 'success',
+    '椤圭洰楠屾敹': 'success',
+    '椤圭洰鍥炴': 'success',
+    '鍥炶ˉ璐�': 'success'
+  } 
+  return typeMap[status] || 'info' 
+} 
+
+// 鑾峰彇鐘舵�佹枃鏈� 
+const getStatusText = (status) => { 
+  const textMap = { 
+    '鏂板缓': '鏂板缓', 
+    '椤圭洰璺熻釜': '椤圭洰璺熻釜', 
+    '鏀惧純': '鏀惧純',
+    '鍚堝悓绛剧害': '鍚堝悓绛剧害', 
+    '澶囨鐢虫姤': '澶囨鐢虫姤',
+    '椤圭洰浜や粯': '椤圭洰浜や粯',
+    '椤圭洰楠屾敹': '椤圭洰楠屾敹',
+    '椤圭洰鍥炴': '椤圭洰鍥炴',
+    '鍥炶ˉ璐�': '鍥炶ˉ璐�'
+  } 
+  return textMap[status] || '鏈煡' 
+}
+
+// 鏍煎紡鍖栨棩鏈�
+const formatDate = (date) => {
+  if (!date) return ''
+  return dayjs(date).format('YYYY-MM-DD')
+}
+
+// 鏌ヨ鍒楄〃
+const handleQuery = () => {
+  page.current = 1
+  getList()
+}
+
+// 閲嶇疆鏌ヨ
+const resetQuery = () => {
+  Object.assign(searchForm, {
+    customerName: '',
+    city: '',
+    status: '',
+    entryPerson: '',
+    entryDate: [],
+    entryDateStart: '',
+    entryDateEnd: ''
+  })
+  handleQuery()
+}
+
+// 鏃ユ湡鑼冨洿鍙樺寲
+const changeDaterange = (val) => {
+  if (val && val.length === 2) {
+    searchForm.entryDateStart = val[0]
+    searchForm.entryDateEnd = val[1]
+  } else {
+    searchForm.entryDateStart = ''
+    searchForm.entryDateEnd = ''
+  }
+  handleQuery()
+}
+
+// 鑾峰彇鍒楄〃鏁版嵁
+const getList = () => {
+  tableLoading.value = true
+  
+  // 鍒涘缓鏌ヨ鍙傛暟锛屾帓闄ntryDate瀛楁锛屽彧浣跨敤entryDateStart鍜宔ntryDateEnd
+  const { entryDate, ...queryParams } = searchForm
+  const params = {
+    ...queryParams,
+    ...page
+  }
+  
+  // 鍒犻櫎绌哄�煎弬鏁�
+  Object.keys(params).forEach(key => {
+    if (params[key] === '' || params[key] === null || params[key] === undefined) {
+      delete params[key]
+    }
+  })
+  
+  opportunityListPage(params).then(res => {
+    tableData.value = res.data.records || []
+    total.value = res.data.total || 0
+  }).catch(err => {
+    console.error('鑾峰彇鍟嗘満鍒楄〃澶辫触:', err)
+    tableData.value = []
+    total.value = 0
+  }).finally(() => {
+    tableLoading.value = false
+  })
+}
+
+// 绛剧害閲戦鍚堣锛堝鐢ㄥ叏灞� summarizeTable锛�
+const contractAmountSummaryMethod = (param) => {
+  return proxy.summarizeTable(param, ["contractAmount"]);
+}
+
+// 鍒嗛〉鍙樺寲
+const paginationChange = (pagination) => {
+  page.current = pagination.page
+  page.size = pagination.limit
+  getList()
+}
+
+// 閫夋嫨鍙樺寲
+const handleSelectionChange = (selection) => {
+  selectedRows.value = selection
+}
+
+// 鏂板缓鍟嗘満
+const handleAdd = async () => {
+  operationType.value = 'add'
+  resetForm()
+  
+  // 鍔犺浇鐢ㄦ埛鍒楄〃鍜屽鎴峰垪琛�
+  await loadUserList()
+  await loadCustomerList()
+	getProvinceList().then(res => {
+		provinceOptions.value = res.data
+	})
+  
+  dialogFormVisible.value = true
+}
+const getCityListChange = (id) => {
+	getCityList({provinceId: id}).then(res => {
+		cityOptions.value = res.data
+	})
+}
+
+// 娣诲姞鎿嶄綔
+const handleAddOperation = async (row) => {
+  operationType.value = 'addOperation'
+
+  // 娓呯┖闄勪欢鍒楄〃
+  fileList.value = []
+
+  // 鍔犺浇鐢ㄦ埛鍒楄〃鍜屽鎴峰垪琛�
+  await loadUserList()
+  await loadCustomerList()
+
+  // 浣跨敤褰撳墠琛屾暟鎹綔涓哄熀纭�锛屼絾鍙兘淇敼鐘舵�佸拰鎷滆璁板綍锛涗粯娆炬弿杩般�佹敼閫犲唴瀹圭瓑淇濈暀鍙嶆樉
+  Object.assign(form, row, {
+    // 淇濈暀鍘熷鍟嗘満ID锛岀敤浜庡叧鑱旀搷浣滆褰�
+    status: row.status,
+    description: '', // 娓呯┖鎷滆璁板綍锛屽厑璁搁噸鏂板~鍐�
+    entryPerson: userStore.nickName, // 璁剧疆褰曞叆浜轰负褰撳墠璐﹀彿
+    entryDate: dayjs().format('YYYY-MM-DD') // 璁剧疆褰曞叆鏃堕棿涓哄綋澶�
+  })
+  dialogFormVisible.value = true
+}
+
+// 鏌ョ湅璇︽儏
+const handleDetail = async (row) => {
+  operationType.value = 'detail'
+  
+  // 鍔犺浇鐢ㄦ埛鍒楄〃鍜屽鎴峰垪琛�
+  await loadUserList()
+  await loadCustomerList()
+  
+  // 浣跨敤updateTime浣滀负褰曞叆鏃堕棿鍙嶆樉
+  Object.assign(form, row, {
+    entryDateStart: row.updateTime || row.entryDateStart
+  })
+  
+  // 鐢熸垚妯℃嫙鍙樻洿璁板綍
+  generateChangeHistory(row)
+  dialogFormVisible.value = true
+}
+
+// 鐢熸垚鍙樻洿璁板綍
+const generateChangeHistory = (row) => {
+  // 浣跨敤businessDescription鏁扮粍鏁版嵁鐢熸垚鍙樻洿璁板綍
+  const history = []
+  
+  if (row.businessDescription && Array.isArray(row.businessDescription)) {
+    row.businessDescription.forEach((item, index) => {
+      history.push({
+        id: item.id || index,
+        timestamp: item.entryDate || item.updateTime || item.createTime,
+        operator: item.entryPerson || '绯荤粺',
+        status: item.status,
+        description: item.description,
+        type: index === 0 ? 'current' : 'info',
+        action: index === 0 ? '褰撳墠鐘舵��' : '鍘嗗彶璁板綍'
+      })
+    })
+  }
+  
+  changeHistory.value = history
+}
+
+// 缂栬緫鍟嗘満
+const handleEdit = async (row) => {
+  operationType.value = 'edit'
+  
+  // 鍔犺浇鐢ㄦ埛鍒楄〃鍜屽鎴峰垪琛�
+  await loadUserList()
+  await loadCustomerList()
+  
+  // 鍔犺浇鐪佷唤鍒楄〃
+  await getProvinceList().then(res => {
+    provinceOptions.value = res.data
+  })
+  
+  // 濡傛灉鍚庣杩斿洖鐨勬槸name锛岄渶瑕佽浆鎹负id
+  let provinceId = row.province
+  let cityId = row.city
+  
+  // 濡傛灉province鏄痭ame瀛楃涓诧紝鏌ユ壘瀵瑰簲鐨刬d
+  if (row.province && typeof row.province === 'string' && !/^\d+$/.test(row.province)) {
+    const provinceOption = provinceOptions.value.find(item => item.name === row.province)
+    if (provinceOption) {
+      provinceId = provinceOption.id
+      // 鍔犺浇瀵瑰簲鐨勫煄甯傚垪琛�
+      await getCityList({ provinceId: provinceId }).then(res => {
+        cityOptions.value = res.data
+        // 濡傛灉city鏄痭ame瀛楃涓诧紝鏌ユ壘瀵瑰簲鐨刬d
+        if (row.city && typeof row.city === 'string' && !/^\d+$/.test(row.city)) {
+          const cityOption = cityOptions.value.find(item => item.name === row.city)
+          if (cityOption) {
+            cityId = cityOption.id
+          }
+        }
+      })
+    }
+  } else if (row.province) {
+    // 濡傛灉province鏄痠d锛岀洿鎺ュ姞杞藉煄甯傚垪琛�
+    await getCityList({ id: row.province }).then(res => {
+      cityOptions.value = res.data
+    })
+  }
+  
+  // 浣跨敤褰撳墠璐﹀彿鍜屽綋澶╂棩鏈熶綔涓洪粯璁ゅ��
+  Object.assign(form, row, {
+    province: provinceId, // 浣跨敤杞崲鍚庣殑id
+    city: cityId, // 浣跨敤杞崲鍚庣殑id
+    entryPerson: userStore.nickName, // 璁剧疆褰曞叆浜轰负褰撳墠璐﹀彿
+    entryDate: dayjs().format('YYYY-MM-DD') // 璁剧疆褰曞叆鏃堕棿涓哄綋澶�
+  })
+  dialogFormVisible.value = true
+}
+
+// 褰曞叆浜哄彉鍖栧鐞�
+const changs = (value) => {
+  // 鍙互鏍规嵁闇�瑕佹坊鍔犲鐞嗛�昏緫
+}
+
+// 鎻愪氦琛ㄥ崟
+const submitForm = () => {
+  formRef.value.validate(valid => {
+    if (valid) {
+      // 鏀堕泦闄勪欢鏂囦欢鍒楄〃
+      const businessCommonFiles = fileList.value || []
+      
+      // 灏嗙渷浠藉拰甯傜殑id杞崲涓簄ame
+      const provinceName = form.province ? provinceOptions.value.find(item => item.id === form.province)?.name || form.province : ''
+      const cityName = form.city ? cityOptions.value.find(item => item.id === form.city)?.name || form.city : ''
+      
+      let api
+      let successMessage
+      let submitData
+      
+      if (operationType.value === 'add') {
+        api = addOpportunity
+        successMessage = '鏂板缓鎴愬姛'
+        submitData = {
+          ...form,
+          province: provinceName, // 浼爊ame鑰屼笉鏄痠d
+          city: cityName, // 浼爊ame鑰屼笉鏄痠d
+          businessCommonFiles: businessCommonFiles,
+          type: 9  // 鍟嗘満绠$悊鐨勭被鍨嬫爣璇�
+        }
+      } else if (operationType.value === 'addOperation') {
+        api = addDescription
+        successMessage = '娣诲姞鎿嶄綔鎴愬姛'
+        // 娣诲姞鎿嶄綔鏃朵紶閫掔姸鎬併�佹弿杩般�佸綍鍏ヤ汉銆佸綍鍏ユ棩鏈熴�侀檮浠跺拰鍟嗘満ID
+        submitData = {
+          status: form.status,
+          description: form.description,
+          paymentDescription: form.paymentDescription,
+          entryPerson: form.entryPerson,
+          entryDate: form.entryDate,
+          businessCommonFiles: businessCommonFiles,
+          type: 9,  // 鍟嗘満绠$悊鐨勭被鍨嬫爣璇�
+          businessOpportunityId: form.id  // 浼犻�掑晢鏈篒D
+        }
+      } else {
+        api = updateOpportunity
+        successMessage = '淇敼鎴愬姛'
+        submitData = {
+          ...form,
+          province: provinceName, // 浼爊ame鑰屼笉鏄痠d
+          city: cityName, // 浼爊ame鑰屼笉鏄痠d
+          businessCommonFiles: businessCommonFiles,
+          type: 9  // 鍟嗘満绠$悊鐨勭被鍨嬫爣璇�
+        }
+      }
+      
+      api(submitData).then(res => {
+        if (res.code === 200) {
+          proxy.$modal.msgSuccess(successMessage)
+          closeDialog()
+          getList()
+        } else {
+          proxy.$modal.msgError(res.msg || '鎿嶄綔澶辫触')
+        }
+      }).catch(err => {
+        console.log(err);
+      })
+    }
+  })
+}
+
+// 鍒犻櫎鍟嗘満
+const handleDelete = () => {
+  if (selectedRows.value.length === 0) {
+    proxy.$modal.msgWarning('璇烽�夋嫨瑕佸垹闄ょ殑鍟嗘満')
+    return
+  }
+  
+  ElMessageBox.confirm('纭畾鍒犻櫎閫変腑鐨勫晢鏈哄悧锛�', '鎻愮ず', {
+    confirmButtonText: '纭畾',
+    cancelButtonText: '鍙栨秷',
+    type: 'warning'
+  }).then(() => {
+    const ids = selectedRows.value.map(item => item.id)
+    delOpportunity(ids).then(res => {
+      if (res.code === 200) {
+        proxy.$modal.msgSuccess('鍒犻櫎鎴愬姛')
+        getList()
+      } else {
+        proxy.$modal.msgError(res.msg || '鍒犻櫎澶辫触')
+      }
+    }).catch(err => {
+      proxy.$modal.msgError('鍒犻櫎澶辫触')
+    })
+  }).catch(() => {
+    // 鐢ㄦ埛鍙栨秷鍒犻櫎
+  })
+}
+
+
+
+// 閲嶇疆琛ㄥ崟
+const resetForm = () => {
+  Object.assign(form, {
+    id: undefined,
+    status: '',
+    province: '',
+    city: '',
+    customerName: '',
+    industry: '',
+    informationState: '',
+    mainBusinessRevenue: '',
+    customerScale: '',
+    mainProducts: '',
+    businessSource: '',
+    contractAmount: '',
+    description: '',
+    renContent: '',
+    paymentDescription: '',
+    entryPerson: userStore.nickName,
+    entryDate: dayjs().format('YYYY-MM-DD')
+  })
+  
+  if (formRef.value) {
+    formRef.value.clearValidate()
+  }
+}
+
+// 鍏抽棴瀵硅瘽妗�
+const closeDialog = () => {
+  dialogFormVisible.value = false
+  resetForm()
+}
+
+
+
+// 鏌ョ湅闄勪欢
+function handleAttachment(row) {
+	currentAttachmentRow.value = row
+	fileListRef.value.open(row.businessCommonFiles, row.id)
+}
+
+// 涓嬭浇闄勪欢锛堣鎯呴〉闈級
+function downloadAttachment(row) {
+	proxy.$download.name(row.url)
+}
+
+// 棰勮闄勪欢锛堣鎯呴〉闈級
+function previewAttachment(row) {
+	if (filePreviewRef.value) {
+		filePreviewRef.value.open(row.previewURL)
+	} else {
+		// 濡傛灉娌℃湁棰勮缁勪欢锛岀洿鎺ユ墦寮�閾炬帴
+		window.open(row.url, '_blank')
+	}
+}
+
+// 闄勪欢鍒楄〃鍒锋柊
+function handleFileListRefresh(rowId) {
+	// 閲嶆柊鑾峰彇鍒楄〃鏁版嵁
+	getList()
+	// 绛夊緟鍒楄〃鏁版嵁鏇存柊鍚庯紝鎵惧埌瀵瑰簲鐨勮骞舵洿鏂伴檮浠跺垪琛�
+	setTimeout(() => {
+		if (currentAttachmentRow.value && tableData.value) {
+			const updatedRow = tableData.value.find(item => item.id === currentAttachmentRow.value.id)
+			if (updatedRow && updatedRow.businessCommonFiles) {
+				currentAttachmentRow.value = updatedRow
+				fileListRef.value.open(updatedRow.businessCommonFiles, updatedRow.id)
+			}
+		}
+	}, 300)
+}
+
+onMounted(async () => {
+  // 鍔犺浇鐢ㄦ埛鍒楄〃渚涙悳绱娇鐢�
+  await loadUserList()
+  getList()
+})
+</script>
+
+<style scoped lang="scss">
+.app-container {
+  padding: 20px;
+}
+.search_form {
+  display: flex;
+  align-items: flex-start;
+    justify-content: space-between;
+}
+
+.table_list {
+  margin-top: unset;
+}
+
+.dialog-footer {
+  text-align: right;
+}
+
+:deep(.el-form-item__label) {
+  font-weight: 500;
+}
+
+:deep(.el-table) {
+  .el-table__header-wrapper {
+    th {
+      background-color: #f0f2f5;
+      color: #333;
+      font-weight: 600;
+    }
+  }
+}
+
+/* 鍙樻洿璁板綍鏃堕棿绾挎牱寮� */
+.change-history-section {
+  margin-top: 20px;
+  
+  .el-divider {
+    margin: 20px 0;
+  }
+  
+  .timeline-card {
+    margin: 8px 0;
+    border-radius: 8px;
+    
+    .card-header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      padding: 8px 0;
+      
+      .action-type {
+        font-weight: 600;
+        color: #333;
+      }
+      
+      .operator {
+        font-size: 12px;
+        color: #666;
+      }
+    }
+    
+    .change-content {
+      .status-change, .description-change {
+        margin-bottom: 8px;
+        
+        .label {
+          font-weight: 500;
+          color: #666;
+          margin-right: 8px;
+        }
+        
+        .description-text {
+          color: #333;
+          line-height: 1.5;
+        }
+      }
+    }
+  }
+  
+  /* 鏃堕棿绾挎牱寮忎紭鍖� */
+  :deep(.el-timeline) {
+    padding-left: 0;
+    
+    .el-timeline-item {
+      .el-timeline-item__node {
+        background-color: #409eff;
+        
+        &.el-timeline-item__node--primary {
+          background-color: #409eff;
+        }
+        
+        &.el-timeline-item__node--success {
+          background-color: #67c23a;
+        }
+        
+        &.el-timeline-item__node--info {
+          background-color: #909399;
+        }
+        
+        &.el-timeline-item__node--hollow {
+          background-color: transparent;
+          border-color: #409eff;
+        }
+      }
+      
+      .el-timeline-item__timestamp {
+        color: #666;
+        font-size: 12px;
+      }
+    }
+  }
+}
+</style>

--
Gitblit v1.9.3