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