feat(ui): desktop piggy/notifications views, support chat fixes, gift card list
This commit is contained in:
@@ -174,9 +174,9 @@
|
||||
<div class="support-composer__row flex items-end gap-2">
|
||||
<BasicButton
|
||||
type="primary"
|
||||
:disabled="disabled || isRecording || (!modelValue.trim() && attachments.length === 0)"
|
||||
html-type="button"
|
||||
:disabled-b="!canSend || !!disabled || isRecording"
|
||||
class="support-send-btn !h-12 !w-12 shrink-0 !p-0"
|
||||
@mousedown.prevent
|
||||
@click="submit"
|
||||
>
|
||||
<BasicIcon icon="vuesax-linear:direct-right" size="20" />
|
||||
@@ -197,9 +197,9 @@
|
||||
|
||||
<BasicButton
|
||||
class="support-tool-btn flex !h-10 !w-10 shrink-0 items-center justify-center rounded-xl"
|
||||
:disabled="disabled || isRecording"
|
||||
:disabled-b="!!disabled || isRecording"
|
||||
html-type="button"
|
||||
type="link"
|
||||
@mousedown.prevent
|
||||
@click="openFilePicker"
|
||||
>
|
||||
<BasicIcon icon="vuesax-linear:gallery" size="20" />
|
||||
@@ -259,6 +259,10 @@ let recordPlugin: ReturnType<typeof RecordPlugin.create> | null = null
|
||||
|
||||
const createId = () => `${Date.now()}-${Math.floor(Math.random() * 100000)}`
|
||||
|
||||
const canSend = computed(
|
||||
() => props.modelValue.trim().length > 0 || attachments.value.length > 0,
|
||||
)
|
||||
|
||||
const getReplySenderLabel = (sender: string) => {
|
||||
if (sender === 'user') return 'خودتان'
|
||||
if (sender === 'support') return 'پشتیبانی'
|
||||
@@ -535,7 +539,7 @@ const submit = async () => {
|
||||
replyTo: props.replyingTo ?? null
|
||||
})
|
||||
|
||||
emit('update:modelValue', '')
|
||||
// متن را والد بعد از موفقیت پاک میکند؛ پیوستها را همینجا خالی میکنیم
|
||||
clearComposerAttachments()
|
||||
|
||||
await nextTick()
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
<template>
|
||||
<header class="support-header">
|
||||
<div class="support-header__inner">
|
||||
<div class="bg-glass rounded-full p-2" @click="$router.back()">
|
||||
<button
|
||||
type="button"
|
||||
class="support-header__back bg-glass rounded-full p-2"
|
||||
aria-label="بازگشت"
|
||||
@click="goBack"
|
||||
>
|
||||
<BasicIcon icon="vuesax-linear:arrow-right" />
|
||||
</div>
|
||||
</button>
|
||||
<div class="support-header__icon">
|
||||
<img
|
||||
v-if="logoUrl"
|
||||
@@ -37,23 +42,34 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import { useConfigStore } from '@/store/configStore'
|
||||
import { getProjectSplashLogo } from '@/utils/config'
|
||||
|
||||
defineProps<{
|
||||
title: string
|
||||
subtitle?: string
|
||||
}>()
|
||||
|
||||
const config = useConfigStore()
|
||||
const logoUrl = computed(() => String(config.config?.avatar || '').trim())
|
||||
const router = useRouter()
|
||||
const logoUrl = computed(() => getProjectSplashLogo())
|
||||
|
||||
function goBack() {
|
||||
const back = (window.history.state as { back?: string | null } | null)?.back
|
||||
if (typeof back === 'string' && back.length > 0) {
|
||||
void router.push(back)
|
||||
return
|
||||
}
|
||||
// ورود معمول از تنظیمات/پروفایل است
|
||||
void router.replace({ name: 'user' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.support-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
z-index: 20;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--colorPrimary, #2563eb) 10%, transparent);
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
@@ -74,6 +90,20 @@ const logoUrl = computed(() => String(config.config?.avatar || '').trim())
|
||||
padding: 12px 14px;
|
||||
}
|
||||
|
||||
.support-header__back {
|
||||
flex: 0 0 auto;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: inherit;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
z-index: 21;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.support-header__icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -95,7 +125,6 @@ const logoUrl = computed(() => String(config.config?.avatar || '').trim())
|
||||
object-fit: contain;
|
||||
object-position: center;
|
||||
padding: 6px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.support-header__icon-svg {
|
||||
|
||||
@@ -272,8 +272,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BasicButton
|
||||
type="primary"
|
||||
<button
|
||||
type="button"
|
||||
class="support-reply-button"
|
||||
:class="
|
||||
message.sender === 'user'
|
||||
@@ -281,14 +281,12 @@
|
||||
: 'support-reply-button--support'
|
||||
"
|
||||
title="پاسخ"
|
||||
aria-label="پاسخ"
|
||||
@pointerdown.stop
|
||||
@pointerup.stop
|
||||
@pointercancel.stop
|
||||
@pointermove.stop
|
||||
@click.stop="emit('reply', message)"
|
||||
@click.stop.prevent="emit('reply', message)"
|
||||
>
|
||||
<BasicIcon icon="vuesax-linear:undo" size="16" />
|
||||
</BasicButton>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -336,7 +334,6 @@
|
||||
import { computed, nextTick, ref, watch, onBeforeUnmount } from 'vue'
|
||||
import type { ChatMessage } from '@/models/support'
|
||||
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
|
||||
import BasicButton from '@/components/BasicPacks/BasicButton.vue'
|
||||
import { useConfigStore } from '@/store/configStore'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -1210,16 +1207,17 @@ defineExpose({
|
||||
padding: 0 !important;
|
||||
border: 1px solid color-mix(in srgb, var(--colorPrimary, #2563eb) 18%, transparent) !important;
|
||||
border-radius: 999px !important;
|
||||
background: color-mix(in srgb, var(--bg, #fff) 92%, transparent) !important;
|
||||
background: #fff !important;
|
||||
color: var(--colorPrimary, #2563eb) !important;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 8px 18px color-mix(in srgb, var(--colorPrimary, #2563eb) 10%, transparent);
|
||||
backdrop-filter: blur(8px);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transform: scale(0.92);
|
||||
cursor: pointer;
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transform: scale(1);
|
||||
transition:
|
||||
opacity 0.15s ease,
|
||||
transform 0.15s ease;
|
||||
@@ -1229,19 +1227,13 @@ defineExpose({
|
||||
display: block;
|
||||
}
|
||||
|
||||
.support-row:hover .support-reply-button,
|
||||
.support-row:focus-within .support-reply-button {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.support-reply-button--user {
|
||||
order: 0;
|
||||
}
|
||||
|
||||
.support-reply-button--support {
|
||||
order: -1;
|
||||
background: #fff !important;
|
||||
}
|
||||
|
||||
.support-avatar {
|
||||
|
||||
+203
-34
@@ -1,28 +1,35 @@
|
||||
<template>
|
||||
<div class="support-page main-with-bottom-chrome-inner--contained">
|
||||
<div class="support-shell">
|
||||
<SupportChatHeader title="پشتیبانی" />
|
||||
<div
|
||||
class="support-page main-with-bottom-chrome-inner--contained"
|
||||
:class="{ 'support-page--desktop': isDesktop }"
|
||||
>
|
||||
<DesktopSidebar v-if="isDesktop" class="support-page__sidebar" />
|
||||
|
||||
<SupportChatMessages
|
||||
ref="messagesRef"
|
||||
:messages="messages"
|
||||
:support-typing="supportTyping"
|
||||
:has-more="hasMoreMessages"
|
||||
:loading-older="isFetchingOlderMessages"
|
||||
:loading="chatLoading"
|
||||
:loading-label="loadingLabel"
|
||||
@load-older="loadOlderMessages"
|
||||
@reply="handleReply"
|
||||
@bottom-state-change="setSupportChatAtBottom"
|
||||
/>
|
||||
<div class="support-page__main">
|
||||
<div class="support-shell">
|
||||
<SupportChatHeader title="پشتیبانی" />
|
||||
|
||||
<SupportChatComposer
|
||||
v-model="draft"
|
||||
:disabled="isSending"
|
||||
:replying-to="replyingTo"
|
||||
@cancel-reply="clearReply"
|
||||
@send="sendMessage"
|
||||
/>
|
||||
<SupportChatMessages
|
||||
ref="messagesRef"
|
||||
:messages="messages"
|
||||
:support-typing="supportTyping"
|
||||
:has-more="hasMoreMessages"
|
||||
:loading-older="isFetchingOlderMessages"
|
||||
:loading="chatLoading"
|
||||
:loading-label="loadingLabel"
|
||||
@load-older="loadOlderMessages"
|
||||
@reply="handleReply"
|
||||
@bottom-state-change="setSupportChatAtBottom"
|
||||
/>
|
||||
|
||||
<SupportChatComposer
|
||||
v-model="draft"
|
||||
:disabled="isSending"
|
||||
:replying-to="replyingTo"
|
||||
@cancel-reply="clearReply"
|
||||
@send="sendMessage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -32,6 +39,8 @@ import { computed, nextTick, onMounted, onBeforeUnmount, ref } from 'vue'
|
||||
import SupportChatHeader from './components/SupportChatHeader.vue'
|
||||
import SupportChatMessages from './components/SupportChatMessages.vue'
|
||||
import SupportChatComposer from './components/SupportChatComposer.vue'
|
||||
import DesktopSidebar from '@/layouts/desktop/DesktopSidebar.vue'
|
||||
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
|
||||
import type {
|
||||
ChatAttachment,
|
||||
ChatMessage,
|
||||
@@ -55,7 +64,9 @@ import {
|
||||
canRequestBrowserNotification,
|
||||
requestBrowserNotificationPermission,
|
||||
} from '@/utils/browserNotification'
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
const isDesktop = useDesktopBreakpoint()
|
||||
const draft = ref('')
|
||||
const isSending = ref(false)
|
||||
const supportTyping = ref(false)
|
||||
@@ -382,10 +393,12 @@ const toReplyPreview = (message: ChatMessage): ChatReplyPreview => {
|
||||
|
||||
const handleReply = async (message: ChatMessage) => {
|
||||
replyingTo.value = toReplyPreview(message)
|
||||
|
||||
console.log('replying to:', replyingTo.value)
|
||||
|
||||
await nextTick()
|
||||
// فوکوس روی کادر نوشتن تا پیشنمایش پاسخ دیده شود
|
||||
const textarea = document.querySelector(
|
||||
'.support-composer textarea',
|
||||
) as HTMLTextAreaElement | null
|
||||
textarea?.focus()
|
||||
}
|
||||
|
||||
const clearReply = () => {
|
||||
@@ -512,6 +525,7 @@ const messages = computed<ChatMessage[]>(() => {
|
||||
|
||||
onMounted(async () => {
|
||||
setSupportChatPageOpen(true)
|
||||
document.documentElement.classList.add('support-page-active')
|
||||
|
||||
if (canRequestBrowserNotification()) {
|
||||
void requestBrowserNotificationPermission()
|
||||
@@ -537,6 +551,7 @@ onMounted(async () => {
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
setSupportChatPageOpen(false)
|
||||
document.documentElement.classList.remove('support-page-active')
|
||||
})
|
||||
|
||||
const loadOlderMessages = async () => {
|
||||
@@ -662,13 +677,15 @@ const sendMessage = async (payload: ComposerSendPayload) => {
|
||||
}
|
||||
|
||||
clearReply()
|
||||
draft.value = ''
|
||||
|
||||
await nextTick()
|
||||
messagesRef.value?.scrollToBottom(true)
|
||||
} catch (error) {
|
||||
console.error('send message failed:', error)
|
||||
draft.value = text
|
||||
message.error('ارسال پیام انجام نشد. اتصال پشتیبانی را بررسی کنید.')
|
||||
} finally {
|
||||
draft.value = ''
|
||||
isSending.value = false
|
||||
}
|
||||
}
|
||||
@@ -676,14 +693,30 @@ const sendMessage = async (payload: ComposerSendPayload) => {
|
||||
|
||||
<style scoped lang="less">
|
||||
.support-page {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
font-family: var(--font-family, iran-sans), serif;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.support-page__main {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.support-shell {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
@@ -711,16 +744,152 @@ const sendMessage = async (payload: ComposerSendPayload) => {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.support-page {
|
||||
height: calc(100dvh - 4.5rem);
|
||||
padding: 12px 16px;
|
||||
.support-page__sidebar {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
/* زیر آستانه دسکتاپ: تمامعرض، بدون منو */
|
||||
@media screen and (max-width: 767.98px) {
|
||||
html.support-page-active {
|
||||
body {
|
||||
display: block !important;
|
||||
justify-content: unset !important;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
}
|
||||
|
||||
.desktopConfig,
|
||||
.app-main,
|
||||
.mobile-app-layout,
|
||||
.main-with-bottom-chrome,
|
||||
.main-with-bottom-chrome-inner,
|
||||
.support-page {
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.wrap-fix,
|
||||
.bottom-appbar,
|
||||
.app-credit-footer--mobile {
|
||||
width: 100% !important;
|
||||
max-width: 100vw !important;
|
||||
left: 0 !important;
|
||||
right: 0 !important;
|
||||
margin: 0 !important;
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.support-shell {
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgb(var(--colorPrimaryRGB, 37 99 235) / 0.12);
|
||||
box-shadow: 0 16px 48px rgb(15 23 42 / 0.06);
|
||||
.support-page,
|
||||
.support-page.support-page--desktop {
|
||||
flex-direction: column !important;
|
||||
align-items: stretch !important;
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
height: 100% !important;
|
||||
max-height: none !important;
|
||||
min-height: 0 !important;
|
||||
background: transparent !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.support-page .support-page__sidebar,
|
||||
.support-page--desktop .support-page__sidebar,
|
||||
.support-page__sidebar.desktop-sidebar {
|
||||
display: none !important;
|
||||
width: 0 !important;
|
||||
min-width: 0 !important;
|
||||
overflow: hidden !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
.support-page .support-page__main,
|
||||
.support-page--desktop .support-page__main {
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
flex: 1 1 auto !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.support-page .support-shell,
|
||||
.support-page--desktop .support-shell {
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
border-radius: 0 !important;
|
||||
border: 0 !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* از آستانه دسکتاپ: منو + چت پرکننده باقیمانده */
|
||||
@media screen and (min-width: 768px) {
|
||||
.support-page.support-page--desktop {
|
||||
flex-direction: row !important;
|
||||
align-items: stretch !important;
|
||||
gap: 0 !important;
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
background: #eef1f5 !important;
|
||||
overflow: hidden !important;
|
||||
height: 100vh !important;
|
||||
max-height: 100vh !important;
|
||||
min-height: 100vh !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.support-page.support-page--desktop .support-page__sidebar,
|
||||
.support-page.support-page--desktop > .support-page__sidebar.desktop-sidebar {
|
||||
display: flex !important;
|
||||
flex: 0 0 var(--app-desktop-sidebar-width, 260px) !important;
|
||||
width: var(--app-desktop-sidebar-width, 260px) !important;
|
||||
min-width: var(--app-desktop-sidebar-width, 260px) !important;
|
||||
max-width: var(--app-desktop-sidebar-width, 260px) !important;
|
||||
position: relative !important;
|
||||
height: 100vh !important;
|
||||
max-height: 100vh !important;
|
||||
align-self: stretch !important;
|
||||
z-index: 1 !important;
|
||||
pointer-events: auto !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
.support-page.support-page--desktop .support-page__main {
|
||||
flex: 1 1 0 !important;
|
||||
width: 0 !important;
|
||||
min-width: 0 !important;
|
||||
max-width: none !important;
|
||||
height: 100% !important;
|
||||
padding: 20px 24px 24px !important;
|
||||
box-sizing: border-box !important;
|
||||
position: relative !important;
|
||||
z-index: 2 !important;
|
||||
}
|
||||
|
||||
.support-page.support-page--desktop .support-shell {
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
height: 100% !important;
|
||||
border-radius: 28px !important;
|
||||
border: 1px solid rgba(15, 23, 42, 0.08) !important;
|
||||
box-shadow:
|
||||
0 1px 2px rgba(15, 23, 42, 0.04),
|
||||
0 18px 48px rgba(15, 23, 42, 0.08) !important;
|
||||
}
|
||||
|
||||
.support-page.desktop-app-layout__content.support-page--desktop {
|
||||
overflow: hidden !important;
|
||||
min-height: 0 !important;
|
||||
}
|
||||
|
||||
body:has(.support-page--desktop) .app-credit-footer--desktop-float {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user