Files
assistant-storefront/app/javascript/shared/components/Logistics.vue
Liang XJ 4bd11c0ecc
Some checks failed
Lock Threads / action (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot EE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot EE docker images / merge (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/amd64, ubuntu-latest) (push) Has been cancelled
Publish Chatwoot CE docker images / build (linux/arm64, ubuntu-22.04-arm) (push) Has been cancelled
Publish Chatwoot CE docker images / merge (push) Has been cancelled
Run Chatwoot CE spec / lint-backend (push) Has been cancelled
Run Chatwoot CE spec / lint-frontend (push) Has been cancelled
Run Chatwoot CE spec / frontend-tests (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (0, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (1, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (10, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (11, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (12, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (13, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (14, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (15, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (2, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (3, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (4, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (5, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (6, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (7, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (8, 16) (push) Has been cancelled
Run Chatwoot CE spec / backend-tests (9, 16) (push) Has been cancelled
Run Linux nightly installer / nightly (push) Has been cancelled
Mark stale issues and pull requests / stale (push) Has been cancelled
feat: add search_image and product_list content types with image upload
## 新增功能

### 1. 新增消息类型
- 添加 search_image (17) 和 product_list (18) content_type
- 支持图片搜索和商品列表消息展示

### 2. 图片上传功能
- 添加 ImageUploadButton 组件,支持图片上传到 Mall API
- 上传后发送 search_image 类型消息,图片 URL 存储在 content_attributes
- 支持图片文件验证(类型、大小)

### 3. Webhook 推送优化
- 修改 webhook_listener.rb,允许 search_image 类型即使 content 为空也推送 webhook
- 解决 search_image 消息不触发 webhook 的问题

### 4. 前端组件
- 新增 SearchImage.vue 组件(widget 和 dashboard)
- 新增 ProductList.vue、Logistics.vue、OrderDetail.vue、OrderList.vue 组件
- 更新 Message.vue 路由逻辑支持新的 content_type
- 更新 UserMessage.vue 支持 search_image 消息显示

### 5. API 层修改
- widget/messages_controller.rb: 允许 content_attributes 参数
- widget/base_controller.rb: 使用前端传入的 content_attributes
- widget/conversation API: 支持 contentAttributes 参数传递
- conversation actions 和 helpers: 完整的 content_attributes 数据流

### 6. Widget 测试页面优化
- 重写 /widget_tests 页面,支持游客/用户模式切换
- 登录流程:使用 reset() + setUser(),不刷新页面
- 退出流程:使用 reset() + 刷新页面
- 添加详细的日志输出和状态显示

## 技术细节

### Message Model
```ruby
enum content_type: {
  # ...existing types...
  search_image: 17,
  product_list: 18
}
```

### Webhook Listener
```ruby
# Allow search_image webhook even if content is blank
return if message.content.blank? && message.content_type != 'search_image'
```

### Widget Upload Flow
```
用户选择图片
  → 上传到 Mall API
  → 获取图片 URL
  → 发送消息: { content: '', content_type: 'search_image', content_attributes: { url: '...' } }
```

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-27 19:03:46 +08:00

176 lines
5.8 KiB
Vue
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.
<template>
<div class="max-w-sm mx-auto bg-white dark:bg-n-solid-1 rounded-xl shadow-md overflow-hidden border border-n-weak">
<!-- 头部状态与单号 -->
<div class="p-4 border-b border-n-weak">
<div class="flex justify-between items-start">
<div>
<h3 class="text-lg font-bold text-n-slate-12 leading-none">{{ currentStatusText }}</h3>
<p class="text-xs text-n-slate-11 mt-2 flex items-center">
{{ logisticsName }}: {{ trackingNumber }}
<button @click="copyTrackingNumber" class="ml-2 text-n-blue-10 hover:text-n-blue-9">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</button>
</p>
</div>
</div>
</div>
<!-- 中间步骤条 -->
<div class="px-6 py-8 relative">
<!-- 进度背景线 -->
<div
class="progress-bar absolute top-[42px] left-10 right-10 h-0.5 bg-n-slate-4 dark:bg-n-slate-6 z-0"
:style="{ '--progress-width': progressWidth }"
></div>
<div class="relative z-10 flex justify-between">
<div
v-for="(step, index) in steps"
:key="index"
class="flex flex-col items-center w-16"
>
<!-- 节点圆圈 -->
<div
class="w-5 h-5 rounded-full flex items-center justify-center flex-shrink-0"
:class="index <= currentStep ? 'bg-n-blue-9 ring-4 ring-n-blue-2' : 'bg-n-slate-4 dark:bg-n-slate-6 ring-4 ring-transparent'"
>
<svg v-if="index < currentStep" class="w-3 h-3 text-white" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
<div v-else-if="index === currentStep" class="w-2 h-2 bg-white rounded-full"></div>
</div>
<!-- 节点文字 -->
<span
class="text-[11px] mt-3 text-center w-full truncate block"
:class="index <= currentStep ? 'text-n-blue-10 font-medium' : 'text-n-slate-11'"
>
{{ step.label }}
</span>
</div>
</div>
</div>
<!-- 详情描述框 -->
<div v-if="latestLog" class="mx-4 mb-4 p-3 bg-n-slate-2 dark:bg-n-solid-2 rounded-lg border-l-4 border-n-blue-9">
<p class="text-xs text-n-slate-11 leading-relaxed">
{{ latestLog }}
</p>
<p v-if="latestTime" class="text-[10px] text-n-slate-11 mt-1">{{ latestTime }}</p>
</div>
<!-- 底部操作栏 -->
<div class="flex border-t border-n-weak" style="border-style: solid;">
<button
v-for="(action, index) in actions"
:key="index"
@click="onActionClick(action)"
class="flex-1 py-3 text-sm transition-colors"
:class="[
action.style === 'primary'
? 'text-n-blue-10 font-semibold hover:bg-n-blue-1 dark:hover:bg-n-solid-blue'
: 'text-n-slate-11 hover:bg-n-slate-2 dark:hover:bg-n-solid-2',
index < actions.length - 1 ? 'border-r border-n-weak' : ''
]"
>
{{ action.text }}
</button>
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
import { useStore } from 'vuex';
const store = useStore();
const { sendMessage } = store;
const props = defineProps({
logisticsName: { type: String, default: '顺丰速运' },
trackingNumber: { type: String, default: 'SF154228901' },
currentStep: { type: Number, default: 2 },
isUrgent: { type: Boolean, default: false },
latestLog: { type: String, default: '上海市 | 包裹正在由派送员王大力13800000000派送中' },
latestTime: { type: String, default: '2023-10-24 14:20:05' },
steps: {
type: Array,
default: () => [
{ label: '已发货' },
{ label: '运输中' },
{ label: '派送中' },
{ label: '已签收' }
]
},
actions: { type: Array, default: () => [] }
});
const currentStatusText = computed(() => {
return props.steps[props.currentStep]?.label || '状态未知';
});
const progressWidth = computed(() => {
// 限制 currentStep 在有效范围内
const validStep = Math.min(props.currentStep, props.steps.length - 1);
// 计算进度条长度:(当前步骤 / 总步数间隔) * 100%
return `${(validStep / (props.steps.length - 1)) * 100}%`;
});
const statusBadgeClass = computed(() => {
return props.isUrgent
? 'bg-n-ruby-2 text-n-ruby-11'
: 'bg-n-teal-2 text-n-teal-11';
});
const copyTrackingNumber = () => {
navigator.clipboard.writeText(props.trackingNumber);
alert('Tracking number copied');
};
const onActionClick = (action) => {
// 如果有 URL根据 target 属性决定打开方式
if (action && action.url) {
const target = action.target || '_self'; // 默认在当前窗口打开
if (window.parent !== window) {
// 在 iframe 中
if (target === '_blank') {
window.parent.open(action.url, '_blank');
} else {
window.parent.location.href = action.url;
}
} else {
// 不在 iframe 中
if (target === '_blank') {
window.open(action.url, '_blank');
} else {
window.location.href = action.url;
}
}
return;
}
// 否则发送消息
if (action && action.reply) {
sendMessage({
type: 'outgoing',
content: action.reply,
});
}
};
</script>
<style scoped>
.progress-bar {
margin: 0 12px;
}
.progress-bar::after {
content: '';
display: block;
height: 100%;
background-color: #2563eb;
width: var(--progress-width, 0%);
}
</style>