377 lines
10 KiB
Markdown
377 lines
10 KiB
Markdown
# Spec: 第四步 - 物流发货信息功能
|
||
|
||
## ADDED Requirements
|
||
|
||
### 发货信息表单
|
||
- **Requirement**: 实现完整的发货信息填写表单,包含收件人、物流公司、运单号等必要信息
|
||
- **Scenario**: 用户进入第四步后可以看到发货信息表单,需要填写完整的收货地址、联系方式和物流信息才能提交
|
||
|
||
### 表单验证
|
||
- **Requirement**: 实现表单字段的完整验证机制,包括必填字段验证、格式验证和业务规则验证
|
||
- **Scenario**: 用户填写表单时能够实时看到验证反馈,提交前进行完整检查,确保数据完整性和正确性
|
||
|
||
### 发货完成流程
|
||
- **Requirement**: 实现发货信息提交后的完整流程,包括状态更新、确认反馈和流程完成
|
||
- **Scenario**: 用户确认发货信息后,系统处理发货请求,更新订单状态,并提供发货完成的确认反馈
|
||
|
||
### 返回上一步功能
|
||
- **Requirement**: 添加返回上一步的导航功能,允许用户修改前面步骤的信息
|
||
- **Scenario**: 用户在发货页面可以点击返回按钮修改模型或拆件结果,已填写的发货信息会被保留
|
||
|
||
### 表单数据持久化
|
||
- **Requirement**: 实现表单数据的本地持久化,防止页面刷新导致数据丢失
|
||
- **Scenario**: 用户在填写发货信息过程中刷新页面,已填写的数据能够自动恢复
|
||
|
||
## 技术实现细节
|
||
|
||
### 布局结构
|
||
```html
|
||
<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>
|
||
```
|
||
|
||
### 表单数据结构和验证
|
||
```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)
|
||
}
|
||
}
|
||
```
|
||
|
||
## 验证标准
|
||
- 发货表单能够正确显示和提交
|
||
- 表单验证规则能够正确工作
|
||
- 返回上一步功能正常
|
||
- 表单数据持久化功能完整
|
||
- 响应式布局在各种设备上表现良好
|
||
- 发货完成流程正常 |