From 51fbf6f09678c657ddce01a0d598c748751692bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=B4=E6=98=8A=E5=A4=A9?= <442384644@qq.com> Date: Tue, 13 Oct 2026 18:07:14 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=94=AE=E5=90=8E):=20=E5=94=AE=E5=90=8E?= =?UTF-8?q?=E7=90=86=E7=94=B1=E6=94=B9=E9=85=8D=E7=BD=AE=E4=B8=8B=E6=8B=89?= =?UTF-8?q?=E5=B9=B6=E6=94=AF=E6=8C=81=E5=A4=87=E6=B3=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 售后理由由文本输入改为下拉选择,选项读系统配置 stor_reason(空配置兜底「其它原因」) - 新增备注字段(选填 200 字),申请/详情链路透传展示 - 移动端动态表单支持下拉类型,类型切换时回填退款金额默认值 --- .../v1/order/AfterSaleController.php | 1 + .../app/services/order/AfterSaleServices.php | 2 + .../order/aftersale/BaseAfterSaleHandler.php | 32 +++++++++++--- .../dynamic-pages/engines/AfterSaleEngine.vue | 43 ++++++++++++++++++- 4 files changed, 71 insertions(+), 7 deletions(-) diff --git a/backend/app/api/controller/v1/order/AfterSaleController.php b/backend/app/api/controller/v1/order/AfterSaleController.php index 83bd0d81b..30125a80f 100644 --- a/backend/app/api/controller/v1/order/AfterSaleController.php +++ b/backend/app/api/controller/v1/order/AfterSaleController.php @@ -87,6 +87,7 @@ class AfterSaleController ['refund_price', 0.00], ['refund_integral', 0], // 积分退款标的:退还积分数 ['reason', ''], + ['remark', ''], // 备注(补充说明,选填) ['evidence_images', []], ['product_snapshot', []], ['items', []], // v8.1.1 新增:商品级售后范围 [{cart_id, refund_num}] diff --git a/backend/app/services/order/AfterSaleServices.php b/backend/app/services/order/AfterSaleServices.php index 1e5b20086..e6acb97ff 100644 --- a/backend/app/services/order/AfterSaleServices.php +++ b/backend/app/services/order/AfterSaleServices.php @@ -736,6 +736,7 @@ class AfterSaleServices extends BaseService 'refund_target' => $afterSale['refund_target'] ?? 'cash', 'refund_integral' => (int)($afterSale['refund_integral'] ?? 0), 'reason' => $afterSale['reason'] ?? '', + 'remark' => $afterSale['remark'] ?? '', 'reject_reason' => $afterSale['reject_reason'] ?? '', 'evidence_images' => $afterSale['evidence_images'] ?? [], 'product_snapshot' => $afterSale['product_snapshot'] ?? [], @@ -1131,6 +1132,7 @@ class AfterSaleServices extends BaseService ? 'both' : ($refundIntegral > 0 ? 'integral' : 'cash'), 'reason' => $data['reason'] ?? '', + 'remark' => trim((string)($data['remark'] ?? '')), 'evidence_images' => !empty($data['evidence_images']) && is_array($data['evidence_images']) ? json_encode($data['evidence_images']) : '', 'product_snapshot'=> !empty($snapshot) && is_array($snapshot) diff --git a/backend/crmeb/trade/order/aftersale/BaseAfterSaleHandler.php b/backend/crmeb/trade/order/aftersale/BaseAfterSaleHandler.php index 1de32cedf..21adfc283 100644 --- a/backend/crmeb/trade/order/aftersale/BaseAfterSaleHandler.php +++ b/backend/crmeb/trade/order/aftersale/BaseAfterSaleHandler.php @@ -136,7 +136,7 @@ abstract class BaseAfterSaleHandler implements AfterSaleHandlerInterface /** * 默认售后申请表单配置 * 资金类售后(refund_only/return_refund)展示退款金额;其余类型不展示 - * 所有类型统一展示"售后原因 + 凭证图片" + * 所有类型统一展示"售后理由(配置选择)+ 备注 + 凭证图片" * @param string $type 售后类型 * @return array */ @@ -153,14 +153,23 @@ abstract class BaseAfterSaleHandler implements AfterSaleHandlerInterface 'placeholder' => '请输入退款金额', ]; } - // 售后原因(所有类型必填) + // 售后理由(选项来自系统配置 stor_reason,所有类型必填) $fields[] = [ 'key' => 'reason', - 'label' => '售后原因', - 'type' => 'textarea', + 'label' => '售后理由', + 'type' => 'select', 'required' => true, - 'maxlength' => 100, - 'placeholder' => '请描述售后原因,100字以内', + 'options' => $this->getReasonOptions(), + 'placeholder' => '请选择售后理由', + ]; + // 备注(补充说明,选填) + $fields[] = [ + 'key' => 'remark', + 'label' => '备注', + 'type' => 'textarea', + 'required' => false, + 'maxlength' => 200, + 'placeholder' => '选填,补充描述您的问题(200字以内)', ]; // 凭证图片(所有类型可选) $fields[] = [ @@ -173,6 +182,17 @@ abstract class BaseAfterSaleHandler implements AfterSaleHandlerInterface return ['fields' => $fields]; } + /** + * 售后理由选项(系统配置 stor_reason,换行分隔;配置为空时兜底"其它原因"保证表单可用) + * @return array + */ + protected function getReasonOptions(): array + { + $reason = str_replace("\r\n", "\n", (string)sys_config('stor_reason')); + $options = array_values(array_filter(array_map('trim', explode("\n", $reason)), fn ($v) => $v !== '')); + return $options ?: ['其它原因']; + } + /** * 默认用户可操作按钮 * 按售后单状态返回可操作动作,子类可覆写以增减按钮 diff --git a/frontend/uni-app/src/dynamic-pages/engines/AfterSaleEngine.vue b/frontend/uni-app/src/dynamic-pages/engines/AfterSaleEngine.vue index 3691ff938..fc878f10a 100644 --- a/frontend/uni-app/src/dynamic-pages/engines/AfterSaleEngine.vue +++ b/frontend/uni-app/src/dynamic-pages/engines/AfterSaleEngine.vue @@ -123,6 +123,24 @@ :placeholder="$t(field.placeholder || '')" /> + + + {{ $t(field.label) }} + + + + {{ formValues[field.key] || $t(field.placeholder || '请选择') }} + + + + + {{ $t(field.label) }} @@ -206,6 +224,10 @@ {{ $t('申请原因') }} {{ detail.reason }} + + {{ $t('备注') }} + {{ detail.remark }} + {{ $t('拒绝原因') }} {{ detail.reject_reason }} @@ -633,6 +655,9 @@ async function loadFormConfig(type) { formValues.value[field.key] = []; } else if (field.type === 'price') { formValues.value[field.key] = (orderDetail.value.pay_price || '0.00').toString(); + } else if (field.type === 'select') { + // 下拉字段默认选中第一个选项(无 options 时留空走必填校验) + formValues.value[field.key] = field.default || (field.options && field.options[0]) || ''; } else { formValues.value[field.key] = field.default || ''; } @@ -649,6 +674,17 @@ async function onTypeChange(e) { const type = availableTypes.value[typeIndex.value]; if (type) { await loadFormConfig(type); + // 类型切换会重置表单值,与首次加载一致地回填退款金额/退还积分默认值 + syncRefundPriceField(); + } +} + +/** 动态表单下拉字段变更(选项文本直接作为字段值提交) */ +function onSelectFieldChange(field, e) { + const options = field.options || []; + const idx = Number(e.detail.value); + if (options[idx] !== undefined) { + formValues.value[field.key] = options[idx]; } } @@ -741,7 +777,8 @@ async function submitApply() { const val = formValues.value[field.key]; const empty = Array.isArray(val) ? val.length === 0 : !String(val || '').trim(); if (empty) { - return util.Tips({ title: i18n.global.t('请填写') + i18n.global.t(field.label) }); + const prefix = field.type === 'select' ? i18n.global.t('请选择') : i18n.global.t('请填写'); + return util.Tips({ title: prefix + i18n.global.t(field.label) }); } } } @@ -1232,6 +1269,10 @@ defineExpose({ white-space: normal; word-break: break-word; padding: 20rpx 12rpx 20rpx 0; + + &.picker-placeholder { + color: #bbb; + } } .list .item .picker {