Commit 84cf56639110e7c6b141abb277dd6a90013f949a

Authored by gesilong
1 parent 925126ad

commit: 业务员报价单模块联调

@@ -8,3 +8,67 @@ export function salesmanQuotationQueryApi(params) { @@ -8,3 +8,67 @@ export function salesmanQuotationQueryApi(params) {
8 params 8 params
9 }) 9 })
10 } 10 }
  11 +
  12 +// 生成编号
  13 +export function salesmanQuotationGenerateCodeApi() {
  14 + return request({
  15 + url: '/procurement/salesmanQuotation/generateCode',
  16 + method: 'get'
  17 + })
  18 +}
  19 +
  20 +// 新增保存
  21 +export function salesmanQuotationSaveApi(data) {
  22 + return request({
  23 + url: '/procurement/salesmanQuotation',
  24 + method: 'post',
  25 + data,
  26 + contentType: ContentTypeEnum.JSON
  27 + })
  28 +}
  29 +
  30 +// 详情
  31 +export function salesmanQuotationDetailApi(params) {
  32 + return request({
  33 + url: '/procurement/salesmanQuotation',
  34 + method: 'get',
  35 + params
  36 + })
  37 +}
  38 +
  39 +// 审核详情
  40 +export function salesmanQuotationGetExamineByIdApi(id) {
  41 + return request({
  42 + url: '/procurement/salesmanQuotation/getExamineById',
  43 + method: 'get',
  44 + params: { id }
  45 + })
  46 +}
  47 +
  48 +// 编辑
  49 +export function salesmanQuotationUpdateApi(data) {
  50 + return request({
  51 + url: '/procurement/salesmanQuotation',
  52 + method: 'put',
  53 + data,
  54 + contentType: ContentTypeEnum.JSON
  55 + })
  56 +}
  57 +
  58 +// 品种关系查询
  59 +export function breedRelationshipQueryApi(params) {
  60 + return request({
  61 + url: '/baseData/breedRelationship/query',
  62 + method: 'get',
  63 + params
  64 + })
  65 +}
  66 +
  67 +// 取消
  68 +export function salesmanQuotationCancelApi(params) {
  69 + return request({
  70 + url: '/procurement/salesmanQuotation/cancel',
  71 + method: 'post',
  72 + params
  73 + })
  74 +}
@@ -283,30 +283,38 @@ @@ -283,30 +283,38 @@
283 "navigationBarTextStyle": "black" 283 "navigationBarTextStyle": "black"
284 } 284 }
285 }, 285 },
286 - // {  
287 - // "path": "pages/sales-price/add",  
288 - // "style": {  
289 - // "navigationBarTitleText": "新增业务员报价单",  
290 - // "navigationBarBackgroundColor": "#ffffff",  
291 - // "navigationBarTextStyle": "black"  
292 - // }  
293 - // },  
294 - // {  
295 - // "path": "pages/sales-price/modify",  
296 - // "style": {  
297 - // "navigationBarTitleText": "编辑业务员报价单",  
298 - // "navigationBarBackgroundColor": "#ffffff",  
299 - // "navigationBarTextStyle": "black"  
300 - // }  
301 - // },  
302 - // {  
303 - // "path": "pages/sales-price/detail",  
304 - // "style": {  
305 - // "navigationBarTitleText": "查看业务员报价单",  
306 - // "navigationBarBackgroundColor": "#ffffff",  
307 - // "navigationBarTextStyle": "black"  
308 - // }  
309 - // }, 286 + {
  287 + "path": "pages/sales-price/add",
  288 + "style": {
  289 + "navigationBarTitleText": "新增业务员报价单",
  290 + "navigationBarBackgroundColor": "#ffffff",
  291 + "navigationBarTextStyle": "black"
  292 + }
  293 + },
  294 + {
  295 + "path": "pages/sales-price/modify",
  296 + "style": {
  297 + "navigationBarTitleText": "编辑业务员报价单",
  298 + "navigationBarBackgroundColor": "#ffffff",
  299 + "navigationBarTextStyle": "black"
  300 + }
  301 + },
  302 + {
  303 + "path": "pages/sales-price/detail",
  304 + "style": {
  305 + "navigationBarTitleText": "查看业务员报价单",
  306 + "navigationBarBackgroundColor": "#ffffff",
  307 + "navigationBarTextStyle": "black"
  308 + }
  309 + },
  310 + {
  311 + "path": "pages/sales-price/copy",
  312 + "style": {
  313 + "navigationBarTitleText": "复制业务员报价单",
  314 + "navigationBarBackgroundColor": "#ffffff",
  315 + "navigationBarTextStyle": "black"
  316 + }
  317 + },
