fix: isolate customer service chats and reload bank details
This commit is contained in:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user