Commit 91ec5a27faecd1a966548d17aac53f5774a1ecb6

Authored by gesilong
1 parent cd0d970b

commit: 经销标准合同新增/查看

1 1 import request from '@/utils/request'
2 2 import { ContentTypeEnum } from '@/utils/httpEnum';
3   -
  3 +const region = 'cloud-api';
4 4 export const statusStyle = [
5 5 { color: '#2BA471', bgColor:'#E3F9E9',text: '审核通过' },
6 6 { color: '#D54941', bgColor:'#FFF0ED',text: '已驳回' },
... ... @@ -83,3 +83,25 @@ export function getRetailCodeApi() {
83 83 method: 'get',
84 84 })
85 85 }
  86 +
  87 +// 新增合同
  88 +export function createContractApi(data) {
  89 + return request({
  90 + url: `${baseUrl}/contractDistributorStandard`,
  91 + method: 'post',
  92 + data,
  93 + contentType: ContentTypeEnum.JSON
  94 + })
  95 +}
  96 +
  97 +// 查看合同
  98 +export function getContractApi(id) {
  99 + return request({
  100 + url: `${baseUrl}/contractDistributorStandard`,
  101 + method: 'get',
  102 + params: { id },
  103 + contentType: ContentTypeEnum.JSON,
  104 + region
  105 + })
  106 +}
  107 +
