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

10 KiB
Raw Blame History

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)
  }
}

验证标准

  • 发货表单能够正确显示和提交
  • 表单验证规则能够正确工作
  • 返回上一步功能正常
  • 表单数据持久化功能完整
  • 响应式布局在各种设备上表现良好
  • 发货完成流程正常