From bb44f08f420fc6b1520c06f3698f3d4f52e4a06b Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期二, 03 三月 2026 16:08:25 +0800
Subject: [PATCH] 公司app 1.添加商机管理功能 2.app部署修改
---
src/pages/index.vue | 496 ++-------------
src/pages.json | 14
src/pages/opportunityManagement/detail.vue | 1035 +++++++++++++++++++++++++++++++
src/pages/opportunityManagement/index.vue | 237 +++++++
src/api/salesManagement/opportunityManagement.js | 70 ++
src/config.js | 7
src/pages/opportunityManagement/fileList.vue | 75 ++
7 files changed, 1,499 insertions(+), 435 deletions(-)
diff --git a/src/api/salesManagement/opportunityManagement.js b/src/api/salesManagement/opportunityManagement.js
new file mode 100644
index 0000000..c67c2b5
--- /dev/null
+++ b/src/api/salesManagement/opportunityManagement.js
@@ -0,0 +1,70 @@
+// 鍟嗘満绠$悊鎺ュ彛
+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: "post",
+ 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,
+ });
+}
+
+// 鍒犻櫎闄勪欢锛堥�氱敤闄勪欢锛�
+export function delCommonFile(ids) {
+ return request({
+ url: "/commonFile/delCommonFile",
+ method: "delete",
+ data: ids,
+ });
+}
diff --git a/src/config.js b/src/config.js
index 028c4a0..6b76085 100644
--- a/src/config.js
+++ b/src/config.js
@@ -1,11 +1,6 @@
// 搴旂敤鍏ㄥ眬閰嶇疆
const config = {
- // baseUrl: 'https://vue.ruoyi.vip/prod-api',
- // baseUrl: 'http://localhost/prod-api',
- baseUrl: 'http://114.132.189.42:7003', // 瀹佸娑︽嘲
- // baseUrl: 'http://192.168.1.147:9036',
- //cloud鍚庡彴缃戝叧鍦板潃
- // baseUrl: 'http://192.168.10.3:8080',
+ baseUrl: 'http://114.132.189.42:7003', // 鍏徃
// 搴旂敤淇℃伅
appInfo: {
// 搴旂敤鍚嶇О
diff --git a/src/pages.json b/src/pages.json
index 4151447..fdc11e7 100644
--- a/src/pages.json
+++ b/src/pages.json
@@ -149,6 +149,20 @@
}
},
{
+ "path": "pages/opportunityManagement/index",
+ "style": {
+ "navigationBarTitleText": "鍟嗘満绠$悊",
+ "navigationStyle": "custom"
+ }
+ },
+ {
+ "path": "pages/opportunityManagement/detail",
+ "style": {
+ "navigationBarTitleText": "鍟嗘満璇︽儏",
+ "navigationStyle": "custom"
+ }
+ },
+ {
"path": "pages/procurementManagement/procurementLedger/index",
"style": {
"navigationBarTitleText": "閲囪喘鍙拌处",
diff --git a/src/pages/index.vue b/src/pages/index.vue
index 3ba08d3..63b3ecb 100644
--- a/src/pages/index.vue
+++ b/src/pages/index.vue
@@ -32,155 +32,20 @@
<!-- </view>-->
<!-- </view>-->
- <!-- 钀ラ攢绠$悊妯″潡 -->
- <view class="common-module marketing-module">
- <view class="module-header">
- <view class="module-title-container">
- <text class="module-title">钀ラ攢绠$悊</text>
+ <!-- 鍟嗘満绠$悊鍏ュ彛锛堝皬鎸夐挳锛� -->
+ <view class="opportunity-entry">
+ <view class="opportunity-item" @click="goOpportunity">
+ <view class="opportunity-icon-wrap">
+ <image
+ class="opportunity-icon"
+ src="/static/images/icon/xiaoshoutaizhang@2x.png"
+ mode="aspectFit"
+ />
</view>
- </view>
- <view class="module-content">
- <up-grid
- :border="false"
- col="4"
- >
- <up-grid-item
- v-for="(item, index) in marketingItems"
- :key="index"
- @click="handleCommonItemClick(item)"
- >
- <view class="icon-container" :style="{ background: item.bgColor }">
- <up-icon
- :name="item.icon"
- :size="58"
- color="#ffffff"
- ></up-icon>
- </view>
- <text class="item-label">{{item.label}}</text>
- </up-grid-item>
- </up-grid>
+ <text class="opportunity-text small-title">鍟嗘満绠$悊</text>
</view>
</view>
- <!-- 閲囪喘绠$悊妯″潡 -->
- <view class="common-module purchase-module">
- <view class="module-header">
- <view class="module-title-container">
- <text class="module-title">閲囪喘绠$悊</text>
- </view>
- </view>
- <view class="module-content">
- <up-grid
- :border="false"
- col="4"
- >
- <up-grid-item
- v-for="(item, index) in purchaseItems"
- :key="index"
- @click="handleCommonItemClick(item)"
- >
- <view class="icon-container" :style="{ background: item.bgColor }">
- <up-icon
- :name="item.icon"
- :size="58"
- color="#ffffff"
- ></up-icon>
- </view>
- <text class="item-label">{{item.label}}</text>
- </up-grid-item>
- </up-grid>
- </view>
- </view>
-
- <!-- 鍗忓悓鍔炲叕妯″潡 -->
- <view class="common-module collaboration-module">
- <view class="module-header">
- <view class="module-title-container">
- <text class="module-title">鍗忓悓鍔炲叕</text>
- </view>
- </view>
- <view class="module-content">
- <up-grid
- :border="false"
- col="4"
- >
- <up-grid-item
- v-for="(item, index) in collaborationItems"
- :key="index"
- @click="handleCommonItemClick(item)"
- >
- <view class="icon-container" :style="{ background: item.bgColor }">
- <up-icon
- :name="item.icon"
- :size="58"
- color="#ffffff"
- ></up-icon>
- </view>
- <text class="item-label">{{item.label}}</text>
- </up-grid-item>
- </up-grid>
- </view>
- </view>
-
- <!-- 鐢熶骇绠℃帶妯″潡 -->
-<!-- <view class="common-module production-module">-->
-<!-- <view class="module-header">-->
-<!-- <view class="module-title-container">-->
-<!-- <text class="module-title">鐢熶骇绠℃帶</text>-->
-<!-- </view>-->
-<!-- </view>-->
-<!-- <view class="module-content">-->
-<!-- <up-grid-->
-<!-- :border="false"-->
-<!-- col="4"-->
-<!-- >-->
-<!-- <up-grid-item-->
-<!-- v-for="(item, index) in productionItems"-->
-<!-- :key="index"-->
-<!-- @click="handleCommonItemClick(item)"-->
-<!-- >-->
-<!-- <view class="icon-container" :style="{ background: item.bgColor }">-->
-<!-- <up-icon-->
-<!-- :name="item.icon"-->
-<!-- :size="58"-->
-<!-- color="#ffffff"-->
-<!-- ></up-icon>-->
-<!-- </view>-->
-<!-- <text class="item-label">{{item.label}}</text>-->
-<!-- </up-grid-item>-->
-<!-- </up-grid>-->
-<!-- </view>-->
-<!-- </view>-->
-
- <!-- 璁惧绠$悊妯″潡 -->
- <view class="common-module equipment-module">
- <view class="module-header">
- <view class="module-title-container">
- <text class="module-title">璁惧绠$悊</text>
- </view>
- </view>
- <view class="module-content">
- <up-grid
- :border="false"
- col="4"
- >
- <up-grid-item
- v-for="(item, index) in equipmentItems"
- :key="index"
- @click="handleCommonItemClick(item)"
- >
- <view class="icon-container" :style="{ background: item.bgColor }">
- <up-icon
- :name="item.icon"
- :size="58"
- color="#ffffff"
- ></up-icon>
- </view>
- <text class="item-label">{{item.label}}</text>
- </up-grid-item>
- </up-grid>
- </view>
- </view>
</view>
</template>
@@ -213,290 +78,6 @@
currentStatus.value = statusList[statusIndex]
}, 3000)
}
-
-// 钀ラ攢绠$悊鍔熻兘鏁版嵁
-const marketingItems = reactive([
- {
- icon: '/static/images/icon/xiaoshoutaizhang@2x.png',
- label: '閿�鍞彴璐�',
- },
- {
- icon: '/static/images/icon/kaipiaodengji@2x.png',
- label: '寮�绁ㄧ櫥璁�',
- },
- {
- icon: '/static/images/icon/kaipiaotaizhang@2x.png',
- label: '寮�绁ㄥ彴璐�',
- },
- {
- icon: '/static/images/icon/huikuandengji@2x.png',
- label: '鍥炴鐧昏',
- },
- {
- icon: '/static/images/icon/huikuanliushui@2x.png',
- label: '鍥炴娴佹按',
- },
- {
- icon: '/static/images/icon/kehuwanglai@2x.png',
- label: '瀹㈡埛寰�鏉�',
- }
-]);
-
-// 閲囪喘绠$悊鍔熻兘鏁版嵁
-const purchaseItems = reactive([
- {
- icon: '/static/images/icon/caigoutaizhang@2x.png',
- label: '閲囪喘鍙拌处',
- },
- {
- icon: '/static/images/icon/laipiaodengji@2x.png',
- label: '鏉ョエ鐧昏',
- },
- {
- icon: '/static/images/icon/laipiaotaizhang@2x.png',
- label: '鏉ョエ鍙拌处',
- },
- {
- icon: '/static/images/icon/fukuanjingji@2x.png',
- label: '浠樻鐧昏',
- },
- {
- icon: '/static/images/icon/fukuanliushui@2x.png',
- label: '浠樻娴佹按',
- },
- {
- icon: '/static/images/icon/gongyingshangwanglai@2x.png',
- label: '渚涘簲鍟嗗線鏉�',
- },
-]);
-
-// 鍗忓悓鍔炲叕鍔熻兘鏁版嵁
-const collaborationItems = reactive([
- {
- icon: '/static/images/icon/xietongshenpi@2x.png',
- label: '鍗忓悓瀹℃壒',
- },
- {
- icon: '/static/images/icon/kehubaifang@2x.png',
- label: '瀹㈡埛鎷滆',
- }
-]);
-
-// 鐢熶骇绠℃帶鍔熻兘鏁版嵁
-const productionItems = reactive([
- {
- icon: '/static/images/icon/shengchandingdan@2x.png',
- label: '鐢熶骇璁㈠崟',
- bgColor: '#FF9800'
- },
- {
- icon: '/static/images/icon/shengchanpaigong@2x.png',
- label: '鐢熶骇娲惧伐',
- bgColor: '#FF6B35'
- },
- {
- icon: '/static/images/icon/shengchanpaichan@2x.png',
- label: '宸ュ簭鎺掍骇',
- bgColor: '#E91E63'
- },
- {
- icon: '/static/images/icon/shengchanbaogong@2x.png',
- label: '鐢熶骇鎶ュ伐',
- bgColor: '#673AB7'
- },
- {
- icon: '/static/images/icon/shengchanhesuan@2x.png',
- label: '鐢熶骇鏍哥畻',
- bgColor: '#3F51B5'
- }
-]);
-
-// 璁惧绠$悊鍔熻兘鏁版嵁
-const equipmentItems = reactive([
- // {
- // icon: '/static/images/icon/shebeitaizhang@2x.png',
- // label: '璁惧鍙拌处',
- // },
- {
- icon: '/static/images/icon/shbeibaoxiu@2x.png',
- label: '璁惧鎶ヤ慨',
- },
- {
- icon: '/static/images/icon/shbeibaoyang@2x.png',
- label: '璁惧淇濆吇',
- },
- {
- icon: '/static/images/icon/xunjianshangchuan@2x.png',
- label: '宸℃涓婁紶',
- },
- {
- icon: '/static/images/icon/guzhangfenxi@2x.png',
- label: '鍒嗘瀽杩芥函',
- bgColor: '#ff9800'
- },
- {
- icon: '/static/images/icon/zhinengpaidan@2x.png',
- label: '鏅鸿兘娲惧崟',
- bgColor: '#ff6b35'
- },
- {
- icon: '/static/images/icon/zuoyezhidao@2x.png',
- label: '浣滀笟鎸囧',
- bgColor: '#4caf50'
- },
- {
- icon: '/static/images/icon/jieguoyanzheng@2x.png',
- label: '缁撴灉楠岃瘉',
- bgColor: '#9c27b0'
- }
-]);
-
-// 澶勭悊甯哥敤鍔熻兘鐐瑰嚮
-const handleCommonItemClick = (item) => {
- // 鏍规嵁涓嶅悓鐨勫姛鑳介」杩涜璺宠浆
- switch (item.label) {
- case '閿�鍞彴璐�':
- uni.navigateTo({
- url: '/pages/sales/salesAccount/index'
- });
- break;
- case '寮�绁ㄧ櫥璁�':
- uni.navigateTo({
- url: '/pages/sales/invoicingRegistration/index'
- });
- break;
- case '寮�绁ㄥ彴璐�':
- uni.navigateTo({
- url: '/pages/sales/invoiceLedger/index'
- });
- break;
- case '鍥炴鐧昏':
- uni.navigateTo({
- url: '/pages/sales/receiptPayment/index'
- });
- break;
- case '鍥炴娴佹按':
- uni.navigateTo({
- url: '/pages/sales/receiptPaymentHistory/index'
- });
- break;
- case '瀹㈡埛寰�鏉�':
- uni.navigateTo({
- url: '/pages/sales/receiptPaymentLedger/index'
- });
- break;
- case '閲囪喘鍙拌处':
- uni.navigateTo({
- url: '/pages/procurementManagement/procurementLedger/index'
- });
- break;
- case '鏉ョエ鐧昏':
- uni.navigateTo({
- url: '/pages/procurementManagement/invoiceEntry/index'
- });
- break;
- case '鏉ョエ鍙拌处':
- uni.navigateTo({
- url: '/pages/procurementManagement/procurementInvoiceLedger/index'
- });
- break;
- case '浠樻鐧昏':
- uni.navigateTo({
- url: '/pages/procurementManagement/paymentEntry/index'
- });
- break;
- case '浠樻娴佹按':
- uni.navigateTo({
- url: '/pages/procurementManagement/receiptPaymentHistory/index'
- });
- break;
- case '渚涘簲鍟嗗線鏉�':
- uni.navigateTo({
- url: '/pages/procurementManagement/paymentLedger/index'
- });
- break;
- case '鍗忓悓瀹℃壒':
- uni.navigateTo({
- url: '/pages/cooperativeOffice/collaborativeApproval/index'
- });
- break;
- case '瀹㈡埛鎷滆':
- uni.navigateTo({
- url: '/pages/cooperativeOffice/clientVisit/index'
- });
- break;
- case '鐢熶骇璁㈠崟':
- uni.navigateTo({
- url: '/pages/productionManagement/productionOrder/index'
- });
- break;
- case '鐢熶骇娲惧伐':
- uni.navigateTo({
- url: '/pages/productionManagement/productionDispatching/index'
- });
- break;
- case '宸ュ簭鎺掍骇':
- uni.navigateTo({
- url: '/pages/productionManagement/processScheduling/index'
- });
- break;
- case '鐢熶骇鎶ュ伐':
- uni.navigateTo({
- url: '/pages/productionManagement/productionReport/index'
- });
- break;
- case '鐢熶骇鏍哥畻':
- uni.navigateTo({
- url: '/pages/productionManagement/productionAccounting/index'
- });
- break;
- case '璁惧鍙拌处':
- uni.navigateTo({
- url: '/pages/equipmentManagement/ledger/index'
- });
- break;
- case '璁惧鎶ヤ慨':
- uni.navigateTo({
- url: '/pages/equipmentManagement/repair/index'
- });
- break;
- case '璁惧淇濆吇':
- uni.navigateTo({
- url: '/pages/equipmentManagement/upkeep/index'
- });
- break;
- case '宸℃涓婁紶':
- uni.navigateTo({
- url: '/pages/inspectionUpload/index'
- });
- break;
- case '鍒嗘瀽杩芥函':
- uni.navigateTo({
- url: '/pages/equipmentManagement/faultAnalysis/index'
- });
- break;
- case '鏅鸿兘娲惧崟':
- uni.navigateTo({
- url: '/pages/equipmentManagement/smartDispatch/index'
- });
- break;
- case '浣滀笟鎸囧':
- uni.navigateTo({
- url: '/pages/equipmentManagement/sop/index'
- });
- break;
- case '缁撴灉楠岃瘉':
- uni.navigateTo({
- url: '/pages/equipmentManagement/verification/index'
- });
- break;
- default:
- uni.showToast({
- title: `鐐瑰嚮浜�${item.label}`,
- icon: 'none'
- });
- }
-};
// 鍒涘缓瀵瑰瓙缁勪欢鐨勫紩鐢�
const uToastRef = ref(null);
@@ -549,6 +130,13 @@
if (uToastRef.value) {
uToastRef.value.success(`鐐瑰嚮浜嗙${name + 1}涓猔); // 娉ㄦ剰锛氳繖閲屽姞1鏄洜涓洪�氬父鎴戜滑鏄粠绗�1涓紑濮嬭鏁扮殑
}
+};
+
+// 璺宠浆鍒板晢鏈虹鐞�
+const goOpportunity = () => {
+ uni.navigateTo({
+ url: '/pages/opportunityManagement/index'
+ });
};
onMounted(() => {
@@ -1003,6 +591,52 @@
/* #endif */
}
+.opportunity-icon {
+ width: 2.4rem;
+ height: 2.4rem;
+}
+
+/* 鍟嗘満绠$悊灏忔寜閽牱寮� */
+.opportunity-entry {
+ margin-top: 0.5rem;
+ display: flex;
+ justify-content: flex-start;
+}
+
+.opportunity-item {
+ flex-direction: row;
+ align-items: center;
+ justify-content: flex-start;
+ background: #ffffff;
+ border-radius: 999px;
+ padding: 0.4rem 0.9rem 0.4rem 0.5rem;
+ box-shadow: 0 0.125rem 0.75rem rgba(15, 23, 42, 0.12);
+ display: inline-flex;
+}
+
+.opportunity-icon-wrap {
+ width: 2.4rem;
+ height: 2.4rem;
+ border-radius: 999px;
+ background: #e3f2ff;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ margin-right: 0.4rem;
+}
+
+.opportunity-text {
+ font-size: 0.9rem;
+ color: #1f2933;
+ font-weight: 500;
+}
+
+.small-title {
+ font-size: 0.78rem;
+ font-weight: 400;
+ color: #4b5563;
+}
+
/* 鏆楄壊妯″紡閫傞厤 */
@media (prefers-color-scheme: dark) {
.content {
@@ -1165,6 +799,10 @@
.item-label { font-size: 0.8125rem; margin-top: 0.25rem; margin-bottom: 0.625rem; }
.grid-text { font-size: 0.875rem; }
+.opportunity-icon {
+ width: 2.4rem;
+ height: 2.4rem;
+}
@media (prefers-color-scheme: dark) {
.common-module { box-shadow: 0 0.375rem 1.5rem rgba(0,0,0,0.35); }
diff --git a/src/pages/opportunityManagement/detail.vue b/src/pages/opportunityManagement/detail.vue
new file mode 100644
index 0000000..14152bb
--- /dev/null
+++ b/src/pages/opportunityManagement/detail.vue
@@ -0,0 +1,1035 @@
+<template>
+ <view class="account-detail">
+ <PageHeader :title="pageTitle" @back="goBack" />
+
+ <view class="detail-body">
+ <view class="detail-card">
+ <view class="section-header">
+ <text class="section-title">鍩烘湰淇℃伅</text>
+ <text class="section-subtitle">璇峰~鍐欏晢鏈虹殑鍩虹璧勬枡</text>
+ </view>
+
+ <up-form
+ ref="formRef"
+ :rules="rules"
+ :model="form"
+ label-width="90"
+ @submit="onSubmit"
+ >
+ <!-- 鍩烘湰淇℃伅 -->
+ <up-form-item label="瀹㈡埛鍚嶇О" prop="customerName" :required="isAddOrEdit">
+ <up-input
+ v-model="form.customerName"
+ :disabled="isDetail || isAddOperation"
+ placeholder="璇疯緭鍏ュ鎴峰悕绉�"
+ />
+ </up-form-item>
+
+ <up-form-item label="鐪佷唤" prop="province">
+ <up-input
+ v-model="form.province"
+ readonly
+ :disabled="isDetail || isAddOperation"
+ placeholder="鐐瑰嚮閫夋嫨鐪佷唤"
+ @click="onProvinceClick"
+ />
+ <template #right>
+ <up-icon
+ name="arrow-right"
+ @click="onProvinceClick"
+ ></up-icon>
+ </template>
+ </up-form-item>
+
+ <up-form-item label="鍩庡競" prop="city">
+ <up-input
+ v-model="form.city"
+ readonly
+ :disabled="isDetail || isAddOperation"
+ placeholder="鐐瑰嚮閫夋嫨鍩庡競"
+ @click="onCityClick"
+ />
+ <template #right>
+ <up-icon
+ name="arrow-right"
+ @click="onCityClick"
+ ></up-icon>
+ </template>
+ </up-form-item>
+
+ <up-form-item label="鍟嗘満鏉ユ簮" prop="businessSource">
+ <up-input
+ v-model="form.businessSource"
+ :disabled="isDetail || isAddOperation"
+ placeholder="璇疯緭鍏ュ晢鏈烘潵婧�"
+ />
+ </up-form-item>
+
+ <up-form-item label="琛屼笟" prop="industry">
+ <up-input
+ v-model="form.industry"
+ :disabled="isDetail || isAddOperation"
+ placeholder="璇疯緭鍏ヨ涓�"
+ />
+ </up-form-item>
+
+ <up-form-item label="涓昏惀浜у搧" prop="mainProducts">
+ <up-input
+ v-model="form.mainProducts"
+ :disabled="isDetail || isAddOperation"
+ placeholder="璇疯緭鍏ヤ富钀ヤ骇鍝�"
+ />
+ </up-form-item>
+
+ <up-form-item label="涓昏惀涓氬姟鏀跺叆" prop="mainBusinessRevenue">
+ <up-input
+ v-model="form.mainBusinessRevenue"
+ :disabled="isDetail || isAddOperation"
+ placeholder="璇疯緭鍏ヤ富钀ヤ笟鍔℃敹鍏�"
+ />
+ </up-form-item>
+
+ <up-form-item label="瀹㈡埛瑙勬ā" prop="customerScale">
+ <up-input
+ v-model="form.customerScale"
+ :disabled="isDetail || isAddOperation"
+ placeholder="璇疯緭鍏ュ鎴疯妯�"
+ />
+ </up-form-item>
+
+ <up-form-item label="淇℃伅鍖栫幇鐘�" prop="informationState">
+ <up-textarea
+ v-model="form.informationState"
+ :disabled="isDetail || isAddOperation"
+ placeholder="璇疯緭鍏ヤ俊鎭寲鐜扮姸"
+ autoHeight
+ :maxlength="300"
+ count
+ />
+ </up-form-item>
+
+ <up-form-item label="鐘舵��" prop="status" required @click="onStatusClick">
+ <up-input
+ v-model="form.status"
+ readonly
+ :disabled="isDetail"
+ placeholder="鐐瑰嚮閫夋嫨鐘舵��"
+ @click="onStatusClick"
+ />
+ <template #right>
+ <up-icon
+ name="arrow-right"
+ @click="onStatusClick"
+ ></up-icon>
+ </template>
+ </up-form-item>
+
+ <up-form-item label="鍚堝悓閲戦(鍏�)" prop="contractAmount" :required="isAddOrEdit">
+ <up-input
+ v-model="form.contractAmount"
+ :disabled="isDetail || isAddOperation"
+ type="number"
+ placeholder="璇疯緭鍏ュ悎鍚岄噾棰�"
+ />
+ </up-form-item>
+
+ <!-- 鎻忚堪淇℃伅 -->
+ <view class="section-header section-header-inner">
+ <text class="section-title">鎻忚堪淇℃伅</text>
+ <text class="section-subtitle">琛ュ厖鍟嗘満璇存槑锛屼究浜庡悗缁窡杩�</text>
+ </view>
+ <up-form-item label="鏀归�犲唴瀹�" prop="description" :required="isAddOrAddOperation">
+ <up-textarea
+ v-model="form.description"
+ :disabled="isDetail"
+ :placeholder="renovationPlaceholder"
+ autoHeight
+ count
+ maxlength="500"
+ />
+ </up-form-item>
+
+ <up-form-item label="浠樻鎻忚堪" prop="paymentDescription">
+ <up-textarea
+ v-model="form.paymentDescription"
+ :disabled="isDetail"
+ placeholder="鏄惁鍨祫锛熶紒涓氭槸鍚﹀紑绁紵浼佷笟鏄惁鍒嗚ˉ璐存垨棰濆鍑洪挶锛�"
+ autoHeight
+ count
+ maxlength="500"
+ />
+ </up-form-item>
+
+ <!-- 闄勪欢鏉愭枡 -->
+ <view class="section-header section-header-inner">
+ <text class="section-title">闄勪欢鏉愭枡</text>
+ <text class="section-subtitle" v-if="!isDetail">鏀寔澶氭枃浠朵笂浼�</text>
+ </view>
+
+ <view v-if="!isDetail" class="upload-wrap">
+ <up-upload
+ :fileList="uploadFileList"
+ @afterRead="afterRead"
+ @delete="deleteUpload"
+ name="attachments"
+ multiple
+ :maxCount="10"
+ :previewImage="false"
+ >
+ <view class="upload-btn">
+ <up-icon name="plus" size="18" color="#667085"></up-icon>
+ <text class="upload-text">涓婁紶闄勪欢</text>
+ </view>
+ </up-upload>
+ </view>
+
+ <view v-if="existingFiles.length" class="existing-files">
+ <view class="existing-title">宸蹭笂浼�</view>
+ <view v-for="f in existingFiles" :key="f.id || f.url" class="existing-item">
+ <text class="file-name">{{ f.name || getFileName(f.url) }}</text>
+ <view class="file-actions">
+ <u-button size="mini" type="primary" plain @click="downloadFile(f)">涓嬭浇</u-button>
+ <u-button
+ v-if="!isDetail"
+ size="mini"
+ type="error"
+ plain
+ @click="removeExistingFile(f)"
+ >
+ 鍒犻櫎
+ </u-button>
+ </view>
+ </view>
+ </view>
+
+ <!-- 褰曞叆淇℃伅 -->
+ <view class="section-header section-header-inner">
+ <text class="section-title">褰曞叆淇℃伅</text>
+ </view>
+ <up-form-item label="褰曞叆浜�" prop="entryPerson" required>
+ <up-input
+ v-model="form.entryPerson"
+ :disabled="true"
+ />
+ </up-form-item>
+
+ <up-form-item label="褰曞叆鏃ユ湡" prop="entryDate" required @click="onEntryDateClick">
+ <up-input
+ v-model="form.entryDate"
+ readonly
+ :disabled="isDetail"
+ placeholder="鐐瑰嚮閫夋嫨鏃ユ湡"
+ @click="onEntryDateClick"
+ />
+ <template #right>
+ <up-icon
+ name="arrow-right"
+ @click="onEntryDateClick"
+ ></up-icon>
+ </template>
+ </up-form-item>
+
+ <!-- 鍘嗗彶璁板綍 -->
+ <view v-if="changeHistory.length" class="change-history-section">
+ <view class="history-title">鍙樻洿璁板綍</view>
+ <view v-for="item in changeHistory" :key="item.id" class="history-item">
+ <view class="history-header">
+ <text class="history-status">{{ getStatusText(item.status) }}</text>
+ <text class="history-operator">{{ item.operator }}</text>
+ </view>
+ <view class="history-time">{{ item.timestamp }}</view>
+ <view v-if="item.description" class="history-desc">
+ {{ item.description }}
+ </view>
+ </view>
+ </view>
+
+ <!-- 搴曢儴鎸夐挳 -->
+ <view v-if="!isDetail" class="footer-btns">
+ <u-button class="cancel-btn" @click="goBack">鍙栨秷</u-button>
+ <u-button class="save-btn" type="primary" @click="onSubmit" :loading="loading">淇濆瓨</u-button>
+ </view>
+ </up-form>
+ </view>
+ </view>
+
+ <!-- 鐪佷唤閫夋嫨 -->
+ <up-action-sheet
+ :show="showProvincePicker"
+ :actions="provinceActionList"
+ title="閫夋嫨鐪佷唤"
+ @select="onProvinceSelect"
+ @close="showProvincePicker = false"
+ />
+
+ <!-- 鍩庡競閫夋嫨 -->
+ <up-action-sheet
+ :show="showCityPicker"
+ :actions="cityActionList"
+ title="閫夋嫨鍩庡競"
+ @select="onCitySelect"
+ @close="showCityPicker = false"
+ />
+
+ <!-- 鐘舵�侀�夋嫨 -->
+ <up-action-sheet
+ :show="showStatusPicker"
+ :actions="statusActionList"
+ title="閫夋嫨鐘舵��"
+ @select="onStatusSelect"
+ @close="showStatusPicker = false"
+ />
+
+ <!-- 鏃ユ湡閫夋嫨 -->
+ <up-popup :show="showDatePicker" mode="bottom" @close="showDatePicker = false">
+ <up-datetime-picker
+ :show="true"
+ v-model="pickerDateValue"
+ mode="date"
+ @confirm="onDateConfirm"
+ @cancel="showDatePicker = false"
+ />
+ </up-popup>
+ </view>
+</template>
+
+<script setup>
+import { ref, computed } from 'vue'
+import { onLoad } from '@dcloudio/uni-app'
+import dayjs from 'dayjs'
+import PageHeader from '@/components/PageHeader.vue'
+import useUserStore from '@/store/modules/user'
+import {
+ addOpportunity,
+ updateOpportunity,
+ addDescription,
+ getProvinceList,
+ getCityList,
+ delCommonFile
+} from '@/api/salesManagement/opportunityManagement.js'
+import { getToken } from '@/utils/auth'
+import config from '@/config.js'
+
+const userStore = useUserStore()
+
+const formRef = ref(null)
+const loading = ref(false)
+const operationType = ref('add')
+const renovationPlaceholder = '1.鏍囧噯鍖栵細\n2.瀹氬埗鍖栵細\n3.澶栭噰锛�'
+
+// 闄勪欢涓婁紶
+const uploadFileList = ref([]) // up-upload 缁戝畾鍒楄〃
+const tempFileIds = ref([]) // 鎻愪氦缁欏悗绔殑涓存椂鏂囦欢ID
+const existingFiles = ref([]) // 宸蹭笂浼犵殑闄勪欢锛堣鎯�/缂栬緫鍙嶆樉锛�
+
+const getFileName = (url) => {
+ try {
+ if (!url) return ''
+ return decodeURIComponent(url.split('/').pop())
+ } catch (e) {
+ return url || ''
+ }
+}
+
+const isImageUrl = (url) => /\.(png|jpe?g|gif|bmp|webp)$/i.test(url || '')
+
+const toAbsoluteUrl = (url) => {
+ if (!url) return ''
+ if (/^https?:\/\//i.test(url)) return url
+ return config.baseUrl.replace(/\/$/, '') + (url.startsWith('/') ? url : `/${url}`)
+}
+
+// 鍙笅杞藉埌鎵嬫満锛堜笉棰勮锛�
+const downloadFile = (file) => {
+ const url = toAbsoluteUrl(file?.url)
+ if (!url) return
+
+ // H5 鐩存帴鎵撳紑閾炬帴瑙﹀彂涓嬭浇
+ if (typeof window !== 'undefined' && window?.open) {
+ window.open(url, '_blank')
+ return
+ }
+
+ uni.showLoading({ title: '涓嬭浇涓�...' })
+ uni.downloadFile({
+ url,
+ success: (res) => {
+ if (res.statusCode !== 200) {
+ uni.hideLoading()
+ uni.showToast({ title: '涓嬭浇澶辫触', icon: 'none' })
+ return
+ }
+ uni.saveFile({
+ tempFilePath: res.tempFilePath,
+ success: () => {
+ uni.hideLoading()
+ uni.showToast({ title: '宸蹭笅杞藉埌鏈湴', icon: 'success' })
+ },
+ fail: () => {
+ uni.hideLoading()
+ uni.showToast({ title: '淇濆瓨澶辫触', icon: 'none' })
+ }
+ })
+ },
+ fail: () => {
+ uni.hideLoading()
+ uni.showToast({ title: '涓嬭浇澶辫触', icon: 'none' })
+ }
+ })
+}
+
+const uploadSingle = (fileObj) => {
+ return new Promise((resolve, reject) => {
+ const filePath = fileObj?.url || fileObj?.tempFilePath || fileObj?.path
+ if (!filePath) {
+ reject(new Error('鏈壘鍒板彲涓婁紶鐨勬枃浠�'))
+ return
+ }
+ uni.uploadFile({
+ url: config.baseUrl + '/file/upload',
+ filePath,
+ name: 'file',
+ formData: { type: 9 },
+ header: { Authorization: 'Bearer ' + getToken() },
+ success: (res) => {
+ try {
+ const data = JSON.parse(res.data || '{}')
+ if (data.code === 200) {
+ resolve(data.data)
+ } else {
+ reject(new Error(data.msg || '涓婁紶澶辫触'))
+ }
+ } catch (e) {
+ reject(e)
+ }
+ },
+ fail: (err) => reject(err)
+ })
+ })
+}
+
+const afterRead = async (event) => {
+ const files = Array.isArray(event.file) ? event.file : [event.file]
+ for (const f of files) {
+ const item = {
+ url: f.url,
+ name: f.name,
+ status: 'uploading',
+ message: '涓婁紶涓�...'
+ }
+ const idx = uploadFileList.value.length
+ uploadFileList.value.push(item)
+ try {
+ uni.showLoading({ title: '涓婁紶涓�...' })
+ const uploaded = await uploadSingle(f)
+ uni.hideLoading()
+ uploadFileList.value[idx] = {
+ ...uploadFileList.value[idx],
+ status: 'success',
+ message: '',
+ url: uploaded?.url || uploadFileList.value[idx]?.url,
+ name: uploaded?.name || uploadFileList.value[idx]?.name,
+ tempId: uploaded?.tempId
+ }
+ if (uploaded?.tempId) {
+ tempFileIds.value.push(uploaded.tempId)
+ }
+ } catch (e) {
+ uni.hideLoading()
+ uploadFileList.value[idx] = {
+ ...uploadFileList.value[idx],
+ status: 'failed',
+ message: '涓婁紶澶辫触'
+ }
+ uni.showToast({ title: '涓婁紶澶辫触', icon: 'none' })
+ }
+ }
+}
+
+const deleteUpload = (event) => {
+ const index = event?.index
+ if (index === undefined || index === null) return
+ const removed = uploadFileList.value[index]
+ uploadFileList.value.splice(index, 1)
+ if (removed?.tempId) {
+ const pos = tempFileIds.value.findIndex(id => String(id) === String(removed.tempId))
+ if (pos > -1) tempFileIds.value.splice(pos, 1)
+ }
+}
+
+const removeExistingFile = (file) => {
+ if (!file?.id) {
+ existingFiles.value = existingFiles.value.filter(f => f !== file)
+ return
+ }
+ uni.showModal({
+ title: '鎻愮ず',
+ content: '纭畾鍒犻櫎璇ラ檮浠跺悧锛�',
+ success: async (res) => {
+ if (!res.confirm) return
+ try {
+ const resp = await delCommonFile([file.id])
+ if (resp.code === 200) {
+ uni.showToast({ title: '鍒犻櫎鎴愬姛', icon: 'success' })
+ existingFiles.value = existingFiles.value.filter(f => f.id !== file.id)
+ } else {
+ uni.showToast({ title: resp.msg || '鍒犻櫎澶辫触', icon: 'none' })
+ }
+ } catch (e) {
+ uni.showToast({ title: '鍒犻櫎澶辫触', icon: 'none' })
+ }
+ }
+ })
+}
+
+const form = ref({
+ id: undefined,
+ status: '',
+ province: '',
+ city: '',
+ customerName: '',
+ industry: '',
+ informationState: '',
+ mainBusinessRevenue: '',
+ customerScale: '',
+ mainProducts: '',
+ businessSource: '',
+ contractAmount: '',
+ description: '',
+ paymentDescription: '',
+ entryPerson: userStore.nickName,
+ entryDate: dayjs().format('YYYY-MM-DD'),
+ businessDescription: [],
+ businessCommonFiles: []
+})
+
+const rules = {
+ customerName: [
+ { required: true, message: '璇疯緭鍏ュ鎴峰悕绉�', trigger: ['blur', 'change'] }
+ ],
+ status: [
+ { required: true, message: '璇烽�夋嫨鐘舵��', trigger: ['blur', 'change'] }
+ ],
+ contractAmount: [
+ { required: true, message: '璇疯緭鍏ュ悎鍚岄噾棰�', trigger: ['blur', 'change'] }
+ ],
+ description: [
+ { required: true, message: '璇疯緭鍏ユ敼閫犲唴瀹�', trigger: ['blur', 'change'] }
+ ],
+ entryPerson: [
+ { required: true, message: '璇疯緭鍏ュ綍鍏ヤ汉', trigger: ['blur', 'change'] }
+ ],
+ entryDate: [
+ { required: true, message: '璇烽�夋嫨褰曞叆鏃ユ湡', trigger: ['blur', 'change'] }
+ ]
+}
+
+// 鐘舵�� / 鐪佸競閫夐」
+const statusOptions = [
+ { value: '鏂板缓', label: '鏂板缓' },
+ { value: '椤圭洰璺熻釜', label: '椤圭洰璺熻釜' },
+ { value: '鍚堝悓绛剧害', label: '鍚堝悓绛剧害' },
+ { value: '澶囨鐢虫姤', label: '澶囨鐢虫姤' },
+ { value: '椤圭洰浜や粯', label: '椤圭洰浜や粯' },
+ { value: '椤圭洰楠屾敹', label: '椤圭洰楠屾敹' }
+]
+
+const provinceOptions = ref([])
+const cityOptions = ref([])
+const selectedProvinceId = ref(null)
+
+const statusActionList = computed(() =>
+ statusOptions.map(item => ({
+ name: item.label,
+ value: item.value
+ }))
+)
+
+const provinceActionList = computed(() =>
+ provinceOptions.value.map(item => ({
+ name: item.name,
+ value: item.id
+ }))
+)
+
+const cityActionList = computed(() =>
+ cityOptions.value.map(item => ({
+ name: item.name,
+ value: item.id
+ }))
+)
+
+const showProvincePicker = ref(false)
+const showCityPicker = ref(false)
+const showStatusPicker = ref(false)
+const showDatePicker = ref(false)
+const pickerDateValue = ref(Date.now())
+
+const changeHistory = ref([])
+
+const isDetail = computed(() => operationType.value === 'detail')
+const isAddOperation = computed(() => operationType.value === 'addOperation')
+const isAddOrEdit = computed(() => ['add', 'edit'].includes(operationType.value))
+const isAddOrAddOperation = computed(() => ['add', 'addOperation'].includes(operationType.value))
+
+const pageTitle = computed(() => {
+ switch (operationType.value) {
+ case 'add':
+ return '鏂板缓鍟嗘満'
+ case 'edit':
+ return '缂栬緫鍟嗘満'
+ case 'addOperation':
+ return '娣诲姞鎻忚堪'
+ case 'detail':
+ default:
+ return '鍟嗘満璇︽儏'
+ }
+})
+
+const goBack = () => {
+ uni.navigateBack()
+}
+
+const getStatusText = (status) => {
+ const map = statusOptions.reduce((acc, cur) => {
+ acc[cur.value] = cur.label
+ return acc
+ }, {})
+ return map[status] || status || '鏈煡'
+}
+
+// 鍔犺浇鐪佷唤
+const loadProvinces = async () => {
+ try {
+ const res = await getProvinceList()
+ provinceOptions.value = res.data || res.records || []
+ } catch (e) {
+ console.error('鑾峰彇鐪佷唤鍒楄〃澶辫触:', e)
+ }
+}
+
+// 鏍规嵁鐪佷唤鍔犺浇鍩庡競
+const loadCitiesByProvinceId = async (provinceId) => {
+ if (!provinceId) {
+ cityOptions.value = []
+ return
+ }
+ try {
+ const res = await getCityList({ provinceId })
+ cityOptions.value = res.data || res.records || []
+ } catch (e) {
+ console.error('鑾峰彇鍩庡競鍒楄〃澶辫触:', e)
+ }
+}
+
+// 鐪佷唤 / 鍩庡競閫夋嫨
+const onProvinceClick = () => {
+ if (isDetail.value || isAddOperation.value) return
+ showProvincePicker.value = true
+}
+
+const onProvinceSelect = async (e) => {
+ selectedProvinceId.value = e.value
+ const target = provinceOptions.value.find(p => p.id === e.value)
+ form.value.province = target ? target.name : e.name
+ // 閲嶇疆鍩庡競骞跺姞杞藉煄甯傚垪琛�
+ form.value.city = ''
+ await loadCitiesByProvinceId(e.value)
+ showProvincePicker.value = false
+}
+
+const onCityClick = () => {
+ if (isDetail.value || isAddOperation.value) return
+ if (!selectedProvinceId.value) {
+ uni.showToast({
+ title: '璇峰厛閫夋嫨鐪佷唤',
+ icon: 'none'
+ })
+ return
+ }
+ showCityPicker.value = true
+}
+
+const onCitySelect = (e) => {
+ const target = cityOptions.value.find(c => c.id === e.value)
+ form.value.city = target ? target.name : e.name
+ showCityPicker.value = false
+}
+
+// 鐘舵�侀�夋嫨
+const onStatusClick = () => {
+ if (isDetail.value) return
+ showStatusPicker.value = true
+}
+
+const onStatusSelect = (e) => {
+ form.value.status = e.value
+ showStatusPicker.value = false
+}
+
+// 褰曞叆鏃ユ湡閫夋嫨
+const onEntryDateClick = () => {
+ if (isDetail.value) return
+ showDatePicker.value = true
+}
+
+const onDateConfirm = (e) => {
+ const val = e.value || e
+ form.value.entryDate = dayjs(val).format('YYYY-MM-DD')
+ showDatePicker.value = false
+}
+
+// 鐢熸垚鍙樻洿璁板綍
+const generateChangeHistory = (row) => {
+ 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
+ })
+ })
+ }
+ changeHistory.value = history
+}
+
+// 鎻愪氦琛ㄥ崟
+const onSubmit = () => {
+ if (isDetail.value) return
+ if (!formRef.value) return
+
+ formRef.value.validate().then(async () => {
+ loading.value = true
+ try {
+ let api
+ let params
+
+ if (operationType.value === 'add') {
+ api = addOpportunity
+ params = {
+ ...form.value,
+ type: 9,
+ tempFileIds: tempFileIds.value
+ }
+ } else if (operationType.value === 'edit') {
+ api = updateOpportunity
+ params = {
+ ...form.value,
+ type: 9,
+ tempFileIds: tempFileIds.value
+ }
+ } else if (operationType.value === 'addOperation') {
+ api = addDescription
+ params = {
+ status: form.value.status,
+ description: form.value.description,
+ paymentDescription: form.value.paymentDescription,
+ entryPerson: form.value.entryPerson,
+ entryDate: form.value.entryDate,
+ type: 9,
+ businessOpportunityId: form.value.id,
+ tempFileIds: tempFileIds.value
+ }
+ }
+
+ const res = await api(params)
+ if (res.code === 200) {
+ uni.showToast({
+ title: '鎿嶄綔鎴愬姛',
+ icon: 'success'
+ })
+ setTimeout(() => {
+ goBack()
+ }, 500)
+ } else {
+ uni.showToast({
+ title: res.msg || '鎿嶄綔澶辫触',
+ icon: 'none'
+ })
+ }
+ } catch (e) {
+ console.error('鍟嗘満鎿嶄綔澶辫触:', e)
+ uni.showToast({
+ title: '鎿嶄綔澶辫触锛岃閲嶈瘯',
+ icon: 'none'
+ })
+ } finally {
+ loading.value = false
+ }
+ }).catch(() => {})
+}
+
+onLoad(async () => {
+ // 璇诲彇鎿嶄綔绫诲瀷鍜屾暟鎹�
+ const type = uni.getStorageSync('opportunityOperationType') || 'add'
+ operationType.value = type
+
+ // 鍔犺浇鐪佷唤鍒楄〃
+ await loadProvinces()
+
+ const raw = uni.getStorageSync('opportunityData')
+ let row = null
+
+ // 鍏煎澶氱瀛樺偍褰㈠紡锛氬瓧绗︿覆 / 瀵硅薄 / null
+ if (raw) {
+ try {
+ if (typeof raw === 'string') {
+ row = JSON.parse(raw)
+ } else if (typeof raw === 'object') {
+ row = raw
+ }
+ } catch (e) {
+ console.error('瑙f瀽鍟嗘満鏁版嵁澶辫触:', e)
+ row = null
+ }
+ }
+
+ if (row && typeof row === 'object' && !Array.isArray(row)) {
+ try {
+ // 淇濈暀宸叉湁瀛楁锛岄伩鍏嶈鐩栨湭鍦ㄨ〃鍗曚腑缂栬緫鐨勫瓧娈�
+ form.value = Object.assign({}, form.value, row)
+
+ // 鍏煎鍚庣杩斿洖 null锛岄伩鍏嶇粍浠跺唴閮ㄨ length 鎶ラ敊
+ const nullToEmpty = (v) => (v === null || v === undefined ? '' : v)
+ form.value.status = nullToEmpty(form.value.status)
+ form.value.province = nullToEmpty(form.value.province)
+ form.value.city = nullToEmpty(form.value.city)
+ form.value.customerName = nullToEmpty(form.value.customerName)
+ form.value.businessSource = nullToEmpty(form.value.businessSource)
+ form.value.industry = nullToEmpty(form.value.industry)
+ form.value.mainProducts = nullToEmpty(form.value.mainProducts)
+ form.value.mainBusinessRevenue = nullToEmpty(form.value.mainBusinessRevenue)
+ form.value.customerScale = nullToEmpty(form.value.customerScale)
+ form.value.informationState = nullToEmpty(form.value.informationState)
+ form.value.contractAmount = nullToEmpty(form.value.contractAmount)
+ form.value.description = nullToEmpty(form.value.description)
+ form.value.paymentDescription = nullToEmpty(form.value.paymentDescription)
+ form.value.entryPerson = nullToEmpty(form.value.entryPerson)
+ form.value.entryDate = nullToEmpty(form.value.entryDate)
+ form.value.businessDescription = Array.isArray(form.value.businessDescription) ? form.value.businessDescription : []
+ form.value.businessCommonFiles = Array.isArray(form.value.businessCommonFiles) ? form.value.businessCommonFiles : []
+
+ // 鍙嶆樉闄勪欢
+ existingFiles.value = form.value.businessCommonFiles
+ uploadFileList.value = []
+ tempFileIds.value = []
+
+ // 鍙嶆樉鐪佷唤鍜屽煄甯�
+ if (row.province) {
+ const provinceMatch = provinceOptions.value.find(p => p.name === row.province || String(p.id) === String(row.province))
+ if (provinceMatch) {
+ selectedProvinceId.value = provinceMatch.id
+ form.value.province = provinceMatch.name
+ await loadCitiesByProvinceId(provinceMatch.id)
+ if (row.city) {
+ const cityMatch = cityOptions.value.find(c => c.name === row.city || String(c.id) === String(row.city))
+ form.value.city = cityMatch ? cityMatch.name : row.city
+ }
+ } else {
+ form.value.province = row.province
+ form.value.city = row.city || ''
+ }
+ } else {
+ form.value.province = ''
+ form.value.city = row.city || ''
+ }
+
+ if (!form.value.entryPerson) {
+ form.value.entryPerson = userStore.nickName
+ }
+ if (!form.value.entryDate) {
+ form.value.entryDate = dayjs().format('YYYY-MM-DD')
+ }
+ generateChangeHistory(row)
+ } catch (e) {
+ console.error('澶勭悊鍟嗘満鏁版嵁澶辫触:', e)
+ }
+ } else {
+ // 鏂板缓妯″紡榛樿褰曞叆淇℃伅
+ form.value.entryPerson = userStore.nickName
+ form.value.entryDate = dayjs().format('YYYY-MM-DD')
+ existingFiles.value = []
+ uploadFileList.value = []
+ tempFileIds.value = []
+ }
+})
+</script>
+
+<style scoped lang="scss">
+@import '@/styles/sales-common.scss';
+
+.account-detail {
+ min-height: 100vh;
+ background: #f8f9fa;
+ padding-bottom: 80px;
+}
+
+.detail-body {
+ padding: 12px 12px 0;
+}
+
+.detail-card {
+ background: #ffffff;
+ border-radius: 16px;
+ box-shadow: 0 8px 24px rgba(15, 35, 52, 0.06);
+ padding: 8px 16px 16px;
+}
+
+.section-header {
+ margin: 8px 0 4px;
+}
+
+.section-header-inner {
+ margin-top: 18px;
+}
+
+.section-title {
+ font-size: 15px;
+ font-weight: 600;
+ color: #1f2933;
+}
+
+.section-subtitle {
+ margin-left: 8px;
+ font-size: 12px;
+ color: #9ca3af;
+}
+
+.footer-btns {
+ display: flex;
+ gap: 12px;
+ padding: 20px;
+}
+
+.cancel-btn {
+ flex: 1;
+}
+
+.save-btn {
+ flex: 1;
+}
+
+.change-history-section {
+ padding: 16px 20px 0 20px;
+}
+
+.history-title {
+ font-size: 14px;
+ font-weight: 600;
+ color: #333;
+ margin-bottom: 8px;
+}
+
+.history-item {
+ background: #ffffff;
+ border-radius: 8px;
+ padding: 12px;
+ margin-bottom: 8px;
+}
+
+.history-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 4px;
+}
+
+.history-status {
+ font-size: 13px;
+ color: #2979ff;
+ font-weight: 500;
+}
+
+.history-operator {
+ font-size: 12px;
+ color: #999;
+}
+
+.history-time {
+ font-size: 12px;
+ color: #999;
+ margin-bottom: 4px;
+}
+
+.history-desc {
+ font-size: 13px;
+ color: #333;
+ line-height: 1.5;
+}
+
+.upload-wrap {
+ padding: 6px 0 2px;
+}
+
+.upload-btn {
+ height: 40px;
+ padding: 0 12px;
+ border-radius: 10px;
+ background: #f2f4f7;
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.upload-text {
+ font-size: 13px;
+ color: #475467;
+}
+
+.existing-files {
+ margin-top: 10px;
+}
+
+.existing-title {
+ font-size: 12px;
+ color: #98a2b3;
+ margin-bottom: 6px;
+}
+
+.existing-item {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 10px 0;
+ border-top: 1px solid #f2f4f7;
+ gap: 10px;
+}
+
+.file-name {
+ flex: 1;
+ font-size: 13px;
+ color: #1f2933;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.file-actions {
+ display: flex;
+ gap: 6px;
+}
+
+// 琛ㄥ崟缁嗚妭浼樺寲
+:deep(.u-form) {
+ .u-form-item__body {
+ padding: 6px 0;
+ }
+}
+
+:deep(.u-form-item__label) {
+ font-size: 13px;
+ color: #6b7280;
+}
+
+:deep(.u-input__content__field) {
+ font-size: 14px;
+}
+
+:deep(.u-textarea__field) {
+ font-size: 14px;
+}
+</style>
+
diff --git a/src/pages/opportunityManagement/fileList.vue b/src/pages/opportunityManagement/fileList.vue
new file mode 100644
index 0000000..51e3d15
--- /dev/null
+++ b/src/pages/opportunityManagement/fileList.vue
@@ -0,0 +1,75 @@
+<template>
+ <el-dialog v-model="dialogVisible" title="闄勪欢" width="40%" :before-close="handleClose">
+ <el-table :data="tableData" border height="40vh" stripe>
+ <el-table-column label="闄勪欢鍚嶇О" prop="name" 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="primary" size="small" @click="delFile(scope.row)">鍒犻櫎</el-button>
+ </template>
+ </el-table-column>
+ </el-table>
+ </el-dialog>
+ <filePreview ref="filePreviewRef" />
+</template>
+
+<script setup>
+import { ref, getCurrentInstance } from 'vue'
+import filePreview from '@/components/filePreview/index.vue'
+import { ElMessageBox } from 'element-plus'
+import {
+ delLedgerFile
+} from "@/api/salesManagement/salesLedger.js";
+
+const emit = defineEmits(['refresh'])
+const dialogVisible = ref(false)
+const tableData = ref([])
+const currentRowId = ref(null)
+const { proxy } = getCurrentInstance();
+const filePreviewRef = ref()
+const handleClose = () => {
+ dialogVisible.value = false
+}
+const open = (list, rowId = null) => {
+ 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 delFile = (row) => {
+ ElMessageBox.confirm('纭畾瑕佸垹闄よ闄勪欢鍚楋紵', '鍒犻櫎纭', {
+ confirmButtonText: '纭畾',
+ cancelButtonText: '鍙栨秷',
+ type: 'warning',
+ }).then(() => {
+ let ids = [];
+ ids.push(row.id);
+ delLedgerFile(ids).then((res) => {
+ if (res.code === 200) {
+ proxy.$modal.msgSuccess("鍒犻櫎鎴愬姛");
+ // 閫氱煡鐖剁粍浠跺埛鏂版暟鎹�
+ emit('refresh', currentRowId.value);
+ } else {
+ proxy.$modal.msgError(res.msg || "鍒犻櫎澶辫触");
+ }
+ }).catch((error) => {
+ console.error("鍒犻櫎闄勪欢澶辫触:", error);
+ proxy.$modal.msgError("鍒犻櫎澶辫触锛岃绋嶅悗閲嶈瘯");
+ });
+ }).catch(() => {
+ // 鐢ㄦ埛鍙栨秷鍒犻櫎
+ });
+}
+defineExpose({
+ open
+})
+</script>
+
+<style></style>
\ No newline at end of file
diff --git a/src/pages/opportunityManagement/index.vue b/src/pages/opportunityManagement/index.vue
new file mode 100644
index 0000000..5a39f58
--- /dev/null
+++ b/src/pages/opportunityManagement/index.vue
@@ -0,0 +1,237 @@
+<template>
+ <view class="sales-account">
+ <!-- 閫氱敤澶撮儴 -->
+ <PageHeader title="鍟嗘満绠$悊" @back="goBack" />
+
+ <!-- 鎼滅储鍖哄煙 -->
+ <view class="search-section">
+ <view class="search-bar">
+ <view class="search-input">
+ <up-input
+ class="search-text"
+ placeholder="璇疯緭鍏ュ鎴峰悕绉版悳绱�"
+ v-model="customerName"
+ @change="getList"
+ clearable
+ />
+ </view>
+ <view class="filter-button" @click="getList">
+ <up-icon name="search" size="24" color="#999"></up-icon>
+ </view>
+ </view>
+ </view>
+
+ <!-- 鍟嗘満鍒楄〃 -->
+ <view class="ledger-list" v-if="opportunityList.length > 0">
+ <view v-for="(item, index) in opportunityList" :key="item.id || index">
+ <view class="ledger-item" @click="openOpportunity('detail', item)">
+ <view class="item-header">
+ <view class="item-left">
+ <view class="document-icon">
+ <up-icon name="file-text" size="16" color="#ffffff"></up-icon>
+ </view>
+ <text class="item-id">{{ item.customerName || '-' }}</text>
+ </view>
+ <view class="item-right">
+ <u-tag
+ size="mini"
+ :type="getStatusTagType(item.status)"
+ >
+ {{ getStatusText(item.status) }}
+ </u-tag>
+ </view>
+ </view>
+ <up-divider></up-divider>
+
+ <view class="item-details">
+ <view class="detail-row">
+ <text class="detail-label">鍩庡競</text>
+ <text class="detail-value">{{ item.city || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍟嗘満鏉ユ簮</text>
+ <text class="detail-value">{{ item.businessSource || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鍚堝悓閲戦(鍏�)</text>
+ <text class="detail-value highlight">{{ item.contractAmount || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">褰曞叆浜�</text>
+ <text class="detail-value">{{ item.entryPerson || '-' }}</text>
+ </view>
+ <view class="detail-row">
+ <text class="detail-label">鏇存柊鏃堕棿</text>
+ <text class="detail-value">
+ {{ formatDate(item.updateTime || item.entryDate) || '-' }}
+ </text>
+ </view>
+ </view>
+
+ <!-- 鎿嶄綔鎸夐挳 -->
+ <view class="action-buttons">
+ <u-button
+ type="primary"
+ size="small"
+ class="action-btn"
+ @click.stop="openOpportunity('edit', item)"
+ >
+ 缂栬緫
+ </u-button>
+ <u-button
+ type="warning"
+ size="small"
+ class="action-btn"
+ @click.stop="openOpportunity('addOperation', item)"
+ >
+ 娣诲姞鏀归�犲唴瀹�
+ </u-button>
+ <u-button
+ type="default"
+ size="small"
+ class="action-btn"
+ @click.stop="openOpportunity('detail', item)"
+ >
+ 璇︽儏
+ </u-button>
+ </view>
+ </view>
+ </view>
+ </view>
+ <view v-else class="no-data">
+ <text>鏆傛棤鍟嗘満鏁版嵁</text>
+ </view>
+
+ <!-- 娴姩鏂板鎸夐挳 -->
+ <view class="fab-button" @click="addOpportunity">
+ <up-icon name="plus" size="24" color="#ffffff"></up-icon>
+ </view>
+ </view>
+</template>
+
+<script setup>
+import { ref } from 'vue'
+import { onShow } from '@dcloudio/uni-app'
+import dayjs from 'dayjs'
+import PageHeader from '@/components/PageHeader.vue'
+import { opportunityListPage } from '@/api/salesManagement/opportunityManagement.js'
+
+// 鎼滅储鏉′欢
+const customerName = ref('')
+
+// 鍒楄〃鏁版嵁
+const opportunityList = ref([])
+const total = ref(0)
+
+// 杩斿洖
+const goBack = () => {
+ uni.navigateBack()
+}
+
+// 鍔犺浇涓彁绀�
+const showLoadingToast = (message) => {
+ uni.showLoading({
+ title: message,
+ mask: true
+ })
+}
+
+const closeToast = () => {
+ uni.hideLoading()
+}
+
+// 鐘舵�佹爣绛剧被鍨�
+const getStatusTagType = (status) => {
+ const typeMap = {
+ '鏂板缓': 'info',
+ '椤圭洰璺熻釜': 'primary',
+ '鍚堝悓绛剧害': 'warning',
+ '澶囨鐢虫姤': 'primary',
+ '椤圭洰浜や粯': 'success',
+ '椤圭洰楠屾敹': 'success'
+ }
+ return typeMap[status] || 'default'
+}
+
+// 鐘舵�佹枃鏈�
+const getStatusText = (status) => {
+ const textMap = {
+ '鏂板缓': '鏂板缓',
+ '椤圭洰璺熻釜': '椤圭洰璺熻釜',
+ '鍚堝悓绛剧害': '鍚堝悓绛剧害',
+ '澶囨鐢虫姤': '澶囨鐢虫姤',
+ '椤圭洰浜や粯': '椤圭洰浜や粯',
+ '椤圭洰楠屾敹': '椤圭洰楠屾敹'
+ }
+ return textMap[status] || '鏈煡'
+}
+
+// 鏍煎紡鍖栨棩鏈�
+const formatDate = (date) => {
+ if (!date) return ''
+ return dayjs(date).format('YYYY-MM-DD')
+}
+
+// 鑾峰彇鍒楄〃
+const getList = () => {
+ showLoadingToast('鍔犺浇涓�...')
+ const params = {
+ current: -1,
+ size: -1,
+ customerName: customerName.value || undefined
+ }
+
+ opportunityListPage(params)
+ .then((res) => {
+ const data = res.data || res
+ opportunityList.value = data.records || []
+ total.value = data.total || 0
+ })
+ .catch(() => {
+ uni.showToast({
+ title: '鑾峰彇鍟嗘満鏁版嵁澶辫触',
+ icon: 'none'
+ })
+ opportunityList.value = []
+ total.value = 0
+ })
+ .finally(() => {
+ closeToast()
+ })
+}
+
+// 鎵撳紑鍟嗘満鎿嶄綔椤甸潰锛堟柊澧炪�佺紪杈戙�佽鎯呫�佹坊鍔犳弿杩帮級
+const openOpportunity = (type, row) => {
+ try {
+ uni.setStorageSync('opportunityOperationType', type)
+ if (row) {
+ uni.setStorageSync('opportunityData', JSON.stringify(row))
+ } else {
+ uni.removeStorageSync('opportunityData')
+ }
+ uni.navigateTo({
+ url: '/pages/opportunityManagement/detail'
+ })
+ } catch (error) {
+ console.error('鎵撳紑鍟嗘満椤甸潰澶辫触:', error)
+ uni.showToast({
+ title: '鎿嶄綔澶辫触锛岃閲嶈瘯',
+ icon: 'none'
+ })
+ }
+}
+
+// 鏂板缓鍟嗘満
+const addOpportunity = () => {
+ openOpportunity('add')
+}
+
+onShow(() => {
+ getList()
+})
+</script>
+
+<style scoped lang="scss">
+@import '@/styles/sales-common.scss';
+</style>
+
--
Gitblit v1.9.3