deotalandAi/openspec/changes/create-disassembly-workflow/specs/shipping-step/spec.md

377 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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)
}
}
```
## 验证标准
- 发货表单能够正确显示和提交
- 表单验证规则能够正确工作
- 返回上一步功能正常
- 表单数据持久化功能完整
- 响应式布局在各种设备上表现良好
- 发货完成流程正常