Commit a20bf1a19bbad92a7cb7675a076da92ddfd62d19

Authored by gesilong
2 parents 1d7e8425 0d09d791

Merge branch 'cjerp-2.0-contract' into publish_cjerp

  1 +import request from '@/utils/request'
  2 +import { ContentTypeEnum } from '@/utils/httpEnum'
  3 +
  4 +export function domesticAgreementQueryApi(params) {
  5 + return request({
  6 + url: '/procurement/contract/query',
  7 + method: 'get',
  8 + params
  9 + })
  10 +}
  11 +
  12 +export function domesticAgreementDetailApi(params) {
  13 + return request({
  14 + url: '/procurement/contract',
  15 + method: 'get',
  16 + params
  17 + })
  18 +}
  19 +
  20 +export function domesticAgreementCancelApi(id) {
  21 + return request({
  22 + url: '/procurement/contract/cancel?id=' + encodeURIComponent(id || ''),
  23 + method: 'put'
  24 + })
  25 +}
  26 +
  27 +export function domesticAgreementSubmitApi(id) {
  28 + return request({
  29 + url: '/procurement/contract/submit?id=' + encodeURIComponent(id || ''),
  30 + method: 'put'
  31 + })
  32 +}
  33 +
  34 +export function domesticAgreementEditApi(data) {
  35 + return request({
  36 + url: '/procurement/contract',
  37 + method: 'put',
  38 + data,
  39 + contentType: ContentTypeEnum.JSON
  40 + })
  41 +}
