# Spec: 第四步 - 物流发货信息功能
## ADDED Requirements
### 发货信息表单
- **Requirement**: 实现完整的发货信息填写表单,包含收件人、物流公司、运单号等必要信息
- **Scenario**: 用户进入第四步后可以看到发货信息表单,需要填写完整的收货地址、联系方式和物流信息才能提交
### 表单验证
- **Requirement**: 实现表单字段的完整验证机制,包括必填字段验证、格式验证和业务规则验证
- **Scenario**: 用户填写表单时能够实时看到验证反馈,提交前进行完整检查,确保数据完整性和正确性
### 发货完成流程
- **Requirement**: 实现发货信息提交后的完整流程,包括状态更新、确认反馈和流程完成
- **Scenario**: 用户确认发货信息后,系统处理发货请求,更新订单状态,并提供发货完成的确认反馈
### 返回上一步功能
- **Requirement**: 添加返回上一步的导航功能,允许用户修改前面步骤的信息
- **Scenario**: 用户在发货页面可以点击返回按钮修改模型或拆件结果,已填写的发货信息会被保留
### 表单数据持久化
- **Requirement**: 实现表单数据的本地持久化,防止页面刷新导致数据丢失
- **Scenario**: 用户在填写发货信息过程中刷新页面,已填写的数据能够自动恢复
## 技术实现细节
### 布局结构
```html
{{ t('disassembly.actions.backToModel') }}
{{ t('disassembly.actions.submitShipping') }}
```
### 表单数据结构和验证
```javascript
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' }
]
}
```
### 表单提交逻辑
```javascript
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'
}
```
### 数据持久化
```javascript
// 保存表单数据到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()
})
```
### 计算属性
```javascript
const shippingFormValid = computed(() => {
return shippingForm.recipient &&
shippingForm.phone &&
shippingForm.address &&
shippingForm.logistics &&
shippingForm.trackingNumber
})
```
### 样式实现
```css
.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;
}
```
### 自动填充逻辑
```javascript
// 如果用户之前有过发货记录,可以自动填充部分信息
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)
}
}
```
## 验证标准
- 发货表单能够正确显示和提交
- 表单验证规则能够正确工作
- 返回上一步功能正常
- 表单数据持久化功能完整
- 响应式布局在各种设备上表现良好
- 发货完成流程正常