310 318
311 { 319 {
312 "path": "pages/supplier-price/index", 320 "path": "pages/supplier-price/index",
  1 +<template>
  2 + <view class="page">
  3 + <scroll-view class="scroll" scroll-y>
  4 + <uni-list>
  5 + <!-- 编号 -->
  6 + <uni-list-item class="select-item" :class="form.code ? 'is-filled' : 'is-empty'">
  7 + <template v-slot:body>
  8 + <view class="item-title"><text class="required">*</text><text>编号</text></view>
  9 + </template>
  10 + <template v-slot:footer>
  11 + <view class="serial-number-row">
  12 + <text class="readonly-text value-code">{{ form.code }}</text>
  13 + <button class="generate-btn" @click="initCode">点此生成</button>
  14 + </view>
  15 + </template>
  16 + </uni-list-item>
  17 +
  18 + <!-- 供应商名称 -->
  19 + <uni-list-item class="select-item" :class="form.supplierId ? 'is-filled' : 'is-empty'" clickable
  20 + @click="openSupplierSheet" :rightText="form.supplierName || '请选择供应商名称'" showArrow>
  21 + <template v-slot:body>
  22 + <view class="item-title"><text class="required">*</text><text>供应商名称</text></view>
  23 + </template>
  24 + </uni-list-item>
  25 +
  26 + <!-- 采购处(带出,禁用) -->
  27 + <uni-list-item title="采购处">
  28 + <template v-slot:footer>
  29 + <uni-easyinput v-model="form.purchaseDepartmentName" :inputBorder="false" disabled placeholder="选择供应商后自动带出" />
  30 + </template>
  31 + </uni-list-item>
  32 +
  33 + <!-- 区域(带出,禁用) -->
  34 + <uni-list-item title="区域">
  35 + <template v-slot:footer>
  36 + <uni-easyinput v-model="form.regionName" :inputBorder="false" disabled placeholder="选择供应商后自动带出" />
  37 + </template>
  38 + </uni-list-item>
  39 +
  40 + <!-- 报价时效 -->
  41 + <uni-list-item>
  42 + <template v-slot:body>
  43 + <view class="item-title"><text class="required">*</text><text>报价时效</text></view>
  44 + </template>
  45 + <template v-slot:footer>
  46 + <uni-easyinput v-model="form.validityPeriod" :inputBorder="false" placeholder="请输入报价时效" />
  47 + </template>
  48 + </uni-list-item>
  49 +
  50 + <!-- 报价人(当前登录用户,禁用) -->
  51 + <uni-list-item title="报价人">
  52 + <template v-slot:footer>
  53 + <uni-easyinput v-model="form.quoterName" :inputBorder="false" disabled placeholder="自动获取" />
  54 + </template>
  55 + </uni-list-item>
  56 +
  57 + <!-- 是否长单操作 -->
  58 + <uni-list-item class="select-item" :class="typeof form.longTermOperation === 'boolean' ? 'is-filled' : 'is-empty'" clickable
  59 + @click="openSheet('longTermOperation')" :rightText="boolText(form.longTermOperation)" showArrow>
  60 + <template v-slot:body>
  61 + <view class="item-title"><text class="required">*</text><text>是否长单操作</text></view>
  62 + </template>
  63 + </uni-list-item>
  64 +
  65 + <!-- 是否终端客户 -->
  66 + <uni-list-item class="select-item" :class="typeof form.terminalCustomer === 'boolean' ? 'is-filled' : 'is-empty'" clickable
  67 + @click="openSheet('terminalCustomer')" :rightText="boolText(form.terminalCustomer)" showArrow>
  68 + <template v-slot:body>
  69 + <view class="item-title"><text class="required">*</text><text>是否终端客户</text></view>
  70 + </template>
  71 + </uni-list-item>
  72 +
  73 + <!-- 是否未点价 -->
  74 + <uni-list-item class="select-item" :class="typeof form.priced === 'boolean' ? 'is-filled' : 'is-empty'" clickable
  75 + @click="openSheet('priced')" :rightText="boolText(form.priced)" showArrow>
  76 + <template v-slot:body>
  77 + <view class="item-title"><text class="required">*</text><text>是否未点价</text></view>
  78 + </template>
  79 + </uni-list-item>
  80 +
  81 + <!-- 点价截止日期 - 仅在 是否未点价 = 否 时显示且必填 -->
  82 + <uni-list-item v-if="form.priced === false" class="select-item" :class="form.pricingDeadline ? 'is-filled' : 'is-empty'">
  83 + <template v-slot:body>
  84 + <view class="item-title"><text class="required">*</text><text>点价截止日期</text></view>
  85 + </template>
  86 + <template v-slot:footer>
  87 + <uni-datetime-picker type="date" v-model="form.pricingDeadline" />
  88 + </template>
  89 + </uni-list-item>
  90 +
  91 + <!-- 其他费用凭票到厂另行结算 -->
  92 + <uni-list-item class="select-item" :class="typeof form.otherExpenseSettlement === 'boolean' ? 'is-filled' : 'is-empty'" clickable
  93 + @click="openSheet('otherExpenseSettlement')" :rightText="boolText(form.otherExpenseSettlement)" showArrow>
  94 + <template v-slot:body>
  95 + <view class="item-title"><text class="required">*</text><text>其他费用凭票到厂另行结算</text></view>
  96 + </template>
  97 + </uni-list-item>
  98 +
  99 + <!-- 交提货方式 -->
  100 + <uni-list-item class="select-item" :class="form.deliveryMode ? 'is-filled' : 'is-empty'" clickable
  101 + @click="openDictSheet('deliveryMode', 'DELIVERY_METHOD', '交提货方式')" :rightText="getDeliveryMethodLabel() || '请选择交提货方式'" showArrow>
  102 + <template v-slot:body>
  103 + <view class="item-title"><text class="required">*</text><text>交提货方式</text></view>
  104 + </template>
  105 + </uni-list-item>
  106 +
  107 + <!-- 是否通货买断 -->
  108 + <uni-list-item class="select-item" :class="typeof form.commodityBuyout === 'boolean' ? 'is-filled' : 'is-empty'" clickable
  109 + @click="openSheet('commodityBuyout')" :rightText="boolText(form.commodityBuyout)" showArrow>
  110 + <template v-slot:body>
  111 + <view class="item-title"><text class="required">*</text><text>是否通货买断</text></view>
  112 + </template>
  113 + </uni-list-item>
  114 +
  115 + <!-- 是否质量买断 -->
  116 + <uni-list-item class="select-item" :class="typeof form.qualityBuyout === 'boolean' ? 'is-filled' : 'is-empty'" clickable
  117 + @click="openSheet('qualityBuyout')" :rightText="boolText(form.qualityBuyout)" showArrow>
  118 + <template v-slot:body>
  119 + <view class="item-title"><text class="required">*</text><text>是否质量买断</text></view>
  120 + </template>
  121 + </uni-list-item>
  122 +
  123 + <!-- 是否数量买断 -->
  124 + <uni-list-item class="select-item" :class="typeof form.quantityBuyout === 'boolean' ? 'is-filled' : 'is-empty'" clickable
  125 + @click="openSheet('quantityBuyout')" :rightText="boolText(form.quantityBuyout)" showArrow>
  126 + <template v-slot:body>
  127 + <view class="item-title"><text class="required">*</text><text>是否数量买断</text></view>
  128 + </template>
  129 + </uni-list-item>
  130 +
  131 + <!-- 是否送货上门 -->
  132 + <uni-list-item class="select-item" :class="typeof form.doorDelivery === 'boolean' ? 'is-filled' : 'is-empty'" clickable
  133 + @click="openSheet('doorDelivery')" :rightText="boolText(form.doorDelivery)" showArrow>
  134 + <template v-slot:body>
  135 + <view class="item-title"><text class="required">*</text><text>是否送货上门</text></view>
  136 + </template>
  137 + </uni-list-item>
  138 +
  139 + <!-- 装车费 -->
  140 + <uni-list-item title="装车费">
  141 + <template v-slot:footer>
  142 + <uni-easyinput v-model="form.loadingFee" type="digit" :inputBorder="false" placeholder="请输入装车费" />
  143 + </template>
  144 + </uni-list-item>
  145 +
  146 + <!-- 开票品种 -->
  147 + <uni-list-item>
  148 + <template v-slot:body>
  149 + <view class="item-title"><text class="required">*</text><text>开票品种</text></view>
  150 + </template>
  151 + <template v-slot:footer>
  152 + <uni-easyinput v-model="form.invoiceProductName" :inputBorder="false" placeholder="请输入开票品种" />
  153 + </template>
  154 + </uni-list-item>
  155 +
  156 + <!-- 交货日期 -->
  157 + <uni-list-item>
  158 + <template v-slot:body>
  159 + <view class="item-title"><text class="required">*</text><text>交货日期</text></view>
  160 + </template>
  161 + <template v-slot:footer>
  162 + <uni-datetime-picker type="date" v-model="form.deliveryDate" />
  163 + </template>
  164 + </uni-list-item>
  165 +
  166 + <!-- 结算方式 -->
  167 + <uni-list-item>
  168 + <template v-slot:body>
  169 + <view class="item-title"><text class="required">*</text><text>结算方式</text></view>
  170 + </template>
  171 + <template v-slot:footer>
  172 + <uni-easyinput v-model="form.settlementMethod" :inputBorder="false" placeholder="请输入结算方式" />
  173 + </template>
  174 + </uni-list-item>
  175 +
  176 + <!-- 结算比例(%) -->
  177 + <uni-list-item>
  178 + <template v-slot:body>
  179 + <view class="item-title"><text class="required">*</text><text>结算比例(%)</text></view>
  180 + </template>
  181 + <template v-slot:footer>
  182 + <uni-easyinput v-model="form.settlementRatio" type="digit" :inputBorder="false" placeholder="请输入结算比例" @blur="onRatioBlur" />
  183 + </template>
  184 + </uni-list-item>
  185 +
  186 + <!-- 协调事由 -->
  187 + <uni-list-item title="协调事由">
  188 + <template v-slot:footer>
  189 + <uni-easyinput v-model="form.coordinationReason" :inputBorder="false" placeholder="请输入协调事由" disabled />
  190 + </template>
  191 + </uni-list-item>
  192 +
  193 + <!-- 票类状况 -->
  194 + <uni-list-item title="票类状况">
  195 + <template v-slot:footer>
  196 + <uni-easyinput v-model="form.invoiceTypeStatus" :inputBorder="false" placeholder="请输入票类状况" disabled />
  197 + </template>
  198 + </uni-list-item>
  199 +
  200 + <!-- 明细信息 -->
  201 + <ProductRel ref="productRel" mode="add" :options="productList" :priced="form.priced" @change="onProductsChange" />
  202 + </uni-list>
  203 +
  204 + <!-- 料质状况模块(明细下方) -->
  205 + <view class="material-condition-section">
  206 + <view class="section-header">
  207 + <image class="opCollapse" src="/static/images/title.png" />
  208 + <text class="section-title">料质状况</text>
  209 + </view>
  210 + <view class="section-content">
  211 + <view class="form-row">
  212 + <text class="form-label">是否启用料质状况</text>
  213 + <view class="bool-btns">
  214 + <view class="bool-btn" :class="{ active: form.materialConditionEnabled === true }" @click="form.materialConditionEnabled = true">是</view>
  215 + <view class="bool-btn" :class="{ active: form.materialConditionEnabled === false }" @click="form.materialConditionEnabled = false">否</view>
  216 + </view>
  217 + </view>
  218 +
  219 + <template v-if="form.materialConditionEnabled">
  220 + <view v-for="mc in materialConditionConfigs" :key="mc.field" class="material-group">
  221 + <text class="material-label">{{ mc.label }}</text>
  222 + <view class="tag-list">
  223 + <view
  224 + v-for="opt in getDictOptions(mc.dictCode)"
  225 + :key="opt.code"
  226 + class="tag-btn"
  227 + :class="{ active: isTagSelected(mc.field, opt.code) }"
  228 + @click="toggleTag(mc.field, opt.code)"
  229 + >
  230 + {{ opt.name }}
  231 + </view>
  232 + </view>
  233 + </view>
  234 + </template>
  235 + </view>
  236 + </view>
  237 +
  238 + <!-- 备注模块 -->
  239 + <view class="remark-section">
  240 + <view class="section-header">
  241 + <image class="opCollapse" src="/static/images/title.png" />
  242 + <text class="section-title">备注</text>
  243 + </view>
  244 + <view class="section-content">
  245 + <uni-easyinput v-model="form.remark" type="textarea" :inputBorder="false" placeholder="请输入备注" maxlength="2000" />
  246 + </view>
  247 + </view>
  248 +
  249 + <view class="footer">
  250 + <button class="btn submit" type="primary" @click="onSubmit">提交</button>
  251 + </view>
  252 + </scroll-view>
  253 +
  254 + <SingleSelectSheet :visible.sync="sheet.visible" :title="sheet.title" :options="sheet.options"
  255 + v-model="sheet.value" @confirm="onSheetConfirm" />
  256 +
  257 + <RelateSelectSheet :visible.sync="supplierSheet.visible" :title="supplierSheet.title"
  258 + :fetchFn="supplierSheet.fetchFn" :display-fields="supplierSheet.displayFields"
  259 + :multiple="false" row-key="id" :selectedKeys.sync="supplierSheet.selectedKeys"
  260 + @confirm="onSupplierConfirm" />
  261 + </view>
  262 +</template>
  263 +<script>
  264 +import SingleSelectSheet from '@/components/single-select/index.vue'
  265 +import RelateSelectSheet from '@/components/relate-select/index.vue'
  266 +import ProductRel from './productRel.vue'
  267 +import { salesmanQuotationGenerateCodeApi, salesmanQuotationSaveApi, breedRelationshipQueryApi } from '@/api/procure-manage/salesmanQuotation.js'
  268 +import { domesticCustomerCreditQueryApi } from '@/api/procure-manage/domesticCustomerCredit.js'
  269 +import { getInfo } from '@/api/login.js'
  270 +import { getDicByCodes } from '@/utils/dic.js'
  271 +
  272 +export default {
  273 + name: 'AddSalesPrice',
  274 + components: { SingleSelectSheet, RelateSelectSheet, ProductRel },
  275 + data() {
  276 + return {
  277 + form: {
  278 + code: '',
  279 + supplierId: '',
  280 + supplierName: '',
  281 + purchaseDepartmentName: '',
  282 + regionName: '',
  283 + validityPeriod: '',
  284 + quoterName: '',
  285 + longTermOperation: false,
  286 + terminalCustomer: false,
  287 + priced: true,
  288 + pricingDeadline: '',
  289 + otherExpenseSettlement: false,
  290 + deliveryMode: '',
  291 + commodityBuyout: false,
  292 + qualityBuyout: false,
  293 + quantityBuyout: false,
  294 + doorDelivery: false,
  295 + loadingFee: '',
  296 + invoiceProductName: '',
  297 + deliveryDate: '',
  298 + settlementMethod: '',
  299 + settlementRatio: '',
  300 + coordinationReason: '受市场影响,申请协调协调事由',
  301 + invoiceTypeStatus: '增票',
  302 + materialConditionEnabled: false,
  303 + materialConditionCopperRice: [],
  304 + materialConditionBrightCopper: [],
  305 + materialConditionFirstClass: [],
  306 + materialConditionSecondClass: [],
  307 + materialConditionTinCoated: [],
  308 + materialConditionScrap: [],
  309 + materialConditionWhiteYellow: [],
  310 + materialConditionPhosphorCopper: [],
  311 + remark: ''
  312 + },
  313 + productList: [],
  314 + productLineList: [],
  315 + sheet: { visible: false, title: '请选择', options: [], value: '', field: '' },
  316 + supplierSheet: {
  317 + visible: false,
  318 + title: '选择供应商',
  319 + fetchFn: null,
  320 + displayFields: [
  321 + { label: '单位名称', field: 'unitName' },
  322 + { label: '采购处', field: 'purchaseDepartmentName' },
  323 + { label: '区域', field: 'regionName' }
  324 + ],
  325 + selectedKeys: []
  326 + },
  327 + materialConditionConfigs: [
  328 + { field: 'materialConditionCopperRice', label: '铜米', dictCode: 'COPPER_RICE' },
  329 + { field: 'materialConditionBrightCopper', label: '光亮铜', dictCode: 'BRIGHT_COPPER' },
  330 + { field: 'materialConditionFirstClass', label: '一特', dictCode: 'FIRST_CLASS' },
  331 + { field: 'materialConditionSecondClass', label: '二特', dictCode: 'SECOND_CLASS' },
  332 + { field: 'materialConditionTinCoated', label: '镀锡紫', dictCode: 'TIN_COATED' },
  333 + { field: 'materialConditionScrap', label: '角料', dictCode: 'SCRAP' },
  334 + { field: 'materialConditionWhiteYellow', label: '镀白黄', dictCode: 'WHITE_YELLOW' },
  335 + { field: 'materialConditionPhosphorCopper', label: '磷铜', dictCode: 'PHOSPHOR_COPPER' }
  336 + ],
  337 + dictData: {}
  338 + }
  339 + },
  340 + created() {
  341 + this.supplierSheet.fetchFn = this.fetchSuppliers.bind(this)
  342 + this.initCode()
  343 + this.loadQuoter()
  344 + this.loadProducts()
  345 + this.loadDictData()
  346 + },
  347 + methods: {
  348 + async loadDictData() {
  349 + try {
  350 + const codes = [...this.materialConditionConfigs.map(mc => mc.dictCode), 'DELIVERY_METHOD']
  351 + const res = await getDicByCodes(codes)
  352 + this.dictData = res || {}
  353 + } catch (e) {
  354 + this.dictData = {}
  355 + }
  356 + },
  357 + getDictOptions(dictCode) {
  358 + const data = this.dictData[dictCode]
  359 + return (data && data.data) ? data.data : []
  360 + },
  361 + isTagSelected(field, code) {
  362 + const selected = this.form[field]
  363 + return Array.isArray(selected) && selected.includes(code)
  364 + },
  365 + toggleTag(field, code) {
  366 + let selected = this.form[field]
  367 + if (!Array.isArray(selected)) {
  368 + selected = []
  369 + }
  370 + const index = selected.indexOf(code)
  371 + if (index > -1) {
  372 + selected.splice(index, 1)
  373 + } else {
  374 + selected.push(code)
  375 + }
  376 + this.$set(this.form, field, [...selected])
  377 + },
  378 + async initCode() {
  379 + try {
  380 + const res = await salesmanQuotationGenerateCodeApi()
  381 + this.form.code = (res && res.data) ? res.data : ''
  382 + } catch (e) {
  383 + this.form.code = ''
  384 + }
  385 + },
  386 + async loadQuoter() {
  387 + try {
  388 + // 优先使用 store 中已缓存的用户信息
  389 + const storeUser = this.$store && this.$store.state && this.$store.state.user ? this.$store.state.user : {}
  390 + if (storeUser.name) {
  391 + this.form.quoterName = storeUser.name
  392 + return
  393 + }
  394 + const res = await getInfo()
  395 + const _data = res && res.data ? res.data : {}
  396 + this.form.quoterName = _data.name || ''
  397 + } catch (e) {
  398 + this.form.quoterName = ''
  399 + }
  400 + },
  401 + async loadProducts() {
  402 + try {
  403 + const res = await breedRelationshipQueryApi({ pageIndex: 1, pageSize: 9999 })
  404 + const _data = res && res.data ? res.data : {}
  405 + const list = _data.datas || _data.list || _data.records || []
  406 + this.productList = list.map(it => ({
  407 + label: it.productName || it.name || '',
  408 + value: it.productId || it.id || '',
  409 + lossRate: it.fixedLossRate != null ? it.fixedLossRate : (it.lossRate != null ? it.lossRate : (it.attritionRate != null ? it.attritionRate : ''))
  410 + }))
  411 + } catch (e) {
  412 + this.productList = []
  413 + }
  414 + },
  415 + // 供应商列表数据源(RelateSelectSheet fetchFn)
  416 + fetchSuppliers({ pageIndex, pageSize, extra }) {
  417 + const name = (extra && (extra.name || extra.keyword || extra.key)) || ''
  418 + const params = {
  419 + pageIndex,
  420 + pageSize,
  421 + available: true,
  422 + unitName: name,
  423 + status: 'PASS',
  424 + freeze: false
  425 + }
  426 + return domesticCustomerCreditQueryApi(params).then(res => {
  427 + const _data = res && res.data ? res.data : {}
  428 + const records = _data.datas || _data.list || _data.records || []
  429 + const totalCount = _data.totalCount || _data.count || 0
  430 + const hasNext = _data.hasNext || false
  431 + return { records, totalCount, hasNext }
  432 + }).catch(() => ({ records: [], totalCount: 0, hasNext: false }))
  433 + },
  434 + openSupplierSheet() {
  435 + this.supplierSheet.selectedKeys = this.form.supplierId ? [this.form.supplierId] : []
  436 + this.supplierSheet.visible = true
  437 + },
  438 + onSupplierConfirm({ items }) {
  439 + const first = (items && items.length > 0) ? items[0] : null
  440 + if (!first) return
  441 + this.form.supplierId = first.id || ''
  442 + this.form.supplierName = first.unitName || ''
  443 + this.form.purchaseDepartmentName = first.purchaseDepartmentName || ''
  444 + this.form.regionName = first.regionName || ''
  445 + },
  446 + boolText(v) {
  447 + if (typeof v !== 'boolean') return '请选择'
  448 + return v ? '是' : '否'
  449 + },
  450 + openSheet(field) {
  451 + const opts = [{ label: '是', value: true }, { label: '否', value: false }]
  452 + const cur = this.form[field]
  453 + const match = opts.find(o => o.value === cur)
  454 + const titleMap = {
  455 + longTermOperation: '是否长单操作',
  456 + terminalCustomer: '是否终端客户',
  457 + priced: '是否未点价',
  458 + otherExpenseSettlement: '其他费用凭票到厂另行结算',
  459 + commodityBuyout: '是否通货买断',
  460 + qualityBuyout: '是否质量买断',
  461 + quantityBuyout: '是否数量买断',
  462 + doorDelivery: '是否送货上门'
  463 + }
  464 + this.sheet = { visible: true, title: titleMap[field] || '请选择', options: opts, value: match ? match.value : '', field, isDict: false }
  465 + },
  466 + openDictSheet(field, dictCode, title) {
  467 + const raw = this.getDictOptions(dictCode) || []
  468 + const opts = raw.map(o => ({ label: o.name || o.label || '', value: o.code != null ? o.code : (o.value != null ? o.value : '') }))
  469 + const cur = this.form[field]
  470 + const match = opts.find(o => String(o.value) === String(cur))
  471 + this.sheet = { visible: true, title: title || '请选择', options: opts, value: match ? match.value : '', field, isDict: true }
  472 + },
  473 + getDeliveryMethodLabel() {
  474 + const opts = this.getDictOptions('DELIVERY_METHOD') || []
  475 + const m = opts.find(o => String(o.code != null ? o.code : o.value) === String(this.form.deliveryMode))
  476 + return m ? (m.name || m.label || '') : ''
  477 + },
  478 + onSheetConfirm({ value }) {
  479 + const field = this.sheet.field
  480 + if (!field) return
  481 + if (this.sheet.isDict) {
  482 + this.form[field] = value
  483 + } else {
  484 + this.form[field] = !!value
  485 + // 切换 是否未点价 时,若改为 是 则清空点价截止日期
  486 + if (field === 'priced' && value === true) {
  487 + this.form.pricingDeadline = ''
  488 + }
  489 + }
  490 + },
  491 + onRatioBlur() {
  492 + let v = parseFloat(this.form.settlementRatio)
  493 + if (isNaN(v)) v = ''
  494 + else v = Math.round(v * 100) / 100
  495 + this.form.settlementRatio = v
  496 + },
  497 + onProductsChange(products) {
  498 + this.productLineList = Array.isArray(products) ? products : []
  499 + },
  500 + validateRequired() {
  501 + const stringChecks = [
  502 + { key: 'code', label: '编号', msg: '请输入编号' },
  503 + { key: 'supplierId', label: '供应商名称', msg: '请选择供应商' },
  504 + { key: 'validityPeriod', label: '报价时效', msg: '请输入报价时效' },
  505 + { key: 'deliveryMode', label: '交提货方式', msg: '请选择交提货方式' },
  506 + { key: 'invoiceProductName', label: '开票品种', msg: '请输入开票品种' },
  507 + { key: 'deliveryDate', label: '交货日期', msg: '请选择交货日期' },
  508 + { key: 'settlementMethod', label: '结算方式', msg: '请输入结算方式' },
  509 + { key: 'settlementRatio', label: '结算比例', msg: '请输入结算比例' }
  510 + ]
  511 + for (const it of stringChecks) {
  512 + const val = this.form[it.key]
  513 + const empty = (val === undefined || val === null || (typeof val === 'string' && val.trim() === ''))
  514 + if (empty) { uni.showToast({ title: it.msg, icon: 'none' }); return false }
  515 + }
  516 + const boolChecks = [
  517 + { key: 'longTermOperation', msg: '请选择是否长单操作' },
  518 + { key: 'terminalCustomer', msg: '请选择是否终端客户' },
  519 + { key: 'priced', msg: '请选择是否未点价' },
  520 + { key: 'otherExpenseSettlement', msg: '请选择其他费用凭票到厂另行结算' },
  521 + { key: 'commodityBuyout', msg: '请选择是否通货买断' },
  522 + { key: 'qualityBuyout', msg: '请选择是否质量买断' },
  523 + { key: 'quantityBuyout', msg: '请选择是否数量买断' },
  524 + { key: 'doorDelivery', msg: '请选择是否送货上门' }
  525 + ]
  526 + for (const it of boolChecks) {
  527 + if (typeof this.form[it.key] !== 'boolean') {
  528 + uni.showToast({ title: it.msg, icon: 'none' })
  529 + return false
  530 + }
  531 + }
  532 + // 点价截止日期:是否未点价=否 时必填
  533 + if (this.form.priced === false && !this.form.pricingDeadline) {
  534 + uni.showToast({ title: '请选择点价截止日期', icon: 'none' })
  535 + return false
  536 + }
  537 + // 明细校验
  538 + const list = Array.isArray(this.productLineList) ? this.productLineList : []
  539 + if (list.length === 0) {
  540 + uni.showToast({ title: '请添加产品明细', icon: 'none' })
  541 + return false
  542 + }
  543 + if (this.$refs.productRel && !this.$refs.productRel.validate(this.form.priced)) return false
  544 + return true
  545 + },
  546 + async onSubmit() {
  547 + if (!this.validateRequired()) return
  548 + const confirmRes = await new Promise(resolve => {
  549 + uni.showModal({ title: '提示', content: '确定新增业务员报价单吗?', confirmText: '确定', cancelText: '取消', success: resolve })
  550 + })
  551 + if (!(confirmRes && confirmRes.confirm)) return
  552 + const clean = (obj) => {
  553 + const out = {}
  554 + Object.keys(obj || {}).forEach(k => {
  555 + const v = obj[k]
  556 + const isEmptyString = typeof v === 'string' && v.trim() === ''
  557 + const isUndef = v === undefined || v === null
  558 + const isNaNNumber = typeof v === 'number' && isNaN(v)
  559 + if (!(isEmptyString || isUndef || isNaNNumber)) out[k] = v
  560 + })
  561 + return out
  562 + }
  563 + const lines = (this.productLineList || []).map(it => clean(it))
  564 + const materialFields = this.materialConditionConfigs.map(mc => mc.field)
  565 + const formCopy = { ...this.form }
  566 + materialFields.forEach(f => {
  567 + const arr = formCopy[f]
  568 + formCopy[f] = Array.isArray(arr) ? arr.join(',') : (arr || '')
  569 + })
  570 + const payload = clean({
  571 + ...formCopy,
  572 + quotationLineList: lines
  573 + })
  574 + try {
  575 + await salesmanQuotationSaveApi(payload)
  576 + uni.showToast({ title: '新增成功', icon: 'none' })
  577 + try { uni.setStorageSync('SALES_PRICE_LIST_NEED_REFRESH', '1') } catch (e) {}
  578 + setTimeout(() => { uni.redirectTo({ url: '/pages/sales-price/index' }) }, 400)
  579 + } catch (e) {
  580 + uni.showToast({ title: e.msg || '新增失败', icon: 'none' })
  581 + }
  582 + }
  583 + }
  584 +}
  585 +</script>
  586 +<style lang="scss" scoped>
  587 +.page {
  588 + display: flex;
  589 + flex-direction: column;
  590 + height: 100%;
  591 +}
  592 +
  593 +.scroll {
  594 + flex: 1;
  595 + padding: 12rpx 0 392rpx !important;
  596 +}
  597 +
  598 +.footer {
  599 + z-index: 2;
  600 + position: fixed;
  601 + left: 0;
  602 + right: 0;
  603 + bottom: 0;
  604 + padding: 32rpx;
  605 + padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
  606 + background: #fff;
  607 + box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
  608 +
  609 + .btn {
  610 + height: 80rpx;
  611 + line-height: 80rpx;
  612 + border-radius: 12rpx;
  613 + font-size: 32rpx;
  614 + }
  615 +
  616 + .submit {
  617 + background: $theme-primary;
  618 + color: #fff;
  619 + }
  620 +}
  621 +
  622 +::v-deep .uni-list {
  623 + .uni-easyinput {
  624 + display: flex;
  625 +
  626 + .uni-input-input {
  627 + color: rgba(0, 0, 0, 0.9);
  628 + }
  629 + }
  630 +
  631 + .uni-input-placeholder {
  632 + z-index: 1;
  633 + }
  634 +
  635 + .uni-input-input {
  636 + background-color: #ffffff;
  637 + }
  638 +
  639 + background: transparent;
  640 +
  641 + &-item {
  642 + &__extra-text {
  643 + font-size: 32rpx;
  644 + }
  645 +
  646 + &__content-title {
  647 + font-size: 32rpx;
  648 + color: rgba(0, 0, 0, 0.9);
  649 + }
  650 +
  651 + &__container {
  652 + padding: 32rpx;
  653 +
  654 + .uni-easyinput {
  655 + &__placeholder-class {
  656 + font-size: 32rpx;
  657 + color: rgba(0, 0, 0, 0.4);
  658 + }
  659 +
  660 + &__content {
  661 + border: none;
  662 + background-color: #ffffff !important;
  663 +
  664 + &-input {
  665 + padding-left: 0 !important;
  666 + height: 48rpx;
  667 + line-height: 48rpx;
  668 + font-size: 32rpx;
  669 + }
  670 +
  671 + .content-clear-icon {
  672 + font-size: 44rpx !important;
  673 + }
  674 + }
  675 + }
  676 +
  677 + .item-title,
  678 + .uni-list-item__content {
  679 + flex: none;
  680 + min-height: 48rpx;
  681 + line-height: 48rpx;
  682 + font-size: 32rpx;
  683 + position: relative;
  684 + width: 240rpx;
  685 + margin-right: 32rpx;
  686 + color: rgba(0, 0, 0, 0.9);
  687 +
  688 + .required {
  689 + color: red;
  690 + position: absolute;
  691 + top: 50%;
  692 + transform: translateY(-50%);
  693 + left: -16rpx;
  694 + }
  695 + }
  696 + }
  697 +
  698 + &.select-item {
  699 + &.is-empty {
  700 + .uni-list-item__extra-text {
  701 + color: rgba(0, 0, 0, 0.4) !important;
  702 + }
  703 + }
  704 +
  705 + &.is-filled {
  706 + .uni-list-item__extra-text {
  707 + color: rgba(0, 0, 0, 0.9) !important;
  708 + }
  709 + }
  710 +
  711 + .serial-number-row {
  712 + display: flex;
  713 + align-items: center;
  714 +
  715 + .generate-btn {
  716 + margin-left: 24rpx;
  717 + height: 48rpx;
  718 + line-height: 48rpx;
  719 + padding: 0 24rpx;
  720 + font-size: 24rpx;
  721 + border-radius: 8rpx;
  722 + background: $theme-primary;
  723 + color: #fff;
  724 + }
  725 + }
  726 + }
  727 + }
  728 +}
  729 +
  730 +.readonly-text {
  731 + color: rgba(0, 0, 0, 0.9);
  732 + font-size: 32rpx;
  733 + line-height: 48rpx;
  734 + text-align: right;
  735 + white-space: pre-wrap;
  736 + word-break: break-all;
  737 +}
  738 +
  739 +.value-code {
  740 + width: 260rpx;
  741 + text-align: left;
  742 +}
  743 +
  744 +.material-condition-section,
  745 +.remark-section {
  746 + margin-top: 20rpx;
  747 + background: #fff;
  748 +}
  749 +
  750 +.section-header {
  751 + display: flex;
  752 + align-items: center;
  753 + padding: 24rpx 32rpx;
  754 + border-bottom: 1rpx solid #f0f0f0;
  755 +}
  756 +
  757 +.section-title {
  758 + font-size: 32rpx;
  759 + color: rgba(0, 0, 0, 0.9);
  760 + font-weight: 600;
  761 +}
  762 +
  763 +.opCollapse {
  764 + width: 32rpx;
  765 + height: 28rpx;
  766 + margin-right: 16rpx;
  767 + margin-top: 8rpx;
  768 +}
  769 +
  770 +.section-content {
  771 + padding: 32rpx;
  772 +}
  773 +
  774 +.form-row {
  775 + display: flex;
  776 + align-items: center;
  777 + justify-content: space-between;
  778 + margin-bottom: 24rpx;
  779 +}
  780 +
  781 +.form-row:last-child {
  782 + margin-bottom: 0;
  783 +}
  784 +
  785 +.form-label {
  786 + font-size: 32rpx;
  787 + color: rgba(0, 0, 0, 0.9);
  788 +}
  789 +
  790 +.bool-btns {
  791 + display: flex;
  792 + gap: 20rpx;
  793 +}
  794 +
  795 +.bool-btn {
  796 + padding: 12rpx 40rpx;
  797 + border: 1rpx solid #ddd;
  798 + border-radius: 8rpx;
  799 + font-size: 28rpx;
  800 + color: #666;
  801 + background: #fff;
  802 +}
  803 +
  804 +.bool-btn.active {
  805 + background: $theme-primary;
  806 + border-color: $theme-primary;
  807 + color: #fff;
  808 +}
  809 +
  810 +.material-group {
  811 + margin-top: 32rpx;
  812 +}
  813 +
  814 +.material-label {
  815 + display: block;
  816 + font-size: 30rpx;
  817 + color: rgba(0, 0, 0, 0.9);
  818 + margin-bottom: 20rpx;
  819 +}
  820 +
  821 +.tag-list {
  822 + display: flex;
  823 + flex-wrap: wrap;
  824 + gap: 16rpx;
  825 +}
  826 +
  827 +.tag-btn {
  828 + padding: 12rpx 24rpx;
  829 + border: 1rpx solid #ddd;
  830 + border-radius: 8rpx;
  831 + font-size: 28rpx;
  832 + color: #666;
  833 + background: #fff;
  834 +}
  835 +
  836 +.tag-btn.active {
  837 + background: $theme-primary;
  838 + border-color: $theme-primary;
  839 + color: #fff;
  840 +}
  841 +
  842 +::v-deep .uni-easyinput {
  843 + width: 100%;
  844 +
  845 + &__content {
  846 + background: #f8f8f8 !important;
  847 + border-radius: 8rpx;
  848 +
  849 + &-input {
  850 + font-size: 30rpx;
  851 + }
  852 + }
  853 +}
  854 +</style>
  1 +<template>
  2 + <view class="page">
  3 + <scroll-view class="scroll" scroll-y>
  4 + <uni-list>
  5 + <!-- 编号 -->
  6 + <uni-list-item class="select-item" :class="form.code ? 'is-filled' : 'is-empty'">
  7 + <template v-slot:body>
  8 + <view class="item-title"><text class="required">*</text><text>编号</text></view>
  9 + </template>
  10 + <template v-slot:footer>
  11 + <view class="serial-number-row">
  12 + <text class="readonly-text value-code">{{ form.code }}</text>
  13 + <button class="generate-btn" @click="initCode">点此生成</button>
  14 + </view>
  15 + </template>
  16 + </uni-list-item>
  17 +
  18 + <!-- 供应商名称 -->
  19 + <uni-list-item class="select-item" :class="form.supplierId ? 'is-filled' : 'is-empty'" clickable
  20 + @click="openSupplierSheet" :rightText="form.supplierName || '请选择供应商名称'" showArrow>
  21 + <template v-slot:body>
  22 + <view class="item-title"><text class="required">*</text><text>供应商名称</text></view>
  23 + </template>
  24 + </uni-list-item>
  25 +
  26 + <!-- 采购处(带出,禁用) -->
  27 + <uni-list-item title="采购处">
  28 + <template v-slot:footer>
  29 + <uni-easyinput v-model="form.purchaseDepartmentName" :inputBorder="false" disabled placeholder="选择供应商后自动带出" />
  30 + </template>
  31 + </uni-list-item>
  32 +
  33 + <!-- 区域(带出,禁用) -->
  34 + <uni-list-item title="区域">
  35 + <template v-slot:footer>
  36 + <uni-easyinput v-model="form.regionName" :inputBorder="false" disabled placeholder="选择供应商后自动带出" />
  37 + </template>
  38 + </uni-list-item>
  39 +
  40 + <!-- 报价时效 -->
  41 + <uni-list-item>
  42 + <template v-slot:body>
  43 + <view class="item-title"><text class="required">*</text><text>报价时效</text></view>
  44 + </template>
  45 + <template v-slot:footer>
  46 + <uni-easyinput v-model="form.validityPeriod" :inputBorder="false" placeholder="请输入报价时效" />
  47 + </template>
  48 + </uni-list-item>
  49 +
  50 + <!-- 报价人(禁用) -->
  51 + <uni-list-item title="报价人">
  52 + <template v-slot:footer>
  53 + <uni-easyinput v-model="form.quoterName" :inputBorder="false" disabled placeholder="自动获取" />
  54 + </template>
  55 + </uni-list-item>
  56 +
  57 + <!-- 是否长单操作 -->
  58 + <uni-list-item class="select-item" :class="typeof form.longTermOperation === 'boolean' ? 'is-filled' : 'is-empty'" clickable
  59 + @click="openSheet('longTermOperation')" :rightText="boolText(form.longTermOperation)" showArrow>
  60 + <template v-slot:body>
  61 + <view class="item-title"><text class="required">*</text><text>是否长单操作</text></view>
  62 + </template>
  63 + </uni-list-item>
  64 +
  65 + <!-- 是否终端客户 -->
  66 + <uni-list-item class="select-item" :class="typeof form.terminalCustomer === 'boolean' ? 'is-filled' : 'is-empty'" clickable
  67 + @click="openSheet('terminalCustomer')" :rightText="boolText(form.terminalCustomer)" showArrow>
  68 + <template v-slot:body>
  69 + <view class="item-title"><text class="required">*</text><text>是否终端客户</text></view>
  70 + </template>
  71 + </uni-list-item>
  72 +
  73 + <!-- 是否未点价 -->
  74 + <uni-list-item class="select-item" :class="typeof form.priced === 'boolean' ? 'is-filled' : 'is-empty'" clickable
  75 + @click="openSheet('priced')" :rightText="boolText(form.priced)" showArrow>
  76 + <template v-slot:body>
  77 + <view class="item-title"><text class="required">*</text><text>是否未点价</text></view>
  78 + </template>
  79 + </uni-list-item>
  80 +
  81 + <!-- 点价截止日期 - 仅在 是否未点价 = 否 时显示且必填 -->
  82 + <uni-list-item v-if="form.priced === false" class="select-item" :class="form.pricingDeadline ? 'is-filled' : 'is-empty'">
  83 + <template v-slot:body>
  84 + <view class="item-title"><text class="required">*</text><text>点价截止日期</text></view>
  85 + </template>
  86 + <template v-slot:footer>
  87 + <uni-datetime-picker type="date" v-model="form.pricingDeadline" />
  88 + </template>
  89 + </uni-list-item>
  90 +
  91 + <!-- 其他费用凭票到厂另行结算 -->
  92 + <uni-list-item class="select-item" :class="typeof form.otherExpenseSettlement === 'boolean' ? 'is-filled' : 'is-empty'" clickable
  93 + @click="openSheet('otherExpenseSettlement')" :rightText="boolText(form.otherExpenseSettlement)" showArrow>
  94 + <template v-slot:body>
  95 + <view class="item-title"><text class="required">*</text><text>其他费用凭票到厂另行结算</text></view>
  96 + </template>
  97 + </uni-list-item>
  98 +
  99 + <!-- 交提货方式 -->
  100 + <uni-list-item class="select-item" :class="form.deliveryMode ? 'is-filled' : 'is-empty'" clickable
  101 + @click="openDictSheet('deliveryMode', 'DELIVERY_METHOD', '交提货方式')" :rightText="getDeliveryMethodLabel() || '请选择交提货方式'" showArrow>
  102 + <template v-slot:body>
  103 + <view class="item-title"><text class="required">*</text><text>交提货方式</text></view>
  104 + </template>
  105 + </uni-list-item>
  106 +
  107 + <!-- 是否通货买断 -->
  108 + <uni-list-item class="select-item" :class="typeof form.commodityBuyout === 'boolean' ? 'is-filled' : 'is-empty'" clickable
  109 + @click="openSheet('commodityBuyout')" :rightText="boolText(form.commodityBuyout)" showArrow>
  110 + <template v-slot:body>
  111 + <view class="item-title"><text class="required">*</text><text>是否通货买断</text></view>
  112 + </template>
  113 + </uni-list-item>
  114 +
  115 + <!-- 是否质量买断 -->
  116 + <uni-list-item class="select-item" :class="typeof form.qualityBuyout === 'boolean' ? 'is-filled' : 'is-empty'" clickable
  117 + @click="openSheet('qualityBuyout')" :rightText="boolText(form.qualityBuyout)" showArrow>
  118 + <template v-slot:body>
  119 + <view class="item-title"><text class="required">*</text><text>是否质量买断</text></view>
  120 + </template>
  121 + </uni-list-item>
  122 +
  123 + <!-- 是否数量买断 -->
  124 + <uni-list-item class="select-item" :class="typeof form.quantityBuyout === 'boolean' ? 'is-filled' : 'is-empty'" clickable
  125 + @click="openSheet('quantityBuyout')" :rightText="boolText(form.quantityBuyout)" showArrow>
  126 + <template v-slot:body>
  127 + <view class="item-title"><text class="required">*</text><text>是否数量买断</text></view>
  128 + </template>
  129 + </uni-list-item>
  130 +
  131 + <!-- 是否送货上门 -->
  132 + <uni-list-item class="select-item" :class="typeof form.doorDelivery === 'boolean' ? 'is-filled' : 'is-empty'" clickable
  133 + @click="openSheet('doorDelivery')" :rightText="boolText(form.doorDelivery)" showArrow>
  134 + <template v-slot:body>
  135 + <view class="item-title"><text class="required">*</text><text>是否送货上门</text></view>
  136 + </template>
  137 + </uni-list-item>
  138 +
  139 + <!-- 装车费 -->
  140 + <uni-list-item title="装车费">
  141 + <template v-slot:footer>
  142 + <uni-easyinput v-model="form.loadingFee" type="digit" :inputBorder="false" placeholder="请输入装车费" />
  143 + </template>
  144 + </uni-list-item>
  145 +
  146 + <!-- 开票品种 -->
  147 + <uni-list-item>
  148 + <template v-slot:body>
  149 + <view class="item-title"><text class="required">*</text><text>开票品种</text></view>
  150 + </template>
  151 + <template v-slot:footer>
  152 + <uni-easyinput v-model="form.invoiceProductName" :inputBorder="false" placeholder="请输入开票品种" />
  153 + </template>
  154 + </uni-list-item>
  155 +
  156 + <!-- 交货日期 -->
  157 + <uni-list-item>
  158 + <template v-slot:body>
  159 + <view class="item-title"><text class="required">*</text><text>交货日期</text></view>
  160 + </template>
  161 + <template v-slot:footer>
  162 + <uni-datetime-picker type="date" v-model="form.deliveryDate" />
  163 + </template>
  164 + </uni-list-item>
  165 +
  166 + <!-- 结算方式 -->
  167 + <uni-list-item>
  168 + <template v-slot:body>
  169 + <view class="item-title"><text class="required">*</text><text>结算方式</text></view>
  170 + </template>
  171 + <template v-slot:footer>
  172 + <uni-easyinput v-model="form.settlementMethod" :inputBorder="false" placeholder="请输入结算方式" />
  173 + </template>
  174 + </uni-list-item>
  175 +
  176 + <!-- 结算比例(%) -->
  177 + <uni-list-item>
  178 + <template v-slot:body>
  179 + <view class="item-title"><text class="required">*</text><text>结算比例(%)</text></view>
  180 + </template>
  181 + <template v-slot:footer>
  182 + <uni-easyinput v-model="form.settlementRatio" type="digit" :inputBorder="false" placeholder="请输入结算比例" @blur="onRatioBlur" />
  183 + </template>
  184 + </uni-list-item>
  185 +
  186 + <!-- 协调事由 -->
  187 + <uni-list-item title="协调事由">
  188 + <template v-slot:footer>
  189 + <uni-easyinput v-model="form.coordinationReason" :inputBorder="false" placeholder="请输入协调事由" disabled />
  190 + </template>
  191 + </uni-list-item>
  192 +
  193 + <!-- 票类状况 -->
  194 + <uni-list-item title="票类状况">
  195 + <template v-slot:footer>
  196 + <uni-easyinput v-model="form.invoiceTypeStatus" :inputBorder="false" placeholder="请输入票类状况" disabled />
  197 + </template>
  198 + </uni-list-item>
  199 +
  200 + <!-- 明细信息 -->
  201 + <ProductRel ref="productRel" mode="add" :options="productList" :priced="form.priced" :list="productLineList" @change="onProductsChange" />
  202 + </uni-list>
  203 +
  204 + <!-- 料质状况模块(明细下方) -->
  205 + <view class="material-condition-section">
  206 + <view class="section-header">
  207 + <image class="opCollapse" src="/static/images/title.png" />
  208 + <text class="section-title">料质状况</text>
  209 + </view>
  210 + <view class="section-content">
  211 + <view class="form-row">
  212 + <text class="form-label">是否启用料质状况</text>
  213 + <view class="bool-btns">
  214 + <view class="bool-btn" :class="{ active: form.materialConditionEnabled === true }" @click="form.materialConditionEnabled = true">是</view>
  215 + <view class="bool-btn" :class="{ active: form.materialConditionEnabled === false }" @click="form.materialConditionEnabled = false">否</view>
  216 + </view>
  217 + </view>
  218 +
  219 + <template v-if="form.materialConditionEnabled">
  220 + <view v-for="mc in materialConditionConfigs" :key="mc.field" class="material-group">
  221 + <text class="material-label">{{ mc.label }}</text>
  222 + <view class="tag-list">
  223 + <view
  224 + v-for="opt in getDictOptions(mc.dictCode)"
  225 + :key="opt.code"
  226 + class="tag-btn"
  227 + :class="{ active: isTagSelected(mc.field, opt.code) }"
  228 + @click="toggleTag(mc.field, opt.code)"
  229 + >
  230 + {{ opt.name }}
  231 + </view>
  232 + </view>
  233 + </view>
  234 + </template>
  235 + </view>
  236 + </view>
  237 +
  238 + <!-- 备注模块 -->
  239 + <view class="remark-section">
  240 + <view class="section-header">
  241 + <image class="opCollapse" src="/static/images/title.png" />
  242 + <text class="section-title">备注</text>
  243 + </view>
  244 + <view class="section-content">
  245 + <uni-easyinput v-model="form.remark" type="textarea" :inputBorder="false" placeholder="请输入备注" maxlength="2000" />
  246 + </view>
  247 + </view>
  248 +
  249 + <view class="footer">
  250 + <button class="btn submit" type="primary" @click="onSubmit">提交</button>
  251 + </view>
  252 + </scroll-view>
  253 +
  254 + <SingleSelectSheet :visible.sync="sheet.visible" :title="sheet.title" :options="sheet.options"
  255 + v-model="sheet.value" @confirm="onSheetConfirm" />
  256 +
  257 + <RelateSelectSheet :visible.sync="supplierSheet.visible" :title="supplierSheet.title"
  258 + :fetchFn="supplierSheet.fetchFn" :display-fields="supplierSheet.displayFields"
  259 + :multiple="false" row-key="id" :selectedKeys.sync="supplierSheet.selectedKeys"
  260 + @confirm="onSupplierConfirm" />
  261 + </view>
  262 +</template>
  263 +<script>
  264 +import SingleSelectSheet from '@/components/single-select/index.vue'
  265 +import RelateSelectSheet from '@/components/relate-select/index.vue'
  266 +import ProductRel from './productRel.vue'
  267 +import { salesmanQuotationDetailApi, salesmanQuotationGenerateCodeApi, salesmanQuotationSaveApi, breedRelationshipQueryApi } from '@/api/procure-manage/salesmanQuotation.js'
  268 +import { domesticCustomerCreditQueryApi } from '@/api/procure-manage/domesticCustomerCredit.js'
  269 +import { getDicByCodes } from '@/utils/dic.js'
  270 +
  271 +export default {
  272 + name: 'CopySalesPrice',
  273 + components: { SingleSelectSheet, RelateSelectSheet, ProductRel },
  274 + data() {
  275 + return {
  276 + sourceId: '',
  277 + form: {
  278 + code: '',
  279 + supplierId: '',
  280 + supplierName: '',
  281 + purchaseDepartmentName: '',
  282 + regionName: '',
  283 + validityPeriod: '',
  284 + quoterName: '',
  285 + longTermOperation: false,
  286 + terminalCustomer: false,
  287 + priced: true,
  288 + pricingDeadline: '',
  289 + otherExpenseSettlement: false,
  290 + deliveryMode: '',
  291 + commodityBuyout: false,
  292 + qualityBuyout: false,
  293 + quantityBuyout: false,
  294 + doorDelivery: false,
  295 + loadingFee: '',
  296 + invoiceProductName: '',
  297 + deliveryDate: '',
  298 + settlementMethod: '',
  299 + settlementRatio: '',
  300 + coordinationReason: '',
  301 + invoiceTypeStatus: '',
  302 + materialConditionEnabled: false,
  303 + materialConditionCopperRice: [],
  304 + materialConditionBrightCopper: [],
  305 + materialConditionFirstClass: [],
  306 + materialConditionSecondClass: [],
  307 + materialConditionTinCoated: [],
  308 + materialConditionScrap: [],
  309 + materialConditionWhiteYellow: [],
  310 + materialConditionPhosphorCopper: [],
  311 + remark: ''
  312 + },
  313 + productList: [],
  314 + productLineList: [],
  315 + sheet: { visible: false, title: '请选择', options: [], value: '', field: '' },
  316 + supplierSheet: {
  317 + visible: false,
  318 + title: '选择供应商',
  319 + fetchFn: null,
  320 + displayFields: [
  321 + { label: '单位名称', field: 'unitName' },
  322 + { label: '采购处', field: 'purchaseDepartmentName' },
  323 + { label: '区域', field: 'regionName' }
  324 + ],
  325 + selectedKeys: []
  326 + },
  327 + materialConditionConfigs: [
  328 + { field: 'materialConditionCopperRice', label: '铜米', dictCode: 'COPPER_RICE' },
  329 + { field: 'materialConditionBrightCopper', label: '光亮铜', dictCode: 'BRIGHT_COPPER' },
  330 + { field: 'materialConditionFirstClass', label: '一特', dictCode: 'FIRST_CLASS' },
  331 + { field: 'materialConditionSecondClass', label: '二特', dictCode: 'SECOND_CLASS' },
  332 + { field: 'materialConditionTinCoated', label: '镀锡紫', dictCode: 'TIN_COATED' },
  333 + { field: 'materialConditionScrap', label: '角料', dictCode: 'SCRAP' },
  334 + { field: 'materialConditionWhiteYellow', label: '镀白黄', dictCode: 'WHITE_YELLOW' },
  335 + { field: 'materialConditionPhosphorCopper', label: '磷铜', dictCode: 'PHOSPHOR_COPPER' }
  336 + ],
  337 + dictData: {}
  338 + }
  339 + },
  340 + onLoad(query) {
  341 + this.sourceId = (query && query.id) || ''
  342 + this.supplierSheet.fetchFn = this.fetchSuppliers.bind(this)
  343 + this.loadProducts()
  344 + this.loadDictData()
  345 + this.loadSourceDetail()
  346 + this.initCode()
  347 + },
  348 + methods: {
  349 + async loadDictData() {
  350 + try {
  351 + const codes = [...this.materialConditionConfigs.map(mc => mc.dictCode), 'DELIVERY_METHOD']
  352 + const res = await getDicByCodes(codes)
  353 + this.dictData = res || {}
  354 + } catch (e) {
  355 + this.dictData = {}
  356 + }
  357 + },
  358 + getDictOptions(dictCode) {
  359 + const data = this.dictData[dictCode]
  360 + return (data && data.data) ? data.data : []
  361 + },
  362 + isTagSelected(field, code) {
  363 + const selected = this.form[field]
  364 + return Array.isArray(selected) && selected.includes(code)
  365 + },
  366 + toggleTag(field, code) {
  367 + let selected = this.form[field]
  368 + if (!Array.isArray(selected)) {
  369 + selected = []
  370 + }
  371 + const index = selected.indexOf(code)
  372 + if (index > -1) {
  373 + selected.splice(index, 1)
  374 + } else {
  375 + selected.push(code)
  376 + }
  377 + this.$set(this.form, field, [...selected])
  378 + },
  379 + async initCode() {
  380 + try {
  381 + const res = await salesmanQuotationGenerateCodeApi()
  382 + this.form.code = (res && res.data) ? res.data : ''
  383 + } catch (e) {
  384 + this.form.code = ''
  385 + }
  386 + },
  387 + async loadProducts() {
  388 + try {
  389 + const res = await breedRelationshipQueryApi({ pageIndex: 1, pageSize: 9999 })
  390 + const _data = res && res.data ? res.data : {}
  391 + const list = _data.datas || _data.list || _data.records || []
  392 + this.productList = list.map(it => ({
  393 + label: it.productName || it.name || '',
  394 + value: it.productId || it.id || '',
  395 + lossRate: it.fixedLossRate != null ? it.fixedLossRate : (it.lossRate != null ? it.lossRate : (it.attritionRate != null ? it.attritionRate : ''))
  396 + }))
  397 + } catch (e) {
  398 + this.productList = []
  399 + }
  400 + },
  401 + async loadSourceDetail() {
  402 + if (!this.sourceId) return
  403 + try {
  404 + const res = await salesmanQuotationDetailApi({ id: this.sourceId })
  405 + const d = (res && res.data) ? res.data : {}
  406 + // 清空 id 和 code,复制为新增
  407 + delete d.id
  408 + delete d.code
  409 + // 清空明细行的 id 和 itemId
  410 + const lineList = Array.isArray(d.quotationLineList) ? d.quotationLineList : (Array.isArray(d.salesmanQuotationLineList) ? d.salesmanQuotationLineList : [])
  411 + lineList.forEach(it => {
  412 + delete it.id
  413 + delete it.itemId
  414 + })
  415 + const materialFields = this.materialConditionConfigs.map(mc => mc.field)
  416 + materialFields.forEach(f => {
  417 + const v = d[f]
  418 + d[f] = (typeof v === 'string' && v) ? v.split(',').filter(Boolean) : (Array.isArray(v) ? v : [])
  419 + })
  420 + this.form = { ...this.form, ...d }
  421 + this.productLineList = lineList
  422 + } catch (e) {
  423 + uni.showToast({ title: '加载源数据失败', icon: 'none' })
  424 + }
  425 + },
  426 + fetchSuppliers({ pageIndex, pageSize, extra }) {
  427 + const name = (extra && (extra.name || extra.keyword || extra.key)) || ''
  428 + const params = {
  429 + pageIndex,
  430 + pageSize,
  431 + available: true,
  432 + unitName: name,
  433 + status: 'PASS',
  434 + freeze: false
  435 + }
  436 + return domesticCustomerCreditQueryApi(params).then(res => {
  437 + const _data = res && res.data ? res.data : {}
  438 + const records = _data.datas || _data.list || _data.records || []
  439 + const totalCount = _data.totalCount || _data.count || 0
  440 + const hasNext = _data.hasNext || false
  441 + return { records, totalCount, hasNext }
  442 + }).catch(() => ({ records: [], totalCount: 0, hasNext: false }))
  443 + },
  444 + openSupplierSheet() {
  445 + this.supplierSheet.selectedKeys = this.form.supplierId ? [this.form.supplierId] : []
  446 + this.supplierSheet.visible = true
  447 + },
  448 + onSupplierConfirm({ items }) {
  449 + const first = (items && items.length > 0) ? items[0] : null
  450 + if (!first) return
  451 + this.form.supplierId = first.id || ''
  452 + this.form.supplierName = first.unitName || ''
  453 + this.form.purchaseDepartmentName = first.purchaseDepartmentName || ''
  454 + this.form.regionName = first.regionName || ''
  455 + },
  456 + boolText(v) {
  457 + if (typeof v !== 'boolean') return '请选择'
  458 + return v ? '是' : '否'
  459 + },
  460 + openSheet(field) {
  461 + const opts = [{ label: '是', value: true }, { label: '否', value: false }]
  462 + const cur = this.form[field]
  463 + const match = opts.find(o => o.value === cur)
  464 + const titleMap = {
  465 + longTermOperation: '是否长单操作',
  466 + terminalCustomer: '是否终端客户',
  467 + priced: '是否未点价',
  468 + otherExpenseSettlement: '其他费用凭票到厂另行结算',
  469 + commodityBuyout: '是否通货买断',
  470 + qualityBuyout: '是否质量买断',
  471 + quantityBuyout: '是否数量买断',
  472 + doorDelivery: '是否送货上门'
  473 + }
  474 + this.sheet = { visible: true, title: titleMap[field] || '请选择', options: opts, value: match ? match.value : '', field, isDict: false }
  475 + },
  476 + openDictSheet(field, dictCode, title) {
  477 + const raw = this.getDictOptions(dictCode) || []
  478 + const opts = raw.map(o => ({ label: o.name || o.label || '', value: o.code != null ? o.code : (o.value != null ? o.value : '') }))
  479 + const cur = this.form[field]
  480 + const match = opts.find(o => String(o.value) === String(cur))
  481 + this.sheet = { visible: true, title: title || '请选择', options: opts, value: match ? match.value : '', field, isDict: true }
  482 + },
  483 + getDeliveryMethodLabel() {
  484 + const opts = this.getDictOptions('DELIVERY_METHOD') || []
  485 + const m = opts.find(o => String(o.code != null ? o.code : o.value) === String(this.form.deliveryMode))
  486 + return m ? (m.name || m.label || '') : ''
  487 + },
  488 + onSheetConfirm({ value }) {
  489 + const field = this.sheet.field
  490 + if (!field) return
  491 + if (this.sheet.isDict) {
  492 + this.form[field] = value
  493 + } else {
  494 + this.form[field] = !!value
  495 + if (field === 'priced' && value === true) {
  496 + this.form.pricingDeadline = ''
  497 + }
  498 + }
  499 + },
  500 + onRatioBlur() {
  501 + let v = parseFloat(this.form.settlementRatio)
  502 + if (isNaN(v)) v = ''
  503 + else v = Math.round(v * 100) / 100
  504 + this.form.settlementRatio = v
  505 + },
  506 + onProductsChange(products) {
  507 + this.productLineList = Array.isArray(products) ? products : []
  508 + },
  509 + validateRequired() {
  510 + const stringChecks = [
  511 + { key: 'code', msg: '请输入编号' },
  512 + { key: 'supplierId', msg: '请选择供应商' },
  513 + { key: 'validityPeriod', msg: '请输入报价时效' },
  514 + { key: 'deliveryMode', msg: '请选择交提货方式' },
  515 + { key: 'invoiceProductName', msg: '请输入开票品种' },
  516 + { key: 'deliveryDate', msg: '请选择交货日期' },
  517 + { key: 'settlementMethod', msg: '请输入结算方式' },
  518 + { key: 'settlementRatio', msg: '请输入结算比例' }
  519 + ]
  520 + for (const it of stringChecks) {
  521 + const val = this.form[it.key]
  522 + const empty = (val === undefined || val === null || (typeof val === 'string' && val.trim() === ''))
  523 + if (empty) { uni.showToast({ title: it.msg, icon: 'none' }); return false }
  524 + }
  525 + const boolChecks = [
  526 + { key: 'longTermOperation', msg: '请选择是否长单操作' },
  527 + { key: 'terminalCustomer', msg: '请选择是否终端客户' },
  528 + { key: 'priced', msg: '请选择是否未点价' },
  529 + { key: 'otherExpenseSettlement', msg: '请选择其他费用凭票到厂另行结算' },
  530 + { key: 'commodityBuyout', msg: '请选择是否通货买断' },
  531 + { key: 'qualityBuyout', msg: '请选择是否质量买断' },
  532 + { key: 'quantityBuyout', msg: '请选择是否数量买断' },
  533 + { key: 'doorDelivery', msg: '请选择是否送货上门' }
  534 + ]
  535 + for (const it of boolChecks) {
  536 + if (typeof this.form[it.key] !== 'boolean') {
  537 + uni.showToast({ title: it.msg, icon: 'none' })
  538 + return false
  539 + }
  540 + }
  541 + if (this.form.priced === false && !this.form.pricingDeadline) {
  542 + uni.showToast({ title: '请选择点价截止日期', icon: 'none' })
  543 + return false
  544 + }
  545 + const list = Array.isArray(this.productLineList) ? this.productLineList : []
  546 + if (list.length === 0) {
  547 + uni.showToast({ title: '请添加产品明细', icon: 'none' })
  548 + return false
  549 + }
  550 + if (this.$refs.productRel && !this.$refs.productRel.validate(this.form.priced)) return false
  551 + return true
  552 + },
  553 + async onSubmit() {
  554 + if (!this.validateRequired()) return
  555 + const confirmRes = await new Promise(resolve => {
  556 + uni.showModal({ title: '提示', content: '确定复制提交吗?', confirmText: '确定', cancelText: '取消', success: resolve })
  557 + })
  558 + if (!(confirmRes && confirmRes.confirm)) return
  559 + const clean = (obj) => {
  560 + const out = {}
  561 + Object.keys(obj || {}).forEach(k => {
  562 + const v = obj[k]
  563 + const isEmptyString = typeof v === 'string' && v.trim() === ''
  564 + const isUndef = v === undefined || v === null
  565 + const isNaNNumber = typeof v === 'number' && isNaN(v)
  566 + if (!(isEmptyString || isUndef || isNaNNumber)) out[k] = v
  567 + })
  568 + return out
  569 + }
  570 + const lines = (this.productLineList || []).map(it => {
  571 + const c = clean(it)
  572 + delete c.id
  573 + delete c.itemId
  574 + return c
  575 + })
  576 + const materialFields = this.materialConditionConfigs.map(mc => mc.field)
  577 + const formCopy = { ...this.form }
  578 + delete formCopy.id
  579 + materialFields.forEach(f => {
  580 + const arr = formCopy[f]
  581 + formCopy[f] = Array.isArray(arr) ? arr.join(',') : (arr || '')
  582 + })
  583 + const payload = clean({
  584 + ...formCopy,
  585 + quotationLineList: lines
  586 + })
  587 + try {
  588 + await salesmanQuotationSaveApi(payload)
  589 + uni.showToast({ title: '复制成功', icon: 'none' })
  590 + try { uni.setStorageSync('SALES_PRICE_LIST_NEED_REFRESH', '1') } catch (e) {}
  591 + setTimeout(() => { uni.redirectTo({ url: '/pages/sales-price/index' }) }, 400)
  592 + } catch (e) {
  593 + uni.showToast({ title: e.msg || '复制失败', icon: 'none' })
  594 + }
  595 + }
  596 + }
  597 +}
  598 +</script>
  599 +<style lang="scss" scoped>
  600 +.page {
  601 + display: flex;
  602 + flex-direction: column;
  603 + height: 100%;
  604 +}
  605 +
  606 +.scroll {
  607 + flex: 1;
  608 + padding: 12rpx 0 392rpx !important;
  609 +}
  610 +
  611 +.footer {
  612 + z-index: 2;
  613 + position: fixed;
  614 + left: 0;
  615 + right: 0;
  616 + bottom: 0;
  617 + padding: 32rpx;
  618 + padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
  619 + background: #fff;
  620 + box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
  621 +
  622 + .btn {
  623 + height: 80rpx;
  624 + line-height: 80rpx;
  625 + border-radius: 12rpx;
  626 + font-size: 32rpx;
  627 + }
  628 +
  629 + .submit {
  630 + background: $theme-primary;
  631 + color: #fff;
  632 + }
  633 +}
  634 +
  635 +::v-deep .uni-list {
  636 + .uni-easyinput {
  637 + display: flex;
  638 +
  639 + .uni-input-input {
  640 + color: rgba(0, 0, 0, 0.9);
  641 + }
  642 + }
  643 +
  644 + .uni-input-placeholder {
  645 + z-index: 1;
  646 + }
  647 +
  648 + .uni-input-input {
  649 + background-color: #ffffff;
  650 + }
  651 +
  652 + background: transparent;
  653 +
  654 + &-item {
  655 + &__extra-text {
  656 + font-size: 32rpx;
  657 + }
  658 +
  659 + &__content-title {
  660 + font-size: 32rpx;
  661 + color: rgba(0, 0, 0, 0.9);
  662 + }
  663 +
  664 + &__container {
  665 + padding: 32rpx;
  666 +
  667 + .uni-easyinput {
  668 + &__placeholder-class {
  669 + font-size: 32rpx;
  670 + color: rgba(0, 0, 0, 0.4);
  671 + }
  672 +
  673 + &__content {
  674 + border: none;
  675 + background-color: #ffffff !important;
  676 +
  677 + &-input {
  678 + padding-left: 0 !important;
  679 + height: 48rpx;
  680 + line-height: 48rpx;
  681 + font-size: 32rpx;
  682 + }
  683 +
  684 + .content-clear-icon {
  685 + font-size: 44rpx !important;
  686 + }
  687 + }
  688 + }
  689 +
  690 + .item-title,
  691 + .uni-list-item__content {
  692 + flex: none;
  693 + min-height: 48rpx;
  694 + line-height: 48rpx;
  695 + font-size: 32rpx;
  696 + position: relative;
  697 + width: 240rpx;
  698 + margin-right: 32rpx;
  699 + color: rgba(0, 0, 0, 0.9);
  700 +
  701 + .required {
  702 + color: red;
  703 + position: absolute;
  704 + top: 50%;
  705 + transform: translateY(-50%);
  706 + left: -16rpx;
  707 + }
  708 + }
  709 + }
  710 +
  711 + &.select-item {
  712 + &.is-empty {
  713 + .uni-list-item__extra-text {
  714 + color: rgba(0, 0, 0, 0.4) !important;
  715 + }
  716 + }
  717 +
  718 + &.is-filled {
  719 + .uni-list-item__extra-text {
  720 + color: rgba(0, 0, 0, 0.9) !important;
  721 + }
  722 + }
  723 +
  724 + .serial-number-row {
  725 + display: flex;
  726 + align-items: center;
  727 +
  728 + .generate-btn {
  729 + margin-left: 24rpx;
  730 + height: 48rpx;
  731 + line-height: 48rpx;
  732 + padding: 0 24rpx;
  733 + font-size: 24rpx;
  734 + border-radius: 8rpx;
  735 + background: $theme-primary;
  736 + color: #fff;
  737 + }
  738 + }
  739 + }
  740 + }
  741 +}
  742 +
  743 +.readonly-text {
  744 + color: rgba(0, 0, 0, 0.9);
  745 + font-size: 32rpx;
  746 + line-height: 48rpx;
  747 + text-align: right;
  748 + white-space: pre-wrap;
  749 + word-break: break-all;
  750 +}
  751 +
  752 +.value-code {
  753 + width: 260rpx;
  754 + text-align: left;
  755 +}
  756 +
  757 +.material-condition-section,
  758 +.remark-section {
  759 + margin-top: 20rpx;
  760 + background: #fff;
  761 +}
  762 +
  763 +.section-header {
  764 + display: flex;
  765 + align-items: center;
  766 + padding: 24rpx 32rpx;
  767 + border-bottom: 1rpx solid #f0f0f0;
  768 +}
  769 +
  770 +.section-title {
  771 + font-size: 32rpx;
  772 + color: rgba(0, 0, 0, 0.9);
  773 + font-weight: 600;
  774 +}
  775 +
  776 +.opCollapse {
  777 + width: 32rpx;
  778 + height: 28rpx;
  779 + margin-right: 16rpx;
  780 + margin-top: 8rpx;
  781 +}
  782 +
  783 +.section-content {
  784 + padding: 32rpx;
  785 +}
  786 +
  787 +.form-row {
  788 + display: flex;
  789 + align-items: center;
  790 + justify-content: space-between;
  791 + margin-bottom: 24rpx;
  792 +}
  793 +
  794 +.form-row:last-child {
  795 + margin-bottom: 0;
  796 +}
  797 +
  798 +.form-label {
  799 + font-size: 32rpx;
  800 + color: rgba(0, 0, 0, 0.9);
  801 +}
  802 +
  803 +.bool-btns {
  804 + display: flex;
  805 + gap: 20rpx;
  806 +}
  807 +
  808 +.bool-btn {
  809 + padding: 12rpx 40rpx;
  810 + border: 1rpx solid #ddd;
  811 + border-radius: 8rpx;
  812 + font-size: 28rpx;
  813 + color: #666;
  814 + background: #fff;
  815 +}
  816 +
  817 +.bool-btn.active {
  818 + background: $theme-primary;
  819 + border-color: $theme-primary;
  820 + color: #fff;
  821 +}
  822 +
  823 +.material-group {
  824 + margin-top: 32rpx;
  825 +}
  826 +
  827 +.material-label {
  828 + display: block;
  829 + font-size: 30rpx;
  830 + color: rgba(0, 0, 0, 0.9);
  831 + margin-bottom: 20rpx;
  832 +}
  833 +
  834 +.tag-list {
  835 + display: flex;
  836 + flex-wrap: wrap;
  837 + gap: 16rpx;
  838 +}
  839 +
  840 +.tag-btn {
  841 + padding: 12rpx 24rpx;
  842 + border: 1rpx solid #ddd;
  843 + border-radius: 8rpx;
  844 + font-size: 28rpx;
  845 + color: #666;
  846 + background: #fff;
  847 +}
  848 +
  849 +.tag-btn.active {
  850 + background: $theme-primary;
  851 + border-color: $theme-primary;
  852 + color: #fff;
  853 +}
  854 +
  855 +::v-deep .uni-easyinput {
  856 + width: 100%;
  857 +
  858 + &__content {
  859 + background: #f8f8f8 !important;
  860 + border-radius: 8rpx;
  861 +
  862 + &-input {
  863 + font-size: 30rpx;
  864 + }
  865 + }
  866 +}
  867 +</style>
  1 +<template>
  2 + <view class="page">
  3 + <scroll-view class="scroll" scroll-y>
  4 + <view class="detail-page">
  5 + <!-- 基础信息 -->
  6 + <view class="section">
  7 + <text class="row company">{{ safeText(form.code) }}</text>
  8 + <view class="row"><text class="label">供应商名称</text><text class="value">{{ safeText(form.supplierName) }}</text></view>
  9 + <view class="row"><text class="label">采购处</text><text class="value">{{ safeText(form.purchaseDepartmentName) }}</text></view>
  10 + <view class="row"><text class="label">区域</text><text class="value">{{ safeText(form.regionName) }}</text></view>
  11 + <view class="row"><text class="label">报价时效</text><text class="value">{{ safeText(form.validityPeriod) }}</text></view>
  12 + <view class="row"><text class="label">报价人</text><text class="value">{{ safeText(form.quoterName) }}</text></view>
  13 + <view class="row"><text class="label">是否长单操作</text><text class="value">{{ boolText(form.longTermOperation) }}</text></view>
  14 + <view class="row"><text class="label">是否终端客户</text><text class="value">{{ boolText(form.terminalCustomer) }}</text></view>
  15 + <view class="row"><text class="label">是否未点价</text><text class="value">{{ boolText(form.priced) }}</text></view>
  16 + <view v-if="form.priced === false" class="row"><text class="label">点价截止日期</text><text class="value">{{ formatDateOnly(form.pricingDeadline) }}</text></view>
  17 + <view class="row"><text class="label">其他费用凭票到厂另行结算</text><text class="value">{{ boolText(form.otherExpenseSettlement) }}</text></view>
  18 + <view class="row"><text class="label">交提货方式</text><text class="value">{{ getDeliveryMethodLabel() }}</text></view>
  19 + <view class="row"><text class="label">是否通货买断</text><text class="value">{{ boolText(form.commodityBuyout) }}</text></view>
  20 + <view class="row"><text class="label">是否质量买断</text><text class="value">{{ boolText(form.qualityBuyout) }}</text></view>
  21 + <view class="row"><text class="label">是否数量买断</text><text class="value">{{ boolText(form.quantityBuyout) }}</text></view>
  22 + <view class="row"><text class="label">是否送货上门</text><text class="value">{{ boolText(form.doorDelivery) }}</text></view>
  23 + <view class="row"><text class="label">装车费</text><text class="value">{{ safeText(form.loadingFee) }}</text></view>
  24 + <view class="row"><text class="label">开票品种</text><text class="value">{{ safeText(form.invoiceProductName) }}</text></view>
  25 + <view class="row"><text class="label">交货日期</text><text class="value">{{ formatDateOnly(form.deliveryDate) }}</text></view>
  26 + <view class="row"><text class="label">结算方式</text><text class="value">{{ safeText(form.settlementMethod) }}</text></view>
  27 + <view class="row"><text class="label">结算比例(%)</text><text class="value">{{ safeText(form.settlementRatio) }}</text></view>
  28 + <view class="row"><text class="label">协调事由</text><text class="value">{{ safeText(form.coordinationReason) }}</text></view>
  29 + <view class="row"><text class="label">票类状况</text><text class="value">{{ safeText(form.invoiceTypeStatus) }}</text></view>
  30 + </view>
  31 +
  32 + <!-- 明细信息 -->
  33 + <ProductRel mode="view" :list="productLineList" />
  34 +
  35 + <!-- 料质状况 -->
  36 + <view class="title-header">
  37 + <image class="title-header_icon" src="/static/images/title.png" />
  38 + <span>料质状况</span>
  39 + </view>
  40 + <view class="section">
  41 + <view class="row"><text class="label">是否启用料质状况</text><text class="value">{{ boolText(form.materialConditionEnabled) }}</text></view>
  42 + <template v-if="form.materialConditionEnabled">
  43 + <view v-for="mc in materialConditionConfigs" :key="mc.field" class="material-group">
  44 + <text class="material-label">{{ mc.label }}</text>
  45 + <view class="tag-list">
  46 + <view
  47 + v-for="opt in getDictOptions(mc.dictCode)"
  48 + :key="opt.code"
  49 + class="tag-btn"
  50 + :class="{ active: isTagSelected(mc.field, opt.code) }"
  51 + >{{ opt.name }}</view>
  52 + </view>
  53 + </view>
  54 + </template>
  55 + </view>
  56 +
  57 + <!-- 备注 -->
  58 + <view class="title-header">
  59 + <image class="title-header_icon" src="/static/images/title.png" />
  60 + <span>备注</span>
  61 + </view>
  62 + <view class="section">
  63 + <text class="remark-text">{{ safeText(form.remark) }}</text>
  64 + </view>
  65 + </view>
  66 + </scroll-view>
  67 + <detail-buttons :buttons="displayButtons" @click="handleButtonClick" />
  68 + </view>
  69 +</template>
  70 +<script>
  71 +import DetailButtons from '@/components/detail-buttons/index.vue'
  72 +import ProductRel from './productRel.vue'
  73 +import { salesmanQuotationDetailApi, salesmanQuotationCancelApi } from '@/api/procure-manage/salesmanQuotation.js'
  74 +import { getDicByCodes } from '@/utils/dic.js'
  75 +import { getInfo } from '@/api/login.js'
  76 +export default {
  77 + name: 'DetailSalesPrice',
  78 + components: { DetailButtons, ProductRel },
  79 + data() {
  80 + return {
  81 + id: '',
  82 + form: {},
  83 + productLineList: [],
  84 + materialConditionConfigs: [
  85 + { field: 'materialConditionCopperRice', label: '铜米', dictCode: 'COPPER_RICE' },
  86 + { field: 'materialConditionBrightCopper', label: '光亮铜', dictCode: 'BRIGHT_COPPER' },
  87 + { field: 'materialConditionFirstClass', label: '一特', dictCode: 'FIRST_CLASS' },
  88 + { field: 'materialConditionSecondClass', label: '二特', dictCode: 'SECOND_CLASS' },
  89 + { field: 'materialConditionTinCoated', label: '镀锡紫', dictCode: 'TIN_COATED' },
  90 + { field: 'materialConditionScrap', label: '角料', dictCode: 'SCRAP' },
  91 + { field: 'materialConditionWhiteYellow', label: '镀白黄', dictCode: 'WHITE_YELLOW' },
  92 + { field: 'materialConditionPhosphorCopper', label: '磷铜', dictCode: 'PHOSPHOR_COPPER' }
  93 + ],
  94 + dictData: {},
  95 + buttons: [
  96 + { text: '编辑', visible: true, variant: 'outline', event: 'edit' },
  97 + { text: '复制', visible: true, variant: 'primary', event: 'copy' },
  98 + { text: '取消', visible: true, variant: 'outline', event: 'cancel' },
  99 + { text: '生成计划', visible: true, variant: 'outline', event: 'plan' },
  100 + { text: '审核', visible: true, variant: 'primary', event: 'approve' },
  101 + { text: '审核详情', visible: true, event: 'approveDetail' },
  102 + ],
  103 + investigatorId: '',
  104 + }
  105 + },
  106 + computed: {
  107 + displayButtons() {
  108 + const s = this.form.approvalStatus || ''
  109 + const c = this.form.createById || ''
  110 + const p = this.form.status || ''
  111 + return [
  112 + { ...this.buttons[0], visible: (s === 'REFUSE' && c === this.investigatorId && this.$auth.hasPermi('procure-manage:sales-price:modify')) },
  113 + { ...this.buttons[1], visible: this.$auth.hasPermi('procure-manage:sales-price:copy') },
  114 + { ...this.buttons[2], visible: (s === 'REFUSE' && c === this.investigatorId && this.$auth.hasPermi('procure-manage:sales-price:cancel')) },
  115 + { ...this.buttons[3], visible: (p === 'DEAL_CLOSED' && this.$auth.hasPermi('procure-manage:sales-price:create')) },
  116 + { ...this.buttons[4], visible: (s === 'AUDIT' && this.form.showExamine && this.$auth.hasPermi('procure-manage:sales-price:approves')) },
  117 + { ...this.buttons[5], visible: (this.form.showExamineDetail && this.$auth.hasPermi('procure-manage:sales-price:view')) },
  118 + ]
  119 + }
  120 + },
  121 + onLoad(query) {
  122 + this.id = (query && query.id) || ''
  123 + this.loadDictData()
  124 + this.loadDetail()
  125 + // 获取当前用户信息
  126 + this.getUserInfo();
  127 + },
  128 + methods: {
  129 + // 获取当前用户信息
  130 + async getUserInfo() {
  131 + getInfo()
  132 + .then((res) => {
  133 + this.investigatorId = res?.id || '';
  134 + })
  135 + .catch((err) => {
  136 + console.error('获取用户信息失败:', err);
  137 + });
  138 + },
  139 + async loadDictData() {
  140 + try {
  141 + const codes = [...this.materialConditionConfigs.map(mc => mc.dictCode), 'DELIVERY_METHOD']
  142 + const res = await getDicByCodes(codes)
  143 + this.dictData = res || {}
  144 + } catch (e) {
  145 + this.dictData = {}
  146 + }
  147 + },
  148 + getDictOptions(dictCode) {
  149 + const data = this.dictData[dictCode]
  150 + return (data && data.data) ? data.data : []
  151 + },
  152 + isTagSelected(field, code) {
  153 + const selected = this.form[field]
  154 + return Array.isArray(selected) && selected.includes(code)
  155 + },
  156 + boolText(v) {
  157 + if (typeof v !== 'boolean') return '-'
  158 + return v ? '是' : '否'
  159 + },
  160 + getDeliveryMethodLabel() {
  161 + const opts = this.getDictOptions('DELIVERY_METHOD') || []
  162 + const m = opts.find(o => String(o.code != null ? o.code : o.value) === String(this.form.deliveryMode))
  163 + return m ? (m.name || m.label || '-') : '-'
  164 + },
  165 + formatDateOnly(v) {
  166 + const s = v == null ? '' : String(v).trim()
  167 + if (!s) return '-'
  168 + if (s.length >= 10) return s.slice(0, 10)
  169 + return s
  170 + },
  171 + safeText(v) {
  172 + const s = v == null ? '' : String(v)
  173 + return s.trim() ? s : '-'
  174 + },
  175 + async loadDetail() {
  176 + if (!this.id) return
  177 + try {
  178 + const res = await salesmanQuotationDetailApi({ id: this.id })
  179 + const d = (res && res.data) ? res.data : {}
  180 + const materialFields = this.materialConditionConfigs.map(mc => mc.field)
  181 + materialFields.forEach(f => {
  182 + const v = d[f]
  183 + d[f] = (typeof v === 'string' && v) ? v.split(',').filter(Boolean) : (Array.isArray(v) ? v : [])
  184 + })
  185 + this.form = d
  186 + this.productLineList = Array.isArray(d.quotationLineList) ? d.quotationLineList : (Array.isArray(d.salesmanQuotationLineList) ? d.salesmanQuotationLineList : [])
  187 + } catch (e) {
  188 + uni.showToast({ title: '加载详情失败', icon: 'none' })
  189 + }
  190 + },
  191 + handleButtonClick(btn) {
  192 + if (!btn || btn.disabled) return
  193 + const e = btn.event || ''
  194 + if (e === 'edit') return this.onEdit()
  195 + if (e === 'copy') return this.onCopy()
  196 + if (e === 'approve') return this.onApprove()
  197 + if (e === 'approveDetail') return this.onApproveDetail()
  198 + if (e === 'cancel') return this.onCancel()
  199 + },
  200 + onEdit() {
  201 + uni.navigateTo({ url: '/pages/sales-price/modify?id=' + this.id })
  202 + },
  203 + onCopy() {
  204 + uni.navigateTo({ url: '/pages/sales-price/copy?id=' + this.id })
  205 + },
  206 + onCancel() {
  207 + const id = this.id || (this.form && this.form.id) || ''
  208 + if (!id) return
  209 + uni.showModal({
  210 + title: '系统提示',
  211 + content: '是否确定取消该流程?',
  212 + confirmText: '确定',
  213 + cancelText: '取消',
  214 + success: (res) => {
  215 + if (res && res.confirm) {
  216 + salesmanQuotationCancelApi({ id }).then(() => {
  217 + uni.showToast({ title: '已取消', icon: 'none' })
  218 + try { uni.setStorageSync('SALES_PRICE_LIST_NEED_REFRESH', '1') } catch (e) {}
  219 + setTimeout(() => { uni.redirectTo({ url: '/pages/supplier-price/index' }) }, 300)
  220 + }).catch((e) => {
  221 + uni.showToast({ title: (e && e.msg) || '取消失败', icon: 'none' })
  222 + })
  223 + }
  224 + }
  225 + })
  226 + },
  227 + onApprove() {
  228 + const id = this.id || (this.form && this.form.id) || ''
  229 + if (!id) return
  230 + const CACHE_KEY = 'sourceBusinessId'
  231 + const TYPE = 'contractType'
  232 + uni.setStorageSync(TYPE, 'SALESMAN_QUOTATION')
  233 + uni.setStorageSync(CACHE_KEY, id)
  234 + uni.navigateTo({ url: '/pages/flow/audit' })
  235 + },
  236 + onApproveDetail() {
  237 + const id = this.id || (this.form && this.form.id) || ''
  238 + if (!id) return
  239 + const CACHE_KEY = 'sourceBusinessId'
  240 + const TYPE = 'contractType'
  241 + uni.setStorageSync(TYPE, 'SALESMAN_QUOTATION')
  242 + uni.setStorageSync(CACHE_KEY, id)
  243 + uni.navigateTo({ url: '/pages/flow/audit_detail' })
  244 + }
  245 + }
  246 +}
  247 +</script>
  248 +<style lang="scss" scoped>
  249 +.page {
  250 + display: flex;
  251 + flex-direction: column;
  252 + height: 100%;
  253 +}
  254 +
  255 +.scroll {
  256 + flex: 1;
  257 + padding: 8rpx 0 144rpx;
  258 +}
  259 +
  260 +.detail-page {
  261 + background: #f3f3f3;
  262 +}
  263 +
  264 +.title-header {
  265 + display: flex;
  266 + align-items: center;
  267 + padding: 24rpx 32rpx 12rpx;
  268 + background-color: #ffffff;
  269 +
  270 + .title-header_icon {
  271 + width: 36rpx;
  272 + height: 36rpx;
  273 + margin-right: 12rpx;
  274 + }
  275 +
  276 + span {
  277 + font-size: 34rpx;
  278 + font-weight: 600;
  279 + color: rgba(0, 0, 0, 0.9);
  280 + }
  281 +}
  282 +
  283 +.section {
  284 + padding: 32rpx;
  285 + background: #fff;
  286 + margin-bottom: 20rpx;
  287 +}
  288 +
  289 +.row {
  290 + display: flex;
  291 + margin-bottom: 28rpx;
  292 +
  293 + &:last-child {
  294 + margin-bottom: 0;
  295 + }
  296 +
  297 + &.company {
  298 + font-size: 36rpx;
  299 + font-weight: 600;
  300 + color: rgba(0, 0, 0, 0.9);
  301 + padding-top: 8rpx;
  302 + line-height: 50rpx;
  303 + }
  304 +
  305 + .label {
  306 + max-width: 420rpx;
  307 + margin-right: 20rpx;
  308 + line-height: 32rpx;
  309 + font-size: 28rpx;
  310 + color: rgba(0, 0, 0, 0.6);
  311 + }
  312 +
  313 + .value {
  314 + flex: 1;
  315 + line-height: 32rpx;
  316 + font-size: 28rpx;
  317 + color: rgba(0, 0, 0, 0.9);
  318 + text-align: right;
  319 + word-break: break-all;
  320 + }
  321 +}
  322 +
  323 +.line-block {
  324 + padding-bottom: 24rpx;
  325 + margin-bottom: 24rpx;
  326 + border-bottom: 1rpx solid #f0f0f0;
  327 +
  328 + &:last-child {
  329 + border-bottom: none;
  330 + margin-bottom: 0;
  331 + padding-bottom: 0;
  332 + }
  333 +}
  334 +
  335 +.material-group {
  336 + margin-top: 32rpx;
  337 +
  338 + &:first-child {
  339 + margin-top: 0;
  340 + }
  341 +}
  342 +
  343 +.material-label {
  344 + display: block;
  345 + font-size: 28rpx;
  346 + color: rgba(0, 0, 0, 0.6);
  347 + margin-bottom: 20rpx;
  348 +}
  349 +
  350 +.tag-list {
  351 + display: flex;
  352 + flex-wrap: wrap;
  353 + gap: 16rpx;
  354 +}
  355 +
  356 +.tag-btn {
  357 + padding: 12rpx 24rpx;
  358 + border: 1rpx solid #ddd;
  359 + border-radius: 8rpx;
  360 + font-size: 28rpx;
  361 + color: #999;
  362 + background: #f5f5f5;
  363 +}
  364 +
  365 +.tag-btn.active {
  366 + background: $theme-primary;
  367 + border-color: $theme-primary;
  368 + color: #fff;
  369 +}
  370 +
  371 +.remark-text {
  372 + font-size: 28rpx;
  373 + color: rgba(0, 0, 0, 0.9);
  374 + line-height: 1.6;
  375 + white-space: pre-wrap;
  376 +}
  377 +
  378 +.empty-tip {
  379 + text-align: center;
  380 + font-size: 28rpx;
  381 + color: rgba(0, 0, 0, 0.4);
  382 + padding: 24rpx 0;
  383 +}
  384 +</style>
@@ -6,6 +6,7 @@ @@ -6,6 +6,7 @@
6 cancelButton="none" bgColor="#F3F3F3" textColor="rgba(0,0,0,0.4)" @confirm="search" 6 cancelButton="none" bgColor="#F3F3F3" textColor="rgba(0,0,0,0.4)" @confirm="search"
7 @input="onSearchInput" /> 7 @input="onSearchInput" />
8 <view class="tool-icons"> 8 <view class="tool-icons">
  9 + <image v-if="$auth.hasPermi('procure-manage:sales-price:add')" class="tool-icon" src="/static/images/dev_manage/add_icon.png" @click="onAdd" />
9 <image class="tool-icon" src="/static/images/dev_manage/filter_icon.png" @click="openFilter" /> 10 <image class="tool-icon" src="/static/images/dev_manage/filter_icon.png" @click="openFilter" />
10 </view> 11 </view>
11 </view> 12 </view>
@@ -115,7 +116,10 @@ export default { @@ -115,7 +116,10 @@ export default {
115 this.searchDebounceTimer = null 116 this.searchDebounceTimer = null
116 } 117 }
117 }, 118 },
118 - methods: { 119 + methods: {
  120 + onAdd() {
  121 + uni.navigateTo({ url: '/pages/sales-price/add' })
  122 + },
119 async loadDicData() { 123 async loadDicData() {
120 try { 124 try {
121 const results = await getDicByCodes(['AUDIT_STATUS']) 125 const results = await getDicByCodes(['AUDIT_STATUS'])
  1 +<template>
  2 + <view class="page">
  3 + <scroll-view class="scroll" scroll-y>
  4 + <uni-list>
  5 + <!-- 编号 -->
  6 + <uni-list-item class="select-item" :class="form.code ? 'is-filled' : 'is-empty'">
  7 + <template v-slot:body>
  8 + <view class="item-title"><text class="required">*</text><text>编号</text></view>
  9 + </template>
  10 + <template v-slot:footer>
  11 + <view class="serial-number-row">
  12 + <text class="readonly-text value-code">{{ form.code }}</text>
  13 + </view>
  14 + </template>
  15 + </uni-list-item>
  16 +
  17 + <!-- 供应商名称 -->
  18 + <uni-list-item class="select-item" :class="form.supplierId ? 'is-filled' : 'is-empty'" clickable
  19 + @click="openSupplierSheet" :rightText="form.supplierName || '请选择供应商名称'" showArrow>
  20 + <template v-slot:body>
  21 + <view class="item-title"><text class="required">*</text><text>供应商名称</text></view>
  22 + </template>
  23 + </uni-list-item>
  24 +
  25 + <!-- 采购处(带出,禁用) -->
  26 + <uni-list-item title="采购处">
  27 + <template v-slot:footer>
  28 + <uni-easyinput v-model="form.purchaseDepartmentName" :inputBorder="false" disabled placeholder="选择供应商后自动带出" />
  29 + </template>
  30 + </uni-list-item>
  31 +
  32 + <!-- 区域(带出,禁用) -->
  33 + <uni-list-item title="区域">
  34 + <template v-slot:footer>
  35 + <uni-easyinput v-model="form.regionName" :inputBorder="false" disabled placeholder="选择供应商后自动带出" />
  36 + </template>
  37 + </uni-list-item>
  38 +
  39 + <!-- 报价时效 -->
  40 + <uni-list-item>
  41 + <template v-slot:body>
  42 + <view class="item-title"><text class="required">*</text><text>报价时效</text></view>
  43 + </template>
  44 + <template v-slot:footer>
  45 + <uni-easyinput v-model="form.validityPeriod" :inputBorder="false" placeholder="请输入报价时效" />
  46 + </template>
  47 + </uni-list-item>
  48 +
  49 + <!-- 报价人(禁用) -->
  50 + <uni-list-item title="报价人">
  51 + <template v-slot:footer>
  52 + <uni-easyinput v-model="form.quoterName" :inputBorder="false" disabled placeholder="自动获取" />
  53 + </template>
  54 + </uni-list-item>
  55 +
  56 + <!-- 是否长单操作 -->
  57 + <uni-list-item class="select-item" :class="typeof form.longTermOperation === 'boolean' ? 'is-filled' : 'is-empty'" clickable
  58 + @click="openSheet('longTermOperation')" :rightText="boolText(form.longTermOperation)" showArrow>
  59 + <template v-slot:body>
  60 + <view class="item-title"><text class="required">*</text><text>是否长单操作</text></view>
  61 + </template>
  62 + </uni-list-item>
  63 +
  64 + <!-- 是否终端客户 -->
  65 + <uni-list-item class="select-item" :class="typeof form.terminalCustomer === 'boolean' ? 'is-filled' : 'is-empty'" clickable
  66 + @click="openSheet('terminalCustomer')" :rightText="boolText(form.terminalCustomer)" showArrow>
  67 + <template v-slot:body>
  68 + <view class="item-title"><text class="required">*</text><text>是否终端客户</text></view>
  69 + </template>
  70 + </uni-list-item>
  71 +
  72 + <!-- 是否未点价 -->
  73 + <uni-list-item class="select-item" :class="typeof form.priced === 'boolean' ? 'is-filled' : 'is-empty'" clickable
  74 + @click="openSheet('priced')" :rightText="boolText(form.priced)" showArrow>
  75 + <template v-slot:body>
  76 + <view class="item-title"><text class="required">*</text><text>是否未点价</text></view>
  77 + </template>
  78 + </uni-list-item>
  79 +
  80 + <!-- 点价截止日期 - 仅在 是否未点价 = 否 时显示且必填 -->
  81 + <uni-list-item v-if="form.priced === false" class="select-item" :class="form.pricingDeadline ? 'is-filled' : 'is-empty'">
  82 + <template v-slot:body>
  83 + <view class="item-title"><text class="required">*</text><text>点价截止日期</text></view>
  84 + </template>
  85 + <template v-slot:footer>
  86 + <uni-datetime-picker type="date" v-model="form.pricingDeadline" />
  87 + </template>
  88 + </uni-list-item>
  89 +
  90 + <!-- 其他费用凭票到厂另行结算 -->
  91 + <uni-list-item class="select-item" :class="typeof form.otherExpenseSettlement === 'boolean' ? 'is-filled' : 'is-empty'" clickable
  92 + @click="openSheet('otherExpenseSettlement')" :rightText="boolText(form.otherExpenseSettlement)" showArrow>
  93 + <template v-slot:body>
  94 + <view class="item-title"><text class="required">*</text><text>其他费用凭票到厂另行结算</text></view>
  95 + </template>
  96 + </uni-list-item>
  97 +
  98 + <!-- 交提货方式 -->
  99 + <uni-list-item class="select-item" :class="form.deliveryMode ? 'is-filled' : 'is-empty'" clickable
  100 + @click="openDictSheet('deliveryMode', 'DELIVERY_METHOD', '交提货方式')" :rightText="getDeliveryMethodLabel() || '请选择交提货方式'" showArrow>
  101 + <template v-slot:body>
  102 + <view class="item-title"><text class="required">*</text><text>交提货方式</text></view>
  103 + </template>
  104 + </uni-list-item>
  105 +
  106 + <!-- 是否通货买断 -->
  107 + <uni-list-item class="select-item" :class="typeof form.commodityBuyout === 'boolean' ? 'is-filled' : 'is-empty'" clickable
  108 + @click="openSheet('commodityBuyout')" :rightText="boolText(form.commodityBuyout)" showArrow>
  109 + <template v-slot:body>
  110 + <view class="item-title"><text class="required">*</text><text>是否通货买断</text></view>
  111 + </template>
  112 + </uni-list-item>
  113 +
  114 + <!-- 是否质量买断 -->
  115 + <uni-list-item class="select-item" :class="typeof form.qualityBuyout === 'boolean' ? 'is-filled' : 'is-empty'" clickable
  116 + @click="openSheet('qualityBuyout')" :rightText="boolText(form.qualityBuyout)" showArrow>
  117 + <template v-slot:body>
  118 + <view class="item-title"><text class="required">*</text><text>是否质量买断</text></view>
  119 + </template>
  120 + </uni-list-item>
  121 +
  122 + <!-- 是否数量买断 -->
  123 + <uni-list-item class="select-item" :class="typeof form.quantityBuyout === 'boolean' ? 'is-filled' : 'is-empty'" clickable
  124 + @click="openSheet('quantityBuyout')" :rightText="boolText(form.quantityBuyout)" showArrow>
  125 + <template v-slot:body>
  126 + <view class="item-title"><text class="required">*</text><text>是否数量买断</text></view>
  127 + </template>
  128 + </uni-list-item>
  129 +
  130 + <!-- 是否送货上门 -->
  131 + <uni-list-item class="select-item" :class="typeof form.doorDelivery === 'boolean' ? 'is-filled' : 'is-empty'" clickable
  132 + @click="openSheet('doorDelivery')" :rightText="boolText(form.doorDelivery)" showArrow>
  133 + <template v-slot:body>
  134 + <view class="item-title"><text class="required">*</text><text>是否送货上门</text></view>
  135 + </template>
  136 + </uni-list-item>
  137 +
  138 + <!-- 装车费 -->
  139 + <uni-list-item title="装车费">
  140 + <template v-slot:footer>
  141 + <uni-easyinput v-model="form.loadingFee" type="digit" :inputBorder="false" placeholder="请输入装车费" />
  142 + </template>
  143 + </uni-list-item>
  144 +
  145 + <!-- 开票品种 -->
  146 + <uni-list-item>
  147 + <template v-slot:body>
  148 + <view class="item-title"><text class="required">*</text><text>开票品种</text></view>
  149 + </template>
  150 + <template v-slot:footer>
  151 + <uni-easyinput v-model="form.invoiceProductName" :inputBorder="false" placeholder="请输入开票品种" />
  152 + </template>
  153 + </uni-list-item>
  154 +
  155 + <!-- 交货日期 -->
  156 + <uni-list-item>
  157 + <template v-slot:body>
  158 + <view class="item-title"><text class="required">*</text><text>交货日期</text></view>
  159 + </template>
  160 + <template v-slot:footer>
  161 + <uni-datetime-picker type="date" v-model="form.deliveryDate" />
  162 + </template>
  163 + </uni-list-item>
  164 +
  165 + <!-- 结算方式 -->
  166 + <uni-list-item>
  167 + <template v-slot:body>
  168 + <view class="item-title"><text class="required">*</text><text>结算方式</text></view>
  169 + </template>
  170 + <template v-slot:footer>
  171 + <uni-easyinput v-model="form.settlementMethod" :inputBorder="false" placeholder="请输入结算方式" />
  172 + </template>
  173 + </uni-list-item>
  174 +
  175 + <!-- 结算比例(%) -->
  176 + <uni-list-item>
  177 + <template v-slot:body>
  178 + <view class="item-title"><text class="required">*</text><text>结算比例(%)</text></view>
  179 + </template>
  180 + <template v-slot:footer>
  181 + <uni-easyinput v-model="form.settlementRatio" type="digit" :inputBorder="false" placeholder="请输入结算比例" @blur="onRatioBlur" />
  182 + </template>
  183 + </uni-list-item>
  184 +
  185 + <!-- 协调事由 -->
  186 + <uni-list-item title="协调事由">
  187 + <template v-slot:footer>
  188 + <uni-easyinput v-model="form.coordinationReason" :inputBorder="false" placeholder="请输入协调事由" disabled />
  189 + </template>
  190 + </uni-list-item>
  191 +
  192 + <!-- 票类状况 -->
  193 + <uni-list-item title="票类状况">
  194 + <template v-slot:footer>
  195 + <uni-easyinput v-model="form.invoiceTypeStatus" :inputBorder="false" placeholder="请输入票类状况" disabled />
  196 + </template>
  197 + </uni-list-item>
  198 +
  199 + <!-- 明细信息 -->
  200 + <ProductRel ref="productRel" mode="edit" :options="productList" :priced="form.priced" :list="productLineList" @change="onProductsChange" />
  201 + </uni-list>
  202 +
  203 + <!-- 料质状况模块(明细下方) -->
  204 + <view class="material-condition-section">
  205 + <view class="section-header">
  206 + <image class="opCollapse" src="/static/images/title.png" />
  207 + <text class="section-title">料质状况</text>
  208 + </view>
  209 + <view class="section-content">
  210 + <view class="form-row">
  211 + <text class="form-label">是否启用料质状况</text>
  212 + <view class="bool-btns">
  213 + <view class="bool-btn" :class="{ active: form.materialConditionEnabled === true }" @click="form.materialConditionEnabled = true">是</view>
  214 + <view class="bool-btn" :class="{ active: form.materialConditionEnabled === false }" @click="form.materialConditionEnabled = false">否</view>
  215 + </view>
  216 + </view>
  217 +
  218 + <template v-if="form.materialConditionEnabled">
  219 + <view v-for="mc in materialConditionConfigs" :key="mc.field" class="material-group">
  220 + <text class="material-label">{{ mc.label }}</text>
  221 + <view class="tag-list">
  222 + <view
  223 + v-for="opt in getDictOptions(mc.dictCode)"
  224 + :key="opt.code"
  225 + class="tag-btn"
  226 + :class="{ active: isTagSelected(mc.field, opt.code) }"
  227 + @click="toggleTag(mc.field, opt.code)"
  228 + >
  229 + {{ opt.name }}
  230 + </view>
  231 + </view>
  232 + </view>
  233 + </template>
  234 + </view>
  235 + </view>
  236 +
  237 + <!-- 备注模块 -->
  238 + <view class="remark-section">
  239 + <view class="section-header">
  240 + <image class="opCollapse" src="/static/images/title.png" />
  241 + <text class="section-title">备注</text>
  242 + </view>
  243 + <view class="section-content">
  244 + <uni-easyinput v-model="form.remark" type="textarea" :inputBorder="false" placeholder="请输入备注" maxlength="2000" />
  245 + </view>
  246 + </view>
  247 +
  248 + <view class="footer">
  249 + <button class="btn submit" type="primary" @click="onSubmit">保存</button>
  250 + </view>
  251 + </scroll-view>
  252 +
  253 + <SingleSelectSheet :visible.sync="sheet.visible" :title="sheet.title" :options="sheet.options"
  254 + v-model="sheet.value" @confirm="onSheetConfirm" />
  255 +
  256 + <RelateSelectSheet :visible.sync="supplierSheet.visible" :title="supplierSheet.title"
  257 + :fetchFn="supplierSheet.fetchFn" :display-fields="supplierSheet.displayFields"
  258 + :multiple="false" row-key="id" :selectedKeys.sync="supplierSheet.selectedKeys"
  259 + @confirm="onSupplierConfirm" />
  260 + </view>
  261 +</template>
  262 +<script>
  263 +import SingleSelectSheet from '@/components/single-select/index.vue'
  264 +import RelateSelectSheet from '@/components/relate-select/index.vue'
  265 +import ProductRel from './productRel.vue'
  266 +import { salesmanQuotationDetailApi, salesmanQuotationUpdateApi, breedRelationshipQueryApi } from '@/api/procure-manage/salesmanQuotation.js'
  267 +import { domesticCustomerCreditQueryApi } from '@/api/procure-manage/domesticCustomerCredit.js'
  268 +import { getDicByCodes } from '@/utils/dic.js'
  269 +
  270 +export default {
  271 + name: 'ModifySalesPrice',
  272 + components: { SingleSelectSheet, RelateSelectSheet, ProductRel },
  273 + data() {
  274 + return {
  275 + id: '',
  276 + form: {
  277 + code: '',
  278 + supplierId: '',
  279 + supplierName: '',
  280 + purchaseDepartmentName: '',
  281 + regionName: '',
  282 + validityPeriod: '',
  283 + quoterName: '',
  284 + longTermOperation: false,
  285 + terminalCustomer: false,
  286 + priced: true,
  287 + pricingDeadline: '',
  288 + otherExpenseSettlement: false,
  289 + deliveryMode: '',
  290 + commodityBuyout: false,
  291 + qualityBuyout: false,
  292 + quantityBuyout: false,
  293 + doorDelivery: false,
  294 + loadingFee: '',
  295 + invoiceProductName: '',
  296 + deliveryDate: '',
  297 + settlementMethod: '',
  298 + settlementRatio: '',
  299 + coordinationReason: '',
  300 + invoiceTypeStatus: '',
  301 + materialConditionEnabled: false,
  302 + materialConditionCopperRice: [],
  303 + materialConditionBrightCopper: [],
  304 + materialConditionFirstClass: [],
  305 + materialConditionSecondClass: [],
  306 + materialConditionTinCoated: [],
  307 + materialConditionScrap: [],
  308 + materialConditionWhiteYellow: [],
  309 + materialConditionPhosphorCopper: [],
  310 + remark: ''
  311 + },
  312 + productList: [],
  313 + productLineList: [],
  314 + sheet: { visible: false, title: '请选择', options: [], value: '', field: '' },
  315 + supplierSheet: {
  316 + visible: false,
  317 + title: '选择供应商',
  318 + fetchFn: null,
  319 + displayFields: [
  320 + { label: '单位名称', field: 'unitName' },
  321 + { label: '采购处', field: 'purchaseDepartmentName' },
  322 + { label: '区域', field: 'regionName' }
  323 + ],
  324 + selectedKeys: []
  325 + },
  326 + materialConditionConfigs: [
  327 + { field: 'materialConditionCopperRice', label: '铜米', dictCode: 'COPPER_RICE' },
  328 + { field: 'materialConditionBrightCopper', label: '光亮铜', dictCode: 'BRIGHT_COPPER' },
  329 + { field: 'materialConditionFirstClass', label: '一特', dictCode: 'FIRST_CLASS' },
  330 + { field: 'materialConditionSecondClass', label: '二特', dictCode: 'SECOND_CLASS' },
  331 + { field: 'materialConditionTinCoated', label: '镀锡紫', dictCode: 'TIN_COATED' },
  332 + { field: 'materialConditionScrap', label: '角料', dictCode: 'SCRAP' },
  333 + { field: 'materialConditionWhiteYellow', label: '镀白黄', dictCode: 'WHITE_YELLOW' },
  334 + { field: 'materialConditionPhosphorCopper', label: '磷铜', dictCode: 'PHOSPHOR_COPPER' }
  335 + ],
  336 + dictData: {}
  337 + }
  338 + },
  339 + onLoad(query) {
  340 + this.id = (query && query.id) || ''
  341 + this.supplierSheet.fetchFn = this.fetchSuppliers.bind(this)
  342 + this.loadProducts()
  343 + this.loadDictData()
  344 + this.loadDetail()
  345 + },
  346 + methods: {
  347 + async loadDictData() {
  348 + try {
  349 + const codes = [...this.materialConditionConfigs.map(mc => mc.dictCode), 'DELIVERY_METHOD']
  350 + const res = await getDicByCodes(codes)
  351 + this.dictData = res || {}
  352 + } catch (e) {
  353 + this.dictData = {}
  354 + }
  355 + },
  356 + getDictOptions(dictCode) {
  357 + const data = this.dictData[dictCode]
  358 + return (data && data.data) ? data.data : []
  359 + },
  360 + isTagSelected(field, code) {
  361 + const selected = this.form[field]
  362 + return Array.isArray(selected) && selected.includes(code)
  363 + },
  364 + toggleTag(field, code) {
  365 + let selected = this.form[field]
  366 + if (!Array.isArray(selected)) {
  367 + selected = []
  368 + }
  369 + const index = selected.indexOf(code)
  370 + if (index > -1) {
  371 + selected.splice(index, 1)
  372 + } else {
  373 + selected.push(code)
  374 + }
  375 + this.$set(this.form, field, [...selected])
  376 + },
  377 + async loadProducts() {
  378 + try {
  379 + const res = await breedRelationshipQueryApi({ pageIndex: 1, pageSize: 9999 })
  380 + const _data = res && res.data ? res.data : {}
  381 + const list = _data.datas || _data.list || _data.records || []
  382 + this.productList = list.map(it => ({
  383 + label: it.productName || it.name || '',
  384 + value: it.productId || it.id || '',
  385 + lossRate: it.fixedLossRate != null ? it.fixedLossRate : (it.lossRate != null ? it.lossRate : (it.attritionRate != null ? it.attritionRate : ''))
  386 + }))
  387 + } catch (e) {
  388 + this.productList = []
  389 + }
  390 + },
  391 + async loadDetail() {
  392 + if (!this.id) return
  393 + try {
  394 + const res = await salesmanQuotationDetailApi({ id: this.id })
  395 + const d = (res && res.data) ? res.data : {}
  396 + const materialFields = this.materialConditionConfigs.map(mc => mc.field)
  397 + materialFields.forEach(f => {
  398 + const v = d[f]
  399 + d[f] = (typeof v === 'string' && v) ? v.split(',').filter(Boolean) : (Array.isArray(v) ? v : [])
  400 + })
  401 + this.form = { ...this.form, ...d }
  402 + this.productLineList = Array.isArray(d.quotationLineList) ? d.quotationLineList : (Array.isArray(d.salesmanQuotationLineList) ? d.salesmanQuotationLineList : [])
  403 + } catch (e) {
  404 + uni.showToast({ title: '加载详情失败', icon: 'none' })
  405 + }
  406 + },
  407 + fetchSuppliers({ pageIndex, pageSize, extra }) {
  408 + const name = (extra && (extra.name || extra.keyword || extra.key)) || ''
  409 + const params = {
  410 + pageIndex,
  411 + pageSize,
  412 + available: true,
  413 + unitName: name,
  414 + status: 'PASS',
  415 + freeze: false
  416 + }
  417 + return domesticCustomerCreditQueryApi(params).then(res => {
  418 + const _data = res && res.data ? res.data : {}
  419 + const records = _data.datas || _data.list || _data.records || []
  420 + const totalCount = _data.totalCount || _data.count || 0
  421 + const hasNext = _data.hasNext || false
  422 + return { records, totalCount, hasNext }
  423 + }).catch(() => ({ records: [], totalCount: 0, hasNext: false }))
  424 + },
  425 + openSupplierSheet() {
  426 + this.supplierSheet.selectedKeys = this.form.supplierId ? [this.form.supplierId] : []
  427 + this.supplierSheet.visible = true
  428 + },
  429 + onSupplierConfirm({ items }) {
  430 + const first = (items && items.length > 0) ? items[0] : null
  431 + if (!first) return
  432 + this.form.supplierId = first.id || ''
  433 + this.form.supplierName = first.unitName || ''
  434 + this.form.purchaseDepartmentName = first.purchaseDepartmentName || ''
  435 + this.form.regionName = first.regionName || ''
  436 + },
  437 + boolText(v) {
  438 + if (typeof v !== 'boolean') return '请选择'
  439 + return v ? '是' : '否'
  440 + },
  441 + openSheet(field) {
  442 + const opts = [{ label: '是', value: true }, { label: '否', value: false }]
  443 + const cur = this.form[field]
  444 + const match = opts.find(o => o.value === cur)
  445 + const titleMap = {
  446 + longTermOperation: '是否长单操作',
  447 + terminalCustomer: '是否终端客户',
  448 + priced: '是否未点价',
  449 + otherExpenseSettlement: '其他费用凭票到厂另行结算',
  450 + commodityBuyout: '是否通货买断',
  451 + qualityBuyout: '是否质量买断',
  452 + quantityBuyout: '是否数量买断',
  453 + doorDelivery: '是否送货上门'
  454 + }
  455 + this.sheet = { visible: true, title: titleMap[field] || '请选择', options: opts, value: match ? match.value : '', field, isDict: false }
  456 + },
  457 + openDictSheet(field, dictCode, title) {
  458 + const raw = this.getDictOptions(dictCode) || []
  459 + const opts = raw.map(o => ({ label: o.name || o.label || '', value: o.code != null ? o.code : (o.value != null ? o.value : '') }))
  460 + const cur = this.form[field]
  461 + const match = opts.find(o => String(o.value) === String(cur))
  462 + this.sheet = { visible: true, title: title || '请选择', options: opts, value: match ? match.value : '', field, isDict: true }
  463 + },
  464 + getDeliveryMethodLabel() {
  465 + const opts = this.getDictOptions('DELIVERY_METHOD') || []
  466 + const m = opts.find(o => String(o.code != null ? o.code : o.value) === String(this.form.deliveryMode))
  467 + return m ? (m.name || m.label || '') : ''
  468 + },
  469 + onSheetConfirm({ value }) {
  470 + const field = this.sheet.field
  471 + if (!field) return
  472 + if (this.sheet.isDict) {
  473 + this.form[field] = value
  474 + } else {
  475 + this.form[field] = !!value
  476 + if (field === 'priced' && value === true) {
  477 + this.form.pricingDeadline = ''
  478 + }
  479 + }
  480 + },
  481 + onRatioBlur() {
  482 + let v = parseFloat(this.form.settlementRatio)
  483 + if (isNaN(v)) v = ''
  484 + else v = Math.round(v * 100) / 100
  485 + this.form.settlementRatio = v
  486 + },
  487 + onProductsChange(products) {
  488 + this.productLineList = Array.isArray(products) ? products : []
  489 + },
  490 + validateRequired() {
  491 + const stringChecks = [
  492 + { key: 'code', msg: '请输入编号' },
  493 + { key: 'supplierId', msg: '请选择供应商' },
  494 + { key: 'validityPeriod', msg: '请输入报价时效' },
  495 + { key: 'deliveryMode', msg: '请选择交提货方式' },
  496 + { key: 'invoiceProductName', msg: '请输入开票品种' },
  497 + { key: 'deliveryDate', msg: '请选择交货日期' },
  498 + { key: 'settlementMethod', msg: '请输入结算方式' },
  499 + { key: 'settlementRatio', msg: '请输入结算比例' }
  500 + ]
  501 + for (const it of stringChecks) {
  502 + const val = this.form[it.key]
  503 + const empty = (val === undefined || val === null || (typeof val === 'string' && val.trim() === ''))
  504 + if (empty) { uni.showToast({ title: it.msg, icon: 'none' }); return false }
  505 + }
  506 + const boolChecks = [
  507 + { key: 'longTermOperation', msg: '请选择是否长单操作' },
  508 + { key: 'terminalCustomer', msg: '请选择是否终端客户' },
  509 + { key: 'priced', msg: '请选择是否未点价' },
  510 + { key: 'otherExpenseSettlement', msg: '请选择其他费用凭票到厂另行结算' },
  511 + { key: 'commodityBuyout', msg: '请选择是否通货买断' },
  512 + { key: 'qualityBuyout', msg: '请选择是否质量买断' },
  513 + { key: 'quantityBuyout', msg: '请选择是否数量买断' },
  514 + { key: 'doorDelivery', msg: '请选择是否送货上门' }
  515 + ]
  516 + for (const it of boolChecks) {
  517 + if (typeof this.form[it.key] !== 'boolean') {
  518 + uni.showToast({ title: it.msg, icon: 'none' })
  519 + return false
  520 + }
  521 + }
  522 + if (this.form.priced === false && !this.form.pricingDeadline) {
  523 + uni.showToast({ title: '请选择点价截止日期', icon: 'none' })
  524 + return false
  525 + }
  526 + const list = Array.isArray(this.productLineList) ? this.productLineList : []
  527 + if (list.length === 0) {
  528 + uni.showToast({ title: '请添加产品明细', icon: 'none' })
  529 + return false
  530 + }
  531 + if (this.$refs.productRel && !this.$refs.productRel.validate(this.form.priced)) return false
  532 + return true
  533 + },
  534 + async onSubmit() {
  535 + if (!this.validateRequired()) return
  536 + const confirmRes = await new Promise(resolve => {
  537 + uni.showModal({ title: '提示', content: '确定保存修改吗?', confirmText: '确定', cancelText: '取消', success: resolve })
  538 + })
  539 + if (!(confirmRes && confirmRes.confirm)) return
  540 + const clean = (obj) => {
  541 + const out = {}
  542 + Object.keys(obj || {}).forEach(k => {
  543 + const v = obj[k]
  544 + const isEmptyString = typeof v === 'string' && v.trim() === ''
  545 + const isUndef = v === undefined || v === null
  546 + const isNaNNumber = typeof v === 'number' && isNaN(v)
  547 + if (!(isEmptyString || isUndef || isNaNNumber)) out[k] = v
  548 + })
  549 + return out
  550 + }
  551 + const lines = (this.productLineList || []).map(it => clean(it))
  552 + const materialFields = this.materialConditionConfigs.map(mc => mc.field)
  553 + const formCopy = { ...this.form }
  554 + materialFields.forEach(f => {
  555 + const arr = formCopy[f]
  556 + formCopy[f] = Array.isArray(arr) ? arr.join(',') : (arr || '')
  557 + })
  558 + const payload = clean({
  559 + ...formCopy,
  560 + id: this.id,
  561 + quotationLineList: lines
  562 + })
  563 + try {
  564 + await salesmanQuotationUpdateApi(payload)
  565 + uni.showToast({ title: '保存成功', icon: 'none' })
  566 + try { uni.setStorageSync('SALES_PRICE_LIST_NEED_REFRESH', '1') } catch (e) {}
  567 + setTimeout(() => { uni.redirectTo({ url: '/pages/sales-price/index' }) }, 400)
  568 + } catch (e) {
  569 + uni.showToast({ title: e.msg || '保存失败', icon: 'none' })
  570 + }
  571 + }
  572 + }
  573 +}
  574 +</script>
  575 +<style lang="scss" scoped>
  576 +.page {
  577 + display: flex;
  578 + flex-direction: column;
  579 + height: 100%;
  580 +}
  581 +
  582 +.scroll {
  583 + flex: 1;
  584 + padding: 12rpx 0 392rpx !important;
  585 +}
  586 +
  587 +.footer {
  588 + z-index: 2;
  589 + position: fixed;
  590 + left: 0;
  591 + right: 0;
  592 + bottom: 0;
  593 + padding: 32rpx;
  594 + padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
  595 + background: #fff;
  596 + box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
  597 +
  598 + .btn {
  599 + height: 80rpx;
  600 + line-height: 80rpx;
  601 + border-radius: 12rpx;
  602 + font-size: 32rpx;
  603 + }
  604 +
  605 + .submit {
  606 + background: $theme-primary;
  607 + color: #fff;
  608 + }
  609 +}
  610 +
  611 +::v-deep .uni-list {
  612 + .uni-easyinput {
  613 + display: flex;
  614 +
  615 + .uni-input-input {
  616 + color: rgba(0, 0, 0, 0.9);
  617 + }
  618 + }
  619 +
  620 + .uni-input-placeholder {
  621 + z-index: 1;
  622 + }
  623 +
  624 + .uni-input-input {
  625 + background-color: #ffffff;
  626 + }
  627 +
  628 + background: transparent;
  629 +
  630 + &-item {
  631 + &__extra-text {
  632 + font-size: 32rpx;
  633 + }
  634 +
  635 + &__content-title {
  636 + font-size: 32rpx;
  637 + color: rgba(0, 0, 0, 0.9);
  638 + }
  639 +
  640 + &__container {
  641 + padding: 32rpx;
  642 +
  643 + .uni-easyinput {
  644 + &__placeholder-class {
  645 + font-size: 32rpx;
  646 + color: rgba(0, 0, 0, 0.4);
  647 + }
  648 +
  649 + &__content {
  650 + border: none;
  651 + background-color: #ffffff !important;
  652 +
  653 + &-input {
  654 + padding-left: 0 !important;
  655 + height: 48rpx;
  656 + line-height: 48rpx;
  657 + font-size: 32rpx;
  658 + }
  659 +
  660 + .content-clear-icon {
  661 + font-size: 44rpx !important;
  662 + }
  663 + }
  664 + }
  665 +
  666 + .item-title,
  667 + .uni-list-item__content {
  668 + flex: none;
  669 + min-height: 48rpx;
  670 + line-height: 48rpx;
  671 + font-size: 32rpx;
  672 + position: relative;
  673 + width: 240rpx;
  674 + margin-right: 32rpx;
  675 + color: rgba(0, 0, 0, 0.9);
  676 +
  677 + .required {
  678 + color: red;
  679 + position: absolute;
  680 + top: 50%;
  681 + transform: translateY(-50%);
  682 + left: -16rpx;
  683 + }
  684 + }
  685 + }
  686 +
  687 + &.select-item {
  688 + &.is-empty {
  689 + .uni-list-item__extra-text {
  690 + color: rgba(0, 0, 0, 0.4) !important;
  691 + }
  692 + }
  693 +
  694 + &.is-filled {
  695 + .uni-list-item__extra-text {
  696 + color: rgba(0, 0, 0, 0.9) !important;
  697 + }
  698 + }
  699 +
  700 + .serial-number-row {
  701 + display: flex;
  702 + align-items: center;
  703 +
  704 + .generate-btn {
  705 + margin-left: 24rpx;
  706 + height: 48rpx;
  707 + line-height: 48rpx;
  708 + padding: 0 24rpx;
  709 + font-size: 24rpx;
  710 + border-radius: 8rpx;
  711 + background: $theme-primary;
  712 + color: #fff;
  713 + }
  714 + }
  715 + }
  716 + }
  717 +}
  718 +
  719 +.readonly-text {
  720 + color: rgba(0, 0, 0, 0.9);
  721 + font-size: 32rpx;
  722 + line-height: 48rpx;
  723 + text-align: right;
  724 + white-space: pre-wrap;
  725 + word-break: break-all;
  726 +}
  727 +
  728 +.value-code {
  729 + width: 260rpx;
  730 + text-align: left;
  731 +}
  732 +
  733 +.material-condition-section,
  734 +.remark-section {
  735 + margin-top: 20rpx;
  736 + background: #fff;
  737 +}
  738 +
  739 +.section-header {
  740 + display: flex;
  741 + align-items: center;
  742 + padding: 24rpx 32rpx;
  743 + border-bottom: 1rpx solid #f0f0f0;
  744 +}
  745 +
  746 +.section-title {
  747 + font-size: 32rpx;
  748 + color: rgba(0, 0, 0, 0.9);
  749 + font-weight: 600;
  750 +}
  751 +
  752 +.opCollapse {
  753 + width: 32rpx;
  754 + height: 28rpx;
  755 + margin-right: 16rpx;
  756 + margin-top: 8rpx;
  757 +}
  758 +
  759 +.section-content {
  760 + padding: 32rpx;
  761 +}
  762 +
  763 +.form-row {
  764 + display: flex;
  765 + align-items: center;
  766 + justify-content: space-between;
  767 + margin-bottom: 24rpx;
  768 +}
  769 +
  770 +.form-row:last-child {
  771 + margin-bottom: 0;
  772 +}
  773 +
  774 +.form-label {
  775 + font-size: 32rpx;
  776 + color: rgba(0, 0, 0, 0.9);
  777 +}
  778 +
  779 +.bool-btns {
  780 + display: flex;
  781 + gap: 20rpx;
  782 +}
  783 +
  784 +.bool-btn {
  785 + padding: 12rpx 40rpx;
  786 + border: 1rpx solid #ddd;
  787 + border-radius: 8rpx;
  788 + font-size: 28rpx;
  789 + color: #666;
  790 + background: #fff;
  791 +}
  792 +
  793 +.bool-btn.active {
  794 + background: $theme-primary;
  795 + border-color: $theme-primary;
  796 + color: #fff;
  797 +}
  798 +
  799 +.material-group {
  800 + margin-top: 32rpx;
  801 +}
  802 +
  803 +.material-label {
  804 + display: block;
  805 + font-size: 30rpx;
  806 + color: rgba(0, 0, 0, 0.9);
  807 + margin-bottom: 20rpx;
  808 +}
  809 +
  810 +.tag-list {
  811 + display: flex;
  812 + flex-wrap: wrap;
  813 + gap: 16rpx;
  814 +}
  815 +
  816 +.tag-btn {
  817 + padding: 12rpx 24rpx;
  818 + border: 1rpx solid #ddd;
  819 + border-radius: 8rpx;
  820 + font-size: 28rpx;
  821 + color: #666;
  822 + background: #fff;
  823 +}
  824 +
  825 +.tag-btn.active {
  826 + background: $theme-primary;
  827 + border-color: $theme-primary;
  828 + color: #fff;
  829 +}
  830 +
  831 +::v-deep .uni-easyinput {
  832 + width: 100%;
  833 +
  834 + &__content {
  835 + background: #f8f8f8 !important;
  836 + border-radius: 8rpx;
  837 +
  838 + &-input {
  839 + font-size: 30rpx;
  840 + }
  841 + }
  842 +}
  843 +</style>
  1 +<template>
  2 + <view class="product-rel">
  3 + <view class="header">
  4 + <image class="opCollapse" src="/static/images/title.png" />
  5 + <text class="title">明细信息</text>
  6 + <view class="ops">
  7 + <image v-if="mode === 'add' || mode === 'edit'" class="opAdd" @click="onAdd" src="/static/images/plus.png" />
  8 + <view v-if="mode === 'view'" class="op1" @click="toggleViewCollapse">
  9 + <image class="opAdd" :src="collapsedView ? '/static/images/down.png' : '/static/images/up.png'" />
  10 + <text class="op">{{ collapsedView ? '展开' : '收起' }}</text>
  11 + </view>
  12 + </view>
  13 + </view>
  14 +
  15 + <view v-if="mode === 'add' || mode === 'edit'" class="add-list">
  16 + <view v-for="(item, idx) in items" :key="item._key || idx" class="block">
  17 + <uni-list v-show="!item.collapsed">
  18 + <uni-list-item class="select-item" :class="item.productName ? 'is-filled' : 'is-empty'" clickable @click="openProductSheet(idx)" :rightText="item.productName || '请选择品种'" showArrow>
  19 + <template v-slot:body>
  20 + <view class="item-title"><text class="required">*</text><text>品种</text></view>
  21 + </template>
  22 + </uni-list-item>
  23 + <uni-list-item title="料型">
  24 + <template v-slot:footer>
  25 + <uni-easyinput v-model="item.materialType" :inputBorder="false" placeholder="请输入料型" />
  26 + </template>
  27 + </uni-list-item>
  28 + <uni-list-item title="数量(吨)">
  29 + <template v-slot:footer>
  30 + <uni-easyinput v-model="item.quantity" type="digit" :inputBorder="false" placeholder="请输入数量" @input="onNonNegativeInput(idx, 'quantity')" @blur="onNonNegativeBlur(idx, 'quantity', 1)" />
  31 + </template>
  32 + </uni-list-item>
  33 + <uni-list-item title="采购价">
  34 + <template v-slot:footer>
  35 + <uni-easyinput v-model="item.purchasePrice" type="digit" :inputBorder="false" placeholder="请输入采购价" @input="onNonNegativeInput(idx, 'purchasePrice')" @blur="onNonNegativeBlur(idx, 'purchasePrice', 2)" @change="recalcEstimatedLoss(idx)" />
  36 + </template>
  37 + </uni-list-item>
  38 + <uni-list-item title="买断料预计损耗">
  39 + <template v-slot:footer>
  40 + <uni-easyinput v-model="item.estimatedLoss" type="digit" :inputBorder="false" disabled placeholder="" />
  41 + </template>
  42 + </uni-list-item>
  43 + <uni-list-item title="申请损耗(%)">
  44 + <template v-slot:footer>
  45 + <uni-easyinput v-model="item.applicationAttrition" type="digit" :inputBorder="false" placeholder="请输入申请损耗" @input="onNonNegativeInput(idx, 'applicationAttrition')" @blur="onNonNegativeBlur(idx, 'applicationAttrition', 2)" @change="recalcEstimatedLoss(idx)" />
  46 + </template>
  47 + </uni-list-item>
  48 + </uni-list>
  49 + <uni-list v-show="item.collapsed">
  50 + <uni-list-item class="select-item" :class="item.productName ? 'is-filled' : 'is-empty'" clickable @click="openProductSheet(idx)" :rightText="item.productName || '请选择品种'" showArrow>
  51 + <template v-slot:body>
  52 + <view class="item-title"><text class="required">*</text><text>品种</text></view>
  53 + </template>
  54 + </uni-list-item>
  55 + </uni-list>
  56 + <view class="block-ops">
  57 + <div class="del" @click="onRemove(idx)">
  58 + <image src="/static/images/delete.png" class="icon" />
  59 + 删除
  60 + </div>
  61 + <div class="toggle" @click="toggleItem(idx)">
  62 + <image :src="item.collapsed ? '/static/images/up.png' : '/static/images/down.png'" class="icon" />
  63 + {{ item.collapsed ? '展开' : '收起' }}</div>
  64 + </view>
  65 + </view>
  66 + </view>
  67 +
  68 + <view v-else class="view-list" v-show="!collapsedView">
  69 + <view v-for="(item, idx) in items" :key="'v-' + (item._key || idx)" class="card">
  70 + <view class="row"><text class="label">品种</text><text class="value">{{ item.productName }}</text></view>
  71 + <view class="row"><text class="label">料型</text><text class="value">{{ item.materialType }}</text></view>
  72 + <view class="row"><text class="label">数量(吨)</text><text class="value">{{ item.quantity }}</text></view>
  73 + <view class="row"><text class="label">采购价</text><text class="value">{{ item.purchasePrice }}</text></view>
  74 + <view class="row"><text class="label">买断料预计损耗</text><text class="value">{{ item.estimatedLoss }}</text></view>
  75 + <view class="row"><text class="label">申请损耗(%)</text><text class="value">{{ item.applicationAttrition }}</text></view>
  76 + </view>
  77 + </view>
  78 +
  79 + <SingleSelectSheet :visible.sync="sheet.visible" :title="sheet.title" :options="sheet.options" v-model="sheet.value" @confirm="onProductConfirm" />
  80 + </view>
  81 +</template>
  82 +<script>
  83 +import SingleSelectSheet from '@/components/single-select/index.vue'
  84 +
  85 +export default {
  86 + name: 'SalesPriceProductRel',
  87 + props: {
  88 + mode: { type: String, default: 'add' },
  89 + list: { type: Array, default: () => [] },
  90 + max: { type: Number, default: 50 },
  91 + options: { type: Array, default: () => [] },
  92 + priced: { type: Boolean, default: true }
  93 + },
  94 + components: { SingleSelectSheet },
  95 + data() {
  96 + return {
  97 + items: [],
  98 + collapsedView: false,
  99 + sheet: { visible: false, title: '请选择', options: [], value: '', idx: -1 },
  100 + _initialized: false
  101 + }
  102 + },
  103 + computed: {
  104 + selectOptions() {
  105 + const list = Array.isArray(this.options) ? this.options : []
  106 + return list.map(o => ({
  107 + label: o.label != null ? o.label : (o.name != null ? o.name : ''),
  108 + value: o.value != null ? o.value : (o.id != null ? o.id : o.productId)
  109 + }))
  110 + }
  111 + },
  112 + watch: {
  113 + items: {
  114 + handler() { this.emitChange() },
  115 + deep: true
  116 + },
  117 + list: {
  118 + handler(v) {
  119 + if (!v || !v.length) return
  120 + this._initialized = false
  121 + this.initItems()
  122 + this.$nextTick(() => { this._initialized = true })
  123 + }
  124 + }
  125 + },
  126 + created() {
  127 + this.initItems()
  128 + this.$nextTick(() => { this._initialized = true })
  129 + },
  130 + methods: {
  131 + initItems() {
  132 + const src = Array.isArray(this.list) && this.list.length > 0 ? this.list : []
  133 + if (!src.length) {
  134 + this.items = [{ ...this.defaultItem(), _key: this.newKey(), collapsed: false }]
  135 + return
  136 + }
  137 + const existing = this.items || []
  138 + const existingMap = {}
  139 + existing.forEach(it => {
  140 + if (it.itemId) existingMap[it.itemId] = it
  141 + })
  142 + this.items = src.map((x, i) => {
  143 + const key = x && x.itemId ? x.itemId : (x && x._key ? x._key : this.newKey())
  144 + const old = existingMap[key]
  145 + return {
  146 + ...this.defaultItem(),
  147 + ...x,
  148 + _key: key,
  149 + itemId: x && x.itemId ? x.itemId : key,
  150 + collapsed: old ? old.collapsed : true
  151 + }
  152 + })
  153 + },
  154 + newKey() {
  155 + return `${Date.now()}-${Math.random().toString(16).slice(2)}`
  156 + },
  157 + defaultItem() {
  158 + return {
  159 + productId: '',
  160 + productName: '',
  161 + materialType: '',
  162 + quantity: '',
  163 + purchasePrice: '',
  164 + estimatedLoss: '',
  165 + applicationAttrition: '',
  166 + attritionRate: ''
  167 + }
  168 + },
  169 + onNonNegativeInput(idx, field) {
  170 + const it = this.items[idx]
  171 + if (!it) return
  172 + let v = String(it[field] != null ? it[field] : '')
  173 + v = v.replace(/[^0-9.]/g, '')
  174 + v = v.replace(/(\..*)\./g, '$1')
  175 + it[field] = v
  176 + this.$set(this.items, idx, it)
  177 + if (field === 'applicationAttrition' || field === 'purchasePrice') {
  178 + this.recalcEstimatedLoss(idx)
  179 + }
  180 + },
  181 + onNonNegativeBlur(idx, field, digits) {
  182 + const it = this.items[idx]
  183 + if (!it) return
  184 + let raw = it[field]
  185 + if (raw === '' || raw === null || raw === undefined) {
  186 + this.$set(this.items, idx, it)
  187 + return
  188 + }
  189 + let val = parseFloat(raw)
  190 + if (isNaN(val)) val = 0
  191 + const m = Math.pow(10, digits)
  192 + const rounded = Math.round(val * m) / m
  193 + it[field] = rounded
  194 + this.$set(this.items, idx, it)
  195 + if (field === 'applicationAttrition' || field === 'purchasePrice') {
  196 + this.recalcEstimatedLoss(idx)
  197 + }
  198 + },
  199 + recalcEstimatedLoss(idx) {
  200 + const it = this.items[idx]
  201 + if (!it) return
  202 + const price = parseFloat(it.purchasePrice)
  203 + if (isNaN(price) || price === 0) {
  204 + it.estimatedLoss = ''
  205 + this.$set(this.items, idx, it)
  206 + return
  207 + }
  208 + const app = it.applicationAttrition
  209 + const appNum = parseFloat(app)
  210 + if (app !== '' && app != null && !isNaN(appNum)) {
  211 + it.estimatedLoss = Math.round(price * appNum / 100 * 100) / 100
  212 + } else {
  213 + const opt = (this.options || []).find(o => String(o.value) === String(it.productId))
  214 + const rate = opt ? (opt.lossRate != null ? opt.lossRate : opt.fixedLossRate) : null
  215 + const rateNum = parseFloat(rate)
  216 + if (!isNaN(rateNum)) {
  217 + it.estimatedLoss = Math.round(price * rateNum / 100 * 100) / 100
  218 + } else {
  219 + it.estimatedLoss = ''
  220 + }
  221 + }
  222 + this.$set(this.items, idx, it)
  223 + },
  224 + onAdd() {
  225 + if (this.items.length >= this.max) {
  226 + uni.showToast({ title: `最多${this.max}条明细`, icon: 'none' })
  227 + return
  228 + }
  229 + this.items.push({ ...this.defaultItem(), _key: this.newKey(), collapsed: false })
  230 + },
  231 + onRemove(idx) {
  232 + this.items.splice(idx, 1)
  233 + if (this.items.length === 0) {
  234 + this.items.push({ ...this.defaultItem(), _key: this.newKey(), collapsed: false })
  235 + }
  236 + },
  237 + toggleItem(idx) {
  238 + const it = this.items[idx]
  239 + if (!it) return
  240 + this.$set(it, 'collapsed', !it.collapsed)
  241 + },
  242 + toggleViewCollapse() {
  243 + this.collapsedView = !this.collapsedView
  244 + },
  245 + openProductSheet(idx) {
  246 + const opts = this.selectOptions
  247 + const cur = this.items[idx] && this.items[idx].productId
  248 + const match = opts.find(o => String(o.value) === String(cur))
  249 + this.sheet = { visible: true, title: '请选择品种', options: opts, value: match ? match.value : '', idx }
  250 + },
  251 + onProductConfirm({ value, label }) {
  252 + const idx = this.sheet.idx
  253 + if (idx < 0 || !this.items[idx]) return
  254 + const it = this.items[idx]
  255 + this.$set(it, 'productId', value)
  256 + this.$set(it, 'productName', label)
  257 + const opt = (this.options || []).find(o => String(o.value != null ? o.value : (o.id != null ? o.id : o.productId)) === String(value))
  258 + const rate = opt ? (opt.lossRate != null ? opt.lossRate : opt.fixedLossRate) : ''
  259 + this.$set(it, 'attritionRate', rate != null ? rate : '')
  260 + this.recalcEstimatedLoss(idx)
  261 + },
  262 + emitChange() {
  263 + if (!this._initialized) return
  264 + const list = (this.items || []).map(it => {
  265 + const c = { ...it }
  266 + const key = c._key
  267 + delete c._key
  268 + delete c.collapsed
  269 + delete c._selected
  270 + if (!c.itemId) c.itemId = key
  271 + return c
  272 + })
  273 + this.$emit('change', list)
  274 + },
  275 + validate(priced) {
  276 + const list = this.items || []
  277 + for (let i = 0; i < list.length; i++) {
  278 + const it = list[i]
  279 + if (!it.productId) {
  280 + uni.showToast({ title: `第${i + 1}条明细:请选择品种`, icon: 'none' })
  281 + return false
  282 + }
  283 + if (priced === false && (it.purchasePrice === '' || it.purchasePrice == null)) {
  284 + uni.showToast({ title: `第${i + 1}条明细:未点价为否时采购价必填`, icon: 'none' })
  285 + return false
  286 + }
  287 + }
  288 + return true
  289 + }
  290 + }
  291 +}
  292 +</script>
  293 +<style lang="scss" scoped>
  294 +.product-rel {
  295 + margin-top: 20rpx;
  296 + background: #fff;
  297 +}
  298 +
  299 +.header {
  300 + display: flex;
  301 + align-items: center;
  302 + padding: 24rpx 32rpx;
  303 + border-bottom: 1rpx solid #f0f0f0;
  304 +}
  305 +
  306 +.dot {
  307 + width: 16rpx;
  308 + height: 16rpx;
  309 + background: #3D48A3;
  310 + border-radius: 50%;
  311 + margin-right: 12rpx;
  312 +}
  313 +
  314 +.title {
  315 + font-size: 32rpx;
  316 + color: rgba(0, 0, 0, 0.9);
  317 + font-weight: 600;
  318 +}
  319 +
  320 +.ops {
  321 + margin-left: auto;
  322 +}
  323 +
  324 +.op {
  325 + color: $theme-primary;
  326 + font-size: 28rpx;
  327 + margin-left: 8rpx;
  328 +}
  329 +
  330 +.op1 {
  331 + display: flex;
  332 + align-items: center;
  333 +}
  334 +
  335 +.opAdd {
  336 + color: rgba(0, 0, 0, 0.6);
  337 + width: 40rpx;
  338 + height: 40rpx;
  339 +}
  340 +
  341 +.opCollapse {
  342 + color: rgba(0, 0, 0, 0.6);
  343 + width: 32rpx;
  344 + height: 28rpx;
  345 + margin-right: 16rpx;
  346 + margin-top: 8rpx;
  347 +}
  348 +
  349 +.block {
  350 + background: #fff;
  351 + margin-bottom: 20rpx;
  352 +}
  353 +
  354 +::v-deep .uni-list-item__content {
  355 + display: flex;
  356 + justify-content: center;
  357 +}
  358 +
  359 +::v-deep .uni-list {
  360 + background: transparent;
  361 +
  362 + &-item {
  363 + &__container {
  364 + padding: 32rpx;
  365 + }
  366 +
  367 + &__content-title {
  368 + font-size: 28rpx;
  369 + color: rgba(0, 0, 0, 0.9);
  370 + }
  371 +
  372 + &__extra-text {
  373 + font-size: 32rpx;
  374 + }
  375 +
  376 + .uni-easyinput {
  377 + width: 100%;
  378 +
  379 + &__placeholder-class {
  380 + font-size: 32rpx;
  381 + color: rgba(0, 0, 0, 0.4);
  382 + }
  383 +
  384 + &__content {
  385 + border: none;
  386 + display: flex;
  387 +
  388 + &-input {
  389 + padding-left: 0 !important;
  390 + height: 48rpx;
  391 + line-height: 48rpx;
  392 + font-size: 32rpx;
  393 + }
  394 + }
  395 + }
  396 + }
  397 +}
  398 +
  399 +.block-ops {
  400 + display: flex;
  401 + padding: 20rpx 32rpx 20rpx;
  402 + justify-content: space-around;
  403 +}
  404 +
  405 +.del {
  406 + color: #D54941;
  407 + font-size: 28rpx;
  408 + display: flex;
  409 + align-items: center;
  410 +
  411 + image {
  412 + width: 40rpx;
  413 + height: 40rpx;
  414 + }
  415 +}
  416 +
  417 +.toggle {
  418 + color: $theme-primary;
  419 + font-size: 28rpx;
  420 + display: flex;
  421 + align-items: center;
  422 +
  423 + image {
  424 + width: 40rpx;
  425 + height: 40rpx;
  426 + }
  427 +}
  428 +
  429 +.view-list {
  430 + padding: 24rpx 32rpx;
  431 + background: #ffffff;
  432 +}
  433 +
  434 +.card {
  435 + background: #f3f3f3;
  436 + border-radius: 16rpx;
  437 + padding: 24rpx;
  438 + margin-bottom: 20rpx;
  439 +}
  440 +
  441 +.row {
  442 + display: flex;
  443 + margin-bottom: 20rpx;
  444 +}
  445 +
  446 +.row:last-child {
  447 + margin-bottom: 0;
  448 +}
  449 +
  450 +.label {
  451 + width: 240rpx;
  452 + color: rgba(0, 0, 0, 0.6);
  453 + font-size: 28rpx;
  454 +}
  455 +
  456 +.value {
  457 + flex: 1;
  458 + color: rgba(0, 0, 0, 0.9);
  459 + font-size: 28rpx;
  460 +}
  461 +
  462 +.item-title {
  463 + position: relative;
  464 + width: 210rpx;
  465 + margin-right: 32rpx;
  466 + color: rgba(0, 0, 0, 0.9);
  467 + font-size: 32rpx;
  468 + min-height: 48rpx;
  469 + line-height: 48rpx;
  470 +
  471 + .required {
  472 + color: red;
  473 + position: absolute;
  474 + top: 50%;
  475 + transform: translateY(-50%);
  476 + left: -16rpx;
  477 + }
  478 +}
  479 +</style>
  1 +<template>
  2 + <view class="detail-page">
  3 + <scroll-view class="scroll" scroll-y>
  4 + <!-- 基础信息 -->
  5 + <view class="section">
  6 + <text class="row company">{{ safeText(form.code) }}</text>
  7 + <view class="row"><text class="label">供应商名称</text><text class="value">{{ safeText(form.supplierName) }}</text></view>
  8 + <view class="row"><text class="label">采购处</text><text class="value">{{ safeText(form.purchaseDepartmentName) }}</text></view>
  9 + <view class="row"><text class="label">区域</text><text class="value">{{ safeText(form.regionName) }}</text></view>
  10 + <view class="row"><text class="label">报价时效</text><text class="value">{{ safeText(form.validityPeriod) }}</text></view>
  11 + <view class="row"><text class="label">报价人</text><text class="value">{{ safeText(form.quoterName) }}</text></view>
  12 + <view class="row"><text class="label">是否长单操作</text><text class="value">{{ boolText(form.longTermOperation) }}</text></view>
  13 + <view class="row"><text class="label">是否终端客户</text><text class="value">{{ boolText(form.terminalCustomer) }}</text></view>
  14 + <view class="row"><text class="label">是否未点价</text><text class="value">{{ boolText(form.priced) }}</text></view>
  15 + <view v-if="form.priced === false" class="row"><text class="label">点价截止日期</text><text class="value">{{ formatDateOnly(form.pricingDeadline) }}</text></view>
  16 + <view class="row"><text class="label">其他费用凭票到厂另行结算</text><text class="value">{{ boolText(form.otherExpenseSettlement) }}</text></view>
  17 + <view class="row"><text class="label">交提货方式</text><text class="value">{{ getDeliveryMethodLabel() }}</text></view>
  18 + <view class="row"><text class="label">是否通货买断</text><text class="value">{{ boolText(form.commodityBuyout) }}</text></view>
  19 + <view class="row"><text class="label">是否质量买断</text><text class="value">{{ boolText(form.qualityBuyout) }}</text></view>
  20 + <view class="row"><text class="label">是否数量买断</text><text class="value">{{ boolText(form.quantityBuyout) }}</text></view>
  21 + <view class="row"><text class="label">是否送货上门</text><text class="value">{{ boolText(form.doorDelivery) }}</text></view>
  22 + <view class="row"><text class="label">装车费</text><text class="value">{{ safeText(form.loadingFee) }}</text></view>
  23 + <view class="row"><text class="label">开票品种</text><text class="value">{{ safeText(form.invoiceProductName) }}</text></view>
  24 + <view class="row"><text class="label">交货日期</text><text class="value">{{ formatDateOnly(form.deliveryDate) }}</text></view>
  25 + <view class="row"><text class="label">结算方式</text><text class="value">{{ safeText(form.settlementMethod) }}</text></view>
  26 + <view class="row"><text class="label">结算比例(%)</text><text class="value">{{ safeText(form.settlementRatio) }}</text></view>
  27 + <view class="row"><text class="label">协调事由</text><text class="value">{{ safeText(form.coordinationReason) }}</text></view>
  28 + <view class="row"><text class="label">票类状况</text><text class="value">{{ safeText(form.invoiceTypeStatus) }}</text></view>
  29 + </view>
  30 +
  31 + <!-- 明细信息 -->
  32 + <ProductRel mode="view" :list="productLineList" />
  33 +
  34 + <!-- 料质状况 -->
  35 + <view class="title-header">
  36 + <image class="title-header_icon" src="/static/images/title.png" />
  37 + <span>料质状况</span>
  38 + </view>
  39 + <view class="section">
  40 + <view class="row"><text class="label">是否启用料质状况</text><text class="value">{{ boolText(form.materialConditionEnabled) }}</text></view>
  41 + <template v-if="form.materialConditionEnabled">
  42 + <view v-for="mc in materialConditionConfigs" :key="mc.field" class="material-group">
  43 + <text class="material-label">{{ mc.label }}</text>
  44 + <view class="tag-list">
  45 + <view
  46 + v-for="opt in getDictOptions(mc.dictCode)"
  47 + :key="opt.code"
  48 + class="tag-btn"
  49 + :class="{ active: isTagSelected(mc.field, opt.code) }"
  50 + >{{ opt.name }}</view>
  51 + </view>
  52 + </view>
  53 + </template>
  54 + </view>
  55 +
  56 + <!-- 备注 -->
  57 + <view class="title-header">
  58 + <image class="title-header_icon" src="/static/images/title.png" />
  59 + <span>备注</span>
  60 + </view>
  61 + <view class="section">
  62 + <text class="remark-text">{{ safeText(form.remark) }}</text>
  63 + </view>
  64 + </scroll-view>
  65 + </view>
  66 +</template>
  67 +<script>
  68 +import ProductRel from './productRel.vue'
  69 +import { salesmanQuotationDetailApi } from '@/api/procure-manage/salesmanQuotation.js'
  70 +import { getDicByCodes } from '@/utils/dic.js'
  71 +
  72 +export default {
  73 + name: 'SalesmanQuotationViewer',
  74 + components: { ProductRel },
  75 + props: { id: { type: [String, Number], default: '' } },
  76 + data() {
  77 + return {
  78 + form: {},
  79 + productLineList: [],
  80 + materialConditionConfigs: [
  81 + { field: 'materialConditionCopperRice', label: '铜米', dictCode: 'COPPER_RICE' },
  82 + { field: 'materialConditionBrightCopper', label: '光亮铜', dictCode: 'BRIGHT_COPPER' },
  83 + { field: 'materialConditionFirstClass', label: '一特', dictCode: 'FIRST_CLASS' },
  84 + { field: 'materialConditionSecondClass', label: '二特', dictCode: 'SECOND_CLASS' },
  85 + { field: 'materialConditionTinCoated', label: '镀锡紫', dictCode: 'TIN_COATED' },
  86 + { field: 'materialConditionScrap', label: '角料', dictCode: 'SCRAP' },
  87 + { field: 'materialConditionWhiteYellow', label: '镀白黄', dictCode: 'WHITE_YELLOW' },
  88 + { field: 'materialConditionPhosphorCopper', label: '磷铜', dictCode: 'PHOSPHOR_COPPER' }
  89 + ],
  90 + dictData: {}
  91 + }
  92 + },
  93 + created() {
  94 + this.loadDictData()
  95 + },
  96 + watch: {
  97 + id: {
  98 + immediate: true,
  99 + handler(val) {
  100 + const v = (val !== undefined && val !== null) ? String(val) : ''
  101 + if (v) this.loadDetail(v)
  102 + }
  103 + }
  104 + },
  105 + methods: {
  106 + async loadDictData() {
  107 + try {
  108 + const codes = [...this.materialConditionConfigs.map(mc => mc.dictCode), 'DELIVERY_METHOD']
  109 + const res = await getDicByCodes(codes)
  110 + this.dictData = res || {}
  111 + } catch (e) {
  112 + this.dictData = {}
  113 + }
  114 + },
  115 + getDictOptions(dictCode) {
  116 + const data = this.dictData[dictCode]
  117 + return (data && data.data) ? data.data : []
  118 + },
  119 + isTagSelected(field, code) {
  120 + const selected = this.form[field]
  121 + return Array.isArray(selected) && selected.includes(code)
  122 + },
  123 + boolText(v) {
  124 + if (typeof v !== 'boolean') return '-'
  125 + return v ? '是' : '否'
  126 + },
  127 + getDeliveryMethodLabel() {
  128 + const opts = this.getDictOptions('DELIVERY_METHOD') || []
  129 + const m = opts.find(o => String(o.code != null ? o.code : o.value) === String(this.form.deliveryMode))
  130 + return m ? (m.name || m.label || '-') : '-'
  131 + },
  132 + formatDateOnly(v) {
  133 + const s = v == null ? '' : String(v).trim()
  134 + if (!s) return '-'
  135 + if (s.length >= 10) return s.slice(0, 10)
  136 + return s
  137 + },
  138 + safeText(v) {
  139 + const s = v == null ? '' : String(v)
  140 + return s.trim() ? s : '-'
  141 + },
  142 + async loadDetail(id) {
  143 + // 审核详情接口不可用时回退到普通详情接口
  144 + try {
  145 + const res = await salesmanQuotationDetailApi({ id })
  146 + const d = (res && res.data) ? res.data : {}
  147 + const materialFields = this.materialConditionConfigs.map(mc => mc.field)
  148 + materialFields.forEach(f => {
  149 + const v = d[f]
  150 + d[f] = (typeof v === 'string' && v) ? v.split(',').filter(Boolean) : (Array.isArray(v) ? v : [])
  151 + })
  152 + this.form = d
  153 + this.productLineList = Array.isArray(d.quotationLineList) ? d.quotationLineList : (Array.isArray(d.salesmanQuotationLineList) ? d.salesmanQuotationLineList : [])
  154 + } catch (err) {
  155 + this.form = {}
  156 + this.productLineList = []
  157 + }
  158 + }
  159 + }
  160 +}
  161 +</script>
  162 +<style lang="scss" scoped>
  163 +.detail-page {
  164 + background: #f3f3f3;
  165 +}
  166 +
  167 +.scroll {
  168 + flex: 1;
  169 + padding: 8rpx 0 144rpx;
  170 +}
  171 +
  172 +.title-header {
  173 + display: flex;
  174 + align-items: center;
  175 + padding: 24rpx 32rpx 12rpx;
  176 + background-color: #ffffff;
  177 +
  178 + .title-header_icon {
  179 + width: 36rpx;
  180 + height: 36rpx;
  181 + margin-right: 12rpx;
  182 + }
  183 +
  184 + span {
  185 + font-size: 34rpx;
  186 + font-weight: 600;
  187 + color: rgba(0, 0, 0, 0.9);
  188 + }
  189 +}
  190 +
  191 +.section {
  192 + padding: 32rpx;
  193 + background: #fff;
  194 + margin-bottom: 20rpx;
  195 +}
  196 +
  197 +.row {
  198 + display: flex;
  199 + margin-bottom: 28rpx;
  200 +
  201 + &:last-child {
  202 + margin-bottom: 0;
  203 + }
  204 +
  205 + &.company {
  206 + font-size: 36rpx;
  207 + font-weight: 600;
  208 + color: rgba(0, 0, 0, 0.9);
  209 + padding-top: 8rpx;
  210 + line-height: 50rpx;
  211 + }
  212 +
  213 + .label {
  214 + max-width: 420rpx;
  215 + margin-right: 20rpx;
  216 + line-height: 32rpx;
  217 + font-size: 28rpx;
  218 + color: rgba(0, 0, 0, 0.6);
  219 + }
  220 +
  221 + .value {
  222 + flex: 1;
  223 + line-height: 32rpx;
  224 + font-size: 28rpx;
  225 + color: rgba(0, 0, 0, 0.9);
  226 + text-align: right;
  227 + word-break: break-all;
  228 + }
  229 +}
  230 +
  231 +.material-group {
  232 + margin-top: 32rpx;
  233 +
  234 + &:first-child {
  235 + margin-top: 0;
  236 + }
  237 +}
  238 +
  239 +.material-label {
  240 + display: block;
  241 + font-size: 28rpx;
  242 + color: rgba(0, 0, 0, 0.6);
  243 + margin-bottom: 20rpx;
  244 +}
  245 +
  246 +.tag-list {
  247 + display: flex;
  248 + flex-wrap: wrap;
  249 + gap: 16rpx;
  250 +}
  251 +
  252 +.tag-btn {
  253 + padding: 12rpx 24rpx;
  254 + border: 1rpx solid #ddd;
  255 + border-radius: 8rpx;
  256 + font-size: 28rpx;
  257 + color: #999;
  258 + background: #f5f5f5;
  259 +}
  260 +
  261 +.tag-btn.active {
  262 + background: $theme-primary;
  263 + border-color: $theme-primary;
  264 + color: #fff;
  265 +}
  266 +
  267 +.remark-text {
  268 + font-size: 28rpx;
  269 + color: rgba(0, 0, 0, 0.9);
  270 + line-height: 1.6;
  271 + white-space: pre-wrap;
  272 +}
  273 +</style>
1 import CustomerDevelopViewer from '@/pages/dev_manage/viewer.vue' 1 import CustomerDevelopViewer from '@/pages/dev_manage/viewer.vue'
2 import CustomerCreditViewer from '@/pages/credit_manage/viewer.vue' 2 import CustomerCreditViewer from '@/pages/credit_manage/viewer.vue'
3 import DomesticCustomerCreditViewer from '@/pages/domestic_trade/domesticCreditViewer.vue' 3 import DomesticCustomerCreditViewer from '@/pages/domestic_trade/domesticCreditViewer.vue'
  4 +import SalesmanQuotationViewer from '@/pages/sales-price/salesmanQuotationViewer.vue'
4 import ForeignTradeCreditViewer from '@/pages/foreign_trade/foreignTradeCreditViewer.vue' 5 import ForeignTradeCreditViewer from '@/pages/foreign_trade/foreignTradeCreditViewer.vue'
5 import RetailViewer from '@/pages/contract_retail/retailViewer.vue' 6 import RetailViewer from '@/pages/contract_retail/retailViewer.vue'
6 import ForeignStdViewer from '@/pages/contract_foreign_std/foreignStdViewer.vue' 7 import ForeignStdViewer from '@/pages/contract_foreign_std/foreignStdViewer.vue'
@@ -29,6 +30,7 @@ export default function registerComponents(Vue) { @@ -29,6 +30,7 @@ export default function registerComponents(Vue) {
29 Vue.component('CustomerDevelopViewer', CustomerDevelopViewer) 30 Vue.component('CustomerDevelopViewer', CustomerDevelopViewer)
30 Vue.component('CustomerCreditViewer', CustomerCreditViewer) 31 Vue.component('CustomerCreditViewer', CustomerCreditViewer)
31 Vue.component('DomesticCustomerCreditViewer', DomesticCustomerCreditViewer) 32 Vue.component('DomesticCustomerCreditViewer', DomesticCustomerCreditViewer)
  33 + Vue.component('SalesmanQuotationViewer', SalesmanQuotationViewer)
32 Vue.component('ForeignTradeCreditViewer', ForeignTradeCreditViewer) 34 Vue.component('ForeignTradeCreditViewer', ForeignTradeCreditViewer)
33 Vue.component('RetailViewer', RetailViewer) 35 Vue.component('RetailViewer', RetailViewer)
34 Vue.component('StandardContractViewer', StandardContractViewer) 36 Vue.component('StandardContractViewer', StandardContractViewer)
@@ -20,6 +20,12 @@ export const getSysFlowComponentPath = (bizFlag) => { @@ -20,6 +20,12 @@ export const getSysFlowComponentPath = (bizFlag) => {
20 case 'CUSTOMER_CREDIT_IN_EDIT': // 客户资信(内贸) 20 case 'CUSTOMER_CREDIT_IN_EDIT': // 客户资信(内贸)
21 componentPath = 'DomesticCustomerCreditViewer'; // 客户资信(内贸)-审批 21 componentPath = 'DomesticCustomerCreditViewer'; // 客户资信(内贸)-审批
22 break; 22 break;
  23 + case 'SALESMAN_QUOTATION': // 业务员报价单
  24 + componentPath = 'SalesmanQuotationViewer'; // 业务员报价单-审批详情
  25 + break;
  26 + case 'SALESMAN_QUOTATION_EDIT': // 业务员报价单
  27 + componentPath = 'SalesmanQuotationViewer'; // 业务员报价单-审批
  28 + break;
23 case 'CUSTOMER_CREDIT_OUT': // 客户资信(外贸) 29 case 'CUSTOMER_CREDIT_OUT': // 客户资信(外贸)
24 componentPath = 'ForeignTradeCreditViewer'; // 客户资信(外贸)-审批详情 30 componentPath = 'ForeignTradeCreditViewer'; // 客户资信(外贸)-审批详情
25 break; 31 break;