@@ -517,6 +517,30 @@ @@ -517,6 +517,30 @@
517 } 517 }
518 }, 518 },
519 { 519 {
  520 + "path": "pages/domestic-agreement/index",
  521 + "style": {
  522 + "navigationBarTitleText": "内贸合同管理",
  523 + "navigationBarBackgroundColor": "#ffffff",
  524 + "navigationBarTextStyle": "black"
  525 + }
  526 + },
  527 + {
  528 + "path": "pages/domestic-agreement/detail",
  529 + "style": {
  530 + "navigationBarTitleText": "内贸合同详情",
  531 + "navigationBarBackgroundColor": "#ffffff",
  532 + "navigationBarTextStyle": "black"
  533 + }
  534 + },
  535 + {
  536 + "path": "pages/domestic-agreement/modify",
  537 + "style": {
  538 + "navigationBarTitleText": "编辑内贸合同",
  539 + "navigationBarBackgroundColor": "#ffffff",
  540 + "navigationBarTextStyle": "black"
  541 + }
  542 + },
  543 + {
520 "path": "pages/order_list/index", 544 "path": "pages/order_list/index",
521 "style": { 545 "style": {
522 "navigationBarTitleText": "订货单列表", 546 "navigationBarTitleText": "订货单列表",
  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.procurementPlanCode) }}</text>
  7 + <view class="row">
  8 + <text class="label">供方</text>
  9 + <text class="value">{{ safeText(form.supplierName) }}</text>
  10 + </view>
  11 + <view class="row">
  12 + <text class="label">需方</text>
  13 + <text class="value">{{ safeText(form.companyFullName) }}</text>
  14 + </view>
  15 + <view class="row">
  16 + <text class="label">签订地点</text>
  17 + <text class="value">{{ safeText(form.signPlace, '芜湖市') }}</text>
  18 + </view>
  19 + <view class="row">
  20 + <text class="label">签订时间</text>
  21 + <text class="value">{{ formatDateOnly(form.signTime) }}</text>
  22 + </view>
  23 + <view class="row">
  24 + <text class="label">投诉编码</text>
  25 + <text class="value">{{ safeText(form.procurementPlanCode) }}</text>
  26 + </view>
  27 + <view class="row">
  28 + <text class="label">结算比例</text>
  29 + <text class="value">{{ formatPercent(form.settlementRatio) }}</text>
  30 + </view>
  31 + <view v-if="form.createUserName" class="row">
  32 + <text class="label">发起人</text>
  33 + <text class="value">{{ safeText(form.createUserName) }}</text>
  34 + </view>
  35 + <view v-if="approvalStatusVisible" class="row">
  36 + <text class="label">审核状态</text>
  37 + <text class="value">{{ getAuditStatusText(form.approvalStatus) }}</text>
  38 + </view>
  39 + <view v-if="attachmentList.length" class="row">
  40 + <text class="label">附件</text>
  41 + <view class="attach-list">
  42 + <view v-for="(at, idx) in attachmentList" :key="idx" class="attach-item">
  43 + <text class="attach-name">{{ at.name }}</text>
  44 + <text class="attach-view" @click.stop="onPreviewAttachment(at)">查看</text>
  45 + </view>
  46 + </view>
  47 + </view>
  48 + </view>
  49 +
  50 + <ProductRel mode="view" :list="lineList" />
  51 +
  52 + <view v-if="form.specialAgreement" class="title-header">
  53 + <image class="title-header_icon" src="/static/images/title.png" />
  54 + <span>特殊约定</span>
  55 + </view>
  56 + <view v-if="form.specialAgreement" class="section">
  57 + <text class="remark-text">{{ safeText(form.specialAgreement) }}</text>
  58 + </view>
  59 + </view>
  60 + </scroll-view>
  61 + <detail-buttons :buttons="displayButtons" @click="handleButtonClick" />
  62 + </view>
  63 +</template>
  64 +
  65 +<script>
  66 +import DetailButtons from '@/components/detail-buttons/index.vue'
  67 +import ProductRel from './productRel.vue'
  68 +import { getInfo } from '@/api/login'
  69 +import { domesticAgreementCancelApi, domesticAgreementDetailApi, domesticAgreementSubmitApi } from '@/api/procure-manage/domesticAgreement.js'
  70 +import { getDicByCodes, getDicName } from '@/utils/dic.js'
  71 +
  72 +export default {
  73 + name: 'DomesticAgreementDetail',
  74 + components: { DetailButtons, ProductRel },
  75 + data() {
  76 + return {
  77 + id: '',
  78 + form: {},
  79 + lineList: [],
  80 + attachmentList: [],
  81 + dicOptions: {
  82 + AUDIT_STATUS: []
  83 + },
  84 + buttons: [{
  85 + text: '审核详情',
  86 + visible: true,
  87 + variant: 'outline',
  88 + event: 'auditDetail',
  89 + },{
  90 + text: '审核',
  91 + visible: true,
  92 + variant: 'primary',
  93 + event: 'audit',
  94 + },{
  95 + text: '编辑',
  96 + visible: true,
  97 + variant: 'outline',
  98 + event: 'edit',
  99 + },
  100 + {
  101 + text: '提交审核',
  102 + visible: true,
  103 + variant: 'outline',
  104 + event: 'submitAudit',
  105 + },
  106 + {
  107 + text: '取消',
  108 + visible: true,
  109 + variant: 'outline',
  110 + event: 'cancel',
  111 + }],
  112 + currentUserId: '',
  113 + }
  114 + },
  115 + computed: {
  116 + displayButtons() {
  117 + const row = this.form || {}
  118 + const status = row.approvalStatus
  119 + const isDraft = !status; //草稿
  120 + const isAudit = status === 'AUDIT';
  121 + const isPass = status === 'PASS';
  122 + const isRefuse = status === 'REFUSE';
  123 + return [
  124 + { ...this.buttons[0], visible: (isAudit || isPass)&& this.form?.showExamineDetail === true && this.$auth.hasPermi('agreement-manage:domestic-agreement:view') },
  125 + { ...this.buttons[1], visible: isAudit && this.form?.showExamine === true && this.$auth.hasPermi('agreement-manage:domestic-agreement:approve') },
  126 + { ...this.buttons[2], visible: this.$auth.hasPermi('agreement-manage:domestic-agreement:modify') },
  127 + { ...this.buttons[3], visible: (isDraft || isRefuse) && row.createById === this.currentUserId&& this.$auth.hasPermi('agreement-manage:domestic-agreement:submit') },
  128 + { ...this.buttons[4], visible:(isRefuse || !status) && this.currentUserId === row?.createById && this.$auth.hasPermi('agreement-manage:domestic-agreement:cancel') },
  129 + ]
  130 + },
  131 + approvalStatusVisible() {
  132 + return !!(this.form && this.form.approvalStatus)
  133 + }
  134 + },
  135 + onLoad(query) {
  136 + this.id = (query && query.id) || ''
  137 + this.loadDicData()
  138 + this.loadDetail()
  139 + this.getUserInfo()
  140 + },
  141 + onShow() {
  142 + let needRefresh = ''
  143 + try { needRefresh = uni.getStorageSync('DOMESTIC_AGREEMENT_DETAIL_NEED_REFRESH') } catch (e) {}
  144 + if (!needRefresh) return
  145 + try { uni.removeStorageSync('DOMESTIC_AGREEMENT_DETAIL_NEED_REFRESH') } catch (e) {}
  146 + this.loadDetail()
  147 + },
  148 + methods: {
  149 + handleButtonClick(btn) {
  150 + if (!btn || btn.disabled) return
  151 + var e = btn.event || ''
  152 + if (e === 'cancel') return this.onCancel()
  153 + if (e === 'submitAudit') return this.onSubmitAudit()
  154 + if (e === 'edit') return this.onEdit()
  155 + if (e === 'audit') return this.onAudit()
  156 + if (e === 'auditDetail') return this.onAuditDetail()
  157 + uni.showToast({ title: (btn.text || '操作') + '待实现', icon: 'none' })
  158 + },
  159 + onAudit() {
  160 + const id = this.id || (this.form && this.form.id) || ''
  161 + if (!id) return
  162 + const CACHE_KEY = 'sourceBusinessId'
  163 + const TYPE = 'contractType'
  164 + uni.setStorageSync(TYPE, 'PROCUREMENT_CONTRACT')
  165 + uni.setStorageSync(CACHE_KEY, id)
  166 + uni.navigateTo({ url: '/pages/flow/audit' })
  167 + },
  168 + onAuditDetail() {
  169 + const id = this.id || (this.form && this.form.id) || ''
  170 + if (!id) return
  171 + const CACHE_KEY = 'sourceBusinessId'
  172 + const TYPE = 'contractType'
  173 + uni.setStorageSync(TYPE, 'PROCUREMENT_CONTRACT')
  174 + uni.setStorageSync(CACHE_KEY, id)
  175 + uni.navigateTo({ url: '/pages/flow/audit_detail' })
  176 + },
  177 + onEdit() {
  178 + uni.navigateTo({
  179 + url: '/pages/domestic-agreement/modify?id=' + this.id
  180 + })
  181 + },
  182 + refreshListFlags() {
  183 + try { uni.setStorageSync('DOMESTIC_AGREEMENT_LIST_NEED_REFRESH', '1') } catch (e) {}
  184 + try { uni.setStorageSync('DOMESTIC_AGREEMENT_DETAIL_NEED_REFRESH', '1') } catch (e) {}
  185 + },
  186 + runWithConfirm(opts) {
  187 + var self = this
  188 + var title = opts.title || '提示'
  189 + var content = opts.content || ''
  190 + var run = opts.run
  191 + var loading = opts.loading || '处理中'
  192 + var successText = opts.successText || '操作成功'
  193 + var failText = opts.failText || '操作失败'
  194 + if (!this.id) {
  195 + uni.showToast({ title: '缺少合同ID', icon: 'none' })
  196 + return
  197 + }
  198 + var apiFn = function () {
  199 + uni.showLoading({ title: loading, mask: true })
  200 + Promise.resolve(run(self.id)).then(function () {
  201 + uni.hideLoading()
  202 + uni.showToast({ title: successText, icon: 'none' })
  203 + self.refreshListFlags()
  204 + self.loadDetail()
  205 + }).catch(function (e) {
  206 + uni.hideLoading()
  207 + uni.showToast({ title: (e && (e.msg || e.message)) ? (e.msg || e.message) : failText, icon: 'none' })
  208 + })
  209 + }
  210 + uni.showModal({
  211 + title: title,
  212 + content: content,
  213 + confirmText: '确定',
  214 + cancelText: '取消',
  215 + success: function (res) {
  216 + if (!res || !res.confirm) return
  217 + apiFn()
  218 + }
  219 + })
  220 + },
  221 + onCancel() {
  222 + this.runWithConfirm({
  223 + title: '取消',
  224 + content: '确认取消该合同吗?',
  225 + loading: '提交中',
  226 + successText: '取消成功',
  227 + failText: '取消失败',
  228 + run: function (id) { return domesticAgreementCancelApi(id) }
  229 + })
  230 + },
  231 + onSubmitAudit() {
  232 + this.runWithConfirm({
  233 + title: '提交审核',
  234 + content: '确认提交审核吗?',
  235 + loading: '提交中',
  236 + successText: '提交成功',
  237 + failText: '提交审核失败',
  238 + run: function (id) { return domesticAgreementSubmitApi(id) }
  239 + })
  240 + },
  241 + async getUserInfo() {
  242 + try {
  243 + var res = await getInfo()
  244 + this.currentUserId = (res && res.id) ? res.id : ''
  245 + } catch (e) {
  246 + this.currentUserId = ''
  247 + }
  248 + },
  249 + async loadDicData() {
  250 + try {
  251 + const results = await getDicByCodes(['AUDIT_STATUS'])
  252 + this.dicOptions.AUDIT_STATUS = (results.AUDIT_STATUS && results.AUDIT_STATUS.data) || []
  253 + } catch (e) {
  254 + this.dicOptions.AUDIT_STATUS = []
  255 + }
  256 + },
  257 + getAuditStatusText(status) {
  258 + const v = status == null ? '' : String(status)
  259 + if (!v) return '-'
  260 + return getDicName('AUDIT_STATUS', v, this.dicOptions.AUDIT_STATUS) || v
  261 + },
  262 + loadDetail() {
  263 + if (!this.id) {
  264 + uni.showToast({ title: '缺少合同ID', icon: 'none' })
  265 + return
  266 + }
  267 + uni.showLoading && uni.showLoading({ title: '加载中...', mask: true })
  268 + this.fetchDetail({ id: this.id })
  269 + .then(res => {
  270 + const data = res && res.data ? (res.data.data || res.data) : {}
  271 + this.form = Object.assign({}, data)
  272 + const lines = data && Array.isArray(data.procurementContractLineList) ? data.procurementContractLineList : []
  273 + this.lineList = lines.map(l => Object.assign({}, l))
  274 + this.buildAttachmentList()
  275 + this.notifyListRefresh()
  276 + })
  277 + .catch(() => {
  278 + uni.showToast({ title: '详情加载失败', icon: 'none' })
  279 + })
  280 + .finally(() => {
  281 + uni.hideLoading && uni.hideLoading()
  282 + })
  283 + },
  284 + fetchDetail(params) {
  285 + const self = this
  286 + return domesticAgreementDetailApi(params).catch(function () {
  287 + return self.mockDetail(params)
  288 + })
  289 + },
  290 + mockDetail(params) {
  291 + return new Promise(resolve => {
  292 + const mock = {
  293 + id: params.id,
  294 + procurementPlanId: 'plan-' + params.id,
  295 + procurementPlanCode: 'NMHT2026000' + params.id,
  296 + supplierId: 'S001',
  297 + supplierName: '上海钢材贸易有限公司',
  298 + companyShortId: 'C001',
  299 + companyFullName: '北京远大集团有限公司',
  300 + signPlace: '芜湖市',
  301 + signTime: '2026-08-15',
  302 + deliveryTime: '2026-09-20',
  303 + settlementRatio: 80,
  304 + specialAgreement: '1. 本合同项下货物质量须符合国标及双方约定的技术标准;\n2. 如出现质量问题,供方须在3个工作日内响应处理;\n3. 价格波动超过±5%时,双方另行协商结算。',
  305 + attachmentFileIds: 'f1,f2',
  306 + attachmentFileNames: '内贸合同扫描件.pdf,补充条款.docx',
  307 + createUserName: '张三',
  308 + approvalStatus: 'AUDIT',
  309 + procurementContractLineList: [
  310 + {
  311 + id: 'L1',
  312 + contractId: params.id,
  313 + itemId: 'I1',
  314 + planLineId: 'PL1',
  315 + invoiceProductName: '废铜',
  316 + productId: 'P001',
  317 + productName: '光亮铜',
  318 + quantity: '25.500',
  319 + unitPrice: '62500.00',
  320 + deliveryTime: '2026-09-15'
  321 + },
  322 + {
  323 + id: 'L2',
  324 + contractId: params.id,
  325 + itemId: 'I2',
  326 + planLineId: 'PL2',
  327 + invoiceProductName: '废铜',
  328 + productId: 'P002',
  329 + productName: '一特铜',
  330 + quantity: '18.200',
  331 + unitPrice: '61800.00',
  332 + deliveryTime: '2026-09-20'
  333 + },
  334 + {
  335 + id: 'L3',
  336 + contractId: params.id,
  337 + itemId: 'I3',
  338 + planLineId: 'PL3',
  339 + invoiceProductName: '废铝',
  340 + productId: 'P003',
  341 + productName: '高纯铝锭',
  342 + quantity: '42.000',
  343 + unitPrice: '19800.00',
  344 + deliveryTime: '2026-09-25'
  345 + }
  346 + ]
  347 + }
  348 + setTimeout(() => resolve({ data: { data: mock } }), 200)
  349 + })
  350 + },
  351 + buildAttachmentList() {
  352 + const ids = this.form.attachmentFileIds
  353 + const names = this.form.attachmentFileNames
  354 + const idArr = (typeof ids === 'string' && ids) ? ids.split(',').filter(Boolean) : (Array.isArray(ids) ? ids : [])
  355 + const nameArr = (typeof names === 'string' && names) ? names.split(',').filter(Boolean) : (Array.isArray(names) ? names : [])
  356 + const len = Math.max(idArr.length, nameArr.length)
  357 + const list = []
  358 + for (let i = 0; i < len; i++) {
  359 + list.push({ id: idArr[i] || '', name: nameArr[i] || ('附件' + (i + 1)) })
  360 + }
  361 + this.attachmentList = list
  362 + },
  363 + notifyListRefresh() {
  364 + try { uni.setStorageSync('DOMESTIC_AGREEMENT_LIST_NEED_REFRESH', '1') } catch (e) {}
  365 + },
  366 + safeText(v, def) {
  367 + const s = v == null ? '' : String(v)
  368 + if (s.trim()) return s
  369 + return def != null ? def : '-'
  370 + },
  371 + formatDateOnly(v) {
  372 + const s = v == null ? '' : String(v).trim()
  373 + if (!s) return '-'
  374 + if (s.length >= 10) return s.slice(0, 10)
  375 + return s
  376 + },
  377 + formatPercent(v) {
  378 + if (v === null || v === undefined || v === '') return '-'
  379 + const n = Number(v)
  380 + if (isNaN(n)) return String(v)
  381 + return n + '%'
  382 + },
  383 + onPreviewAttachment(at) {
  384 + if (!at || !at.id) {
  385 + uni.showToast({ title: '无法查看', icon: 'none' })
  386 + return
  387 + }
  388 + uni.showToast({ title: '查看附件 ' + at.name, icon: 'none' })
  389 + }
  390 + }
  391 +}
  392 +</script>
  393 +
  394 +<style lang="scss" scoped>
  395 +.page {
  396 + display: flex;
  397 + flex-direction: column;
  398 + height: 100%;
  399 +}
  400 +.scroll {
  401 + flex: 1;
  402 + padding: 8rpx 0 144rpx;
  403 +}
  404 +.detail-page {
  405 + background: #f3f3f3;
  406 +}
  407 +.title-header {
  408 + display: flex;
  409 + align-items: center;
  410 + padding: 24rpx 32rpx 12rpx;
  411 + background-color: #ffffff;
  412 + .title-header_icon {
  413 + width: 36rpx;
  414 + height: 36rpx;
  415 + margin-right: 12rpx;
  416 + }
  417 + span {
  418 + font-size: 34rpx;
  419 + font-weight: 600;
  420 + color: rgba(0, 0, 0, 0.9);
  421 + }
  422 +}
  423 +.section {
  424 + padding: 32rpx;
  425 + background: #fff;
  426 + margin-bottom: 20rpx;
  427 +}
  428 +.row {
  429 + display: flex;
  430 + margin-bottom: 28rpx;
  431 + &:last-child { margin-bottom: 0; }
  432 + &.company {
  433 + font-size: 36rpx;
  434 + font-weight: 600;
  435 + color: rgba(0, 0, 0, 0.9);
  436 + padding-top: 8rpx;
  437 + line-height: 50rpx;
  438 + }
  439 + .label {
  440 + max-width: 420rpx;
  441 + margin-right: 20rpx;
  442 + line-height: 32rpx;
  443 + font-size: 28rpx;
  444 + color: rgba(0, 0, 0, 0.6);
  445 + }
  446 + .value {
  447 + flex: 1;
  448 + line-height: 32rpx;
  449 + font-size: 28rpx;
  450 + color: rgba(0, 0, 0, 0.9);
  451 + text-align: right;
  452 + word-break: break-all;
  453 + }
  454 +}
  455 +.attach-list {
  456 + flex: 1;
  457 + display: flex;
  458 + flex-direction: column;
  459 + align-items: flex-end;
  460 + gap: 8rpx;
  461 +}
  462 +.attach-item {
  463 + display: flex;
  464 + align-items: center;
  465 + gap: 16rpx;
  466 + .attach-name {
  467 + font-size: 26rpx;
  468 + color: rgba(0, 0, 0, 0.9);
  469 + }
  470 + .attach-view {
  471 + font-size: 26rpx;
  472 + color: $theme-primary;
  473 + }
  474 +}
  475 +.remark-text {
  476 + font-size: 28rpx;
  477 + color: rgba(0, 0, 0, 0.9);
  478 + line-height: 1.6;
  479 + white-space: pre-wrap;
  480 +}
  481 +.empty-tip {
  482 + text-align: center;
  483 + font-size: 28rpx;
  484 + color: rgba(0, 0, 0, 0.4);
  485 + padding: 24rpx 0;
  486 +}
  487 +</style>
  1 +<template>
  2 + <view class="detail-page">
  3 + <scroll-view class="scroll" scroll-y>
  4 + <view class="section">
  5 + <text class="row company">{{ safeText(form.procurementPlanCode) }}</text>
  6 + <view class="row">
  7 + <text class="label">供方</text>
  8 + <text class="value">{{ safeText(form.supplierName) }}</text>
  9 + </view>
  10 + <view class="row">
  11 + <text class="label">需方</text>
  12 + <text class="value">{{ safeText(form.companyFullName) }}</text>
  13 + </view>
  14 + <view class="row">
  15 + <text class="label">签订地点</text>
  16 + <text class="value">{{ safeText(form.signPlace, '芜湖市') }}</text>
  17 + </view>
  18 + <view class="row">
  19 + <text class="label">签订时间</text>
  20 + <text class="value">{{ formatDateOnly(form.signTime) }}</text>
  21 + </view>
  22 + <view class="row">
  23 + <text class="label">投诉编码</text>
  24 + <text class="value">{{ safeText(form.procurementPlanCode) }}</text>
  25 + </view>
  26 + <view class="row">
  27 + <text class="label">结算比例</text>
  28 + <text class="value">{{ formatPercent(form.settlementRatio) }}</text>
  29 + </view>
  30 + <view v-if="form.createUserName" class="row">
  31 + <text class="label">发起人</text>
  32 + <text class="value">{{ safeText(form.createUserName) }}</text>
  33 + </view>
  34 + <view v-if="approvalStatusVisible" class="row">
  35 + <text class="label">审核状态</text>
  36 + <text class="value">{{ getAuditStatusText(form.approvalStatus) }}</text>
  37 + </view>
  38 + <view v-if="attachmentList.length" class="row">
  39 + <text class="label">附件</text>
  40 + <view class="attach-list">
  41 + <view v-for="(at, idx) in attachmentList" :key="idx" class="attach-item">
  42 + <text class="attach-name">{{ at.name }}</text>
  43 + <text class="attach-view" @click.stop="onPreviewAttachment(at)">查看</text>
  44 + </view>
  45 + </view>
  46 + </view>
  47 + </view>
  48 +
  49 + <ProductRel mode="view" :list="lineList" />
  50 +
  51 + <view v-if="form.specialAgreement" class="title-header">
  52 + <image class="title-header_icon" src="/static/images/title.png" />
  53 + <span>特殊约定</span>
  54 + </view>
  55 + <view v-if="form.specialAgreement" class="section">
  56 + <text class="remark-text">{{ safeText(form.specialAgreement) }}</text>
  57 + </view>
  58 + </scroll-view>
  59 + </view>
  60 +</template>
  61 +
  62 +<script>
  63 +import ProductRel from './productRel.vue'
  64 +import { domesticAgreementDetailApi } from '@/api/procure-manage/domesticAgreement.js'
  65 +import { getDicByCodes, getDicName } from '@/utils/dic.js'
  66 +
  67 +export default {
  68 + name: 'DomesticAgreementViewer',
  69 + components: { ProductRel },
  70 + props: {
  71 + id: { type: [String, Number], default: '' }
  72 + },
  73 + data() {
  74 + return {
  75 + form: {},
  76 + lineList: [],
  77 + attachmentList: [],
  78 + dicOptions: {
  79 + AUDIT_STATUS: []
  80 + }
  81 + }
  82 + },
  83 + computed: {
  84 + approvalStatusVisible() {
  85 + return !!(this.form && this.form.approvalStatus)
  86 + }
  87 + },
  88 + created() {
  89 + this.loadDicData()
  90 + },
  91 + watch: {
  92 + id: {
  93 + immediate: true,
  94 + handler(val) {
  95 + const v = (val !== undefined && val !== null) ? String(val) : ''
  96 + if (v) this.loadDetail(v)
  97 + }
  98 + }
  99 + },
  100 + methods: {
  101 + async loadDicData() {
  102 + try {
  103 + const results = await getDicByCodes(['AUDIT_STATUS'])
  104 + this.dicOptions.AUDIT_STATUS = (results.AUDIT_STATUS && results.AUDIT_STATUS.data) || []
  105 + } catch (e) {
  106 + this.dicOptions.AUDIT_STATUS = []
  107 + }
  108 + },
  109 + getAuditStatusText(status) {
  110 + const v = status == null ? '' : String(status)
  111 + if (!v) return '-'
  112 + return getDicName('AUDIT_STATUS', v, this.dicOptions.AUDIT_STATUS) || v
  113 + },
  114 + safeText(v, def) {
  115 + const s = v == null ? '' : String(v)
  116 + if (s.trim()) return s
  117 + return def != null ? def : '-'
  118 + },
  119 + formatDateOnly(v) {
  120 + const s = v == null ? '' : String(v).trim()
  121 + if (!s) return '-'
  122 + if (s.length >= 10) return s.slice(0, 10)
  123 + return s
  124 + },
  125 + formatPercent(v) {
  126 + if (v === null || v === undefined || v === '') return '-'
  127 + const n = Number(v)
  128 + if (isNaN(n)) return String(v)
  129 + return n + '%'
  130 + },
  131 + buildAttachmentList() {
  132 + const ids = this.form.attachmentFileIds
  133 + const names = this.form.attachmentFileNames
  134 + const idArr = (typeof ids === 'string' && ids) ? ids.split(',').filter(Boolean) : (Array.isArray(ids) ? ids : [])
  135 + const nameArr = (typeof names === 'string' && names) ? names.split(',').filter(Boolean) : (Array.isArray(names) ? names : [])
  136 + const len = Math.max(idArr.length, nameArr.length)
  137 + const list = []
  138 + for (let i = 0; i < len; i++) {
  139 + list.push({ id: idArr[i] || '', name: nameArr[i] || ('附件' + (i + 1)) })
  140 + }
  141 + this.attachmentList = list
  142 + },
  143 + getFormValues() {
  144 + return JSON.parse(JSON.stringify(this.form || {}))
  145 + },
  146 + checkForm() {
  147 + return true
  148 + },
  149 + async loadDetail(id) {
  150 + try {
  151 + const res = await domesticAgreementDetailApi({ id: id })
  152 + const data = res && res.data ? (res.data.data || res.data) : {}
  153 + this.form = Object.assign({}, data)
  154 + const lines = data && Array.isArray(data.procurementContractLineList) ? data.procurementContractLineList : []
  155 + this.lineList = lines.map(l => Object.assign({}, l))
  156 + this.buildAttachmentList()
  157 + } catch (e) {
  158 + this.form = {}
  159 + this.lineList = []
  160 + this.attachmentList = []
  161 + }
  162 + },
  163 + onPreviewAttachment(at) {
  164 + if (!at || !at.id) {
  165 + uni.showToast({ title: '无法查看', icon: 'none' })
  166 + return
  167 + }
  168 + uni.showToast({ title: '查看附件 ' + at.name, icon: 'none' })
  169 + }
  170 + }
  171 +}
  172 +</script>
  173 +
  174 +<style lang="scss" scoped>
  175 +.detail-page {
  176 + background: #f3f3f3;
  177 +}
  178 +.scroll {
  179 + flex: 1;
  180 + padding: 8rpx 0 144rpx;
  181 +}
  182 +.title-header {
  183 + display: flex;
  184 + align-items: center;
  185 + padding: 24rpx 32rpx 12rpx;
  186 + background-color: #ffffff;
  187 + .title-header_icon {
  188 + width: 36rpx;
  189 + height: 36rpx;
  190 + margin-right: 12rpx;
  191 + }
  192 + span {
  193 + font-size: 34rpx;
  194 + font-weight: 600;
  195 + color: rgba(0, 0, 0, 0.9);
  196 + }
  197 +}
  198 +.section {
  199 + padding: 32rpx;
  200 + background: #fff;
  201 + margin-bottom: 20rpx;
  202 +}
  203 +.row {
  204 + display: flex;
  205 + margin-bottom: 28rpx;
  206 + &:last-child { margin-bottom: 0; }
  207 + &.company {
  208 + font-size: 36rpx;
  209 + font-weight: 600;
  210 + color: rgba(0, 0, 0, 0.9);
  211 + padding-top: 8rpx;
  212 + line-height: 50rpx;
  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 + .value {
  222 + flex: 1;
  223 + line-height: 32rpx;
  224 + font-size: 28rpx;
  225 + color: rgba(0, 0, 0, 0.9);
  226 + text-align: right;
  227 + word-break: break-all;
  228 + }
  229 +}
  230 +.attach-list {
  231 + flex: 1;
  232 + display: flex;
  233 + flex-direction: column;
  234 + align-items: flex-end;
  235 + gap: 8rpx;
  236 +}
  237 +.attach-item {
  238 + display: flex;
  239 + align-items: center;
  240 + gap: 16rpx;
  241 + .attach-name {
  242 + font-size: 26rpx;
  243 + color: rgba(0, 0, 0, 0.9);
  244 + }
  245 + .attach-view {
  246 + font-size: 26rpx;
  247 + color: $theme-primary;
  248 + }
  249 +}
  250 +.remark-text {
  251 + font-size: 28rpx;
  252 + color: rgba(0, 0, 0, 0.9);
  253 + line-height: 1.6;
  254 + white-space: pre-wrap;
  255 +}
  256 +</style>
  1 +<template>
  2 + <view class="page">
  3 + <view class="agreement-list-fixed">
  4 + <view class="search-row">
  5 + <uni-search-bar
  6 + v-model="searchKeyword"
  7 + radius="6"
  8 + placeholder="请输入供方/需方名称或投诉编码"
  9 + clearButton="auto"
  10 + cancelButton="none"
  11 + bgColor="#F3F3F3"
  12 + textColor="rgba(0,0,0,0.4)"
  13 + @confirm="search"
  14 + @input="onSearchInput"
  15 + />
  16 + <view class="tool-icons">
  17 + <image class="tool-icon" src="/static/images/dev_manage/filter_icon.png" @click="openFilter" />
  18 + </view>
  19 + </view>
  20 +
  21 + <view class="tabs">
  22 + <view
  23 + v-for="(t, i) in tabs"
  24 + :key="i"
  25 + :class="['tab', { active: deleted === t.value }]"
  26 + @click="switchTab(t)"
  27 + >
  28 + {{ t.label }}
  29 + </view>
  30 + </view>
  31 + </view>
  32 +
  33 + <view class="list-box">
  34 + <card-list
  35 + ref="cardRef"
  36 + :fetch-fn="fetchList"
  37 + :query="query"
  38 + :extra="extraParams"
  39 + row-key="id"
  40 + :enable-refresh="true"
  41 + :enable-load-more="true"
  42 + @loaded="onCardLoaded"
  43 + @error="onCardError"
  44 + >
  45 + <template v-slot="{ item }">
  46 + <view class="card" @click="onCardClick(item)">
  47 + <view class="card-header">
  48 + <text class="title omit2">{{ item.procurementPlanCode || '-' }}</text>
  49 + <text :class="['status', `status_${item.approvalStatus}`]">{{ getAuditStatusText(item.approvalStatus) }}</text>
  50 + </view>
  51 + <view class="info-row">
  52 + <text>供方</text>
  53 + <text>{{ item.supplierName || '-' }}</text>
  54 + </view>
  55 + <view class="info-row">
  56 + <text>需方</text>
  57 + <text>{{ item.companyFullName || '-' }}</text>
  58 + </view>
  59 + <view class="info-row">
  60 + <text>签订时间</text>
  61 + <text>{{ formatDateOnly(item.signTime) }}</text>
  62 + </view>
  63 + <view class="info-row">
  64 + <text>发起人</text>
  65 + <text>{{ item.originatorName || '-' }}</text>
  66 + </view>
  67 + </view>
  68 + </template>
  69 + </card-list>
  70 + </view>
  71 +
  72 + <filter-modal
  73 + :visible.sync="filterVisible"
  74 + :value.sync="filterForm"
  75 + title="筛选"
  76 + @reset="onFilterReset"
  77 + @confirm="onFilterConfirm"
  78 + >
  79 + <template v-slot="{ model }">
  80 + <view class="filter-form">
  81 + <view class="form-item">
  82 + <view class="label">供方</view>
  83 + <input class="input" v-model="model.supplierName" placeholder="请输入供应商名称" />
  84 + </view>
  85 + <view class="form-item">
  86 + <view class="label">需方</view>
  87 + <input class="input" v-model="model.companyFullName" placeholder="请输入公司名称(全称)" />
  88 + </view>
  89 + <view class="form-item">
  90 + <view class="label">投诉编码</view>
  91 + <input class="input" v-model="model.procurementPlanCode" placeholder="请输入计划单编号" />
  92 + </view>
  93 + <view class="form-item">
  94 + <view class="label">审核状态</view>
  95 + <uni-data-checkbox mode="tag" :multiple="false" :value-field="'value'" :text-field="'text'"
  96 + v-model="model.approvalStatus" @change="onApprovalStatusChange" :localdata="approvalStatusOptions" />
  97 + </view>
  98 + <view class="form-item">
  99 + <view class="label">签订时间</view>
  100 + <uni-datetime-picker type="daterange" v-model="model.signTimeRange" start="2023-01-01" />
  101 + </view>
  102 + <view class="form-item">
  103 + <view class="label">交货日期</view>
  104 + <uni-datetime-picker type="daterange" v-model="model.deliveryTimeRange" start="2023-01-01" />
  105 + </view>
  106 + </view>
  107 + </template>
  108 + </filter-modal>
  109 + </view>
  110 +</template>
  111 +
  112 +<script>
  113 +import CardList from '@/components/card/index.vue'
  114 +import FilterModal from '@/components/filter/index.vue'
  115 +import { getDicByCodes, getDicName } from '@/utils/dic.js'
  116 +import { domesticAgreementQueryApi } from '@/api/procure-manage/domesticAgreement.js'
  117 +
  118 +export default {
  119 + name: 'DomesticAgreement',
  120 + components: { CardList, FilterModal },
  121 + data() {
  122 + return {
  123 + searchKeyword: '',
  124 + searchKeywordDebounced: '',
  125 + searchDebounceTimer: null,
  126 + deleted: 0,
  127 + tabs: [
  128 + { label: '有效合同', value: 0 },
  129 + { label: '废除合同', value: 1 }
  130 + ],
  131 + query: {
  132 + supplierName: '',
  133 + companyFullName: '',
  134 + procurementPlanCode: '',
  135 + approvalStatus: '',
  136 + signTimeRange: [],
  137 + deliveryTimeRange: []
  138 + },
  139 + extraParams: {},
  140 + currentItems: [],
  141 + filterVisible: false,
  142 + filterForm: {
  143 + supplierName: '',
  144 + companyFullName: '',
  145 + procurementPlanCode: '',
  146 + approvalStatus: '',
  147 + signTimeRange: [],
  148 + deliveryTimeRange: []
  149 + },
  150 + approvalStatusOptions: [
  151 + { value: '', text: '全部' }
  152 + ],
  153 + dicOptions: {
  154 + AUDIT_STATUS: []
  155 + }
  156 + }
  157 + },
  158 + computed: {
  159 + extraCombined() {
  160 + const keyword = this.searchKeywordDebounced || ''
  161 + return {
  162 + keyword: keyword || undefined,
  163 + deleted: this.deleted
  164 + }
  165 + }
  166 + },
  167 + watch: {
  168 + extraCombined: {
  169 + deep: true,
  170 + handler(v) {
  171 + this.extraParams = v
  172 + },
  173 + immediate: true
  174 + }
  175 + },
  176 + created() {
  177 + this.loadDicData()
  178 + },
  179 + onShow() {
  180 + let needRefresh = ''
  181 + try { needRefresh = uni.getStorageSync('DOMESTIC_AGREEMENT_LIST_NEED_REFRESH') } catch (e) {}
  182 + if (!needRefresh) return
  183 + try { uni.removeStorageSync('DOMESTIC_AGREEMENT_LIST_NEED_REFRESH') } catch (e) {}
  184 + if (this.$refs && this.$refs.cardRef && this.$refs.cardRef.reload) {
  185 + this.$refs.cardRef.reload()
  186 + }
  187 + },
  188 + onReachBottom() {
  189 + if (this.$refs && this.$refs.cardRef && this.$refs.cardRef.onLoadMore) {
  190 + this.$refs.cardRef.onLoadMore()
  191 + }
  192 + },
  193 + beforeDestroy() {
  194 + if (this.searchDebounceTimer) {
  195 + clearTimeout(this.searchDebounceTimer)
  196 + this.searchDebounceTimer = null
  197 + }
  198 + },
  199 + methods: {
  200 + async loadDicData() {
  201 + try {
  202 + const results = await getDicByCodes(['AUDIT_STATUS'])
  203 + this.dicOptions.AUDIT_STATUS = (results.AUDIT_STATUS && results.AUDIT_STATUS.data) || []
  204 + const list = Array.isArray(this.dicOptions.AUDIT_STATUS) ? this.dicOptions.AUDIT_STATUS : []
  205 + this.approvalStatusOptions = [{ value: '', text: '全部' }].concat(
  206 + list.map(it => ({ value: it && it.code != null ? String(it.code) : '', text: it && it.name != null ? String(it.name) : '' })).filter(it => it.value)
  207 + )
  208 + } catch (e) {
  209 + this.dicOptions.AUDIT_STATUS = []
  210 + this.approvalStatusOptions = [{ value: '', text: '全部' }]
  211 + }
  212 + },
  213 + getAuditStatusText(status) {
  214 + const v = status == null ? '' : String(status)
  215 + if (!v) return '-'
  216 + return getDicName('AUDIT_STATUS', v, this.dicOptions.AUDIT_STATUS) || v
  217 + },
  218 + formatDateOnly(v) {
  219 + const s = v == null ? '' : String(v).trim()
  220 + if (!s) return '-'
  221 + if (s.length >= 10) return s.slice(0, 10)
  222 + return s
  223 + },
  224 + onCardLoaded(_ref) {
  225 + var items = _ref.items
  226 + this.currentItems = items
  227 + },
  228 + onCardError() {
  229 + uni.showToast({ title: '列表加载失败', icon: 'none' })
  230 + },
  231 + onSearchInput() {
  232 + var _this = this
  233 + if (this.searchDebounceTimer) clearTimeout(this.searchDebounceTimer)
  234 + this.searchDebounceTimer = setTimeout(function () {
  235 + _this.searchKeywordDebounced = _this.searchKeyword
  236 + _this.searchDebounceTimer = null
  237 + }, 1200)
  238 + },
  239 + search(e) {
  240 + const val = e && e.value != null ? e.value : this.searchKeyword
  241 + this.searchKeyword = val
  242 + this.searchKeywordDebounced = val
  243 + },
  244 + switchTab(t) {
  245 + this.deleted = t.value
  246 + },
  247 + openFilter() {
  248 + this.filterForm = {
  249 + supplierName: this.query.supplierName || '',
  250 + companyFullName: this.query.companyFullName || '',
  251 + procurementPlanCode: this.query.procurementPlanCode || '',
  252 + approvalStatus: this.query.approvalStatus || '',
  253 + signTimeRange: Array.isArray(this.query.signTimeRange) ? this.query.signTimeRange.slice(0) : [],
  254 + deliveryTimeRange: Array.isArray(this.query.deliveryTimeRange) ? this.query.deliveryTimeRange.slice(0) : []
  255 + }
  256 + this.filterVisible = true
  257 + },
  258 + onFilterReset(payload) {
  259 + this.filterForm = payload
  260 + },
  261 + onFilterConfirm(payload) {
  262 + this.query = {
  263 + supplierName: payload.supplierName || '',
  264 + companyFullName: payload.companyFullName || '',
  265 + procurementPlanCode: payload.procurementPlanCode || '',
  266 + approvalStatus: payload.approvalStatus || '',
  267 + signTimeRange: Array.isArray(payload.signTimeRange) ? payload.signTimeRange : [],
  268 + deliveryTimeRange: Array.isArray(payload.deliveryTimeRange) ? payload.deliveryTimeRange : []
  269 + }
  270 + },
  271 + onApprovalStatusChange(e) {
  272 + const raw = e && e.detail && e.detail.value !== undefined ? e.detail.value : (e && e.value !== undefined ? e.value : '')
  273 + this.filterForm.approvalStatus = raw
  274 + },
  275 + fetchList(_ref2) {
  276 + var _this2 = this
  277 + var pageIndex = _ref2.pageIndex
  278 + var pageSize = _ref2.pageSize
  279 + var query = _ref2.query
  280 + var extra = _ref2.extra
  281 + const q = query || {}
  282 + const e = extra || {}
  283 + const signRange = Array.isArray(q.signTimeRange) ? q.signTimeRange : []
  284 + const deliveryRange = Array.isArray(q.deliveryTimeRange) ? q.deliveryTimeRange : []
  285 + const keyword = e.keyword || ''
  286 + const params = {
  287 + pageIndex: pageIndex,
  288 + pageSize: pageSize,
  289 + deleted: e.deleted != null ? e.deleted : 0,
  290 + supplierName: q.supplierName || '',
  291 + companyFullName: q.companyFullName || '',
  292 + procurementPlanCode: q.procurementPlanCode || '',
  293 + approvalStatus: q.approvalStatus || '',
  294 + startSignTime: signRange && signRange.length === 2 ? signRange[0] : '',
  295 + endSignTime: signRange && signRange.length === 2 ? signRange[1] : '',
  296 + startDeliveryTime: deliveryRange && deliveryRange.length === 2 ? deliveryRange[0] : '',
  297 + endDeliveryTime: deliveryRange && deliveryRange.length === 2 ? deliveryRange[1] : ''
  298 + }
  299 + if (keyword) {
  300 + if (!params.supplierName) params.supplierName = keyword
  301 + if (!params.companyFullName) params.companyFullName = keyword
  302 + if (!params.procurementPlanCode) params.procurementPlanCode = keyword
  303 + }
  304 + return domesticAgreementQueryApi(params).then(function (res) {
  305 + const _data = res && res.data ? res.data : {}
  306 + let records = _data.datas || _data.list || _data.records || []
  307 + const totalCount = _data.totalCount || _data.count || 0
  308 + const hasNext = _data.hasNext || false
  309 + records = records.map(function (it) { return Object.assign({}, it) })
  310 + return { records: records, totalCount: totalCount, hasNext: hasNext }
  311 + }).catch(function () {
  312 + _this2.onCardError()
  313 + return { records: [], totalCount: 0, hasNext: false }
  314 + })
  315 + },
  316 + onCardClick(item) {
  317 + const id = item && (item.id || item.itemId)
  318 + if (!id) {
  319 + uni.showToast({ title: '缺少ID', icon: 'none' })
  320 + return
  321 + }
  322 + uni.navigateTo({ url: '/pages/domestic-agreement/detail?id=' + id })
  323 + }
  324 + }
  325 +}
  326 +</script>
  327 +
  328 +<style lang="scss" scoped>
  329 +.page {
  330 + display: flex;
  331 + flex-direction: column;
  332 + height: 100vh;
  333 +}
  334 +.agreement-list-fixed {
  335 + position: fixed;
  336 + top: 96rpx;
  337 + left: 0;
  338 + right: 0;
  339 + z-index: 2;
  340 + background: #fff;
  341 + .search-row {
  342 + display: flex;
  343 + align-items: center;
  344 + padding: 16rpx 32rpx;
  345 + .uni-searchbar { padding: 0; flex: 1; }
  346 + .tool-icons {
  347 + display: flex;
  348 + .tool-icon { width: 48rpx; height: 48rpx; display: block; margin-left: 32rpx; }
  349 + }
  350 + }
  351 + .tabs {
  352 + display: flex;
  353 + align-items: center;
  354 + justify-content: space-between;
  355 + padding: 26rpx 34rpx;
  356 + border-bottom: solid 1px #E7E7E7;
  357 + .tab { width: 50%; text-align: center; color: rgba(0,0,0,0.9); line-height: 44rpx; position: relative; font-weight: 600; }
  358 + .tab.active { color: $theme-primary; }
  359 + .tab.active::after {
  360 + content: '';
  361 + position: absolute;
  362 + left: 50%;
  363 + transform: translateX(-50%);
  364 + bottom: -13px;
  365 + width: 32rpx;
  366 + height: 6rpx;
  367 + border-radius: 4rpx;
  368 + background: $theme-primary;
  369 + }
  370 + }
  371 +}
  372 +::v-deep .uni-searchbar__box { height: 80rpx !important; justify-content: start; .uni-searchbar__box-search-input { font-size: 32rpx !important; } }
  373 +.list-box {
  374 + flex: 1;
  375 + padding-top: 236rpx;
  376 + .card { position: relative; }
  377 + .card-header {
  378 + margin-bottom: 28rpx;
  379 + position: relative;
  380 + .title { font-size: 36rpx; font-weight: 600; line-height: 50rpx; color: rgba(0,0,0,0.9); width: 578rpx; }
  381 + .status {
  382 + position: absolute;
  383 + top: -32rpx;
  384 + right: -12rpx;
  385 + height: 48rpx;
  386 + line-height: 48rpx;
  387 + font-weight: 600;
  388 + color: #fff;
  389 + font-size: 24rpx;
  390 + padding: 0 14rpx;
  391 + border-radius: 6rpx;
  392 + box-sizing: content-box;
  393 +
  394 + &.status_AUDIT {
  395 + background: $theme-primary;
  396 + }
  397 +
  398 + &.status_PASS {
  399 + background: #2BA471;
  400 + }
  401 +
  402 + &.status_REFUSE {
  403 + background: #D54941;
  404 + }
  405 +
  406 + &.status_CANCEL {
  407 + background: #E7E7E7;
  408 + color: rgba(0, 0, 0, 0.9);
  409 + }
  410 + }
  411 + }
  412 + .info-row {
  413 + display: flex;
  414 + align-items: center;
  415 + color: rgba(0,0,0,0.6);
  416 + font-size: 28rpx;
  417 + margin-bottom: 24rpx;
  418 + height: 32rpx;
  419 + &:last-child { margin-bottom: 0; }
  420 + text {
  421 + width: 50%;
  422 + &:last-child { color: rgba(0,0,0,0.9); width: 50%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  423 + }
  424 + }
  425 +}
  426 +.filter-form {
  427 + .form-item { margin-bottom: 24rpx; }
  428 + .label { margin-bottom: 20rpx; color: rgba(0,0,0,0.9); height: 44rpx; line-height: 44rpx; font-size: 30rpx; }
  429 + .input {
  430 + width: 100%;
  431 + height: 72rpx;
  432 + line-height: 72rpx;
  433 + padding: 0 24rpx;
  434 + border: 1rpx solid #E7E7E7;
  435 + border-radius: 8rpx;
  436 + font-size: 28rpx;
  437 + color: rgba(0,0,0,0.9);
  438 + background: #fff;
  439 + box-sizing: border-box;
  440 + }
  441 +}
  442 +</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.procurementPlanCode) }}</text>
  7 + <view class="row">
  8 + <text class="label">供方</text>
  9 + <text class="value">{{ safeText(form.supplierName) }}</text>
  10 + </view>
  11 + <view class="row">
  12 + <text class="label">需方</text>
  13 + <text class="value">{{ safeText(form.companyFullName) }}</text>
  14 + </view>
  15 + <view class="row">
  16 + <text class="label">签订地点</text>
  17 + <text class="value">{{ safeText(form.signPlace, '芜湖市') }}</text>
  18 + </view>
  19 + <view class="row">
  20 + <text class="label">签订时间</text>
  21 + <text class="value">{{ formatDateOnly(form.signTime) }}</text>
  22 + </view>
  23 + <view class="row">
  24 + <text class="label">投诉编码</text>
  25 + <text class="value">{{ safeText(form.procurementPlanCode) }}</text>
  26 + </view>
  27 + <view class="row">
  28 + <text class="label">结算比例</text>
  29 + <text class="value">{{ formatPercent(form.settlementRatio) }}</text>
  30 + </view>
  31 + <view v-if="form.createUserName" class="row">
  32 + <text class="label">发起人</text>
  33 + <text class="value">{{ safeText(form.createUserName) }}</text>
  34 + </view>
  35 + <view v-if="approvalStatusVisible" class="row">
  36 + <text class="label">审核状态</text>
  37 + <text class="value">{{ getAuditStatusText(form.approvalStatus) }}</text>
  38 + </view>
  39 + </view>
  40 +
  41 + <ProductRel mode="view" :list="lineList" />
  42 +
  43 + <view class="title-header">
  44 + <image class="title-header_icon" src="/static/images/title.png" />
  45 + <span>特殊约定</span>
  46 + </view>
  47 + <view class="section">
  48 + <textarea v-model="form.specialAgreement" class="remark-text" placeholder="请输入特殊约定" />
  49 + </view>
  50 + <view class="section">
  51 + <text class="section-title">附件上传:</text>
  52 + <view class="attach-box">
  53 + <view v-for="(f, idx) in attachmentFiles" :key="idx" class="attach-row">
  54 + <FileUpload :value="f" @input="onAttachmentInput(idx, $event)" />
  55 + <text class="attach-del" @click.stop="removeAttachment(idx)">删除</text>
  56 + </view>
  57 + <view class="attach-row">
  58 + <FileUpload :value="newAttachment" @input="onAddAttachment" />
  59 + </view>
  60 + </view>
  61 + </view>
  62 + </view>
  63 + </scroll-view>
  64 + <view class="footer">
  65 + <button class="btn cancel" @click="onCancelEdit">取消</button>
  66 + <button class="btn submit" type="primary" :disabled="submitting" @click="onSave">{{ submitting ? '保存中...' : '保存' }}</button>
  67 + </view>
  68 + </view>
  69 +</template>
  70 +
  71 +<script>
  72 +import DetailButtons from '@/components/detail-buttons/index.vue'
  73 +import ProductRel from './productRel.vue'
  74 +import FileUpload from '@/components/file-upload/index.vue'
  75 +import { getInfo } from '@/api/login'
  76 +import { domesticAgreementCancelApi, domesticAgreementDetailApi, domesticAgreementSubmitApi, domesticAgreementEditApi } from '@/api/procure-manage/domesticAgreement.js'
  77 +import { getDicByCodes, getDicName } from '@/utils/dic.js'
  78 +
  79 +export default {
  80 + name: 'DomesticAgreementModify',
  81 + components: { DetailButtons, ProductRel, FileUpload },
  82 + data() {
  83 + return {
  84 + id: '',
  85 + submitting: false,
  86 + rawDetail: {},
  87 + form: {
  88 + id: '',
  89 + procurementPlanId: '',
  90 + procurementPlanCode: '',
  91 + supplierId: '',
  92 + supplierName: '',
  93 + companyShortId: '',
  94 + companyFullName: '',
  95 + signPlace: '',
  96 + signTime: '',
  97 + deliveryTime: '',
  98 + settlementRatio: '',
  99 + specialAgreement: '',
  100 + attachmentFileIds: '',
  101 + attachmentFileNames: '',
  102 + createUserName: '',
  103 + createById: '',
  104 + approvalStatus: ''
  105 + },
  106 + lineList: [],
  107 + attachmentFiles: [],
  108 + newAttachment: { id: '', name: '' },
  109 + dicOptions: {
  110 + AUDIT_STATUS: []
  111 + },
  112 + buttons: [{
  113 + text: '审核详情',
  114 + visible: true,
  115 + variant: 'outline',
  116 + event: 'auditDetail',
  117 + },{
  118 + text: '审核',
  119 + visible: true,
  120 + variant: 'primary',
  121 + event: 'audit',
  122 + },{
  123 + text: '编辑',
  124 + visible: true,
  125 + variant: 'outline',
  126 + event: 'edit',
  127 + },
  128 + {
  129 + text: '提交审核',
  130 + visible: true,
  131 + variant: 'outline',
  132 + event: 'submitAudit',
  133 + },
  134 + {
  135 + text: '取消',
  136 + visible: true,
  137 + variant: 'outline',
  138 + event: 'cancel',
  139 + }],
  140 + currentUserId: '',
  141 + }
  142 + },
  143 + computed: {
  144 + displayButtons() {
  145 + const row = this.form || {}
  146 + const status = row.approvalStatus
  147 + const isDraft = !status; //草稿
  148 + const isAudit = status === 'AUDIT';
  149 + const isPass = status === 'PASS';
  150 + const isRefuse = status === 'REFUSE';
  151 + return [
  152 + { ...this.buttons[0], visible: (isAudit || isPass)&& this.form?.showExamineDetail === true && this.$auth.hasPermi('agreement-manage:domestic-agreement:view') },
  153 + { ...this.buttons[1], visible: isAudit && this.form?.showExamine === true && this.$auth.hasPermi('agreement-manage:domestic-agreement:approve') },
  154 + { ...this.buttons[2], visible: (isDraft || isRefuse)&&this.form.createById === this.currentUserId && this.$auth.hasPermi('agreement-manage:domestic-agreement:modify') },
  155 + { ...this.buttons[3], visible: (isDraft || isRefuse) && row.createById === this.currentUserId&& this.$auth.hasPermi('agreement-manage:domestic-agreement:submit') },
  156 + { ...this.buttons[4], visible:(isRefuse || !status) && this.currentUserId === row?.createById && this.$auth.hasPermi('agreement-manage:domestic-agreement:cancel') },
  157 + ]
  158 + },
  159 + approvalStatusVisible() {
  160 + return !!(this.form && this.form.approvalStatus)
  161 + }
  162 + },
  163 + onLoad(query) {
  164 + this.id = (query && query.id) || ''
  165 + this.loadDicData()
  166 + this.loadDetail()
  167 + this.getUserInfo()
  168 + },
  169 + onShow() {
  170 + let needRefresh = ''
  171 + try { needRefresh = uni.getStorageSync('DOMESTIC_AGREEMENT_DETAIL_NEED_REFRESH') } catch (e) {}
  172 + if (!needRefresh) return
  173 + try { uni.removeStorageSync('DOMESTIC_AGREEMENT_DETAIL_NEED_REFRESH') } catch (e) {}
  174 + this.loadDetail()
  175 + },
  176 + methods: {
  177 + handleButtonClick(btn) {
  178 + if (!btn || btn.disabled) return
  179 + var e = btn.event || ''
  180 + if (e === 'cancel') return this.onCancel()
  181 + if (e === 'submitAudit') return this.onSubmitAudit()
  182 + uni.showToast({ title: (btn.text || '操作') + '待实现', icon: 'none' })
  183 + },
  184 + refreshListFlags() {
  185 + try { uni.setStorageSync('DOMESTIC_AGREEMENT_LIST_NEED_REFRESH', '1') } catch (e) {}
  186 + try { uni.setStorageSync('DOMESTIC_AGREEMENT_DETAIL_NEED_REFRESH', '1') } catch (e) {}
  187 + },
  188 + runWithConfirm(opts) {
  189 + var self = this
  190 + var title = opts.title || '提示'
  191 + var content = opts.content || ''
  192 + var run = opts.run
  193 + var loading = opts.loading || '处理中'
  194 + var successText = opts.successText || '操作成功'
  195 + var failText = opts.failText || '操作失败'
  196 + if (!this.id) {
  197 + uni.showToast({ title: '缺少合同ID', icon: 'none' })
  198 + return
  199 + }
  200 + var apiFn = function () {
  201 + uni.showLoading({ title: loading, mask: true })
  202 + Promise.resolve(run(self.id)).then(function () {
  203 + uni.hideLoading()
  204 + uni.showToast({ title: successText, icon: 'none' })
  205 + self.refreshListFlags()
  206 + self.loadDetail()
  207 + }).catch(function (e) {
  208 + uni.hideLoading()
  209 + uni.showToast({ title: (e && (e.msg || e.message)) ? (e.msg || e.message) : failText, icon: 'none' })
  210 + })
  211 + }
  212 + uni.showModal({
  213 + title: title,
  214 + content: content,
  215 + confirmText: '确定',
  216 + cancelText: '取消',
  217 + success: function (res) {
  218 + if (!res || !res.confirm) return
  219 + apiFn()
  220 + }
  221 + })
  222 + },
  223 + onCancel() {
  224 + this.runWithConfirm({
  225 + title: '取消',
  226 + content: '确认取消该合同吗?',
  227 + loading: '提交中',
  228 + successText: '取消成功',
  229 + failText: '取消失败',
  230 + run: function (id) { return domesticAgreementCancelApi(id) }
  231 + })
  232 + },
  233 + onSubmitAudit() {
  234 + this.runWithConfirm({
  235 + title: '提交审核',
  236 + content: '确认提交审核吗?',
  237 + loading: '提交中',
  238 + successText: '提交成功',
  239 + failText: '提交审核失败',
  240 + run: function (id) { return domesticAgreementSubmitApi(id) }
  241 + })
  242 + },
  243 + onCancelEdit() {
  244 + uni.navigateTo({ url: '/pages/domestic-agreement/detail?id=' + this.id })
  245 + },
  246 + async getUserInfo() {
  247 + try {
  248 + var res = await getInfo()
  249 + this.currentUserId = (res && res.id) ? res.id : ''
  250 + } catch (e) {
  251 + this.currentUserId = ''
  252 + }
  253 + },
  254 + async loadDicData() {
  255 + try {
  256 + const results = await getDicByCodes(['AUDIT_STATUS'])
  257 + this.dicOptions.AUDIT_STATUS = (results.AUDIT_STATUS && results.AUDIT_STATUS.data) || []
  258 + } catch (e) {
  259 + this.dicOptions.AUDIT_STATUS = []
  260 + }
  261 + },
  262 + getAuditStatusText(status) {
  263 + const v = status == null ? '' : String(status)
  264 + if (!v) return '-'
  265 + return getDicName('AUDIT_STATUS', v, this.dicOptions.AUDIT_STATUS) || v
  266 + },
  267 + loadDetail() {
  268 + if (!this.id) {
  269 + uni.showToast({ title: '缺少合同ID', icon: 'none' })
  270 + return
  271 + }
  272 + uni.showLoading && uni.showLoading({ title: '加载中...', mask: true })
  273 + this.fetchDetail({ id: this.id })
  274 + .then(res => {
  275 + const data = res && res.data ? (res.data.data || res.data) : {}
  276 + this.rawDetail = Object.assign({}, data)
  277 + this.form = Object.assign({}, this.form, data)
  278 + const lines = data && Array.isArray(data.procurementContractLineList) ? data.procurementContractLineList : []
  279 + this.lineList = lines.map(l => Object.assign({}, l))
  280 + this.buildAttachmentFiles()
  281 + this.notifyListRefresh()
  282 + })
  283 + .catch(() => {
  284 + uni.showToast({ title: '详情加载失败', icon: 'none' })
  285 + })
  286 + .finally(() => {
  287 + uni.hideLoading && uni.hideLoading()
  288 + })
  289 + },
  290 + fetchDetail(params) {
  291 + const self = this
  292 + return domesticAgreementDetailApi(params).catch(function () {
  293 + return self.mockDetail(params)
  294 + })
  295 + },
  296 + mockDetail(params) {
  297 + return new Promise(resolve => {
  298 + const mock = {
  299 + id: params.id,
  300 + procurementPlanId: 'plan-' + params.id,
  301 + procurementPlanCode: 'NMHT2026000' + params.id,
  302 + supplierId: 'S001',
  303 + supplierName: '上海钢材贸易有限公司',
  304 + companyShortId: 'C001',
  305 + companyFullName: '北京远大集团有限公司',
  306 + signPlace: '芜湖市',
  307 + signTime: '2026-08-15',
  308 + deliveryTime: '2026-09-20',
  309 + settlementRatio: 80,
  310 + specialAgreement: '1. 本合同项下货物质量须符合国标及双方约定的技术标准;\n2. 如出现质量问题,供方须在3个工作日内响应处理;\n3. 价格波动超过±5%时,双方另行协商结算。',
  311 + attachmentFileIds: 'f1,f2',
  312 + attachmentFileNames: '内贸合同扫描件.pdf,补充条款.docx',
  313 + createUserName: '张三',
  314 + approvalStatus: 'AUDIT',
  315 + procurementContractLineList: [
  316 + {
  317 + id: 'L1',
  318 + contractId: params.id,
  319 + itemId: 'I1',
  320 + planLineId: 'PL1',
  321 + invoiceProductName: '废铜',
  322 + productId: 'P001',
  323 + productName: '光亮铜',
  324 + quantity: '25.500',
  325 + unitPrice: '62500.00',
  326 + deliveryTime: '2026-09-15'
  327 + },
  328 + {
  329 + id: 'L2',
  330 + contractId: params.id,
  331 + itemId: 'I2',
  332 + planLineId: 'PL2',
  333 + invoiceProductName: '废铜',
  334 + productId: 'P002',
  335 + productName: '一特铜',
  336 + quantity: '18.200',
  337 + unitPrice: '61800.00',
  338 + deliveryTime: '2026-09-20'
  339 + },
  340 + {
  341 + id: 'L3',
  342 + contractId: params.id,
  343 + itemId: 'I3',
  344 + planLineId: 'PL3',
  345 + invoiceProductName: '废铝',
  346 + productId: 'P003',
  347 + productName: '高纯铝锭',
  348 + quantity: '42.000',
  349 + unitPrice: '19800.00',
  350 + deliveryTime: '2026-09-25'
  351 + }
  352 + ]
  353 + }
  354 + setTimeout(() => resolve({ data: { data: mock } }), 200)
  355 + })
  356 + },
  357 + buildAttachmentFiles() {
  358 + const ids = this.form.attachmentFileIds
  359 + const names = this.form.attachmentFileNames
  360 + const idArr = (typeof ids === 'string' && ids) ? ids.split(',').filter(Boolean) : (Array.isArray(ids) ? ids : [])
  361 + const nameArr = (typeof names === 'string' && names) ? names.split(',').filter(Boolean) : (Array.isArray(names) ? names : [])
  362 + const len = Math.max(idArr.length, nameArr.length)
  363 + const list = []
  364 + for (let i = 0; i < len; i++) {
  365 + list.push({ id: idArr[i] || '', name: nameArr[i] || ('附件' + (i + 1)) })
  366 + }
  367 + this.attachmentFiles = list
  368 + },
  369 + syncAttachmentToForm() {
  370 + const list = Array.isArray(this.attachmentFiles) ? this.attachmentFiles : []
  371 + const ids = list.map(f => (f && f.id) ? String(f.id) : '').filter(Boolean)
  372 + const names = list.map(f => (f && f.name) ? String(f.name) : '').filter(Boolean)
  373 + this.form.attachmentFileIds = ids.join(',')
  374 + this.form.attachmentFileNames = names.join(',')
  375 + },
  376 + removeAttachment(idx) {
  377 + const list = Array.isArray(this.attachmentFiles) ? this.attachmentFiles.slice(0) : []
  378 + list.splice(idx, 1)
  379 + this.attachmentFiles = list
  380 + },
  381 + onAddAttachment(val) {
  382 + const payload = val || { id: '', name: '' }
  383 + if (!payload.id) return
  384 + const list = Array.isArray(this.attachmentFiles) ? this.attachmentFiles.slice(0) : []
  385 + list.push(payload)
  386 + this.attachmentFiles = list
  387 + this.newAttachment = { id: '', name: '' }
  388 + },
  389 + onAttachmentInput(idx, val) {
  390 + const list = Array.isArray(this.attachmentFiles) ? this.attachmentFiles.slice(0) : []
  391 + list[idx] = val || { id: '', name: '' }
  392 + this.attachmentFiles = list
  393 + },
  394 + onLineListInput(list) {
  395 + this.lineList = Array.isArray(list) ? list.slice(0) : []
  396 + },
  397 + buildPayload() {
  398 + this.syncAttachmentToForm()
  399 + const f = this.form || {}
  400 + const cleanedLines = (this.lineList || []).map(line => {
  401 + const l = Object.assign({}, line)
  402 + delete l._key
  403 + delete l.fixedLossRate
  404 + delete l.collapsed
  405 + return l
  406 + })
  407 + const payload = Object.assign({}, this.rawDetail, f, {
  408 + id: f.id || this.id || '',
  409 + procurementContractLineList: cleanedLines,
  410 + attachmentFileIds: f.attachmentFileIds || '',
  411 + attachmentFileNames: f.attachmentFileNames || ''
  412 + })
  413 + delete payload.showExamine
  414 + delete payload.showExamineDetail
  415 + return payload
  416 + },
  417 + onSave() {
  418 + if (this.submitting) return
  419 + this.submitting = true
  420 + const payload = this.buildPayload()
  421 + uni.showLoading({ title: '保存中...', mask: true })
  422 + domesticAgreementEditApi(payload)
  423 + .then(() => {
  424 + uni.hideLoading()
  425 + uni.showToast({ title: '保存成功', icon: 'none' })
  426 + this.refreshListFlags()
  427 + setTimeout(() => {
  428 + uni.navigateTo({ url: '/pages/domestic-agreement/detail?id=' + this.id })
  429 + }, 800)
  430 + })
  431 + .catch((e) => {
  432 + uni.hideLoading()
  433 + uni.showToast({ title: (e && (e.msg || e.message)) ? (e.msg || e.message) : '保存失败', icon: 'none' })
  434 + })
  435 + .finally(() => {
  436 + this.submitting = false
  437 + })
  438 + },
  439 + notifyListRefresh() {
  440 + try { uni.setStorageSync('DOMESTIC_AGREEMENT_LIST_NEED_REFRESH', '1') } catch (e) {}
  441 + },
  442 + safeText(v, def) {
  443 + const s = v == null ? '' : String(v)
  444 + if (s.trim()) return s
  445 + return def != null ? def : '-'
  446 + },
  447 + formatDateOnly(v) {
  448 + const s = v == null ? '' : String(v).trim()
  449 + if (!s) return '-'
  450 + if (s.length >= 10) return s.slice(0, 10)
  451 + return s
  452 + },
  453 + formatPercent(v) {
  454 + if (v === null || v === undefined || v === '') return '-'
  455 + const n = Number(v)
  456 + if (isNaN(n)) return String(v)
  457 + return n + '%'
  458 + }
  459 + }
  460 +}
  461 +</script>
  462 +
  463 +<style lang="scss" scoped>
  464 +.page {
  465 + display: flex;
  466 + flex-direction: column;
  467 + height: 100%;
  468 +}
  469 +.scroll {
  470 + flex: 1;
  471 + padding: 8rpx 0 144rpx;
  472 +}
  473 +.detail-page {
  474 + background: #f3f3f3;
  475 +}
  476 +.title-header {
  477 + display: flex;
  478 + align-items: center;
  479 + padding: 24rpx 32rpx 12rpx;
  480 + background-color: #ffffff;
  481 + .title-header_icon {
  482 + width: 36rpx;
  483 + height: 36rpx;
  484 + margin-right: 12rpx;
  485 + }
  486 + span {
  487 + font-size: 34rpx;
  488 + font-weight: 600;
  489 + color: rgba(0, 0, 0, 0.9);
  490 + }
  491 +}
  492 +.section {
  493 + padding: 32rpx;
  494 + background: #fff;
  495 + margin-bottom: 20rpx;
  496 +}
  497 +.section-title {
  498 + font-size: 28rpx;
  499 + color: rgba(0, 0, 0, 0.9);
  500 + font-weight: 600;
  501 + display: block;
  502 + margin-bottom: 24rpx;
  503 +}
  504 +.row {
  505 + display: flex;
  506 + align-items: center;
  507 + margin-bottom: 28rpx;
  508 + &:last-child { margin-bottom: 0; }
  509 + &.company {
  510 + font-size: 36rpx;
  511 + font-weight: 600;
  512 + color: rgba(0, 0, 0, 0.9);
  513 + padding-top: 8rpx;
  514 + line-height: 50rpx;
  515 + }
  516 + .label {
  517 + min-width: 160rpx;
  518 + max-width: 420rpx;
  519 + margin-right: 20rpx;
  520 + line-height: 44rpx;
  521 + font-size: 28rpx;
  522 + color: rgba(0, 0, 0, 0.6);
  523 + flex-shrink: 0;
  524 + }
  525 + .value {
  526 + flex: 1;
  527 + line-height: 44rpx;
  528 + font-size: 28rpx;
  529 + color: rgba(0, 0, 0, 0.9);
  530 + text-align: right;
  531 + word-break: break-all;
  532 + }
  533 + .input {
  534 + flex: 1;
  535 + height: 72rpx;
  536 + line-height: 72rpx;
  537 + padding: 0 24rpx;
  538 + border: 1rpx solid #E7E7E7;
  539 + border-radius: 8rpx;
  540 + font-size: 28rpx;
  541 + color: rgba(0,0,0,0.9);
  542 + background: #fff;
  543 + box-sizing: border-box;
  544 + text-align: right;
  545 + }
  546 + :deep(.uni-datetime-picker) {
  547 + flex: 1;
  548 + justify-content: flex-end;
  549 + :deep(.uni-datetime-picker__input) {
  550 + text-align: right;
  551 + justify-content: flex-end;
  552 + }
  553 + }
  554 +}
  555 +.attach-box {
  556 + display: flex;
  557 + flex-direction: column;
  558 + gap: 20rpx;
  559 +}
  560 +.attach-row {
  561 + display: flex;
  562 + align-items: center;
  563 + justify-content: space-between;
  564 + .attach-del {
  565 + color: #D54941;
  566 + font-size: 28rpx;
  567 + margin-left: 20rpx;
  568 + flex-shrink: 0;
  569 + }
  570 + .file-upload {
  571 + flex: 1;
  572 + }
  573 +}
  574 +.remark-text {
  575 + font-size: 28rpx;
  576 + color: rgba(0, 0, 0, 0.9);
  577 + line-height: 1.6;
  578 + white-space: pre-wrap;
  579 + width: 100%;
  580 + min-height: 200rpx;
  581 + padding: 24rpx;
  582 + border: 1rpx solid #E7E7E7;
  583 + border-radius: 8rpx;
  584 + box-sizing: border-box;
  585 + background: #fff;
  586 +}
  587 +.empty-tip {
  588 + text-align: center;
  589 + font-size: 28rpx;
  590 + color: rgba(0, 0, 0, 0.4);
  591 + padding: 24rpx 0;
  592 +}
  593 +.footer {
  594 + position: fixed;
  595 + left: 0;
  596 + right: 0;
  597 + bottom: 0;
  598 + padding: 16rpx 32rpx calc(16rpx + env(safe-area-inset-bottom));
  599 + background: #fff;
  600 + border-top: 1rpx solid #E7E7E7;
  601 + display: flex;
  602 + gap: 24rpx;
  603 + z-index: 10;
  604 + .btn {
  605 + flex: 1;
  606 + height: 88rpx;
  607 + line-height: 88rpx;
  608 + border-radius: 8rpx;
  609 + font-size: 32rpx;
  610 + font-weight: 600;
  611 + &.cancel {
  612 + background: #F5F5F5;
  613 + color: rgba(0, 0, 0, 0.9);
  614 + border: none;
  615 + }
  616 + &.submit {
  617 + background: $theme-primary;
  618 + color: #fff;
  619 + border: none;
  620 + &[disabled] {
  621 + opacity: 0.6;
  622 + }
  623 + }
  624 + }
  625 +}
  626 +</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 + <view class="op1" @click="toggleViewCollapse">
  8 + <image class="opAdd" :src="collapsedView ? '/static/images/down.png' : '/static/images/up.png'" />
  9 + <text class="op">{{ collapsedView ? '展开' : '收起' }}</text>
  10 + </view>
  11 + </view>
  12 + </view>
  13 +
  14 + <view v-if="!collapsedView" class="view-list">
  15 + <view v-if="!items.length" class="empty-tip">暂无明细</view>
  16 + <view
  17 + v-for="(item, idx) in items"
  18 + :key="'v-' + (item.id || item.itemId || item._key || idx)"
  19 + class="card"
  20 + >
  21 + <view class="card-index">第 {{ idx + 1 }} 行</view>
  22 + <view class="row"><text class="label">名称</text><text class="value">{{ safeText(item.invoiceProductName) }}</text></view>
  23 + <view class="row"><text class="label">规格型号</text><text class="value">{{ safeText(item.productName) }}</text></view>
  24 + <view class="row"><text class="label">数量</text><text class="value">{{ formatNumber(item.quantity) }}</text></view>
  25 + <view class="row"><text class="label">单价</text><text class="value">{{ formatNumber(item.unitPrice) }}</text></view>
  26 + <view class="row"><text class="label">不含税金额</text><text class="value highlight">{{ formatNumber(item.excludingTaxAmount) }}</text></view>
  27 + <view class="row"><text class="label">税额</text><text class="value highlight">{{ formatNumber(item.taxAmount) }}</text></view>
  28 + <view class="row"><text class="label">合计金额</text><text class="value total">{{ formatNumber(item.totalAmount) }}</text></view>
  29 + <view class="row"><text class="label">交货时间</text><text class="value">{{ formatDateOnly(item.deliveryTime) }}</text></view>
  30 + </view>
  31 +
  32 + <view v-if="items.length" class="summary-card">
  33 + <view class="summary-title">合计</view>
  34 + <view class="row"><text class="label">数量</text><text class="value">{{ formatNumber(totalQuantity) }}</text></view>
  35 + <view class="row"><text class="label">不含税金额</text><text class="value highlight">{{ formatNumber(totalExcludeTaxAmount) }}</text></view>
  36 + <view class="row"><text class="label">税额</text><text class="value highlight">{{ formatNumber(totalTaxAmount) }}</text></view>
  37 + <view class="row"><text class="label">合计金额</text><text class="value total">{{ formatNumber(totalAmount) }}</text></view>
  38 + </view>
  39 + </view>
  40 + </view>
  41 +</template>
  42 +
  43 +<script>
  44 +export default {
  45 + name: 'DomesticAgreementProductRel',
  46 + props: {
  47 + mode: { type: String, default: 'view' },
  48 + list: { type: Array, default: function () { return [] } }
  49 + },
  50 + data() {
  51 + return {
  52 + items: [],
  53 + collapsedView: false
  54 + }
  55 + },
  56 + computed: {
  57 + totalQuantity() {
  58 + return round2(sum(this.items, 'quantity'))
  59 + },
  60 + totalExcludeTaxAmount() {
  61 + return sum(this.items, 'excludingTaxAmount')
  62 + },
  63 + totalTaxAmount() {
  64 + return sum(this.items, 'taxAmount')
  65 + },
  66 + totalAmount() {
  67 + return sum(this.items, 'totalAmount')
  68 + }
  69 + },
  70 + watch: {
  71 + list: {
  72 + handler(v) {
  73 + this.initItems(v)
  74 + },
  75 + immediate: true,
  76 + deep: true
  77 + }
  78 + },
  79 + methods: {
  80 + initItems(src) {
  81 + const arr = Array.isArray(src) ? src : []
  82 + this.items = arr.map(x => Object.assign({}, x))
  83 + },
  84 + toggleViewCollapse() {
  85 + this.collapsedView = !this.collapsedView
  86 + },
  87 + safeText(v) {
  88 + if (v === null || v === undefined || v === '') return '-'
  89 + return String(v)
  90 + },
  91 + formatNumber(v) {
  92 + if (v === null || v === undefined || v === '') return '-'
  93 + const n = Number(v)
  94 + if (isNaN(n)) return String(v)
  95 + return n.toLocaleString('zh-CN', { maximumFractionDigits: 4 })
  96 + },
  97 + formatDateOnly(v) {
  98 + const s = v == null ? '' : String(v).trim()
  99 + if (!s) return '-'
  100 + if (s.length >= 10) return s.slice(0, 10)
  101 + return s
  102 + }
  103 + }
  104 +}
  105 +
  106 +function num(v) {
  107 + if (v === null || v === undefined || v === '') return 0
  108 + const n = Number(v)
  109 + return isNaN(n) ? 0 : n
  110 +}
  111 +function round2(v) {
  112 + const n = Number(v)
  113 + if (isNaN(n)) return ''
  114 + return Math.round(n * 100) / 100
  115 +}
  116 +function sum(list, field) {
  117 + let s = 0
  118 + if (!Array.isArray(list)) return s
  119 + list.forEach(it => { s += num(it && it[field]) })
  120 + return s
  121 +}
  122 +</script>
  123 +
  124 +<style lang="scss" scoped>
  125 +.product-rel {
  126 + width: 100%;
  127 + margin-top: 24rpx;
  128 +}
  129 +.header {
  130 + display: flex;
  131 + align-items: center;
  132 + padding: 16rpx 0 16rpx 32rpx;
  133 + .opCollapse { width: 6rpx; height: 32rpx; margin-right: 14rpx; }
  134 + .title { font-size: 30rpx; font-weight: 600; color: rgba(0,0,0,0.9); flex: 1; line-height: 42rpx; }
  135 + .ops { display: flex; align-items: center; padding-right: 32rpx; }
  136 + .op1 {
  137 + display: flex;
  138 + align-items: center;
  139 + padding: 0 12rpx;
  140 + height: 56rpx;
  141 + .opAdd { width: 24rpx; height: 24rpx; margin-right: 8rpx; }
  142 + .op { font-size: 26rpx; color: rgba(0,0,0,0.6); }
  143 + }
  144 +}
  145 +.view-list {
  146 + padding: 0 32rpx 24rpx;
  147 + .empty-tip {
  148 + text-align: center;
  149 + padding: 80rpx 0;
  150 + color: rgba(0,0,0,0.4);
  151 + font-size: 28rpx;
  152 + }
  153 + .card {
  154 + position: relative;
  155 + background: #fff;
  156 + border-radius: 12rpx;
  157 + padding: 32rpx 28rpx 20rpx;
  158 + margin-bottom: 24rpx;
  159 + box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
  160 + .card-index {
  161 + position: absolute;
  162 + top: 0;
  163 + right: 0;
  164 + background: rgba(24, 144, 255, 0.08);
  165 + color: $theme-primary;
  166 + font-size: 22rpx;
  167 + line-height: 36rpx;
  168 + padding: 0 16rpx;
  169 + border-bottom-left-radius: 12rpx;
  170 + border-top-right-radius: 12rpx;
  171 + }
  172 + .row {
  173 + display: flex;
  174 + align-items: center;
  175 + justify-content: space-between;
  176 + padding: 14rpx 0;
  177 + border-bottom: 1rpx dashed #F2F2F2;
  178 + &:last-child { border-bottom: none; }
  179 + .label {
  180 + flex-shrink: 0;
  181 + width: 220rpx;
  182 + font-size: 28rpx;
  183 + color: rgba(0,0,0,0.6);
  184 + line-height: 40rpx;
  185 + }
  186 + .value {
  187 + flex: 1;
  188 + text-align: right;
  189 + font-size: 28rpx;
  190 + color: rgba(0,0,0,0.9);
  191 + line-height: 40rpx;
  192 + overflow: hidden;
  193 + text-overflow: ellipsis;
  194 + white-space: nowrap;
  195 + &.highlight { color: #E6A23C; }
  196 + &.total { color: #D54941; font-weight: 600; font-size: 30rpx; }
  197 + }
  198 + }
  199 + }
  200 + .summary-card {
  201 + background: linear-gradient(135deg, rgba(24, 144, 255, 0.06), rgba(24, 144, 255, 0.02));
  202 + border: 1rpx solid rgba(24, 144, 255, 0.16);
  203 + border-radius: 12rpx;
  204 + padding: 28rpx;
  205 + .summary-title {
  206 + font-size: 30rpx;
  207 + font-weight: 700;
  208 + color: $theme-primary;
  209 + margin-bottom: 12rpx;
  210 + line-height: 42rpx;
  211 + }
  212 + .row {
  213 + display: flex;
  214 + align-items: center;
  215 + justify-content: space-between;
  216 + padding: 12rpx 0;
  217 + border-bottom: 1rpx dashed rgba(24, 144, 255, 0.12);
  218 + &:last-child { border-bottom: none; }
  219 + .label {
  220 + flex-shrink: 0;
  221 + width: 220rpx;
  222 + font-size: 28rpx;
  223 + color: rgba(0,0,0,0.6);
  224 + line-height: 40rpx;
  225 + }
  226 + .value {
  227 + flex: 1;
  228 + text-align: right;
  229 + font-size: 28rpx;
  230 + color: rgba(0,0,0,0.9);
  231 + line-height: 40rpx;
  232 + font-weight: 500;
  233 + &.highlight { color: #E6A23C; }
  234 + &.total { color: #D54941; font-weight: 700; font-size: 32rpx; }
  235 + }
  236 + }
  237 + }
  238 +}
  239 +</style>
@@ -181,11 +181,15 @@ export default { @@ -181,11 +181,15 @@ export default {
181 Name = _contractType 181 Name = _contractType
182 }else if(_contractType === 'PROCESS_STD_AGMT'){ 182 }else if(_contractType === 'PROCESS_STD_AGMT'){
183 Name = 'AGMT_VIEW' 183 Name = 'AGMT_VIEW'
  184 + }else if(_contractType === 'PROCUREMENT_CONTRACT'){
  185 + Name = 'PROCUREMENT_CONTRACT'
184 }else{ 186 }else{
185 Name = this.auditCtx.bizFlag || '' 187 Name = this.auditCtx.bizFlag || ''
186 } 188 }
187 }else if (_contractType === 'PROCESS_STD_AGMT') { 189 }else if (_contractType === 'PROCESS_STD_AGMT') {
188 Name = 'PROCESS_STD_AGMT' 190 Name = 'PROCESS_STD_AGMT'
  191 + }else if (_contractType === 'PROCUREMENT_CONTRACT') {
  192 + Name = 'PROCUREMENT_CONTRACT'
189 }else{ 193 }else{
190 Name = this.auditCtx.bizFlag || '' 194 Name = this.auditCtx.bizFlag || ''
191 } 195 }
@@ -44,6 +44,8 @@ export default { @@ -44,6 +44,8 @@ export default {
44 let Name = '' 44 let Name = ''
45 if (_contractType === 'PROCESS_STD_AGMT') { 45 if (_contractType === 'PROCESS_STD_AGMT') {
46 Name = 'PROCESS_STD_AGMT' 46 Name = 'PROCESS_STD_AGMT'
  47 + }else if (_contractType === 'PROCUREMENT_CONTRACT') {
  48 + Name = 'PROCUREMENT_CONTRACT'
47 }else{ 49 }else{
48 Name = this.auditCtx.bizFlag || '' 50 Name = this.auditCtx.bizFlag || ''
49 } 51 }
@@ -197,6 +197,12 @@ export default { @@ -197,6 +197,12 @@ export default {
197 icon: '/static/images/index/revoke_list.png', 197 icon: '/static/images/index/revoke_list.png',
198 link: '/pages/supple-plan/index', 198 link: '/pages/supple-plan/index',
199 name: 'SupplePlan' 199 name: 'SupplePlan'
  200 + },
  201 + {
  202 + text: '内贸合同管理',
  203 + icon: '/static/images/index/draft_order.png',
  204 + link: '/pages/domestic-agreement/index',
  205 + name: 'DomesticAgreement'
200 } 206 }
201 ] 207 ]
202 }, 208 },
@@ -28,6 +28,7 @@ import ConfirmationFormApprove from '@/pages/confirmation_form/approve.vue' @@ -28,6 +28,7 @@ import ConfirmationFormApprove from '@/pages/confirmation_form/approve.vue'
28 import FollowUpFormViewer from '@/pages/follow_up_form/viewer.vue' 28 import FollowUpFormViewer from '@/pages/follow_up_form/viewer.vue'
29 import DomesticPlanViewer from '@/pages/domestic-plan/domesticPlanViewer.vue' 29 import DomesticPlanViewer from '@/pages/domestic-plan/domesticPlanViewer.vue'
30 import SupplePlanViewer from '@/pages/supple-plan/supplePlanViewer.vue' 30 import SupplePlanViewer from '@/pages/supple-plan/supplePlanViewer.vue'
  31 +import DomesticAgreementViewer from '@/pages/domestic-agreement/domesticAgreementViewer.vue'
