Commit 0fb948ec84b4b4e9d0788b3fba2bdf59585f1f6c

Authored by gesilong
2 parents ec9822ff ae3cd638

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