From f13a5e682a71f55236b041e226a734cb744ec568 Mon Sep 17 00:00:00 2001 From: Dev Date: Wed, 30 Sep 2026 11:28:43 +0800 Subject: [PATCH] =?UTF-8?q?=E5=89=8D=E7=AB=AF=E4=BB=A3=E7=A0=81=E4=BC=98?= =?UTF-8?q?=E5=8C=96=EF=BC=8C=E5=8F=8A=E5=90=8E=E7=AB=AF=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/application/api/controller/Demand.php | 14 +- .../miniapp/pages/demand/contract/index.js | 334 +++++++++++++++++- .../miniapp/pages/demand/contract/index.json | 4 +- .../miniapp/pages/demand/contract/index.wxml | 45 ++- .../miniapp/pages/demand/contract/index.wxss | 49 +++ frontend/miniapp/pages/demand/detail/index.js | 50 ++- .../miniapp/pages/demand/detail/index.wxml | 1 + .../miniapp/pages/demand/detail/index.wxs | 16 + .../miniapp/pages/demand/publish/index.wxml | 2 +- 9 files changed, 469 insertions(+), 46 deletions(-) diff --git a/backend/application/api/controller/Demand.php b/backend/application/api/controller/Demand.php index 7af71b0..0bbbc9f 100644 --- a/backend/application/api/controller/Demand.php +++ b/backend/application/api/controller/Demand.php @@ -120,7 +120,12 @@ class Demand extends Api if ($item->status == 1 && $item->isMyWin && $item->winBidding->bidding_status == 2 && $item->winBidding->process_status == 1) { $button = ['name' => '确认接单', 'theme' => 'danger']; } elseif ($item->status == 2 && $item->isMyWin && $item->winBidding->bidding_status == 2 && $item->winBidding->process_status == 2) { - $button = ['name' => '合同签约', 'theme' => 'danger']; + if ($item->demand_contracts && $item->demand_contracts->status == ContractService::STATUS_FINISHED && $item->demand_contracts->status2 == ContractService::STATUS_ONGOING) { + // 开发者已签约,等待客户签约 + $button = ['name' => '签约中', 'theme' => 'light']; + } else { + $button = ['name' => '合同签约', 'theme' => 'danger']; + } } elseif ($item->status == 3 && $item->isMyWin && $item->winBidding->bidding_status == 2 && $item->winBidding->process_status == 2) { $button = ['name' => '工作完成', 'theme' => 'danger']; } elseif ($item->is_bid) { @@ -132,7 +137,12 @@ class Demand extends Api if ($item->status == 1 && $item->isMyWin && $item->winBidding->bidding_status == 2 && $item->winBidding->process_status == 1) { $button = ['name' => '确认接单', 'theme' => 'danger']; } elseif ($item->status == 2 && $item->isMyWin && $item->winBidding->bidding_status == 2 && $item->winBidding->process_status == 2) { - $button = ['name' => '合同签约', 'theme' => 'danger']; + if ($item->demand_contracts && $item->demand_contracts->status == ContractService::STATUS_FINISHED && $item->demand_contracts->status2 == ContractService::STATUS_ONGOING) { + // 开发者已签约,等待客户签约 + $button = ['name' => '签约中', 'theme' => 'light']; + } else { + $button = ['name' => '合同签约', 'theme' => 'danger']; + } } elseif ($item->status == 3 && $item->isMyWin && $item->winBidding->bidding_status == 2 && $item->winBidding->process_status == 2) { $button = ['name' => '工作完成', 'theme' => 'danger']; } elseif ($item->status == 4 && $item->accept_status == 1) { diff --git a/frontend/miniapp/pages/demand/contract/index.js b/frontend/miniapp/pages/demand/contract/index.js index 1321cb5..b1ad317 100644 --- a/frontend/miniapp/pages/demand/contract/index.js +++ b/frontend/miniapp/pages/demand/contract/index.js @@ -11,6 +11,22 @@ Page({ actualAccount: 0, contract: null, agreementUrl: '', // 签约条款URL + phoneError: '', // 个人联系电话错误提示 + companyPhoneError: '', // 企业联系电话错误提示 + // 附件上传 + attachFiles: [], + attachMax: 5, // 最多上传5个附件 + attachConfig: { + count: 5, + }, + attachSizeLimit: { + size: 20, unit: 'MB', message: '附件大小不超过 {sizeLimit} MB' + }, + gridConfig: { + column: 4, + width: 160, + height: 160, + }, formData: { contractType: 'personal', // 默认选择个人签约 // 个人签约信息 @@ -25,6 +41,7 @@ Page({ taskContent: '', needDays: '', totalCost: '', + attachFiles: [], // 签约条款 agreementChecked: false } @@ -114,6 +131,16 @@ Page({ this.setData({ 'formData.phone': e.detail.value }); + // 输入过程中若已修正为合法手机号,立即清除错误提示 + if (this.data.phoneError && this.isPhone(e.detail.value)) { + this.setData({ phoneError: '' }); + } + }, + + onPhoneBlur(e) { + this.setData({ + phoneError: this.getPhoneError(e.detail.value) + }); }, onIdCardChange(e) { @@ -139,6 +166,15 @@ Page({ this.setData({ 'formData.companyPhone': e.detail.value }); + if (this.data.companyPhoneError && this.isPhone(e.detail.value)) { + this.setData({ companyPhoneError: '' }); + } + }, + + onCompanyPhoneBlur(e) { + this.setData({ + companyPhoneError: this.getPhoneError(e.detail.value) + }); }, // 开发信息变更 @@ -162,6 +198,183 @@ Page({ }); }, + // 附件选择成功(仅开发者可上传) + handleSuccessAttach(e) { + if (this.data.source != 'dev') { + return; + } + const files = e.detail?.files || []; + const { attachFiles, attachMax } = this.data; + const remain = attachMax - attachFiles.length; + const accepted = files.slice(0, remain); + if (files.length > remain) { + wx.showToast({ + title: `最多上传${attachMax}个附件`, + icon: 'none' + }); + } + if (!accepted.length) { + return; + } + const list = [...attachFiles, ...accepted]; + this.setData({ + attachFiles: list, + attachConfig: { count: attachMax - list.length }, + 'formData.attachFiles': list + }); + this.uploadAttachFiles(accepted); + }, + + // 移除附件(仅开发者可移除;客户侧附件为开发者上传,只读) + handleRemoveAttach(e) { + if (this.data.source != 'dev') { + return; + } + const { index } = e.currentTarget.dataset; + const { attachFiles, attachMax } = this.data; + attachFiles.splice(index, 1); + this.setData({ + attachFiles, + attachConfig: { count: attachMax - attachFiles.length }, + 'formData.attachFiles': attachFiles + }); + }, + + // 附件预览:客户点击开发者上传的附件进行查看 + handlePreviewAttach(e) { + // 仅客户侧可预览;开发者侧附件可删除,避免删除时误触预览 + if (this.data.source == 'dev') { + return; + } + const { index } = e.currentTarget.dataset; + const file = this.data.attachFiles[index]; + const fileUrl = file ? (file.fullUrl || file.url) : ''; + if (!fileUrl) { + return; + } + // 图片直接预览 + if (/\.(jpe?g|png|gif|webp|bmp)$/i.test(fileUrl)) { + wx.previewImage({ + urls: [fileUrl] + }); + return; + } + // 其他文件下载后打开 + wx.showLoading({ title: '加载中...' }); + wx.downloadFile({ + url: fileUrl, + success: (res) => { + wx.hideLoading(); + if (res.statusCode === 200) { + wx.openDocument({ + filePath: res.tempFilePath, + showMenu: true, + fail: () => { + wx.showToast({ title: '该文件类型暂不支持预览', icon: 'none' }); + } + }); + } else { + wx.showToast({ title: '附件加载失败', icon: 'none' }); + } + }, + fail: () => { + wx.hideLoading(); + wx.showToast({ title: '附件加载失败', icon: 'none' }); + } + }); + }, + + // 上传附件到服务器 + uploadAttachFiles(files) { + files.forEach((file, index) => { + if (file.status === 'done') { + return; + } + wx.uploadFile({ + url: wx.$api.UploadFile, + filePath: file.url, + name: 'file', + formData: { + 'name': file.name, + 'type': file.type + }, + header: { + 'Accept': 'application/json', + 'token': wx.getStorageSync('token') + }, + success: (res) => { + if (res.statusCode == 413) { + wx.showToast({ + title: '附件大小超出限制', + icon: 'none' + }); + this.removeAttachByUrl(file.url); + return; + } + let response = {}; + try { + response = JSON.parse(res.data); + } catch (err) { + response = {}; + } + if (response.code === 1 && response.data && response.data.length > 0) { + this.updateAttachFile({ + ...file, + fullUrl: response.data[0].url, + status: 'done' + }); + if (index === files.length - 1) { + wx.showToast({ + title: '附件上传成功', + icon: 'success' + }); + } + } else { + wx.showToast({ + title: response.msg || '附件上传失败', + icon: 'none' + }); + this.removeAttachByUrl(file.url); + } + }, + fail: () => { + wx.showToast({ + title: '附件上传失败', + icon: 'none' + }); + this.removeAttachByUrl(file.url); + } + }); + }); + }, + + // 更新单个附件(上传完成后回写) + updateAttachFile(newFile) { + const { attachFiles, attachMax } = this.data; + const idx = attachFiles.findIndex(item => item.url === newFile.url); + if (idx === -1) { + return; + } + const list = [...attachFiles]; + list[idx] = newFile; + this.setData({ + attachFiles: list, + attachConfig: { count: attachMax - list.length }, + 'formData.attachFiles': list + }); + }, + + // 上传失败时移除附件 + removeAttachByUrl(url) { + const { attachFiles, attachMax } = this.data; + const list = attachFiles.filter(item => item.url !== url); + this.setData({ + attachFiles: list, + attachConfig: { count: attachMax - list.length }, + 'formData.attachFiles': list + }); + }, + // 签约条款变更 onAgreementChange(e) { console.log('checkbox', e); @@ -170,18 +383,55 @@ Page({ }); }, + // 是否为合法手机号(复用项目内置正则) + isPhone(phone) { + const value = String(phone === undefined || phone === null ? '' : phone).trim(); + if (!value) { + return false; + } + return wx.$util.phoneRegCheck(value); + }, + + // 手机号错误提示文案,合法或为空时返回空串(为空由必填校验负责提示) + getPhoneError(phone) { + const value = String(phone === undefined || phone === null ? '' : phone).trim(); + if (!value || this.isPhone(value)) { + return ''; + } + return '手机号格式不正确,请输入11位手机号'; + }, + + // 提交时校验手机号:写入错误态并 toast,返回是否存在错误 + hasPhoneError(phone, errorField) { + const error = this.getPhoneError(phone); + this.setData({ + [errorField]: error + }); + if (error) { + wx.showToast({ + title: error, + icon: 'none' + }); + } + return !!error; + }, + // 表单验证 validateForm() { const { formData } = this.data; const contractType = formData.contractType; if (contractType === 'personal') { - if (!formData.name || !formData.phone || !formData.idCard) { + // 身份证号暂不采集 + if (!formData.name || !formData.phone) { wx.showToast({ title: '请填写完整的个人信息', icon: 'none' }); return false; } + if (this.hasPhoneError(formData.phone, 'phoneError')) { + return false; + } } else { if (!formData.companyName || !formData.legalPerson || !formData.companyPhone) { wx.showToast({ @@ -190,6 +440,9 @@ Page({ }); return false; } + if (this.hasPhoneError(formData.companyPhone, 'companyPhoneError')) { + return false; + } } if (!formData.taskContent || !formData.needDays || !formData.totalCost) { @@ -200,6 +453,14 @@ Page({ return false; } + if (this.data.attachFiles.some(item => item.status !== 'done')) { + wx.showToast({ + title: '附件上传中,请稍候', + icon: 'none' + }); + return false; + } + if (!formData.agreementChecked) { wx.showToast({ title: '请阅读并同意签约条款', @@ -218,22 +479,49 @@ Page({ demandId, } fetchDemandContractDetail(params).then( - (res) => { - console.log(res); - if (res.code == 1) { - let resData = res.data; - let resFormData = this.data.source == 'dev' ? resData.data_json.devData : resData.data_json.customData; - this.setData({ - contract: resData, - formData: resFormData, - 'formData.contractType': resFormData?.contractType || 'personal', - 'formData.taskContent': resData.task_content, - 'formData.needDays': resData.need_days, - 'formData.totalCost': resData.total_amount, - }); + (res) => { + console.log(res); + if (res.code == 1) { + let resData = res.data; + const isDev = this.data.source == 'dev'; + let resFormData = isDev ? resData.data_json.devData : resData.data_json.customData; + this.setData({ + contract: resData, + formData: resFormData, + 'formData.contractType': resFormData?.contractType || 'personal', + 'formData.taskContent': resData.task_content, + 'formData.needDays': resData.need_days, + 'formData.totalCost': resData.total_amount, + }); + + // 客户签约时,附件展示开发者签约时上传的附件,仅可预览,不可删除和上传 + const rawAttachFiles = isDev + ? (resFormData?.attachFiles || []) + : (resData.data_json?.devData?.attachFiles || []); + // 统一补齐 fullUrl 与上传状态,便于预览取地址、提交时跳过上传中校验 + const attachFiles = rawAttachFiles.map((file) => ({ + ...file, + fullUrl: file.fullUrl || file.url, + status: file.status || 'done' + })); + this.setData({ + attachFiles, + 'formData.attachFiles': attachFiles, + attachConfig: { count: this.data.attachMax - attachFiles.length } + }); + } } - } - ); + ); + }, + + // 返回列表页并刷新,同时给中间的详情页打刷新标记(详情页 onShow 检测 freshPage 重新拉取) + backAndRefreshAll() { + const pages = getCurrentPages(); + const detailPage = pages.length >= 2 ? pages[pages.length - 2] : null; + if (detailPage) { + detailPage.setData({ freshPage: true }); + } + wx.$util.backAndRefresh(2); }, //签约订单支付 @@ -281,9 +569,17 @@ Page({ }); const { demandId, formData, source } = this.data; + // 附件只提交服务端地址与文件名,避免提交本地临时路径 + const submitFormData = { + ...formData, + attachFiles: (formData.attachFiles || []).map(file => ({ + url: file.fullUrl || file.url, + name: file.name + })) + }; let params = { demandId, - formData, + formData: submitFormData, source, 'agree': 1, 'contractType': 1, @@ -303,7 +599,7 @@ Page({ if (res2.confirm) { //返回上一页 // wx.navigateBack({ delta: 2 }); - wx.$util.backAndRefresh(2); + this.backAndRefreshAll(); console.log('用户点击确定'); } } @@ -317,7 +613,7 @@ Page({ success: (res3) => { if (res3.confirm) { // wx.navigateBack({ delta: 2 }); - wx.$util.backAndRefresh(2); + this.backAndRefreshAll(); console.log('用户点击确定'); } } diff --git a/frontend/miniapp/pages/demand/contract/index.json b/frontend/miniapp/pages/demand/contract/index.json index e32ad95..9a1e011 100644 --- a/frontend/miniapp/pages/demand/contract/index.json +++ b/frontend/miniapp/pages/demand/contract/index.json @@ -6,6 +6,8 @@ "t-divider": "tdesign-miniprogram/divider/divider", "t-textarea": "tdesign-miniprogram/textarea/textarea", "t-checkbox": "tdesign-miniprogram/checkbox/checkbox", - "t-button": "tdesign-miniprogram/button/button" + "t-button": "tdesign-miniprogram/button/button", + "t-upload": "tdesign-miniprogram/upload/upload", + "t-tag": "tdesign-miniprogram/tag/tag" } } \ No newline at end of file diff --git a/frontend/miniapp/pages/demand/contract/index.wxml b/frontend/miniapp/pages/demand/contract/index.wxml index a43a05e..14920e6 100644 --- a/frontend/miniapp/pages/demand/contract/index.wxml +++ b/frontend/miniapp/pages/demand/contract/index.wxml @@ -11,8 +11,9 @@ - - + + + @@ -21,14 +22,48 @@ - + - + + + + + + + {{source == 'dev' ? '上传附件(从聊天记录选择文件)' : '开发者上传的附件(点击可预览)'}} + {{attachFiles.length}}/{{attachMax}} + + + {{item.name}} + + + + @@ -37,7 +72,7 @@ 我已阅读并同意 - 《签约服务条款》 + 《签约服务条款》 diff --git a/frontend/miniapp/pages/demand/contract/index.wxss b/frontend/miniapp/pages/demand/contract/index.wxss index bed75f0..98673fb 100644 --- a/frontend/miniapp/pages/demand/contract/index.wxss +++ b/frontend/miniapp/pages/demand/contract/index.wxss @@ -71,5 +71,54 @@ .agreement-link { display: inline; color: #0052d9; + text-decoration: none; +} + +/* 仅正文加下划线,《》符号不加 */ +.agreement-text { text-decoration: underline; +} + +.agreement-mark { + text-decoration: none; +} + +/* ================= 附件上传 ================= */ +/* 不加左右内边距,与页面容器 32rpx 对齐 */ +.attach-card { + padding: 24rpx 0 8rpx; +} + +.attach-head { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + margin-bottom: 8rpx; +} + +.attach-title { + font-size: 28rpx; + font-weight: 500; + color: var(--text-primary); +} + +.attach-extra { + font-size: 24rpx; + color: var(--text-placeholder, #bbb); +} + +.attach-tip { + font-size: 24rpx; + color: var(--text-secondary, #888); + margin-bottom: 8rpx; +} + +.attach-list { + display: flex; + flex-wrap: wrap; +} + +.file-tag-margin { + margin: 8rpx 16rpx 8rpx 0; } \ No newline at end of file diff --git a/frontend/miniapp/pages/demand/detail/index.js b/frontend/miniapp/pages/demand/detail/index.js index cd7c342..2de8e20 100644 --- a/frontend/miniapp/pages/demand/detail/index.js +++ b/frontend/miniapp/pages/demand/detail/index.js @@ -18,19 +18,28 @@ Page({ /** * 获取需求详情 */ + /** + * 获取需求详情 + * @param {number|string} id + * @returns {Promise} 详情渲染完成后 resolve + */ loadDemandDetail(id) { - fetchDemandDetail({ id: id }).then( - (res) => { - console.log(res); - this.setData({ - demandInfo: res.data, - actualAccount: parseFloat(res.data?.myBidding?.quoted_price || res.data?.demand_contracts?.total_amount || 0) * 0.9 - }); - }, - () => { - //接口异常处理 - }, - ); + return new Promise((resolve) => { + fetchDemandDetail({ id: id }).then( + (res) => { + console.log(res); + const demandInfo = res.data; + this.setData({ + demandInfo, + actualAccount: parseFloat(demandInfo?.myBidding?.quoted_price || demandInfo?.demand_contracts?.total_amount || 0) * 0.9 + }, () => resolve()); // setData 回调:页面已重新渲染 + }, + () => { + //接口异常处理 + resolve(); + }, + ); + }); }, //文件预览 fileView(e) { @@ -185,11 +194,12 @@ Page({ success: (res) => { if (res.confirm) { if (agree) { - //刷新当前页面 - this.loadDemandDetail(this.data.id); - wx.pageScrollTo({ - scrollTop: 0, - duration: 300 + //刷新当前页面,等沟通群二维码渲染完成后再回到顶部,方便直接看到 + this.loadDemandDetail(this.data.id).then(() => { + wx.pageScrollTo({ + scrollTop: 0, + duration: 300 + }); }); } else { //返回上一页 @@ -564,7 +574,11 @@ Page({ * 生命周期函数--监听页面显示 */ onShow() { - + // 从签约页等操作页返回时,backAndRefresh 会给本页打 freshPage 标记,此时刷新详情 + if (this.data.freshPage) { + this.setData({ freshPage: false }); + this.loadDemandDetail(this.data.id); + } }, /** diff --git a/frontend/miniapp/pages/demand/detail/index.wxml b/frontend/miniapp/pages/demand/detail/index.wxml index 3564cdb..e423852 100644 --- a/frontend/miniapp/pages/demand/detail/index.wxml +++ b/frontend/miniapp/pages/demand/detail/index.wxml @@ -68,6 +68,7 @@ 放弃签约 合同签约 + 签约中(等待客户签约) 完成失败 工作完成 diff --git a/frontend/miniapp/pages/demand/detail/index.wxs b/frontend/miniapp/pages/demand/detail/index.wxs index 2da4920..5783916 100644 --- a/frontend/miniapp/pages/demand/detail/index.wxs +++ b/frontend/miniapp/pages/demand/detail/index.wxs @@ -14,6 +14,12 @@ var canConfirmOrder = function (demandInfo) { var canSignContractDev = function (demandInfo) { if (!demandInfo) return false; + // 开发者已签约(合同 status==2)、等待客户签约(status2==1)时,不再显示签约按钮 + if (demandInfo.demand_contracts && + demandInfo.demand_contracts.status == 2 && + demandInfo.demand_contracts.status2 == 1) { + return false; + } return demandInfo.status == 2 && demandInfo.isMyWin && demandInfo.winBidding && @@ -21,6 +27,15 @@ var canSignContractDev = function (demandInfo) { demandInfo.winBidding.process_status == 2; } +// 开发者已签约、等待客户签约中 +var isWaitingCustomerSign = function (demandInfo) { + if (!demandInfo) return false; + return demandInfo.status == 2 && + demandInfo.demand_contracts && + demandInfo.demand_contracts.status == 2 && + demandInfo.demand_contracts.status2 == 1; +} + var canSignContractCustomer = function (demandInfo) { if (!demandInfo) return false; return demandInfo.status == 2 && @@ -61,6 +76,7 @@ module.exports = { canSubmitBidding: canSubmitBidding, canConfirmOrder: canConfirmOrder, canSignContractDev: canSignContractDev, + isWaitingCustomerSign: isWaitingCustomerSign, canSignContractCustomer: canSignContractCustomer, canCompleteWork: canCompleteWork, canAcceptPassed: canAcceptPassed, diff --git a/frontend/miniapp/pages/demand/publish/index.wxml b/frontend/miniapp/pages/demand/publish/index.wxml index 05de029..d0f7e58 100644 --- a/frontend/miniapp/pages/demand/publish/index.wxml +++ b/frontend/miniapp/pages/demand/publish/index.wxml @@ -86,7 +86,7 @@ - 从聊天记录上传附件 + 上传附件(从聊天记录选择文件) 选填