10 KiB
10 KiB
Spec: 第四步 - 物流发货信息功能
ADDED Requirements
发货信息表单
- Requirement: 实现完整的发货信息填写表单,包含收件人、物流公司、运单号等必要信息
- Scenario: 用户进入第四步后可以看到发货信息表单,需要填写完整的收货地址、联系方式和物流信息才能提交
表单验证
- Requirement: 实现表单字段的完整验证机制,包括必填字段验证、格式验证和业务规则验证
- Scenario: 用户填写表单时能够实时看到验证反馈,提交前进行完整检查,确保数据完整性和正确性
发货完成流程
- Requirement: 实现发货信息提交后的完整流程,包括状态更新、确认反馈和流程完成
- Scenario: 用户确认发货信息后,系统处理发货请求,更新订单状态,并提供发货完成的确认反馈
返回上一步功能
- Requirement: 添加返回上一步的导航功能,允许用户修改前面步骤的信息
- Scenario: 用户在发货页面可以点击返回按钮修改模型或拆件结果,已填写的发货信息会被保留
表单数据持久化
- Requirement: 实现表单数据的本地持久化,防止页面刷新导致数据丢失
- Scenario: 用户在填写发货信息过程中刷新页面,已填写的数据能够自动恢复
技术实现细节
布局结构
<div class="shipping-step-content">
<div class="shipping-form-container">
<div class="form-header">
<h3>{{ t('disassembly.steps.shipping') }}</h3>
<el-tag type="info">{{ t('disassembly.status.current') }}</el-tag>
</div>
<el-form
ref="shippingFormRef"
:model="shippingForm"
:rules="shippingRules"
label-width="120px"
class="shipping-form"
>
<!-- 收件人信息 -->
<el-form-item :label="t('disassembly.shipping.recipient')" prop="recipient">
<el-input
v-model="shippingForm.recipient"
:placeholder="t('disassembly.shipping.recipientPlaceholder')"
/>
</el-form-item>
<el-form-item :label="t('disassembly.shipping.phone')" prop="phone">
<el-input
v-model="shippingForm.phone"
:placeholder="t('disassembly.shipping.phonePlaceholder')"
/>
</el-form-item>
<!-- 收货地址 -->
<el-form-item :label="t('disassembly.shipping.address')" prop="address">
<el-input
v-model="shippingForm.address"
type="textarea"
:rows="3"
:placeholder="t('disassembly.shipping.addressPlaceholder')"
/>
</el-form-item>
<!-- 物流公司 -->
<el-form-item :label="t('disassembly.shipping.logistics')" prop="logistics">
<el-select
v-model="shippingForm.logistics"
:placeholder="t('disassembly.shipping.logisticsPlaceholder')"
style="width: 100%;"
>
<el-option label="顺丰速运" value="sf" />
<el-option label="圆通快递" value="yt" />
<el-option label="中通快递" value="zt" />
<el-option label="韵达快递" value="yd" />
<el-option label="申通快递" value="st" />
<el-option label="其他" value="other" />
</el-select>
</el-form-item>
<!-- 运单号 -->
<el-form-item :label="t('disassembly.shipping.trackingNumber')" prop="trackingNumber">
<el-input
v-model="shippingForm.trackingNumber"
:placeholder="t('disassembly.shipping.trackingNumberPlaceholder')"
/>
</el-form-item>
<!-- 备注信息 -->
<el-form-item :label="t('disassembly.shipping.remarks')">
<el-input
v-model="shippingForm.remarks"
type="textarea"
:rows="2"
:placeholder="t('disassembly.shipping.remarksPlaceholder')"
/>
</el-form-item>
</el-form>
</div>
<div class="step-actions">
<el-button @click="goToPreviousStep">
{{ t('disassembly.actions.backToModel') }}
</el-button>
<el-button
type="primary"
@click="submitShipping"
:loading="submitLoading"
:disabled="!shippingFormValid"
>
{{ t('disassembly.actions.submitShipping') }}
</el-button>
</div>
</div>
表单数据结构和验证
const shippingForm = reactive({
recipient: '',
phone: '',
address: '',
logistics: '',
trackingNumber: '',
remarks: ''
})
const shippingRules = {
recipient: [
{ required: true, message: t('disassembly.validation.recipientRequired'), trigger: 'blur' },
{ min: 2, max: 20, message: t('disassembly.validation.recipientLength'), trigger: 'blur' }
],
phone: [
{ required: true, message: t('disassembly.validation.phoneRequired'), trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: t('disassembly.validation.phoneFormat'), trigger: 'blur' }
],
address: [
{ required: true, message: t('disassembly.validation.addressRequired'), trigger: 'blur' },
{ min: 10, max: 200, message: t('disassembly.validation.addressLength'), trigger: 'blur' }
],
logistics: [
{ required: true, message: t('disassembly.validation.logisticsRequired'), trigger: 'change' }
],
trackingNumber: [
{ required: true, message: t('disassembly.validation.trackingNumberRequired'), trigger: 'blur' },
{ pattern: /^[a-zA-Z0-9]{8,20}$/, message: t('disassembly.validation.trackingNumberFormat'), trigger: 'blur' }
]
}
表单提交逻辑
const submitShipping = async () => {
try {
// 表单验证
const valid = await shippingFormRef.value.validate()
if (!valid) return
submitLoading.value = true
// 调用发货API(用户后续实现)
// const result = await submitShippingAPI(contentId, shippingForm)
// 模拟提交过程
await new Promise(resolve => setTimeout(resolve, 2000))
// 更新步骤状态
updateStepStatus(4, 'completed')
workflowState.contentData.shippingInfo = { ...shippingForm }
// 显示成功消息
ElMessage.success(t('disassembly.success.shippingSubmitted'))
// 跳转到完成页面或返回列表
setTimeout(() => {
router.push('/admin/orders/content-review')
}, 2000)
} catch (error) {
ElMessage.error(t('disassembly.errors.shippingSubmitFailed'))
} finally {
submitLoading.value = false
}
}
const goToPreviousStep = () => {
workflowState.currentStep = 3
workflowState.stepStatus[4] = 'pending'
}
数据持久化
// 保存表单数据到sessionStorage
const saveFormData = () => {
sessionStorage.setItem('shippingFormData', JSON.stringify(shippingForm))
}
// 从sessionStorage恢复表单数据
const restoreFormData = () => {
const savedData = sessionStorage.getItem('shippingFormData')
if (savedData) {
try {
const parsed = JSON.parse(savedData)
Object.assign(shippingForm, parsed)
} catch (error) {
console.error('Failed to restore form data:', error)
}
}
}
// 监听表单变化自动保存
watch(shippingForm, saveFormData, { deep: true })
// 组件挂载时恢复数据
onMounted(() => {
restoreFormData()
})
计算属性
const shippingFormValid = computed(() => {
return shippingForm.recipient &&
shippingForm.phone &&
shippingForm.address &&
shippingForm.logistics &&
shippingForm.trackingNumber
})
样式实现
.shipping-form-container {
background: #ffffff;
border-radius: 12px;
padding: 24px;
margin-bottom: 32px;
box-shadow: 0 2px 8px rgba(107, 70, 193, 0.1);
}
.form-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
padding-bottom: 16px;
border-bottom: 1px solid #e5e7eb;
}
.form-header h3 {
margin: 0;
color: #1f2937;
font-size: 18px;
font-weight: 600;
}
.shipping-form {
max-width: 600px;
}
.shipping-form .el-form-item {
margin-bottom: 24px;
}
.shipping-form .el-form-item__label {
font-weight: 500;
color: #374151;
}
.step-actions {
display: flex;
gap: 16px;
justify-content: space-between;
padding-top: 24px;
border-top: 1px solid #e5e7eb;
}
.step-actions .el-button:first-child {
margin-right: auto;
}
.step-actions .el-button:last-child {
min-width: 120px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.shipping-form-container {
padding: 16px;
}
.shipping-form {
max-width: 100%;
}
.shipping-form .el-form-item {
margin-bottom: 16px;
}
.step-actions {
flex-direction: column;
gap: 12px;
}
.step-actions .el-button {
width: 100%;
}
.step-actions .el-button:first-child {
margin-right: 0;
order: 2;
}
.step-actions .el-button:last-child {
order: 1;
}
}
/* 表单验证样式 */
.el-form-item.is-error .el-input__wrapper,
.el-form-item.is-error .el-textarea__inner {
border-color: #f56c6c;
}
.el-form-item__error {
color: #f56c6c;
font-size: 12px;
margin-top: 4px;
}
/* 成功状态样式 */
.shipping-form-container.completed {
background: #f0f9ff;
border: 1px solid #0ea5e9;
}
自动填充逻辑
// 如果用户之前有过发货记录,可以自动填充部分信息
const autoFillFromHistory = async () => {
try {
// 调用历史记录API(用户后续实现)
// const history = await getShippingHistory(userId)
// 模拟获取历史数据
const history = {
recipient: '张三',
phone: '138****8888',
address: '北京市朝阳区***街道**号',
logistics: 'sf'
}
// 只填充非空的历史数据
Object.keys(history).forEach(key => {
if (history[key] && !shippingForm[key]) {
shippingForm[key] = history[key]
}
})
} catch (error) {
console.error('Failed to auto-fill from history:', error)
}
}
验证标准
- 发货表单能够正确显示和提交
- 表单验证规则能够正确工作
- 返回上一步功能正常
- 表单数据持久化功能完整
- 响应式布局在各种设备上表现良好
- 发货完成流程正常