<template>
|
<el-dialog
|
v-model="dialogVisible"
|
:title="dialogTitle"
|
width="95%"
|
top="5vh"
|
destroy-on-close
|
@close="closeDialog"
|
>
|
<el-form
|
ref="formRef"
|
:model="form"
|
:rules="rules"
|
label-position="top"
|
label-width="120px"
|
:disabled="isView"
|
>
|
<div class="section">
|
<div class="section-header" @click="toggleSection('base')">
|
<div class="section-title">
|
<span class="section-bar" />
|
<span>基础资料</span>
|
</div>
|
<el-icon class="toggle-icon">
|
<ArrowDown v-if="sectionCollapsed.base" />
|
<ArrowUp v-else />
|
</el-icon>
|
</div>
|
<div v-show="!sectionCollapsed.base" class="section-body">
|
<el-row :gutter="20">
|
<el-col :span="6">
|
<el-form-item label="单据编号" prop="billNo">
|
<el-input v-model="form.billNo" placeholder="系统生成" disabled />
|
</el-form-item>
|
</el-col>
|
<el-col :span="6">
|
<el-form-item label="项目名称" prop="projectName">
|
<el-input v-model="form.projectName" placeholder="请输入" clearable />
|
</el-form-item>
|
</el-col>
|
<el-col :span="6">
|
<el-form-item label="客户名称" prop="customerName">
|
<el-input v-model="form.customerName" placeholder="请输入" clearable />
|
</el-form-item>
|
</el-col>
|
</el-row>
|
|
<el-row :gutter="20">
|
<el-col :span="6">
|
<el-form-item label="立项日期" prop="setupDate">
|
<el-date-picker
|
v-model="form.setupDate"
|
type="date"
|
value-format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
placeholder="请选择"
|
style="width: 100%"
|
clearable
|
/>
|
</el-form-item>
|
</el-col>
|
<el-col :span="6">
|
<el-form-item label="项目来源" prop="projectSource">
|
<el-input v-model="form.projectSource" placeholder="请输入" clearable />
|
</el-form-item>
|
</el-col>
|
<el-col :span="6">
|
<el-form-item label="立项人" prop="creatorName">
|
<el-input v-model="form.creatorName" placeholder="请输入" clearable />
|
</el-form-item>
|
</el-col>
|
</el-row>
|
|
<el-row :gutter="20">
|
<el-col :span="6">
|
<el-form-item label="预计工期(天)" prop="estimatedDays">
|
<el-input-number v-model="form.estimatedDays" :min="0" controls-position="right" style="width: 100%" />
|
</el-form-item>
|
</el-col>
|
<el-col :span="6">
|
<el-form-item label="计划开始日期" prop="planStartDate">
|
<el-date-picker
|
v-model="form.planStartDate"
|
type="date"
|
value-format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
placeholder="请选择"
|
style="width: 100%"
|
clearable
|
/>
|
</el-form-item>
|
</el-col>
|
<el-col :span="6">
|
<el-form-item label="计划完成日期" prop="planEndDate">
|
<el-date-picker
|
v-model="form.planEndDate"
|
type="date"
|
value-format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
placeholder="请选择"
|
style="width: 100%"
|
clearable
|
/>
|
</el-form-item>
|
</el-col>
|
</el-row>
|
|
<el-row :gutter="20">
|
<el-col :span="6">
|
<el-form-item label="项目类型" prop="projectManagementPlanId">
|
<el-select v-model="form.projectManagementPlanId" placeholder="请选择" clearable style="width: 100%">
|
<el-option v-for="opt in projectTypeOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
|
</el-select>
|
</el-form-item>
|
</el-col>
|
<el-col :span="6">
|
<el-form-item label="项目金额" prop="projectAmount">
|
<el-input-number v-model="form.projectAmount" :min="0" controls-position="right" style="width: 100%" />
|
</el-form-item>
|
</el-col>
|
<el-col :span="6">
|
<el-form-item label="审核状态" prop="auditStatus">
|
<el-select v-model="form.auditStatus" placeholder="请选择" clearable style="width: 100%">
|
<el-option v-for="d in project_management" :key="d.value" :label="d.label" :value="d.value" />
|
</el-select>
|
</el-form-item>
|
</el-col>
|
|
</el-row>
|
<el-row :gutter="10" >
|
<el-col :span="24">
|
<el-upload
|
v-model:file-list="fileList"
|
:action="upload.url"
|
:headers="upload.headers"
|
multiple
|
:disabled="isView"
|
:before-upload="beforeUpload"
|
:on-success="handleUploadSuccess"
|
:on-error="handleUploadError"
|
name="files"
|
:on-remove="handleRemove"
|
>
|
<el-button type="primary" :disabled="isView">上传文件</el-button>
|
</el-upload>
|
<div v-if="existingAttachments.length > 0" class="attachment-list">
|
<div
|
v-for="(att, idx) in existingAttachments"
|
:key="att.id || att.url || idx"
|
class="attachment-item"
|
>
|
<el-icon><Document /></el-icon>
|
<span class="attachment-name">{{ att.name || att.fileName || att.url || '附件' }}</span>
|
<el-button link type="primary" size="small" @click="downloadAttachment(att)">下载</el-button>
|
</div>
|
</div>
|
</el-col>
|
</el-row>
|
<el-row :gutter="20">
|
<el-col :span="24">
|
<el-form-item label="备注" prop="remark">
|
<el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入" maxlength="100" show-word-limit />
|
</el-form-item>
|
</el-col>
|
</el-row>
|
</div>
|
</div>
|
|
<div class="section">
|
<div class="section-header" @click="toggleSection('product')">
|
<div class="section-title">
|
<span class="section-bar" />
|
<span>产品信息</span>
|
</div>
|
<div class="section-actions" @click.stop>
|
<el-button v-if="!isView" type="primary" @click="openProductForm('add')">添加</el-button>
|
<el-button v-if="!isView" plain type="danger" @click="deleteProduct">删除</el-button>
|
<el-icon class="toggle-icon" @click="toggleSection('product')">
|
<ArrowDown v-if="sectionCollapsed.product" />
|
<ArrowUp v-else />
|
</el-icon>
|
</div>
|
</div>
|
<div v-show="!sectionCollapsed.product" class="section-body">
|
<el-table
|
:data="productData"
|
border
|
show-summary
|
:summary-method="summarizeProductTable"
|
@selection-change="productSelected"
|
>
|
<el-table-column v-if="!isView" align="center" type="selection" width="55" />
|
<el-table-column align="center" label="序号" type="index" width="60" />
|
<el-table-column label="产品大类" prop="productCategory" />
|
<el-table-column label="规格型号" prop="specificationModel" />
|
<el-table-column label="单位" prop="unit" />
|
<el-table-column label="数量" prop="quantity" />
|
<el-table-column label="税率(%)" prop="taxRate" />
|
<el-table-column label="含税单价(元)" prop="taxInclusiveUnitPrice" :formatter="formattedNumber" />
|
<el-table-column label="含税总价(元)" prop="taxInclusiveTotalPrice" :formatter="formattedNumber" />
|
<el-table-column label="不含税总价(元)" prop="taxExclusiveTotalPrice" :formatter="formattedNumber" />
|
<el-table-column v-if="!isView" fixed="right" label="操作" min-width="60" align="center">
|
<template #default="scope">
|
<el-button link type="primary" size="small" @click="openProductForm('edit', scope.row, scope.$index)">编辑</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
</div>
|
</div>
|
|
<div class="section">
|
<div class="section-header" @click="toggleSection('team')">
|
<div class="section-title">
|
<span class="section-bar" />
|
<span>项目团队</span>
|
</div>
|
<div class="section-actions" @click.stop>
|
<el-button v-if="!isView" type="primary" :icon="Plus" @click="addTeamRow">新增行</el-button>
|
<el-icon class="toggle-icon" @click="toggleSection('team')">
|
<ArrowDown v-if="sectionCollapsed.team" />
|
<ArrowUp v-else />
|
</el-icon>
|
</div>
|
</div>
|
<div v-show="!sectionCollapsed.team" class="section-body">
|
<PIMTable
|
:column="teamColumns"
|
:tableData="form.teamList"
|
:tableLoading="false"
|
:isSelection="false"
|
:isShowPagination="false"
|
height="220"
|
>
|
<template #memberId="{ row }">
|
<el-select v-model="row.memberId" placeholder="请选择" filterable clearable style="width: 100%" :disabled="isView">
|
<el-option v-for="u in userOptions" :key="u.value" :label="u.label" :value="u.value" />
|
</el-select>
|
</template>
|
<template #roleId="{ row }">
|
<el-select v-model="row.roleId" placeholder="请选择" clearable style="width: 100%" :disabled="isView">
|
<el-option v-for="r in roleOptions" :key="r.value" :label="r.label" :value="r.value" />
|
</el-select>
|
</template>
|
<template #enterDate="{ row }">
|
<el-date-picker
|
v-model="row.enterDate"
|
type="date"
|
value-format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
placeholder="请选择"
|
style="width: 100%"
|
clearable
|
:disabled="isView"
|
/>
|
</template>
|
<template #leaveDate="{ row }">
|
<el-date-picker
|
v-model="row.leaveDate"
|
type="date"
|
value-format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
placeholder="请选择"
|
style="width: 100%"
|
clearable
|
:disabled="isView"
|
/>
|
</template>
|
<template #phone="{ row }">
|
<el-input v-model="row.phone" placeholder="请输入" clearable :disabled="isView" />
|
</template>
|
<template #teamRemark="{ row }">
|
<el-input v-model="row.remark" placeholder="请输入" clearable :disabled="isView" />
|
</template>
|
<template #teamAction="{ row, index }">
|
<el-button v-if="!isView" link type="danger" :icon="Delete" @click="removeTeamRow(index)">删除</el-button>
|
<span v-else>—</span>
|
</template>
|
</PIMTable>
|
</div>
|
</div>
|
|
<!-- <div class="section">
|
<div class="section-header" @click="toggleSection('phase')">
|
<div class="section-title">
|
<span class="section-bar" />
|
<span>项目阶段</span>
|
</div>
|
<div class="section-actions" @click.stop>
|
<el-button v-if="!isView" type="primary" :icon="Plus" @click="addPhaseRow">新增行</el-button>
|
<el-icon class="toggle-icon" @click="toggleSection('phase')">
|
<ArrowDown v-if="sectionCollapsed.phase" />
|
<ArrowUp v-else />
|
</el-icon>
|
</div>
|
</div>
|
<div v-show="!sectionCollapsed.phase" class="section-body">
|
<PIMTable
|
:column="phaseColumns"
|
:tableData="form.phaseList"
|
:tableLoading="false"
|
:isSelection="false"
|
:isShowPagination="false"
|
height="240"
|
>
|
<template #phaseName="{ row }">
|
<el-input v-model="row.phaseName" placeholder="请输入" clearable :disabled="isView" />
|
</template>
|
<template #phaseDesc="{ row }">
|
<el-input v-model="row.description" placeholder="请输入" clearable :disabled="isView" />
|
</template>
|
<template #ownerId="{ row }">
|
<el-select v-model="row.ownerId" placeholder="请选择" filterable clearable style="width: 100%" :disabled="isView">
|
<el-option v-for="u in userOptions" :key="u.value" :label="u.label" :value="u.value" />
|
</el-select>
|
</template>
|
<template #planDays="{ row }">
|
<el-input-number v-model="row.planDays" :min="0" controls-position="right" style="width: 100%" :disabled="isView" />
|
</template>
|
<template #planStart="{ row }">
|
<el-date-picker
|
v-model="row.planStartDate"
|
type="date"
|
value-format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
placeholder="请选择"
|
style="width: 100%"
|
clearable
|
:disabled="isView"
|
/>
|
</template>
|
<template #planEnd="{ row }">
|
<el-date-picker
|
v-model="row.planEndDate"
|
type="date"
|
value-format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
placeholder="请选择"
|
style="width: 100%"
|
clearable
|
:disabled="isView"
|
/>
|
</template>
|
<template #progress="{ row }">
|
<el-input-number v-model="row.progress" :min="0" :max="100" controls-position="right" style="width: 100%" :disabled="isView" />
|
</template>
|
<template #actualStart="{ row }">
|
<el-date-picker
|
v-model="row.actualStartDate"
|
type="date"
|
value-format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
placeholder="请选择"
|
style="width: 100%"
|
clearable
|
:disabled="isView"
|
/>
|
</template>
|
<template #actualEnd="{ row }">
|
<el-date-picker
|
v-model="row.actualEndDate"
|
type="date"
|
value-format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
placeholder="请选择"
|
style="width: 100%"
|
clearable
|
:disabled="isView"
|
/>
|
</template>
|
<template #overdueDays="{ row }">
|
<el-input-number v-model="row.overdueDays" :min="0" controls-position="right" style="width: 100%" :disabled="isView" />
|
</template>
|
<template #completion="{ row }">
|
<el-input v-model="row.completionRemark" placeholder="请输入" clearable :disabled="isView" />
|
</template>
|
<template #phaseAction="{ row, index }">
|
<el-button v-if="!isView" link type="danger" :icon="Delete" @click="removePhaseRow(index)">删除</el-button>
|
<span v-else>—</span>
|
</template>
|
</PIMTable>
|
</div>
|
</div> -->
|
|
<div class="section">
|
<div class="section-header" @click="toggleSection('address')">
|
<div class="section-title">
|
<span class="section-bar" />
|
<span>收货地址</span>
|
</div>
|
<div class="section-actions" @click.stop>
|
<el-button v-if="!isView" type="primary" :icon="Plus" @click="addAddressRow">新增行</el-button>
|
<el-icon class="toggle-icon" @click="toggleSection('address')">
|
<ArrowDown v-if="sectionCollapsed.address" />
|
<ArrowUp v-else />
|
</el-icon>
|
</div>
|
</div>
|
<div v-show="!sectionCollapsed.address" class="section-body">
|
<PIMTable
|
:column="addressColumns"
|
:tableData="form.addressList"
|
:tableLoading="false"
|
:isSelection="false"
|
:isShowPagination="false"
|
height="200"
|
>
|
<template #receiver="{ row }">
|
<el-input v-model="row.receiver" placeholder="请输入" clearable :disabled="isView" />
|
</template>
|
<template #receiverPhone="{ row }">
|
<el-input v-model="row.phone" placeholder="请输入" clearable :disabled="isView" />
|
</template>
|
<template #receiverAddress="{ row }">
|
<el-input v-model="row.address" placeholder="请输入" clearable :disabled="isView" />
|
</template>
|
<template #addressAction="{ row, index }">
|
<el-button v-if="!isView" link type="danger" :icon="Delete" @click="removeAddressRow(index)">删除</el-button>
|
<span v-else>—</span>
|
</template>
|
</PIMTable>
|
</div>
|
</div>
|
|
<div class="section">
|
<div class="section-header" @click="toggleSection('contact')">
|
<div class="section-title">
|
<span class="section-bar" />
|
<span>联系信息</span>
|
</div>
|
<el-icon class="toggle-icon">
|
<ArrowDown v-if="sectionCollapsed.contact" />
|
<ArrowUp v-else />
|
</el-icon>
|
</div>
|
<div v-show="!sectionCollapsed.contact" class="section-body">
|
<el-row :gutter="20">
|
<el-col :span="6">
|
<el-form-item label="联系人姓名" prop="contactName">
|
<el-input v-model="form.contactName" placeholder="请输入" clearable />
|
</el-form-item>
|
</el-col>
|
<el-col :span="6">
|
<el-form-item label="性别" prop="contactGender">
|
<el-select v-model="form.contactGender" placeholder="请选择" clearable style="width: 100%">
|
<el-option label="男" value="1" />
|
<el-option label="女" value="2" />
|
</el-select>
|
</el-form-item>
|
</el-col>
|
<el-col :span="6">
|
<el-form-item label="生日" prop="contactBirthday">
|
<el-date-picker
|
v-model="form.contactBirthday"
|
type="date"
|
value-format="YYYY-MM-DD"
|
format="YYYY-MM-DD"
|
placeholder="请选择"
|
style="width: 100%"
|
clearable
|
/>
|
</el-form-item>
|
</el-col>
|
<el-col :span="6">
|
<el-form-item label="邮箱" prop="contactEmail">
|
<el-input v-model="form.contactEmail" placeholder="请输入" clearable />
|
</el-form-item>
|
</el-col>
|
</el-row>
|
|
<el-row :gutter="20">
|
<el-col :span="6">
|
<el-form-item label="部门" prop="contactDept">
|
<el-input v-model="form.contactDept" placeholder="请输入" clearable />
|
</el-form-item>
|
</el-col>
|
<el-col :span="6">
|
<el-form-item label="职务" prop="contactJob">
|
<el-input v-model="form.contactJob" placeholder="请输入" clearable />
|
</el-form-item>
|
</el-col>
|
<el-col :span="6">
|
<el-form-item label="手机号码" prop="contactMobile">
|
<el-input v-model="form.contactMobile" placeholder="请输入" clearable />
|
</el-form-item>
|
</el-col>
|
<el-col :span="6">
|
<el-form-item label="微信号码" prop="contactWechat">
|
<el-input v-model="form.contactWechat" placeholder="请输入" clearable />
|
</el-form-item>
|
</el-col>
|
</el-row>
|
|
<el-row :gutter="20">
|
<el-col :span="6">
|
<el-form-item label="QQ" prop="contactQq">
|
<el-input v-model="form.contactQq" placeholder="请输入" clearable />
|
</el-form-item>
|
</el-col>
|
<el-col :span="6">
|
<el-form-item label="企业微信" prop="contactWorkWechat">
|
<el-input v-model="form.contactWorkWechat" placeholder="请输入" clearable />
|
</el-form-item>
|
</el-col>
|
<el-col :span="12">
|
<el-form-item label="地址" prop="contactAddress">
|
<el-input v-model="form.contactAddress" placeholder="请输入" clearable />
|
</el-form-item>
|
</el-col>
|
</el-row>
|
|
<el-row :gutter="20">
|
<el-col :span="24">
|
<el-form-item label="备注" prop="contactRemark">
|
<el-input v-model="form.contactRemark" type="textarea" :rows="2" placeholder="请输入" maxlength="200" show-word-limit />
|
</el-form-item>
|
</el-col>
|
</el-row>
|
</div>
|
</div>
|
</el-form>
|
|
<template #footer>
|
<div class="dialog-footer">
|
<el-button v-if="!isView" type="primary" @click="submitForm">确认</el-button>
|
<el-button @click="closeDialog">{{ isView ? '关闭' : '取消' }}</el-button>
|
</div>
|
</template>
|
</el-dialog>
|
|
<FormDialog
|
v-model="productFormVisible"
|
:title="productOperationType === 'add' ? '新增产品' : '编辑产品'"
|
:width="'40%'"
|
:operation-type="productOperationType"
|
@close="closeProductDia"
|
@confirm="submitProduct"
|
@cancel="closeProductDia"
|
>
|
<el-form ref="productFormRef" :model="productForm" label-width="140px" label-position="top" :rules="productRules">
|
<el-row :gutter="30">
|
<el-col :span="24">
|
<el-form-item label="产品大类:" prop="productCategoryId">
|
<el-tree-select
|
v-model="productForm.productCategoryId"
|
placeholder="请选择"
|
clearable
|
check-strictly
|
:data="productCategoryOptions"
|
:render-after-expand="false"
|
style="width: 100%"
|
@change="getModels"
|
/>
|
</el-form-item>
|
</el-col>
|
</el-row>
|
<el-row :gutter="30">
|
<el-col :span="24">
|
<el-form-item label="规格型号:" prop="productModelId">
|
<el-select v-model="productForm.productModelId" placeholder="请选择" clearable filterable @change="getProductModel">
|
<el-option v-for="item in modelOptions" :key="item.id" :label="item.model" :value="item.id" />
|
</el-select>
|
</el-form-item>
|
</el-col>
|
</el-row>
|
<el-row :gutter="30">
|
<el-col :span="12">
|
<el-form-item label="单位:" prop="unit">
|
<el-input v-model="productForm.unit" placeholder="请输入" clearable />
|
</el-form-item>
|
</el-col>
|
<el-col :span="12">
|
<el-form-item label="税率(%):" prop="taxRate">
|
<el-select v-model="productForm.taxRate" placeholder="请选择" clearable @change="calculateFromTaxRate">
|
<el-option label="1" value="1" />
|
<el-option label="6" value="6" />
|
<el-option label="13" value="13" />
|
</el-select>
|
</el-form-item>
|
</el-col>
|
</el-row>
|
<el-row :gutter="30">
|
<el-col :span="12">
|
<el-form-item label="含税单价(元):" prop="taxInclusiveUnitPrice">
|
<el-input-number
|
v-model="productForm.taxInclusiveUnitPrice"
|
:step="0.01"
|
:min="0"
|
:precision="2"
|
style="width: 100%"
|
placeholder="请输入"
|
clearable
|
@change="calculateFromUnitPrice"
|
/>
|
</el-form-item>
|
</el-col>
|
<el-col :span="12">
|
<el-form-item label="数量:" prop="quantity">
|
<el-input-number
|
v-model="productForm.quantity"
|
:step="0.1"
|
:min="0"
|
:precision="2"
|
style="width: 100%"
|
placeholder="请输入"
|
clearable
|
@change="calculateFromQuantity"
|
/>
|
</el-form-item>
|
</el-col>
|
</el-row>
|
<el-row :gutter="30">
|
<el-col :span="12">
|
<el-form-item label="含税总价(元):" prop="taxInclusiveTotalPrice">
|
<el-input v-model="productForm.taxInclusiveTotalPrice" placeholder="请输入" clearable @change="calculateFromTotalPrice" />
|
</el-form-item>
|
</el-col>
|
<el-col :span="12">
|
<el-form-item label="不含税总价(元):" prop="taxExclusiveTotalPrice">
|
<el-input v-model="productForm.taxExclusiveTotalPrice" placeholder="请输入" clearable @change="calculateFromExclusiveTotalPrice" />
|
</el-form-item>
|
</el-col>
|
</el-row>
|
<el-row :gutter="30">
|
<el-col :span="12">
|
<el-form-item label="发票类型:" prop="invoiceType">
|
<el-select v-model="productForm.invoiceType" placeholder="请选择" clearable>
|
<el-option label="增普票" value="增普票" />
|
<el-option label="增专票" value="增专票" />
|
</el-select>
|
</el-form-item>
|
</el-col>
|
</el-row>
|
</el-form>
|
</FormDialog>
|
</template>
|
|
<script setup name="ProjectManagementFormDia">
|
import { computed, getCurrentInstance, reactive, ref, toRefs } from 'vue'
|
import { ArrowDown, ArrowUp, Delete, Plus, Document } from '@element-plus/icons-vue'
|
import { ElMessage } from 'element-plus'
|
import { getToken } from '@/utils/auth'
|
import PIMTable from '@/components/PIMTable/PIMTable.vue'
|
import FormDialog from '@/components/Dialog/FormDialog.vue'
|
import { listPlan } from '@/api/projectManagement/projectType'
|
import { findRoleListPage } from '@/api/projectManagement/role'
|
import { userListAll } from '@/api/publicApi'
|
import { addProject, getProject, updateProject } from '@/api/projectManagement/project'
|
import { modelList, productTreeList } from '@/api/basicData/product'
|
import { delProduct as delSalesProduct } from '@/api/salesManagement/salesLedger'
|
|
const emit = defineEmits(['completed'])
|
const { proxy } = getCurrentInstance()
|
const { bill_status, project_management, plan_status } = proxy.useDict('bill_status', 'project_management', 'plan_status')
|
|
const dialogVisible = ref(false)
|
const operationType = ref('add')
|
const formRef = ref()
|
const fileList = ref([])
|
const existingAttachments = ref([])
|
const upload = reactive({
|
url: import.meta.env.VITE_APP_BASE_API + '/basic/customer-follow/upload',
|
headers: { Authorization: 'Bearer ' + getToken() }
|
})
|
|
const projectTypeOptions = ref([])
|
const roleOptions = ref([])
|
const userOptions = ref([])
|
const productData = ref([])
|
const productSelectedRows = ref([])
|
const productCategoryOptions = ref([])
|
const modelOptions = ref([])
|
const productFormVisible = ref(false)
|
const productOperationType = ref('add')
|
const productFormRef = ref()
|
const productIndex = ref(0)
|
const isCalculating = ref(false)
|
|
const productFormData = reactive({
|
productForm: {
|
productCategoryId: undefined,
|
productCategory: '',
|
productModelId: undefined,
|
specificationModel: '',
|
unit: '',
|
quantity: '',
|
taxInclusiveUnitPrice: '',
|
taxRate: '',
|
taxInclusiveTotalPrice: '',
|
taxExclusiveTotalPrice: '',
|
invoiceType: ''
|
},
|
productRules: {
|
productCategoryId: [{ required: true, message: '请选择', trigger: 'change' }],
|
productModelId: [{ required: true, message: '请选择', trigger: 'change' }],
|
unit: [{ required: true, message: '请输入', trigger: 'blur' }],
|
quantity: [{ required: true, message: '请输入', trigger: 'blur' }],
|
taxInclusiveUnitPrice: [{ required: true, message: '请输入', trigger: 'blur' }],
|
taxRate: [{ required: true, message: '请选择', trigger: 'change' }],
|
taxInclusiveTotalPrice: [{ required: true, message: '请输入', trigger: 'blur' }],
|
taxExclusiveTotalPrice: [{ required: true, message: '请输入', trigger: 'blur' }],
|
invoiceType: [{ required: true, message: '请选择', trigger: 'change' }]
|
}
|
})
|
|
const { productForm, productRules } = toRefs(productFormData)
|
|
const data = reactive({
|
form: {
|
id: undefined,
|
clientId: undefined,
|
parentProjectId: undefined,
|
projectManagementPlanId: undefined,
|
managerId: undefined,
|
salesmanId: undefined,
|
salesmanName: '',
|
actualStartDate: '',
|
actualEndDate: '',
|
departmentId: undefined,
|
departmentName: '',
|
orderDate: '',
|
billNo: '',
|
projectName: '',
|
customerName: '',
|
parentProjectName: '',
|
setupDate: '',
|
projectSource: '',
|
creatorName: '',
|
billStatus: '',
|
projectStage: '',
|
estimatedDays: 0,
|
planStartDate: '',
|
planEndDate: '',
|
projectManagementPlanId: undefined,
|
projectAmount: 0,
|
auditStatus: '',
|
remark: '',
|
attachmentIds: [],
|
teamList: [],
|
phaseList: [],
|
addressList: [],
|
contactName: '',
|
contactGender: '',
|
contactBirthday: '',
|
contactEmail: '',
|
contactDept: '',
|
contactJob: '',
|
contactMobile: '',
|
contactWechat: '',
|
contactQq: '',
|
contactWorkWechat: '',
|
contactAddress: '',
|
contactRemark: ''
|
},
|
rules: {
|
projectName: [{ required: true, message: '请输入项目名称', trigger: 'blur' }]
|
}
|
})
|
|
const { form, rules } = toRefs(data)
|
|
const sectionCollapsed = reactive({
|
base: false,
|
product: false,
|
team: false,
|
phase: false,
|
address: false,
|
contact: false
|
})
|
|
const isView = computed(() => operationType.value === 'view')
|
const dialogTitle = computed(() => {
|
if (operationType.value === 'add') return '新增项目'
|
if (operationType.value === 'edit') return '编辑项目'
|
return '项目详情'
|
})
|
|
const teamColumns = [
|
{ label: '姓名', prop: 'memberId', align: 'center', width: 180, dataType: 'slot', slot: 'memberId' },
|
{ label: '项目组角色', prop: 'roleId', align: 'center', width: 160, dataType: 'slot', slot: 'roleId' },
|
{ label: '进入日期', prop: 'enterDate', align: 'center', width: 160, dataType: 'slot', slot: 'enterDate' },
|
{ label: '离开日期', prop: 'leaveDate', align: 'center', width: 160, dataType: 'slot', slot: 'leaveDate' },
|
{ label: '联系方式', prop: 'phone', align: 'center', width: 180, dataType: 'slot', slot: 'phone' },
|
{ label: '备注', prop: 'remark', align: 'center', dataType: 'slot', slot: 'teamRemark' },
|
{ label: '操作', prop: 'action', align: 'center', width: 100, dataType: 'slot', slot: 'teamAction', fixed: 'right' }
|
]
|
|
const phaseColumns = [
|
{ label: '阶段名称', prop: 'phaseName', align: 'center', width: 160, dataType: 'slot', slot: 'phaseName' },
|
{ label: '描述', prop: 'description', align: 'center', width: 200, dataType: 'slot', slot: 'phaseDesc' },
|
{ label: '负责人', prop: 'ownerId', align: 'center', width: 160, dataType: 'slot', slot: 'ownerId' },
|
{ label: '预计工期(天)', prop: 'planDays', align: 'center', width: 140, dataType: 'slot', slot: 'planDays' },
|
{ label: '计划开始日期', prop: 'planStartDate', align: 'center', width: 160, dataType: 'slot', slot: 'planStart' },
|
{ label: '计划结束日期', prop: 'planEndDate', align: 'center', width: 160, dataType: 'slot', slot: 'planEnd' },
|
{ label: '进度(%)', prop: 'progress', align: 'center', width: 120, dataType: 'slot', slot: 'progress' },
|
{ label: '实际开始日期', prop: 'actualStartDate', align: 'center', width: 160, dataType: 'slot', slot: 'actualStart' },
|
{ label: '实际结束日期', prop: 'actualEndDate', align: 'center', width: 160, dataType: 'slot', slot: 'actualEnd' },
|
{ label: '逾期天数', prop: 'overdueDays', align: 'center', width: 120, dataType: 'slot', slot: 'overdueDays' },
|
{ label: '完成情况', prop: 'completionRemark', align: 'center', width: 200, dataType: 'slot', slot: 'completion' },
|
{ label: '操作', prop: 'action', align: 'center', width: 100, dataType: 'slot', slot: 'phaseAction', fixed: 'right' }
|
]
|
|
const addressColumns = [
|
{ label: '收货人', prop: 'receiver', align: 'center', width: 180, dataType: 'slot', slot: 'receiver' },
|
{ label: '联系方式', prop: 'phone', align: 'center', width: 180, dataType: 'slot', slot: 'receiverPhone' },
|
{ label: '收货地址', prop: 'address', align: 'center', dataType: 'slot', slot: 'receiverAddress' },
|
{ label: '操作', prop: 'action', align: 'center', width: 100, dataType: 'slot', slot: 'addressAction', fixed: 'right' }
|
]
|
|
function toggleSection(key) {
|
sectionCollapsed[key] = !sectionCollapsed[key]
|
}
|
|
function resetFormData() {
|
Object.assign(form.value, {
|
id: undefined,
|
clientId: undefined,
|
parentProjectId: undefined,
|
projectManagementPlanId: undefined,
|
managerId: undefined,
|
salesmanId: undefined,
|
salesmanName: '',
|
actualStartDate: '',
|
actualEndDate: '',
|
departmentId: undefined,
|
departmentName: '',
|
orderDate: '',
|
billNo: '',
|
projectName: '',
|
customerName: '',
|
parentProjectName: '',
|
setupDate: '',
|
projectSource: '',
|
creatorName: '',
|
billStatus: '',
|
projectStage: '',
|
estimatedDays: 0,
|
planStartDate: '',
|
planEndDate: '',
|
projectManagementPlanId: undefined,
|
projectAmount: 0,
|
auditStatus: '',
|
remark: '',
|
attachmentIds: [],
|
teamList: [],
|
phaseList: [],
|
addressList: [],
|
contactName: '',
|
contactGender: '',
|
contactBirthday: '',
|
contactEmail: '',
|
contactDept: '',
|
contactJob: '',
|
contactMobile: '',
|
contactWechat: '',
|
contactQq: '',
|
contactWorkWechat: '',
|
contactAddress: '',
|
contactRemark: ''
|
})
|
fileList.value = []
|
productData.value = []
|
}
|
|
function formattedNumber(row, column, cellValue) {
|
const val = Number(cellValue ?? 0)
|
return Number.isFinite(val) ? val.toFixed(2) : '0.00'
|
}
|
|
function summarizeProductTable(param) {
|
return proxy.summarizeTable(param, ['taxInclusiveTotalPrice', 'taxExclusiveTotalPrice'])
|
}
|
|
function productSelected(selection) {
|
productSelectedRows.value = selection
|
}
|
|
function convertIdToValue(data) {
|
return (Array.isArray(data) ? data : []).map(item => {
|
const { id, children, ...rest } = item
|
const newItem = {
|
...rest,
|
value: id
|
}
|
if (children && children.length > 0) {
|
newItem.children = convertIdToValue(children)
|
}
|
return newItem
|
})
|
}
|
|
function findNodeById(nodes, productId) {
|
for (let i = 0; i < (nodes || []).length; i++) {
|
if (nodes[i].value === productId) {
|
return nodes[i].label
|
}
|
if (nodes[i].children && nodes[i].children.length > 0) {
|
const foundNode = findNodeById(nodes[i].children, productId)
|
if (foundNode) return foundNode
|
}
|
}
|
return null
|
}
|
|
function findNodeIdByLabel(nodes, label) {
|
if (!label) return null
|
for (let i = 0; i < (nodes || []).length; i++) {
|
const node = nodes[i]
|
if (node.label === label) return node.value
|
if (node.children && node.children.length > 0) {
|
const found = findNodeIdByLabel(node.children, label)
|
if (found !== null && found !== undefined) return found
|
}
|
}
|
return null
|
}
|
|
function getProductOptions() {
|
return productTreeList().then(res => {
|
const list = res?.data || res
|
productCategoryOptions.value = convertIdToValue(list)
|
return productCategoryOptions.value
|
})
|
}
|
|
function getModels(value) {
|
const categoryLabel = findNodeById(productCategoryOptions.value, value)
|
productForm.value.productCategory = categoryLabel || ''
|
modelList({ id: value }).then(res => {
|
modelOptions.value = res?.data || res || []
|
})
|
}
|
|
function getProductModel(value) {
|
const index = (modelOptions.value || []).findIndex(item => item.id === value)
|
if (index !== -1) {
|
productForm.value.specificationModel = modelOptions.value[index].model
|
productForm.value.unit = modelOptions.value[index].unit
|
} else {
|
productForm.value.specificationModel = ''
|
productForm.value.unit = ''
|
}
|
}
|
|
async function openProductForm(type, row, index) {
|
productOperationType.value = type
|
productIndex.value = index || 0
|
productForm.value = {}
|
proxy.resetForm('productFormRef')
|
|
if (!productCategoryOptions.value || productCategoryOptions.value.length === 0) {
|
await getProductOptions()
|
}
|
|
if (type === 'edit' && row) {
|
productForm.value = { ...row }
|
try {
|
const categoryId = findNodeIdByLabel(productCategoryOptions.value, productForm.value.productCategory)
|
if (categoryId) {
|
productForm.value.productCategoryId = categoryId
|
const models = await modelList({ id: categoryId })
|
modelOptions.value = models?.data || models || []
|
const currentModel = (modelOptions.value || []).find(m => m.model === productForm.value.specificationModel)
|
if (currentModel) {
|
productForm.value.productModelId = currentModel.id
|
}
|
}
|
} catch {}
|
} else {
|
productForm.value = {
|
productCategoryId: undefined,
|
productCategory: '',
|
productModelId: undefined,
|
specificationModel: '',
|
unit: '',
|
quantity: '',
|
taxInclusiveUnitPrice: '',
|
taxRate: '',
|
taxInclusiveTotalPrice: '',
|
taxExclusiveTotalPrice: '',
|
invoiceType: ''
|
}
|
}
|
|
productFormVisible.value = true
|
}
|
|
function closeProductDia() {
|
proxy.resetForm('productFormRef')
|
productFormVisible.value = false
|
}
|
|
function submitProduct() {
|
productFormRef.value?.validate?.(valid => {
|
if (!valid) return
|
const payload = { ...productForm.value }
|
if (productOperationType.value === 'add') {
|
productData.value.push(payload)
|
} else {
|
productData.value[productIndex.value] = payload
|
}
|
closeProductDia()
|
})
|
}
|
|
function deleteProduct() {
|
if (!productSelectedRows.value || productSelectedRows.value.length === 0) {
|
proxy.$modal?.msgWarning?.('请选择数据')
|
return
|
}
|
const selectedIds = productSelectedRows.value.map(r => r?.id).filter(Boolean)
|
if (operationType.value !== 'add' && selectedIds.length > 0) {
|
delSalesProduct(selectedIds)
|
.then(() => {
|
proxy.$modal?.msgSuccess?.('删除成功')
|
productData.value = productData.value.filter(row => !selectedIds.includes(row?.id))
|
productSelectedRows.value = []
|
})
|
.catch(() => {})
|
return
|
}
|
|
productData.value = productData.value.filter(row => !productSelectedRows.value.includes(row))
|
productSelectedRows.value = []
|
}
|
|
function calculateFromTotalPrice() {
|
if (isCalculating.value) return
|
const totalPrice = parseFloat(productForm.value.taxInclusiveTotalPrice)
|
const quantity = parseFloat(productForm.value.quantity)
|
if (!totalPrice || !quantity || quantity <= 0) return
|
isCalculating.value = true
|
productForm.value.taxInclusiveUnitPrice = (totalPrice / quantity).toFixed(2)
|
if (productForm.value.taxRate) {
|
productForm.value.taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice(totalPrice, productForm.value.taxRate)
|
}
|
isCalculating.value = false
|
}
|
|
function calculateFromExclusiveTotalPrice() {
|
if (!productForm.value.taxRate) {
|
proxy.$modal?.msgWarning?.('请先选择税率')
|
return
|
}
|
if (isCalculating.value) return
|
const exclusiveTotalPrice = parseFloat(productForm.value.taxExclusiveTotalPrice)
|
const quantity = parseFloat(productForm.value.quantity)
|
const taxRate = parseFloat(productForm.value.taxRate)
|
if (!exclusiveTotalPrice || !quantity || quantity <= 0 || !taxRate) return
|
isCalculating.value = true
|
const taxRateDecimal = taxRate / 100
|
const inclusiveTotalPrice = exclusiveTotalPrice / (1 - taxRateDecimal)
|
productForm.value.taxInclusiveTotalPrice = inclusiveTotalPrice.toFixed(2)
|
productForm.value.taxInclusiveUnitPrice = (inclusiveTotalPrice / quantity).toFixed(2)
|
isCalculating.value = false
|
}
|
|
function calculateFromQuantity() {
|
if (!productForm.value.taxRate) {
|
proxy.$modal?.msgWarning?.('请先选择税率')
|
return
|
}
|
if (isCalculating.value) return
|
const quantity = parseFloat(productForm.value.quantity)
|
const unitPrice = parseFloat(productForm.value.taxInclusiveUnitPrice)
|
if (!quantity || quantity <= 0 || !unitPrice) return
|
isCalculating.value = true
|
productForm.value.taxInclusiveTotalPrice = (unitPrice * quantity).toFixed(2)
|
if (productForm.value.taxRate) {
|
productForm.value.taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice(
|
productForm.value.taxInclusiveTotalPrice,
|
productForm.value.taxRate
|
)
|
}
|
isCalculating.value = false
|
}
|
|
function calculateFromUnitPrice() {
|
if (!productForm.value.taxRate) {
|
proxy.$modal?.msgWarning?.('请先选择税率')
|
return
|
}
|
if (isCalculating.value) return
|
const quantity = parseFloat(productForm.value.quantity)
|
const unitPrice = parseFloat(productForm.value.taxInclusiveUnitPrice)
|
if (!quantity || quantity <= 0 || !unitPrice) return
|
isCalculating.value = true
|
productForm.value.taxInclusiveTotalPrice = (unitPrice * quantity).toFixed(2)
|
if (productForm.value.taxRate) {
|
productForm.value.taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice(
|
productForm.value.taxInclusiveTotalPrice,
|
productForm.value.taxRate
|
)
|
}
|
isCalculating.value = false
|
}
|
|
function calculateFromTaxRate() {
|
if (!productForm.value.taxRate) {
|
proxy.$modal?.msgWarning?.('请先选择税率')
|
return
|
}
|
if (isCalculating.value) return
|
const inclusiveTotalPrice = parseFloat(productForm.value.taxInclusiveTotalPrice)
|
const taxRate = parseFloat(productForm.value.taxRate)
|
if (!inclusiveTotalPrice || !taxRate) return
|
isCalculating.value = true
|
productForm.value.taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice(inclusiveTotalPrice, taxRate)
|
isCalculating.value = false
|
}
|
|
async function loadProjectTypeOptions() {
|
try {
|
const res = await listPlan({ current: 1, size: 999 })
|
const records = res?.data?.records || res?.records || res?.rows || []
|
projectTypeOptions.value = records.map(item => ({ label: item.name, value: item.id }))
|
} catch {
|
projectTypeOptions.value = []
|
}
|
}
|
|
async function loadRoleOptions() {
|
try {
|
const res = await findRoleListPage({ pageNum: 1, pageSize: 999 })
|
const records = res?.data?.records || res?.rows || res?.records || []
|
roleOptions.value = records.map(item => ({ label: item.roleName || item.name, value: item.id }))
|
} catch {
|
roleOptions.value = []
|
}
|
}
|
|
async function loadUserOptions() {
|
try {
|
const res = await userListAll()
|
const list = res?.data || res?.rows || res || []
|
userOptions.value = (Array.isArray(list) ? list : []).map(u => ({
|
label: u.nickName || u.userName || u.username || u.name,
|
value: u.userId || u.id
|
}))
|
} catch {
|
userOptions.value = []
|
}
|
}
|
|
function addTeamRow() {
|
form.value.teamList.push({
|
memberId: undefined,
|
roleId: undefined,
|
enterDate: '',
|
leaveDate: '',
|
phone: '',
|
remark: ''
|
})
|
}
|
|
function removeTeamRow(index) {
|
if (index > -1) form.value.teamList.splice(index, 1)
|
}
|
|
function addPhaseRow() {
|
form.value.phaseList.push({
|
phaseName: '',
|
description: '',
|
ownerId: undefined,
|
planDays: 0,
|
planStartDate: '',
|
planEndDate: '',
|
progress: 0,
|
actualStartDate: '',
|
actualEndDate: '',
|
overdueDays: 0,
|
completionRemark: ''
|
})
|
}
|
|
function removePhaseRow(index) {
|
if (index > -1) form.value.phaseList.splice(index, 1)
|
}
|
|
function addAddressRow() {
|
form.value.addressList.push({
|
receiver: '',
|
phone: '',
|
address: ''
|
})
|
}
|
|
function removeAddressRow(index) {
|
if (index > -1) form.value.addressList.splice(index, 1)
|
}
|
|
function beforeUpload() {
|
if (isView.value) return false
|
proxy.$modal?.loading?.('正在上传文件,请稍候...')
|
return true
|
}
|
|
function handleUploadError() {
|
proxy.$modal?.closeLoading?.()
|
ElMessage.error('上传文件失败')
|
}
|
|
function handleUploadSuccess(res, file) {
|
console.log(res, file)
|
proxy.$modal?.closeLoading?.()
|
if (res?.code !== 200) {
|
ElMessage.error(res?.msg || '上传失败')
|
return
|
}
|
const attachmentId = res?.data?.[0]?.id ?? ""
|
if (!attachmentId) return
|
form.value.attachmentIds.push(attachmentId)
|
console.log(form.value.attachmentIds)
|
ElMessage.success('上传成功')
|
}
|
|
function handleRemove(file) {
|
const attachmentId = file?.attachmentId
|
if (!attachmentId) return
|
form.value.attachmentIds = (form.value.attachmentIds || []).filter(id => id !== attachmentId)
|
}
|
|
async function openDialog(payload = {}) {
|
operationType.value = payload.operationType || 'add'
|
resetFormData()
|
await Promise.all([loadProjectTypeOptions(), loadRoleOptions(), loadUserOptions(), getProductOptions()])
|
if (payload.row?.id) {
|
try {
|
const res = await getProject(payload.row.id)
|
const detail = res?.data?.data ?? res?.data ?? res
|
const info = detail?.info || {}
|
const shippingAddress = detail?.shippingAddress || {}
|
const contractInfo = detail?.contractInfo || {}
|
|
const normalizeId = v => {
|
if (v === undefined || v === null || v === '') return undefined
|
const n = Number(v)
|
return Number.isNaN(n) ? v : n
|
}
|
|
const normalizeDictValue = v => {
|
if (v === undefined || v === null || v === '') return ''
|
return String(v)
|
}
|
|
const computeEstimatedDays = (start, end) => {
|
if (!start || !end) return 0
|
const startTime = new Date(`${start}T00:00:00`).getTime()
|
const endTime = new Date(`${end}T00:00:00`).getTime()
|
if (!Number.isFinite(startTime) || !Number.isFinite(endTime)) return 0
|
if (endTime < startTime) return 0
|
return Math.floor((endTime - startTime) / (24 * 60 * 60 * 1000)) + 1
|
}
|
|
Object.assign(form.value, {
|
id: info.id,
|
billNo: info.no ?? '',
|
projectManagementPlanId: info.projectManagementPlanId ?? '',
|
estimatedDays: Number(info.estimatedDays) || computeEstimatedDays(info.planStartTime, info.planEndTime) || 0,
|
projectName: info.title ?? '',
|
customerName: info.clientName ?? '',
|
parentProjectName: info.projectManagementInfoParentName ?? '',
|
setupDate: info.establishTime ?? '',
|
projectSource: info.source ?? '',
|
creatorName: info.managerName ?? '',
|
billStatus: normalizeDictValue(info.status),
|
projectStage: normalizeDictValue(info.stage ?? info.projectStage),
|
planStartDate: info.planStartTime ?? '',
|
planEndDate: info.planEndTime ?? '',
|
projectAmount: info.orderAmount ?? 0,
|
auditStatus: normalizeDictValue(info.reviewStatus),
|
remark: info.remark ?? '',
|
attachmentIds: Array.isArray(info.attachmentIds) ? info.attachmentIds : [],
|
teamList: Array.isArray(info.teamList) ? info.teamList.map(t => ({
|
memberId: normalizeId(t.userId),
|
roleId: normalizeId(t.userRoleId),
|
enterDate: t.joinTime,
|
leaveDate: t.departTime,
|
phone: t.contact,
|
remark: t.remark
|
})) : [],
|
addressList: shippingAddress?.address
|
? [{
|
receiver: shippingAddress.consignee,
|
phone: shippingAddress.contract,
|
address: shippingAddress.address
|
}]
|
: [],
|
contactName: contractInfo.name ?? '',
|
contactGender: contractInfo.sex === '男' ? '1' : contractInfo.sex === '女' ? '2' : '',
|
contactBirthday: contractInfo.birthday ?? '',
|
contactDept: contractInfo.department ?? '',
|
contactJob: contractInfo.job ?? '',
|
contactMobile: contractInfo.phoneNumber ?? '',
|
contactEmail: contractInfo.email ?? '',
|
contactQq: contractInfo.qq ?? '',
|
contactWechat: contractInfo.wx ?? '',
|
contactWorkWechat: contractInfo.lineaFissa ?? '',
|
contactAddress: contractInfo.origineEtnica ?? '',
|
contactRemark: contractInfo.rappresentanteLegale ?? ''
|
})
|
|
existingAttachments.value = Array.isArray(info.attachmentList)
|
? info.attachmentList.map(a => ({
|
id: a.id ?? a.fileId,
|
name: a.fileName ?? a.name,
|
url: a.url ?? a.fileUrl ?? a.path
|
}))
|
: []
|
|
const rawPhaseList =
|
detail?.phaseList ||
|
detail?.projectPhaseList ||
|
detail?.projectStageList ||
|
info?.phaseList ||
|
info?.projectPhaseList ||
|
[]
|
form.value.phaseList = Array.isArray(rawPhaseList)
|
? rawPhaseList.map(p => ({
|
phaseName: p.phaseName ?? p.name ?? p.title ?? '',
|
description: p.description ?? p.workContent ?? p.desc ?? '',
|
ownerId: normalizeId(p.ownerId ?? p.leaderId ?? p.userId),
|
planDays: Number(p.planDays ?? p.estimatedDuration ?? p.estimatedDays) || 0,
|
planStartDate: p.planStartDate ?? p.planStartTime ?? p.startDate ?? '',
|
planEndDate: p.planEndDate ?? p.planEndTime ?? p.endDate ?? '',
|
progress: Number(p.progress ?? p.schedule) || 0,
|
actualStartDate: p.actualStartDate ?? p.actualStartTime ?? '',
|
actualEndDate: p.actualEndDate ?? p.actualEndTime ?? '',
|
overdueDays: Number(p.overdueDays ?? p.overDays) || 0,
|
completionRemark: p.completionRemark ?? p.remark ?? ''
|
}))
|
: []
|
|
productData.value = detail?.salesLedgerProductList || detail?.productData || []
|
} catch {}
|
}
|
if (form.value.teamList.length === 0 && !isView.value) addTeamRow()
|
if (form.value.phaseList.length === 0 && !isView.value) addPhaseRow()
|
dialogVisible.value = true
|
}
|
|
function downloadAttachment(att) {
|
if (att?.name) {
|
try {
|
proxy.$download.name(att.url);
|
return
|
} catch (e) {}
|
}
|
ElMessage.warning('附件暂无下载地址')
|
}
|
function closeDialog() {
|
dialogVisible.value = false
|
}
|
|
async function submitForm() {
|
if (isView.value) {
|
closeDialog()
|
return
|
}
|
await formRef.value?.validate?.()
|
if (!productData.value || productData.value.length === 0) {
|
proxy.$modal?.msgWarning?.('请添加产品信息')
|
return
|
}
|
const findLabel = (list, value) => (list || []).find(i => String(i.value) === String(value))?.label
|
const teamList = (form.value.teamList || []).map(t => ({
|
userId: t.memberId,
|
userName: findLabel(userOptions.value, t.memberId),
|
userRoleId: t.roleId,
|
userRoleName: findLabel(roleOptions.value, t.roleId),
|
joinTime: t.enterDate,
|
departTime: t.leaveDate,
|
contact: t.phone,
|
remark: t.remark
|
}))
|
|
const shippingRow = (form.value.addressList || [])[0] || {}
|
const shippingAddress = {
|
id: undefined,
|
consignee: shippingRow.receiver,
|
contract: shippingRow.phone,
|
address: shippingRow.address
|
}
|
|
const contractInfo = {
|
id: undefined,
|
name: form.value.contactName,
|
sex: form.value.contactGender === '1' ? '男' : form.value.contactGender === '2' ? '女' : '',
|
birthday: form.value.contactBirthday,
|
department: form.value.contactDept,
|
job: form.value.contactJob,
|
phoneNumber: form.value.contactMobile,
|
email: form.value.contactEmail,
|
qq: form.value.contactQq,
|
lineaFissa: form.value.contactWorkWechat,
|
wx: form.value.contactWechat,
|
origineEtnica: form.value.contactAddress,
|
rappresentanteLegale: form.value.contactRemark
|
}
|
const info = {
|
id: form.value.id ?? null,
|
no: form.value.billNo,
|
title: form.value.projectName,
|
clientId: form.value.clientId ?? null,
|
clientName: form.value.customerName,
|
projectManagementInfoParentId: form.value.parentProjectId ?? null,
|
projectManagementPlanId: form.value.projectManagementPlanId ?? null,
|
establishTime: form.value.setupDate,
|
source: form.value.projectSource,
|
managerId: form.value.managerId ?? null,
|
managerName: form.value.creatorName,
|
salesmanId: form.value.salesmanId ?? null,
|
salesmanName: form.value.salesmanName ?? '',
|
planStartTime: form.value.planStartDate,
|
planEndTime: form.value.planEndDate,
|
actualStartTime: form.value.actualStartDate,
|
actualEndTime: form.value.actualEndDate,
|
status: form.value.billStatus === '' || form.value.billStatus === undefined || form.value.billStatus === null ? null : Number(form.value.billStatus),
|
departmentId: form.value.departmentId ?? null,
|
departmentName: form.value.departmentName ?? '',
|
orderDate: form.value.orderDate,
|
orderAmount: form.value.projectAmount,
|
reviewStatus: form.value.auditStatus === '' || form.value.auditStatus === undefined || form.value.auditStatus === null ? null : Number(form.value.auditStatus),
|
stage: form.value.projectStage === '' || form.value.projectStage === undefined || form.value.projectStage === null ? null : Number(form.value.projectStage),
|
remark: form.value.remark,
|
attachmentIds: Array.isArray(form.value.attachmentIds) ? form.value.attachmentIds : [],
|
teamList
|
}
|
|
const payload = {
|
info,
|
shippingAddress,
|
contractInfo,
|
salesLedgerProductList: productData.value
|
}
|
|
const req = operationType.value === 'edit' ? updateProject : addProject
|
const res = await req(payload)
|
if (res?.code === 200) {
|
ElMessage.success('保存成功')
|
closeDialog()
|
emit('completed')
|
return
|
}
|
ElMessage.error(res?.msg || '保存失败')
|
}
|
|
defineExpose({ openDialog })
|
</script>
|
|
<style scoped lang="scss">
|
.section {
|
border: 1px solid #ebeef5;
|
border-radius: 8px;
|
margin-bottom: 14px;
|
background: #fff;
|
}
|
|
.section-header {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
padding: 12px 14px;
|
cursor: pointer;
|
}
|
|
.section-title {
|
display: flex;
|
align-items: center;
|
gap: 8px;
|
font-weight: 600;
|
color: #303133;
|
}
|
|
.section-bar {
|
width: 3px;
|
height: 14px;
|
background: #e61e1e;
|
border-radius: 2px;
|
}
|
|
.section-actions {
|
display: flex;
|
align-items: center;
|
gap: 10px;
|
}
|
|
.toggle-icon {
|
color: #909399;
|
}
|
|
.section-body {
|
padding: 0 14px 14px;
|
}
|
|
.dialog-footer {
|
display: flex;
|
justify-content: center;
|
gap: 12px;
|
}
|
.attachment-upload{
|
|
}
|
</style>
|