Merge branch 'dev_New' of http://114.132.189.42:9002/r/product-inventory-management into dev_New
| | |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row> |
| | | <el-row v-if="!isQuotationApproval"> |
| | | <el-col :span="24"> |
| | | <el-form-item label="审批事由:" prop="approveReason"> |
| | | <el-input v-model="form.approveReason" placeholder="请输入" clearable type="textarea" disabled/> |
| | |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> |
| | | |
| | | <!-- 报价审批:展示报价详情(复用销售报价“查看详情对话框”内容结构) --> |
| | | <div v-if="isQuotationApproval" style="margin: 10px 0 18px;"> |
| | | <el-divider content-position="left">报价详情</el-divider> |
| | | <el-skeleton :loading="quotationLoading" animated> |
| | | <template #template> |
| | | <el-skeleton-item variant="h3" style="width: 30%" /> |
| | | <el-skeleton-item variant="text" style="width: 100%" /> |
| | | <el-skeleton-item variant="text" style="width: 100%" /> |
| | | </template> |
| | | <template #default> |
| | | <el-empty v-if="!currentQuotation || !currentQuotation.quotationNo" description="未查询到对应报价详情" /> |
| | | <template v-else> |
| | | <el-descriptions :column="2" border> |
| | | <el-descriptions-item label="报价单号">{{ currentQuotation.quotationNo }}</el-descriptions-item> |
| | | <el-descriptions-item label="客户名称">{{ currentQuotation.customer }}</el-descriptions-item> |
| | | <el-descriptions-item label="业务员">{{ currentQuotation.salesperson }}</el-descriptions-item> |
| | | <el-descriptions-item label="报价日期">{{ currentQuotation.quotationDate }}</el-descriptions-item> |
| | | <el-descriptions-item label="有效期至">{{ currentQuotation.validDate }}</el-descriptions-item> |
| | | <el-descriptions-item label="付款方式">{{ currentQuotation.paymentMethod }}</el-descriptions-item> |
| | | <el-descriptions-item label="报价总额" :span="2"> |
| | | <span style="font-size: 18px; color: #e6a23c; font-weight: bold;"> |
| | | ¥{{ Number(currentQuotation.totalAmount ?? 0).toFixed(2) }} |
| | | </span> |
| | | </el-descriptions-item> |
| | | </el-descriptions> |
| | | |
| | | <div style="margin-top: 20px;"> |
| | | <h4>产品明细</h4> |
| | | <el-table :data="currentQuotation.products || []" border style="width: 100%"> |
| | | <el-table-column prop="product" label="产品名称" /> |
| | | <el-table-column prop="specification" label="规格型号" /> |
| | | <el-table-column prop="unit" label="单位" /> |
| | | <el-table-column prop="unitPrice" label="单价"> |
| | | <template #default="scope">¥{{ Number(scope.row.unitPrice ?? 0).toFixed(2) }}</template> |
| | | </el-table-column> |
| | | </el-table> |
| | | </div> |
| | | |
| | | <div v-if="currentQuotation.remark" style="margin-top: 20px;"> |
| | | <h4>备注</h4> |
| | | <p>{{ currentQuotation.remark }}</p> |
| | | </div> |
| | | </template> |
| | | </template> |
| | | </el-skeleton> |
| | | </div> |
| | | |
| | | <el-form :model="{ activities }" ref="formRef" label-position="top"> |
| | | <el-steps :active="getActiveStep()" finish-status="success" process-status="process" align-center direction="vertical"> |
| | | <el-step |
| | |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { getCurrentInstance, reactive, ref, toRefs } from "vue"; |
| | | import { computed, getCurrentInstance, reactive, ref, toRefs } from "vue"; |
| | | import { |
| | | approveProcessDetails, |
| | | getDept, |
| | |
| | | import useUserStore from "@/store/modules/user.js"; |
| | | import {userListNoPageByTenantId} from "@/api/system/user.js"; |
| | | import { WarningFilled, Edit, Check, MoreFilled } from '@element-plus/icons-vue' |
| | | import { getQuotationList } from "@/api/salesManagement/salesQuotation.js"; |
| | | const emit = defineEmits(['close']) |
| | | const { proxy } = getCurrentInstance() |
| | | |
| | | const props = defineProps({ |
| | | approveType: { |
| | | type: [Number, String], |
| | | default: 0 |
| | | } |
| | | }) |
| | | |
| | | const dialogFormVisible = ref(false); |
| | | const operationType = ref('') |
| | |
| | | const userStore = useUserStore() |
| | | const productOptions = ref([]); |
| | | const userList = ref([]) |
| | | const quotationLoading = ref(false) |
| | | const currentQuotation = ref({}) |
| | | const isQuotationApproval = computed(() => Number(props.approveType) === 6) |
| | | |
| | | const data = reactive({ |
| | | form: { |
| | | approveTime: "", |
| | |
| | | const openDialog = (type, row) => { |
| | | operationType.value = type; |
| | | dialogFormVisible.value = true; |
| | | currentQuotation.value = {} |
| | | userListNoPageByTenantId().then((res) => { |
| | | userList.value = res.data; |
| | | }); |
| | | form.value = {...row} |
| | | getProductOptions() |
| | | |
| | | // 报价审批:用审批事由字段承载的“报价单号”去查报价列表 |
| | | if (isQuotationApproval.value) { |
| | | const quotationNo = row?.approveReason; |
| | | if (quotationNo) { |
| | | quotationLoading.value = true |
| | | getQuotationList({ quotationNo }).then((res) => { |
| | | const records = res?.data?.records || [] |
| | | currentQuotation.value = records[0] || {} |
| | | }).finally(() => { |
| | | quotationLoading.value = false |
| | | }) |
| | | } |
| | | } |
| | | |
| | | approveProcessDetails(row.approveId).then((res) => { |
| | | activities.value = res.data |
| | | // 增加isApproval字段 |
| | |
| | | const closeDia = () => { |
| | | proxy.resetForm("formRef"); |
| | | dialogFormVisible.value = false; |
| | | quotationLoading.value = false |
| | | currentQuotation.value = {} |
| | | emit('close') |
| | | }; |
| | | defineExpose({ |
| | |
| | | > |
| | | </div> |
| | | <div> |
| | | <el-button type="primary" @click="openForm('add')">新增</el-button> |
| | | <el-button type="primary" @click="openForm('add')" v-if="currentApproveType !== 6">新增</el-button> |
| | | <el-button @click="handleOut">导出</el-button> |
| | | <el-button type="danger" plain @click="handleDelete">删除</el-button> |
| | | </div> |
| | |
| | | ></PIMTable> |
| | | </div> |
| | | <info-form-dia ref="infoFormDia" @close="handleQuery" :approveType="currentApproveType"></info-form-dia> |
| | | <approval-dia ref="approvalDia" @close="handleQuery"></approval-dia> |
| | | <approval-dia ref="approvalDia" @close="handleQuery" :approveType="currentApproveType"></approval-dia> |
| | | <FileList ref="fileListRef" /> |
| | | </div> |
| | | </template> |
| | |
| | | const tableColumnCopy = computed(() => { |
| | | const isLeaveType = currentApproveType.value === 2; // 请假管理 |
| | | const isReimburseType = currentApproveType.value === 4; // 报销管理 |
| | | const isQuotationType = currentApproveType.value === 6; // 报价审批 |
| | | |
| | | // 基础列配置 |
| | | const baseColumns = [ |
| | |
| | | width: 220 |
| | | }, |
| | | { |
| | | label: "审批事由", |
| | | label: isQuotationType ? "报价单号" : "审批事由", |
| | | prop: "approveReason", |
| | | width: 200 |
| | | }, |
| | |
| | | clickFun: (row) => { |
| | | openForm("edit", row); |
| | | }, |
| | | disabled: (row) => row.approveStatus == 2 || row.approveStatus == 1 || row.approveStatus == 4 |
| | | disabled: (row) => currentApproveType.value === 6 || row.approveStatus == 2 || row.approveStatus == 1 || row.approveStatus == 4 |
| | | }, |
| | | { |
| | | name: "审核", |
| | |
| | | <!-- 员工信息展示区域 --> |
| | | <div class="info-section"> |
| | | <div class="info-title">员工信息</div> |
| | | <el-form :model="form" label-width="200px" label-position="left" :rules="rules" ref="formRef" style="margin-top: 20px"> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <div class="info-item"> |
| | | <span class="info-label">姓名:</span> |
| | | <el-form-item label="姓名:" prop="staffName"> |
| | | <el-select v-model="form.staffName" placeholder="请选择人员" style="width: 100%" @change="handleSelect"> |
| | | <el-option |
| | | v-for="item in personList" |
| | |
| | | :value="item.staffName" |
| | | /> |
| | | </el-select> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="info-item"> |
| | | <span class="info-label">员工编号:</span> |
| | | <span class="info-value">{{ form.staffNo || '-' }}</span> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <div class="info-item"> |
| | | <span class="info-label">性别:</span> |
| | | <span class="info-value">{{ form.sex || '-' }}</span> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="info-item"> |
| | | <span class="info-label">户籍住址:</span> |
| | | <span class="info-value">{{ form.nativePlace || '-' }}</span> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <div class="info-item"> |
| | | <span class="info-label">岗位:</span> |
| | | <span class="info-value">{{ form.postJob || '-' }}</span> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="info-item"> |
| | | <span class="info-label">现住址:</span> |
| | | <span class="info-value">{{ form.adress || '-' }}</span> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <div class="info-item"> |
| | | <span class="info-label">第一学历:</span> |
| | | <span class="info-value">{{ form.firstStudy || '-' }}</span> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="info-item"> |
| | | <span class="info-label">专业:</span> |
| | | <span class="info-value">{{ form.profession || '-' }}</span> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <div class="info-item"> |
| | | <span class="info-label">年龄:</span> |
| | | <span class="info-value">{{ form.age || '-' }}</span> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <div class="info-item"> |
| | | <span class="info-label">联系电话:</span> |
| | | <span class="info-value">{{ form.phone || '-' }}</span> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="info-item"> |
| | | <span class="info-label">紧急联系人:</span> |
| | | <span class="info-value">{{ form.emergencyContact || '-' }}</span> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <div class="info-item"> |
| | | <span class="info-label">紧急联系人联系电话:</span> |
| | | <span class="info-value">{{ form.emergencyContactPhone || '-' }}</span> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <div class="info-item"> |
| | | <span class="info-label">合同开始日期:</span> |
| | | <span class="info-value">{{ form.contractStartTime || '-' }}</span> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <div class="info-item"> |
| | | <span class="info-label">合同结束日期:</span> |
| | | <span class="info-value">{{ form.contractEndTime || '-' }}</span> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | |
| | | <!-- 离职信息填写区域 --> |
| | | <!-- <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef" style="margin-top: 20px"> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="离职日期:" prop="dimissionDate"> |
| | | <el-date-picker |
| | | v-model="form.dimissionDate" |
| | | type="date" |
| | | placeholder="请选择离职日期" |
| | | value-format="YYYY-MM-DD" |
| | | format="YYYY-MM-DD" |
| | | clearable |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="员工编号:"> |
| | | {{ form.staffNo || '-' }} |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="性别:"> |
| | | {{ form.sex || '-' }} |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="户籍住址:"> |
| | | {{ form.nativePlace || '-' }} |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="岗位:"> |
| | | {{ form.postJob || '-' }} |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="现住址:"> |
| | | {{ form.adress || '-' }} |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="第一学历:"> |
| | | {{ form.firstStudy || '-' }} |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="专业:"> |
| | | {{ form.profession || '-' }} |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="年龄:"> |
| | | {{ form.age || '-' }} |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="联系电话:"> |
| | | {{ form.phone || '-' }} |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="紧急联系人:"> |
| | | {{ form.emergencyContact || '-' }} |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="紧急联系人联系电话:"> |
| | | {{ form.emergencyContactPhone || '-' }} |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="合同开始日期:"> |
| | | {{ form.contractStartTime || '-' }} |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="合同结束日期:"> |
| | | {{ form.contractEndTime || '-' }} |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="离职原因:" prop="dimissionReason"> |
| | | <el-select v-model="form.dimissionReason" placeholder="请选择离职原因" style="width: 100%" @change="handleSelectDimissionReason"> |
| | | <el-option |
| | | v-for="(item, index) in dimissionReasonOptions" |
| | | :key="index" |
| | | :label="item.label" |
| | | :value="item.value" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="备注:" prop="dimissionRemark" v-show="form.dimissionReason === 'other'"> |
| | | <el-input |
| | | v-model="form.dimissionReason" |
| | | v-model="form.dimissionRemark" |
| | | type="textarea" |
| | | v-show="form.dimissionReason === 'other'" |
| | | :rows="3" |
| | | placeholder="请输入离职原因" |
| | | placeholder="备注" |
| | | maxlength="500" |
| | | show-word-limit |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </el-form> --> |
| | | </el-form> |
| | | |
| | | <!-- <el-row :gutter="30">--> |
| | | <!-- <el-col :span="12">--> |
| | | <!-- <div class="info-item">--> |
| | | <!-- <span class="info-label">离职原因:</span>--> |
| | | <!-- <el-select v-model="form.dimissionReason" placeholder="请选择人员" style="width: 100%" @change="handleSelect">--> |
| | | <!-- <el-option--> |
| | | <!-- v-for="(item, index) in dimissionReasonOptions"--> |
| | | <!-- :key="index"--> |
| | | <!-- :label="item.label"--> |
| | | <!-- :value="item.value"--> |
| | | <!-- />--> |
| | | <!-- </el-select>--> |
| | | <!-- </div>--> |
| | | <!-- </el-col>--> |
| | | <!-- <el-col :span="12">--> |
| | | <!-- <div class="info-item">--> |
| | | <!-- <span class="info-label">员工编号:</span>--> |
| | | <!-- <span class="info-value">{{ form.staffNo || '-' }}</span>--> |
| | | <!-- </div>--> |
| | | <!-- </el-col>--> |
| | | <!-- </el-row>--> |
| | | </div> |
| | | |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button type="primary" @click="submitForm">确认</el-button> |
| | |
| | | contractEndTime: "", |
| | | dimissionDate: "", |
| | | dimissionReason: "", |
| | | dimissionRemark: "", |
| | | staffState: "", |
| | | }, |
| | | rules: { |
| | | staffName: [{ required: true, message: "请选择人员", trigger: "change" }], |
| | | dimissionDate: [{ required: true, message: "请选择离职日期", trigger: "change" }], |
| | | dimissionReason: [{ required: true, message: "请输入离职原因", trigger: "blur" }], |
| | | staffName: [{ required: true, message: "请选择人员" }], |
| | | dimissionReason: [{ required: true, message: "请选择离职原因"}], |
| | | }, |
| | | dimissionReasonOptions: [ |
| | | {label: '薪资待遇', value: 'salary'}, |
| | | {label: '职业发展', value: 'career_development'}, |
| | | {label: '工作环境', value: 'work_environment'}, |
| | | {label: '个人原因', value: 'personal_reason'}, |
| | | {label: '其他', value: 'other'}, |
| | | ] |
| | | }); |
| | | const { form, rules } = toRefs(data); |
| | | const { form, rules, dimissionReasonOptions } = toRefs(data); |
| | | |
| | | // 打开弹框 |
| | | const openDialog = (type, row) => { |
| | |
| | | }) |
| | | } |
| | | } |
| | | |
| | | const handleSelectDimissionReason = (val) => { |
| | | if (val === 'other') { |
| | | form.value.dimissionRemark = '' |
| | | } |
| | | } |
| | | // 提交产品表单 |
| | | const submitForm = () => { |
| | | // 表单已注释,直接提交,不进行验证 |
| | | if (!form.value.staffName) { |
| | | proxy.$modal.msgError("请选择人员"); |
| | | return; |
| | | } |
| | | form.value.staffState = 0 |
| | | if (form.value.dimissionReason !== 'other') { |
| | | form.value.dimissionRemark = '' |
| | | } |
| | | proxy.$refs["formRef"].validate(valid => { |
| | | if (valid) { |
| | | if (operationType.value === "add") { |
| | | staffJoinAdd(form.value).then(res => { |
| | | proxy.$modal.msgSuccess("提交成功"); |
| | |
| | | }) |
| | | } |
| | | } |
| | | }) |
| | | |
| | | } |
| | | // 关闭弹框 |
| | | const closeDia = () => { |
| | | // 表单已注释,手动重置表单数据 |
| | |
| | | <el-card class="box-card"> |
| | | <!-- KPI 汇总 --> |
| | | <el-row :gutter="20" class="stats-row"> |
| | | <el-col :span="6"> |
| | | <el-col :span="8"> |
| | | <div class="stat-card"> |
| | | <div class="stat-icon" style="background: #ecf5ff;"> |
| | | <el-icon :size="30" color="#409eff"><Document /></el-icon> |
| | |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-col :span="8"> |
| | | <div class="stat-card"> |
| | | <div class="stat-icon" style="background: #f0f9ff;"> |
| | | <el-icon :size="30" color="#67c23a"><Tickets /></el-icon> |
| | |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <el-col :span="8"> |
| | | <div class="stat-card"> |
| | | <div class="stat-icon" style="background: #fef0f0;"> |
| | | <el-icon :size="30" color="#e6a23c"><Van /></el-icon> |
| | |
| | | <div class="stat-content"> |
| | | <div class="stat-value">{{ indicatorKpis.shipmentRate }}%</div> |
| | | <div class="stat-label">发货率</div> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <div class="stat-card"> |
| | | <div class="stat-icon" style="background: #f4f4f5;"> |
| | | <el-icon :size="30" color="#f56c6c"><Wallet /></el-icon> |
| | | </div> |
| | | <div class="stat-content"> |
| | | <div class="stat-value">{{ indicatorKpis.collectionRate }}%</div> |
| | | <div class="stat-label">回款率</div> |
| | | </div> |
| | | </div> |
| | | </el-col> |
| | |
| | | </div> |
| | | |
| | | <!-- 业绩统计(团队维度,无个人姓名) --> |
| | | <el-table :data="teamPerformanceList" border stripe style="margin-top: 20px;"> |
| | | <el-table v-if="showTeamPerformance" :data="teamPerformanceList" border stripe style="margin-top: 20px;"> |
| | | <el-table-column prop="team" label="销售团队"/> |
| | | <el-table-column prop="orderCount" label="订单数"/> |
| | | <el-table-column prop="salesAmount" label="销售额"> |
| | |
| | | </el-table-column> |
| | | <el-table-column prop="shipmentRate" label="发货率"> |
| | | <template #default="scope">{{ scope.row.shipmentRate }}%</template> |
| | | </el-table-column> |
| | | <el-table-column prop="collectionRate" label="回款率"> |
| | | <template #default="scope">{{ scope.row.collectionRate }}%</template> |
| | | </el-table-column> |
| | | <el-table-column prop="attainment" label="目标达成率"> |
| | | <template #default="scope"> |
| | |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted, nextTick } from 'vue' |
| | | import { Document, Van, Tickets, Wallet } from '@element-plus/icons-vue' |
| | | import { Document, Van, Tickets } from '@element-plus/icons-vue' |
| | | import * as echarts from 'echarts' |
| | | |
| | | const indicatorKpis = reactive({ |
| | | orderCount: 1280, |
| | | salesAmount: 9650000, |
| | | shipmentRate: 89.2, |
| | | collectionRate: 76.4 |
| | | shipmentRate: 89.2 |
| | | }) |
| | | |
| | | // 是否展示销售团队明细表,按需开启 |
| | | const showTeamPerformance = ref(false) |
| | | |
| | | const indicatorFilter = reactive({ |
| | | product: '', |
| | |
| | | let indicatorChart = null |
| | | |
| | | const teamPerformanceList = ref([ |
| | | { team: '华东大区', orderCount: 320, salesAmount: 2850000, shipmentRate: 90, collectionRate: 80, attainment: 105 }, |
| | | { team: '华北大区', orderCount: 280, salesAmount: 2150000, shipmentRate: 86, collectionRate: 73, attainment: 92 }, |
| | | { team: '华南大区', orderCount: 210, salesAmount: 1850000, shipmentRate: 88, collectionRate: 70, attainment: 78 }, |
| | | { team: '西南大区', orderCount: 180, salesAmount: 1500000, shipmentRate: 83, collectionRate: 68, attainment: 74 } |
| | | { team: '华东大区', orderCount: 320, salesAmount: 2850000, shipmentRate: 90, attainment: 105 }, |
| | | { team: '华北大区', orderCount: 280, salesAmount: 2150000, shipmentRate: 86, attainment: 92 }, |
| | | { team: '华南大区', orderCount: 210, salesAmount: 1850000, shipmentRate: 88, attainment: 78 }, |
| | | { team: '西南大区', orderCount: 180, salesAmount: 1500000, shipmentRate: 83, attainment: 74 } |
| | | ]) |
| | | |
| | | const initIndicatorChart = () => { |
| | |
| | | const option = { |
| | | title: { text: '多维度销售指标趋势', left: 'center' }, |
| | | tooltip: { trigger: 'axis' }, |
| | | legend: { data: ['订单数', '销售额', '发货率', '回款率'], top: 30 }, |
| | | legend: { data: ['订单数', '销售额', '发货率'], top: 30 }, |
| | | grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, |
| | | xAxis: { type: 'category', data: ['2024-12', '2025-01', '2025-02', '2025-03', '2025-04', '2025-05'] }, |
| | | yAxis: [ |
| | |
| | | series: [ |
| | | { name: '订单数', type: 'bar', data: [180, 220, 210, 260, 205, 225], itemStyle: { color: '#409eff' } }, |
| | | { name: '销售额', type: 'bar', data: [820, 950, 910, 1080, 980, 1020], itemStyle: { color: '#67c23a' } }, |
| | | { name: '发货率', type: 'line', yAxisIndex: 1, data: [86, 89, 88, 91, 87, 90], itemStyle: { color: '#e6a23c' } }, |
| | | { name: '回款率', type: 'line', yAxisIndex: 1, data: [72, 76, 74, 79, 75, 78], itemStyle: { color: '#f56c6c' } } |
| | | { name: '发货率', type: 'line', yAxisIndex: 1, data: [86, 89, 88, 91, 87, 90], itemStyle: { color: '#e6a23c' } } |
| | | ] |
| | | } |
| | | indicatorChart.setOption(option) |
| | |
| | | indicatorKpis.orderCount = random(1280, 120) |
| | | indicatorKpis.salesAmount = random(9650000, 350000) |
| | | indicatorKpis.shipmentRate = (85 + Math.random() * 10).toFixed(1) * 1 |
| | | indicatorKpis.collectionRate = (70 + Math.random() * 12).toFixed(1) * 1 |
| | | setTimeout(() => initIndicatorChart(), 200) |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | const exportIndicatorTable = () => { |
| | | const header = ['销售团队', '订单数', '销售额', '发货率(%)', '回款率(%)', '目标达成率(%)'] |
| | | const header = ['销售团队', '订单数', '销售额', '发货率(%)', '目标达成率(%)'] |
| | | const rows = teamPerformanceList.value.map(r => [ |
| | | r.team, |
| | | r.orderCount, |
| | | r.salesAmount, |
| | | r.shipmentRate, |
| | | r.collectionRate, |
| | | r.attainment |
| | | ]) |
| | | const csv = [header, ...rows].map(r => r.join(',')).join('\n') |
| | |
| | | width="200" |
| | | /> |
| | | <el-table-column |
| | | label="开票金额(元)" |
| | | label="合同金额(元)" |
| | | prop="invoiceTotal" |
| | | show-overflow-tooltip |
| | | :formatter="formattedNumber" |
| | |
| | | /> |
| | | <el-table-column |
| | | label="发生日期" |
| | | prop="happenTime" |
| | | prop="receiptPaymentDate" |
| | | show-overflow-tooltip |
| | | width="110" |
| | | /> |
| | | <el-table-column |
| | | label="开票金额(元)" |
| | | prop="invoiceAmount" |
| | | label="销售合同号" |
| | | prop="salesContractNo" |
| | | show-overflow-tooltip |
| | | width="200" |
| | | /> |
| | | <el-table-column |
| | | label="合同金额(元)" |
| | | prop="invoiceTotal" |
| | | show-overflow-tooltip |
| | | :formatter="formattedNumber" |
| | | width="200" |
| | | /> |
| | | <el-table-column |
| | | label="回款金额(元)" |
| | | prop="receiptAmount" |
| | | prop="receiptPaymentAmount" |
| | | show-overflow-tooltip |
| | | :formatter="formattedNumber" |
| | | width="200" |
| | | /> |
| | | <el-table-column |
| | | label="应收金额(元)" |
| | | prop="unReceiptAmount" |
| | | prop="unReceiptPaymentAmount" |
| | | show-overflow-tooltip |
| | | width="200" |
| | | > |
| | | <template #default="{ row, column }"> |
| | | <el-text type="danger"> |
| | | {{ formattedNumber(row, column, row.unReceiptAmount) }} |
| | | {{ formattedNumber(row, column, row.unReceiptPaymentAmount) }} |
| | | </el-text> |
| | | </template> |
| | | </el-table-column> |
| | |
| | | @confirm="submitForm" |
| | | @cancel="closeDia"> |
| | | <el-form :model="form" label-width="140px" label-position="top" :rules="rules" ref="formRef"> |
| | | <el-row v-if="operationType !== 'view'"> |
| | | <el-col :span="24" style="display:flex; justify-content:flex-end; gap:10px; margin-bottom: 6px;"> |
| | | <el-button type="primary" plain @click="openQuotationDialog">从审批通过的报价单导入</el-button> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="30"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="销售合同号:" prop="salesContractNo"> |
| | |
| | | </el-row> |
| | | </el-form> |
| | | </FormDialog> |
| | | |
| | | <!-- 从报价单导入(仅审批通过) --> |
| | | <el-dialog |
| | | v-model="quotationDialogVisible" |
| | | title="选择审批通过的销售报价单" |
| | | width="80%" |
| | | :close-on-click-modal="false" |
| | | > |
| | | <div style="margin-bottom: 12px; display:flex; gap: 12px; align-items:center;"> |
| | | <el-input |
| | | v-model="quotationSearchForm.quotationNo" |
| | | placeholder="请输入报价单号" |
| | | clearable |
| | | style="max-width: 260px;" |
| | | @change="fetchQuotationList" |
| | | /> |
| | | <el-input |
| | | v-model="quotationSearchForm.customer" |
| | | placeholder="请输入客户名称" |
| | | clearable |
| | | style="max-width: 260px;" |
| | | @change="fetchQuotationList" |
| | | /> |
| | | <el-button type="primary" @click="fetchQuotationList">搜索</el-button> |
| | | <el-button @click="resetQuotationSearch">重置</el-button> |
| | | </div> |
| | | |
| | | <el-table |
| | | :data="quotationList" |
| | | border |
| | | stripe |
| | | v-loading="quotationLoading" |
| | | height="420px" |
| | | > |
| | | <el-table-column align="center" label="序号" type="index" width="60" /> |
| | | <el-table-column prop="quotationNo" label="报价单号" width="180" show-overflow-tooltip /> |
| | | <el-table-column prop="customer" label="客户名称" min-width="220" show-overflow-tooltip /> |
| | | <el-table-column prop="salesperson" label="业务员" width="120" show-overflow-tooltip /> |
| | | <el-table-column prop="quotationDate" label="报价日期" width="140" /> |
| | | <el-table-column prop="status" label="审批状态" width="120" align="center" /> |
| | | <el-table-column prop="totalAmount" label="报价金额(元)" width="160" align="right"> |
| | | <template #default="scope"> |
| | | {{ Number(scope.row.totalAmount ?? 0).toFixed(2) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column fixed="right" label="操作" width="120" align="center"> |
| | | <template #default="scope"> |
| | | <el-button type="primary" link @click="applyQuotation(scope.row)">选择</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <template #footer> |
| | | <el-button @click="quotationDialogVisible = false">关闭</el-button> |
| | | </template> |
| | | </el-dialog> |
| | | <FormDialog |
| | | v-model="productFormVisible" |
| | | :title="productOperationType === 'add' ? '新增产品' : '编辑产品'" |
| | |
| | | import { userListNoPage } from "@/api/system/user.js"; |
| | | import FileListDialog from '@/components/Dialog/FileListDialog.vue'; |
| | | import FormDialog from '@/components/Dialog/FormDialog.vue'; |
| | | import { getQuotationList } from "@/api/salesManagement/salesQuotation.js"; |
| | | import { |
| | | ledgerListPage, |
| | | productList, |
| | |
| | | const printPreviewVisible = ref(false); |
| | | const printData = ref([]); |
| | | |
| | | // 报价单导入相关 |
| | | const quotationDialogVisible = ref(false); |
| | | const quotationLoading = ref(false); |
| | | const quotationList = ref([]); |
| | | const quotationSearchForm = reactive({ |
| | | quotationNo: "", |
| | | customer: "", |
| | | }); |
| | | const selectedQuotation = ref(null); |
| | | |
| | | // 发货相关 |
| | | const deliveryFormVisible = ref(false); |
| | | const currentDeliveryRow = ref(null); |
| | |
| | | }; |
| | | // 获取产品大类tree数据 |
| | | const getProductOptions = () => { |
| | | productTreeList().then((res) => { |
| | | // 返回 Promise,便于在编辑产品时等待加载完成 |
| | | return productTreeList().then((res) => { |
| | | productOptions.value = convertIdToValue(res); |
| | | return productOptions.value; |
| | | }); |
| | | }; |
| | | const formattedNumber = (row, column, cellValue) => { |
| | |
| | | |
| | | return newItem; |
| | | }); |
| | | } |
| | | // 根据名称反查产品大类 id,便于仅存名称时的反显 |
| | | 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; |
| | | } |
| | | // 表格选择数据 |
| | | const handleSelectionChange = (selection) => { |
| | |
| | | operationType.value = type; |
| | | form.value = {}; |
| | | productData.value = []; |
| | | selectedQuotation.value = null; |
| | | let userLists = await userListNoPage(); |
| | | userList.value = userLists.data; |
| | | customerList().then((res) => { |
| | |
| | | // }); |
| | | form.value.entryDate = getCurrentDate(); // 设置默认录入日期为当前日期 |
| | | dialogFormVisible.value = true; |
| | | }; |
| | | |
| | | // 打开报价单选择弹窗(仅审批通过) |
| | | const openQuotationDialog = async () => { |
| | | if (operationType.value === "view") return; |
| | | quotationDialogVisible.value = true; |
| | | // 先确保客户列表已加载,便于后续回填 customerId |
| | | if (!customerOption.value || customerOption.value.length === 0) { |
| | | try { |
| | | const res = await customerList(); |
| | | customerOption.value = res; |
| | | } catch (e) { |
| | | // ignore,允许用户后续手动选择客户 |
| | | } |
| | | } |
| | | await fetchQuotationList(); |
| | | }; |
| | | |
| | | const fetchQuotationList = async () => { |
| | | quotationLoading.value = true; |
| | | try { |
| | | const params = { |
| | | // 兼容后端分页字段:这里沿用报价页面已有可用的字段命名 |
| | | currentPage: 1, |
| | | pageSize: 100, |
| | | ...quotationSearchForm, |
| | | status: "通过", |
| | | }; |
| | | const res = await getQuotationList(params); |
| | | quotationList.value = res?.data?.records || []; |
| | | } finally { |
| | | quotationLoading.value = false; |
| | | } |
| | | }; |
| | | |
| | | const resetQuotationSearch = async () => { |
| | | quotationSearchForm.quotationNo = ""; |
| | | quotationSearchForm.customer = ""; |
| | | await fetchQuotationList(); |
| | | }; |
| | | |
| | | // 选中报价单后回填到台账表单 |
| | | const applyQuotation = (row) => { |
| | | if (!row) return; |
| | | selectedQuotation.value = row; |
| | | |
| | | // 业务员 |
| | | form.value.salesman = row.salesperson || ""; |
| | | |
| | | // 客户名称 -> customerId |
| | | const customer = (customerOption.value || []).find((c) => c.customerName === row.customer); |
| | | if (customer?.id) { |
| | | form.value.customerId = customer.id; |
| | | } else { |
| | | // 如果找不到,保留原值(允许用户手动选择/不打断已有输入) |
| | | form.value.customerId = form.value.customerId || ""; |
| | | } |
| | | |
| | | // 产品信息映射:报价 products -> 台账 productData |
| | | const products = Array.isArray(row.products) ? row.products : []; |
| | | productData.value = products.map((p) => { |
| | | const quantity = Number(p.quantity ?? 0) || 0; |
| | | const unitPrice = Number(p.unitPrice ?? 0) || 0; |
| | | const taxRate = "13"; // 默认 13%,便于直接提交(如需可在产品中自行修改) |
| | | const taxInclusiveTotalPrice = (unitPrice * quantity).toFixed(2); |
| | | const taxExclusiveTotalPrice = proxy.calculateTaxExclusiveTotalPrice(taxInclusiveTotalPrice, taxRate); |
| | | return { |
| | | // 台账字段 |
| | | productCategory: p.product || p.productName || "", |
| | | specificationModel: p.specification || "", |
| | | unit: p.unit || "", |
| | | quantity: quantity, |
| | | taxRate: taxRate, |
| | | taxInclusiveUnitPrice: unitPrice.toFixed(2), |
| | | taxInclusiveTotalPrice: taxInclusiveTotalPrice, |
| | | taxExclusiveTotalPrice: taxExclusiveTotalPrice, |
| | | invoiceType: "增普票", |
| | | }; |
| | | }); |
| | | |
| | | quotationDialogVisible.value = false; |
| | | }; |
| | | function changs(val) { |
| | | console.log(val); |
| | |
| | | |
| | | const productIndex = ref(0); |
| | | // 打开产品弹框 |
| | | const openProductForm = (type, row,index) => { |
| | | const openProductForm = async (type, row, index) => { |
| | | productOperationType.value = type; |
| | | productForm.value = {}; |
| | | proxy.resetForm("productFormRef"); |
| | | if (type === "edit") { |
| | | productForm.value = { ...row }; |
| | | productIndex.value = index; |
| | | // 编辑时根据产品大类名称反查 tree 节点 id,并加载规格型号列表 |
| | | try { |
| | | const options = productOptions.value && productOptions.value.length > 0 |
| | | ? productOptions.value |
| | | : await getProductOptions(); |
| | | const categoryId = findNodeIdByLabel(options, productForm.value.productCategory); |
| | | if (categoryId) { |
| | | const models = await modelList({ id: categoryId }); |
| | | modelOptions.value = models || []; |
| | | // 根据当前规格型号名称反查并设置 productModelId,便于下拉框显示已选值 |
| | | const currentModel = (modelOptions.value || []).find( |
| | | (m) => m.model === productForm.value.specificationModel |
| | | ); |
| | | if (currentModel) { |
| | | productForm.value.productModelId = currentModel.id; |
| | | } |
| | | } |
| | | } catch (e) { |
| | | // 加载失败时保持可编辑,不中断弹窗 |
| | | console.error("加载产品规格型号失败", e); |
| | | } |
| | | } |
| | | productFormVisible.value = true; |
| | | getProductOptions(); |
| | | }; |
| | | // 提交产品表单 |
| | | const submitProduct = () => { |
| | |
| | | <el-table-column prop="salesperson" label="业务员" width="100" /> |
| | | <el-table-column prop="quotationDate" label="报价日期" width="120" /> |
| | | <el-table-column prop="validDate" label="有效期至" width="120" /> |
| | | <el-table-column prop="status" label="审批状态" width="120" align="center"> |
| | | <template #default="{ row }"> |
| | | <el-tag :type="getStatusType(row.status)" disable-transitions> |
| | | {{ row.status || '--' }} |
| | | </el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="totalAmount" label="报价金额" width="120"> |
| | | <template #default="scope"> |
| | | ¥{{ scope.row.totalAmount.toFixed(2) }} |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="操作" width="250" fixed="right" align="center"> |
| | | <el-table-column label="操作" width="200" fixed="right" align="center"> |
| | | <template #default="scope"> |
| | | <el-button link type="primary" @click="handleView(scope.row)">查看</el-button> |
| | | <el-button link type="primary" @click="handleEdit(scope.row)" v-if="scope.row.status === '草稿'">编辑</el-button> |
| | | <el-button link type="danger" @click="handleDelete(scope.row)" v-if="scope.row.status === '草稿'">删除</el-button> |
| | | <el-button link type="primary" @click="handleEdit(scope.row)" :disabled="!['待审批','拒绝'].includes(scope.row.status)">编辑</el-button> |
| | | <el-button link type="danger" @click="handleDelete(scope.row)">删除</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | |
| | | </el-card> |
| | | |
| | | <!-- 新增/编辑对话框 --> |
| | | <FormDialog v-model="dialogVisible" :title="dialogTitle" width="80%" :close-on-click-modal="false" @close="dialogVisible = false" @confirm="handleSubmit" @cancel="dialogVisible = false"> |
| | | <el-form :model="form" :rules="rules" ref="formRef" label-width="100px"> |
| | | <FormDialog v-model="dialogVisible" :title="dialogTitle" width="85%" :close-on-click-modal="false" @close="dialogVisible = false" @confirm="handleSubmit" @cancel="dialogVisible = false"> |
| | | <div class="quotation-form-container"> |
| | | <el-form :model="form" :rules="rules" ref="formRef" label-width="120px" class="quotation-form"> |
| | | <!-- 基本信息 --> |
| | | <el-card class="form-card" shadow="never"> |
| | | <el-card class="form-card" shadow="hover"> |
| | | <template #header> |
| | | <div class="card-header-wrapper"> |
| | | <el-icon class="card-icon"><Document /></el-icon> |
| | | <span class="card-title">基本信息</span> |
| | | </div> |
| | | </template> |
| | | <el-row :gutter="20"> |
| | | <div class="form-content"> |
| | | <el-row :gutter="24"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="客户名称" prop="customer"> |
| | | <el-select v-model="form.customer" placeholder="请选择客户" style="width: 100%" @change="handleCustomerChange"> |
| | | <el-select v-model="form.customer" placeholder="请选择客户" style="width: 100%" @change="handleCustomerChange" clearable> |
| | | <el-option v-for="item in customerOption" :key="item.id" :label="item.customerName" :value="item.customerName"> |
| | | {{ |
| | | item.customerName + "——" + item.taxpayerIdentificationNumber |
| | |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="业务员" prop="salesperson"> |
| | | <el-select v-model="form.salesperson" placeholder="请选择业务员" style="width: 100%"> |
| | | <el-select v-model="form.salesperson" placeholder="请选择业务员" style="width: 100%" clearable> |
| | | <el-option v-for="item in userList" :key="item.nickName" :label="item.nickName" |
| | | :value="item.nickName" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-row :gutter="24"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="报价日期" prop="quotationDate"> |
| | | <el-date-picker |
| | |
| | | style="width: 100%" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | |
| | | style="width: 100%" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | clearable |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-row :gutter="24"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="付款方式" prop="paymentMethod"> |
| | | <el-select v-model="form.paymentMethod" placeholder="请选择付款方式" style="width: 100%"> |
| | | <el-option label="全款到付" value="全款到付"></el-option> |
| | | <el-option label="分期付款" value="分期付款"></el-option> |
| | | <el-option label="月结" value="月结"></el-option> |
| | | </el-select> |
| | | <el-input v-model="form.paymentMethod" placeholder="请输入付款方式" clearable /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <!-- 审批人信息 --> |
| | | <el-card class="form-card" shadow="hover"> |
| | | <template #header> |
| | | <div class="card-header-wrapper"> |
| | | <el-icon class="card-icon"><UserFilled /></el-icon> |
| | | <span class="card-title">审批人选择</span> |
| | | <el-button type="primary" size="small" @click="addApproverNode" class="header-btn"> |
| | | <el-icon><Plus /></el-icon> |
| | | 新增节点 |
| | | </el-button> |
| | | </div> |
| | | </template> |
| | | <div class="form-content"> |
| | | <el-row> |
| | | <el-col :span="24"> |
| | | <el-form-item> |
| | | <div class="approver-nodes-container"> |
| | | <div |
| | | v-for="(node, index) in approverNodes" |
| | | :key="node.id" |
| | | class="approver-node-item" |
| | | > |
| | | <div class="approver-node-label"> |
| | | <span class="node-step">{{ index + 1 }}</span> |
| | | <span class="node-text">审批人</span> |
| | | <el-icon class="arrow-icon"><ArrowRight /></el-icon> |
| | | </div> |
| | | <el-select |
| | | v-model="node.userId" |
| | | placeholder="选择人员" |
| | | class="approver-select" |
| | | clearable |
| | | > |
| | | <el-option |
| | | v-for="user in userList" |
| | | :key="user.userId" |
| | | :label="user.nickName" |
| | | :value="user.userId" |
| | | /> |
| | | </el-select> |
| | | <el-button |
| | | type="danger" |
| | | size="small" |
| | | :icon="Delete" |
| | | @click="removeApproverNode(index)" |
| | | v-if="approverNodes.length > 1" |
| | | class="remove-btn" |
| | | >删除</el-button> |
| | | </div> |
| | | </div> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <!-- 产品信息 --> |
| | | <el-card class="form-card" shadow="never"> |
| | | <el-card class="form-card" shadow="hover"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <div class="card-header-wrapper"> |
| | | <el-icon class="card-icon"><Box /></el-icon> |
| | | <span class="card-title">产品信息</span> |
| | | <el-button type="primary" size="small" @click="addProduct">添加产品</el-button> |
| | | <el-button type="primary" size="small" @click="addProduct" class="header-btn"> |
| | | <el-icon><Plus /></el-icon> |
| | | 添加产品 |
| | | </el-button> |
| | | </div> |
| | | </template> |
| | | <el-table :data="form.products" border style="width: 100%"> |
| | | <div class="form-content"> |
| | | <el-table :data="form.products" border style="width: 100%" class="product-table" v-if="form.products.length > 0"> |
| | | <el-table-column prop="product" label="产品名称" width="200"> |
| | | <template #default="scope"> |
| | | <el-tree-select |
| | |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | <el-empty v-else description="暂无产品,请点击添加产品" :image-size="80" /> |
| | | </div> |
| | | </el-card> |
| | | |
| | | <!-- 备注信息 --> |
| | | <el-card class="form-card" shadow="never"> |
| | | <el-card class="form-card" shadow="hover"> |
| | | <template #header> |
| | | <div class="card-header-wrapper"> |
| | | <el-icon class="card-icon"><EditPen /></el-icon> |
| | | <span class="card-title">备注信息</span> |
| | | </div> |
| | | </template> |
| | | <div class="form-content"> |
| | | <el-form-item label="备注" prop="remark"> |
| | | <el-input type="textarea" v-model="form.remark" placeholder="请输入备注信息" rows="3"></el-input> |
| | | <el-input |
| | | type="textarea" |
| | | v-model="form.remark" |
| | | placeholder="请输入备注信息(选填)" |
| | | :rows="4" |
| | | maxlength="500" |
| | | show-word-limit |
| | | ></el-input> |
| | | </el-form-item> |
| | | </div> |
| | | </el-card> |
| | | </el-form> |
| | | </div> |
| | | </FormDialog> |
| | | |
| | | <!-- 查看详情对话框 --> |
| | |
| | | <script setup> |
| | | import { ref, reactive, computed, onMounted, markRaw, shallowRef } from 'vue' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import { Search } from '@element-plus/icons-vue' |
| | | import { Search, Document, UserFilled, Box, EditPen, Plus, ArrowRight, Delete } from '@element-plus/icons-vue' |
| | | import Pagination from '@/components/PIMTable/Pagination.vue' |
| | | import FormDialog from '@/components/Dialog/FormDialog.vue' |
| | | import {getQuotationList,addQuotation,updateQuotation,deleteQuotation} from '@/api/salesManagement/salesQuotation.js' |
| | |
| | | salesperson: [{ required: true, message: '请选择业务员', trigger: 'change' }], |
| | | quotationDate: [{ required: true, message: '请选择报价日期', trigger: 'change' }], |
| | | validDate: [{ required: true, message: '请选择有效期', trigger: 'change' }], |
| | | paymentMethod: [{ required: true, message: '请选择付款方式', trigger: 'change' }] |
| | | paymentMethod: [{ required: true, message: '请输入付款方式', trigger: 'blur' }] |
| | | } |
| | | const userList = ref([]); |
| | | const customerOption = ref([]); |
| | | |
| | | // 审批人节点相关 |
| | | const approverNodes = ref([ |
| | | { id: 1, userId: null } |
| | | ]) |
| | | let nextApproverId = 2 |
| | | |
| | | const isEdit = ref(false) |
| | | const editId = ref(null) |
| | | const currentQuotation = ref({}) |
| | | const formRef = ref() |
| | | |
| | | // 添加审批人节点 |
| | | function addApproverNode() { |
| | | approverNodes.value.push({ id: nextApproverId++, userId: null }) |
| | | } |
| | | |
| | | // 删除审批人节点 |
| | | function removeApproverNode(index) { |
| | | approverNodes.value.splice(index, 1) |
| | | } |
| | | |
| | | // 计算属性 |
| | | const filteredList = computed(() => { |
| | |
| | | // 方法 |
| | | const getStatusType = (status) => { |
| | | const statusMap = { |
| | | '草稿': 'info', |
| | | '已发送': 'primary', |
| | | '客户确认': 'success', |
| | | '已过期': 'danger' |
| | | '待审批': 'info', |
| | | '审核中': 'primary', |
| | | '通过': 'success', |
| | | '拒绝': 'danger' |
| | | } |
| | | return statusMap[status] || 'info' |
| | | } |
| | |
| | | dialogTitle.value = '新增报价' |
| | | isEdit.value = false |
| | | resetForm() |
| | | // 重置审批人节点 |
| | | approverNodes.value = [{ id: 1, userId: null }] |
| | | nextApproverId = 2 |
| | | dialogVisible.value = true |
| | | let userLists = await userListNoPage(); |
| | | // 只复制需要的字段,避免将组件引用放入响应式对象 |
| | |
| | | }); |
| | | } |
| | | const getProductOptions = () => { |
| | | productTreeList().then((res) => { |
| | | // 返回 Promise,便于编辑时 await 确保能反显 |
| | | return productTreeList().then((res) => { |
| | | productOptions.value = convertIdToValue(res); |
| | | return productOptions.value |
| | | }); |
| | | }; |
| | | function convertIdToValue(data) { |
| | |
| | | |
| | | return newItem; |
| | | }); |
| | | } |
| | | // 根据名称反查节点 id,便于仅存名称时的反显 |
| | | 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; |
| | | } |
| | | const getModels = (value, row) => { |
| | | if (!row) return; |
| | |
| | | viewDialogVisible.value = true |
| | | } |
| | | |
| | | const handleEdit = (row) => { |
| | | const handleEdit = async (row) => { |
| | | dialogTitle.value = '编辑报价' |
| | | isEdit.value = true |
| | | editId.value = row.id |
| | | form.id = row.id || form.id || null |
| | | // 先加载产品树数据,否则 el-tree-select 无法反显产品名称 |
| | | await getProductOptions() |
| | | |
| | | // 只复制需要的字段,避免将组件引用放入响应式对象 |
| | | form.quotationNo = row.quotationNo || '' |
| | | form.customer = row.customer || '' |
| | |
| | | form.paymentMethod = row.paymentMethod || '' |
| | | form.status = row.status || '草稿' |
| | | form.remark = row.remark || '' |
| | | form.products = row.products ? row.products.map(product => ({ |
| | | productId: product.productId || '', |
| | | product: product.product || product.productName || '', |
| | | form.products = row.products ? row.products.map(product => { |
| | | const productName = product.product || product.productName || '' |
| | | // 优先用 productId;如果只有名称,尝试反查 id 以便树选择器反显 |
| | | const resolvedId = product.productId |
| | | ? Number(product.productId) |
| | | : findNodeIdByLabel(productOptions.value, productName) || '' |
| | | return { |
| | | productId: resolvedId, |
| | | product: productName, |
| | | specificationId: product.specificationId || '', |
| | | specification: product.specification || '', |
| | | quantity: product.quantity || 0, |
| | | unit: product.unit || '', |
| | | unitPrice: product.unitPrice || 0, |
| | | amount: product.amount || 0 |
| | | })) : [] |
| | | } |
| | | }) : [] |
| | | form.subtotal = row.subtotal || 0 |
| | | form.freight = row.freight || 0 |
| | | form.otherFee = row.otherFee || 0 |
| | | form.discountRate = row.discountRate || 0 |
| | | form.discountAmount = row.discountAmount || 0 |
| | | form.totalAmount = row.totalAmount || 0 |
| | | |
| | | // 反显审批人 |
| | | if (row.approveUserIds) { |
| | | const userIds = row.approveUserIds.split(',') |
| | | approverNodes.value = userIds.map((userId, idx) => ({ |
| | | id: idx + 1, |
| | | userId: parseInt(userId.trim()) |
| | | })) |
| | | nextApproverId = userIds.length + 1 |
| | | } else { |
| | | approverNodes.value = [{ id: 1, userId: null }] |
| | | nextApproverId = 2 |
| | | } |
| | | |
| | | // 加载用户列表 |
| | | let userLists = await userListNoPage(); |
| | | userList.value = (userLists.data || []).map(item => ({ |
| | | userId: item.userId, |
| | | nickName: item.nickName || '', |
| | | userName: item.userName || '' |
| | | })); |
| | | |
| | | dialogVisible.value = true |
| | | } |
| | | |
| | |
| | | return |
| | | } |
| | | |
| | | // 审批人必填校验 |
| | | const hasEmptyApprover = approverNodes.value.some(node => !node.userId) |
| | | if (hasEmptyApprover) { |
| | | ElMessage.error('请为所有审批节点选择审批人!') |
| | | return |
| | | } |
| | | |
| | | // 收集所有节点的审批人id |
| | | form.approveUserIds = approverNodes.value.map(node => node.userId).join(',') |
| | | |
| | | // 计算所有产品的单价总和 |
| | | form.totalAmount = form.products.reduce((sum, product) => { |
| | | const price = Number(product.unitPrice) || 0 |
| | | return sum + price |
| | | }, 0) |
| | | |
| | | if (isEdit.value) { |
| | | // 编辑 |
| | | const index = quotationList.value.findIndex(item => item.id === editId.value) |
| | |
| | | handleSearch() |
| | | } |
| | | }) |
| | | // quotationList.value[index] = { ...form, id: editId.value } |
| | | // ElMessage.success('编辑成功') |
| | | } |
| | | } else { |
| | | // 新增 |
| | | // const newId = Math.max(...quotationList.value.map(item => item.id)) + 1 |
| | | form.quotationNo = `QT${new Date().getFullYear()}${String(new Date().getMonth() + 1).padStart(2, '0')}${String(new Date().getDate()).padStart(2, '0')}` |
| | | |
| | | addQuotation(form).then(res=>{ |
| | | // console.log(res) |
| | | if(res.code===200){ |
| | | ElMessage.success('新增成功') |
| | | dialogVisible.value = false |
| | | handleSearch() |
| | | } |
| | | }) |
| | | |
| | | // quotationList.value.push({ |
| | | // ...form, |
| | | // // id: newId, |
| | | // quotationNo: quotationNo |
| | | // }) |
| | | // pagination.total++ |
| | | // ElMessage.success('新增成功') |
| | | } |
| | | |
| | | } |
| | |
| | | } |
| | | const handleSearch = ()=>{ |
| | | const params = { |
| | | page:pagination, |
| | | ...pagination, |
| | | ...searchForm |
| | | } |
| | | getQuotationList(params).then(res=>{ |
| | |
| | | validDate: item.validDate || '', |
| | | paymentMethod: item.paymentMethod || '', |
| | | status: item.status || '草稿', |
| | | // 审批人(用于编辑时反显) |
| | | approveUserIds: item.approveUserIds || '', |
| | | remark: item.remark || '', |
| | | products: item.products ? item.products.map(product => ({ |
| | | productId: product.productId || '', |
| | |
| | | }) |
| | | </script> |
| | | |
| | | <style scoped> |
| | | <style scoped lang="scss"> |
| | | .search-row { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | .quotation-form-container { |
| | | padding: 10px 0; |
| | | max-height: calc(100vh - 200px); |
| | | overflow-y: auto; |
| | | |
| | | &::-webkit-scrollbar { |
| | | width: 6px; |
| | | height: 6px; |
| | | } |
| | | |
| | | &::-webkit-scrollbar-thumb { |
| | | background: #c1c1c1; |
| | | border-radius: 3px; |
| | | |
| | | &:hover { |
| | | background: #a8a8a8; |
| | | } |
| | | } |
| | | } |
| | | |
| | | .quotation-form { |
| | | .el-form-item { |
| | | margin-bottom: 22px; |
| | | } |
| | | } |
| | | |
| | | .form-card { |
| | | margin-bottom: 20px; |
| | | margin-bottom: 24px; |
| | | border-radius: 8px; |
| | | transition: all 0.3s ease; |
| | | |
| | | &:hover { |
| | | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08) !important; |
| | | } |
| | | |
| | | :deep(.el-card__header) { |
| | | padding: 16px 20px; |
| | | background: linear-gradient(135deg, #f5f7fa 0%, #ffffff 100%); |
| | | border-bottom: 1px solid #ebeef5; |
| | | } |
| | | |
| | | :deep(.el-card__body) { |
| | | padding: 20px; |
| | | } |
| | | } |
| | | |
| | | .card-header-wrapper { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | |
| | | .card-icon { |
| | | font-size: 18px; |
| | | color: #409eff; |
| | | } |
| | | |
| | | .card-title { |
| | | font-weight: bold; |
| | | font-weight: 600; |
| | | font-size: 16px; |
| | | color: #303133; |
| | | flex: 1; |
| | | } |
| | | |
| | | .card-header { |
| | | .header-btn { |
| | | margin-left: auto; |
| | | } |
| | | } |
| | | |
| | | .form-content { |
| | | padding: 8px 0; |
| | | } |
| | | |
| | | .approver-nodes-container { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | flex-wrap: wrap; |
| | | gap: 24px; |
| | | padding: 12px 0; |
| | | } |
| | | |
| | | .approver-node-item { |
| | | display: flex; |
| | | flex-direction: column; |
| | | align-items: center; |
| | | gap: 12px; |
| | | padding: 16px; |
| | | background: #f8f9fa; |
| | | border-radius: 8px; |
| | | border: 1px solid #e4e7ed; |
| | | transition: all 0.3s ease; |
| | | min-width: 180px; |
| | | |
| | | &:hover { |
| | | border-color: #409eff; |
| | | background: #f0f7ff; |
| | | box-shadow: 0 2px 8px rgba(64, 158, 255, 0.1); |
| | | } |
| | | } |
| | | |
| | | .approver-node-label { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | font-size: 14px; |
| | | color: #606266; |
| | | |
| | | .node-step { |
| | | display: inline-flex; |
| | | align-items: center; |
| | | justify-content: center; |
| | | width: 24px; |
| | | height: 24px; |
| | | background: #409eff; |
| | | color: #fff; |
| | | border-radius: 50%; |
| | | font-size: 12px; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .node-text { |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .arrow-icon { |
| | | color: #909399; |
| | | font-size: 14px; |
| | | } |
| | | } |
| | | |
| | | .approver-select { |
| | | width: 100%; |
| | | min-width: 150px; |
| | | } |
| | | |
| | | .remove-btn { |
| | | margin-top: 4px; |
| | | } |
| | | |
| | | .product-table { |
| | | :deep(.el-table__header) { |
| | | background-color: #f5f7fa; |
| | | |
| | | th { |
| | | background-color: #f5f7fa !important; |
| | | color: #606266; |
| | | font-weight: 600; |
| | | } |
| | | } |
| | | |
| | | :deep(.el-table__row) { |
| | | &:hover { |
| | | background-color: #f5f7fa; |
| | | } |
| | | } |
| | | |
| | | :deep(.el-table__cell) { |
| | | padding: 12px 0; |
| | | } |
| | | } |
| | | |
| | | .dialog-footer { |
| | | text-align: right; |
| | | } |
| | | |
| | | // 响应式优化 |
| | | @media (max-width: 1200px) { |
| | | .approver-nodes-container { |
| | | gap: 16px; |
| | | } |
| | | |
| | | .approver-node-item { |
| | | min-width: 160px; |
| | | } |
| | | } |
| | | </style> |