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 {