Commit 17224cd0fbcc1a0e9cc24eee380168b98958b4a2

Authored by gesilong
1 parent 4f327862

commit:内贸合同管理开发联调

  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 + uni.showToast({ title: (btn.text || '操作') + '待实现', icon: 'none' })
  156 + },
  157 + onEdit() {
  158 + uni.navigateTo({
  159 + url: '/pages/domestic-agreement/modify?id=' + this.id
  160 + })
  161 + },
  162 + refreshListFlags() {
  163 + try { uni.setStorageSync('DOMESTIC_AGREEMENT_LIST_NEED_REFRESH', '1') } catch (e) {}
  164 + try { uni.setStorageSync('DOMESTIC_AGREEMENT_DETAIL_NEED_REFRESH', '1') } catch (e) {}
  165 + },
  166 + runWithConfirm(opts) {
  167 + var self = this
  168 + var title = opts.title || '提示'
  169 + var content = opts.content || ''
  170 + var run = opts.run
  171 + var loading = opts.loading || '处理中'
  172 + var successText = opts.successText || '操作成功'
  173 + var failText = opts.failText || '操作失败'
  174 + if (!this.id) {
  175 + uni.showToast({ title: '缺少合同ID', icon: 'none' })
  176 + return
  177 + }
  178 + var apiFn = function () {
  179 + uni.showLoading({ title: loading, mask: true })
  180 + Promise.resolve(run(self.id)).then(function () {
  181 + uni.hideLoading()
  182 + uni.showToast({ title: successText, icon: 'none' })
  183 + self.refreshListFlags()
  184 + self.loadDetail()
  185 + }).catch(function (e) {
  186 + uni.hideLoading()
  187 + uni.showToast({ title: (e && (e.msg || e.message)) ? (e.msg || e.message) : failText, icon: 'none' })
  188 + })
  189 + }
  190 + uni.showModal({
  191 + title: title,
  192 + content: content,
  193 + confirmText: '确定',
  194 + cancelText: '取消',
  195 + success: function (res) {
  196 + if (!res || !res.confirm) return
  197 + apiFn()
  198 + }
  199 + })
  200 + },
  201 + onCancel() {
  202 + this.runWithConfirm({
  203 + title: '取消',
  204 + content: '确认取消该合同吗?',
  205 + loading: '提交中',
  206 + successText: '取消成功',
  207 + failText: '取消失败',
  208 + run: function (id) { return domesticAgreementCancelApi(id) }
  209 + })
  210 + },
  211 + onSubmitAudit() {
  212 + this.runWithConfirm({
  213 + title: '提交审核',
  214 + content: '确认提交审核吗?',
  215 + loading: '提交中',
  216 + successText: '提交成功',
  217 + failText: '提交审核失败',
  218 + run: function (id) { return domesticAgreementSubmitApi(id) }
  219 + })
  220 + },
  221 + async getUserInfo() {
  222 + try {
  223 + var res = await getInfo()
  224 + this.currentUserId = (res && res.id) ? res.id : ''
  225 + } catch (e) {
  226 + this.currentUserId = ''
  227 + }
  228 + },
  229 + async loadDicData() {
  230 + try {
  231 + const results = await getDicByCodes(['AUDIT_STATUS'])
  232 + this.dicOptions.AUDIT_STATUS = (results.AUDIT_STATUS && results.AUDIT_STATUS.data) || []
  233 + } catch (e) {
  234 + this.dicOptions.AUDIT_STATUS = []
  235 + }
  236 + },
  237 + getAuditStatusText(status) {
  238 + const v = status == null ? '' : String(status)
  239 + if (!v) return '-'
  240 + return getDicName('AUDIT_STATUS', v, this.dicOptions.AUDIT_STATUS) || v
  241 + },
  242 + loadDetail() {
  243 + if (!this.id) {
  244 + uni.showToast({ title: '缺少合同ID', icon: 'none' })
  245 + return
  246 + }
  247 + uni.showLoading && uni.showLoading({ title: '加载中...', mask: true })
  248 + this.fetchDetail({ id: this.id })
  249 + .then(res => {
  250 + const data = res && res.data ? (res.data.data || res.data) : {}
  251 + this.form = Object.assign({}, data)
  252 + const lines = data && Array.isArray(data.procurementContractLineList) ? data.procurementContractLineList : []
  253 + this.lineList = lines.map(l => Object.assign({}, l))
  254 + this.buildAttachmentList()
  255 + this.notifyListRefresh()
  256 + })
  257 + .catch(() => {
  258 + uni.showToast({ title: '详情加载失败', icon: 'none' })
  259 + })
  260 + .finally(() => {
  261 + uni.hideLoading && uni.hideLoading()
  262 + })
  263 + },
  264 + fetchDetail(params) {
  265 + const self = this
  266 + return domesticAgreementDetailApi(params).catch(function () {
  267 + return self.mockDetail(params)
  268 + })
  269 + },
  270 + mockDetail(params) {
  271 + return new Promise(resolve => {
  272 + const mock = {
  273 + id: params.id,
  274 + procurementPlanId: 'plan-' + params.id,
  275 + procurementPlanCode: 'NMHT2026000' + params.id,
  276 + supplierId: 'S001',
  277 + supplierName: '上海钢材贸易有限公司',
  278 + companyShortId: 'C001',
  279 + companyFullName: '北京远大集团有限公司',
  280 + signPlace: '芜湖市',
  281 + signTime: '2026-08-15',
  282 + deliveryTime: '2026-09-20',
  283 + settlementRatio: 80,
  284 + specialAgreement: '1. 本合同项下货物质量须符合国标及双方约定的技术标准;\n2. 如出现质量问题,供方须在3个工作日内响应处理;\n3. 价格波动超过±5%时,双方另行协商结算。',
  285 + attachmentFileIds: 'f1,f2',
  286 + attachmentFileNames: '内贸合同扫描件.pdf,补充条款.docx',
  287 + createUserName: '张三',
  288 + approvalStatus: 'AUDIT',
  289 + procurementContractLineList: [
  290 + {
  291 + id: 'L1',
  292 + contractId: params.id,
  293 + itemId: 'I1',
  294 + planLineId: 'PL1',
  295 + invoiceProductName: '废铜',
  296 + productId: 'P001',
  297 + productName: '光亮铜',
  298 + quantity: '25.500',
  299 + unitPrice: '62500.00',
  300 + deliveryTime: '2026-09-15'
  301 + },
  302 + {
  303 + id: 'L2',
  304 + contractId: params.id,
  305 + itemId: 'I2',
  306 + planLineId: 'PL2',
  307 + invoiceProductName: '废铜',
  308 + productId: 'P002',
  309 + productName: '一特铜',
  310 + quantity: '18.200',
  311 + unitPrice: '61800.00',
  312 + deliveryTime: '2026-09-20'
  313 + },
  314 + {
  315 + id: 'L3',
  316 + contractId: params.id,
  317 + itemId: 'I3',
  318 + planLineId: 'PL3',
  319 + invoiceProductName: '废铝',
  320 + productId: 'P003',
  321 + productName: '高纯铝锭',
  322 + quantity: '42.000',
  323 + unitPrice: '19800.00',
  324 + deliveryTime: '2026-09-25'
  325 + }
  326 + ]
  327 + }
  328 + setTimeout(() => resolve({ data: { data: mock } }), 200)
  329 + })
  330 + },
  331 + buildAttachmentList() {
  332 + const ids = this.form.attachmentFileIds
  333 + const names = this.form.attachmentFileNames
  334 + const idArr = (typeof ids === 'string' && ids) ? ids.split(',').filter(Boolean) : (Array.isArray(ids) ? ids : [])
  335 + const nameArr = (typeof names === 'string' && names) ? names.split(',').filter(Boolean) : (Array.isArray(names) ? names : [])
  336 + const len = Math.max(idArr.length, nameArr.length)
  337 + const list = []
  338 + for (let i = 0; i < len; i++) {
  339 + list.push({ id: idArr[i] || '', name: nameArr[i] || ('附件' + (i + 1)) })
  340 + }
  341 + this.attachmentList = list
  342 + },
  343 + notifyListRefresh() {
  344 + try { uni.setStorageSync('DOMESTIC_AGREEMENT_LIST_NEED_REFRESH', '1') } catch (e) {}
  345 + },
  346 + safeText(v, def) {
  347 + const s = v == null ? '' : String(v)
  348 + if (s.trim()) return s
  349 + return def != null ? def : '-'
  350 + },
  351 + formatDateOnly(v) {
  352 + const s = v == null ? '' : String(v).trim()
  353 + if (!s) return '-'
  354 + if (s.length >= 10) return s.slice(0, 10)
  355 + return s
  356 + },
  357 + formatPercent(v) {
  358 + if (v === null || v === undefined || v === '') return '-'
  359 + const n = Number(v)
  360 + if (isNaN(n)) return String(v)
  361 + return n + '%'
  362 + },
  363 + onPreviewAttachment(at) {
  364 + if (!at || !at.id) {
  365 + uni.showToast({ title: '无法查看', icon: 'none' })
  366 + return
  367 + }
  368 + uni.showToast({ title: '查看附件 ' + at.name, icon: 'none' })
  369 + }
  370 + }
  371 +}
  372 +</script>
  373 +
  374 +<style lang="scss" scoped>
  375 +.page {
  376 + display: flex;
  377 + flex-direction: column;
  378 + height: 100%;
  379 +}
  380 +.scroll {
  381 + flex: 1;
  382 + padding: 8rpx 0 144rpx;
  383 +}
  384 +.detail-page {
  385 + background: #f3f3f3;
  386 +}
  387 +.title-header {
  388 + display: flex;
  389 + align-items: center;
  390 + padding: 24rpx 32rpx 12rpx;
  391 + background-color: #ffffff;
  392 + .title-header_icon {
  393 + width: 36rpx;
  394 + height: 36rpx;
  395 + margin-right: 12rpx;
  396 + }
  397 + span {
  398 + font-size: 34rpx;
  399 + font-weight: 600;
  400 + color: rgba(0, 0, 0, 0.9);
  401 + }
  402 +}
  403 +.section {
  404 + padding: 32rpx;
  405 + background: #fff;
  406 + margin-bottom: 20rpx;
  407 +}
  408 +.row {
  409 + display: flex;
  410 + margin-bottom: 28rpx;
  411 + &:last-child { margin-bottom: 0; }
  412 + &.company {
  413 + font-size: 36rpx;
  414 + font-weight: 600;
  415 + color: rgba(0, 0, 0, 0.9);
  416 + padding-top: 8rpx;
  417 + line-height: 50rpx;
  418 + }
  419 + .label {
  420 + max-width: 420rpx;
  421 + margin-right: 20rpx;
  422 + line-height: 32rpx;
  423 + font-size: 28rpx;
  424 + color: rgba(0, 0, 0, 0.6);
  425 + }
  426 + .value {
  427 + flex: 1;
  428 + line-height: 32rpx;
  429 + font-size: 28rpx;
  430 + color: rgba(0, 0, 0, 0.9);
  431 + text-align: right;
  432 + word-break: break-all;
  433 + }
  434 +}
  435 +.attach-list {
  436 + flex: 1;
  437 + display: flex;
  438 + flex-direction: column;
  439 + align-items: flex-end;
  440 + gap: 8rpx;
  441 +}
  442 +.attach-item {
  443 + display: flex;
  444 + align-items: center;
  445 + gap: 16rpx;
  446 + .attach-name {
  447 + font-size: 26rpx;
  448 + color: rgba(0, 0, 0, 0.9);
  449 + }
  450 + .attach-view {
  451 + font-size: 26rpx;
  452 + color: $theme-primary;
  453 + }
  454 +}
  455 +.remark-text {
  456 + font-size: 28rpx;
  457 + color: rgba(0, 0, 0, 0.9);
  458 + line-height: 1.6;
  459 + white-space: pre-wrap;
  460 +}
  461 +.empty-tip {
  462 + text-align: center;
  463 + font-size: 28rpx;
  464 + color: rgba(0, 0, 0, 0.4);
  465 + padding: 24rpx 0;
  466 +}
  467 +</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>
@@ -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 },