gaoluyang
2026-03-03 bb44f08f420fc6b1520c06f3698f3d4f52e4a06b
公司app
1.添加商机管理功能
2.app部署修改
已添加4个文件
已修改3个文件
1934 ■■■■ 文件已修改
src/api/salesManagement/opportunityManagement.js 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/config.js 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages.json 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/index.vue 496 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/opportunityManagement/detail.vue 1035 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/opportunityManagement/fileList.vue 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/pages/opportunityManagement/index.vue 237 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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,
  });
}
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: {
     // åº”用名称
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": "采购台账",
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); }
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('解析商机数据失败:', 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>
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>
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>