... ...
... ... @@ -239,6 +239,7 @@ export default {
239 239 }
240 240
241 241 .footer {
  242 + z-index: 2;
242 243 position: fixed;
243 244 left: 0;
244 245 right: 0;
... ...
... ... @@ -33,10 +33,161 @@
33 33 <uni-easyinput v-model="form.unit" :inputBorder="false" disabled />
34 34 </template>
35 35 </uni-list-item>
36   - <ProductRel type="add" v-model="form.productRel" />
  36 + <ProductRel mode="add" :orderDateBase="form.orderDate" @change="onProductsChange" :options="productList" />
  37 + <uni-list-item title="合计人民币金额(大写)">
  38 + <template v-slot:footer>
  39 + <uni-easyinput v-model="form.totalAmountCapital" placeholder="自动计算" :inputBorder="false" disabled />
  40 + </template>
  41 + </uni-list-item>
  42 + <uni-list-item title="交付定金、数额、时间">
  43 + <template v-slot:footer>
  44 + <uni-easyinput v-model="form.depositInfo" placeholder="请输入交付定金、数额、时间" :inputBorder="false" />
  45 + </template>
  46 + </uni-list-item>
  47 + <uni-list-item title="包装要求">
  48 + <template v-slot:footer>
  49 + <uni-easyinput v-model="form.packagingRequirements" placeholder="请输入包装要求"
  50 + :inputBorder="false" />
  51 + </template>
  52 + </uni-list-item>
  53 + <uni-list-item title="付款方式、付款期限">
  54 + <template v-slot:footer>
  55 + <uni-easyinput v-model="form.paymentTerms" placeholder="请输入付款方式、付款期限" :inputBorder="false" />
  56 + </template>
  57 + </uni-list-item>
  58 + <uni-list-item title="运输方式">
  59 + <template v-slot:footer>
  60 + <uni-easyinput v-model="form.transportMode" placeholder="请输入运输方式" :inputBorder="false" />
  61 + </template>
  62 + </uni-list-item>
  63 + <uni-list-item title="目的地">
  64 + <template v-slot:footer>
  65 + <uni-easyinput v-model="form.destinationId" placeholder="请输入目的地" :inputBorder="false" />
  66 + </template>
  67 + </uni-list-item>
  68 + <uni-list-item class="select-item" :class="form.includesPackagingFeeName ? 'is-filled' : 'is-empty'" clickable
  69 + @click="openSheet('includesPackagingFee')" :rightText="form.includesPackagingFeeName || '请选择'" showArrow>
  70 + <template v-slot:body>
  71 + <view class="item-title"><text>单价中是否已包含包装费</text></view>
  72 + </template>
  73 + </uni-list-item>
  74 + <uni-list-item class="select-item" :class="form.includesTransportFeeName ? 'is-filled' : 'is-empty'" clickable
  75 + @click="openSheet('includesTransportFee')" :rightText="form.includesTransportFeeName || '请选择'" showArrow>
  76 + <template v-slot:body>
  77 + <view class="item-title"><text>单价中是否已包含运费</text></view>
  78 + </template>
  79 + </uni-list-item>
  80 + <uni-list-item title="需方指定收货人">
  81 + <template v-slot:footer>
  82 + <uni-easyinput v-model="form.designatedConsignee" placeholder="请输入需方指定收货人"
  83 + :inputBorder="false" />
  84 + </template>
  85 + </uni-list-item>
  86 + <view class="group">
  87 + <view class="group-title">特别条款要求</view>
  88 + <view class="radio-list">
  89 + <view v-for="(opt, i) in specialTermsList" :key="'cr-' + i" class="radio-item"
  90 + @click="onRadioSelect('specialTerms', 'specialTermsName', opt)">
  91 + <view :class="['radio', { checked: form.specialTerms === opt.value }]" />
  92 + <text class="label">{{ opt.label }}</text>
  93 + </view>
  94 + </view>
  95 + </view>
  96 + <view class="group">
  97 + <view class="group-title">执行标准</view>
  98 + <view class="radio-list">
  99 + <view v-for="(opt, i) in executionStandardList" :key="'es-' + i" class="radio-item"
  100 + @click="onRadioSelect('executionStandard', 'executionStandardName', opt)">
  101 + <view :class="['radio', { checked: form.executionStandard === opt.value }]" />
  102 + <text class="label">{{ opt.label }}</text>
  103 + </view>
  104 + </view>
  105 + </view>
  106 + <uni-list-item v-if="form.executionStandard === 'OTHER'" title="其他">
  107 + <template v-slot:footer>
  108 + <uni-easyinput v-model="form.executionStandardRemarks" placeholder="请输入其他标准备注"
  109 + :inputBorder="false" />
  110 + </template>
  111 + </uni-list-item>
  112 + <uni-list-item title="特别说明">
  113 + <template v-slot:footer>
  114 + <uni-easyinput v-model="form.specialInstructions" placeholder="请输入特别说明" :inputBorder="false" />
  115 + </template>
  116 + </uni-list-item>
  117 + <uni-list-item title="备注">
  118 + <template v-slot:footer>
  119 + <uni-easyinput v-model="form.remarks" placeholder="请输入备注" :inputBorder="false" />
  120 + </template>
  121 + </uni-list-item>
  122 + <view class="quality">
  123 + <image class="opCollapse" src="/static/images/title.png" />
  124 + <text class="title">具体质量要求</text>
  125 + </view>
  126 + <uni-list-item title="件重条头">
  127 + <template v-slot:footer>
  128 + <uni-easyinput v-model="form.pieceWeightHead" placeholder="请输入" :inputBorder="false" />
  129 + </template>
  130 + </uni-list-item>
  131 + <uni-list-item title="表面">
  132 + <template v-slot:footer>
  133 + <uni-easyinput v-model="form.surface" placeholder="请输入" :inputBorder="false" />
  134 + </template>
  135 + </uni-list-item>
  136 + <uni-list-item title="公差">
  137 + <template v-slot:footer>
  138 + <uni-easyinput v-model="form.tolerance" placeholder="请输入" :inputBorder="false" />
  139 + </template>
  140 + </uni-list-item>
  141 + <uni-list-item title="性能">
  142 + <template v-slot:footer>
  143 + <uni-easyinput v-model="form.performance" placeholder="请输入" :inputBorder="false" />
  144 + </template>
  145 + </uni-list-item>
  146 + <uni-list-item title="成分">
  147 + <template v-slot:footer>
  148 + <uni-easyinput v-model="form.component" placeholder="请输入" :inputBorder="false" />
  149 + </template>
  150 + </uni-list-item>
  151 + <uni-list-item title="包装">
  152 + <template v-slot:footer>
  153 + <uni-easyinput v-model="form.packaging" placeholder="请输入" :inputBorder="false" />
  154 + </template>
  155 + </uni-list-item>
37 156 </uni-list>
38   - </scroll-view>
39 157
  158 + </scroll-view>
  159 + <view class="footer">
  160 + <div class="total">
  161 + <div class="total-text">
  162 + 合计
  163 + </div>
  164 + <div class="total-item">
  165 + <div class="total-item-text">
  166 + 数量
  167 + </div>
  168 + <div class="total-item-price">
  169 + {{ (sumQuantity || 0).toFixed(2) }}t
  170 + </div>
  171 + </div>
  172 + <div class="total-item">
  173 + <div class="total-item-text">
  174 + 不含税金额
  175 + </div>
  176 + <div class="total-item-price text-red">
  177 + ¥{{ (sumAmountExcl || 0).toFixed(2) }}
  178 + </div>
  179 + </div>
  180 + <div class="total-item">
  181 + <div class="total-item-text">
  182 + 总金额
  183 + </div>
  184 + <div class="total-item-price text-red">
  185 + ¥{{ (sumTotal || 0).toFixed(2) }}
  186 + </div>
  187 + </div>
  188 + </div>
  189 + <button class="btn submit" type="primary" @click="onSubmit">提交</button>
  190 + </view>
40 191 <SingleSelectSheet :visible.sync="sheet.visible" :title="sheet.title" :options="sheet.options"
41 192 v-model="sheet.value" @confirm="onSheetConfirm" />
42 193 <RelateSelectSheet :visible.sync="relate.visible" :title="relate.title" :source="relate.source"
... ... @@ -49,8 +200,9 @@
49 200 import SingleSelectSheet from '@/components/single-select/index.vue'
50 201 import RelateSelectSheet from '@/components/relate-select/index.vue'
51 202 import ProductRel from './productRel.vue'
52   -import { getRetailCodeApi } from '@/api/contract'
  203 +import { getRetailCodeApi, createContractApi } from '@/api/contract'
53 204 import { getDicByCodes } from '@/utils/dic'
  205 +import { formatCurrencyToChinese } from '@/utils/common'
54 206
55 207 export default {
56 208 name: 'AddContractRetail',
... ... @@ -64,19 +216,49 @@ export default {
64 216 buyer: '',
65 217 buyerName: '',
66 218 orderDate: '',
  219 + designatedConsignee: '',
  220 + specialTerms: '',
  221 + specialTermsName: '',
  222 + executionStandard: '',
  223 + executionStandardName: '',
  224 + executionStandardRemarks: '',
  225 + includesPackagingFee: false,
  226 + includesPackagingFeeName: '',
  227 + includesTransportFee: false,
  228 + includesTransportFeeName: '',
67 229 unit: '元、公斤、元/公斤'
68 230 },
69 231 supplierList: [],
  232 + specialTermsList: [],
  233 + executionStandardList: [],
  234 + yesNoList: [{ label: '是', value: true }, { label: '否', value: false }],
70 235 sheet: { visible: false, title: '请选择', field: '', options: [], value: '' },
71   - relate: { visible: false, title: '选择', source: '', display: [], multiple: false, rowKey: 'id', selectedKeys: [], fieldKey: '' }
  236 + relate: { visible: false, title: '选择', source: '', display: [], multiple: false, rowKey: 'id', selectedKeys: [], fieldKey: '' },
  237 + sumQuantity: 0,
  238 + sumAmountExcl: 0,
  239 + sumTotal: 0,
  240 + productLineList: [],
  241 + productList: [],
72 242 }
73 243 },
74 244 created() {
75 245 this.loadSuppliers()
  246 + this.loadExtraOptions()
76 247 this.initCode()
77 248 this.form.orderDate = this.formatDate(new Date())
78 249 },
79 250 methods: {
  251 + onProductsChange(products) {
  252 + const list = Array.isArray(products) ? products : []
  253 + const sumQ = list.reduce((acc, it) => acc + (parseFloat(it.quantity) || 0), 0)
  254 + const sumE = list.reduce((acc, it) => acc + (parseFloat(it.amountExcludingTax) || 0), 0)
  255 + const sumT = list.reduce((acc, it) => acc + (parseFloat(it.totalAmount) || 0), 0)
  256 + this.sumQuantity = sumQ
  257 + this.sumAmountExcl = sumE
  258 + this.sumTotal = sumT
  259 + this.form.totalAmountCapital = formatCurrencyToChinese(sumT)
  260 + this.productLineList = list
  261 + },
80 262 formatDate(d) {
81 263 const y = d.getFullYear()
82 264 const m = String(d.getMonth() + 1).padStart(2, '0')
... ... @@ -97,6 +279,21 @@ export default {
97 279 this.supplierList = items.map(it => ({ label: it.name, value: it.code }))
98 280 } catch (e) { this.supplierList = [] }
99 281 },
  282 + async loadExtraOptions() {
  283 + try {
  284 + const results = await getDicByCodes(['CONDITIONS_REQUIRED', 'APPLICABLE_STANDARD', 'CONTRACT_PRODUCT'])
  285 + const c1 = results && results.CONDITIONS_REQUIRED && results.CONDITIONS_REQUIRED.data ? results.CONDITIONS_REQUIRED.data : []
  286 + const c2 = results && results.APPLICABLE_STANDARD && results.APPLICABLE_STANDARD.data ? results.APPLICABLE_STANDARD.data : []
  287 + const c3 = results && results.CONTRACT_PRODUCT && results.CONTRACT_PRODUCT.data ? results.CONTRACT_PRODUCT.data : []
  288 + this.specialTermsList = c1.map(it => ({ label: it.name, value: it.code }))
  289 + this.executionStandardList = c2.map(it => ({ label: it.name, value: it.code }))
  290 + this.productList = c3.map(it => ({ label: it.name, value: it.code }))
  291 + } catch (e) {
  292 + this.specialTermsList = []
  293 + this.executionStandardList = []
  294 + this.productList = []
  295 + }
  296 + },
100 297 displayLabel(field) {
101 298 const m = this.form
102 299 const map = { supplierName: '请选择供方' }
... ... @@ -110,6 +307,8 @@ export default {
110 307 this.sheet = { ...this.sheet, visible: true, title, options, field, value: match ? match.value : '' }
111 308 }
112 309 if (field === 'supplier') setSheet('供方', this.supplierList)
  310 + if (field === 'includesPackagingFee') setSheet('单价中是否已包含包装费', this.yesNoList)
  311 + if (field === 'includesTransportFee') setSheet('单价中是否已包含运费', this.yesNoList)
113 312 },
114 313 onSheetConfirm({ value, label }) {
115 314 const field = this.sheet.field
... ... @@ -133,16 +332,188 @@ export default {
133 332 const first = (items && items.length > 0) ? items[0] : null
134 333 this.form[_fieldKey] = (first && first.id) ? first.id : ''
135 334 this.form[_fieldKey + 'Name'] = (first && first.name) ? first.name : ''
  335 + },
  336 + onRadioSelect(field, nameField, opt) {
  337 + const val = opt && opt.value != null ? opt.value : ''
  338 + const label = opt && opt.label != null ? opt.label : ''
  339 + this.form[field] = val
  340 + this.form[nameField] = label
  341 + if (field === 'executionStandard' && val !== 'OTHER') {
  342 + this.form.executionStandardRemarks = ''
  343 + }
  344 + },
  345 + async onSubmit() {
  346 + if (!this.validateRequired()) return
  347 + const confirmRes = await new Promise(resolve => {
  348 + uni.showModal({ title: '提示', content: '确定新增经销未锁规合同吗?', confirmText: '确定', cancelText: '取消', success: resolve })
  349 + })
  350 + if (!(confirmRes && confirmRes.confirm)) return
  351 + const clean = (obj) => {
  352 + const out = {}
  353 + Object.keys(obj || {}).forEach(k => {
  354 + const v = obj[k]
  355 + const isEmptyString = typeof v === 'string' && v.trim() === ''
  356 + const isUndef = v === undefined || v === null
  357 + const isNaNNumber = typeof v === 'number' && isNaN(v)
  358 + if (!(isEmptyString || isUndef || isNaNNumber)) out[k] = v
  359 + })
  360 + return out
  361 + }
  362 + const lines = (this.productLineList || []).map(it => clean(it))
  363 + const payload = clean({
  364 + ...this.form,
  365 + type: 'DISTRIB_STD',
  366 + sumQuantity: this.sumQuantity,
  367 + sumAmountExcl: this.sumAmountExcl,
  368 + sumTotal: this.sumTotal,
  369 + contractDistributorLineList: lines
  370 + })
  371 + try {
  372 + await createContractApi(payload)
  373 + uni.showToast({ title: '新增成功', icon: 'none' })
  374 + setTimeout(() => { uni.redirectTo({ url: '/pages/contract_retail/index' }) }, 400)
  375 + } catch (e) {
  376 + uni.showToast({ title: '提交失败', icon: 'none' })
  377 + }
  378 + },
  379 + validateRequired() {
  380 + const checks = [
  381 + { key: 'code', label: '编号' },
  382 + { key: 'supplier', label: '供方' },
  383 + { key: 'buyer', label: '需方' },
  384 + { key: 'orderDate', label: '订货日期' }
  385 + ]
  386 + for (const it of checks) {
  387 + const val = this.form[it.key]
  388 + const empty = (val === undefined || val === null || (typeof val === 'string' && val.trim() === '') || (typeof val === 'number' && isNaN(val)))
  389 + if (empty) { uni.showToast({ title: `请先选择${it.label}`, icon: 'none' }); return false }
  390 + }
  391 + if (!Array.isArray(this.productLineList) || this.productLineList.length === 0) {
  392 + uni.showToast({ title: '请至少添加一条产品明细', icon: 'none' }); return false
  393 + }
  394 + for (const [idx, it] of this.productLineList.entries()) {
  395 + if (!it.productName || !it.quantity || !it.unitPrice) {
  396 + uni.showToast({ title: `第${idx + 1}条明细未完整填写`, icon: 'none' }); return false
  397 + }
  398 + }
  399 + return true
136 400 }
137 401 }
138 402 }
139 403
140 404 </script>
141 405 <style lang="scss" scoped>
  406 +.total {
  407 + .total-text {
  408 + font-weight: 600;
  409 + font-size: 32rpx;
  410 + color: rgba(0, 0, 0, 0.9);
  411 + padding-bottom: 28rpx;
  412 + border-bottom: 2rpx solid #E7E7E7;
  413 + }
  414 + .total-item {
  415 + display: flex;
  416 + align-items: center;
  417 + .total-item-text {
  418 + font-weight: 400;
  419 + font-size: 28rpx;
  420 + color: rgba(0, 0, 0, 0.6);
  421 + line-height: 32rpx;
  422 + width: 240rpx;
  423 + padding: 24rpx 0;
  424 + }
  425 + .total-item-price {
  426 + font-weight: 600;
  427 + font-size: 32rpx;
  428 + color: rgba(0, 0, 0, 0.9);
  429 + line-height: 32rpx;
  430 + }
  431 + .text-red {
  432 + color: #D54941;
  433 + }
  434 + }
  435 +
  436 +}