31 32
32 export default function registerComponents(Vue) { 33 export default function registerComponents(Vue) {
33 Vue.component('CustomerDevelopViewer', CustomerDevelopViewer) 34 Vue.component('CustomerDevelopViewer', CustomerDevelopViewer)
@@ -60,4 +61,5 @@ export default function registerComponents(Vue) { @@ -60,4 +61,5 @@ export default function registerComponents(Vue) {
60 Vue.component('AgmtViewer', AgmtViewer) 61 Vue.component('AgmtViewer', AgmtViewer)
61 Vue.component('DomesticPlanViewer', DomesticPlanViewer) 62 Vue.component('DomesticPlanViewer', DomesticPlanViewer)
62 Vue.component('SupplePlanViewer', SupplePlanViewer) 63 Vue.component('SupplePlanViewer', SupplePlanViewer)
  64 + Vue.component('DomesticAgreementViewer', DomesticAgreementViewer)
63 } 65 }
@@ -147,6 +147,11 @@ export const getSysFlowComponentPath = (bizFlag) => { @@ -147,6 +147,11 @@ export const getSysFlowComponentPath = (bizFlag) => {
147 break; 147 break;
148 case 'PLAN_REPLENISH_EDIT': // 补库计划 148 case 'PLAN_REPLENISH_EDIT': // 补库计划
149 componentPath = 'SupplePlanViewer'; // 补库计划-审批 149 componentPath = 'SupplePlanViewer'; // 补库计划-审批
  150 + case 'PROCUREMENT_CONTRACT': // 内贸协议
  151 + componentPath = 'DomesticAgreementViewer'; // 内贸协议-审批详情
  152 + break;
  153 + case 'PROCUREMENT_CONTRACT_EDIT': // 内贸协议
  154 + componentPath = 'DomesticAgreementViewer'; // 内贸协议-审批
150 break; 155 break;
151 } 156 }
152 return componentPath; 157 return componentPath;