Commit 7b58cc8d7d484be051938c922ea36813a1d2337b

Authored by gesilong
2 parents a0aba82d ec6b490d

Merge branch 'cjerp-2.0-price' into test_cjerp

  1 +import request from '@/utils/request'
  2 +import { ContentTypeEnum } from '@/utils/httpEnum'
  3 +
  4 +export function salesmanQuotationQueryApi(params) {
  5 + return request({
  6 + url: '/procurement/salesmanQuotation/query',
  7 + method: 'get',
  8 + params
  9 + })
  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 +}
  1 +import request from '@/utils/request'
  2 +import { ContentTypeEnum } from '@/utils/httpEnum'
  3 +
  4 +export function salesmanQuotationPeerQueryApi(params) {
  5 + return request({
  6 + url: '/procurement/salesmanQuotationPeer/query',
  7 + method: 'get',
  8 + params
  9 + })
  10 +}
  11 +
  12 +export function salesmanQuotationPeerSaveApi(data) {
  13 + return request({
  14 + url: '/procurement/salesmanQuotationPeer',
  15 + method: 'post',
  16 + data,
  17 + contentType: ContentTypeEnum.JSON
  18 + })
  19 +}
  20 +
  21 +export function salesmanQuotationPeerGetApi(id) {
  22 + return request({
  23 + url: '/procurement/salesmanQuotationPeer',
  24 + method: 'get',
  25 + params: { id }
  26 + })
  27 +}
  28 +
  29 +export function salesmanQuotationPeerUpdateApi(data) {
  30 + return request({
  31 + url: '/procurement/salesmanQuotationPeer',
  32 + method: 'put',
  33 + data,
  34 + contentType: ContentTypeEnum.JSON
  35 + })
  36 +}
  37 +
  38 +export function salesmanQuotationPeerDeleteApi(id) {
  39 + return request({
  40 + url: '/procurement/salesmanQuotationPeer',
  41 + method: 'delete',
  42 + params: { id }
  43 + })
  44 +}
  1 +import request from '@/utils/request'
  2 +import { ContentTypeEnum } from '@/utils/httpEnum'
  3 +
  4 +export function supplierQuotationDealQueryApi(params) {
  5 + return request({
  6 + url: '/procurement/supplierQuotationDeal/query',
  7 + method: 'get',
  8 + params
  9 + })
  10 +}
  11 +
  12 +export function supplierQuotationDealGetApi(id) {
  13 + return request({
  14 + url: '/procurement/supplierQuotationDeal',
  15 + method: 'get',
  16 + params: { id }
  17 + })
  18 +}
  19 +
  20 +// 可分配人员
  21 +export function supplierQuotationDealAssignUsersApi(params) {
  22 + return request({
  23 + url: '/procurement/supplierQuotationDeal/assignUsers',
  24 + method: 'get',
  25 + params
  26 + })
  27 +}
  28 +
  29 +// 分配
  30 +export function supplierQuotationDealAssignApi(data) {
  31 + return request({
  32 + url: '/procurement/supplierQuotationDeal/assign',
  33 + method: 'put',
  34 + data,
  35 + contentType: ContentTypeEnum.JSON
  36 + })
  37 +}
  1 +import request from '@/utils/request'
  2 +import { ContentTypeEnum } from '@/utils/httpEnum'
  3 +
  4 +export function supplierQuotationProjectQueryApi(params) {
  5 + return request({
  6 + url: '/procurement/supplierQuotationProject/query',
  7 + method: 'get',
  8 + params
  9 + })
  10 +}
  11 +
  12 +// 新增报价项目
  13 +export function supplierQuotationProjectSaveApi(data) {
  14 + return request({
  15 + url: '/procurement/supplierQuotationProject',
  16 + method: 'post',
  17 + data,
  18 + contentType: ContentTypeEnum.JSON
  19 + })
  20 +}
  21 +
  22 +// 详情
  23 +export function supplierQuotationProjectDetailApi(params) {
  24 + return request({
  25 + url: '/procurement/supplierQuotationProject',
  26 + method: 'get',
  27 + params
  28 + })
  29 +}
  30 +
  31 +// 修改
  32 +export function supplierQuotationProjectUpdateApi(data) {
  33 + return request({
  34 + url: '/procurement/supplierQuotationProject',
  35 + method: 'put',
  36 + data,
  37 + contentType: ContentTypeEnum.JSON
  38 + })
  39 +}
  40 +
  41 +// 报价
  42 +export function supplierQuotationProjectQuoteApi(data) {
  43 + return request({
  44 + url: '/procurement/supplierQuotationProjectQuote',
  45 + method: 'post',
  46 + data,
  47 + contentType: ContentTypeEnum.JSON
  48 + })
  49 +}
  50 +
  51 +// 发布(更新状态)
  52 +export function supplierQuotationProjectUpdateStatusApi(params) {
  53 + return request({
  54 + url: '/procurement/supplierQuotationProject/updateStatus',
  55 + method: 'get',
  56 + params
  57 + })
  58 +}