142 437 .page {
  438 + display: flex;
  439 + flex-direction: column;
  440 + height: 100%;
  441 +}
  442 +
  443 +.scroll {
  444 + flex: 1;
  445 + padding: 12rpx 0 480rpx !important;
  446 +}
  447 +
  448 +.footer {
  449 + z-index: 2;
  450 + position: fixed;
  451 + left: 0;
  452 + right: 0;
  453 + bottom: 0;
  454 + padding: 32rpx;
  455 + padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
  456 + background: #fff;
  457 + box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
  458 +
  459 + .btn {
  460 + height: 80rpx;
  461 + line-height: 80rpx;
  462 + border-radius: 12rpx;
  463 + font-size: 32rpx;
  464 + }
  465 +
  466 + .submit {
  467 + background: $theme-primary;
  468 + color: #fff;
  469 + }
  470 +}
  471 +
  472 +.group {
  473 + background: #fff;
  474 + padding: 24rpx 32rpx;
  475 + margin-bottom: 20rpx;
  476 +}
  477 +
  478 +.group-title {
  479 + color: rgba(0, 0, 0, 0.9);
  480 + font-size: 32rpx;
  481 + padding-bottom: 16rpx;
  482 +}
  483 +
  484 +.radio-list {
143 485 display: flex;
144 486 flex-direction: column;
145   - height: 100%;
  487 +}
  488 +
  489 +.radio-item {
  490 + display: flex;
  491 + align-items: center;
  492 + padding: 24rpx 0;
  493 + border-bottom: 1rpx solid #f0f0f0;
  494 +}
  495 +
  496 +.radio-item:last-child {
  497 + border-bottom: none;
  498 +}
  499 +
  500 +.radio {
  501 + width: 32rpx;
  502 + height: 32rpx;
  503 + border-radius: 50%;
  504 + border: 2rpx solid #DCDCDC;
  505 + margin-right: 20rpx;
  506 + box-sizing: border-box;
  507 +}
  508 +
  509 +.radio.checked {
  510 + background: $theme-primary;
  511 + border-color: $theme-primary;
  512 +}
  513 +
  514 +.radio-item .label {
  515 + font-size: 32rpx;
  516 + color: rgba(0, 0, 0, 0.9);
146 517 }
147 518
148 519 .scroll {
... ... @@ -150,6 +521,29 @@ export default {
150 521 padding: 12rpx 0 160rpx;
151 522 }
152 523
  524 +.quality {
  525 + background-color: #fff;
  526 + display: flex;
  527 + align-items: center;
  528 + padding: 24rpx 32rpx;
  529 + border-bottom: 1rpx solid #f0f0f0;
  530 + margin-top: 20rpx;
  531 +
  532 + .title {
  533 + font-size: 32rpx;
  534 + color: rgba(0, 0, 0, 0.9);
  535 + font-weight: 600;
  536 + }
  537 +
  538 + .opCollapse {
  539 + color: rgba(0, 0, 0, 0.6);
  540 + width: 24rpx;
  541 + height: 24rpx;
  542 + margin-right: 16rpx;
  543 + margin-top: 8rpx;
  544 + }
  545 +}
  546 +
153 547 ::v-deep .uni-list {
154 548 .uni-easyinput {
155 549 display: flex;
... ... @@ -159,6 +553,10 @@ export default {
159 553 }
160 554 }
161 555
  556 + .uni-input-placeholder {
  557 + z-index: 1;
  558 + }
  559 +
162 560 .uni-input-input {
163 561 background-color: #ffffff;
164 562 }
... ... @@ -186,6 +584,7 @@ export default {
186 584
187 585 &__content {
188 586 border: none;
  587 + background-color: #ffffff !important;
189 588
190 589 &-input {
191 590 padding-left: 0 !important;
... ... @@ -240,4 +639,4 @@ export default {
240 639 }
241 640 }
242 641 }
243   -</style>
\ No newline at end of file
  642 +</style>
... ...
  1 +<template>
  2 + <view class="page">
  3 + <scroll-view class="scroll" scroll-y>
  4 + <view class="detail-page">
  5 + <view class="section">
  6 + <text class="row customer">{{ detail.code }}</text>
  7 + <view class="row"><text class="label">供方</text><text class="value">{{ detail.supplierName || '-'
  8 + }}</text></view>
  9 + <view class="row"><text class="label">需方</text><text class="value">{{ detail.buyerName || '-'
  10 + }}</text></view>
  11 + <view class="row"><text class="label">订货日期</text><text class="value">{{ detail.orderDate }}</text>
  12 + </view>
  13 + <view class="row"><text class="label">单位</text><text class="value">{{ detail.unit }}</text></view>
  14 + </view>
  15 +
  16 + <view class="section1">
  17 + <ProductRel mode="view" :value="productList" />
  18 + </view>
  19 +
  20 + <view class="section">
  21 + <view class="row"><text class="label">合计人民币金额(大写)</text><text class="value">{{
  22 + detail.totalAmountCapital || '-' }}</text></view>
  23 + <view class="row"><text class="label">交付定金、数额、时间</text><text class="value">{{ detail.depositInfo ||
  24 + '-' }}</text></view>
  25 + <view class="row"><text class="label">包装要求</text><text class="value">{{ detail.packagingRequirements
  26 + || '-' }}</text></view>
  27 + <view class="row"><text class="label">付款方式、付款期限</text><text class="value">{{ detail.paymentTerms ||
  28 + '-' }}</text></view>
  29 + <view class="row"><text class="label">运输方式</text><text class="value">{{ detail.transportMode || '-'
  30 + }}</text></view>
  31 + <view class="row"><text class="label">目的地</text><text class="value">{{ detail.destinationId || '-'
  32 + }}</text></view>
  33 + <view class="row"><text class="label">单价中是否已包含包装费</text><text class="value">{{
  34 + detail.includesPackagingFeeName || '-' }}</text></view>
  35 + <view class="row"><text class="label">单价中是否已包含运费</text><text class="value">{{
  36 + detail.includesTransportFeeName || '-' }}</text></view>
  37 + <view class="row"><text class="label">需方指定收货人</text><text class="value">{{
  38 + detail.designatedConsignee || '-' }}</text></view>
  39 +
  40 + <view class="row"><text class="label">特别条款要求</text><text class="value">{{ detail.specialTermsName ||
  41 + '-' }}</text></view>
  42 + <view class="row"><text class="label">执行标准</text><text class="value">{{ detail.executionStandardName
  43 + || '-' }}</text></view>
  44 + <view class="row" v-if="detail.executionStandard === 'OTHER'"><text class="label">其他</text><text
  45 + class="value">{{ detail.executionStandardRemarks || '-' }}</text></view>
  46 + <view class="row"><text class="label">特别说明</text><text class="value">{{ detail.specialInstructions
  47 + || '-' }}</text></view>
  48 + <view class="row"><text class="label">备注</text><text class="value">{{ detail.remarks || '-'
  49 + }}</text></view>
  50 + </view>
  51 +
  52 + <view class="section">
  53 + <view class="row"><text class="label">规范性合同</text><text class="value">{{ detail.standardFileName || '-'
  54 + }}</text></view>
  55 + <view class="row"><text class="label">合同是否规范</text><text class="value">{{ detail.standardStandardized ? '是' : '否'
  56 + }}</text></view>
  57 + </view>
  58 +
  59 + <view class="section">
  60 + <text class="row customer">具体质量要求</text>
  61 + <view class="row"><text class="label">件重条头</text><text class="value">{{ detail.pieceWeightHead ||
  62 + '-' }}</text></view>
  63 + <view class="row"><text class="label">表面</text><text class="value">{{ detail.surface || '-'
  64 + }}</text></view>
  65 + <view class="row"><text class="label">公差</text><text class="value">{{ detail.tolerance || '-'
  66 + }}</text></view>
  67 + <view class="row"><text class="label">性能</text><text class="value">{{ detail.performance || '-'
  68 + }}</text></view>
  69 + <view class="row"><text class="label">成分</text><text class="value">{{ detail.component || '-'
  70 + }}</text></view>
  71 + <view class="row"><text class="label">包装</text><text class="value">{{ detail.packaging || '-'
  72 + }}</text></view>
  73 + </view>
  74 + </view>
  75 + </scroll-view>
  76 + </view>
  77 +</template>
  78 +
  79 +<script>
  80 +import { getContractApi } from '@/api/contract'
  81 +import ProductRel from './productRel.vue'
  82 +export default {
  83 + name: 'ContractRetailDetail',
  84 + components: { ProductRel },
  85 + data() {
  86 + return {
  87 + id: '',
  88 + detail: {
  89 + code: '',
  90 + supplier: '',
  91 + supplierName: '',
  92 + buyer: '',
  93 + buyerName: '',
  94 + orderDate: '',
  95 + unit: '',
  96 + designatedConsignee: '',
  97 + specialTerms: '',
  98 + specialTermsName: '',
  99 + executionStandard: '',
  100 + executionStandardName: '',
  101 + executionStandardRemarks: '',
  102 + includesPackagingFee: false,
  103 + includesPackagingFeeName: '',
  104 + includesTransportFee: false,
  105 + includesTransportFeeName: '',
  106 + totalAmountCapital: '',
  107 + depositInfo: '',
  108 + packagingRequirements: '',
  109 + paymentTerms: '',
  110 + transportMode: '',
  111 + destinationId: '',
  112 + specialInstructions: '',
  113 + remarks: '',
  114 + pieceWeightHead: '',
  115 + surface: '',
  116 + tolerance: '',
  117 + performance: '',
  118 + component: '',
  119 + packaging: ''
  120 + },
  121 + productList: []
  122 + }
  123 + },
  124 + onLoad(options) {
  125 + const id = options && options.id ? options.id : ''
  126 + this.id = id
  127 + this.loadDetail()
  128 + },
  129 + methods: {
  130 + async loadDetail() {
  131 + if (!this.id) return
  132 + try {
  133 + const res = await getContractApi(this.id)
  134 + const data = res && res.data ? res.data : {}
  135 + const includesPackagingFeeName = data.includesPackagingFeeName || (data.includesPackagingFee ? '是' : '否')
  136 + const includesTransportFeeName = data.includesTransportFeeName || (data.includesTransportFee ? '是' : '否')
  137 + this.detail = { ...this.detail, ...data, includesPackagingFeeName, includesTransportFeeName }
  138 + const lines = Array.isArray(data.contractDistributorLineList) ? data.contractDistributorLineList : []
  139 + this.productList = lines
  140 + } catch (e) {
  141 + this.detail = { ...this.detail }
  142 + this.productList = []
  143 + }
  144 + }
  145 + }
  146 +}
  147 +</script>
  148 +
  149 +<style lang="scss" scoped>
  150 +.page {
  151 + display: flex;
  152 + flex-direction: column;
  153 + height: 100%;
  154 +}
  155 +
  156 +.scroll {
  157 + flex: 1;
  158 + padding: 8rpx 0 144rpx;
  159 +}
  160 +
  161 +.detail-page {
  162 + background: #f3f3f3;
  163 +}
  164 +
  165 +.section {
  166 + padding: 32rpx;
  167 + background: #fff;
  168 + margin-bottom: 20rpx;
  169 +}
  170 +.section1 {
  171 + background: #fff;
  172 + margin-bottom: 20rpx;
  173 +}
  174 +
  175 +.row {
  176 + display: flex;
  177 + margin-bottom: 20rpx;
  178 + align-items: center;
  179 +}
  180 +
  181 +.row:last-child {
  182 + margin-bottom: 0;
  183 +}
  184 +
  185 +.label {
  186 + width: 280rpx;
  187 + color: rgba(0, 0, 0, 0.6);
  188 + font-size: 28rpx;
  189 +}
  190 +
  191 +.value {
  192 + flex: 1;
  193 + text-align: right;
  194 + color: rgba(0, 0, 0, 0.9);
  195 + font-size: 28rpx;
  196 +}
  197 +
  198 +.customer {
  199 + font-weight: 600;
  200 + font-size: 36rpx;
  201 + color: rgba(0, 0, 0, 0.9);
  202 + padding-bottom: 12rpx;
  203 +}
  204 +</style>
\ No newline at end of file
... ...
... ... @@ -46,7 +46,7 @@
46 46 @error="onCardError"
47 47 >
48 48 <template v-slot="{ item }">
49   - <view class="card">
  49 + <view class="card" @click="goDetail(item)">
50 50 <view class="card-header">
51 51 <text class="title omit2">{{ item.buyerName }}</text>
52 52 <text v-if="item.status === 'STANDARD'" :class="['status']" :style="{ background: statusMap[item.shippingStatusName] }">{{ item.shippingStatusName }}</text>
... ... @@ -203,6 +203,11 @@ export default {
203 203 onAdd() {
204 204 uni.navigateTo({ url: '/pages/contract_retail/add' })
205 205 },
  206 + goDetail(item) {
  207 + const id = item && (item.id || item.contractId)
  208 + if (!id) return uni.showToast({ title: '缺少ID,无法查看详情', icon: 'none' })
  209 + uni.navigateTo({ url: `/pages/contract_retail/detail?id=${id}` })
  210 + },
206 211 fetchList({ pageIndex, pageSize, query, extra }) {
207 212 console.log('fetchList', pageIndex, pageSize, query, extra)
208 213 const params = { pageIndex, pageSize, ...extra, ...query }
... ...
... ... @@ -16,9 +16,9 @@
16 16 <view v-if="mode === 'add'" class="add-list">
17 17 <view v-for="(item, idx) in items" :key="idx" class="block">
18 18 <uni-list v-show="item.collapsed">
19   - <uni-list-item title="产品名称">
20   - <template v-slot:footer>
21   - <uni-easyinput v-model="item.name" :inputBorder="false" placeholder="请输入产品名称" />
  19 + <uni-list-item class="select-item" :class="item.productName ? 'is-filled' : 'is-empty'" clickable @click="openProductSheet(idx)" :rightText="item.productName || '请选择产品名称'" showArrow>
  20 + <template v-slot:body>
  21 + <view class="item-title"><text>产品名称</text></view>
22 22 </template>
23 23 </uni-list-item>
24 24 <uni-list-item title="行业">
... ... @@ -33,9 +33,9 @@
33 33 </uni-list-item>
34 34 </uni-list>
35 35 <uni-list v-show="!item.collapsed">
36   - <uni-list-item title="产品名称">
37   - <template v-slot:footer>
38   - <uni-easyinput v-model="item.name" :inputBorder="false" placeholder="请输入产品名称" />
  36 + <uni-list-item class="select-item" :class="item.productName ? 'is-filled' : 'is-empty'" clickable @click="openProductSheet(idx)" :rightText="item.productName || '请选择产品名称'" showArrow>
  37 + <template v-slot:body>
  38 + <view class="item-title"><text>产品名称</text></view>
39 39 </template>
40 40 </uni-list-item>
41 41 <uni-list-item title="行业">
... ... @@ -109,28 +109,27 @@
109 109 </uni-list-item>
110 110 <uni-list-item title="数量">
111 111 <template v-slot:footer>
112   - <uni-easyinput v-model="item.quantity" :inputBorder="false" placeholder="请输入数量" />
  112 + <uni-easyinput v-model="item.quantity" type="number" :inputBorder="false" placeholder="请输入数量" @input="onImmediateChange(idx)" @blur="onNumberBlur(idx, 'quantity', 0)" />
113 113 </template>
114 114 </uni-list-item>
115 115 <uni-list-item title="单价">
116 116 <template v-slot:footer>
117   - <uni-easyinput v-model="item.unitPrice" :inputBorder="false" placeholder="请输入单价" />
  117 + <uni-easyinput v-model="item.unitPrice" type="number" :inputBorder="false" placeholder="请输入单价" @input="onImmediateChange(idx)" @blur="onNumberBlur(idx, 'unitPrice', 0)" />
118 118 </template>
119 119 </uni-list-item>
120 120 <uni-list-item title="不含税金额">
121 121 <template v-slot:footer>
122   - <uni-easyinput v-model="item.amountExcludingTax" :inputBorder="false"
123   - placeholder="请输入不含税金额" />
  122 + <uni-easyinput v-model="item.amountExcludingTax" type="number" :inputBorder="false" disabled placeholder="自动计算" />
124 123 </template>
125 124 </uni-list-item>
126 125 <uni-list-item title="总金额">
127 126 <template v-slot:footer>
128   - <uni-easyinput v-model="item.totalAmount" :inputBorder="false" placeholder="请输入总金额" />
  127 + <uni-easyinput v-model="item.totalAmount" type="number" :inputBorder="false" disabled placeholder="自动计算" />
129 128 </template>
130 129 </uni-list-item>
131 130 <uni-list-item title="发货日期">
132 131 <template v-slot:footer>
133   - <uni-datetime-picker type="date" v-model="item.orderDate" />
  132 + <uni-datetime-picker type="date" v-model="item.orderDate" @change="onDateChange(idx, $event)" />
134 133 </template>
135 134 </uni-list-item>
136 135 </uni-list>
... ... @@ -148,7 +147,7 @@
148 147
149 148 <view v-else class="view-list" v-show="!collapsedView">
150 149 <view v-for="(item, idx) in items" :key="'v-' + idx" class="card">
151   - <view class="row"><text class="label">产品名称</text><text class="value">{{ item.name }}</text></view>
  150 + <view class="row"><text class="label">产品名称</text><text class="value">{{ item.productName }}</text></view>
152 151 <view class="row"><text class="label">行业</text><text class="value">{{ item.industry }}</text></view>
153 152 <view class="row"><text class="label">牌号</text><text class="value">{{ item.brand }}</text></view>
154 153 <view class="row"><text class="label">品质</text><text class="value">{{ item.quality }}</text></view>
... ... @@ -166,20 +165,35 @@
166 165 <view class="row"><text class="label">发货日期</text><text class="value">{{ item.orderDate }}</text></view>
167 166 </view>
168 167 </view>
  168 + <SingleSelectSheet :visible.sync="sheet.visible" :title="sheet.title" :options="sheet.options" v-model="sheet.value" @confirm="onProductConfirm" />
169 169 </view>
170 170 </template>
171 171 <script>
  172 +import SingleSelectSheet from '@/components/single-select/index.vue'
172 173 export default {
173 174 name: 'ProductRel',
174 175 props: {
175 176 mode: { type: String, default: 'add' },
176 177 value: { type: Array, default: () => [] },
177   - max: { type: Number, default: 8 }
  178 + max: { type: Number, default: 8 },
  179 + orderDateBase: { type: String, default: '' },
  180 + options: { type: Array, default: () => [] }
178 181 },
  182 + components: { SingleSelectSheet },
179 183 data() {
180 184 return {
181 185 items: [],
182   - collapsedView: false
  186 + collapsedView: false,
  187 + sheet: { visible: false, title: '请选择产品', options: [], value: '', idx: -1 }
  188 + }
  189 + },
  190 + computed: {
  191 + selectOptions() {
  192 + const list = Array.isArray(this.options) ? this.options : []
  193 + return list.map(o => ({
  194 + label: o.label != null ? o.label : (o.text != null ? o.text : (o.name != null ? o.name : '')),
  195 + value: o.value != null ? o.value : (o.id != null ? o.id : o.productId)
  196 + }))
183 197 }
184 198 },
185 199 watch: {
... ... @@ -198,10 +212,34 @@ export default {
198 212 created() {
199 213 const init = Array.isArray(this.value) && this.value.length > 0 ? this.value.map(v => ({ ...this.defaultItem(), ...v, collapsed: true })) : [{ ...this.defaultItem(), collapsed: false }]
200 214 this.items = init
  215 + this.recalculateAll()
201 216 },
202 217 methods: {
203 218 defaultItem() {
204   - return { name: '', industry: '', brand: '', quality: '', thickness: '', thicknessTolPos: '', thicknessTolNeg: '', width: '', widthTolPos: '', widthTolNeg: '', length: '', lengthTolPos: '', lengthTolNeg: '', status: '', quantity: '', unitPrice: '', amountExcludingTax: '', totalAmount: '', orderDate: '' }
  219 + return { productId: '', productName: '', industry: '', brand: '', quality: '', thickness: '', thicknessTolPos: '', thicknessTolNeg: '', width: '', widthTolPos: '', widthTolNeg: '', length: '', lengthTolPos: '', lengthTolNeg: '', status: '', quantity: 0, unitPrice: 0, amountExcludingTax: 0, totalAmount: 0, orderDate: '' }
  220 + },
  221 + onImmediateChange(idx) {
  222 + this.$nextTick(() => this.recalculate(idx))
  223 + },
  224 + toNumber(val) {
  225 + if (typeof val === 'number') return isNaN(val) ? 0 : val
  226 + const n = parseFloat(String(val).replace(/[^0-9.\-]/g, ''))
  227 + return isNaN(n) ? 0 : n
  228 + },
  229 + round(val, digits = 2) {
  230 + const n = Number(val)
  231 + if (isNaN(n)) return 0
  232 + const m = Math.pow(10, digits)
  233 + return Math.round(n * m) / m
  234 + },
  235 + onNumberBlur(idx, field, digits) {
  236 + const it = this.items[idx]
  237 + if (!it) return
  238 + const num = this.toNumber(it[field])
  239 + const rounded = this.round(num, digits)
  240 + it[field] = rounded
  241 + this.$set(this.items, idx, it)
  242 + this.recalculate(idx)
205 243 },
206 244 formatCurrency(val) {
207 245 if (val == null || val === '') return ''
... ... @@ -211,10 +249,40 @@ export default {
211 249 return `${pre}${fixed}`
212 250 },
213 251 specOf(item) {
214   - const t = [item.thickness, item.thicknessTolPos, item.thicknessTolNeg].filter(Boolean).join('/')
215   - const w = [item.width, item.widthTolPos, item.widthTolNeg].filter(Boolean).join('/')
216   - const l = [item.length, item.lengthTolPos, item.lengthTolNeg].filter(Boolean).join('/')
217   - return [t, w, l].filter(Boolean).join(' × ')
  252 + const t = [item.thickness, item.thicknessTolPos, item.thicknessTolNeg].filter(Boolean).join('/')
  253 + const w = [item.width, item.widthTolPos, item.widthTolNeg].filter(Boolean).join('/')
  254 + const l = [item.length, item.lengthTolPos, item.lengthTolNeg].filter(Boolean).join('/')
  255 + return [t, w, l].filter(Boolean).join(' × ')
  256 + },
  257 + openProductSheet(idx) {
  258 + const opts = this.selectOptions
  259 + const current = this.items[idx] && this.items[idx].productId
  260 + const match = opts.find(o => o.value === current)
  261 + this.sheet = { ...this.sheet, visible: true, title: '请选择产品', options: opts, idx, value: match ? match.value : '' }
  262 + },
  263 + onProductConfirm({ value, label }) {
  264 + const idx = this.sheet.idx
  265 + const it = this.items[idx]
  266 + if (!it) { this.sheet.visible = false; return }
  267 + it.productId = value
  268 + it.productName = label || ''
  269 + this.$set(this.items, idx, it)
  270 + this.sheet.visible = false
  271 + this.emitChange()
  272 + },
  273 + recalculate(idx) {
  274 + const TAX_RATE = 0.13
  275 + const it = this.items[idx]
  276 + if (!it) return
  277 + const qty = this.toNumber(it.quantity)
  278 + const price = this.toNumber(it.unitPrice)
  279 + const total = this.round(qty * price, 2)
  280 + const excl = this.round(total / (1 + TAX_RATE), 2)
  281 + const next = { ...it, totalAmount: total, amountExcludingTax: excl }
  282 + this.$set(this.items, idx, next)
  283 + },
  284 + recalculateAll() {
  285 + for (let i = 0; i < this.items.length; i++) this.recalculate(i)
218 286 },
219 287 onAdd() {
220 288 if (this.items.length >= this.max) return uni.showToast({ title: `最多添加${this.max}个`, icon: 'none' })
... ... @@ -239,6 +307,21 @@ export default {
239 307 this.$emit('update:value', out)
240 308 this.$emit('change', out)
241 309 },
  310 + onDateChange(idx, e) {
  311 + const it = this.items[idx]
  312 + if (!it) return
  313 + const val = typeof e === 'string' ? e : (e && e.detail && e.detail.value) ? e.detail.value : it.orderDate
  314 + const dateStr = String(val).slice(0, 10)
  315 + const base = this.orderDateBase ? new Date(this.orderDateBase) : null
  316 + const d = new Date(dateStr)
  317 + if (base && !isNaN(d.getTime()) && d.getTime() < base.getTime()) {
  318 + uni.showToast({ title: '发货日期不得早于订货日期', icon: 'none' })
  319 + it.orderDate = this.orderDateBase
  320 + } else {
  321 + it.orderDate = dateStr
  322 + }
  323 + this.$set(this.items, idx, it)
  324 + },
242 325 toggleViewCollapse() {
243 326 this.collapsedView = !this.collapsedView
244 327 }
... ... @@ -269,6 +352,7 @@ export default {
269 352 .title {
270 353 font-size: 32rpx;
271 354 color: rgba(0, 0, 0, 0.9);
  355 + font-weight: 600;
272 356 }
273 357
274 358 .ops {
... ... @@ -295,7 +379,7 @@ export default {
295 379 width: 24rpx;
296 380 height: 24rpx;
297 381 margin-right: 16rpx;
298   - margin-top: 16rpx;
  382 + margin-top: 8rpx;
299 383 }
300 384
301 385
... ... @@ -344,7 +428,7 @@ export default {
344 428 }
345 429
346 430 .uni-input-placeholder {
347   - z-index: 2;
  431 + // z-index: 2;
348 432 }
349 433 }
350 434 }
... ...
... ... @@ -51,4 +51,59 @@ export function tansParams(params) {
51 51 }
52 52 }
53 53 return result
  54 +}
  55 +
  56 +// 金额转人民币大写
  57 +export function formatCurrencyToChinese(num) {
  58 + // 处理空值或非数字
  59 + if (!num || isNaN(Number(num))) return '零元整';
  60 +
  61 + // 转换为数字并保留2位小数(避免分以下的单位)
  62 + const n = parseFloat(Number(num).toFixed(2));
  63 + if (n === 0) return '零元整';
  64 +
  65 + const digit = ['零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖'];
  66 + const unit = ['', '拾', '佰', '仟', '万', '拾', '佰', '仟', '亿', '拾', '佰', '仟'];
  67 + const decUnit = ['角', '分'];
  68 + let str = '';
  69 +
  70 + // 处理整数部分
  71 + const integer = Math.floor(n);
  72 + // 处理小数部分(角、分)
  73 + const decimal = Math.round((n - integer) * 100);
  74 + const hasDecimal = decimal > 0;
  75 +
  76 + // 转换整数部分
  77 + if (integer > 0) {
  78 + let intStr = integer.toString();
  79 + for (let i = 0; i < intStr.length; i++) {
  80 + const digitNum = parseInt(intStr[i]);
  81 + const unitIndex = intStr.length - 1 - i;
  82 + if (digitNum !== 0) {
  83 + str += digit[digitNum] + unit[unitIndex];
  84 + } else {
  85 + // 避免连续多个零
  86 + if (intStr[i - 1] !== '0' || (unitIndex % 4 === 0 && unitIndex > 0)) {
  87 + str += digit[0];
  88 + }
  89 + }
  90 + }
  91 + str += '元';
  92 + } else {
  93 + str += '零元';
  94 + }
  95 +
  96 + // 转换小数部分
  97 + if (hasDecimal) {
  98 + const jiao = Math.floor(decimal / 10);
  99 + const fen = decimal % 10;
  100 + if (jiao > 0) str += digit[jiao] + decUnit[0];
  101 + if (fen > 0) str += digit[fen] + decUnit[1];
  102 + } else {
  103 + str += '整';
  104 + }
  105 +
  106 + // 处理特殊情况(如"零元整"已在开头处理,此处处理多零情况)
  107 + return str.replace(/零+/g, '零').replace(/零([万亿])/g, '$1').replace(/零元/, '元')
  108 + .replace(/零角零分$/, '整').replace(/零分$/, '整');
54 109 }
\ No newline at end of file
... ...