| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |