fix: isolate customer service chats and reload bank details

This commit is contained in:
richjohnrambo
2026-10-16 14:01:31 +08:00
parent 443622cd3c
commit 70cd0e592a
3 changed files with 73 additions and 46 deletions
+1 -18
View File
@@ -1856,7 +1856,7 @@ body {
padding: 16px; padding: 16px;
color: var(--m-ink); color: var(--m-ink);
} }
.m-bound-account > div:not(.m-bound-account-qr) { .m-bound-account > div {
display: grid; display: grid;
gap: 6px; gap: 6px;
} }
@@ -1867,23 +1867,6 @@ body {
overflow-wrap: anywhere; overflow-wrap: anywhere;
font-weight: 500; font-weight: 500;
} }
.m-bound-account-qr {
display: flex;
flex-wrap: wrap;
gap: 16px;
}
.m-bound-account-qr figure {
margin: 0;
text-align: center;
color: var(--m-muted);
}
.m-bound-account-qr img {
display: block;
width: 100px;
height: 100px;
margin-bottom: 6px;
object-fit: contain;
}
.m-bank-form-title { .m-bank-form-title {
padding: 12px 0 0; padding: 12px 0 0;
} }
+20 -12
View File
@@ -60,10 +60,6 @@
<div><span>银行名</span><strong>{{ account.bank_name }}</strong></div> <div><span>银行名</span><strong>{{ account.bank_name }}</strong></div>
<div><span>银行账户</span><strong>{{ account.account_no }}</strong></div> <div><span>银行账户</span><strong>{{ account.account_no }}</strong></div>
<div v-if="account.digital_rmb_code"><span>人民币编码</span><strong>{{ account.digital_rmb_code }}</strong></div> <div v-if="account.digital_rmb_code"><span>人民币编码</span><strong>{{ account.digital_rmb_code }}</strong></div>
<div v-if="safeImage(account.alipay_qr_url) || safeImage(account.wechat_qr_url)" class="m-bound-account-qr">
<figure v-if="safeImage(account.alipay_qr_url)"><img :src="safeImage(account.alipay_qr_url)" alt="支付宝收款码" /><figcaption>支付宝收款码</figcaption></figure>
<figure v-if="safeImage(account.wechat_qr_url)"><img :src="safeImage(account.wechat_qr_url)" alt="微信收款码" /><figcaption>微信收款码</figcaption></figure>
</div>
</div> </div>
</section> </section>
<form class="m-plain-form m-bank-form" @submit.prevent="saveBank"> <form class="m-plain-form m-bank-form" @submit.prevent="saveBank">
@@ -238,7 +234,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'; import { computed, reactive, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { import {
CardOutline, CardOutline,
@@ -286,6 +282,7 @@
masked: boolean; masked: boolean;
} }
const paymentAccounts = ref<PaymentAccountDetail[]>([]); const paymentAccounts = ref<PaymentAccountDetail[]>([]);
let paymentAccountsLoadVersion = 0;
const content = ref<any>(); const content = ref<any>();
const resetPassword = ref(''); const resetPassword = ref('');
const bank = reactive({ const bank = reactive({
@@ -348,10 +345,14 @@
} }
} }
async function loadPaymentAccounts() { async function loadPaymentAccounts() {
const version = ++paymentAccountsLoadVersion;
error.value = '';
paymentAccounts.value = [];
try { try {
paymentAccounts.value = await request<PaymentAccountDetail[]>({ url: '/v1/payment-accounts/details' }); const accounts = await request<PaymentAccountDetail[]>({ url: '/v1/payment-accounts/details' });
if (version === paymentAccountsLoadVersion && formType.value === 'bank') paymentAccounts.value = accounts;
} catch (err) { } catch (err) {
error.value = errorText(err); if (version === paymentAccountsLoadVersion && formType.value === 'bank') error.value = errorText(err);
} }
} }
async function saveBank() { async function saveBank() {
@@ -509,9 +510,16 @@
loading.value = false; loading.value = false;
} }
} }
onMounted(() => { watch(
if (formType.value === 'profile') loadProfile(); () => [formType.value, String(route.params.key || '')],
else if (formType.value === 'bank') loadPaymentAccounts(); ([type]) => {
else if (formType.value === 'content') loadContent(); paymentAccountsLoadVersion += 1;
}); error.value = '';
if (type !== 'content') content.value = undefined;
if (type === 'profile') void loadProfile();
else if (type === 'bank') void loadPaymentAccounts();
else if (type === 'content') void loadContent();
},
{ immediate: true }
);
</script> </script>
+52 -16
View File
@@ -144,7 +144,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'; import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { import {
ChatbubbleEllipsesOutline, ChatbubbleEllipsesOutline,
@@ -214,6 +214,7 @@
const sessionStartedAt = ref(new Date().toISOString()); const sessionStartedAt = ref(new Date().toISOString());
const messageList = ref<HTMLElement | null>(null); const messageList = ref<HTMLElement | null>(null);
let chat: CustomerServiceChat | null = null; let chat: CustomerServiceChat | null = null;
let connectionVersion = 0;
async function load() { async function load() {
loading.value = true; loading.value = true;
error.value = ''; error.value = '';
@@ -224,7 +225,6 @@
]); ]);
services.value = serviceRows; services.value = serviceRows;
me.value = profile; me.value = profile;
if (isChat.value) await connectChat();
} catch (err) { } catch (err) {
const message = errorText(err); const message = errorText(err);
error.value = message; error.value = message;
@@ -239,54 +239,79 @@
if (status === 'busy') return '忙碌'; if (status === 'busy') return '忙碌';
return '离线留言'; return '离线留言';
} }
async function connectChat() { function disconnectChat() {
const serviceID = Number(route.params.id); connectionVersion += 1;
if (!serviceID || chatLoading.value) return;
chat?.disconnect(); chat?.disconnect();
chat = null; chat = null;
chatLoading.value = true;
chatError.value = '';
messages.value = []; messages.value = [];
historyBoundarySeq.value = 0; historyBoundarySeq.value = 0;
chatLoading.value = false;
chatError.value = '';
draft.value = '';
sending.value = false;
mediaSending.value = false;
uploadProgress.value = 0;
toolsOpen.value = false;
emojiOpen.value = false;
}
async function connectChat() {
const serviceID = Number(route.params.id);
disconnectChat();
if (!serviceID) return;
const version = connectionVersion;
chatLoading.value = true;
try { try {
const session = await request<CustomerServiceSession>({ const session = await request<CustomerServiceSession>({
url: `/v1/customer-services/${serviceID}/session`, url: `/v1/customer-services/${serviceID}/session`,
method: 'POST', method: 'POST',
data: { device_id: deviceID(), platform: 'web', app_version: '1.0.0' }, data: { device_id: deviceID(), platform: 'web', app_version: '1.0.0' },
}); });
chat = new CustomerServiceChat(session, (rows) => { if (version !== connectionVersion) return;
const nextChat = new CustomerServiceChat(session, (rows) => {
if (version !== connectionVersion) return;
messages.value = rows; messages.value = rows;
void scrollToBottom(); void scrollToBottom();
}); });
await chat.connect(session.token); chat = nextChat;
await nextChat.connect(session.token);
if (version !== connectionVersion) {
nextChat.disconnect();
return;
}
// Tinode delivers cached topic history just after subscribe resolves. // Tinode delivers cached topic history just after subscribe resolves.
// Give that initial batch a brief window so it can be separated from the new session. // Give that initial batch a brief window so it can be separated from the new session.
await new Promise<void>((resolve) => window.setTimeout(resolve, 250)); await new Promise<void>((resolve) => window.setTimeout(resolve, 250));
if (version !== connectionVersion) return;
historyBoundarySeq.value = messages.value.reduce((max, message) => Math.max(max, message.seq), 0); historyBoundarySeq.value = messages.value.reduce((max, message) => Math.max(max, message.seq), 0);
sessionStartedAt.value = new Date().toISOString(); sessionStartedAt.value = new Date().toISOString();
} catch (err) { } catch (err) {
if (version !== connectionVersion) return;
chat?.disconnect(); chat?.disconnect();
chat = null; chat = null;
chatError.value = errorText(err); chatError.value = errorText(err);
} finally { } finally {
chatLoading.value = false; if (version === connectionVersion) chatLoading.value = false;
} }
} }
async function sendMessage() { async function sendMessage() {
if (!chat || sending.value || !draft.value.trim()) return; if (!chat || sending.value || !draft.value.trim()) return;
const activeChat = chat;
const version = connectionVersion;
const value = draft.value; const value = draft.value;
draft.value = ''; draft.value = '';
toolsOpen.value = false; toolsOpen.value = false;
emojiOpen.value = false; emojiOpen.value = false;
sending.value = true; sending.value = true;
try { try {
await chat.send(value); await activeChat.send(value);
if (version !== connectionVersion) return;
await scrollToBottom(); await scrollToBottom();
} catch (err) { } catch (err) {
if (version !== connectionVersion) return;
draft.value = value; draft.value = value;
chatError.value = errorText(err); chatError.value = errorText(err);
} finally { } finally {
sending.value = false; if (version === connectionVersion) sending.value = false;
} }
} }
async function sendMedia(type: 'image' | 'video', event: Event) { async function sendMedia(type: 'image' | 'video', event: Event) {
@@ -294,6 +319,8 @@
const file = input.files?.[0]; const file = input.files?.[0];
input.value = ''; input.value = '';
if (!file || !chat || mediaSending.value) return; if (!file || !chat || mediaSending.value) return;
const activeChat = chat;
const version = connectionVersion;
const maxSize = type === 'image' ? 10 * 1024 * 1024 : 50 * 1024 * 1024; const maxSize = type === 'image' ? 10 * 1024 * 1024 : 50 * 1024 * 1024;
if (!file.type.startsWith(`${type}/`)) { if (!file.type.startsWith(`${type}/`)) {
window.$message?.error(type === 'image' ? '请选择照片文件' : '请选择视频文件'); window.$message?.error(type === 'image' ? '请选择照片文件' : '请选择视频文件');
@@ -308,16 +335,21 @@
mediaSending.value = true; mediaSending.value = true;
uploadProgress.value = 0; uploadProgress.value = 0;
try { try {
await chat.sendMedia(file, type, (progress) => { await activeChat.sendMedia(file, type, (progress) => {
if (version !== connectionVersion) return;
uploadProgress.value = Math.min(99, Math.round(progress * 100)); uploadProgress.value = Math.min(99, Math.round(progress * 100));
}); });
if (version !== connectionVersion) return;
uploadProgress.value = 100; uploadProgress.value = 100;
await scrollToBottom(); await scrollToBottom();
} catch (err) { } catch (err) {
if (version !== connectionVersion) return;
window.$message?.error(errorText(err)); window.$message?.error(errorText(err));
} finally { } finally {
mediaSending.value = false; if (version === connectionVersion) {
uploadProgress.value = 0; mediaSending.value = false;
uploadProgress.value = 0;
}
} }
} }
function toggleTools() { function toggleTools() {
@@ -346,6 +378,10 @@
hour12: false, hour12: false,
}).format(new Date(value)); }).format(new Date(value));
} }
watch(() => route.params.id, (serviceID) => {
if (serviceID) void connectChat();
else disconnectChat();
}, { immediate: true, flush: 'sync' });
onMounted(load); onMounted(load);
onBeforeUnmount(() => chat?.disconnect()); onBeforeUnmount(disconnectChat);
</script> </script>