feat: add search_image and product_list content types with image upload
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
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
## 新增功能
### 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>
This commit is contained in:
@@ -42,6 +42,8 @@ import TableBubble from './bubbles/Table.vue';
|
||||
import OrderListBubble from './bubbles/OrderList.vue';
|
||||
import OrderDetailBubble from './bubbles/OrderDetail.vue';
|
||||
import LogisticsBubble from './bubbles/Logistics.vue';
|
||||
import ProductListBubble from './bubbles/ProductList.vue';
|
||||
import SearchImageBubble from './bubbles/SearchImage.vue';
|
||||
|
||||
import MessageError from './MessageError.vue';
|
||||
import ContextMenu from 'dashboard/modules/conversations/components/MessageContextMenu.vue';
|
||||
@@ -308,6 +310,14 @@ const componentToRender = computed(() => {
|
||||
return LogisticsBubble;
|
||||
}
|
||||
|
||||
if (props.contentType === CONTENT_TYPES.PRODUCT_LIST) {
|
||||
return ProductListBubble;
|
||||
}
|
||||
|
||||
if (props.contentType === CONTENT_TYPES.SEARCH_IMAGE) {
|
||||
return SearchImageBubble;
|
||||
}
|
||||
|
||||
if (props.contentType === CONTENT_TYPES.INCOMING_EMAIL) {
|
||||
return EmailBubble;
|
||||
}
|
||||
@@ -404,6 +414,8 @@ const shouldRenderMessage = computed(() => {
|
||||
const isOrderList = props.contentType === CONTENT_TYPES.ORDER_LIST;
|
||||
const isOrderDetail = props.contentType === CONTENT_TYPES.ORDER_DETAIL;
|
||||
const isLogistics = props.contentType === CONTENT_TYPES.LOGISTICS;
|
||||
const isProductList = props.contentType === CONTENT_TYPES.PRODUCT_LIST;
|
||||
const isSearchImage = props.contentType === CONTENT_TYPES.SEARCH_IMAGE;
|
||||
|
||||
return (
|
||||
hasAttachments ||
|
||||
@@ -414,7 +426,9 @@ const shouldRenderMessage = computed(() => {
|
||||
isDataTable ||
|
||||
isOrderList ||
|
||||
isOrderDetail ||
|
||||
isLogistics
|
||||
isLogistics ||
|
||||
isProductList ||
|
||||
isSearchImage
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useMessageContext } from '../provider.js';
|
||||
|
||||
const { contentAttributes } = useMessageContext();
|
||||
|
||||
const title = computed(() => contentAttributes.value?.title || '');
|
||||
const products = computed(() => contentAttributes.value?.products || []);
|
||||
const actions = computed(() => contentAttributes.value?.actions || []);
|
||||
const productCount = computed(() => products.value.length);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="product-list-message">
|
||||
<div v-if="title" class="product-list-title">
|
||||
<h4>{{ title }}</h4>
|
||||
</div>
|
||||
|
||||
<div class="product-grid">
|
||||
<div
|
||||
v-for="(product, index) in products"
|
||||
:key="index"
|
||||
class="product-item"
|
||||
>
|
||||
<div class="product-image">
|
||||
<img
|
||||
:src="product.image"
|
||||
:alt="product.name"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<div class="product-info">
|
||||
<h5 class="product-name">{{ product.name }}</h5>
|
||||
<p class="product-price">{{ product.price }}</p>
|
||||
<p v-if="product.url" class="product-url text-xs text-n-blue-9">
|
||||
<span class="text-xs">🔗 </span>{{ product.url }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="actions && actions.length > 0" class="product-actions">
|
||||
<span
|
||||
v-for="(action, index) in actions"
|
||||
:key="index"
|
||||
class="product-action"
|
||||
>
|
||||
{{ action.text }}{{ index < actions.length - 1 ? ' · ' : '' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.product-list-message {
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.product-list-title {
|
||||
padding: 12px 16px 4px;
|
||||
}
|
||||
|
||||
.product-list-title h4 {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
.dark .product-list-title h4 {
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
.product-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
padding: 0 16px 12px 16px;
|
||||
}
|
||||
|
||||
.product-item {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.dark .product-item {
|
||||
border-color: #374151;
|
||||
background: #1e293b;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
aspect-ratio: 4/5;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.dark .product-image {
|
||||
background: #374151;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
margin: 0 0 4px 0;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.dark .product-name {
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
||||
.product-price {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.product-url {
|
||||
margin: 4px 0 0 0;
|
||||
word-break: break-all;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.product-actions {
|
||||
padding: 8px 16px 12px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.dark .product-actions {
|
||||
color: #94a3b8;
|
||||
border-top-color: #374151;
|
||||
}
|
||||
|
||||
.product-action {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.product-action:hover {
|
||||
color: #2563eb;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div class="search-image-bubble">
|
||||
<div v-if="!imageUrl" class="p-4 text-n-slate-11 text-sm">
|
||||
No image URL
|
||||
</div>
|
||||
<div v-else class="inline-block">
|
||||
<img
|
||||
:src="imageUrl"
|
||||
alt="Search Image"
|
||||
class="max-w-md rounded-lg shadow-sm"
|
||||
@error="handleImageError"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useMessageContext } from '../provider.js';
|
||||
|
||||
const { contentAttributes, content } = useMessageContext();
|
||||
|
||||
const imageUrl = computed(() => {
|
||||
return contentAttributes.value?.url || content.value || '';
|
||||
});
|
||||
|
||||
const handleImageError = (event) => {
|
||||
console.error('[SearchImageBubble] Image failed to load:', imageUrl.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search-image-bubble img {
|
||||
max-height: 400px;
|
||||
width: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -72,6 +72,8 @@ export const CONTENT_TYPES = {
|
||||
ORDER_LIST: 'order_list',
|
||||
ORDER_DETAIL: 'order_detail',
|
||||
LOGISTICS: 'logistics',
|
||||
PRODUCT_LIST: 'product_list',
|
||||
SEARCH_IMAGE: 'search_image',
|
||||
};
|
||||
|
||||
export const MEDIA_TYPES = [
|
||||
|
||||
Reference in New Issue
Block a user