From c415d6d13f81009d61d26b3ffe0952a1b7504d19 Mon Sep 17 00:00:00 2001 From: Rogee Date: Sat, 12 Sep 2026 14:42:57 +0800 Subject: [PATCH] feat: improve inbox management and conversation list --- backend/internal/router/router.go | 2 +- backend/internal/router/router_test.go | 3 +- .../ConversationCardExpanded.vue | 291 +++++++------- .../dashboard/components/ChatList.vue | 45 ++- .../dashboard/components/ConversationItem.vue | 21 +- .../dashboard/components/ConversationList.vue | 2 + .../components/ui/DatePicker/DatePicker.vue | 2 +- .../widgets/conversation/ConversationCard.vue | 363 +++++++++--------- .../javascript/dashboard/helper/pushHelper.js | 4 +- .../dashboard/i18n/locale/en/components.json | 1 + .../dashboard/i18n/locale/en/datePicker.json | 1 + .../dashboard/i18n/locale/en/inboxMgmt.json | 1 + .../i18n/locale/zh_CN/components.json | 4 +- .../i18n/locale/zh_CN/datePicker.json | 1 + .../i18n/locale/zh_CN/inboxMgmt.json | 9 +- .../dashboard/i18n/locale/zh_CN/index.js | 6 + .../dashboard/i18n/locale/zh_CN/login.json | 4 +- .../ShangwutongClassifications.vue | 8 +- .../settings/inbox/channels/Shangwutong.vue | 306 +++++++-------- .../channels/ShangwutongConfiguration.vue | 10 +- .../app/javascript/entrypoints/dashboard.js | 2 +- frontend/app/javascript/entrypoints/v3app.js | 2 +- 22 files changed, 551 insertions(+), 537 deletions(-) diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index b703bf8b..a46b9a8b 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -2294,7 +2294,7 @@ func dashboardRuntimeConfig(c *gin.Context) { "helpCenterURL": strings.TrimRight(os.Getenv("HELPCENTER_URL"), "/"), "allowedLoginMethods": []string{"email"}, "signupEnabled": "false", - "selectedLocale": "en", + "selectedLocale": "zh_CN", "globalConfig": map[string]any{ "INSTALLATION_NAME": installationName, "BRAND_NAME": installationName, diff --git a/backend/internal/router/router_test.go b/backend/internal/router/router_test.go index 515d74ee..6c60d870 100644 --- a/backend/internal/router/router_test.go +++ b/backend/internal/router/router_test.go @@ -1119,7 +1119,8 @@ func TestDashboardIndexServesChatwootShell(t *testing.T) { if !strings.Contains(runtimeConfig.Body.String(), `"hostURL":"https://app.example.test"`) || !strings.Contains(runtimeConfig.Body.String(), `"INSTALLATION_NAME":"GoChat Test"`) || !strings.Contains(runtimeConfig.Body.String(), `"BRAND_NAME":"GoChat Test"`) || - !strings.Contains(runtimeConfig.Body.String(), `"LOGO":"/brand-assets/logo.svg"`) { + !strings.Contains(runtimeConfig.Body.String(), `"LOGO":"/brand-assets/logo.svg"`) || + !strings.Contains(runtimeConfig.Body.String(), `"selectedLocale":"zh_CN"`) { t.Fatalf("expected environment-backed runtime config: %s", runtimeConfig.Body.String()) } if strings.Contains(runtimeConfig.Body.String(), " ({}) }, - inbox: { type: Object, default: () => ({}) }, - selected: { type: Boolean, default: false }, - isActiveChat: { type: Boolean, default: false }, - showAssignee: { type: Boolean, default: false }, - showInboxName: { type: Boolean, default: false }, - isInboxView: { type: Boolean, default: false }, + chat: { type: Object, required: true }, + currentContact: { type: Object, required: true }, + assignee: { type: Object, default: () => ({}) }, + inbox: { type: Object, default: () => ({}) }, + selected: { type: Boolean, default: false }, + isActiveChat: { type: Boolean, default: false }, + showAssignee: { type: Boolean, default: false }, + showInboxName: { type: Boolean, default: false }, + isInboxView: { type: Boolean, default: false }, }); const emit = defineEmits([ - 'selectConversation', - 'deSelectConversation', - 'click', - 'contextmenu', + 'selectConversation', + 'deSelectConversation', + 'click', + 'contextmenu', ]); const lastMessageInChat = computed(() => getLastMessage(props.chat)); const showLabelsSection = computed(() => props.chat.labels?.length > 0); const voiceCallData = computed(() => { - const last = lastMessageInChat.value; - if (last?.content_type !== 'voice_call' || !last.call) { - return { status: null, direction: null }; - } - return { - status: last.call.status, - direction: last.call.direction === 'outgoing' ? 'outbound' : 'inbound', - }; + const last = lastMessageInChat.value; + if (last?.content_type !== 'voice_call' || !last.call) { + return { status: null, direction: null }; + } + return { + status: last.call.status, + direction: last.call.direction === 'outgoing' ? 'outbound' : 'inbound', + }; }); const unreadCount = computed(() => props.chat.unread_count); @@ -51,154 +51,143 @@ const unreadCount = computed(() => props.chat.unread_count); const slaCardLabel = useTemplateRef('slaCardLabel'); const hasSlaPolicyId = computed( - () => props.chat?.sla_policy_id || slaCardLabel.value?.hasSlaThreshold + () => props.chat?.sla_policy_id || slaCardLabel.value?.hasSlaThreshold ); const selectedModel = computed({ - get: () => props.selected, - set: value => { - if (value) { - emit('selectConversation', value); - } else { - emit('deSelectConversation', value); - } - }, + get: () => props.selected, + set: value => { + if (value) { + emit('selectConversation', value); + } else { + emit('deSelectConversation', value); + } + }, }); diff --git a/frontend/app/javascript/dashboard/components/ChatList.vue b/frontend/app/javascript/dashboard/components/ChatList.vue index b7192ba4..df735929 100644 --- a/frontend/app/javascript/dashboard/components/ChatList.vue +++ b/frontend/app/javascript/dashboard/components/ChatList.vue @@ -237,7 +237,11 @@ const conversationListPagination = computed(() => { activeAssigneeTabCount.value < conversationsPerPage && activeAssigneeTabCount.value > chatsOnView.value.length; - if (isNoFiltersOrFoldersAndChatListNotEmpty && isUnderPerPage && !hasCurrentPageEndReached.value) { + if ( + isNoFiltersOrFoldersAndChatListNotEmpty && + isUnderPerPage && + !hasCurrentPageEndReached.value + ) { return 1; } @@ -359,6 +363,10 @@ const uniqueInboxes = computed(() => { return [...new Set(selectedInboxes.value)]; }); +const showInboxNameInConversationCard = computed(() => { + return !props.conversationInbox; +}); + // ---------------------- Methods ----------------------- function setFiltersFromUISettings() { const { conversations_filter_by: filterBy = {} } = uiSettings.value; @@ -791,6 +799,9 @@ onMounted(() => { setFiltersFromUISettings(); store.dispatch('setChatStatusFilter', activeStatus.value); store.dispatch('setChatSortFilter', activeSortBy.value); + if (!inboxesList.value.length) { + store.dispatch('inboxes/get'); + } resetAndFetchData(); if (hasActiveFolders.value) { store.dispatch('campaigns/get'); @@ -800,15 +811,26 @@ onMounted(() => { // Fallback: if fetchAllConversations wasn't triggered during onMounted // (e.g., race condition with route transition), retry after store settles. // Uses a one-shot watch on conversationStats to detect empty state. -const _fallbackWatch = watch(conversationStats, (stats) => { - if (!stats) return; - const totalCount = (stats.mineCount || 0) + (stats.unAssignedCount || 0) + (stats.allCount || 0); - // If stats show conversations exist but chatList is empty, re-fetch - if (totalCount > 0 && chatLists.value.length === 0 && !chatListLoading.value) { - _fallbackWatch(); // unwatch immediately — one-shot - fetchConversations(); - } -}, { immediate: false }); +const stopFallbackWatch = watch( + conversationStats, + stats => { + if (!stats) return; + const totalCount = + (stats.mineCount || 0) + + (stats.unAssignedCount || 0) + + (stats.allCount || 0); + // If stats show conversations exist but chatList is empty, re-fetch + if ( + totalCount > 0 && + chatLists.value.length === 0 && + !chatListLoading.value + ) { + stopFallbackWatch(); // unwatch immediately — one-shot + fetchConversations(); + } + }, + { immediate: false } +); const deleteConversationDialogRef = ref(null); const selectedConversationId = ref(null); @@ -877,7 +899,7 @@ watch(chatLists, () => { class="flex flex-col flex-shrink-0 conversations-list-wrap bg-n-surface-1 relative" :class="[ { hidden: !showConversationList }, - isOnExpandedLayout ? 'basis-full' : 'w-[340px] 2xl:w-[412px]', + isOnExpandedLayout ? 'basis-full' : 'w-[320px] 2xl:w-[360px]', ]" > @@ -957,6 +979,7 @@ watch(chatLists, () => { :folders-id="foldersId" :conversation-type="conversationType" :show-assignee="showAssigneeInConversationCard" + :show-inbox-name="showInboxNameInConversationCard" :is-on-expanded-layout="isOnExpandedLayout" @load-more="loadMoreConversations" /> diff --git a/frontend/app/javascript/dashboard/components/ConversationItem.vue b/frontend/app/javascript/dashboard/components/ConversationItem.vue index 06047381..9ba31133 100644 --- a/frontend/app/javascript/dashboard/components/ConversationItem.vue +++ b/frontend/app/javascript/dashboard/components/ConversationItem.vue @@ -15,6 +15,7 @@ const props = defineProps({ conversationType: { type: String, default: '' }, foldersId: { type: [String, Number], default: 0 }, showAssignee: { type: Boolean, default: false }, + showInboxName: { type: Boolean, default: false }, showExpanded: { type: Boolean, default: false }, }); @@ -52,7 +53,6 @@ watch( ); const currentChat = useMapGetter('getSelectedChat'); -const inboxesList = useMapGetter('inboxes/getInboxes'); const activeInbox = useMapGetter('getSelectedInbox'); const accountId = useMapGetter('getCurrentAccountId'); @@ -68,13 +68,22 @@ const isActiveChat = computed(() => currentChat.value.id === props.source.id); const inbox = computed(() => { const inboxId = props.source.inbox_id; - return inboxId ? store.getters['inboxes/getInbox'](inboxId) : {}; + if (!inboxId) return {}; + + const inboxRecord = store.getters['inboxes/getInbox'](inboxId); + return { + ...inboxRecord, + id: inboxRecord.id || inboxId, + name: inboxRecord.name || `#${inboxId}`, + channel_type: inboxRecord.channel_type || props.source.meta?.channel, + }; }); -const showInboxName = computed( - () => !activeInbox.value && inboxesList.value.length > 1 +const showInboxName = computed(() => props.showInboxName); +const activeInboxForRoute = computed(() => + props.showInboxName ? null : activeInbox.value ); -const isInboxView = computed(() => !!activeInbox.value); +const isInboxView = computed(() => !!activeInboxForRoute.value); const showAssigneeForExpandedCard = computed( () => props.showExpanded || props.showAssignee ); @@ -83,7 +92,7 @@ const conversationPath = computed(() => frontendURL( conversationUrl({ accountId: accountId.value, - activeInbox: activeInbox.value, + activeInbox: activeInboxForRoute.value, id: props.source.id, label: props.label, teamId: props.teamId, diff --git a/frontend/app/javascript/dashboard/components/ConversationList.vue b/frontend/app/javascript/dashboard/components/ConversationList.vue index 1cf64e90..f81ee95e 100644 --- a/frontend/app/javascript/dashboard/components/ConversationList.vue +++ b/frontend/app/javascript/dashboard/components/ConversationList.vue @@ -19,6 +19,7 @@ const props = defineProps({ foldersId: { type: [String, Number], default: 0 }, conversationType: { type: String, default: '' }, showAssignee: { type: Boolean, default: false }, + showInboxName: { type: Boolean, default: false }, isOnExpandedLayout: { type: Boolean, default: false }, }); @@ -77,6 +78,7 @@ defineExpose({ conversationListRef }); :folders-id="foldersId" :conversation-type="conversationType" :show-assignee="showAssignee" + :show-inbox-name="showInboxName" :show-expanded="showExpandedCards" /> diff --git a/frontend/app/javascript/dashboard/components/ui/DatePicker/DatePicker.vue b/frontend/app/javascript/dashboard/components/ui/DatePicker/DatePicker.vue index 58a31fba..45f0dd3a 100644 --- a/frontend/app/javascript/dashboard/components/ui/DatePicker/DatePicker.vue +++ b/frontend/app/javascript/dashboard/components/ui/DatePicker/DatePicker.vue @@ -316,7 +316,7 @@ const resetDatePicker = () => { const emitDateRange = () => { if (!isValid(selectedStartDate.value) || !isValid(selectedEndDate.value)) { - useAlert('Please select a valid time range'); + useAlert(t('DATE_PICKER.INVALID_TIME_RANGE')); } else { showDatePicker.value = false; emit('dateRangeChanged', [ diff --git a/frontend/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue b/frontend/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue index 0108acaa..734d43ca 100644 --- a/frontend/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue +++ b/frontend/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue @@ -13,23 +13,23 @@ import VoiceCallStatus from './VoiceCallStatus.vue'; import Checkbox from 'dashboard/components-next/checkbox/Checkbox.vue'; const props = defineProps({ - chat: { type: Object, required: true }, - currentContact: { type: Object, required: true }, - assignee: { type: Object, default: () => ({}) }, - inbox: { type: Object, default: () => ({}) }, - selected: { type: Boolean, default: false }, - isActiveChat: { type: Boolean, default: false }, - showAssignee: { type: Boolean, default: false }, - showInboxName: { type: Boolean, default: false }, - hideThumbnail: { type: Boolean, default: false }, - compact: { type: Boolean, default: false }, + chat: { type: Object, required: true }, + currentContact: { type: Object, required: true }, + assignee: { type: Object, default: () => ({}) }, + inbox: { type: Object, default: () => ({}) }, + selected: { type: Boolean, default: false }, + isActiveChat: { type: Boolean, default: false }, + showAssignee: { type: Boolean, default: false }, + showInboxName: { type: Boolean, default: false }, + hideThumbnail: { type: Boolean, default: false }, + compact: { type: Boolean, default: false }, }); const emit = defineEmits([ - 'click', - 'contextmenu', - 'selectConversation', - 'deSelectConversation', + 'click', + 'contextmenu', + 'selectConversation', + 'deSelectConversation', ]); const hovered = ref(false); @@ -39,205 +39,198 @@ const hasUnread = computed(() => unreadCount.value > 0); const lastMessageInChat = computed(() => getLastMessage(props.chat)); const voiceCallData = computed(() => { - const last = lastMessageInChat.value; - if (last?.content_type !== 'voice_call' || !last.call) { - return { status: null, direction: null }; - } - return { - status: last.call.status, - direction: last.call.direction === 'outgoing' ? 'outbound' : 'inbound', - }; + const last = lastMessageInChat.value; + if (last?.content_type !== 'voice_call' || !last.call) { + return { status: null, direction: null }; + } + return { + status: last.call.status, + direction: last.call.direction === 'outgoing' ? 'outbound' : 'inbound', + }; }); const showMetaSection = computed(() => { - return ( - props.showInboxName || - (props.showAssignee && props.assignee.name) || - props.chat.priority - ); + return ( + props.showInboxName || + (props.showAssignee && props.assignee.name) || + props.chat.priority + ); }); const hasSlaPolicyId = computed(() => props.chat?.sla_policy_id); const showLabelsSection = computed(() => { - return props.chat.labels?.length > 0 || hasSlaPolicyId.value; + return props.chat.labels?.length > 0 || hasSlaPolicyId.value; }); const messagePreviewClass = computed(() => { - return [ - hasUnread.value ? 'font-medium text-n-slate-12' : 'text-n-slate-11', - !props.compact && hasUnread.value ? 'ltr:pr-4 rtl:pl-4' : '', - props.compact && hasUnread.value ? 'ltr:pr-6 rtl:pl-6' : '', - ]; + return [ + '!h-5 !leading-5', + hasUnread.value ? 'font-medium text-n-slate-12' : 'text-n-slate-11', + !props.compact && hasUnread.value ? 'ltr:pr-4 rtl:pl-4' : '', + props.compact && hasUnread.value ? 'ltr:pr-6 rtl:pl-6' : '', + ]; }); const onThumbnailHover = () => { - hovered.value = !props.hideThumbnail; + hovered.value = !props.hideThumbnail; }; const onThumbnailLeave = () => { - hovered.value = false; + hovered.value = false; }; const onSelectConversation = checked => { - if (checked) { - emit('selectConversation', props.chat.id, props.inbox.id); - } else { - emit('deSelectConversation', props.chat.id, props.inbox.id); - } + if (checked) { + emit('selectConversation', props.chat.id, props.inbox.id); + } else { + emit('deSelectConversation', props.chat.id, props.inbox.id); + } }; const selectedModel = computed({ - get: () => props.selected, - set: value => onSelectConversation(value), + get: () => props.selected, + set: value => onSelectConversation(value), }); watch( - () => props.chat.id, - () => { - hovered.value = false; - } + () => props.chat.id, + () => { + hovered.value = false; + } ); diff --git a/frontend/app/javascript/dashboard/helper/pushHelper.js b/frontend/app/javascript/dashboard/helper/pushHelper.js index 44757ffc..e362dcb2 100644 --- a/frontend/app/javascript/dashboard/helper/pushHelper.js +++ b/frontend/app/javascript/dashboard/helper/pushHelper.js @@ -71,7 +71,7 @@ export const registerSubscription = (onSuccess = () => {}) => { .catch(error => { // eslint-disable-next-line no-console console.error('Push subscription registration failed:', error); - useAlert('This browser does not support desktop notification'); + useAlert('DESKTOP_NOTIFICATION_UNSUPPORTED', { usei18n: true }); }); }; @@ -79,7 +79,7 @@ export const requestPushPermissions = ({ onSuccess }) => { if (!('Notification' in window)) { // eslint-disable-next-line no-console console.warn('Notification is not supported'); - useAlert('This browser does not support desktop notification'); + useAlert('DESKTOP_NOTIFICATION_UNSUPPORTED', { usei18n: true }); } else if (Notification.permission === 'granted') { registerSubscription(onSuccess); } else if (Notification.permission !== 'denied') { diff --git a/frontend/app/javascript/dashboard/i18n/locale/en/components.json b/frontend/app/javascript/dashboard/i18n/locale/en/components.json index 5e84ac02..72c69fa1 100644 --- a/frontend/app/javascript/dashboard/i18n/locale/en/components.json +++ b/frontend/app/javascript/dashboard/i18n/locale/en/components.json @@ -53,6 +53,7 @@ "CHANNEL_SELECTOR": { "COMING_SOON": "Coming Soon!" }, + "DESKTOP_NOTIFICATION_UNSUPPORTED": "This browser does not support desktop notifications", "SLASH_COMMANDS": { "HEADING_1": "Heading 1", "HEADING_2": "Heading 2", diff --git a/frontend/app/javascript/dashboard/i18n/locale/en/datePicker.json b/frontend/app/javascript/dashboard/i18n/locale/en/datePicker.json index 95d304cc..a437b3e7 100644 --- a/frontend/app/javascript/dashboard/i18n/locale/en/datePicker.json +++ b/frontend/app/javascript/dashboard/i18n/locale/en/datePicker.json @@ -5,6 +5,7 @@ "WEEK_NUMBER": "Week #{weekNumber}", "APPLY_BUTTON": "Apply", "CLEAR_BUTTON": "Clear", + "INVALID_TIME_RANGE": "Please select a valid time range", "DATE_RANGE_INPUT": { "START": "Start Date", "END": "End Date" diff --git a/frontend/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/frontend/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index 682c8b1b..e8853ee9 100644 --- a/frontend/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/frontend/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -400,6 +400,7 @@ "OFFLINE": "Offline" }, "WEBHOOK_URL": "Connector webhook URL", + "WEBHOOK_URL_PLACEHOLDER": "http://shangwutong-connector:9100/webhooks/gochat/v1", "SUBMIT_BUTTON": "Create Shangwutong channel", "API": { "ERROR_MESSAGE": "We were not able to save the Shangwutong channel" diff --git a/frontend/app/javascript/dashboard/i18n/locale/zh_CN/components.json b/frontend/app/javascript/dashboard/i18n/locale/zh_CN/components.json index 3ac8dfa4..ddf3c04d 100644 --- a/frontend/app/javascript/dashboard/i18n/locale/zh_CN/components.json +++ b/frontend/app/javascript/dashboard/i18n/locale/zh_CN/components.json @@ -53,6 +53,7 @@ "CHANNEL_SELECTOR": { "COMING_SOON": "即将到来!" }, + "DESKTOP_NOTIFICATION_UNSUPPORTED": "此浏览器不支持桌面通知", "SLASH_COMMANDS": { "HEADING_1": "标题 1", "HEADING_2": "标题 2", @@ -63,6 +64,7 @@ "CODE": "代码", "BULLET_LIST": "无序列表", "ORDERED_LIST": "有序列表", - "TABLE": "表格" + "TABLE": "表格", + "IMAGE": "图片" } } diff --git a/frontend/app/javascript/dashboard/i18n/locale/zh_CN/datePicker.json b/frontend/app/javascript/dashboard/i18n/locale/zh_CN/datePicker.json index b4a27a76..365b26ed 100644 --- a/frontend/app/javascript/dashboard/i18n/locale/zh_CN/datePicker.json +++ b/frontend/app/javascript/dashboard/i18n/locale/zh_CN/datePicker.json @@ -5,6 +5,7 @@ "WEEK_NUMBER": "周#{weekNumber}", "APPLY_BUTTON": "应用", "CLEAR_BUTTON": "清除", + "INVALID_TIME_RANGE": "请选择有效的时间范围", "DATE_RANGE_INPUT": { "START": "开始日期", "END": "结束时间" diff --git a/frontend/app/javascript/dashboard/i18n/locale/zh_CN/inboxMgmt.json b/frontend/app/javascript/dashboard/i18n/locale/zh_CN/inboxMgmt.json index 4d837671..fcf4f2c4 100644 --- a/frontend/app/javascript/dashboard/i18n/locale/zh_CN/inboxMgmt.json +++ b/frontend/app/javascript/dashboard/i18n/locale/zh_CN/inboxMgmt.json @@ -67,7 +67,7 @@ "CONNECT_YOUR_TIKTOK_PROFILE": "连接您的 TikTok 主页", "HELP": "要将您的 TikTok 主页添加为渠道,您需要点击\"使用 TikTok 继续\"来认证您的 TikTok 主页", "ERROR_MESSAGE": "连接 TikTok 时出错,请重试", - "ERROR_AUTH": "There was an error connecting to TikTok, please try again" + "ERROR_AUTH": "连接 TikTok 时出错,请重试" }, "TWITTER": { "HELP": "若要将您的Twitter个人资料添加为频道,您需要通过点击“使用Twitter登录”来验证您的Twitter个人资料。 ", @@ -400,6 +400,7 @@ "OFFLINE": "离线" }, "WEBHOOK_URL": "Connector Webhook 地址", + "WEBHOOK_URL_PLACEHOLDER": "http://shangwutong-connector:9100/webhooks/gochat/v1", "SUBMIT_BUTTON": "创建商务通渠道", "API": { "ERROR_MESSAGE": "无法保存商务通渠道" @@ -950,8 +951,8 @@ "NOT_FOUND": "The template does not exist in the Meta platform." }, "TEMPLATE_CREATION": { - "SUCCESS_MESSAGE": "WhatsApp template created successfully and sent for approval", - "ERROR_MESSAGE": "Failed to create WhatsApp template" + "SUCCESS_MESSAGE": "WhatsApp 模板创建成功,已提交审核", + "ERROR_MESSAGE": "WhatsApp 模板创建失败" }, "TEMPLATE_UPDATE_DIALOG": { "TITLE": "Edit survey details", @@ -966,7 +967,7 @@ "GUIDANCE_NOTE": "This is a guidance check, not a guarantee of Meta approval.", "SUGGESTION_LABEL": "Suggested utility-safe rewrite", "APPLY": "Use this rewrite", - "ERROR_MESSAGE": "Couldn't analyze the message. Please try again.", + "ERROR_MESSAGE": "无法分析消息,请重试。", "CLASSIFICATION": { "LIKELY_UTILITY": "Likely Utility", "LIKELY_MARKETING": "Likely Marketing", diff --git a/frontend/app/javascript/dashboard/i18n/locale/zh_CN/index.js b/frontend/app/javascript/dashboard/i18n/locale/zh_CN/index.js index 542ef79b..36d6c97e 100644 --- a/frontend/app/javascript/dashboard/i18n/locale/zh_CN/index.js +++ b/frontend/app/javascript/dashboard/i18n/locale/zh_CN/index.js @@ -10,6 +10,7 @@ import cannedMgmt from './cannedMgmt.json'; import chatlist from './chatlist.json'; import companies from './companies.json'; import components from './components.json'; +import contentTemplates from './contentTemplates.json'; import contact from './contact.json'; import contactFilters from './contactFilters.json'; import conversation from './conversation.json'; @@ -35,9 +36,11 @@ import setNewPassword from './setNewPassword.json'; import settings from './settings.json'; import signup from './signup.json'; import sla from './sla.json'; +import snooze from './snooze.json'; import superAdmin from './superAdmin.json'; import teamsSettings from './teamsSettings.json'; import whatsappTemplates from './whatsappTemplates.json'; +import yearInReview from './yearInReview.json'; export default { ...advancedFilters, @@ -52,6 +55,7 @@ export default { ...chatlist, ...companies, ...components, + ...contentTemplates, ...contact, ...contactFilters, ...conversation, @@ -77,7 +81,9 @@ export default { ...settings, ...signup, ...sla, + ...snooze, ...superAdmin, ...teamsSettings, ...whatsappTemplates, + ...yearInReview, }; diff --git a/frontend/app/javascript/dashboard/i18n/locale/zh_CN/login.json b/frontend/app/javascript/dashboard/i18n/locale/zh_CN/login.json index b616bf7d..9357513e 100644 --- a/frontend/app/javascript/dashboard/i18n/locale/zh_CN/login.json +++ b/frontend/app/javascript/dashboard/i18n/locale/zh_CN/login.json @@ -32,9 +32,9 @@ "LABEL": "Work Email", "PLACEHOLDER": "Enter your work email" }, - "SUBMIT": "Continue with SSO", + "SUBMIT": "使用单点登录继续", "API": { - "ERROR_MESSAGE": "SSO authentication failed. Please check your credentials and try again." + "ERROR_MESSAGE": "单点登录认证失败,请检查凭据后重试。" } } } diff --git a/frontend/app/javascript/dashboard/routes/dashboard/conversation/ShangwutongClassifications.vue b/frontend/app/javascript/dashboard/routes/dashboard/conversation/ShangwutongClassifications.vue index 08bab4a9..f228d298 100644 --- a/frontend/app/javascript/dashboard/routes/dashboard/conversation/ShangwutongClassifications.vue +++ b/frontend/app/javascript/dashboard/routes/dashboard/conversation/ShangwutongClassifications.vue @@ -71,10 +71,7 @@ const fetchClassifications = async () => { catalog.value = response.data; resetSelections(); } catch (error) { - useAlert( - error.message || - t('CONVERSATION_SIDEBAR.SHANGWUTONG_CLASSIFICATIONS.LOAD_ERROR') - ); + useAlert(t('CONVERSATION_SIDEBAR.SHANGWUTONG_CLASSIFICATIONS.LOAD_ERROR')); } finally { isLoading.value = false; } @@ -140,8 +137,7 @@ const updateClassification = async (type, value) => { } catch (error) { resetSelections(); useAlert( - error.message || - t('CONVERSATION_SIDEBAR.SHANGWUTONG_CLASSIFICATIONS.UPDATE_ERROR') + t('CONVERSATION_SIDEBAR.SHANGWUTONG_CLASSIFICATIONS.UPDATE_ERROR') ); } finally { saving.value = ''; diff --git a/frontend/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Shangwutong.vue b/frontend/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Shangwutong.vue index 4e2c92f6..ae6093c0 100644 --- a/frontend/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Shangwutong.vue +++ b/frontend/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/Shangwutong.vue @@ -9,176 +9,166 @@ import NextButton from 'dashboard/components-next/button/Button.vue'; const validSessionId = value => /^[A-Za-z0-9]{11}$/.test(value || ''); const validWebhookUrl = value => { - try { - const parsed = new URL(value); - return ( - ['http:', 'https:'].includes(parsed.protocol) && - !parsed.username && - !parsed.password && - !parsed.hash - ); - } catch { - return false; - } + try { + const parsed = new URL(value); + return ( + ['http:', 'https:'].includes(parsed.protocol) && + !parsed.username && + !parsed.password && + !parsed.hash + ); + } catch { + return false; + } }; export default { - components: { PageHeader, NextButton }, - setup() { - return { v$: useVuelidate() }; - }, - data() { - return { - channelName: '', - sessionId: '', - username: '', - password: '', - desiredPresence: 'online', - webhookUrl: '', - }; - }, - computed: { - ...mapGetters({ uiFlags: 'inboxes/getUIFlags' }), - }, - validations: { - channelName: { required }, - sessionId: { required, validSessionId }, - username: { required }, - password: { required }, - webhookUrl: { required, validWebhookUrl }, - }, - methods: { - async createChannel() { - this.v$.$touch(); - if (this.v$.$invalid) return; + components: { PageHeader, NextButton }, + setup() { + return { v$: useVuelidate() }; + }, + data() { + return { + channelName: '', + sessionId: '', + username: '', + password: '', + desiredPresence: 'online', + webhookUrl: '', + }; + }, + computed: { + ...mapGetters({ uiFlags: 'inboxes/getUIFlags' }), + }, + validations: { + channelName: { required }, + sessionId: { required, validSessionId }, + username: { required }, + password: { required }, + webhookUrl: { required, validWebhookUrl }, + }, + methods: { + async createChannel() { + this.v$.$touch(); + if (this.v$.$invalid) return; - try { - const inbox = await this.$store.dispatch( - 'inboxes/createChannel', - { - name: this.channelName.trim(), - channel: { - type: 'shangwutong', - session_id: this.sessionId.trim(), - username: this.username.trim(), - password: this.password, - desired_presence: this.desiredPresence, - webhook_url: this.webhookUrl.trim(), - }, - } - ); - router.replace({ - name: 'settings_inboxes_add_agents', - params: { page: 'new', inbox_id: inbox.id }, - }); - } catch (error) { - useAlert( - error.message || - this.$t( - 'INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.API.ERROR_MESSAGE' - ) - ); - } - }, - }, + try { + const inbox = await this.$store.dispatch('inboxes/createChannel', { + name: this.channelName.trim(), + channel: { + type: 'shangwutong', + session_id: this.sessionId.trim(), + username: this.username.trim(), + password: this.password, + desired_presence: this.desiredPresence, + webhook_url: this.webhookUrl.trim(), + }, + }); + router.replace({ + name: 'settings_inboxes_add_agents', + params: { page: 'new', inbox_id: inbox.id }, + }); + } catch (error) { + useAlert( + this.$t('INBOX_MGMT.ADD.SHANGWUTONG_CHANNEL.API.ERROR_MESSAGE') + ); + } + }, + }, }; diff --git a/frontend/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/ShangwutongConfiguration.vue b/frontend/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/ShangwutongConfiguration.vue index 06664063..5db707cf 100644 --- a/frontend/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/ShangwutongConfiguration.vue +++ b/frontend/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/ShangwutongConfiguration.vue @@ -49,7 +49,7 @@ export default { const response = await InboxHealthAPI.getHealthStatus(this.inbox.id); this.health = response.data; } catch (error) { - useAlert(error.message || this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE')); + useAlert(this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE')); } finally { this.isLoadingHealth = false; } @@ -60,8 +60,7 @@ export default { this.classifications = response.data; } catch (error) { useAlert( - error.message || - this.$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.CLASSIFICATION_LOAD_ERROR') + this.$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.CLASSIFICATION_LOAD_ERROR') ); } }, @@ -96,8 +95,7 @@ export default { } } catch (error) { useAlert( - error.message || - this.$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.CLASSIFICATION_SYNC_ERROR') + this.$t('INBOX_MGMT.SHANGWUTONG_SETTINGS.CLASSIFICATION_SYNC_ERROR') ); } finally { this.isSyncingClassifications = false; @@ -120,7 +118,7 @@ export default { useAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE')); await this.fetchHealth(); } catch (error) { - useAlert(error.message || this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE')); + useAlert(this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE')); } finally { this.isSaving = false; } diff --git a/frontend/app/javascript/entrypoints/dashboard.js b/frontend/app/javascript/entrypoints/dashboard.js index ff38ebdd..adcc7b7b 100644 --- a/frontend/app/javascript/entrypoints/dashboard.js +++ b/frontend/app/javascript/entrypoints/dashboard.js @@ -37,7 +37,7 @@ import 'floating-vue/dist/style.css'; const i18n = createI18n({ legacy: false, // https://github.com/intlify/vue-i18n/issues/1902 - locale: 'en', + locale: window.chatwootConfig?.selectedLocale || 'zh_CN', messages: i18nMessages, }); diff --git a/frontend/app/javascript/entrypoints/v3app.js b/frontend/app/javascript/entrypoints/v3app.js index e69efdb8..036c276b 100644 --- a/frontend/app/javascript/entrypoints/v3app.js +++ b/frontend/app/javascript/entrypoints/v3app.js @@ -19,7 +19,7 @@ import FluentIcon from 'shared/components/FluentIcon/DashboardIcon.vue'; const i18n = createI18n({ legacy: false, // https://github.com/intlify/vue-i18n/issues/1902 - locale: 'en', + locale: window.chatwootConfig?.selectedLocale || 'zh_CN', messages: i18nMessages, });