mirror of
https://github.com/crmeb/CRMEB.git
synced 2026-10-28 21:40:50 +00:00
feat(售后): 售后理由改配置下拉并支持备注
- 售后理由由文本输入改为下拉选择,选项读系统配置 stor_reason(空配置兜底「其它原因」) - 新增备注字段(选填 200 字),申请/详情链路透传展示 - 移动端动态表单支持下拉类型,类型切换时回填退款金额默认值
This commit is contained in:
parent
b09d029ef9
commit
51fbf6f096
@ -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}]
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 ?: ['其它原因'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认用户可操作按钮
|
||||
* 按售后单状态返回可操作动作,子类可覆写以增减按钮
|
||||
|
||||
@ -123,6 +123,24 @@
|
||||
:placeholder="$t(field.placeholder || '')"
|
||||
/>
|
||||
</view>
|
||||
<!-- 下拉选择(选项由后端 options 下发) -->
|
||||
<view class="item acea-row row-between-wrapper" v-else-if="field.type === 'select'">
|
||||
<view>{{ $t(field.label) }}</view>
|
||||
<picker
|
||||
class="num"
|
||||
mode="selector"
|
||||
:range="field.options || []"
|
||||
:value="(field.options || []).indexOf(formValues[field.key])"
|
||||
@change="onSelectFieldChange(field, $event)"
|
||||
>
|
||||
<view class="picker acea-row row-between-wrapper">
|
||||
<view class="reason" :class="{ 'picker-placeholder': !formValues[field.key] }">
|
||||
{{ formValues[field.key] || $t(field.placeholder || '请选择') }}
|
||||
</view>
|
||||
<text class="iconfont icon-jiantou"></text>
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
<!-- 数字 -->
|
||||
<view class="item acea-row row-between-wrapper" v-else-if="field.type === 'number'">
|
||||
<view>{{ $t(field.label) }}</view>
|
||||
@ -206,6 +224,10 @@
|
||||
<text class="label">{{ $t('申请原因') }}</text>
|
||||
<text class="value reason-text">{{ detail.reason }}</text>
|
||||
</view>
|
||||
<view class="info-row" v-if="detail.remark">
|
||||
<text class="label">{{ $t('备注') }}</text>
|
||||
<text class="value reason-text">{{ detail.remark }}</text>
|
||||
</view>
|
||||
<view class="info-row" v-if="detail.reject_reason">
|
||||
<text class="label">{{ $t('拒绝原因') }}</text>
|
||||
<text class="value reason-text error">{{ detail.reject_reason }}</text>
|
||||
@ -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 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user