@@ -276,6 +276,143 @@ @@ -276,6 +276,143 @@
276 } 276 }
277 }, 277 },
278 { 278 {
  279 + "path": "pages/sales-price/index",
  280 + "style": {
  281 + "navigationBarTitleText": "业务员报价单",
  282 + "navigationBarBackgroundColor": "#ffffff",
  283 + "navigationBarTextStyle": "black"
  284 + }
  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 + },
  310 + {
  311 + "path": "pages/sales-price/copy",
  312 + "style": {
  313 + "navigationBarTitleText": "复制业务员报价单",
  314 + "navigationBarBackgroundColor": "#ffffff",
  315 + "navigationBarTextStyle": "black"
  316 + }
  317 + },
  318 +
  319 + {
  320 + "path": "pages/supplier-price/index",
  321 + "style": {
  322 + "navigationBarTitleText": "报价项目",
  323 + "navigationBarBackgroundColor": "#ffffff",
  324 + "navigationBarTextStyle": "black"
  325 + }
  326 + },
  327 + {
  328 + "path": "pages/supplier-price/add",
  329 + "style": {
  330 + "navigationBarTitleText": "新增报价项目",
  331 + "navigationBarBackgroundColor": "#ffffff",
  332 + "navigationBarTextStyle": "black"
  333 + }
  334 + },
  335 + {
  336 + "path": "pages/supplier-price/detail",
  337 + "style": {
  338 + "navigationBarTitleText": "查看报价项目",
  339 + "navigationBarBackgroundColor": "#ffffff",
  340 + "navigationBarTextStyle": "black"
  341 + }
  342 + },
  343 + {
  344 + "path": "pages/supplier-price/modify",
  345 + "style": {
  346 + "navigationBarTitleText": "编辑报价项目",
  347 + "navigationBarBackgroundColor": "#ffffff",
  348 + "navigationBarTextStyle": "black"
  349 + }
  350 + },
  351 + {
  352 + "path": "pages/supplier-price/quote",
  353 + "style": {
  354 + "navigationBarTitleText": "报价",
  355 + "navigationBarBackgroundColor": "#ffffff",
  356 + "navigationBarTextStyle": "black"
  357 + }
  358 + },
  359 + {
  360 + "path": "pages/quote-manage/index",
  361 + "style": {
  362 + "navigationBarTitleText": "报价管理",
  363 + "navigationBarBackgroundColor": "#ffffff",
  364 + "navigationBarTextStyle": "black"
  365 + }
  366 + },
  367 + {
  368 + "path": "pages/quote-manage/detail",
  369 + "style": {
  370 + "navigationBarTitleText": "报价管理详情",
  371 + "navigationBarBackgroundColor": "#ffffff",
  372 + "navigationBarTextStyle": "black"
  373 + }
  374 + },
  375 + {
  376 + "path": "pages/quote-manage/assign",
  377 + "style": {
  378 + "navigationBarTitleText": "分配负责人",
  379 + "navigationBarBackgroundColor": "#ffffff",
  380 + "navigationBarTextStyle": "black"
  381 + }
  382 + },
  383 + {
  384 + "path": "pages/peer-info/index",
  385 + "style": {
  386 + "navigationBarTitleText": "同行信息",
  387 + "navigationBarBackgroundColor": "#ffffff",
  388 + "navigationBarTextStyle": "black"
  389 + }
  390 + },
  391 + {
  392 + "path": "pages/peer-info/add",
  393 + "style": {
  394 + "navigationBarTitleText": "新增同行信息",
  395 + "navigationBarBackgroundColor": "#ffffff",
  396 + "navigationBarTextStyle": "black"
  397 + }
  398 + },
  399 + {
  400 + "path": "pages/peer-info/modify",
  401 + "style": {
  402 + "navigationBarTitleText": "编辑同行信息",
  403 + "navigationBarBackgroundColor": "#ffffff",
  404 + "navigationBarTextStyle": "black"
  405 + }
  406 + },
  407 + {
  408 + "path": "pages/peer-info/detail",
  409 + "style": {
  410 + "navigationBarTitleText": "查看同行信息",
  411 + "navigationBarBackgroundColor": "#ffffff",
  412 + "navigationBarTextStyle": "black"
  413 + }
  414 + },
  415 + {
279 "path": "pages/order_list/index", 416 "path": "pages/order_list/index",
280 "style": { 417 "style": {
281 "navigationBarTitleText": "订货单列表", 418 "navigationBarTitleText": "订货单列表",
@@ -94,7 +94,7 @@ @@ -94,7 +94,7 @@
94 <template v-slot:footer> 94 <template v-slot:footer>
95 <uni-easyinput v-model="item.unitPrice" type="digit" :inputBorder="false" 95 <uni-easyinput v-model="item.unitPrice" type="digit" :inputBorder="false"
96 placeholder="请输入单价" @input="onImmediateChange(idx)" 96 placeholder="请输入单价" @input="onImmediateChange(idx)"
97 - @blur="onNumberBlur(idx, 'unitPrice', 0)" /> 97 + @blur="onNumberBlur(idx, 'unitPrice', 9)" />
98 </template> 98 </template>
99 </uni-list-item> 99 </uni-list-item>
100 <!-- <uni-list-item title="不含税金额"> 100 <!-- <uni-list-item title="不含税金额">
@@ -86,7 +86,7 @@ @@ -86,7 +86,7 @@
86 </uni-list-item> 86 </uni-list-item>
87 <uni-list-item title="单价"> 87 <uni-list-item title="单价">
88 <template v-slot:footer> 88 <template v-slot:footer>
89 - <uni-easyinput v-model="item.unitPrice" type="digit" :inputBorder="false" placeholder="请输入单价" @input="onImmediateChange(idx)" @blur="onNumberBlur(idx, 'unitPrice', 0)" /> 89 + <uni-easyinput v-model="item.unitPrice" type="digit" :inputBorder="false" placeholder="请输入单价" @input="onImmediateChange(idx)" @blur="onNumberBlur(idx, 'unitPrice', 9)" />
90 </template> 90 </template>
91 </uni-list-item> 91 </uni-list-item>
92 <uni-list-item title="不含税金额"> 92 <uni-list-item title="不含税金额">
@@ -152,12 +152,35 @@ export default { @@ -152,12 +152,35 @@ export default {
152 }, 152 },
153 { 153 {
154 text: '外贸客户资信管理', 154 text: '外贸客户资信管理',
155 - icon: '/static/images/index/order_list.png', 155 + icon: '/static/images/index/change_list.png',
156 link: '/pages/foreign_trade/index', 156 link: '/pages/foreign_trade/index',
157 name: 'ForeignTrade' 157 name: 'ForeignTrade'
  158 + },
  159 + {
  160 + text: '业务员报价单',
  161 + icon: '/static/images/index/revoke_list.png',
  162 + link: '/pages/sales-price/index',
  163 + name: 'SalesPrice'
  164 + },
  165 + {
  166 + text: '报价项目',
  167 + icon: '/static/images/index/invoice.png',
  168 + link: '/pages/supplier-price/index',
  169 + name: 'SupplierPrice'
  170 + },
  171 + {
  172 + text: '同行信息',
  173 + icon: '/static/images/index/draft_order.png',
  174 + link: '/pages/peer-info/index',
  175 + name: 'PeerInfo'
  176 + },
  177 + {
  178 + text: '报价管理',
  179 + icon: '/static/images/index/delay_invoice.png',
  180 + link: '/pages/quote-manage/index',
  181 + name: 'QuoteManage'
158 } 182 }
159 ] 183 ]
160 -  
161 }, 184 },
162 { 185 {
163 title: '订货单管理', 186 title: '订货单管理',
  1 +<template>
  2 + <view class="page">
  3 + <scroll-view class="scroll" scroll-y>
  4 + <uni-list>
  5 + <uni-list-item>
  6 + <template v-slot:body>
  7 + <view class="item-title"><text class="required">*</text><text>同行名称</text></view>
  8 + </template>
  9 + <template v-slot:footer>
  10 + <uni-easyinput v-model="form.peerName" placeholder="请输入同行名称" :inputBorder="false" />
  11 + </template>
  12 + </uni-list-item>
  13 +
  14 + <uni-list-item class="select-item" :class="form.peerProductName ? 'is-filled' : 'is-empty'" clickable
  15 + @click="openSheet('peerProductId')" :rightText="displayLabel('peerProductName')" showArrow>
  16 + <template v-slot:body>
  17 + <view class="item-title"><text class="required">*</text><text>同行品种</text></view>
  18 + </template>
  19 + </uni-list-item>
  20 +
  21 + <uni-list-item>
  22 + <template v-slot:body>
  23 + <view class="item-title"><text class="required">*</text><text>同行报价</text></view>
  24 + </template>
  25 + <template v-slot:footer>
  26 + <uni-easyinput type="digit" v-model="form.peerPrice" placeholder="请输入同行报价" :inputBorder="false" />
  27 + </template>
  28 + </uni-list-item>
  29 + </uni-list>
  30 + </scroll-view>
  31 +
  32 + <view class="footer">
  33 + <button class="btn submit" type="primary" :disabled="submitting" @click="onSubmit">{{ submitting ? '提交中...' : '提交' }}</button>
  34 + </view>
  35 +
  36 + <SingleSelectSheet :visible.sync="sheet.visible" :title="sheet.title" :options="sheet.options" v-model="sheet.value"
  37 + @confirm="onSheetConfirm" />
  38 + </view>
  39 +</template>
  40 +
  41 +<script>
  42 +import SingleSelectSheet from '@/components/single-select/index.vue'
  43 +import { breedRelationshipQueryApi } from '@/api/procure-manage/salesmanQuotation.js'
  44 +import { salesmanQuotationPeerSaveApi } from '@/api/procure-manage/salesmanQuotationPeer.js'
  45 +
  46 +export default {
  47 + name: 'PeerInfoAdd',
  48 + components: { SingleSelectSheet },
  49 + data() {
  50 + return {
  51 + submitting: false,
  52 + productVarietyList: [],
  53 + sheet: { visible: false, title: '请选择', options: [], field: '', value: '' },
  54 + form: {
  55 + peerName: '',
  56 + peerProductId: '',
  57 + peerProductName: '',
  58 + peerPrice: ''
  59 + }
  60 + }
  61 + },
  62 + created() {
  63 + this.loadProductVariety()
  64 + },
  65 + methods: {
  66 + async loadProductVariety() {
  67 + try {
  68 + const res = await breedRelationshipQueryApi({ pageIndex: 1, pageSize: 9999 })
  69 + const _data = res && res.data ? res.data : {}
  70 + const list = _data.datas || _data.list || _data.records || []
  71 + this.productVarietyList = list.map(it => ({
  72 + label: it.productName || it.name || '',
  73 + value: it.productId || it.id || ''
  74 + })).filter(it => it.value)
  75 + } catch (e) {
  76 + this.productVarietyList = []
  77 + }
  78 + },
  79 + displayLabel(field) {
  80 + const map = { peerProductName: '请选择同行品种' }
  81 + const val = this.form[field]
  82 + return val ? String(val) : (map[field] || '请选择')
  83 + },
  84 + openSheet(field) {
  85 + if (field === 'peerProductId') {
  86 + const current = this.form.peerProductId
  87 + const match = (this.productVarietyList || []).find(o => String(o.value) === String(current))
  88 + this.sheet = {
  89 + visible: true,
  90 + title: '同行品种',
  91 + options: this.productVarietyList || [],
  92 + field,
  93 + value: match ? match.value : ''
  94 + }
  95 + }
  96 + },
  97 + onSheetConfirm({ value, label }) {
  98 + const field = this.sheet.field
  99 + if (!field) return
  100 + if (field === 'peerProductId') {
  101 + this.form.peerProductId = value || ''
  102 + this.form.peerProductName = label || ''
  103 + }
  104 + },
  105 + validate() {
  106 + const checks = [
  107 + { key: 'peerName', label: '同行名称' },
  108 + { key: 'peerProductId', label: '同行品种' },
  109 + { key: 'peerPrice', label: '同行报价' }
  110 + ]
  111 + for (const it of checks) {
  112 + const val = this.form[it.key]
  113 + const empty = (val === undefined || val === null || (typeof val === 'string' && String(val).trim() === ''))
  114 + if (empty) {
  115 + uni.showToast({ title: `${it.label}必填`, icon: 'none' })
  116 + return false
  117 + }
  118 + }
  119 + return true
  120 + },
  121 + buildPayload() {
  122 + return {
  123 + peerName: this.form.peerName,
  124 + peerProductId: this.form.peerProductId,
  125 + peerPrice: this.form.peerPrice
  126 + }
  127 + },
  128 + async onSubmit() {
  129 + if (this.submitting) return
  130 + if (!this.validate()) return
  131 + this.submitting = true
  132 + const payload = this.buildPayload()
  133 + try {
  134 + await salesmanQuotationPeerSaveApi(payload)
  135 + uni.showToast({ title: '保存成功', icon: 'success' })
  136 + try { uni.setStorageSync('PEER_INFO_LIST_NEED_REFRESH', '1') } catch (e) {}
  137 + setTimeout(() => { uni.redirectTo({ url: '/pages/peer-info/index' }) }, 300)
  138 + } catch (e) {
  139 + uni.showToast({ title: (e && e.msg) || '保存失败', icon: 'none' })
  140 + } finally {
  141 + this.submitting = false
  142 + }
  143 + }
  144 + }
  145 +}
  146 +</script>
  147 +
  148 +<style lang="scss" scoped>
  149 +.page {
  150 + display: flex;
  151 + flex-direction: column;
  152 + height: 100%;
  153 +}
  154 +
  155 +.scroll {
  156 + flex: 1;
  157 + padding: 0rpx 0 160rpx;
  158 + background-color: #f3f3f3;
  159 +}
  160 +
  161 +.footer {
  162 + position: fixed;
  163 + left: 0;
  164 + right: 0;
  165 + bottom: 0;
  166 + padding: 32rpx;
  167 + padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
  168 + background: #fff;
  169 + box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
  170 + z-index: 10;
  171 +
  172 + .btn {
  173 + height: 80rpx;
  174 + line-height: 80rpx;
  175 + border-radius: 12rpx;
  176 + font-size: 32rpx;
  177 + }
  178 +
  179 + .submit {
  180 + background: $theme-primary;
  181 + color: #fff;
  182 + }
  183 +}
  184 +
  185 +.title-header {
  186 + display: flex;
  187 + align-items: center;
  188 + padding: 24rpx 32rpx 12rpx;
  189 + background-color: #ffffff;
  190 +
  191 + .title-header_icon {
  192 + width: 36rpx;
  193 + height: 36rpx;
  194 + margin-right: 12rpx;
  195 + }
  196 +
  197 + span {
  198 + font-size: 34rpx;
  199 + font-weight: 600;
  200 + color: rgba(0, 0, 0, 0.9);
  201 + }
  202 +}
  203 +
  204 +.mgb10 {
  205 + margin-bottom: 20rpx;
  206 +}
  207 +
  208 +::v-deep .uni-list {
  209 + .uni-easyinput {
  210 + display: flex;
  211 +
  212 + .uni-input-input {
  213 + color: rgba(0, 0, 0, 0.9);
  214 + }
  215 + }
  216 +
  217 + .uni-input-placeholder {
  218 + z-index: 1;
  219 + }
  220 +
  221 + .uni-input-input {
  222 + background-color: #ffffff;
  223 + }
  224 +
  225 + background: transparent;
  226 +
  227 + &-item {
  228 + &__extra-text {
  229 + font-size: 32rpx;
  230 + }
  231 +
  232 + &__content-title {
  233 + font-size: 32rpx;
  234 + color: rgba(0, 0, 0, 0.9);
  235 + }
  236 +
  237 + &__container {
  238 + padding: 32rpx;
  239 +
  240 + .uni-easyinput {
  241 + &__placeholder-class {
  242 + font-size: 32rpx;
  243 + color: rgba(0, 0, 0, 0.4);
  244 + }
  245 +
  246 + &__content {
  247 + border: none;
  248 + background-color: #ffffff !important;
  249 +
  250 + &-input {
  251 + padding-left: 0 !important;
  252 + height: 48rpx;
  253 + line-height: 48rpx;
  254 + font-size: 32rpx;
  255 + }
  256 +
  257 + .content-clear-icon {
  258 + font-size: 44rpx !important;
  259 + }
  260 + }
  261 + }
  262 +
  263 + .item-title,
  264 + .uni-list-item__content {
  265 + flex: none;
  266 + min-height: 48rpx;
  267 + line-height: 48rpx;
  268 + font-size: 32rpx;
  269 + position: relative;
  270 + width: 210rpx;
  271 + margin-right: 32rpx;
  272 + color: rgba(0, 0, 0, 0.9);
  273 +
  274 + .required {
  275 + color: red;
  276 + position: absolute;
  277 + top: 50%;
  278 + transform: translateY(-50%);
  279 + left: -16rpx;
  280 + }
  281 + }
  282 + }
  283 +
  284 + &.select-item {
  285 + &.is-empty {
  286 + .uni-list-item__extra-text {
  287 + color: rgba(0, 0, 0, 0.4) !important;
  288 + }
  289 + }
  290 +
  291 + &.is-filled {
  292 + .uni-list-item__extra-text {
  293 + color: rgba(0, 0, 0, 0.9) !important;
  294 + }
  295 + }
  296 + }
  297 +
  298 + &.mgb10 {
  299 + margin-bottom: 20rpx;
  300 + }
  301 + }
  302 +}
  303 +</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 company">{{ safeText(form.peerName) }}</text>
  7 + <view class="row"><text class="label">同行品种</text><text class="value">{{ safeText(form.peerProductName) }}</text></view>
  8 + <view class="row"><text class="label">同行报价</text><text class="value">{{ safeText(form.peerPrice) }}</text></view>
  9 + <view class="row"><text class="label">创建人</text><text class="value">{{ safeText(form.createBy) }}</text></view>
  10 + <view class="row"><text class="label">创建时间</text><text class="value">{{ formatDateTime(form.createTime) }}</text></view>
  11 + </view>
  12 + </view>
  13 + </scroll-view>
  14 + <detail-buttons :buttons="displayButtons" @click="handleButtonClick" />
  15 + </view>
  16 +</template>
  17 +
  18 +<script>
  19 +import DetailButtons from '@/components/detail-buttons/index.vue'
  20 +import { salesmanQuotationPeerGetApi, salesmanQuotationPeerDeleteApi } from '@/api/procure-manage/salesmanQuotationPeer.js'
  21 +
  22 +export default {
  23 + name: 'PeerInfoDetail',
  24 + components: { DetailButtons },
  25 + data() {
  26 + return {
  27 + id: '',
  28 + loading: false,
  29 + form: {},
  30 + buttons: [
  31 + { text: '编辑', visible: true, variant: 'outline', event: 'edit' },
  32 + { text: '删除', visible: true, variant: 'danger', event: 'delete' }
  33 + ]
  34 + }
  35 + },
  36 + computed: {
  37 + displayButtons() {
  38 + const isToday = this.isToday(this.form.createTime);
  39 + return [
  40 + { ...this.buttons[0], visible: isToday && this.$auth.hasPermi('procure-manage:peer-info:modify') },
  41 + { ...this.buttons[1], visible: isToday && this.$auth.hasPermi('procure-manage:peer-info:delete') }
  42 + ]
  43 + }
  44 + },
  45 + onShow() {
  46 + const options = (this.$route && this.$route.query) ? this.$route.query : {}
  47 + const id = options && options.id ? String(options.id) : ''
  48 + if (id && id !== this.id) {
  49 + this.id = id
  50 + }
  51 + if (this.id) this.loadDetail()
  52 + },
  53 + methods: {
  54 + isToday(dateStr) {
  55 + if (!dateStr) return false;
  56 + const date = new Date(dateStr);
  57 + const today = new Date();
  58 + return (
  59 + date.getFullYear() === today.getFullYear() &&
  60 + date.getMonth() === today.getMonth() &&
  61 + date.getDate() === today.getDate()
  62 + );
  63 + },
  64 + safeText(v) {
  65 + const s = v == null ? '' : String(v)
  66 + return s.trim() ? s : ''
  67 + },
  68 + formatDateTime(v) {
  69 + const s = v == null ? '' : String(v).trim()
  70 + if (!s) return '-'
  71 + if (s.length >= 16) return s.slice(0, 16)
  72 + if (s.length >= 10) return s.slice(0, 10)
  73 + return s
  74 + },
  75 + handleButtonClick(btn) {
  76 + if (!btn || btn.disabled) return
  77 + const e = btn.event || ''
  78 + if (e === 'edit') return this.onEdit()
  79 + if (e === 'delete') return this.onDelete()
  80 + },
  81 + onEdit() {
  82 + uni.navigateTo({ url: '/pages/peer-info/modify?id=' + this.id })
  83 + },
  84 + onDelete() {
  85 + const id = this.id || (this.form && this.form.id) || ''
  86 + if (!id) return
  87 + uni.showModal({
  88 + title: '系统提示',
  89 + content: '是否确定删除该同行信息?',
  90 + confirmText: '确定',
  91 + cancelText: '取消',
  92 + success: (res) => {
  93 + if (res && res.confirm) {
  94 + salesmanQuotationPeerDeleteApi(id).then(() => {
  95 + uni.showToast({ title: '已删除', icon: 'none' })
  96 + try { uni.setStorageSync('PEER_INFO_LIST_NEED_REFRESH', '1') } catch (e) {}
  97 + setTimeout(() => { uni.redirectTo({ url: '/pages/peer-info/index' }) }, 300)
  98 + }).catch((e) => {
  99 + uni.showToast({ title: (e && e.msg) || '删除失败', icon: 'none' })
  100 + })
  101 + }
  102 + }
  103 + })
  104 + },
  105 + async loadDetail() {
  106 + this.loading = true
  107 + try {
  108 + const res = await salesmanQuotationPeerGetApi(this.id)
  109 + this.form = (res && res.data) ? res.data : {}
  110 + } catch (e) {
  111 + this.form = {}
  112 + uni.showToast({ title: '详情加载失败', icon: 'none' })
  113 + } finally {
  114 + this.loading = false
  115 + }
  116 + }
  117 + }
  118 +}
  119 +</script>
  120 +
  121 +<style lang="scss" scoped>
  122 +.page {
  123 + display: flex;
  124 + flex-direction: column;
  125 + height: 100%;
  126 +}
  127 +
  128 +.scroll {
  129 + flex: 1;
  130 + padding: 8rpx 0 144rpx;
  131 +}
  132 +
  133 +.detail-page {
  134 + background: #f3f3f3;
  135 +}
  136 +
  137 +.section {
  138 + padding: 32rpx;
  139 + background: #fff;
  140 + margin-bottom: 20rpx;
  141 +}
  142 +
  143 +.row {
  144 + display: flex;
  145 + margin-bottom: 28rpx;
  146 +
  147 + &:last-child {
  148 + margin-bottom: 0;
  149 + }
  150 +
  151 + &.company {
  152 + font-size: 36rpx;
  153 + font-weight: 600;
  154 + color: rgba(0, 0, 0, 0.9);
  155 + padding-top: 8rpx;
  156 + line-height: 50rpx;
  157 + margin-bottom: 28rpx;
  158 + }
  159 +
  160 + .label {
  161 + max-width: 420rpx;
  162 + margin-right: 20rpx;
  163 + line-height: 32rpx;
  164 + font-size: 28rpx;
  165 + color: rgba(0, 0, 0, 0.6);
  166 + }
  167 +
  168 + .value {
  169 + flex: 1;
  170 + line-height: 32rpx;
  171 + font-size: 28rpx;
  172 + color: rgba(0, 0, 0, 0.9);
  173 + text-align: right;
  174 + word-break: break-all;
  175 +
  176 + &.pre {
  177 + white-space: pre-wrap;
  178 + }
  179 + }
  180 +}
  181 +</style>
  1 +<template>
  2 + <view class="page">
  3 + <view class="dev-list-fixed">
  4 + <view class="search-row">
  5 + <uni-search-bar v-model="searchKeyword" radius="6" placeholder="请输入同行名称" clearButton="auto"
  6 + cancelButton="none" bgColor="#F3F3F3" textColor="rgba(0,0,0,0.4)" @confirm="search"
  7 + @input="onSearchInput" />
  8 + <view class="tool-icons">
  9 + <image class="tool-icon" src="/static/images/dev_manage/add_icon.png" @click="onAdd" />
  10 + <image class="tool-icon" src="/static/images/dev_manage/filter_icon.png" @click="openFilter" />
  11 + </view>
  12 + </view>
  13 + </view>
  14 +
  15 + <view class="list-box">
  16 + <card-list ref="cardRef" :fetch-fn="fetchList" :query="query" :extra="extraParams" row-key="id"
  17 + :enable-refresh="true" :enable-load-more="true" @loaded="onCardLoaded" @error="onCardError">
  18 + <template v-slot="{ item }">
  19 + <view class="card" @click.stop="onCardClick(item)">
  20 + <view class="card-header">
  21 + <text class="title omit2">{{ item.peerName || '-' }}</text>
  22 + </view>
  23 + <view class="info-row">
  24 + <text>同行品种</text><text>{{ item.peerProductName || '-' }}</text>
  25 + </view>
  26 + <view class="info-row">
  27 + <text>同行报价</text><text>{{ item.peerPrice != null ? item.peerPrice : '-' }}</text>
  28 + </view>
  29 + <view class="info-row">
  30 + <text>创建人</text><text>{{ item.createBy || '-' }}</text>
  31 + </view>
  32 + <view class="info-row">
  33 + <text>创建时间</text><text>{{ formatDateTime(item.createTime) }}</text>
  34 + </view>
  35 + </view>
  36 + </template>
  37 + </card-list>
  38 + </view>
  39 +
  40 + <filter-modal :visible.sync="filterVisible" :value.sync="filterForm" title="筛选" @reset="onFilterReset"
  41 + @confirm="onFilterConfirm">
  42 + <template v-slot="{ model }">
  43 + <view class="filter-form">
  44 + <view class="form-item">
  45 + <view class="label">同行名称</view>
  46 + <uni-easyinput v-model="model.peerName" placeholder="请输入同行名称" :inputBorder="false"
  47 + placeholderStyle="font-size:14px" @input="onPeerNameInput" />
  48 + </view>
  49 + <view class="form-item">
  50 + <view class="label">同行品种名称</view>
  51 + <uni-easyinput v-model="model.peerProductName" placeholder="请输入同行品种名称" :inputBorder="false"
  52 + placeholderStyle="font-size:14px" @input="onPeerProductNameInput" />
  53 + </view>
  54 + <view class="form-item">
  55 + <view class="label">创建日期</view>
  56 + <uni-datetime-picker type="daterange" v-model="model.dateRange" start="2023-01-01" />
  57 + </view>
  58 + </view>
  59 + </template>
  60 + </filter-modal>
  61 + </view>
  62 +</template>
  63 +
  64 +<script>
  65 +import CardList from '@/components/card/index.vue'
  66 +import FilterModal from '@/components/filter/index.vue'
  67 +import { salesmanQuotationPeerQueryApi } from '@/api/procure-manage/salesmanQuotationPeer.js'
  68 +
  69 +export default {
  70 + name: 'PeerInfoList',
  71 + components: { CardList, FilterModal },
  72 + data() {
  73 + return {
  74 + searchKeyword: '',
  75 + searchKeywordDebounced: '',
  76 + searchDebounceTimer: null,
  77 + query: {
  78 + peerName: '',
  79 + peerProductName: '',
  80 + dateRange: []
  81 + },
  82 + extraParams: {},
  83 + currentItems: [],
  84 + filterVisible: false,
  85 + filterForm: {
  86 + peerName: '',
  87 + peerProductName: '',
  88 + dateRange: []
  89 + }
  90 + }
  91 + },
  92 + watch: {
  93 + searchKeywordDebounced(v) {
  94 + this.query = { ...this.query, peerName: v || '' }
  95 + }
  96 + },
  97 + onShow() {
  98 + let needRefresh = ''
  99 + try { needRefresh = uni.getStorageSync('PEER_INFO_LIST_NEED_REFRESH') } catch (e) {}
  100 + if (!needRefresh) return
  101 + try { uni.removeStorageSync('PEER_INFO_LIST_NEED_REFRESH') } catch (e) {}
  102 + if (this.$refs && this.$refs.cardRef && this.$refs.cardRef.reload) {
  103 + this.$refs.cardRef.reload()
  104 + }
  105 + },
  106 + onReachBottom() {
  107 + if (this.$refs && this.$refs.cardRef && this.$refs.cardRef.onLoadMore) {
  108 + this.$refs.cardRef.onLoadMore()
  109 + }
  110 + },
  111 + beforeDestroy() {
  112 + if (this.searchDebounceTimer) {
  113 + clearTimeout(this.searchDebounceTimer)
  114 + this.searchDebounceTimer = null
  115 + }
  116 + },
  117 + methods: {
  118 + formatDateTime(v) {
  119 + const s = v == null ? '' : String(v).trim()
  120 + if (!s) return '-'
  121 + if (s.length >= 16) return s.slice(0, 16)
  122 + if (s.length >= 10) return s.slice(0, 10)
  123 + return s
  124 + },
  125 + onAdd() {
  126 + uni.navigateTo({ url: '/pages/peer-info/add' })
  127 + },
  128 + onCardLoaded({ items }) {
  129 + this.currentItems = items
  130 + },
  131 + onCardError() {
  132 + uni.showToast({ title: '列表加载失败', icon: 'none' })
  133 + },
  134 + onSearchInput() {
  135 + if (this.searchDebounceTimer) clearTimeout(this.searchDebounceTimer)
  136 + this.searchDebounceTimer = setTimeout(() => {
  137 + this.searchKeywordDebounced = this.searchKeyword
  138 + this.searchDebounceTimer = null
  139 + }, 1200)
  140 + },
  141 + search(e) {
  142 + const val = e && e.value != null ? e.value : this.searchKeyword
  143 + this.searchKeyword = val
  144 + this.searchKeywordDebounced = val
  145 + },
  146 + openFilter() {
  147 + this.filterForm = {
  148 + peerName: this.query.peerName || '',
  149 + peerProductName: this.query.peerProductName || '',
  150 + dateRange: Array.isArray(this.query.dateRange) ? this.query.dateRange.slice(0) : []
  151 + }
  152 + this.filterVisible = true
  153 + },
  154 + onFilterReset(payload) {
  155 + this.filterForm = payload
  156 + },
  157 + onFilterConfirm(payload) {
  158 + this.query = {
  159 + peerName: this.searchKeywordDebounced || payload.peerName || '',
  160 + peerProductName: payload.peerProductName || '',
  161 + dateRange: Array.isArray(payload.dateRange) ? payload.dateRange : []
  162 + }
  163 + },
  164 + onPeerNameInput(val) {
  165 + this.filterForm.peerName = val
  166 + },
  167 + onPeerProductNameInput(val) {
  168 + this.filterForm.peerProductName = val
  169 + },
  170 + fetchList({ pageIndex, pageSize, query }) {
  171 + const q = query || {}
  172 + const range = Array.isArray(q.dateRange) ? q.dateRange : []
  173 + const params = {
  174 + pageIndex,
  175 + pageSize,
  176 + peerName: q.peerName || '',
  177 + peerProductName: q.peerProductName || '',
  178 + createDateStart: (range && range.length === 2) ? range[0] : '',
  179 + createDateEnd: (range && range.length === 2) ? range[1] : ''
  180 + }
  181 + return salesmanQuotationPeerQueryApi(params).then(res => {
  182 + const _data = res && res.data ? res.data : {}
  183 + let records = _data.datas || _data.list || _data.records || []
  184 + const totalCount = _data.totalCount || _data.count || 0
  185 + const hasNext = _data.hasNext || false
  186 + records = records.map(it => ({ ...it }))
  187 + return { records, totalCount, hasNext }
  188 + }).catch(() => {
  189 + this.onCardError()
  190 + return { records: [], totalCount: 0, hasNext: false }
  191 + })
  192 + },
  193 + onCardClick(item) {
  194 + uni.navigateTo({ url: '/pages/peer-info/detail?id=' + item.id })
  195 + }
  196 + }
  197 +}
  198 +</script>
  199 +
  200 +<style lang="scss" scoped>
  201 +.page {
  202 + display: flex;
  203 + flex-direction: column;
  204 + height: 100vh;
  205 +}
  206 +
  207 +.dev-list-fixed {
  208 + position: fixed;
  209 + top: 96rpx;
  210 + left: 0;
  211 + right: 0;
  212 + z-index: 2;
  213 + background: #fff;
  214 +
  215 + .search-row {
  216 + display: flex;
  217 + align-items: center;
  218 + padding: 16rpx 32rpx;
  219 +
  220 + .uni-searchbar {
  221 + padding: 0;
  222 + flex: 1;
  223 + }
  224 +
  225 + .tool-icons {
  226 + display: flex;
  227 +
  228 + .tool-icon {
  229 + width: 48rpx;
  230 + height: 48rpx;
  231 + display: block;
  232 + margin-left: 32rpx;
  233 + }
  234 + }
  235 + }
  236 +}
  237 +
  238 +::v-deep .uni-searchbar__box {
  239 + height: 80rpx !important;
  240 + justify-content: start;
  241 +
  242 + .uni-searchbar__box-search-input {
  243 + font-size: 32rpx !important;
  244 + }
  245 +}
  246 +
  247 +.list-box {
  248 + flex: 1;
  249 + padding-top: 140rpx;
  250 +
  251 + .card {
  252 + position: relative;
  253 + }
  254 +
  255 + .card-header {
  256 + margin-bottom: 28rpx;
  257 + position: relative;
  258 +
  259 + .title {
  260 + font-size: 36rpx;
  261 + font-weight: 600;
  262 + line-height: 50rpx;
  263 + color: rgba(0, 0, 0, 0.9);
  264 + width: 100%;
  265 + }
  266 + }
  267 +
  268 + .info-row {
  269 + display: flex;
  270 + align-items: center;
  271 + color: rgba(0, 0, 0, 0.6);
  272 + font-size: 28rpx;
  273 + margin-bottom: 24rpx;
  274 + height: 32rpx;
  275 +
  276 + &:last-child {
  277 + margin-bottom: 0;
  278 + }
  279 +
  280 + text {
  281 + width: 50%;
  282 +
  283 + &:last-child {
  284 + color: rgba(0, 0, 0, 0.9);
  285 + width: 50%;
  286 + }
  287 + }
  288 + }
  289 +}
  290 +
  291 +.filter-form {
  292 + .form-item {
  293 + margin-bottom: 24rpx;
  294 + }
  295 +
  296 + .label {
  297 + margin-bottom: 20rpx;
  298 + color: rgba(0, 0, 0, 0.9);
  299 + height: 44rpx;
  300 + line-height: 44rpx;
  301 + font-size: 30rpx;
  302 + }
  303 +
  304 + .uni-easyinput {
  305 + border: 1rpx solid #f3f3f3;
  306 + }
  307 +}
  308 +</style>
  1 +<template>
  2 + <view class="page">
  3 + <scroll-view class="scroll" scroll-y>
  4 + <uni-list>
  5 + <view class="title-header">
  6 + <image class="title-header_icon" src="/static/images/title.png" />
  7 + <span>同行信息</span>
  8 + </view>
  9 +
  10 + <uni-list-item>
  11 + <template v-slot:body>
  12 + <view class="item-title"><text class="required">*</text><text>同行名称</text></view>
  13 + </template>
  14 + <template v-slot:footer>
  15 + <uni-easyinput v-model="form.peerName" placeholder="请输入同行名称" :inputBorder="false" />
  16 + </template>
  17 + </uni-list-item>
  18 +
  19 + <uni-list-item class="select-item" :class="form.peerProductName ? 'is-filled' : 'is-empty'" clickable
  20 + @click="openSheet('peerProductId')" :rightText="displayLabel('peerProductName')" 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 + <uni-list-item>
  27 + <template v-slot:body>
  28 + <view class="item-title"><text class="required">*</text><text>同行报价</text></view>
  29 + </template>
  30 + <template v-slot:footer>
  31 + <uni-easyinput type="digit" v-model="form.peerPrice" placeholder="请输入同行报价" :inputBorder="false" />
  32 + </template>
  33 + </uni-list-item>
  34 + </uni-list>
  35 + </scroll-view>
  36 +
  37 + <view class="footer">
  38 + <button class="btn submit" type="primary" :disabled="submitting" @click="onSubmit">{{ submitting ? '提交中...' : '提交' }}</button>
  39 + </view>
  40 +
  41 + <SingleSelectSheet :visible.sync="sheet.visible" :title="sheet.title" :options="sheet.options" v-model="sheet.value"
  42 + @confirm="onSheetConfirm" />
  43 + </view>
  44 +</template>
  45 +
  46 +<script>
  47 +import SingleSelectSheet from '@/components/single-select/index.vue'
  48 +import { breedRelationshipQueryApi } from '@/api/procure-manage/salesmanQuotation.js'
  49 +import { salesmanQuotationPeerGetApi, salesmanQuotationPeerUpdateApi } from '@/api/procure-manage/salesmanQuotationPeer.js'
  50 +
  51 +export default {
  52 + name: 'PeerInfoModify',
  53 + components: { SingleSelectSheet },
  54 + data() {
  55 + return {
  56 + id: '',
  57 + submitting: false,
  58 + loading: false,
  59 + productVarietyList: [],
  60 + sheet: { visible: false, title: '请选择', options: [], field: '', value: '' },
  61 + form: {
  62 + id: '',
  63 + peerName: '',
  64 + peerProductId: '',
  65 + peerProductName: '',
  66 + peerPrice: ''
  67 + }
  68 + }
  69 + },
  70 + onLoad(query) {
  71 + this.id = (query && query.id) ? String(query.id) : ''
  72 + },
  73 + created() {
  74 + this.loadProductVariety()
  75 + if (this.id) this.loadDetail()
  76 + },
  77 + methods: {
  78 + async loadProductVariety() {
  79 + try {
  80 + const res = await breedRelationshipQueryApi({ pageIndex: 1, pageSize: 9999 })
  81 + const _data = res && res.data ? res.data : {}
  82 + const list = _data.datas || _data.list || _data.records || []
  83 + this.productVarietyList = list.map(it => ({
  84 + label: it.productName || it.name || '',
  85 + value: it.productId || it.id || ''
  86 + })).filter(it => it.value)
  87 + } catch (e) {
  88 + this.productVarietyList = []
  89 + }
  90 + },
  91 + async loadDetail() {
  92 + this.loading = true
  93 + try {
  94 + const res = await salesmanQuotationPeerGetApi(this.id)
  95 + const data = (res && res.data) ? res.data : {}
  96 + this.form = {
  97 + id: data.id || this.id,
  98 + peerName: data.peerName || '',
  99 + peerProductId: data.peerProductId || '',
  100 + peerProductName: data.peerProductName || this.getOptionLabel(this.productVarietyList, data.peerProductId),
  101 + peerPrice: data.peerPrice != null ? String(data.peerPrice) : ''
  102 + }
  103 + } catch (e) {
  104 + uni.showToast({ title: '加载失败', icon: 'none' })
  105 + } finally {
  106 + this.loading = false
  107 + }
  108 + },
  109 + getOptionLabel(options, value) {
  110 + const v = value != null ? String(value) : ''
  111 + if (!v) return ''
  112 + const match = (options || []).find(o => String(o.value) === v)
  113 + return match ? String(match.label || '') : ''
  114 + },
  115 + displayLabel(field) {
  116 + const map = { peerProductName: '请选择同行品种' }
  117 + const val = this.form[field]
  118 + return val ? String(val) : (map[field] || '请选择')
  119 + },
  120 + openSheet(field) {
  121 + if (field === 'peerProductId') {
  122 + const current = this.form.peerProductId
  123 + const match = (this.productVarietyList || []).find(o => String(o.value) === String(current))
  124 + this.sheet = {
  125 + visible: true,
  126 + title: '同行品种',
  127 + options: this.productVarietyList || [],
  128 + field,
  129 + value: match ? match.value : ''
  130 + }
  131 + }
  132 + },
  133 + onSheetConfirm({ value, label }) {
  134 + const field = this.sheet.field
  135 + if (!field) return
  136 + if (field === 'peerProductId') {
  137 + this.form.peerProductId = value || ''
  138 + this.form.peerProductName = label || ''
  139 + }
  140 + },
  141 + validate() {
  142 + const checks = [
  143 + { key: 'peerName', label: '同行名称' },
  144 + { key: 'peerProductId', label: '同行品种' },
  145 + { key: 'peerPrice', label: '同行报价' }
  146 + ]
  147 + for (const it of checks) {
  148 + const val = this.form[it.key]
  149 + const empty = (val === undefined || val === null || (typeof val === 'string' && String(val).trim() === ''))
  150 + if (empty) {
  151 + uni.showToast({ title: `${it.label}必填`, icon: 'none' })
  152 + return false
  153 + }
  154 + }
  155 + return true
  156 + },
  157 + buildPayload() {
  158 + return {
  159 + id: this.form.id,
  160 + peerName: this.form.peerName,
  161 + peerProductId: this.form.peerProductId,
  162 + peerPrice: this.form.peerPrice
  163 + }
  164 + },
  165 + async onSubmit() {
  166 + if (this.submitting) return
  167 + if (!this.validate()) return
  168 + this.submitting = true
  169 + const payload = this.buildPayload()
  170 + try {
  171 + await salesmanQuotationPeerUpdateApi(payload)
  172 + uni.showToast({ title: '保存成功', icon: 'success' })
  173 + try { uni.setStorageSync('PEER_INFO_LIST_NEED_REFRESH', '1') } catch (e) {}
  174 + setTimeout(() => { uni.redirectTo({ url: '/pages/peer-info/index' }) }, 300)
  175 + } catch (e) {
  176 + uni.showToast({ title: (e && e.msg) || '保存失败', icon: 'none' })
  177 + } finally {
  178 + this.submitting = false
  179 + }
  180 + }
  181 + }
  182 +}
  183 +</script>
  184 +
  185 +<style lang="scss" scoped>
  186 +.page {
  187 + display: flex;
  188 + flex-direction: column;
  189 + height: 100%;
  190 +}
  191 +
  192 +.scroll {
  193 + flex: 1;
  194 + padding: 0rpx 0 160rpx;
  195 + background-color: #f3f3f3;
  196 +}
  197 +
  198 +.footer {
  199 + position: fixed;
  200 + left: 0;
  201 + right: 0;
  202 + bottom: 0;
  203 + padding: 32rpx;
  204 + padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
  205 + background: #fff;
  206 + box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
  207 + z-index: 10;
  208 +
  209 + .btn {
  210 + height: 80rpx;
  211 + line-height: 80rpx;
  212 + border-radius: 12rpx;
  213 + font-size: 32rpx;
  214 + }
  215 +
  216 + .submit {
  217 + background: $theme-primary;
  218 + color: #fff;
  219 + }
  220 +}
  221 +
  222 +.title-header {
  223 + display: flex;
  224 + align-items: center;
  225 + padding: 24rpx 32rpx 12rpx;
  226 + background-color: #ffffff;
  227 +
  228 + .title-header_icon {
  229 + width: 36rpx;
  230 + height: 36rpx;
  231 + margin-right: 12rpx;
  232 + }
  233 +
  234 + span {
  235 + font-size: 34rpx;
  236 + font-weight: 600;
  237 + color: rgba(0, 0, 0, 0.9);
  238 + }
  239 +}
  240 +
  241 +.mgb10 {
  242 + margin-bottom: 20rpx;
  243 +}
  244 +
  245 +::v-deep .uni-list {
  246 + .uni-easyinput {
  247 + display: flex;
  248 +
  249 + .uni-input-input {
  250 + color: rgba(0, 0, 0, 0.9);
  251 + }
  252 + }
  253 +
  254 + .uni-input-placeholder {
  255 + z-index: 1;
  256 + }
  257 +
  258 + .uni-input-input {
  259 + background-color: #ffffff;
  260 + }
  261 +
  262 + background: transparent;
  263 +
  264 + &-item {
  265 + &__extra-text {
  266 + font-size: 32rpx;
  267 + }
  268 +
  269 + &__content-title {
  270 + font-size: 32rpx;
  271 + color: rgba(0, 0, 0, 0.9);
  272 + }
  273 +
  274 + &__container {
  275 + padding: 32rpx;
  276 +
  277 + .uni-easyinput {
  278 + &__placeholder-class {
  279 + font-size: 32rpx;
  280 + color: rgba(0, 0, 0, 0.4);
  281 + }
  282 +
  283 + &__content {
  284 + border: none;
  285 + background-color: #ffffff !important;
  286 +
  287 + &-input {
  288 + padding-left: 0 !important;
  289 + height: 48rpx;
  290 + line-height: 48rpx;
  291 + font-size: 32rpx;
  292 + }
  293 +
  294 + .content-clear-icon {
  295 + font-size: 44rpx !important;
  296 + }
  297 + }
  298 + }
  299 +
  300 + .item-title,
  301 + .uni-list-item__content {
  302 + flex: none;
  303 + min-height: 48rpx;
  304 + line-height: 48rpx;
  305 + font-size: 32rpx;
  306 + position: relative;
  307 + width: 210rpx;
  308 + margin-right: 32rpx;
  309 + color: rgba(0, 0, 0, 0.9);
  310 +
  311 + .required {
  312 + color: red;
  313 + position: absolute;
  314 + top: 50%;
  315 + transform: translateY(-50%);
  316 + left: -16rpx;
  317 + }
  318 + }
  319 + }
  320 +
  321 + &.select-item {
  322 + &.is-empty {
  323 + .uni-list-item__extra-text {
  324 + color: rgba(0, 0, 0, 0.4) !important;
  325 + }
  326 + }
  327 +
  328 + &.is-filled {
  329 + .uni-list-item__extra-text {
  330 + color: rgba(0, 0, 0, 0.9) !important;
  331 + }
  332 + }
  333 + }
  334 +
  335 + &.mgb10 {
  336 + margin-bottom: 20rpx;
  337 + }
  338 + }
  339 +}
  340 +</style>
  1 +<template>
  2 + <view class="page">
  3 + <scroll-view class="scroll" scroll-y>
  4 + <view class="section" v-if="users.length > 0">
  5 + <view
  6 + v-for="it in users"
  7 + :key="it.id"
  8 + class="card"
  9 + :class="{ 'is-selected': selectedId === it.id }"
  10 + @click="onSelect(it)"
  11 + >
  12 + <view class="card-row">
  13 + <text class="card-name">{{ safeText(it.name) }}</text>
  14 + <text class="card-check" v-if="selectedId === it.id">✓</text>
  15 + </view>
  16 + <view class="card-row sub">
  17 + <text class="card-label">部门:</text>
  18 + <text class="card-value">{{ safeText(it.deptName) }}</text>
  19 + </view>
  20 + <view class="card-row sub">
  21 + <text class="card-label">角色:</text>
  22 + <text class="card-value">{{ safeText(it.roleName) }}</text>
  23 + </view>
  24 + <view class="card-row sub">
  25 + <text class="card-label">用户名:</text>
  26 + <text class="card-value">{{ safeText(it.username) }}</text>
  27 + </view>
  28 + </view>
  29 + </view>
  30 + <view v-else-if="!loading" class="empty">暂无可分配人员</view>
  31 + </scroll-view>
  32 +
  33 + <view class="footer">
  34 + <button class="btn submit" :disabled="!selectedId" @click="onSubmit">确定分配</button>
  35 + </view>
  36 + </view>
  37 +</template>
  38 +
  39 +<script>
  40 +import { supplierQuotationDealAssignUsersApi, supplierQuotationDealAssignApi } from '@/api/procure-manage/supplierQuotationDeal.js'
  41 +
  42 +export default {
  43 + name: 'AssignQuoteManage',
  44 + data() {
  45 + return {
  46 + id: '',
  47 + purchaseDepartment: '',
  48 + loading: false,
  49 + users: [],
  50 + selectedId: ''
  51 + }
  52 + },
  53 + onLoad(options) {
  54 + this.id = (options && options.id) ? options.id : ''
  55 + this.purchaseDepartment = (options && options.purchaseDepartment) ? options.purchaseDepartment : ''
  56 + if (!this.id) {
  57 + uni.showToast({ title: '缺少主键ID', icon: 'none' })
  58 + return
  59 + }
  60 + this.loadUsers()
  61 + },
  62 + methods: {
  63 + safeText(v) {
  64 + const s = v == null ? '' : String(v)
  65 + return s.trim() ? s : '-'
  66 + },
  67 + async loadUsers() {
  68 + this.loading = true
  69 + try {
  70 + const res = await supplierQuotationDealAssignUsersApi({ purchaseDepartment: this.purchaseDepartment })
  71 + const data = (res && res.data) ? res.data : []
  72 + this.users = Array.isArray(data) ? data : []
  73 + } catch (e) {
  74 + this.users = []
  75 + uni.showToast({ title: '加载人员失败', icon: 'none' })
  76 + } finally {
  77 + this.loading = false
  78 + }
  79 + },
  80 + onSelect(it) {
  81 + this.selectedId = it.id
  82 + },
  83 + async onSubmit() {
  84 + if (!this.selectedId) {
  85 + uni.showToast({ title: '请选择负责人', icon: 'none' })
  86 + return
  87 + }
  88 + const confirmRes = await new Promise(resolve => {
  89 + uni.showModal({ title: '提示', content: '确定分配给该负责人吗?', confirmText: '确定', cancelText: '取消', success: resolve })
  90 + })
  91 + if (!(confirmRes && confirmRes.confirm)) return
  92 + try {
  93 + await supplierQuotationDealAssignApi({ id: this.id, principalId: this.selectedId })
  94 + uni.showToast({ title: '分配成功', icon: 'none' })
  95 + try { uni.setStorageSync('QUOTE_MANAGE_LIST_NEED_REFRESH', '1') } catch (e) {}
  96 + setTimeout(() => { uni.navigateBack() }, 400)
  97 + } catch (e) {
  98 + uni.showToast({ title: e.msg || '分配失败', icon: 'none' })
  99 + }
  100 + }
  101 + }
  102 +}
  103 +</script>
  104 +
  105 +<style lang="scss" scoped>
  106 +.page {
  107 + display: flex;
  108 + flex-direction: column;
  109 + height: 100%;
  110 + background: #f3f3f3;
  111 +}
  112 +
  113 +.scroll {
  114 + flex: 1;
  115 + padding: 20rpx 0 200rpx;
  116 +}
  117 +
  118 +.section {
  119 + padding: 0 24rpx;
  120 +}
  121 +
  122 +.card {
  123 + background: #fff;
  124 + border-radius: 12rpx;
  125 + padding: 28rpx 32rpx;
  126 + margin-bottom: 20rpx;
  127 + border: 2rpx solid transparent;
  128 +
  129 + &.is-selected {
  130 + border-color: $theme-primary;
  131 + background: rgba($theme-primary, 0.04);
  132 + }
  133 +
  134 + .card-row {
  135 + display: flex;
  136 + align-items: center;
  137 + margin-bottom: 12rpx;
  138 +
  139 + &.sub {
  140 + margin-bottom: 8rpx;
  141 + }
  142 +
  143 + &:last-child {
  144 + margin-bottom: 0;
  145 + }
  146 + }
  147 +
  148 + .card-name {
  149 + flex: 1;
  150 + font-size: 32rpx;
  151 + font-weight: 600;
  152 + color: rgba(0, 0, 0, 0.9);
  153 + }
  154 +
  155 + .card-check {
  156 + color: $theme-primary;
  157 + font-size: 36rpx;
  158 + font-weight: 700;
  159 + }
  160 +
  161 + .card-label {
  162 + font-size: 28rpx;
  163 + color: rgba(0, 0, 0, 0.6);
  164 + }
  165 +
  166 + .card-value {
  167 + flex: 1;
  168 + font-size: 28rpx;
  169 + color: rgba(0, 0, 0, 0.9);
  170 + }
  171 +}
  172 +
  173 +.empty {
  174 + text-align: center;
  175 + color: rgba(0, 0, 0, 0.4);
  176 + font-size: 28rpx;
  177 + padding: 120rpx 0;
  178 +}
  179 +
  180 +.footer {
  181 + z-index: 2;
  182 + position: fixed;
  183 + left: 0;
  184 + right: 0;
  185 + bottom: 0;
  186 + padding: 32rpx;
  187 + padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
  188 + background: #fff;
  189 + box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
  190 +
  191 + .btn {
  192 + height: 80rpx;
  193 + line-height: 80rpx;
  194 + border-radius: 12rpx;
  195 + font-size: 32rpx;
  196 + }
  197 +
  198 + .submit {
  199 + background: $theme-primary;
  200 + color: #fff;
  201 +
  202 + &[disabled] {
  203 + opacity: 0.5;
  204 + }
  205 + }
  206 +}
  207 +</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 company">{{ safeText(form.projectName) }}</text>
  7 + <view class="row"><text class="label">项目编号</text><text class="value">{{ safeText(form.projectCode) }}</text></view>
  8 + <view class="row"><text class="label">报价时间</text><text class="value">{{ formatDateTimeMinute(form.quoteTime) }}</text></view>
  9 + <view class="row"><text class="label">供应商名称</text><text class="value">{{ safeText(form.supplierName) }}</text></view>
  10 + <view class="row"><text class="label">品种</text><text class="value">{{ safeText(form.productName) }}</text></view>
  11 + <view class="row"><text class="label">数量</text><text class="value">{{ safeText(form.quantity) }}</text></view>
  12 + <view class="row"><text class="label">价格</text><text class="value">{{ safeText(form.price) }}</text></view>
  13 + <view class="row"><text class="label">采购处</text><text class="value">{{ safeText(form.purchaseDepartmentName) }}</text></view>
  14 + <view class="row"><text class="label">负责人</text><text class="value">{{ safeText(form.principalName) }}</text></view>
  15 + <view class="row"><text class="label">分配时间</text><text class="value">{{ formatDateTimeMinute(form.assignTime) }}</text></view>
  16 + </view>
  17 + </view>
  18 + </scroll-view>
  19 +
  20 + <detail-buttons :buttons="displayButtons" @click="handleButtonClick" />
  21 + </view>
  22 +</template>
  23 +
  24 +<script>
  25 +import DetailButtons from '@/components/detail-buttons/index.vue'
  26 +import { supplierQuotationDealGetApi } from '@/api/procure-manage/supplierQuotationDeal.js'
  27 +
  28 +export default {
  29 + name: 'QuoteManageDetail',
  30 + components: { DetailButtons },
  31 + data() {
  32 + return {
  33 + id: '',
  34 + loading: false,
  35 + form: {},
  36 + buttons: [{
  37 + text: '分配',
  38 + visible: true,
  39 + variant: 'primary',
  40 + event: 'assign'
  41 + }]
  42 + }
  43 + },
  44 + computed: {
  45 + displayButtons() {
  46 + return [
  47 + { ...this.buttons[0], visible: !this.form.principalId && this.$auth.hasPermi('procure-manage:quote-manage:assign') }
  48 + ]
  49 + }
  50 + },
  51 + onShow() {
  52 + const options = (this.$route && this.$route.query) ? this.$route.query : {}
  53 + const id = options && options.id ? String(options.id) : ''
  54 + this.id = id
  55 + this.loadDetail()
  56 + },
  57 + methods: {
  58 + safeText(v) {
  59 + const s = v == null ? '' : String(v)
  60 + return s.trim() ? s : '-'
  61 + },
  62 + formatDateTimeMinute(v) {
  63 + const s = v == null ? '' : String(v).trim()
  64 + if (!s) return '-'
  65 + if (s.length >= 16) return s.slice(0, 16)
  66 + if (s.length >= 10) return s.slice(0, 10)
  67 + return s
  68 + },
  69 + async loadDetail() {
  70 + this.loading = true
  71 + try {
  72 + const res = await supplierQuotationDealGetApi(this.id)
  73 + const data = (res && res.data) ? res.data : {}
  74 + this.form = data
  75 + } catch (e) {
  76 + this.form = {}
  77 + uni.showToast({ title: '详情加载失败', icon: 'none' })
  78 + } finally {
  79 + this.loading = false
  80 + }
  81 + },
  82 + handleButtonClick(btn) {
  83 + if (!btn || btn.disabled) return
  84 + const e = btn.event || ''
  85 + if (e === 'assign') return this.onAssign()
  86 + },
  87 + onAssign() {
  88 + const pd = this.form.purchaseDepartment || this.form.purchaseDepartmentId || ''
  89 + uni.navigateTo({ url: '/pages/quote-manage/assign?id=' + this.id + '&purchaseDepartment=' + pd })
  90 + }
  91 + }
  92 +}
  93 +</script>
  94 +
  95 +<style lang="scss" scoped>
  96 +.page {
  97 + display: flex;
  98 + flex-direction: column;
  99 + height: 100%;
  100 +}
  101 +
  102 +.scroll {
  103 + flex: 1;
  104 + padding: 8rpx 0 144rpx;
  105 +}
  106 +
  107 +.detail-page {
  108 + background: #f3f3f3;
  109 +}
  110 +
  111 +.section {
  112 + padding: 32rpx;
  113 + background: #fff;
  114 + margin-bottom: 20rpx;
  115 +}
  116 +
  117 +.row {
  118 + display: flex;
  119 + margin-bottom: 28rpx;
  120 +
  121 + &:last-child {
  122 + margin-bottom: 0;
  123 + }
  124 +
  125 + &.company {
  126 + font-size: 36rpx;
  127 + font-weight: 600;
  128 + color: rgba(0, 0, 0, 0.9);
  129 + padding-top: 8rpx;
  130 + line-height: 50rpx;
  131 + margin-bottom: 32rpx;
  132 + }
  133 +
  134 + .label {
  135 + max-width: 420rpx;
  136 + margin-right: 20rpx;
  137 + line-height: 32rpx;
  138 + font-size: 28rpx;
  139 + color: rgba(0, 0, 0, 0.6);
  140 + }
  141 +
  142 + .value {
  143 + flex: 1;
  144 + line-height: 32rpx;
  145 + font-size: 28rpx;
  146 + color: rgba(0, 0, 0, 0.9);
  147 + text-align: right;
  148 + word-break: break-all;
  149 + }
  150 +}
  151 +</style>
  1 +<template>
  2 + <view class="page">
  3 + <view class="dev-list-fixed">
  4 + <view class="search-row">
  5 + <uni-search-bar v-model="searchKeyword" radius="6" placeholder="请输入项目名称" clearButton="auto"
  6 + cancelButton="none" bgColor="#F3F3F3" textColor="rgba(0,0,0,0.4)" @confirm="search"
  7 + @input="onSearchInput" />
  8 + <view class="tool-icons">
  9 + <image class="tool-icon" src="/static/images/dev_manage/filter_icon.png" @click="openFilter" />
  10 + </view>
  11 + </view>
  12 + </view>
  13 +
  14 + <view class="list-box">
  15 + <card-list ref="cardRef" :fetch-fn="fetchList" :query="query" :extra="extraParams" row-key="id"
  16 + :enable-refresh="true" :enable-load-more="true" @loaded="onCardLoaded" @error="onCardError">
  17 + <template v-slot="{ item }">
  18 + <view class="card" @click.stop="onCardClick(item)">
  19 + <view class="card-header">
  20 + <text class="title omit2">{{ item.projectName || '-' }}</text>
  21 + </view>
  22 + <view class="info-row">
  23 + <text>项目编号</text><text>{{ item.projectCode || '-' }}</text>
  24 + </view>
  25 + <view class="info-row">
  26 + <text>品种</text><text>{{ item.productName || '-' }}</text>
  27 + </view>
  28 + <view class="info-row">
  29 + <text>供应商名称</text><text>{{ item.supplierName || '-' }}</text>
  30 + </view>
  31 + <view class="info-row">
  32 + <text>价格</text><text>{{ item.price != null ? item.price : '-' }}</text>
  33 + </view>
  34 + </view>
  35 + </template>
  36 + </card-list>
  37 + </view>
  38 +
  39 + <filter-modal :visible.sync="filterVisible" :value.sync="filterForm" title="筛选" @reset="onFilterReset"
  40 + @confirm="onFilterConfirm">
  41 + <template v-slot="{ model }">
  42 + <view class="filter-form">
  43 + <view class="form-item">
  44 + <view class="label">项目编号</view>
  45 + <uni-easyinput v-model="model.projectCode" placeholder="请输入项目编号" :inputBorder="true" />
  46 + </view>
  47 + <view class="form-item">
  48 + <view class="label">供应商名称</view>
  49 + <uni-easyinput v-model="model.supplierName" placeholder="请输入供应商名称" :inputBorder="true" />
  50 + </view>
  51 + </view>
  52 + </template>
  53 + </filter-modal>
  54 + </view>
  55 +</template>
  56 +
  57 +<script>
  58 +import CardList from '@/components/card/index.vue'
  59 +import FilterModal from '@/components/filter/index.vue'
  60 +import { supplierQuotationDealQueryApi } from '@/api/procure-manage/supplierQuotationDeal.js'
  61 +
  62 +export default {
  63 + name: 'QuoteManageList',
  64 + components: { CardList, FilterModal },
  65 + data() {
  66 + return {
  67 + searchKeyword: '',
  68 + searchKeywordDebounced: '',
  69 + searchDebounceTimer: null,
  70 + query: {
  71 + projectName: '',
  72 + projectCode: '',
  73 + supplierName: ''
  74 + },
  75 + extraParams: {},
  76 + currentItems: [],
  77 + filterVisible: false,
  78 + filterForm: {
  79 + projectCode: '',
  80 + supplierName: ''
  81 + }
  82 + }
  83 + },
  84 + watch: {
  85 + searchKeywordDebounced(v) {
  86 + this.query = { ...this.query, projectName: v || '' }
  87 + }
  88 + },
  89 + onShow() {
  90 + let needRefresh = ''
  91 + try { needRefresh = uni.getStorageSync('QUOTE_MANAGE_LIST_NEED_REFRESH') } catch (e) {}
  92 + if (!needRefresh) return
  93 + try { uni.removeStorageSync('QUOTE_MANAGE_LIST_NEED_REFRESH') } catch (e) {}
  94 + if (this.$refs && this.$refs.cardRef && this.$refs.cardRef.reload) {
  95 + this.$refs.cardRef.reload()
  96 + }
  97 + },
  98 + onReachBottom() {
  99 + if (this.$refs && this.$refs.cardRef && this.$refs.cardRef.onLoadMore) {
  100 + this.$refs.cardRef.onLoadMore()
  101 + }
  102 + },
  103 + beforeDestroy() {
  104 + if (this.searchDebounceTimer) {
  105 + clearTimeout(this.searchDebounceTimer)
  106 + this.searchDebounceTimer = null
  107 + }
  108 + },
  109 + methods: {
  110 + onCardLoaded({ items }) {
  111 + this.currentItems = items
  112 + },
  113 + onCardError() {
  114 + uni.showToast({ title: '列表加载失败', icon: 'none' })
  115 + },
  116 + onSearchInput() {
  117 + if (this.searchDebounceTimer) clearTimeout(this.searchDebounceTimer)
  118 + this.searchDebounceTimer = setTimeout(() => {
  119 + this.searchKeywordDebounced = this.searchKeyword
  120 + this.searchDebounceTimer = null
  121 + }, 1200)
  122 + },
  123 + search(e) {
  124 + const val = e && e.value != null ? e.value : this.searchKeyword
  125 + this.searchKeyword = val
  126 + this.searchKeywordDebounced = val
  127 + },
  128 + openFilter() {
  129 + this.filterForm = {
  130 + projectCode: this.query.projectCode || '',
  131 + supplierName: this.query.supplierName || ''
  132 + }
  133 + this.filterVisible = true
  134 + },
  135 + onFilterReset(payload) {
  136 + this.filterForm = payload
  137 + },
  138 + onFilterConfirm(payload) {
  139 + this.query = {
  140 + projectName: this.query.projectName || '',
  141 + projectCode: payload.projectCode || '',
  142 + supplierName: payload.supplierName || ''
  143 + }
  144 + },
  145 + onCardClick(item) {
  146 + uni.navigateTo({ url: '/pages/quote-manage/detail?id=' + item.id })
  147 + },
  148 + fetchList({ pageIndex, pageSize, query }) {
  149 + const q = query || {}
  150 + const params = {
  151 + pageIndex,
  152 + pageSize,
  153 + projectName: q.projectName || '',
  154 + projectCode: q.projectCode || '',
  155 + supplierName: q.supplierName || ''
  156 + }
  157 + return supplierQuotationDealQueryApi(params).then(res => {
  158 + const _data = res && res.data ? res.data : {}
  159 + let records = _data.datas || _data.list || _data.records || []
  160 + const totalCount = _data.totalCount || _data.count || 0
  161 + const hasNext = _data.hasNext || false
  162 + records = records.map(it => ({ ...it }))
  163 + return { records, totalCount, hasNext }
  164 + }).catch(() => {
  165 + this.onCardError()
  166 + return { records: [], totalCount: 0, hasNext: false }
  167 + })
  168 + }
  169 + }
  170 +}
  171 +</script>
  172 +
  173 +<style lang="scss" scoped>
  174 +.page {
  175 + display: flex;
  176 + flex-direction: column;
  177 + height: 100vh;
  178 +}
  179 +
  180 +.dev-list-fixed {
  181 + position: fixed;
  182 + top: 96rpx;
  183 + left: 0;
  184 + right: 0;
  185 + z-index: 2;
  186 + background: #fff;
  187 +
  188 + .search-row {
  189 + display: flex;
  190 + align-items: center;
  191 + padding: 16rpx 32rpx;
  192 +
  193 + .uni-searchbar {
  194 + padding: 0;
  195 + flex: 1;
  196 + }
  197 +
  198 + .tool-icons {
  199 + display: flex;
  200 +
  201 + .tool-icon {
  202 + width: 48rpx;
  203 + height: 48rpx;
  204 + display: block;
  205 + margin-left: 32rpx;
  206 + }
  207 + }
  208 + }
  209 +}
  210 +
  211 +::v-deep .uni-searchbar__box {
  212 + height: 80rpx !important;
  213 + justify-content: start;
  214 +
  215 + .uni-searchbar__box-search-input {
  216 + font-size: 32rpx !important;
  217 + }
  218 +}
  219 +
  220 +.list-box {
  221 + flex: 1;
  222 + padding-top: 140rpx;
  223 +
  224 + .card {
  225 + position: relative;
  226 + }
  227 +
  228 + .card-header {
  229 + margin-bottom: 28rpx;
  230 + position: relative;
  231 +
  232 + .title {
  233 + font-size: 36rpx;
  234 + font-weight: 600;
  235 + line-height: 50rpx;
  236 + color: rgba(0, 0, 0, 0.9);
  237 + width: 578rpx;
  238 + }
  239 + }
  240 +
  241 + .info-row {
  242 + display: flex;
  243 + align-items: center;
  244 + color: rgba(0, 0, 0, 0.6);
  245 + font-size: 28rpx;
  246 + margin-bottom: 24rpx;
  247 + height: 32rpx;
  248 +
  249 + &:last-child {
  250 + margin-bottom: 0;
  251 + }
  252 +
  253 + text {
  254 + width: 50%;
  255 +
  256 + &:last-child {
  257 + color: rgba(0, 0, 0, 0.9);
  258 + width: 50%;
  259 + }
  260 + }
  261 + }
  262 +}
  263 +
  264 +.filter-form {
  265 + .form-item {
  266 + margin-bottom: 24rpx;
  267 + }
  268 +
  269 + .label {
  270 + margin-bottom: 20rpx;
  271 + color: rgba(0, 0, 0, 0.9);
  272 + height: 44rpx;
  273 + line-height: 44rpx;
  274 + font-size: 30rpx;
  275 + }
  276 +}
  277 +</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" @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>
  1 +<template>
  2 + <view class="page">
  3 + <view class="dev-list-fixed">
  4 + <view class="search-row">
  5 + <uni-search-bar v-model="searchKeyword" radius="6" placeholder="请输入供应商名称" clearButton="auto"
  6 + cancelButton="none" bgColor="#F3F3F3" textColor="rgba(0,0,0,0.4)" @confirm="search"
  7 + @input="onSearchInput" />
  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" />
  10 + <image class="tool-icon" src="/static/images/dev_manage/filter_icon.png" @click="openFilter" />
  11 + </view>
  12 + </view>
  13 + </view>
  14 +
  15 + <view class="list-box">
  16 + <card-list ref="cardRef" :fetch-fn="fetchList" :query="query" :extra="extraParams" row-key="id"
  17 + :enable-refresh="true" :enable-load-more="true" @loaded="onCardLoaded" @error="onCardError">
  18 + <template v-slot="{ item }">
  19 + <view class="card" @click.stop="onCardClick(item)">
  20 + <view class="card-header">
  21 + <text class="title omit2">{{ item.supplierName || '-' }}</text>
  22 + <text :class="['status', `status_${item.approvalStatus}`]">{{ getAuditStatusText(item.approvalStatus) }}</text>
  23 + </view>
  24 + <view class="info-row">
  25 + <text>编号</text><text>{{ item.code || '-' }}</text>
  26 + </view>
  27 + <view class="info-row">
  28 + <text>采购处</text><text>{{ item.purchaseDepartmentName || '-' }}</text>
  29 + </view>
  30 + <view class="info-row">
  31 + <text>区域</text><text>{{ item.regionName || '-' }}</text>
  32 + </view>
  33 + <view class="info-row">
  34 + <text>日期</text><text>{{ formatDateOnly(item.quoteTime) }}</text>
  35 + </view>
  36 + </view>
  37 + </template>
  38 + </card-list>
  39 + </view>
  40 +
  41 + <filter-modal :visible.sync="filterVisible" :value.sync="filterForm" title="筛选" @reset="onFilterReset"
  42 + @confirm="onFilterConfirm">
  43 + <template v-slot="{ model }">
  44 + <view class="filter-form">
  45 + <view class="form-item">
  46 + <view class="label">编号</view>
  47 + <uni-easyinput v-model="model.code" placeholder="请输入编号" :inputBorder="false"
  48 + placeholderStyle="font-size:14px" @input="onCodeInput" />
  49 + </view>
  50 + <view class="form-item">
  51 + <view class="label">报价时间</view>
  52 + <uni-datetime-picker type="daterange" v-model="model.dateRange" start="2023-01-01" />
  53 + </view>
  54 + </view>
  55 + </template>
  56 + </filter-modal>
  57 + </view>
  58 +</template>
  59 +
  60 +<script>
  61 +import CardList from '@/components/card/index.vue'
  62 +import FilterModal from '@/components/filter/index.vue'
  63 +import { getDicByCodes, getDicName } from '@/utils/dic.js'
  64 +import { salesmanQuotationQueryApi } from '@/api/procure-manage/salesmanQuotation.js'
  65 +
  66 +export default {
  67 + name: 'SalesPriceList',
  68 + components: { CardList, FilterModal },
  69 + data() {
  70 + return {
  71 + searchKeyword: '',
  72 + searchKeywordDebounced: '',
  73 + searchDebounceTimer: null,
  74 + query: {
  75 + supplierName: '',
  76 + code: '',
  77 + dateRange: []
  78 + },
  79 + extraParams: {},
  80 + currentItems: [],
  81 + filterVisible: false,
  82 + filterForm: {
  83 + code: '',
  84 + dateRange: []
  85 + },
  86 + dicOptions: {
  87 + AUDIT_STATUS: []
  88 + },
  89 + }
  90 + },
  91 + watch: {
  92 + searchKeywordDebounced(v) {
  93 + this.query = { ...this.query, supplierName: v || '' }
  94 + }
  95 + },
  96 + created() {
  97 + this.loadDicData()
  98 + },
  99 + onShow() {
  100 + let needRefresh = ''
  101 + try { needRefresh = uni.getStorageSync('SALES_PRICE_LIST_NEED_REFRESH') } catch (e) {}
  102 + if (!needRefresh) return
  103 + try { uni.removeStorageSync('SALES_PRICE_LIST_NEED_REFRESH') } catch (e) {}
  104 + if (this.$refs && this.$refs.cardRef && this.$refs.cardRef.reload) {
  105 + this.$refs.cardRef.reload()
  106 + }
  107 + },
  108 + onReachBottom() {
  109 + if (this.$refs && this.$refs.cardRef && this.$refs.cardRef.onLoadMore) {
  110 + this.$refs.cardRef.onLoadMore()
  111 + }
  112 + },
  113 + beforeDestroy() {
  114 + if (this.searchDebounceTimer) {
  115 + clearTimeout(this.searchDebounceTimer)
  116 + this.searchDebounceTimer = null
  117 + }
  118 + },
  119 + methods: {
  120 + onAdd() {
  121 + uni.navigateTo({ url: '/pages/sales-price/add' })
  122 + },
  123 + async loadDicData() {
  124 + try {
  125 + const results = await getDicByCodes(['AUDIT_STATUS'])
  126 + this.dicOptions.AUDIT_STATUS = (results.AUDIT_STATUS && results.AUDIT_STATUS.data) || []
  127 + } catch (e) {
  128 + this.dicOptions.AUDIT_STATUS = []
  129 + }
  130 + },
  131 + formatDateOnly(v) {
  132 + const s = v == null ? '' : String(v).trim()
  133 + if (!s) return '-'
  134 + if (s.length >= 10) return s.slice(0, 10)
  135 + return s
  136 + },
  137 + onCardLoaded({ items }) {
  138 + this.currentItems = items
  139 + },
  140 + onCardError() {
  141 + uni.showToast({ title: '列表加载失败', icon: 'none' })
  142 + },
  143 + onSearchInput() {
  144 + if (this.searchDebounceTimer) clearTimeout(this.searchDebounceTimer)
  145 + this.searchDebounceTimer = setTimeout(() => {
  146 + this.searchKeywordDebounced = this.searchKeyword
  147 + this.searchDebounceTimer = null
  148 + }, 1200)
  149 + },
  150 + search(e) {
  151 + const val = e && e.value != null ? e.value : this.searchKeyword
  152 + this.searchKeyword = val
  153 + this.searchKeywordDebounced = val
  154 + },
  155 + openFilter() {
  156 + this.filterForm = {
  157 + code: this.query.code || '',
  158 + dateRange: Array.isArray(this.query.dateRange) ? this.query.dateRange.slice(0) : []
  159 + }
  160 + this.filterVisible = true
  161 + },
  162 + onFilterReset(payload) {
  163 + this.filterForm = payload
  164 + },
  165 + onFilterConfirm(payload) {
  166 + this.query = {
  167 + supplierName: this.query.supplierName || '',
  168 + code: payload.code || '',
  169 + dateRange: Array.isArray(payload.dateRange) ? payload.dateRange : []
  170 + }
  171 + },
  172 + onCodeInput(val) {
  173 + this.filterForm.code = val
  174 + },
  175 + getAuditStatusText(status) {
  176 + const v = status == null ? '' : String(status)
  177 + if (!v) return '-'
  178 + return getDicName('AUDIT_STATUS', v, this.dicOptions.AUDIT_STATUS) || v
  179 + },
  180 + fetchList({ pageIndex, pageSize, query }) {
  181 + const q = query || {}
  182 + const range = Array.isArray(q.dateRange) ? q.dateRange : []
  183 + const params = {
  184 + pageIndex,
  185 + pageSize,
  186 + supplierName: q.supplierName || '',
  187 + code: q.code || '',
  188 + quoteDateStart: (range && range.length === 2) ? range[0] : '',
  189 + quoteDateEnd: (range && range.length === 2) ? range[1] : ''
  190 + }
  191 + return salesmanQuotationQueryApi(params).then(res => {
  192 + const _data = res && res.data ? res.data : {}
  193 + let records = _data.datas || _data.list || _data.records || []
  194 + const totalCount = _data.totalCount || _data.count || 0
  195 + const hasNext = _data.hasNext || false
  196 + records = records.map(it => ({ ...it }))
  197 + return { records, totalCount, hasNext }
  198 + }).catch(() => {
  199 + this.onCardError()
  200 + return { records: [], totalCount: 0, hasNext: false }
  201 + })
  202 + },
  203 + onCardClick(item) {
  204 + uni.navigateTo({ url: '/pages/sales-price/detail?id=' + item.id })
  205 + },
  206 + }
  207 +}
  208 +</script>
  209 +
  210 +<style lang="scss" scoped>
  211 +.page {
  212 + display: flex;
  213 + flex-direction: column;
  214 + height: 100vh;
  215 +}
  216 +
  217 +.dev-list-fixed {
  218 + position: fixed;
  219 + top: 96rpx;
  220 + left: 0;
  221 + right: 0;
  222 + z-index: 2;
  223 + background: #fff;
  224 +
  225 + .search-row {
  226 + display: flex;
  227 + align-items: center;
  228 + padding: 16rpx 32rpx;
  229 +
  230 + .uni-searchbar {
  231 + padding: 0;
  232 + flex: 1;
  233 + }
  234 +
  235 + .tool-icons {
  236 + display: flex;
  237 +
  238 + .tool-icon {
  239 + width: 48rpx;
  240 + height: 48rpx;
  241 + display: block;
  242 + margin-left: 32rpx;
  243 + }
  244 + }
  245 + }
  246 +}
  247 +
  248 +::v-deep .uni-searchbar__box {
  249 + height: 80rpx !important;
  250 + justify-content: start;
  251 +
  252 + .uni-searchbar__box-search-input {
  253 + font-size: 32rpx !important;
  254 + }
  255 +}
  256 +
  257 +.list-box {
  258 + flex: 1;
  259 + padding-top: 140rpx;
  260 +
  261 + .card {
  262 + position: relative;
  263 + }
  264 +
  265 + .card-header {
  266 + margin-bottom: 28rpx;
  267 + position: relative;
  268 +
  269 + .title {
  270 + font-size: 36rpx;
  271 + font-weight: 600;
  272 + line-height: 50rpx;
  273 + color: rgba(0, 0, 0, 0.9);
  274 + width: 578rpx;
  275 + }
  276 +
  277 + .status {
  278 + position: absolute;
  279 + top: -32rpx;
  280 + right: -12rpx;
  281 + height: 48rpx;
  282 + line-height: 48rpx;
  283 + font-weight: 600;
  284 + color: #fff;
  285 + font-size: 24rpx;
  286 + padding: 0 14rpx;
  287 + border-radius: 6rpx;
  288 +
  289 + &.status_AUDIT {
  290 + background: $theme-primary;
  291 + }
  292 +
  293 + &.status_PASS {
  294 + background: #2BA471;
  295 + }
  296 +
  297 + &.status_REFUSE {
  298 + background: #D54941;
  299 + }
  300 +
  301 + &.status_CANCEL {
  302 + background: #E7E7E7;
  303 + color: rgba(0, 0, 0, 0.9);
  304 + }
  305 + }
  306 + }
  307 +
  308 + .info-row {
  309 + display: flex;
  310 + align-items: center;
  311 + color: rgba(0, 0, 0, 0.6);
  312 + font-size: 28rpx;
  313 + margin-bottom: 24rpx;
  314 + height: 32rpx;
  315 +
  316 + &:last-child {
  317 + margin-bottom: 0;
  318 + }
  319 +
  320 + text {
  321 + width: 50%;
  322 +
  323 + &:last-child {
  324 + color: rgba(0, 0, 0, 0.9);
  325 + width: 50%;
  326 + }
  327 + }
  328 + }
  329 +}
  330 +
  331 +.filter-form {
  332 + .form-item {
  333 + margin-bottom: 24rpx;
  334 + }
  335 +
  336 + .label {
  337 + margin-bottom: 20rpx;
  338 + color: rgba(0, 0, 0, 0.9);
  339 + height: 44rpx;
  340 + line-height: 44rpx;
  341 + font-size: 30rpx;
  342 + }
  343 +
  344 + .uni-easyinput {
  345 + border: 1rpx solid #f3f3f3;
  346 + }
  347 +}
  348 +</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 + </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 +<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.name ? '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 + <uni-easyinput v-model="form.name" :inputBorder="false" placeholder="请输入项目名称" />
  12 + </template>
  13 + </uni-list-item>
  14 +
  15 + <!-- 品种 -->
  16 + <uni-list-item class="select-item" :class="form.productId ? 'is-filled' : 'is-empty'" clickable
  17 + @click="openProductSheet" :rightText="form.productName || '请选择品种'" showArrow>
  18 + <template v-slot:body>
  19 + <view class="item-title"><text class="required">*</text><text>品种</text></view>
  20 + </template>
  21 + </uni-list-item>
  22 +
  23 + <!-- 数量 -->
  24 + <uni-list-item class="select-item" :class="form.quantity !== '' && form.quantity != null ? 'is-filled' : 'is-empty'">
  25 + <template v-slot:body>
  26 + <view class="item-title"><text class="required">*</text><text>数量(吨)</text></view>
  27 + </template>
  28 + <template v-slot:footer>
  29 + <uni-easyinput v-model="form.quantity" type="digit" :inputBorder="false" placeholder="请输入数量" @blur="onQuantityBlur" />
  30 + </template>
  31 + </uni-list-item>
  32 +
  33 + <!-- 开始时间 -->
  34 + <uni-list-item class="select-item" :class="form.startTime ? 'is-filled' : 'is-empty'">
  35 + <template v-slot:body>
  36 + <view class="item-title"><text class="required">*</text><text>开始时间</text></view>
  37 + </template>
  38 + <template v-slot:footer>
  39 + <uni-datetime-picker type="datetime" v-model="form.startTime" :hide-second="true" :clear-icon="false" />
  40 + </template>
  41 + </uni-list-item>
  42 +
  43 + <!-- 截止时间 -->
  44 + <uni-list-item class="select-item" :class="form.endTime ? 'is-filled' : 'is-empty'">
  45 + <template v-slot:body>
  46 + <view class="item-title"><text class="required">*</text><text>截止时间</text></view>
  47 + </template>
  48 + <template v-slot:footer>
  49 + <uni-datetime-picker type="datetime" v-model="form.endTime" :hide-second="true" :clear-icon="false" />
  50 + </template>
  51 + </uni-list-item>
  52 +
  53 + <!-- 项目类型 -->
  54 + <uni-list-item class="select-item" :class="form.projectType ? 'is-filled' : 'is-empty'" clickable
  55 + @click="openDictSheet('projectType', 'PROJECT_TYPE', '项目类型')" :rightText="getProjectTypeLabel() || '请选择项目类型'" showArrow>
  56 + <template v-slot:body>
  57 + <view class="item-title"><text class="required">*</text><text>项目类型</text></view>
  58 + </template>
  59 + </uni-list-item>
  60 +
  61 + <!-- 开标时间 - 仅项目类型为集中开标时显示 -->
  62 + <uni-list-item v-if="isConcentratedBid" class="select-item" :class="form.bidTime ? 'is-filled' : 'is-empty'">
  63 + <template v-slot:body>
  64 + <view class="item-title"><text class="required">*</text><text>开标时间</text></view>
  65 + </template>
  66 + <template v-slot:footer>
  67 + <uni-datetime-picker type="datetime" v-model="form.bidTime" :hide-second="true" :clear-icon="false" />
  68 + </template>
  69 + </uni-list-item>
  70 +
  71 + <!-- 采购规则 -->
  72 + <uni-list-item class="select-item textarea-item" :class="form.purchaseRule ? 'is-filled' : 'is-empty'">
  73 + <template v-slot:body>
  74 + <view class="item-title"><text class="required">*</text><text>采购规则</text></view>
  75 + </template>
  76 + <template v-slot:footer>
  77 + <uni-easyinput type="textarea" v-model="form.purchaseRule" :inputBorder="false"
  78 + placeholder="请输入采购规则" :autoHeight="true" />
  79 + </template>
  80 + </uni-list-item>
  81 + </uni-list>
  82 + </scroll-view>
  83 +
  84 + <view class="footer">
  85 + <button class="btn submit" @click="onSubmit">提交</button>
  86 + </view>
  87 +
  88 + <SingleSelectSheet :visible.sync="sheet.visible" :title="sheet.title" :options="sheet.options"
  89 + v-model="sheet.value" @confirm="onSheetConfirm" />
  90 + </view>
  91 +</template>
  92 +
  93 +<script>
  94 +import SingleSelectSheet from '@/components/single-select/index.vue'
  95 +import { supplierQuotationProjectSaveApi } from '@/api/procure-manage/supplierQuotationProject.js'
  96 +import { breedRelationshipQueryApi } from '@/api/procure-manage/salesmanQuotation.js'
  97 +import { getDicByCodes } from '@/utils/dic.js'
  98 +
  99 +export default {
  100 + name: 'AddSupplierPrice',
  101 + components: { SingleSelectSheet },
  102 + data() {
  103 + return {
  104 + form: {
  105 + name: '',
  106 + productId: '',
  107 + productName: '',
  108 + quantity: '',
  109 + startTime: '',
  110 + endTime: '',
  111 + projectType: '',
  112 + bidTime: '',
  113 + purchaseRule: ''
  114 + },
  115 + productList: [],
  116 + dictData: {},
  117 + sheet: {
  118 + visible: false,
  119 + title: '',
  120 + options: [],
  121 + value: '',
  122 + field: '',
  123 + isDict: false
  124 + }
  125 + }
  126 + },
  127 + computed: {
  128 + isConcentratedBid() {
  129 + // 项目类型为"集中开标"时显示开标时间
  130 + const opts = this.getDictOptions('PROJECT_TYPE') || []
  131 + const m = opts.find(o => String(o.code != null ? o.code : o.value) === String(this.form.projectType))
  132 + const label = m ? (m.name || m.label || '') : ''
  133 + return label.indexOf('集中开标') > -1
  134 + }
  135 + },
  136 + onLoad() {
  137 + this.loadDictData()
  138 + this.loadProducts()
  139 + },
  140 + methods: {
  141 + async loadDictData() {
  142 + try {
  143 + const res = await getDicByCodes(['PROJECT_TYPE'])
  144 + this.dictData = res || {}
  145 + } catch (e) {
  146 + this.dictData = {}
  147 + }
  148 + },
  149 + getDictOptions(dictCode) {
  150 + const data = this.dictData[dictCode]
  151 + return (data && data.data) ? data.data : (Array.isArray(data) ? data : [])
  152 + },
  153 + async loadProducts() {
  154 + try {
  155 + const res = await breedRelationshipQueryApi({ pageIndex: 1, pageSize: 9999 })
  156 + const _data = res && res.data ? res.data : {}
  157 + const list = _data.datas || _data.list || _data.records || []
  158 + this.productList = list.map(it => ({
  159 + label: it.productName || it.name || '',
  160 + value: it.productId || it.id || ''
  161 + }))
  162 + } catch (e) {
  163 + this.productList = []
  164 + }
  165 + },
  166 + getProjectTypeLabel() {
  167 + const opts = this.getDictOptions('PROJECT_TYPE') || []
  168 + const m = opts.find(o => String(o.code != null ? o.code : o.value) === String(this.form.projectType))
  169 + return m ? (m.name || m.label || '') : ''
  170 + },
  171 + openProductSheet() {
  172 + const opts = (this.productList || []).map(p => ({ label: p.label, value: p.value }))
  173 + const cur = this.form.productId
  174 + const match = opts.find(o => String(o.value) === String(cur))
  175 + this.sheet = {
  176 + visible: true,
  177 + title: '请选择品种',
  178 + options: opts,
  179 + value: match ? match.value : '',
  180 + field: 'productId',
  181 + isDict: false
  182 + }
  183 + },
  184 + openDictSheet(field, dictCode, title) {
  185 + const raw = this.getDictOptions(dictCode) || []
  186 + const opts = raw.map(o => ({ label: o.name || o.label || '', value: o.code != null ? o.code : (o.value != null ? o.value : '') }))
  187 + const cur = this.form[field]
  188 + const match = opts.find(o => String(o.value) === String(cur))
  189 + this.sheet = {
  190 + visible: true,
  191 + title: title || '请选择',
  192 + options: opts,
  193 + value: match ? match.value : '',
  194 + field,
  195 + isDict: true
  196 + }
  197 + },
  198 + onSheetConfirm({ value }) {
  199 + const field = this.sheet.field
  200 + if (!field) return
  201 + this.form[field] = value
  202 + if (field === 'productId') {
  203 + const m = (this.productList || []).find(p => String(p.value) === String(value))
  204 + this.form.productName = m ? m.label : ''
  205 + }
  206 + // 切换项目类型时,若不再是集中开标则清空开标时间
  207 + if (field === 'projectType' && !this.isConcentratedBid) {
  208 + this.form.bidTime = ''
  209 + }
  210 + },
  211 + onQuantityBlur() {
  212 + let v = this.form.quantity
  213 + if (v === '' || v == null) return
  214 + v = parseFloat(v)
  215 + if (isNaN(v)) {
  216 + this.form.quantity = ''
  217 + return
  218 + }
  219 + this.form.quantity = String(v)
  220 + },
  221 + formatDateTimeMinute(v) {
  222 + const s = v == null ? '' : String(v).trim()
  223 + if (!s) return ''
  224 + if (s.length >= 16) return s.slice(0, 16)
  225 + return s
  226 + },
  227 + formatDateTimeSecond(v) {
  228 + const s = v == null ? '' : String(v).trim()
  229 + if (!s) return ''
  230 + // 截取到分钟,补齐秒
  231 + const minute = s.length >= 16 ? s.slice(0, 16) : s
  232 + return minute + ':00'
  233 + },
  234 + validateRequired() {
  235 + const checks = [
  236 + { key: 'name', msg: '请输入项目名称' },
  237 + { key: 'productId', msg: '请选择品种' },
  238 + { key: 'quantity', msg: '请输入数量' },
  239 + { key: 'startTime', msg: '请选择开始时间' },
  240 + { key: 'endTime', msg: '请选择截止时间' },
  241 + { key: 'projectType', msg: '请选择项目类型' },
  242 + { key: 'purchaseRule', msg: '请输入采购规则' }
  243 + ]
  244 + for (const it of checks) {
  245 + const val = this.form[it.key]
  246 + const empty = (val === undefined || val === null || (typeof val === 'string' && val.trim() === ''))
  247 + if (empty) { uni.showToast({ title: it.msg, icon: 'none' }); return false }
  248 + }
  249 + if (this.isConcentratedBid && !this.form.bidTime) {
  250 + uni.showToast({ title: '请选择开标时间', icon: 'none' })
  251 + return false
  252 + }
  253 + // 数量必须为有效数字
  254 + if (isNaN(parseFloat(this.form.quantity))) {
  255 + uni.showToast({ title: '数量格式不正确', icon: 'none' })
  256 + return false
  257 + }
  258 + // 开始时间不能晚于截止时间
  259 + if (this.form.startTime && this.form.endTime && this.formatDateTimeMinute(this.form.startTime) > this.formatDateTimeMinute(this.form.endTime)) {
  260 + uni.showToast({ title: '开始时间不能晚于截止时间', icon: 'none' })
  261 + return false
  262 + }
  263 + return true
  264 + },
  265 + async onSubmit() {
  266 + if (!this.validateRequired()) return
  267 + const confirmRes = await new Promise(resolve => {
  268 + uni.showModal({ title: '提示', content: '确定新增报价项目吗?', confirmText: '确定', cancelText: '取消', success: resolve })
  269 + })
  270 + if (!(confirmRes && confirmRes.confirm)) return
  271 + const clean = (obj) => {
  272 + const out = {}
  273 + Object.keys(obj || {}).forEach(k => {
  274 + const v = obj[k]
  275 + const isEmptyString = typeof v === 'string' && v.trim() === ''
  276 + const isUndef = v === undefined || v === null
  277 + const isNaNNumber = typeof v === 'number' && isNaN(v)
  278 + if (!(isEmptyString || isUndef || isNaNNumber)) out[k] = v
  279 + })
  280 + return out
  281 + }
  282 + const payload = clean({
  283 + name: this.form.name,
  284 + productId: this.form.productId,
  285 + productName: this.form.productName,
  286 + quantity: this.form.quantity,
  287 + startTime: this.formatDateTimeSecond(this.form.startTime),
  288 + endTime: this.formatDateTimeSecond(this.form.endTime),
  289 + projectType: this.form.projectType,
  290 + bidTime: this.isConcentratedBid ? this.formatDateTimeSecond(this.form.bidTime) : '',
  291 + purchaseRule: this.form.purchaseRule
  292 + })
  293 + try {
  294 + await supplierQuotationProjectSaveApi(payload)
  295 + uni.showToast({ title: '新增成功', icon: 'none' })
  296 + try { uni.setStorageSync('SUPPLIER_PRICE_LIST_NEED_REFRESH', '1') } catch (e) {}
  297 + setTimeout(() => { uni.redirectTo({ url: '/pages/supplier-price/index' }) }, 400)
  298 + } catch (e) {
  299 + uni.showToast({ title: e.msg || '新增失败', icon: 'none' })
  300 + }
  301 + }
  302 + }
  303 +}
  304 +</script>
  305 +
  306 +<style lang="scss" scoped>
  307 +.page {
  308 + display: flex;
  309 + flex-direction: column;
  310 + height: 100%;
  311 +}
  312 +
  313 +.scroll {
  314 + flex: 1;
  315 + padding: 12rpx 0 200rpx !important;
  316 +}
  317 +
  318 +.footer {
  319 + z-index: 2;
  320 + position: fixed;
  321 + left: 0;
  322 + right: 0;
  323 + bottom: 0;
  324 + padding: 32rpx;
  325 + padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
  326 + background: #fff;
  327 + box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
  328 +
  329 + .btn {
  330 + height: 80rpx;
  331 + line-height: 80rpx;
  332 + border-radius: 12rpx;
  333 + font-size: 32rpx;
  334 + }
  335 +
  336 + .submit {
  337 + background: $theme-primary;
  338 + color: #fff;
  339 + }
  340 +}
  341 +
  342 +::v-deep .uni-list {
  343 + .uni-easyinput {
  344 + display: flex;
  345 +
  346 + .uni-input-input {
  347 + color: rgba(0, 0, 0, 0.9);
  348 + }
  349 + }
  350 +
  351 + .uni-input-placeholder {
  352 + z-index: 1;
  353 + }
  354 +
  355 + .uni-input-input {
  356 + background-color: #ffffff;
  357 + }
  358 +
  359 + background: transparent;
  360 +
  361 + &-item {
  362 + &__extra-text {
  363 + font-size: 32rpx;
  364 + }
  365 +
  366 + &__content-title {
  367 + font-size: 32rpx;
  368 + color: rgba(0, 0, 0, 0.9);
  369 + }
  370 +
  371 + &__container {
  372 + padding: 32rpx;
  373 +
  374 + .uni-easyinput {
  375 + &__placeholder-class {
  376 + font-size: 32rpx;
  377 + color: rgba(0, 0, 0, 0.4);
  378 + }
  379 +
  380 + &__content {
  381 + border: none;
  382 + background-color: #ffffff !important;
  383 +
  384 + &-input {
  385 + padding-left: 0 !important;
  386 + height: 48rpx;
  387 + line-height: 48rpx;
  388 + font-size: 32rpx;
  389 + }
  390 +
  391 + .content-clear-icon {
  392 + font-size: 44rpx !important;
  393 + }
  394 + }
  395 + }
  396 +
  397 + .item-title,
  398 + .uni-list-item__content {
  399 + flex: none;
  400 + min-height: 48rpx;
  401 + line-height: 48rpx;
  402 + font-size: 32rpx;
  403 + position: relative;
  404 + width: 240rpx;
  405 + margin-right: 32rpx;
  406 + color: rgba(0, 0, 0, 0.9);
  407 +
  408 + .required {
  409 + color: red;
  410 + position: absolute;
  411 + top: 50%;
  412 + transform: translateY(-50%);
  413 + left: -16rpx;
  414 + }
  415 + }
  416 + }
  417 +
  418 + &.select-item {
  419 + &.is-empty {
  420 + .uni-list-item__extra-text {
  421 + color: rgba(0, 0, 0, 0.4) !important;
  422 + }
  423 + }
  424 +
  425 + &.is-filled {
  426 + .uni-list-item__extra-text {
  427 + color: rgba(0, 0, 0, 0.9) !important;
  428 + }
  429 + }
  430 + }
  431 + }
  432 +}
  433 +
  434 +::v-deep .uni-datetime-picker {
  435 + flex: 1;
  436 +
  437 + .uni-input-wrapper {
  438 + justify-content: flex-end;
  439 + }
  440 +
  441 + .uni-input-input {
  442 + text-align: right;
  443 + font-size: 32rpx;
  444 + color: rgba(0, 0, 0, 0.9);
  445 + }
  446 +}
  447 +
  448 +.textarea-item {
  449 + ::v-deep .uni-list-item__container {
  450 + flex-direction: column;
  451 + align-items: stretch;
  452 +
  453 + .item-title {
  454 + width: auto;
  455 + margin-right: 0;
  456 + margin-bottom: 16rpx;
  457 + }
  458 +
  459 + .uni-easyinput {
  460 + width: 100%;
  461 + }
  462 +
  463 + .uni-textarea-textarea {
  464 + text-align: left;
  465 + min-height: 120rpx;
  466 + font-size: 32rpx;
  467 + color: rgba(0, 0, 0, 0.9);
  468 + }
  469 + }
  470 +}
  471 +</style>
  1 +<template>
  2 + <view class="page">
  3 + <scroll-view class="scroll" scroll-y>
  4 + <!-- 基础信息 -->
  5 + <view class="section">
  6 + <text class="row company">{{ safeText(form.name) }}</text>
  7 + <view class="row"><text class="label">编号</text><text class="value">{{ safeText(form.code) }}</text></view>
  8 + <view class="row"><text class="label">品种</text><text class="value">{{ safeText(form.productName) }}</text></view>
  9 + <view class="row"><text class="label">数量(吨)</text><text class="value">{{ safeText(form.quantity) }}</text></view>
  10 + <view class="row"><text class="label">开始时间</text><text class="value">{{ formatDateTimeMinute(form.startTime) }}</text></view>
  11 + <view class="row"><text class="label">截止时间</text><text class="value">{{ formatDateTimeMinute(form.endTime) }}</text></view>
  12 + <view class="row"><text class="label">项目类型</text><text class="value">{{ getProjectTypeLabel() }}</text></view>
  13 + <view v-if="showBidTime" class="row"><text class="label">开标时间</text><text class="value">{{ formatDateTimeMinute(form.bidTime) }}</text></view>
  14 + <view class="row"><text class="label">采购规则</text><text class="value">{{ safeText(form.purchaseRule) }}</text></view>
  15 + <view class="row"><text class="label">状态</text><text class="value">{{ safeText(form.status) }}</text></view>
  16 + <view class="row"><text class="label">发布人</text><text class="value">{{ safeText(form.createBy) }}</text></view>
  17 + </view>
  18 + </scroll-view>
  19 +
  20 + <detail-buttons :buttons="displayButtons" @click="handleButtonClick" />
  21 + </view>
  22 +</template>
  23 +
  24 +<script>
  25 +import DetailButtons from '@/components/detail-buttons/index.vue'
  26 +import { supplierQuotationProjectDetailApi, supplierQuotationProjectUpdateStatusApi } from '@/api/procure-manage/supplierQuotationProject.js'
  27 +import { getDicByCodes } from '@/utils/dic.js'
  28 +
  29 +export default {
  30 + name: 'DetailSupplierPrice',
  31 + components: { DetailButtons },
  32 + data() {
  33 + return {
  34 + id: '',
  35 + form: {},
  36 + dictData: {},
  37 + buttons: [{
  38 + text: '编辑',
  39 + visible: true,
  40 + variant: 'primary',
  41 + event: 'edit'
  42 + },{
  43 + text: '报价',
  44 + visible: true,
  45 + event: 'bid'
  46 + },{
  47 + text: '发布',
  48 + visible: true,
  49 + event: 'publish'
  50 + }]
  51 + }
  52 + },
  53 + computed: {
  54 + showBidTime() {
  55 + const opts = (this.dictData.PROJECT_TYPE && this.dictData.PROJECT_TYPE.data) || []
  56 + const m = opts.find(o => String(o.code != null ? o.code : o.value) === String(this.form.projectType))
  57 + const label = m ? (m.name || m.label || '') : ''
  58 + return label.indexOf('集中开标') > -1
  59 + },
  60 + displayButtons() {
  61 + const s = this.form.status || ''
  62 + return [
  63 + { ...this.buttons[0], visible: this.$auth.hasPermi('procure-manage:supplier-price:modify') && s === 'UNPUBLISHED' },
  64 + { ...this.buttons[1], visible: this.$auth.hasPermi('procure-manage:supplier-price:quotation') && s === 'QUOTING' },
  65 + { ...this.buttons[2], visible: this.$auth.hasPermi('procure-manage:supplier-price:publish') && s === 'UNPUBLISHED' }
  66 + ]
  67 + }
  68 + },
  69 + onLoad(options) {
  70 + this.id = (options && options.id) ? options.id : ''
  71 + if (!this.id) return
  72 + this.loadDictData().then(() => {
  73 + this.loadDetail()
  74 + })
  75 + },
  76 + methods: {
  77 + async loadDictData() {
  78 + try {
  79 + const res = await getDicByCodes(['PROJECT_TYPE'])
  80 + this.dictData = res || {}
  81 + } catch (e) {
  82 + this.dictData = {}
  83 + }
  84 + },
  85 + getProjectTypeLabel() {
  86 + const opts = (this.dictData.PROJECT_TYPE && this.dictData.PROJECT_TYPE.data) || []
  87 + const m = opts.find(o => String(o.code != null ? o.code : o.value) === String(this.form.projectType))
  88 + return m ? (m.name || m.label || '-') : '-'
  89 + },
  90 + async loadDetail() {
  91 + try {
  92 + const res = await supplierQuotationProjectDetailApi({ id: this.id })
  93 + this.form = (res && res.data) ? res.data : {}
  94 + } catch (e) {
  95 + uni.showToast({ title: '加载详情失败', icon: 'none' })
  96 + }
  97 + },
  98 + formatDateTimeMinute(v) {
  99 + const s = v == null ? '' : String(v).trim()
  100 + if (!s) return '-'
  101 + if (s.length >= 16) return s.slice(0, 16)
  102 + return s
  103 + },
  104 + safeText(v) {
  105 + const s = v == null ? '' : String(v)
  106 + return s.trim() ? s : '-'
  107 + },
  108 + handleButtonClick(btn) {
  109 + if (!btn || btn.disabled) return
  110 + const e = btn.event || ''
  111 + if (e === 'edit') return this.onEdit()
  112 + if (e === 'bid') return this.onQuote()
  113 + if (e === 'publish') return this.onPublish()
  114 + },
  115 + onEdit() {
  116 + uni.navigateTo({ url: '/pages/supplier-price/modify?id=' + this.id })
  117 + },
  118 + onQuote() {
  119 + uni.navigateTo({ url: '/pages/supplier-price/quote?id=' + this.id })
  120 + },
  121 + async onPublish() {
  122 + const confirmRes = await new Promise(resolve => {
  123 + uni.showModal({ title: '提示', content: '确定发布该项目吗?', confirmText: '确定', cancelText: '取消', success: resolve })
  124 + })
  125 + if (!(confirmRes && confirmRes.confirm)) return
  126 + try {
  127 + await supplierQuotationProjectUpdateStatusApi({ id: this.id, status: 'PUBLISHED' })
  128 + uni.showToast({ title: '发布成功', icon: 'none' })
  129 + this.loadDetail()
  130 + try { uni.setStorageSync('SUPPLIER_PRICE_LIST_NEED_REFRESH', '1') } catch (e) {}
  131 + } catch (e) {
  132 + uni.showToast({ title: e.msg || '发布失败', icon: 'none' })
  133 + }
  134 + }
  135 + }
  136 +}
  137 +</script>
  138 +
  139 +<style lang="scss" scoped>
  140 +.page {
  141 + display: flex;
  142 + flex-direction: column;
  143 + height: 100vh;
  144 + background: #f3f3f3;
  145 +}
  146 +
  147 +.scroll {
  148 + flex: 1;
  149 + padding: 8rpx 0 144rpx;
  150 +}
  151 +
  152 +.section {
  153 + padding: 32rpx;
  154 + background: #fff;
  155 + margin-bottom: 20rpx;
  156 +}
  157 +
  158 +.row {
  159 + display: flex;
  160 + margin-bottom: 28rpx;
  161 +
  162 + &:last-child {
  163 + margin-bottom: 0;
  164 + }
  165 +
  166 + &.company {
  167 + font-size: 36rpx;
  168 + font-weight: 600;
  169 + color: rgba(0, 0, 0, 0.9);
  170 + padding-top: 8rpx;
  171 + line-height: 50rpx;
  172 + }
  173 +
  174 + .label {
  175 + max-width: 420rpx;
  176 + margin-right: 20rpx;
  177 + line-height: 32rpx;
  178 + font-size: 28rpx;
  179 + color: rgba(0, 0, 0, 0.6);
  180 + }
  181 +
  182 + .value {
  183 + flex: 1;
  184 + line-height: 32rpx;
  185 + font-size: 28rpx;
  186 + color: rgba(0, 0, 0, 0.9);
  187 + text-align: right;
  188 + word-break: break-all;
  189 + }
  190 +}
  191 +</style>
  1 +<template>
  2 + <view class="page">
  3 + <view class="dev-list-fixed">
  4 + <view class="search-row">
  5 + <uni-search-bar v-model="searchKeyword" radius="6" placeholder="请输入项目名称" clearButton="auto"
  6 + cancelButton="none" bgColor="#F3F3F3" textColor="rgba(0,0,0,0.4)" @confirm="search"
  7 + @input="onSearchInput" />
  8 + <view class="tool-icons">
  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/add_icon.png" @click="onAdd" />
  11 + </view>
  12 + </view>
  13 + </view>
  14 +
  15 + <view class="list-box">
  16 + <card-list ref="cardRef" :fetch-fn="fetchList" :query="query" :extra="extraParams" row-key="id"
  17 + :enable-refresh="true" :enable-load-more="true" @loaded="onCardLoaded" @error="onCardError">
  18 + <template v-slot="{ item }">
  19 + <view class="card" @click.stop="onCardClick(item)">
  20 + <view class="card-header">
  21 + <text class="title omit2">{{ item.name || '-' }}</text>
  22 + </view>
  23 + <view class="info-row">
  24 + <text>项目编号</text><text>{{ item.code || '-' }}</text>
  25 + </view>
  26 + <view class="info-row">
  27 + <text>品种</text><text>{{ item.productName || '-' }}</text>
  28 + </view>
  29 + <view class="info-row">
  30 + <text>开始时间</text><text>{{ formatDateTimeMinute(item.startTime) }}</text>
  31 + </view>
  32 + <view class="info-row">
  33 + <text>截止时间</text><text>{{ formatDateTimeMinute(item.endTime) }}</text>
  34 + </view>
  35 + </view>
  36 + </template>
  37 + </card-list>
  38 + </view>
  39 +
  40 + <filter-modal :visible.sync="filterVisible" :value.sync="filterForm" title="筛选" @reset="onFilterReset"
  41 + @confirm="onFilterConfirm">
  42 + <template v-slot="{ model }">
  43 + <view class="filter-form">
  44 + <view class="form-item">
  45 + <view class="label">开始时间</view>
  46 + <uni-datetime-picker type="datetimerange" v-model="model.startTimeRange" start="2023-01-01" />
  47 + </view>
  48 + <view class="form-item">
  49 + <view class="label">截止时间</view>
  50 + <uni-datetime-picker type="datetimerange" v-model="model.endTimeRange" start="2023-01-01" />
  51 + </view>
  52 + <view class="form-item">
  53 + <view class="label">开标时间</view>
  54 + <uni-datetime-picker type="datetimerange" v-model="model.bidTimeRange" start="2023-01-01" />
  55 + </view>
  56 + </view>
  57 + </template>
  58 + </filter-modal>
  59 + </view>
  60 +</template>
  61 +
  62 +<script>
  63 +import CardList from '@/components/card/index.vue'
  64 +import FilterModal from '@/components/filter/index.vue'
  65 +import { supplierQuotationProjectQueryApi } from '@/api/procure-manage/supplierQuotationProject.js'
  66 +
  67 +export default {
  68 + name: 'SupplierPriceList',
  69 + components: { CardList, FilterModal },
  70 + data() {
  71 + return {
  72 + searchKeyword: '',
  73 + searchKeywordDebounced: '',
  74 + searchDebounceTimer: null,
  75 + query: {
  76 + name: '',
  77 + startTimeRange: [],
  78 + endTimeRange: [],
  79 + bidTimeRange: []
  80 + },
  81 + extraParams: {},
  82 + currentItems: [],
  83 + filterVisible: false,
  84 + filterForm: {
  85 + startTimeRange: [],
  86 + endTimeRange: [],
  87 + bidTimeRange: []
  88 + }
  89 + }
  90 + },
  91 + watch: {
  92 + searchKeywordDebounced(v) {
  93 + this.query = { ...this.query, name: v || '' }
  94 + }
  95 + },
  96 + onShow() {
  97 + let needRefresh = ''
  98 + try { needRefresh = uni.getStorageSync('SUPPLIER_PRICE_LIST_NEED_REFRESH') } catch (e) {}
  99 + if (!needRefresh) return
  100 + try { uni.removeStorageSync('SUPPLIER_PRICE_LIST_NEED_REFRESH') } catch (e) {}
  101 + if (this.$refs && this.$refs.cardRef && this.$refs.cardRef.reload) {
  102 + this.$refs.cardRef.reload()
  103 + }
  104 + },
  105 + onReachBottom() {
  106 + if (this.$refs && this.$refs.cardRef && this.$refs.cardRef.onLoadMore) {
  107 + this.$refs.cardRef.onLoadMore()
  108 + }
  109 + },
  110 + beforeDestroy() {
  111 + if (this.searchDebounceTimer) {
  112 + clearTimeout(this.searchDebounceTimer)
  113 + this.searchDebounceTimer = null
  114 + }
  115 + },
  116 + methods: {
  117 + formatDateTimeMinute(v) {
  118 + const s = v == null ? '' : String(v).trim()
  119 + if (!s) return '-'
  120 + // 截取到分钟 (yyyy-MM-dd HH:mm)
  121 + if (s.length >= 16) return s.slice(0, 16)
  122 + if (s.length >= 10) return s.slice(0, 10)
  123 + return s
  124 + },
  125 + onCardLoaded({ items }) {
  126 + this.currentItems = items
  127 + },
  128 + onCardError() {
  129 + uni.showToast({ title: '列表加载失败', icon: 'none' })
  130 + },
  131 + onSearchInput() {
  132 + if (this.searchDebounceTimer) clearTimeout(this.searchDebounceTimer)
  133 + this.searchDebounceTimer = setTimeout(() => {
  134 + this.searchKeywordDebounced = this.searchKeyword
  135 + this.searchDebounceTimer = null
  136 + }, 1200)
  137 + },
  138 + search(e) {
  139 + const val = e && e.value != null ? e.value : this.searchKeyword
  140 + this.searchKeyword = val
  141 + this.searchKeywordDebounced = val
  142 + },
  143 + openFilter() {
  144 + this.filterForm = {
  145 + startTimeRange: Array.isArray(this.query.startTimeRange) ? this.query.startTimeRange.slice(0) : [],
  146 + endTimeRange: Array.isArray(this.query.endTimeRange) ? this.query.endTimeRange.slice(0) : [],
  147 + bidTimeRange: Array.isArray(this.query.bidTimeRange) ? this.query.bidTimeRange.slice(0) : []
  148 + }
  149 + this.filterVisible = true
  150 + },
  151 + onFilterReset(payload) {
  152 + this.filterForm = payload
  153 + },
  154 + onFilterConfirm(payload) {
  155 + this.query = {
  156 + name: this.query.name || '',
  157 + startTimeRange: Array.isArray(payload.startTimeRange) ? payload.startTimeRange : [],
  158 + endTimeRange: Array.isArray(payload.endTimeRange) ? payload.endTimeRange : [],
  159 + bidTimeRange: Array.isArray(payload.bidTimeRange) ? payload.bidTimeRange : []
  160 + }
  161 + },
  162 + fetchList({ pageIndex, pageSize, query }) {
  163 + const q = query || {}
  164 + const sRange = Array.isArray(q.startTimeRange) ? q.startTimeRange : []
  165 + const eRange = Array.isArray(q.endTimeRange) ? q.endTimeRange : []
  166 + const bRange = Array.isArray(q.bidTimeRange) ? q.bidTimeRange : []
  167 + const params = {
  168 + pageIndex,
  169 + pageSize,
  170 + name: q.name || '',
  171 + startTimeStart: (sRange && sRange.length === 2) ? sRange[0] : '',
  172 + startTimeEnd: (sRange && sRange.length === 2) ? sRange[1] : '',
  173 + endTimeStart: (eRange && eRange.length === 2) ? eRange[0] : '',
  174 + endTimeEnd: (eRange && eRange.length === 2) ? eRange[1] : '',
  175 + bidTimeStart: (bRange && bRange.length === 2) ? bRange[0] : '',
  176 + bidTimeEnd: (bRange && bRange.length === 2) ? bRange[1] : ''
  177 + }
  178 + return supplierQuotationProjectQueryApi(params).then(res => {
  179 + const _data = res && res.data ? res.data : {}
  180 + let records = _data.datas || _data.list || _data.records || []
  181 + const totalCount = _data.totalCount || _data.count || 0
  182 + const hasNext = _data.hasNext || false
  183 + records = records.map(it => ({ ...it }))
  184 + return { records, totalCount, hasNext }
  185 + }).catch(() => {
  186 + this.onCardError()
  187 + return { records: [], totalCount: 0, hasNext: false }
  188 + })
  189 + },
  190 + onCardClick(item) {
  191 + uni.navigateTo({ url: '/pages/supplier-price/detail?id=' + item.id })
  192 + },
  193 + onAdd() {
  194 + uni.navigateTo({ url: '/pages/supplier-price/add' })
  195 + }
  196 + }
  197 +}
  198 +</script>
  199 +
  200 +<style lang="scss" scoped>
  201 +.page {
  202 + display: flex;
  203 + flex-direction: column;
  204 + height: 100vh;
  205 +}
  206 +
  207 +.dev-list-fixed {
  208 + position: fixed;
  209 + top: 96rpx;
  210 + left: 0;
  211 + right: 0;
  212 + z-index: 2;
  213 + background: #fff;
  214 +
  215 + .search-row {
  216 + display: flex;
  217 + align-items: center;
  218 + padding: 16rpx 32rpx;
  219 +
  220 + .uni-searchbar {
  221 + padding: 0;
  222 + flex: 1;
  223 + }
  224 +
  225 + .tool-icons {
  226 + display: flex;
  227 +
  228 + .tool-icon {
  229 + width: 48rpx;
  230 + height: 48rpx;
  231 + display: block;
  232 + margin-left: 32rpx;
  233 + }
  234 + }
  235 + }
  236 +}
  237 +
  238 +::v-deep .uni-searchbar__box {
  239 + height: 80rpx !important;
  240 + justify-content: start;
  241 +
  242 + .uni-searchbar__box-search-input {
  243 + font-size: 32rpx !important;
  244 + }
  245 +}
  246 +
  247 +.list-box {
  248 + flex: 1;
  249 + padding-top: 140rpx;
  250 +
  251 + .card {
  252 + position: relative;
  253 + }
  254 +
  255 + .card-header {
  256 + margin-bottom: 28rpx;
  257 + position: relative;
  258 +
  259 + .title {
  260 + font-size: 36rpx;
  261 + font-weight: 600;
  262 + line-height: 50rpx;
  263 + color: rgba(0, 0, 0, 0.9);
  264 + width: 578rpx;
  265 + }
  266 + }
  267 +
  268 + .info-row {
  269 + display: flex;
  270 + align-items: center;
  271 + color: rgba(0, 0, 0, 0.6);
  272 + font-size: 28rpx;
  273 + margin-bottom: 24rpx;
  274 + height: 32rpx;
  275 +
  276 + &:last-child {
  277 + margin-bottom: 0;
  278 + }
  279 +
  280 + text {
  281 + width: 50%;
  282 +
  283 + &:last-child {
  284 + color: rgba(0, 0, 0, 0.9);
  285 + width: 50%;
  286 + }
  287 + }
  288 + }
  289 +}
  290 +
  291 +.filter-form {
  292 + .form-item {
  293 + margin-bottom: 24rpx;
  294 + }
  295 +
  296 + .label {
  297 + margin-bottom: 20rpx;
  298 + color: rgba(0, 0, 0, 0.9);
  299 + height: 44rpx;
  300 + line-height: 44rpx;
  301 + font-size: 30rpx;
  302 + }
  303 +}
  304 +</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.name ? '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 + <uni-easyinput v-model="form.name" :inputBorder="false" placeholder="请输入项目名称" />
  12 + </template>
  13 + </uni-list-item>
  14 +
  15 + <!-- 品种 -->
  16 + <uni-list-item class="select-item" :class="form.productId ? 'is-filled' : 'is-empty'" clickable
  17 + @click="openProductSheet" :rightText="form.productName || '请选择品种'" showArrow>
  18 + <template v-slot:body>
  19 + <view class="item-title"><text class="required">*</text><text>品种</text></view>
  20 + </template>
  21 + </uni-list-item>
  22 +
  23 + <!-- 数量 -->
  24 + <uni-list-item class="select-item" :class="form.quantity !== '' && form.quantity != null ? 'is-filled' : 'is-empty'">
  25 + <template v-slot:body>
  26 + <view class="item-title"><text class="required">*</text><text>数量(吨)</text></view>
  27 + </template>
  28 + <template v-slot:footer>
  29 + <uni-easyinput v-model="form.quantity" type="digit" :inputBorder="false" placeholder="请输入数量" @blur="onQuantityBlur" />
  30 + </template>
  31 + </uni-list-item>
  32 +
  33 + <!-- 开始时间 -->
  34 + <uni-list-item class="select-item" :class="form.startTime ? 'is-filled' : 'is-empty'">
  35 + <template v-slot:body>
  36 + <view class="item-title"><text class="required">*</text><text>开始时间</text></view>
  37 + </template>
  38 + <template v-slot:footer>
  39 + <uni-datetime-picker type="datetime" v-model="form.startTime" :hide-second="true" :clear-icon="false" />
  40 + </template>
  41 + </uni-list-item>
  42 +
  43 + <!-- 截止时间 -->
  44 + <uni-list-item class="select-item" :class="form.endTime ? 'is-filled' : 'is-empty'">
  45 + <template v-slot:body>
  46 + <view class="item-title"><text class="required">*</text><text>截止时间</text></view>
  47 + </template>
  48 + <template v-slot:footer>
  49 + <uni-datetime-picker type="datetime" v-model="form.endTime" :hide-second="true" :clear-icon="false" />
  50 + </template>
  51 + </uni-list-item>
  52 +
  53 + <!-- 项目类型 -->
  54 + <uni-list-item class="select-item" :class="form.projectType ? 'is-filled' : 'is-empty'" clickable
  55 + @click="openDictSheet('projectType', 'PROJECT_TYPE', '项目类型')" :rightText="getProjectTypeLabel() || '请选择项目类型'" showArrow>
  56 + <template v-slot:body>
  57 + <view class="item-title"><text class="required">*</text><text>项目类型</text></view>
  58 + </template>
  59 + </uni-list-item>
  60 +
  61 + <!-- 开标时间 - 仅项目类型为集中开标时显示 -->
  62 + <uni-list-item v-if="isConcentratedBid" class="select-item" :class="form.bidTime ? 'is-filled' : 'is-empty'">
  63 + <template v-slot:body>
  64 + <view class="item-title"><text class="required">*</text><text>开标时间</text></view>
  65 + </template>
  66 + <template v-slot:footer>
  67 + <uni-datetime-picker type="datetime" v-model="form.bidTime" :hide-second="true" :clear-icon="false" />
  68 + </template>
  69 + </uni-list-item>
  70 +
  71 + <!-- 采购规则 -->
  72 + <uni-list-item class="select-item textarea-item" :class="form.purchaseRule ? 'is-filled' : 'is-empty'">
  73 + <template v-slot:body>
  74 + <view class="item-title"><text class="required">*</text><text>采购规则</text></view>
  75 + </template>
  76 + <template v-slot:footer>
  77 + <uni-easyinput type="textarea" v-model="form.purchaseRule" :inputBorder="false"
  78 + placeholder="请输入采购规则" :autoHeight="true" />
  79 + </template>
  80 + </uni-list-item>
  81 + </uni-list>
  82 + </scroll-view>
  83 +
  84 + <view class="footer">
  85 + <button class="btn submit" @click="onSubmit">保存</button>
  86 + </view>
  87 +
  88 + <SingleSelectSheet :visible.sync="sheet.visible" :title="sheet.title" :options="sheet.options"
  89 + v-model="sheet.value" @confirm="onSheetConfirm" />
  90 + </view>
  91 +</template>
  92 +
  93 +<script>
  94 +import SingleSelectSheet from '@/components/single-select/index.vue'
  95 +import { supplierQuotationProjectDetailApi, supplierQuotationProjectUpdateApi } from '@/api/procure-manage/supplierQuotationProject.js'
  96 +import { breedRelationshipQueryApi } from '@/api/procure-manage/salesmanQuotation.js'
  97 +import { getDicByCodes } from '@/utils/dic.js'
  98 +
  99 +export default {
  100 + name: 'ModifySupplierPrice',
  101 + components: { SingleSelectSheet },
  102 + data() {
  103 + return {
  104 + id: '',
  105 + form: {
  106 + id: '',
  107 + name: '',
  108 + code: '',
  109 + productId: '',
  110 + productName: '',
  111 + quantity: '',
  112 + startTime: '',
  113 + endTime: '',
  114 + projectType: '',
  115 + bidTime: '',
  116 + purchaseRule: ''
  117 + },
  118 + productList: [],
  119 + dictData: {},
  120 + sheet: {
  121 + visible: false,
  122 + title: '',
  123 + options: [],
  124 + value: '',
  125 + field: '',
  126 + isDict: false
  127 + }
  128 + }
  129 + },
  130 + computed: {
  131 + isConcentratedBid() {
  132 + const opts = this.getDictOptions('PROJECT_TYPE') || []
  133 + const m = opts.find(o => String(o.code != null ? o.code : o.value) === String(this.form.projectType))
  134 + const label = m ? (m.name || m.label || '') : ''
  135 + return label.indexOf('集中开标') > -1
  136 + }
  137 + },
  138 + onLoad(options) {
  139 + this.id = (options && options.id) ? options.id : ''
  140 + this.loadDictData().then(() => {
  141 + this.loadProducts().then(() => {
  142 + if (this.id) this.loadDetail()
  143 + })
  144 + })
  145 + },
  146 + methods: {
  147 + async loadDictData() {
  148 + try {
  149 + const res = await getDicByCodes(['PROJECT_TYPE'])
  150 + this.dictData = res || {}
  151 + } catch (e) {
  152 + this.dictData = {}
  153 + }
  154 + },
  155 + getDictOptions(dictCode) {
  156 + const data = this.dictData[dictCode]
  157 + return (data && data.data) ? data.data : (Array.isArray(data) ? data : [])
  158 + },
  159 + async loadProducts() {
  160 + try {
  161 + const res = await breedRelationshipQueryApi({ pageIndex: 1, pageSize: 9999 })
  162 + const _data = res && res.data ? res.data : {}
  163 + const list = _data.datas || _data.list || _data.records || []
  164 + this.productList = list.map(it => ({
  165 + label: it.productName || it.name || '',
  166 + value: it.productId || it.id || ''
  167 + }))
  168 + } catch (e) {
  169 + this.productList = []
  170 + }
  171 + },
  172 + getProjectTypeLabel() {
  173 + const opts = this.getDictOptions('PROJECT_TYPE') || []
  174 + const m = opts.find(o => String(o.code != null ? o.code : o.value) === String(this.form.projectType))
  175 + return m ? (m.name || m.label || '') : ''
  176 + },
  177 + openProductSheet() {
  178 + const opts = (this.productList || []).map(p => ({ label: p.label, value: p.value }))
  179 + const cur = this.form.productId
  180 + const match = opts.find(o => String(o.value) === String(cur))
  181 + this.sheet = {
  182 + visible: true,
  183 + title: '请选择品种',
  184 + options: opts,
  185 + value: match ? match.value : '',
  186 + field: 'productId',
  187 + isDict: false
  188 + }
  189 + },
  190 + openDictSheet(field, dictCode, title) {
  191 + const raw = this.getDictOptions(dictCode) || []
  192 + const opts = raw.map(o => ({ label: o.name || o.label || '', value: o.code != null ? o.code : (o.value != null ? o.value : '') }))
  193 + const cur = this.form[field]
  194 + const match = opts.find(o => String(o.value) === String(cur))
  195 + this.sheet = {
  196 + visible: true,
  197 + title: title || '请选择',
  198 + options: opts,
  199 + value: match ? match.value : '',
  200 + field,
  201 + isDict: true
  202 + }
  203 + },
  204 + onSheetConfirm({ value }) {
  205 + const field = this.sheet.field
  206 + if (!field) return
  207 + this.form[field] = value
  208 + if (field === 'productId') {
  209 + const m = (this.productList || []).find(p => String(p.value) === String(value))
  210 + this.form.productName = m ? m.label : ''
  211 + }
  212 + if (field === 'projectType' && !this.isConcentratedBid) {
  213 + this.form.bidTime = ''
  214 + }
  215 + },
  216 + onQuantityBlur() {
  217 + let v = this.form.quantity
  218 + if (v === '' || v == null) return
  219 + v = parseFloat(v)
  220 + if (isNaN(v)) {
  221 + this.form.quantity = ''
  222 + return
  223 + }
  224 + this.form.quantity = String(v)
  225 + },
  226 + formatDateTimeMinute(v) {
  227 + const s = v == null ? '' : String(v).trim()
  228 + if (!s) return ''
  229 + if (s.length >= 16) return s.slice(0, 16)
  230 + return s
  231 + },
  232 + formatDateTimeSecond(v) {
  233 + const s = v == null ? '' : String(v).trim()
  234 + if (!s) return ''
  235 + const minute = s.length >= 16 ? s.slice(0, 16) : s
  236 + return minute + ':00'
  237 + },
  238 + async loadDetail() {
  239 + try {
  240 + const res = await supplierQuotationProjectDetailApi({ id: this.id })
  241 + const d = (res && res.data) ? res.data : {}
  242 + // 时间格式截取到分钟用于选择器回显
  243 + if (d.startTime && d.startTime.length >= 16) d.startTime = d.startTime.slice(0, 16)
  244 + if (d.endTime && d.endTime.length >= 16) d.endTime = d.endTime.slice(0, 16)
  245 + if (d.bidTime && d.bidTime.length >= 16) d.bidTime = d.bidTime.slice(0, 16)
  246 + this.form = { ...this.form, ...d }
  247 + } catch (e) {
  248 + uni.showToast({ title: '加载详情失败', icon: 'none' })
  249 + }
  250 + },
  251 + validateRequired() {
  252 + const checks = [
  253 + { key: 'name', msg: '请输入项目名称' },
  254 + { key: 'productId', msg: '请选择品种' },
  255 + { key: 'quantity', msg: '请输入数量' },
  256 + { key: 'startTime', msg: '请选择开始时间' },
  257 + { key: 'endTime', msg: '请选择截止时间' },
  258 + { key: 'projectType', msg: '请选择项目类型' },
  259 + { key: 'purchaseRule', msg: '请输入采购规则' }
  260 + ]
  261 + for (const it of checks) {
  262 + const val = this.form[it.key]
  263 + const empty = (val === undefined || val === null || (typeof val === 'string' && val.trim() === ''))
  264 + if (empty) { uni.showToast({ title: it.msg, icon: 'none' }); return false }
  265 + }
  266 + if (this.isConcentratedBid && !this.form.bidTime) {
  267 + uni.showToast({ title: '请选择开标时间', icon: 'none' })
  268 + return false
  269 + }
  270 + if (isNaN(parseFloat(this.form.quantity))) {
  271 + uni.showToast({ title: '数量格式不正确', icon: 'none' })
  272 + return false
  273 + }
  274 + if (this.form.startTime && this.form.endTime && this.formatDateTimeMinute(this.form.startTime) > this.formatDateTimeMinute(this.form.endTime)) {
  275 + uni.showToast({ title: '开始时间不能晚于截止时间', icon: 'none' })
  276 + return false
  277 + }
  278 + return true
  279 + },
  280 + async onSubmit() {
  281 + if (!this.validateRequired()) return
  282 + const confirmRes = await new Promise(resolve => {
  283 + uni.showModal({ title: '提示', content: '确定保存修改吗?', confirmText: '确定', cancelText: '取消', success: resolve })
  284 + })
  285 + if (!(confirmRes && confirmRes.confirm)) return
  286 + const clean = (obj) => {
  287 + const out = {}
  288 + Object.keys(obj || {}).forEach(k => {
  289 + const v = obj[k]
  290 + const isEmptyString = typeof v === 'string' && v.trim() === ''
  291 + const isUndef = v === undefined || v === null
  292 + const isNaNNumber = typeof v === 'number' && isNaN(v)
  293 + if (!(isEmptyString || isUndef || isNaNNumber)) out[k] = v
  294 + })
  295 + return out
  296 + }
  297 + const payload = clean({
  298 + id: this.form.id,
  299 + name: this.form.name,
  300 + productId: this.form.productId,
  301 + productName: this.form.productName,
  302 + quantity: this.form.quantity,
  303 + startTime: this.formatDateTimeSecond(this.form.startTime),
  304 + endTime: this.formatDateTimeSecond(this.form.endTime),
  305 + projectType: this.form.projectType,
  306 + bidTime: this.isConcentratedBid ? this.formatDateTimeSecond(this.form.bidTime) : '',
  307 + purchaseRule: this.form.purchaseRule
  308 + })
  309 + try {
  310 + await supplierQuotationProjectUpdateApi(payload)
  311 + uni.showToast({ title: '保存成功', icon: 'none' })
  312 + try { uni.setStorageSync('SUPPLIER_PRICE_LIST_NEED_REFRESH', '1') } catch (e) {}
  313 + setTimeout(() => { uni.navigateBack() }, 400)
  314 + } catch (e) {
  315 + uni.showToast({ title: e.msg || '保存失败', icon: 'none' })
  316 + }
  317 + }
  318 + }
  319 +}
  320 +</script>
  321 +
  322 +<style lang="scss" scoped>
  323 +.page {
  324 + display: flex;
  325 + flex-direction: column;
  326 + height: 100%;
  327 +}
  328 +
  329 +.scroll {
  330 + flex: 1;
  331 + padding: 12rpx 0 200rpx !important;
  332 +}
  333 +
  334 +.footer {
  335 + z-index: 2;
  336 + position: fixed;
  337 + left: 0;
  338 + right: 0;
  339 + bottom: 0;
  340 + padding: 32rpx;
  341 + padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
  342 + background: #fff;
  343 + box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
  344 +
  345 + .btn {
  346 + height: 80rpx;
  347 + line-height: 80rpx;
  348 + border-radius: 12rpx;
  349 + font-size: 32rpx;
  350 + }
  351 +
  352 + .submit {
  353 + background: $theme-primary;
  354 + color: #fff;
  355 + }
  356 +}
  357 +
  358 +::v-deep .uni-list {
  359 + .uni-easyinput {
  360 + display: flex;
  361 +
  362 + .uni-input-input {
  363 + color: rgba(0, 0, 0, 0.9);
  364 + }
  365 + }
  366 +
  367 + .uni-input-placeholder {
  368 + z-index: 1;
  369 + }
  370 +
  371 + .uni-input-input {
  372 + background-color: #ffffff;
  373 + }
  374 +
  375 + background: transparent;
  376 +
  377 + &-item {
  378 + &__extra-text {
  379 + font-size: 32rpx;
  380 + }
  381 +
  382 + &__content-title {
  383 + font-size: 32rpx;
  384 + color: rgba(0, 0, 0, 0.9);
  385 + }
  386 +
  387 + &__container {
  388 + padding: 32rpx;
  389 +
  390 + .uni-easyinput {
  391 + &__placeholder-class {
  392 + font-size: 32rpx;
  393 + color: rgba(0, 0, 0, 0.4);
  394 + }
  395 +
  396 + &__content {
  397 + border: none;
  398 + background-color: #ffffff !important;
  399 +
  400 + &-input {
  401 + padding-left: 0 !important;
  402 + height: 48rpx;
  403 + line-height: 48rpx;
  404 + font-size: 32rpx;
  405 + }
  406 +
  407 + .content-clear-icon {
  408 + font-size: 44rpx !important;
  409 + }
  410 + }
  411 + }
  412 +
  413 + .item-title,
  414 + .uni-list-item__content {
  415 + flex: none;
  416 + min-height: 48rpx;
  417 + line-height: 48rpx;
  418 + font-size: 32rpx;
  419 + position: relative;
  420 + width: 240rpx;
  421 + margin-right: 32rpx;
  422 + color: rgba(0, 0, 0, 0.9);
  423 +
  424 + .required {
  425 + color: red;
  426 + position: absolute;
  427 + top: 50%;
  428 + transform: translateY(-50%);
  429 + left: -16rpx;
  430 + }
  431 + }
  432 + }
  433 +
  434 + &.select-item {
  435 + &.is-empty {
  436 + .uni-list-item__extra-text {
  437 + color: rgba(0, 0, 0, 0.4) !important;
  438 + }
  439 + }
  440 +
  441 + &.is-filled {
  442 + .uni-list-item__extra-text {
  443 + color: rgba(0, 0, 0, 0.9) !important;
  444 + }
  445 + }
  446 + }
  447 + }
  448 +}
  449 +
  450 +::v-deep .uni-datetime-picker {
  451 + flex: 1;
  452 +
  453 + .uni-input-wrapper {
  454 + justify-content: flex-end;
  455 + }
  456 +
  457 + .uni-input-input {
  458 + text-align: right;
  459 + font-size: 32rpx;
  460 + color: rgba(0, 0, 0, 0.9);
  461 + }
  462 +}
  463 +
  464 +.textarea-item {
  465 + ::v-deep .uni-list-item__container {
  466 + flex-direction: column;
  467 + align-items: stretch;
  468 +
  469 + .item-title {
  470 + width: auto;
  471 + margin-right: 0;
  472 + margin-bottom: 16rpx;
  473 + }
  474 +
  475 + .uni-easyinput {
  476 + width: 100%;
  477 + }
  478 +
  479 + .uni-textarea-textarea {
  480 + text-align: left;
  481 + min-height: 120rpx;
  482 + font-size: 32rpx;
  483 + color: rgba(0, 0, 0, 0.9);
  484 + }
  485 + }
  486 +}
  487 +</style>
  1 +<template>
  2 + <view class="page">
  3 + <scroll-view class="scroll" scroll-y>
  4 + <uni-list>
  5 + <uni-list-item class="select-item is-filled">
  6 + <template v-slot:body>
  7 + <view class="item-title">项目名称</view>
  8 + </template>
  9 + <template v-slot:footer>
  10 + <text class="readonly-text">{{ form.name || '-' }}</text>
  11 + </template>
  12 + </uni-list-item>
  13 + <uni-list-item class="select-item is-filled">
  14 + <template v-slot:body>
  15 + <view class="item-title">开始时间</view>
  16 + </template>
  17 + <template v-slot:footer>
  18 + <text class="readonly-text">{{ form.startTime || '-' }}</text>
  19 + </template>
  20 + </uni-list-item>
  21 + <uni-list-item class="select-item is-filled">
  22 + <template v-slot:body>
  23 + <view class="item-title">截止时间</view>
  24 + </template>
  25 + <template v-slot:footer>
  26 + <text class="readonly-text">{{ form.endTime || '-' }}</text>
  27 + </template>
  28 + </uni-list-item>
  29 + <!-- 品种 -->
  30 + <uni-list-item class="select-item is-filled">
  31 + <template v-slot:body>
  32 + <view class="item-title"><text class="required">*</text><text>品种</text></view>
  33 + </template>
  34 + <template v-slot:footer>
  35 + <text class="readonly-text">{{ form.productName || '-' }}</text>
  36 + </template>
  37 + </uni-list-item>
  38 +
  39 + <!-- 数量 -->
  40 + <uni-list-item class="select-item" :class="form.quantity !== '' && form.quantity != null ? 'is-filled' : 'is-empty'">
  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.quantity" type="digit" :inputBorder="false" placeholder="请输入数量" @blur="onQuantityBlur" />
  46 + </template>
  47 + </uni-list-item>
  48 +
  49 + <!-- 价格 -->
  50 + <uni-list-item class="select-item" :class="form.price !== '' && form.price != null ? 'is-filled' : 'is-empty'">
  51 + <template v-slot:body>
  52 + <view class="item-title"><text class="required">*</text><text>价格</text></view>
  53 + </template>
  54 + <template v-slot:footer>
  55 + <uni-easyinput v-model="form.price" type="digit" :inputBorder="false" placeholder="请输入价格" @blur="onPriceBlur" />
  56 + </template>
  57 + </uni-list-item>
  58 + </uni-list>
  59 + </scroll-view>
  60 +
  61 + <view class="footer">
  62 + <button class="btn submit" @click="onSubmit">提交报价</button>
  63 + </view>
  64 + </view>
  65 +</template>
  66 +
  67 +<script>
  68 +import { supplierQuotationProjectDetailApi, supplierQuotationProjectQuoteApi } from '@/api/procure-manage/supplierQuotationProject.js'
  69 +
  70 +export default {
  71 + name: 'QuoteSupplierPrice',
  72 + data() {
  73 + return {
  74 + projectId: '',
  75 + form: {
  76 + projectId: '',
  77 + productId: '',
  78 + productName: '',
  79 + quantity: '',
  80 + price: '',
  81 + startTime: '',
  82 + endTime: '',
  83 + name: ''
  84 + }
  85 + }
  86 + },
  87 + onLoad(options) {
  88 + this.projectId = (options && options.id) ? options.id : ''
  89 + this.form.projectId = this.projectId
  90 + if (!this.projectId) return
  91 + this.loadDetail()
  92 + },
  93 + methods: {
  94 + async loadDetail() {
  95 + try {
  96 + const res = await supplierQuotationProjectDetailApi({ id: this.projectId })
  97 + const d = (res && res.data) ? res.data : {}
  98 + this.form.productId = d.productId || ''
  99 + this.form.productName = d.productName || ''
  100 + this.form.name = d.name || ''
  101 + this.form.startTime = d.startTime?.substring(0, 16) || ''
  102 + this.form.endTime = d.endTime?.substring(0, 16) || ''
  103 + // 数量默认带出项目数量,可修改
  104 + if (d.quantity != null && d.quantity !== '') this.form.quantity = String(d.quantity)
  105 + } catch (e) {
  106 + uni.showToast({ title: '加载项目信息失败', icon: 'none' })
  107 + }
  108 + },
  109 + onQuantityBlur() {
  110 + let v = this.form.quantity
  111 + if (v === '' || v == null) return
  112 + v = parseFloat(v)
  113 + if (isNaN(v)) { this.form.quantity = ''; return }
  114 + this.form.quantity = String(v)
  115 + },
  116 + onPriceBlur() {
  117 + let v = this.form.price
  118 + if (v === '' || v == null) return
  119 + v = parseFloat(v)
  120 + if (isNaN(v)) { this.form.price = ''; return }
  121 + this.form.price = String(v)
  122 + },
  123 + validateRequired() {
  124 + if (!this.form.projectId) { uni.showToast({ title: '缺少项目ID', icon: 'none' }); return false }
  125 + if (!this.form.productId) { uni.showToast({ title: '缺少品种', icon: 'none' }); return false }
  126 + if (this.form.quantity === '' || this.form.quantity == null) { uni.showToast({ title: '请输入数量', icon: 'none' }); return false }
  127 + if (isNaN(parseFloat(this.form.quantity))) { uni.showToast({ title: '数量格式不正确', icon: 'none' }); return false }
  128 + if (this.form.price === '' || this.form.price == null) { uni.showToast({ title: '请输入价格', icon: 'none' }); return false }
  129 + if (isNaN(parseFloat(this.form.price))) { uni.showToast({ title: '价格格式不正确', icon: 'none' }); return false }
  130 + return true
  131 + },
  132 + async onSubmit() {
  133 + if (!this.validateRequired()) return
  134 + const confirmRes = await new Promise(resolve => {
  135 + uni.showModal({ title: '提示', content: '确定提交报价吗?', confirmText: '确定', cancelText: '取消', success: resolve })
  136 + })
  137 + if (!(confirmRes && confirmRes.confirm)) return
  138 + const payload = {
  139 + projectId: this.form.projectId,
  140 + productId: this.form.productId,
  141 + quantity: parseFloat(this.form.quantity),
  142 + price: parseFloat(this.form.price)
  143 + }
  144 + try {
  145 + await supplierQuotationProjectQuoteApi(payload)
  146 + uni.showToast({ title: '报价成功', icon: 'none' })
  147 + try { uni.setStorageSync('SUPPLIER_PRICE_LIST_NEED_REFRESH', '1') } catch (e) {}
  148 + setTimeout(() => { uni.navigateBack() }, 400)
  149 + } catch (e) {
  150 + uni.showToast({ title: e.msg || '报价失败', icon: 'none' })
  151 + }
  152 + }
  153 + }
  154 +}
  155 +</script>
  156 +
  157 +<style lang="scss" scoped>
  158 +.page {
  159 + display: flex;
  160 + flex-direction: column;
  161 + height: 100%;
  162 +}
  163 +
  164 +.scroll {
  165 + flex: 1;
  166 + padding: 12rpx 0 200rpx !important;
  167 +}
  168 +
  169 +.footer {
  170 + z-index: 2;
  171 + position: fixed;
  172 + left: 0;
  173 + right: 0;
  174 + bottom: 0;
  175 + padding: 32rpx;
  176 + padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
  177 + background: #fff;
  178 + box-shadow: 0 -8rpx 24rpx rgba(0, 0, 0, 0.06);
  179 +
  180 + .btn {
  181 + height: 80rpx;
  182 + line-height: 80rpx;
  183 + border-radius: 12rpx;
  184 + font-size: 32rpx;
  185 + }
  186 +
  187 + .submit {
  188 + background: $theme-primary;
  189 + color: #fff;
  190 + }
  191 +}
  192 +
  193 +::v-deep .uni-list {
  194 + .uni-easyinput {
  195 + display: flex;
  196 +
  197 + .uni-input-input {
  198 + color: rgba(0, 0, 0, 0.9);
  199 + }
  200 + }
  201 +
  202 + .uni-input-placeholder {
  203 + z-index: 1;
  204 + }
  205 +
  206 + .uni-input-input {
  207 + background-color: #ffffff;
  208 + }
  209 +
  210 + background: transparent;
  211 +
  212 + &-item {
  213 + &__extra-text {
  214 + font-size: 32rpx;
  215 + }
  216 +
  217 + &__content-title {
  218 + font-size: 32rpx;
  219 + color: rgba(0, 0, 0, 0.9);
  220 + }
  221 +
  222 + &__container {
  223 + padding: 32rpx;
  224 +
  225 + .uni-easyinput {
  226 + &__placeholder-class {
  227 + font-size: 32rpx;
  228 + color: rgba(0, 0, 0, 0.4);
  229 + }
  230 +
  231 + &__content {
  232 + border: none;
  233 + background-color: #ffffff !important;
  234 +
  235 + &-input {
  236 + padding-left: 0 !important;
  237 + height: 48rpx;
  238 + line-height: 48rpx;
  239 + font-size: 32rpx;
  240 + }
  241 +
  242 + .content-clear-icon {
  243 + font-size: 44rpx !important;
  244 + }
  245 + }
  246 + }
  247 +
  248 + .item-title,
  249 + .uni-list-item__content {
  250 + flex: none;
  251 + min-height: 48rpx;
  252 + line-height: 48rpx;
  253 + font-size: 32rpx;
  254 + position: relative;
  255 + width: 240rpx;
  256 + margin-right: 32rpx;
  257 + color: rgba(0, 0, 0, 0.9);
  258 +
  259 + .required {
  260 + color: red;
  261 + position: absolute;
  262 + top: 50%;
  263 + transform: translateY(-50%);
  264 + left: -16rpx;
  265 + }
  266 + }
  267 + }
  268 +
  269 + &.select-item {
  270 + .readonly-text {
  271 + font-size: 32rpx;
  272 + color: rgba(0, 0, 0, 0.9);
  273 + text-align: right;
  274 + }
  275 +
  276 + &.is-empty {
  277 + .uni-list-item__extra-text {
  278 + color: rgba(0, 0, 0, 0.4) !important;
  279 + }
  280 + }
  281 +
  282 + &.is-filled {
  283 + .uni-list-item__extra-text {
  284 + color: rgba(0, 0, 0, 0.9) !important;
  285 + }
  286 + }
  287 + }
  288 + }
  289 +}
  290 +</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;