Port modular app notifications, pargar date display, and closed-store product UX.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-25 16:29:33 +02:00
co-authored by Cursor
parent 4fd12277e7
commit e6dea15752
33 changed files with 1253 additions and 47 deletions
@@ -0,0 +1,96 @@
<template>
<article
class="app-notif-card"
:class="[
`app-notif-card--${item.severity}`,
{ 'app-notif-card--unread': !item.is_read },
{ 'app-notif-card--compact': compact },
]"
@click="$emit('open', item.id)"
>
<div class="flex items-start justify-between gap-2">
<div class="flex min-w-0 flex-1 items-center gap-2">
<h3 class="app-notif-card__title m-0 font-bold text-gray-900">{{ item.title }}</h3>
<span v-if="!item.is_read" class="app-notif-card__new">جدید</span>
</div>
<span class="app-notif-card__badge">{{ severityLabel(item.severity) }}</span>
</div>
<p class="app-notif-card__body app-notif-card__body--preview m-0 mt-2 text-sm text-gray-600">
{{ previewText(item.body) }}
</p>
<div class="app-notif-card__footer">
<span class="app-notif-card__more">مشاهده کامل</span>
<BasicIcon icon="vuesax-linear:arrow-left-1" size="16" class="text-gray-500" />
</div>
</article>
</template>
<script setup lang="ts">
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import type { CustomerAppNotification } from '@/api/appNotifications'
import { previewText, severityLabel } from '../composables/notificationDisplay'
withDefaults(
defineProps<{
item: CustomerAppNotification
compact?: boolean
}>(),
{ compact: false },
)
defineEmits<{
open: [id: number]
}>()
</script>
<style scoped>
.app-notif-card {
@apply rounded-xl bg-white p-3 border border-gray-100 cursor-pointer transition-shadow;
}
.app-notif-card--compact {
@apply p-4;
}
.app-notif-card--unread {
border-color: #ef4444;
box-shadow: 0 0 0 1px rgba(239, 68, 68, 0.25);
}
.app-notif-card__title {
@apply text-sm;
}
.app-notif-card--compact .app-notif-card__title {
@apply text-base;
}
.app-notif-card__new {
@apply shrink-0 rounded-md bg-red-50 px-1.5 py-0.5 text-[10px] font-bold text-red-600;
}
.app-notif-card__badge {
@apply shrink-0 rounded-md px-2 py-0.5 text-xs font-bold;
}
.app-notif-card__body--preview {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
white-space: normal;
}
.app-notif-card__footer {
@apply mt-3 flex items-center justify-between gap-2 border-0 border-t border-solid border-gray-100 pt-2;
}
.app-notif-card__more {
@apply text-xs font-bold text-gray-600;
}
.app-notif-card--info .app-notif-card__badge {
@apply bg-blue-50 text-blue-700;
}
.app-notif-card--success .app-notif-card__badge {
@apply bg-green-50 text-green-700;
}
.app-notif-card--warning .app-notif-card__badge {
@apply bg-orange-50 text-orange-700;
}
.app-notif-card--danger .app-notif-card__badge {
@apply bg-red-50 text-red-700;
}
</style>
@@ -0,0 +1,132 @@
<template>
<div v-if="visible" class="notif-perm">
<div class="notif-perm__text">
<p class="notif-perm__title">{{ title }}</p>
<p class="notif-perm__desc">{{ description }}</p>
</div>
<button
v-if="canEnable"
type="button"
class="notif-perm__btn"
:disabled="loading"
@click="onEnable"
>
{{ loading ? '…' : 'فعال‌سازی' }}
</button>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import {
getBrowserNotificationState,
requestBrowserNotificationPermission,
type BrowserNotifState,
} from '@/utils/browserNotification'
const emit = defineEmits<{
changed: [state: BrowserNotifState]
}>()
const state = ref<BrowserNotifState>('unsupported')
const loading = ref(false)
const dismissed = ref(false)
const visible = computed(() => {
if (dismissed.value) return false
return state.value === 'default' || state.value === 'needs-pwa' || state.value === 'denied'
})
const canEnable = computed(() => state.value === 'default')
const title = computed(() => {
if (state.value === 'needs-pwa') return 'اعلان سیستم در دسترس نیست'
if (state.value === 'denied') return 'اعلان سیستم غیرفعال است'
return 'اعلان‌های فوری'
})
const description = computed(() => {
if (state.value === 'needs-pwa') {
return 'برای دریافت نوتیف روی آیفون، اپ را به صفحه اصلی اضافه کنید و از همان‌جا باز کنید.'
}
if (state.value === 'denied') {
return 'مجوز اعلان در تنظیمات مرورگر مسدود است. از قفل کنار آدرس → Site settings → Notifications اجازه بدهید.'
}
return 'روی «فعال‌سازی» بزنید تا مرورگر اجازه نوتیف را بپرسد.'
})
function refreshState() {
state.value = getBrowserNotificationState()
emit('changed', state.value)
}
async function onEnable() {
loading.value = true
try {
const next = await requestBrowserNotificationPermission()
state.value = next
emit('changed', next)
if (next === 'granted') {
dismissed.value = true
}
} finally {
loading.value = false
}
}
onMounted(() => {
refreshState()
})
defineExpose({ refreshState })
</script>
<style scoped>
.notif-perm {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 0.9rem;
border-radius: 0.85rem;
background: #fff;
border: 1px solid rgba(15, 23, 42, 0.08);
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
}
.notif-perm__text {
flex: 1 1 auto;
min-width: 0;
}
.notif-perm__title {
margin: 0;
font-size: 0.8125rem;
font-weight: 700;
color: #0f172a;
}
.notif-perm__desc {
margin: 0.2rem 0 0;
font-size: 0.72rem;
line-height: 1.45;
color: #64748b;
}
.notif-perm__btn {
flex-shrink: 0;
border: none;
border-radius: 999px;
padding: 0.45rem 0.85rem;
font: inherit;
font-size: 0.75rem;
font-weight: 700;
color: #fff;
background: var(--App-primary, #0b5fff);
cursor: pointer;
}
.notif-perm__btn:disabled {
opacity: 0.6;
cursor: default;
}
</style>
@@ -0,0 +1,24 @@
const PREVIEW_MAX = 90
export function severityLabel(severity: string) {
switch (severity) {
case 'success':
return 'موفق'
case 'warning':
return 'هشدار'
case 'danger':
return 'مهم'
default:
return 'اطلاعات'
}
}
export function previewText(body: string, max = PREVIEW_MAX): string {
const text = String(body ?? '').replace(/\s+/g, ' ').trim()
if (text.length <= max) return text
return `${text.slice(0, max).trim()}…`
}
export function needsPreview(body: string, max = PREVIEW_MAX): boolean {
return String(body ?? '').replace(/\s+/g, ' ').trim().length > max
}
@@ -0,0 +1,120 @@
import { computed, inject, onMounted, provide, ref, type InjectionKey } from 'vue'
import {
getAppNotifications,
markAllAppNotificationsRead,
markAppNotificationRead,
type CustomerAppNotification,
} from '@/api/appNotifications'
import { useSettingsStore } from '@/store/settingsStore'
export interface AppNotificationsPageContext {
loading: ReturnType<typeof ref<boolean>>
items: ReturnType<typeof ref<CustomerAppNotification[]>>
detailVisible: ReturnType<typeof ref<boolean>>
selectedItem: ReturnType<typeof ref<CustomerAppNotification | null>>
enabled: ReturnType<typeof computed<boolean>>
refresh: () => Promise<void>
openItem: (id: number) => Promise<void>
closeDetail: () => void
markRead: (id: number) => Promise<void>
markAllRead: () => Promise<void>
}
export const APP_NOTIFICATIONS_PAGE_KEY: InjectionKey<AppNotificationsPageContext> =
Symbol('appNotificationsPage')
export function useAppNotificationsPage() {
const settingsStore = useSettingsStore()
const loading = ref(false)
const items = ref<CustomerAppNotification[]>([])
const detailVisible = ref(false)
const selectedItem = ref<CustomerAppNotification | null>(null)
const enabled = computed(() => settingsStore.isSettingTruthy('has_page_notification'))
async function refresh() {
if (!enabled.value) {
items.value = []
return
}
loading.value = true
try {
const data = await getAppNotifications()
items.value = Array.isArray(data) ? data : []
} catch (e) {
console.error(e)
items.value = []
} finally {
loading.value = false
}
}
async function markRead(id: number) {
const row = items.value.find((item) => item.id === id)
if (!row || row.is_read) return
try {
await markAppNotificationRead(id)
row.is_read = true
if (selectedItem.value?.id === id) {
selectedItem.value = { ...selectedItem.value, is_read: true }
}
const { refreshUnreadCount } = await import('@/composables/useAppNotificationBadge')
void refreshUnreadCount()
} catch (e) {
console.error(e)
}
}
/** باز کردن جزئیات در مدال/اکشن‌شیت + سین */
async function openItem(id: number) {
const row = items.value.find((item) => item.id === id)
if (!row) return
selectedItem.value = row
detailVisible.value = true
await markRead(id)
}
function closeDetail() {
detailVisible.value = false
selectedItem.value = null
}
async function markAllRead() {
try {
await markAllAppNotificationsRead()
items.value = items.value.map((item) => ({ ...item, is_read: true }))
const { clearUnreadLocally } = await import('@/composables/useAppNotificationBadge')
clearUnreadLocally()
} catch (e) {
console.error(e)
}
}
onMounted(() => {
void refresh()
})
const ctx: AppNotificationsPageContext = {
loading,
items,
detailVisible,
selectedItem,
enabled,
refresh,
openItem,
closeDetail,
markRead,
markAllRead,
}
provide(APP_NOTIFICATIONS_PAGE_KEY, ctx)
return ctx
}
export function useAppNotificationsPageContext() {
const ctx = inject(APP_NOTIFICATIONS_PAGE_KEY)
if (!ctx) {
throw new Error('useAppNotificationsPageContext must be used within useAppNotificationsPage')
}
return ctx
}
@@ -0,0 +1,55 @@
<template>
<div class="dapp-notif">
<header class="dapp-notif__top">
<div>
<p class="dapp-notif__eyebrow">اعلان‌ها</p>
<h1 class="dapp-notif__title">پیام‌های مدیریت</h1>
</div>
</header>
<BrowserNotificationPermissionBanner class="mb-4 max-w-3xl" />
<a-spin :spinning="loading">
<div v-if="!items.length" class="dapp-notif__empty">
<BasicIcon icon="vuesax-linear:notification-bing" size="36" />
<p>اعلانی برای نمایش نیست.</p>
</div>
<div v-else class="dapp-notif__list">
<AppNotificationCard
v-for="item in items"
:key="item.id"
:item="item"
compact
@open="openItem"
/>
</div>
</a-spin>
</div>
</template>
<script setup lang="ts">
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import AppNotificationCard from '../components/AppNotificationCard.vue'
import BrowserNotificationPermissionBanner from '../components/BrowserNotificationPermissionBanner.vue'
import { useAppNotificationsPageContext } from '../composables/useAppNotificationsPage'
const { loading, items, openItem } = useAppNotificationsPageContext()
</script>
<style scoped>
.dapp-notif {
@apply p-6 min-h-full;
}
.dapp-notif__eyebrow {
@apply m-0 text-xs text-gray-500;
}
.dapp-notif__title {
@apply m-0 mt-1 text-xl font-bold;
}
.dapp-notif__empty {
@apply mt-10 flex flex-col items-center gap-2 text-gray-500;
}
.dapp-notif__list {
@apply mt-6 flex flex-col gap-3 max-w-3xl;
}
</style>
+73
View File
@@ -0,0 +1,73 @@
<template>
<div class="app-notifications-page-root">
<DesktopAppNotificationsView v-show="isDesktop" />
<MobileAppNotificationsView v-show="!isDesktop" />
<BasicActionSheet
v-model:show="detailVisible"
:title="selectedItem?.title || 'جزئیات اعلان'"
width="32rem"
:show-button="true"
:show-cancel="false"
:show-ok="true"
ok-text="بستن"
@ok="closeDetail"
@close="closeDetail"
@cancel="closeDetail"
>
<div v-if="selectedItem" class="app-notif-detail">
<div class="app-notif-detail__meta">
<span
class="app-notif-detail__badge"
:class="`app-notif-detail__badge--${selectedItem.severity}`"
>
{{ severityLabel(selectedItem.severity) }}
</span>
</div>
<p class="app-notif-detail__body">{{ selectedItem.body }}</p>
</div>
</BasicActionSheet>
</div>
</template>
<script lang="ts" setup>
import BasicActionSheet from '@/components/BasicPacks/BasicActionSheet.vue'
import { useDesktopBreakpoint } from '@/layouts/composables/useAppLayout'
import { useAppNotificationsPage } from './composables/useAppNotificationsPage'
import { severityLabel } from './composables/notificationDisplay'
import DesktopAppNotificationsView from './desktop/DesktopAppNotificationsView.vue'
import MobileAppNotificationsView from './mobile/MobileAppNotificationsView.vue'
const isDesktop = useDesktopBreakpoint()
const { detailVisible, selectedItem, closeDetail } = useAppNotificationsPage()
</script>
<style scoped>
.app-notifications-page-root {
min-height: 100%;
height: 100%;
}
.app-notif-detail__meta {
@apply mb-3 flex items-center;
}
.app-notif-detail__badge {
@apply rounded-md px-2 py-0.5 text-xs font-bold;
}
.app-notif-detail__badge--info {
@apply bg-blue-50 text-blue-700;
}
.app-notif-detail__badge--success {
@apply bg-green-50 text-green-700;
}
.app-notif-detail__badge--warning {
@apply bg-orange-50 text-orange-700;
}
.app-notif-detail__badge--danger {
@apply bg-red-50 text-red-700;
}
.app-notif-detail__body {
@apply m-0 whitespace-pre-wrap text-sm leading-7 text-gray-700;
max-height: min(60vh, 28rem);
overflow-y: auto;
}
</style>
@@ -0,0 +1,45 @@
<template>
<div class="app-notif-page">
<MobilePageHeader title="اعلان‌ها" show-back />
<div class="app-notif-page__body mpage-content">
<BrowserNotificationPermissionBanner class="mb-3" />
<a-spin :spinning="loading">
<div v-if="!items.length" class="app-notif-page__empty">
<BasicIcon icon="vuesax-linear:notification-bing" size="40" class="text-gray-400" />
<p class="m-0 mt-3 font-bold text-gray-700">اعلانی نیست</p>
<p class="m-0 mt-1 text-sm text-gray-500">هنوز اعلانی از طرف مدیریت ارسال نشده است.</p>
</div>
<div v-else class="flex flex-col gap-3">
<AppNotificationCard
v-for="item in items"
:key="item.id"
:item="item"
@open="openItem"
/>
</div>
</a-spin>
</div>
</div>
</template>
<script setup lang="ts">
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import MobilePageHeader from '@/layouts/components/MobilePageHeader.vue'
import AppNotificationCard from '../components/AppNotificationCard.vue'
import BrowserNotificationPermissionBanner from '../components/BrowserNotificationPermissionBanner.vue'
import { useAppNotificationsPageContext } from '../composables/useAppNotificationsPage'
const { loading, items, openItem } = useAppNotificationsPageContext()
</script>
<style scoped>
.app-notif-page {
@apply min-h-full flex flex-col bg-gray-50;
}
.app-notif-page__body {
@apply px-4 pb-8;
}
.app-notif-page__empty {
@apply flex flex-col items-center justify-center rounded-xl bg-white p-8 text-center;
}
</style>
+3 -7
View File
@@ -10,7 +10,7 @@
<div class="w-full mr-3">
<div class="flex justify-between w-full">
<div>{{ item.title }}</div>
<div>{{ toJalaliNotime(item.expire_at) }}</div>
<div>{{ toJalali(item.expire_at) }}</div>
</div>
<div class="flex justify-between mt-2">
<div :class="item.gold >= 0? 'text-green-600':'text-red-600'" class="text-gray-500 text-xs number-ltr">
@@ -69,11 +69,7 @@
</div>
<div class="flex justify-between mb-3">
<div class="text-primary">تاریخ:</div>
<div>{{ toJalaliNotime(item.expire_at) }}</div>
</div>
<div class="flex justify-between mb-3">
<div class="text-primary">زمان:</div>
<div>{{ toTime(item.expire_at) }}</div>
<div>{{ toJalali(item.expire_at) }}</div>
</div>
<BasicButton class="w-full mt-5" @click="infoVisible = false">بازگشت</BasicButton>
</div>
@@ -89,7 +85,7 @@ import {computed, ref} from "vue";
import BasicButton from "@/components/BasicPacks/BasicButton.vue";
import type {Transaction} from "@/models/transaction";
import {formatGoldAmount, usePriceWithoutType} from "../../../../../utils/usePrice";
import {toJalaliNotime, toTime} from "../../../../../utils/useDateTime";
import {toJalali} from "../../../../../utils/useDateTime";
const infoVisible = ref<boolean>(false)
+56 -1
View File
@@ -10,6 +10,20 @@
</div>
<div class="flex items-center gap-2">
<button
v-if="notificationEnabled"
type="button"
class="mhome-header-btn mhome-header-btn--notify"
aria-label="اعلان"
@click.stop="goNotifications"
>
<span class="mhome-header-btn__icon-wrap">
<Icon icon="vuesax-linear:notification-bing" size="18" />
<span v-if="unreadCount > 0" class="mhome-header-btn__badge">
{{ unreadCount > 99 ? '99+' : unreadCount }}
</span>
</span>
</button>
<button
type="button"
class="mhome-header-btn"
@@ -36,12 +50,27 @@ import { useRoute } from 'vue-router'
import router from '@/router'
import Icon from '@/components/BasicPacks/BasicIcon.vue'
import { getProjectMobileNavLogo } from '@/utils/config'
import { useAppNotificationBadge } from '@/composables/useAppNotificationBadge'
import { canRequestBrowserNotification, requestBrowserNotificationPermission } from '@/utils/browserNotification'
const headerRef = ref<HTMLElement | null>(null)
const isHeaderScrolled = ref(false)
const route = useRoute()
const projectLogo = computed(() => getProjectMobileNavLogo())
const {
enabled: notificationEnabled,
unreadCount,
clearUnreadLocally,
} = useAppNotificationBadge()
async function goNotifications() {
clearUnreadLocally()
if (canRequestBrowserNotification()) {
await requestBrowserNotificationPermission()
}
void router.push({ name: 'appNotifications' })
}
let intersectionObserver: IntersectionObserver | null = null
let scrollTarget: HTMLElement | Window | null = null
@@ -177,7 +206,7 @@ defineEmits<{
.app-header {
z-index: 1999;
box-sizing: border-box;
padding-top: calc(0.5rem + var(--app-safe-top, env(safe-area-inset-top, 0px)));
padding-top: calc(0.5rem + var(--app-safe-top, 0px));
background-color: transparent;
}
@@ -209,4 +238,30 @@ defineEmits<{
border-color: #fecaca;
background: #fef2f2;
}
.mhome-header-btn--notify {
color: #0a1b37;
border-color: #e5e7eb;
background: #fff;
}
.mhome-header-btn__icon-wrap {
@apply relative inline-flex;
}
.mhome-header-btn__badge {
position: absolute;
top: -8px;
left: -8px;
min-width: 16px;
height: 16px;
padding: 0 4px;
border-radius: 999px;
background: #dc2626;
color: #fff;
font-size: 10px;
font-weight: 700;
line-height: 16px;
text-align: center;
}
</style>
@@ -10,6 +10,24 @@
<BasicIcon icon="vuesax-linear:user" size="16" />
<span>حساب کاربری</span>
</button>
<button
v-if="notificationEnabled"
type="button"
class="dhome-btn-ghost dhome-btn-ghost--notify"
aria-label="اعلان"
@click="goNotifications"
>
<span class="dhome-notify-wrap">
<BasicIcon icon="vuesax-linear:notification-bing" size="16" />
<span v-if="unreadCount > 0" class="dhome-notify-badge">
{{ unreadCount > 99 ? '99+' : unreadCount }}
</span>
</span>
<span>اعلان</span>
</button>
<button type="button" class="dhome-btn-ghost" aria-label="بروزرسانی" @click="reloadApp">
<BasicIcon icon="vuesax-linear:refresh-2" size="16" />
</button>
<button
v-if="showLogout"
type="button"
@@ -26,6 +44,8 @@
<script lang="ts" setup>
import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import { useUserStore } from '@/store/userStore'
import { useAppNotificationBadge } from '@/composables/useAppNotificationBadge'
import { canRequestBrowserNotification, requestBrowserNotificationPermission } from '@/utils/browserNotification'
import router from '@/router'
withDefaults(
@@ -40,4 +60,43 @@ defineEmits<{
}>()
const userStore = useUserStore()
const {
enabled: notificationEnabled,
unreadCount,
clearUnreadLocally,
} = useAppNotificationBadge()
async function goNotifications() {
clearUnreadLocally()
if (canRequestBrowserNotification()) {
await requestBrowserNotificationPermission()
}
void router.push({ name: 'appNotifications' })
}
function reloadApp() {
window.location.reload()
}
</script>
<style scoped>
.dhome-notify-wrap {
position: relative;
display: inline-flex;
}
.dhome-notify-badge {
position: absolute;
top: -8px;
left: -8px;
min-width: 16px;
height: 16px;
padding: 0 4px;
border-radius: 999px;
background: #dc2626;
color: #fff;
font-size: 10px;
font-weight: 700;
line-height: 16px;
text-align: center;
}
</style>
@@ -1,6 +1,13 @@
<template>
<div class="flex bg-white border-1 border-gray-200 rounded-lg"
:class="isClickable ? 'bg-white opacity-100' : 'bg-gray-200 opacity-50 cursor-not-allowed'">
<div
class="flex bg-white border-1 border-gray-200 rounded-lg"
:class="{
'bg-white opacity-100': isClickable,
'bg-gray-200 opacity-50 cursor-not-allowed': !isClickable && !locked,
'product-item--locked': locked,
}"
@click="handleClick"
>
<div class="bg-gray-100 m-1.5 rounded-lg w-24 h-24 flex justify-center p-1.5 shrink-0">
<img :src="displayImage" class="object-contain w-full h-full" alt="">
</div>
@@ -24,20 +31,36 @@ import { getProductDisplayImage } from '@/utils/productAssets'
interface Prop {
ProductItem: Product
inactive?: boolean
/** فروشگاه بسته — خاکستری و غیرقابل کلیک (قیمت همچنان نمایش داده می‌شود) */
locked?: boolean
}
const props = withDefaults(defineProps<Prop>(), {
inactive: false,
locked: false,
})
const isClickable = computed(() => props.ProductItem.show && !props.inactive)
const emit = defineEmits<{
click: []
}>()
const isClickable = computed(() => props.ProductItem.show && !props.inactive && !props.locked)
const displayImage = computed(() => getProductDisplayImage(props.ProductItem))
function handleClick() {
if (!isClickable.value) return
emit('click')
}
</script>
<style scoped>
.product-item--locked {
filter: grayscale(0.85);
opacity: 0.55;
background: #f3f4f6;
cursor: not-allowed;
}
</style>
@@ -42,6 +42,7 @@
:key="item.id"
:product="item"
:inactive="isProductPriceInactive(item)"
:locked="storeClosed"
@click="clickProduct(item)"
/>
<div v-if="!loading && !items?.length" class="dproducts-empty">
@@ -57,7 +58,7 @@
import DesktopPageTopbar from '@/layouts/components/DesktopPageTopbar.vue'
import DesktopProductCard from './components/DesktopProductCard.vue'
import { useProductsPageContext } from '../composables/useProductsPage'
import { isMarketClosedForDisplay, STORE_CLOSED_TITLE } from '@/utils/storeHours'
import { isStoreClosed, STORE_CLOSED_TITLE } from '@/utils/storeHours'
import { computed } from 'vue'
const {
@@ -71,7 +72,7 @@ const {
storeClosedMessage,
} = useProductsPageContext()
const storeClosed = computed(() => isMarketClosedForDisplay())
const storeClosed = computed(() => isStoreClosed())
</script>
<style lang="less" src="./styles/desktop-products.less"></style>
@@ -2,7 +2,10 @@
<button
type="button"
class="dproducts-card"
:class="{ 'dproducts-card--disabled': !isClickable }"
:class="{
'dproducts-card--disabled': !isClickable,
'dproducts-card--locked': locked,
}"
:disabled="!isClickable"
@click="$emit('click')"
>
@@ -39,15 +42,17 @@ const props = withDefaults(
defineProps<{
product: Product
inactive?: boolean
/** فروشگاه بسته — خاکستری و غیرقابل کلیک */
locked?: boolean
}>(),
{ inactive: false },
{ inactive: false, locked: false },
)
defineEmits<{
click: []
}>()
const isClickable = computed(() => props.product.show && !props.inactive)
const isClickable = computed(() => props.product.show && !props.inactive && !props.locked)
const displayImage = computed(() => getProductDisplayImage(props.product))
@@ -61,7 +61,7 @@
font-family: inherit;
transition: box-shadow 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
&:hover:not(&--disabled) {
&:hover:not(&--disabled):not(&--locked) {
border-color: #e5e7eb;
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
transform: translateY(-1px);
@@ -72,6 +72,12 @@
opacity: 0.55;
}
&--locked {
filter: grayscale(0.85);
opacity: 0.55;
background: #f3f4f6;
}
&__image-wrap {
display: flex;
align-items: center;
@@ -45,6 +45,7 @@
<ProductItem
:ProductItem="item"
:inactive="isProductPriceInactive(item)"
:locked="storeClosed"
@click="clickProduct(item)"
/>
</div>
@@ -59,7 +60,7 @@ import img from '@/assets/img/user_ly1.svg'
import MobilePageHeader from '@/layouts/components/MobilePageHeader.vue'
import ProductItem from '@/views/base/products/components/ProductItem.vue'
import { useProductsPageContext } from '../composables/useProductsPage'
import { isMarketClosedForDisplay, STORE_CLOSED_TITLE } from '@/utils/storeHours'
import { isStoreClosed, STORE_CLOSED_TITLE } from '@/utils/storeHours'
import { computed } from 'vue'
const {
@@ -73,7 +74,7 @@ const {
storeClosedMessage,
} = useProductsPageContext()
const storeClosed = computed(() => isMarketClosedForDisplay())
const storeClosed = computed(() => isStoreClosed())
</script>
<style scoped>
@@ -100,11 +100,7 @@
</div>
<div :class="variant === 'desktop' ? 'dtxn-detail__row' : 'flex justify-between mb-3'">
<div :class="variant === 'desktop' ? 'dtxn-detail__label' : 'text-primary'">تاریخ:</div>
<div :class="variant === 'desktop' ? 'dtxn-detail__value' : ''">{{ toJalaliNotime(item.expire_at) }}</div>
</div>
<div :class="variant === 'desktop' ? 'dtxn-detail__row' : 'flex justify-between mb-3'">
<div :class="variant === 'desktop' ? 'dtxn-detail__label' : 'text-primary'">زمان:</div>
<div :class="variant === 'desktop' ? 'dtxn-detail__value' : ''">{{ toTime(item.expire_at) }}</div>
<div :class="variant === 'desktop' ? 'dtxn-detail__value' : ''">{{ toJalali(item.expire_at) }}</div>
</div>
<div v-if="item.eaj" :class="variant === 'desktop' ? 'dtxn-detail__row' : 'flex justify-between mb-3'">
<div :class="variant === 'desktop' ? 'dtxn-detail__label' : 'text-primary'">مهلت ارسال فیش:</div>
@@ -129,7 +125,7 @@ import {
resolveTransactionTradeProduct,
shouldShowTransactionTotalWeightLine,
} from '@/utils/productDealMethod'
import { toJalaliNotime, toTime } from '@/utils/useDateTime'
import { toJalali } from '@/utils/useDateTime'
const props = withDefaults(
defineProps<{
@@ -17,6 +17,7 @@ import { useConfigStore } from '@/store/configStore'
import { getBoursecuActionIconSrc } from '@/utils/config'
import { getReferralShareText, getReferralShareTitle, shareReferralText } from '@/utils/referralShare'
import { normalizeAuthFormFields } from '@/utils/persianDigits'
import { useSettingsStore } from '@/store/settingsStore'
export interface UserMenuAction {
id: string
@@ -64,6 +65,7 @@ export function useUserPage() {
const formRef = ref<FormInstance>()
const formPassRef = ref<FormInstance>()
const userStore = useUserStore()
const settingsStore = useSettingsStore()
const nameProject = useConfigStore().config.name_fa
const editPassVisible = ref(false)
const editPasswordVisible = ref(false)
@@ -117,7 +119,7 @@ export function useUserPage() {
label: 'هشدار قیمت',
icon: 'vuesax-linear:bill',
onClick: () => router.push({ name: 'notifyTrigger' }),
visible: true,
visible: settingsStore.isSettingTruthy('has_page_price_trigger'),
},
{
id: 'about',
+3 -7
View File
@@ -20,7 +20,7 @@
<div class="w-full mr-3">
<div class="flex justify-between w-full">
<div>{{ item.type_str }}</div>
<div>{{ toJalaliNotime(item.created_at) }}</div>
<div>{{ toJalali(item.created_at) }}</div>
</div>
<div class="flex justify-between mt-2">
<div :class="item.gold >= 0? 'text-green-600':'text-red-600'" class="text-gray-500 text-xs number-ltr">
@@ -111,11 +111,7 @@
</div>
<div class="flex justify-between mb-3">
<div class="text-black">تاریخ:</div>
<div>{{ toJalaliNotime(item.expire_at) }}</div>
</div>
<div class="flex justify-between mb-3">
<div class="text-black">زمان:</div>
<div>{{ toTime(item.expire_at) }}</div>
<div>{{ toJalali(item.expire_at) }}</div>
</div>
<!-- <div class="flex justify-between mb-3">
<div class="text-black">وضعیت معامله:</div>
@@ -144,7 +140,7 @@ import {computed, ref} from "vue";
import BasicButton from "@/components/BasicPacks/BasicButton.vue";
import type {Transaction} from "@/models/transaction";
import {getCashWalletLabel, getGoldWalletLabel, getMetalTypeTradeLabel, getTradePriceUnitLabel, getWeightUnit} from '@/utils/productDealMethod'
import {toJalaliNotime, toJalaliOutOfUTC, toTime} from "../../../../utils/useDateTime";
import {toJalali, toJalaliOutOfUTC} from "../../../../utils/useDateTime";
const infoVisible = ref<boolean>(false)