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' ? '上传附件(从聊天记录选择文件)' : '开发者上传的附件(点击可预览)'}}
+
+
+
+ {{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 @@
- 从聊天记录上传附件
+ 上传附件(从聊天记录选择文件)