added function for trigger in admin

This commit is contained in:
2026-07-06 19:06:57 +03:30
parent c380fc89d3
commit 6756641efd
14 changed files with 1072 additions and 442 deletions
+5
View File
@@ -21,6 +21,8 @@ declare module '@vue/runtime-core' {
ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel'] ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel']
AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider'] AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
ADivider: typeof import('ant-design-vue/es')['Divider'] ADivider: typeof import('ant-design-vue/es')['Divider']
AdminFieldHelp: typeof import('./src/components/AdminFieldHelp.vue')['default']
AdminLabelWithHelp: typeof import('./src/components/AdminLabelWithHelp.vue')['default']
ADrawer: typeof import('ant-design-vue/es')['Drawer'] ADrawer: typeof import('ant-design-vue/es')['Drawer']
ADropdown: typeof import('ant-design-vue/es')['Dropdown'] ADropdown: typeof import('ant-design-vue/es')['Dropdown']
AEmpty: typeof import('ant-design-vue/es')['Empty'] AEmpty: typeof import('ant-design-vue/es')['Empty']
@@ -50,6 +52,9 @@ declare module '@vue/runtime-core' {
ASubMenu: typeof import('ant-design-vue/es')['SubMenu'] ASubMenu: typeof import('ant-design-vue/es')['SubMenu']
ASwitch: typeof import('ant-design-vue/es')['Switch'] ASwitch: typeof import('ant-design-vue/es')['Switch']
ATable: typeof import('ant-design-vue/es')['Table'] ATable: typeof import('ant-design-vue/es')['Table']
ATabPane: typeof import('ant-design-vue/es')['TabPane']
ATabs: typeof import('ant-design-vue/es')['Tabs']
ATag: typeof import('ant-design-vue/es')['Tag']
ATextarea: typeof import('ant-design-vue/es')['Textarea'] ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATimePicker: typeof import('ant-design-vue/es')['TimePicker'] ATimePicker: typeof import('ant-design-vue/es')['TimePicker']
ATooltip: typeof import('ant-design-vue/es')['Tooltip'] ATooltip: typeof import('ant-design-vue/es')['Tooltip']
+15 -3
View File
@@ -1,6 +1,18 @@
import type {TableInput} from "@core/type/baseTable"; import type { TableInput } from '@core/type/baseTable'
import useAxios from "@core/utils/useAxios"; import useAxios from '@core/utils/useAxios'
import type {PriceTriggers} from "@/model/priceTriggers"; import type { PriceTriggers } from '@/model/priceTriggers'
export const getPriceTriggersTable = async (params: TableInput) => export const getPriceTriggersTable = async (params: TableInput) =>
await useAxios.post<Array<PriceTriggers>>('/admin/price_triggers/table', params) await useAxios.post<Array<PriceTriggers>>('/admin/price_triggers/table', params)
export const createPriceTrigger = async (payload: Partial<PriceTriggers>) =>
await useAxios.post<PriceTriggers>('/admin/price_triggers', payload)
export const updatePriceTrigger = async (id: number, payload: Partial<PriceTriggers>) =>
await useAxios.put(`/admin/price_triggers/${id}`, payload)
export const togglePriceTriggerActive = async (id: number) =>
await useAxios.put<{ is_active: boolean }>(`/admin/price_triggers/${id}/toggle`)
export const deletePriceTrigger = async (id: number) =>
await useAxios.delete(`/admin/price_triggers/${id}`)
+40 -5
View File
@@ -1,25 +1,60 @@
<template> <template>
<p v-if="text" class="admin-field-help" :class="rootClass">{{ text }}</p> <a-tooltip
v-if="text"
placement="top"
:title="text"
overlay-class-name="admin-field-help-tooltip"
:overlay-style="{ zIndex: TOOLTIP_ABOVE_MODAL_Z_INDEX }"
:overlay-inner-style="{
maxWidth: '22rem',
direction: 'rtl',
textAlign: 'right',
whiteSpace: 'pre-line',
}"
:mouse-enter-delay="0.15"
>
<span
class="admin-field-help"
:class="rootClass"
role="img"
:aria-label="text"
tabindex="0"
>
<IosInfoBadge :size="size" />
</span>
</a-tooltip>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import IosInfoBadge from '@/pages/dashboard/components/IosInfoBadge.vue'
import { TOOLTIP_ABOVE_MODAL_Z_INDEX } from '@/constants/overlayZIndex'
withDefaults( withDefaults(
defineProps<{ defineProps<{
text?: string text?: string
rootClass?: string rootClass?: string
size?: 'xs' | 'sm' | 'md'
}>(), }>(),
{ {
text: '', text: '',
rootClass: '', rootClass: '',
size: 'xs',
} }
) )
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
.admin-field-help { .admin-field-help {
margin: 0; display: inline-flex;
font-size: 11px; align-items: center;
color: var(--admin-text-secondary); line-height: 0;
line-height: 1.5; cursor: help;
vertical-align: middle;
}
</style>
<style>
.ant-tooltip.admin-field-help-tooltip {
z-index: 1250 !important;
} }
</style> </style>
+31
View File
@@ -0,0 +1,31 @@
<template>
<span
class="admin-label-with-help inline-flex max-w-full items-center gap-1.5"
:class="[textClass, rootClass]"
>
<span :class="labelClass">{{ label }}</span>
<AdminFieldHelp v-if="help" :text="help" :size="size" />
</span>
</template>
<script setup lang="ts">
import AdminFieldHelp from '@/components/AdminFieldHelp.vue'
withDefaults(
defineProps<{
label: string
help?: string
size?: 'xs' | 'sm' | 'md'
textClass?: string
labelClass?: string
rootClass?: string
}>(),
{
help: '',
size: 'xs',
textClass: 'text-sm',
labelClass: '',
rootClass: '',
}
)
</script>
@@ -0,0 +1,227 @@
<template>
<div class="banner-image-upload">
<a-upload
v-if="!imageUrl"
class="banner-image-upload__zone"
name="file"
accept="image/*"
:show-upload-list="false"
:action="uploadURL"
:headers="fileHeader"
@change="onChange"
>
<div class="banner-image-upload__empty">
<div v-if="loading" class="banner-image-upload__loading">
<LoadingOutlined />
<span>در حال آپلود...</span>
</div>
<template v-else>
<BaseIcon icon="vuesax-linear:gallery-add" size="40" class="banner-image-upload__icon" />
<span class="banner-image-upload__cta">افزودن تصویر بنر</span>
<span class="banner-image-upload__hint">کلیک کنید — تصویر افقی با نسبت ۱۶:۹</span>
</template>
</div>
</a-upload>
<div v-else class="banner-image-upload__filled">
<img :src="imageUrl" alt="" class="banner-image-upload__img" />
<div class="banner-image-upload__bar">
<a-upload
name="file"
accept="image/*"
:show-upload-list="false"
:action="uploadURL"
:headers="fileHeader"
@change="onChange"
>
<button type="button" class="banner-image-upload__btn banner-image-upload__btn--primary">
تغییر تصویر
</button>
</a-upload>
<button
type="button"
class="banner-image-upload__btn banner-image-upload__btn--danger"
@click="emit('remove')"
>
حذف تصویر
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { LoadingOutlined } from '@ant-design/icons-vue'
import { message } from 'ant-design-vue'
import { uploadURL } from '@root/config'
import { getToken } from '@/utils'
import { normalizeUploadFileResponse } from '@/utils/file'
defineProps<{
imageUrl?: string | null
}>()
const emit = defineEmits<{
upload: [url: string]
remove: []
}>()
const loading = ref(false)
const fileHeader = computed(() => ({
Authorization: `Bearer ${getToken()}`,
}))
function onChange({ file }: { file: { status?: string; response?: unknown } }) {
if (file.status === 'uploading') {
loading.value = true
return
}
if (file.status === 'error') {
loading.value = false
message.error('خطا در آپلود تصویر')
return
}
if (file.status === 'done') {
loading.value = false
const fileResource = normalizeUploadFileResponse(file.response)
if (!fileResource?.full_src) {
message.error('خطا در ساخت آدرس تصویر')
return
}
emit('upload', fileResource.full_src)
message.success('تصویر با موفقیت آپلود شد')
}
}
</script>
<style scoped lang="less">
.banner-image-upload {
width: 100%;
height: 100%;
}
.banner-image-upload__zone {
display: block;
width: 100%;
height: 100%;
:deep(.ant-upload) {
display: block;
width: 100%;
height: 100%;
}
:deep(.ant-upload-select) {
display: block !important;
width: 100% !important;
height: 100% !important;
margin: 0 !important;
border: none !important;
background: transparent !important;
}
}
.banner-image-upload__empty {
display: flex;
width: 100%;
height: 100%;
min-height: 10rem;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 1.25rem;
border: 2px dashed color-mix(in srgb, var(--admin-primary, #f5b800) 45%, var(--admin-border));
border-radius: 12px;
background: color-mix(in srgb, var(--admin-bg-muted) 85%, transparent);
color: var(--admin-text-secondary);
text-align: center;
cursor: pointer;
transition: border-color 0.15s ease, background 0.15s ease;
&:hover {
border-color: var(--admin-primary, #f5b800);
background: color-mix(in srgb, var(--admin-primary, #f5b800) 8%, var(--admin-bg-muted));
}
}
.banner-image-upload__icon {
color: var(--admin-primary, #f5b800);
opacity: 0.9;
}
.banner-image-upload__cta {
font-size: 0.9375rem;
font-weight: 600;
color: var(--admin-text);
}
.banner-image-upload__hint {
font-size: 0.75rem;
line-height: 1.5;
}
.banner-image-upload__loading {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
font-size: 0.8125rem;
}
.banner-image-upload__filled {
position: relative;
width: 100%;
height: 100%;
min-height: 10rem;
overflow: hidden;
border-radius: 12px;
border: 1px solid var(--admin-border);
background: #000;
}
.banner-image-upload__img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.banner-image-upload__bar {
position: absolute;
inset: auto 0 0 0;
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.5rem;
padding: 0.75rem;
background: linear-gradient(to top, rgba(0, 0, 0, 0.78), transparent);
}
.banner-image-upload__btn {
padding: 0.4rem 0.85rem;
border: 1px solid rgba(255, 255, 255, 0.35);
border-radius: 8px;
background: rgba(0, 0, 0, 0.45);
color: #fff;
font-size: 0.8125rem;
cursor: pointer;
transition: background 0.15s ease;
&--primary:hover {
background: rgba(0, 0, 0, 0.65);
}
&--danger {
border-color: rgba(255, 77, 79, 0.55);
color: #ffb3b3;
}
&--danger:hover {
background: rgba(255, 77, 79, 0.35);
}
}
</style>
+399 -91
View File
@@ -1,104 +1,126 @@
<template> <template>
<div class="p-4"> <div class="banners-page">
<AdminFieldHelp :text="bannersPageIntro" root-class="mb-4 text-xs md:text-sm" /> <header class="banners-page__header">
<div class="banners-page__header-text">
<div class="banners-page__title-row">
<h1 class="banners-page__title">بنرهای اسلایدر</h1>
<AdminFieldHelp :text="bannersPageIntro" size="sm" />
</div>
<p class="banners-page__subtitle">تصاویر بالای صفحهٔ اصلی اپ مشتری — هر ردیف یک اسلاید</p>
</div>
<div class="banners-page__header-actions">
<BaseButton size="small" type="primary" @click="addRow"> <BaseButton size="small" type="primary" @click="addRow">
<span>افزودن بنر</span> <span>افزودن اسلاید</span>
<BaseIcon icon="vuesax-linear:add" size="17" /> <BaseIcon icon="vuesax-linear:add" size="17" />
</BaseButton> </BaseButton>
<AdminFieldHelp <BaseButton type="primary" :loading="saving" @click="handleSubmit">
text="هر بار «افزودن بنر» یک اسلاید جدید به انتهای لیست اضافه می‌کند." ذخیره تغییرات
root-class="mt-2" </BaseButton>
/> </div>
</header>
<div class="my-4 flex flex-col gap-2"> <div class="banners-page__body">
<div <div v-if="!benefitsList.length" class="banners-page__empty">
class="grid max-h-52vh grid-cols-1 gap-4 overflow-y-scroll md:max-h-75vh md:grid-cols-3" <div class="banners-page__empty-inner">
<BaseIcon icon="vuesax-linear:image" size="48" class="banners-page__empty-icon" />
<p class="banners-page__empty-title">هنوز بنری اضافه نشده</p>
<p class="banners-page__empty-text">اولین اسلاید را بسازید تا در اپ مشتری نمایش داده شود</p>
<BaseButton type="primary" @click="addRow">
<span>افزودن اسلاید</span>
<BaseIcon icon="vuesax-linear:add" size="17" />
</BaseButton>
</div>
</div>
<div v-else class="banners-page__list">
<article
v-for="(item, index) in benefitsList"
:key="index"
class="banner-card"
:class="{ 'banner-card--hidden': !item.show }"
> >
<Card v-for="(item, index) in benefitsList" :key="index"> <header class="banner-card__head">
<div class="mt-3 grid grid-cols-1 gap-4"> <div class="banner-card__head-start">
<div class="my-2 flex items-center justify-between gap-2"> <span class="banner-card__index">{{ index + 1 }}</span>
<div class="flex flex-col gap-0.5"> <div>
<div class="text-sm font-medium">نسبت تصویر ۱۶:۹</div> <div class="banner-card__title">اسلاید {{ index + 1 }}</div>
<AdminFieldHelp :text="bannersAspectHelp" /> <div class="banner-card__meta">
<span class="banner-card__pill" :class="{ 'banner-card__pill--off': !item.show }">
{{ item.show ? 'فعال در اپ' : 'مخفی' }}
</span>
<span class="banner-card__ratio">۱۶:۹</span>
</div> </div>
<div class="flex shrink-0 flex-col items-end gap-0.5">
<div class="flex items-center gap-2">
<span>نمایش:</span>
<a-switch v-model:checked="item.show" />
</div>
<AdminFieldHelp :text="bannersShowHelp" root-class="text-left" />
</div> </div>
</div> </div>
<div class="text-center"> <div class="banner-card__head-end">
<AdminFieldHelp :text="bannersImageHelp" root-class="mb-2 text-center" /> <label class="banner-card__switch">
<UploadPic <span>نمایش</span>
:compressed="true" <a-switch v-model:checked="item.show" size="small" />
@upload="handleUpload($event, index, 'icon')" <AdminFieldHelp :text="bannersShowHelp" />
@remove="handelRemoveFile(index, 'icon')" </label>
:imageUrl="item.icon" <button type="button" class="banner-card__remove" title="حذف اسلاید" @click="removeRow(index)">
<BaseIcon icon="vuesax-linear:trash" size="18" />
</button>
</div>
</header>
<div class="banner-card__content">
<div class="banner-card__preview-wrap">
<AdminLabelWithHelp label="پیش‌نمایش تصویر" :help="bannersImageSectionHelp" />
<div class="banner-card__preview">
<BannerImageUpload
:image-url="item.icon"
@upload="item.icon = $event"
@remove="item.icon = null"
/> />
</div> </div>
</div> </div>
<div class="mt-3 flex flex-col gap-1"> <div class="banner-card__fields" :class="{ 'banner-card__fields--stack': item.typeClick === 'modal' }">
<div class="text-sm font-medium">انتخاب رویداد (پس از کلیک):</div> <div class="banner-card__field">
<AdminFieldHelp :text="bannersEventHelp" /> <AdminLabelWithHelp label="بعد از کلیک مشتری" :help="clickEventHelp(item.typeClick)" />
<BasicSelect <BasicSelect
v-model:value="item.typeClick" v-model:value="item.typeClick"
:items="typeList" :items="typeList"
value-name="lable" value-name="lable"
key-name="value" key-name="value"
/> />
<AdminFieldHelp v-if="item.typeClick === 'link'" :text="bannersLinkHelp" />
<AdminFieldHelp v-else-if="item.typeClick === 'modal'" :text="bannersModalHelp" />
<AdminFieldHelp v-else-if="item.typeClick === 'share'" :text="bannersShareHelp" />
<AdminFieldHelp v-else :text="bannersNoEventHelp" />
</div> </div>
<div v-if="item.typeClick == 'link'" class="mt-2 flex flex-col gap-1"> <div v-if="item.typeClick === 'link'" class="banner-card__field banner-card__field--wide">
<div class="text-sm font-medium">آدرس لینک</div> <AdminLabelWithHelp label="آدرس لینک" :help="bannersLinkHelp" />
<BasicInput v-model:value="item.link" placeholder="https://..." /> <BasicInput v-model:value="item.link" placeholder="https://example.com" />
</div> </div>
<div v-if="item.typeClick == 'modal'" class="mt-2 flex flex-col gap-1"> <div v-if="item.typeClick === 'modal'" class="banner-card__field banner-card__field--wide">
<div class="text-sm font-medium">متن پیام</div> <AdminLabelWithHelp label="متن پیام" :help="bannersModalHelp" />
<QuillEditor <a-textarea
placeholder="متن پیام" :value="htmlToPlain(item.modalText)"
v-model:content="item.modalText" :rows="7"
toolbar="full" class="banner-card__message"
theme="snow" placeholder="متن پیامی که مشتری در پنجرهٔ بازشو می‌بیند..."
content-type="html" @update:value="item.modalText = plainToHtml($event)"
class="editor-box2"
/> />
</div> </div>
</Card>
</div> </div>
</div> </div>
</article>
<div class="mt-5 text-left md:mt-0"> </div>
<BaseButton type="primary" @click="handleSubmit">
<span>ذخیره تغییرات</span>
</BaseButton>
<AdminFieldHelp
text="بدون زدن این دکمه، هیچ‌کدام از تغییرات بالا در اپ مشتری دیده نمی‌شود."
root-class="mt-2"
/>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Card, message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import type { IBenefits } from '@/model/banner' import type { IBenefits } from '@/model/banner'
import UploadPic from '@/components/UploadPic.vue' import BannerImageUpload from './components/BannerImageUpload.vue'
import AdminFieldHelp from '@/components/AdminFieldHelp.vue' import AdminFieldHelp from '@/components/AdminFieldHelp.vue'
import AdminLabelWithHelp from '@/components/AdminLabelWithHelp.vue'
import { updateBannerConst, getHeaderBannerConst } from '@/api/consts' import { updateBannerConst, getHeaderBannerConst } from '@/api/consts'
import { import {
bannersAspectHelp,
bannersEventHelp, bannersEventHelp,
bannersImageHelp, bannersImageSectionHelp,
bannersLinkHelp, bannersLinkHelp,
bannersModalHelp, bannersModalHelp,
bannersNoEventHelp, bannersNoEventHelp,
@@ -106,19 +128,45 @@ import {
bannersShareHelp, bannersShareHelp,
bannersShowHelp, bannersShowHelp,
} from '@/utils/adminHelpCopy' } from '@/utils/adminHelpCopy'
import '@vueup/vue-quill/dist/vue-quill.snow.css'
import { QuillEditor } from '@vueup/vue-quill'
import { normalizeUploadFileResponse } from '@/utils/file'
const benefitsList = ref<IBenefits[]>([]) const benefitsList = ref<IBenefits[]>([])
const saving = ref(false)
let id: number | null = null let id: number | null = null
const typeList = ref([ const typeList = ref([
{ lable: 'بدون رویداد', value: null },
{ lable: 'باز شدن لینک', value: 'link' }, { lable: 'باز شدن لینک', value: 'link' },
{ lable: 'پنجره بازشو', value: 'modal' }, { lable: 'پنجره بازشو', value: 'modal' },
{ lable: 'اشتراک گذاری', value: 'share' }, { lable: 'اشتراک‌گذاری', value: 'share' },
{ lable: 'بدون رویداد', value: null },
]) ])
function clickEventHelp(typeClick: string | null) {
if (typeClick === 'link') return `${bannersEventHelp}\n\n${bannersLinkHelp}`
if (typeClick === 'modal') return `${bannersEventHelp}\n\n${bannersModalHelp}`
if (typeClick === 'share') return `${bannersEventHelp}\n\n${bannersShareHelp}`
return `${bannersEventHelp}\n\n${bannersNoEventHelp}`
}
function htmlToPlain(html: string | null | undefined): string {
if (!html) return ''
if (typeof document === 'undefined') {
return html.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]+>/g, '').trim()
}
const el = document.createElement('div')
el.innerHTML = html
return (el.textContent ?? el.innerText ?? '').trim()
}
function plainToHtml(text: string | null | undefined): string | null {
const trimmed = (text ?? '').trim()
if (!trimmed) return null
const escaped = trimmed
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
return `<p>${escaped.replace(/\n/g, '<br>')}</p>`
}
function parseBannerList(value: string | null | undefined): IBenefits[] { function parseBannerList(value: string | null | undefined): IBenefits[] {
if (!value) return [] if (!value) return []
try { try {
@@ -143,19 +191,8 @@ function addRow() {
}) })
} }
function handleUpload(response: unknown, index: number, field: keyof IBenefits) { function removeRow(index: number) {
const fileResource = normalizeUploadFileResponse(response) benefitsList.value.splice(index, 1)
if (!fileResource?.full_src) {
message.error('خطا در ساخت آدرس تصویر')
return
}
benefitsList.value[index][field] = fileResource.full_src
}
function handelRemoveFile(index: number, field: keyof IBenefits) {
benefitsList.value[index][field] = null
} }
async function handleSubmit() { async function handleSubmit() {
@@ -165,6 +202,7 @@ async function handleSubmit() {
} }
try { try {
saving.value = true
await updateBannerConst({ await updateBannerConst({
id, id,
value: JSON.stringify(benefitsList.value), value: JSON.stringify(benefitsList.value),
@@ -173,6 +211,8 @@ async function handleSubmit() {
message.success('اطلاعات با موفقیت ذخیره شد') message.success('اطلاعات با موفقیت ذخیره شد')
} catch (error) { } catch (error) {
message.error('خطا در ذخیره اطلاعات') message.error('خطا در ذخیره اطلاعات')
} finally {
saving.value = false
} }
} }
@@ -191,18 +231,286 @@ onMounted(async () => {
}) })
</script> </script>
<style lang="less"> <style scoped lang="less">
.editor-box2 { .banners-page {
max-height: 40vh; display: flex;
overflow-y: auto; flex-direction: column;
border: 1px solid #ddd; width: 100%;
border-radius: 8px; min-width: 0;
padding: 10px; height: 100%;
min-height: 0;
} }
.editor-box2 .ql-editor { .banners-page__header {
min-height: 10vh; display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
padding: 1rem 1.25rem;
border-bottom: 1px solid var(--admin-border);
background: var(--admin-bg-elevated);
}
.banners-page__title-row {
display: flex;
align-items: center;
gap: 0.5rem;
}
.banners-page__title {
margin: 0;
font-size: 1.25rem;
font-weight: 700;
color: var(--admin-text);
}
.banners-page__subtitle {
margin: 0.35rem 0 0;
font-size: 0.8125rem;
color: var(--admin-text-secondary);
}
.banners-page__header-actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.625rem;
}
.banners-page__body {
flex: 1;
min-height: 0;
overflow-y: auto; overflow-y: auto;
box-sizing: border-box; padding: 1rem 1.25rem 1.5rem;
}
.banners-page__empty {
display: flex;
width: 100%;
min-height: calc(100vh - 12rem);
align-items: center;
justify-content: center;
}
.banners-page__empty-inner {
display: flex;
width: 100%;
max-width: 28rem;
flex-direction: column;
align-items: center;
gap: 0.625rem;
padding: 3rem 1.5rem;
border: 1px dashed var(--admin-border);
border-radius: 16px;
background: var(--admin-bg-elevated);
text-align: center;
}
.banners-page__empty-icon {
color: var(--admin-text-secondary);
opacity: 0.65;
}
.banners-page__empty-title {
margin: 0.25rem 0 0;
font-size: 1rem;
font-weight: 600;
color: var(--admin-text);
}
.banners-page__empty-text {
margin: 0 0 0.75rem;
font-size: 0.875rem;
color: var(--admin-text-secondary);
line-height: 1.6;
}
.banners-page__list {
display: flex;
width: 100%;
flex-direction: column;
gap: 1rem;
}
.banner-card {
width: 100%;
overflow: hidden;
border: 1px solid var(--admin-border);
border-radius: 14px;
background: var(--admin-bg-elevated);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
&--hidden {
opacity: 0.88;
}
}
.banner-card__head {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0.875rem 1rem;
border-bottom: 1px solid var(--admin-border);
background: color-mix(in srgb, var(--admin-bg-muted) 70%, transparent);
}
.banner-card__head-start {
display: flex;
align-items: center;
gap: 0.75rem;
min-width: 0;
}
.banner-card__index {
display: inline-flex;
width: 2.25rem;
height: 2.25rem;
flex-shrink: 0;
align-items: center;
justify-content: center;
border-radius: 10px;
background: var(--admin-primary, #f5b800);
color: #1a1a1a;
font-size: 0.9375rem;
font-weight: 800;
}
.banner-card__title {
font-size: 0.9375rem;
font-weight: 700;
color: var(--admin-text);
}
.banner-card__meta {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
margin-top: 0.2rem;
}
.banner-card__pill {
font-size: 0.75rem;
color: #52c41a;
&--off {
color: var(--admin-text-secondary);
}
}
.banner-card__ratio {
font-size: 0.6875rem;
color: var(--admin-text-secondary);
}
.banner-card__head-end {
display: flex;
align-items: center;
gap: 0.75rem;
}
.banner-card__switch {
display: inline-flex;
align-items: center;
gap: 0.4rem;
margin: 0;
font-size: 0.8125rem;
color: var(--admin-text-secondary);
cursor: pointer;
}
.banner-card__remove {
display: inline-flex;
width: 2.25rem;
height: 2.25rem;
align-items: center;
justify-content: center;
border: 1px solid rgba(255, 77, 79, 0.35);
border-radius: 10px;
background: rgba(255, 77, 79, 0.08);
color: #ff4d4f;
cursor: pointer;
transition: background 0.15s ease;
&:hover {
background: rgba(255, 77, 79, 0.16);
}
}
.banner-card__content {
display: grid;
grid-template-columns: 1fr;
gap: 1.25rem;
padding: 1rem;
@media (min-width: 900px) {
grid-template-columns: minmax(280px, 42%) minmax(0, 1fr);
padding: 1.125rem;
}
}
.banner-card__preview-wrap {
display: flex;
min-width: 0;
flex-direction: column;
gap: 0.5rem;
}
.banner-card__preview {
aspect-ratio: 16 / 9;
min-height: 11rem;
}
.banner-card__fields {
display: grid;
grid-template-columns: 1fr;
gap: 1rem;
min-width: 0;
align-content: start;
&--stack {
grid-template-columns: 1fr;
}
}
.banner-card__field {
display: flex;
flex-direction: column;
gap: 0.5rem;
min-width: 0;
&--wide {
grid-column: 1 / -1;
}
}
.banner-card__message {
width: 100%;
min-height: 10.5rem;
padding: 0.75rem 0.875rem;
border: 1px solid var(--admin-border) !important;
border-radius: 10px !important;
background: var(--admin-bg-muted) !important;
color: var(--admin-text) !important;
font-size: 0.875rem;
line-height: 1.7;
resize: vertical;
&::placeholder {
color: var(--admin-text-secondary);
}
&:hover {
border-color: color-mix(in srgb, var(--admin-primary, #f5b800) 35%, var(--admin-border)) !important;
}
&:focus {
border-color: var(--admin-primary, #f5b800) !important;
box-shadow: 0 0 0 2px color-mix(in srgb, var(--admin-primary, #f5b800) 18%, transparent);
outline: none;
}
} }
</style> </style>
@@ -1,46 +1,57 @@
<template> <template>
<a-form :model="form" ref="formRef" autocomplete="off"> <a-form :model="form" ref="formRef" autocomplete="off">
<div class="space-y-3"> <div class="space-y-3">
<a-form-item label="شماره موبایل مشتری" name="mobile" :rules="inputLenRules(11)"> <a-form-item name="mobile" :rules="inputLenRules(11)">
<template #label>
<AdminLabelWithHelp label="شماره موبایل مشتری" :help="giftCardMobileHelp" />
</template>
<BasicInput v-model:value="form.mobile" type="tel" placeholder="09123456789" /> <BasicInput v-model:value="form.mobile" type="tel" placeholder="09123456789" />
<AdminFieldHelp :text="giftCardMobileHelp" root-class="mt-1" />
</a-form-item> </a-form-item>
<a-form-item label="نام مشتری" name="name" :rules="inputRules()"> <a-form-item name="name" :rules="inputRules()">
<template #label>
<AdminLabelWithHelp label="نام مشتری" :help="giftCardNameHelp" />
</template>
<BasicInput v-model:value="form.name" placeholder="نام و نام خانوادگی مشتری" /> <BasicInput v-model:value="form.name" placeholder="نام و نام خانوادگی مشتری" />
<AdminFieldHelp :text="giftCardNameHelp" root-class="mt-1" />
</a-form-item> </a-form-item>
<a-form-item label="نوع کارت هدیه" name="unit" :rules="inputRules()"> <a-form-item name="unit" :rules="inputRules()">
<template #label>
<AdminLabelWithHelp label="نوع کارت هدیه" :help="giftCardUnitHelp" />
</template>
<a-radio-group v-model:value="form.unit" button-style="solid"> <a-radio-group v-model:value="form.unit" button-style="solid">
<a-radio-button value="1">ریالی</a-radio-button> <a-radio-button value="1">ریالی</a-radio-button>
<a-radio-button value="2">{{ metalLabel.goldWalletShort() }}</a-radio-button> <a-radio-button value="2">{{ metalLabel.goldWalletShort() }}</a-radio-button>
</a-radio-group> </a-radio-group>
<AdminFieldHelp :text="giftCardUnitHelp" root-class="mt-1" />
</a-form-item> </a-form-item>
<a-form-item label="تعداد کدها" name="count" :rules="inputRulesNumber()"> <a-form-item name="count" :rules="inputRulesNumber()">
<template #label>
<AdminLabelWithHelp label="تعداد کدها" :help="giftCardCountHelp" />
</template>
<BasicInput v-model:value="form.count" type="tel" placeholder="تعداد کدهای کارت هدیه" /> <BasicInput v-model:value="form.count" type="tel" placeholder="تعداد کدهای کارت هدیه" />
<AdminFieldHelp :text="giftCardCountHelp" root-class="mt-1" />
</a-form-item> </a-form-item>
<a-form-item <a-form-item name="amount" :rules="inputRulesNumber()">
<template #label>
<AdminLabelWithHelp
:label="`مقدار ${form.unit === '2' ? '(گرم)' : '(ریال)'}`" :label="`مقدار ${form.unit === '2' ? '(گرم)' : '(ریال)'}`"
name="amount" :help="giftCardAmountHelp"
:rules="inputRulesNumber()" />
> </template>
<BasicInput <BasicInput
v-model:value="form.amount" v-model:value="form.amount"
:addon-before="form.unit === '2' ? 'گرم' : 'ریال'" :addon-before="form.unit === '2' ? 'گرم' : 'ریال'"
type="tel" type="tel"
placeholder="مقدار کارت هدیه" placeholder="مقدار کارت هدیه"
/> />
<AdminFieldHelp :text="giftCardAmountHelp" root-class="mt-1" />
</a-form-item> </a-form-item>
<a-form-item label="تاریخ انقضا" name="expiry_date" :rules="inputRules()"> <a-form-item name="expiry_date" :rules="inputRules()">
<template #label>
<AdminLabelWithHelp label="تاریخ انقضا" :help="giftCardExpiryHelp" />
</template>
<BaseDatePicker v-model:value="form.expiry_date" format="YYYY-MM-DD" /> <BaseDatePicker v-model:value="form.expiry_date" format="YYYY-MM-DD" />
<AdminFieldHelp :text="giftCardExpiryHelp" root-class="mt-1" />
</a-form-item> </a-form-item>
<a-form-item label="توضیحات" name="description" class="mb-0"> <a-form-item label="توضیحات" name="description" class="mb-0">
@@ -53,7 +64,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { FormInstance } from 'ant-design-vue' import type { FormInstance } from 'ant-design-vue'
import type { GiftcardInput } from '@/model/giftCard' import type { GiftcardInput } from '@/model/giftCard'
import AdminFieldHelp from '@/components/AdminFieldHelp.vue' import AdminLabelWithHelp from '@/components/AdminLabelWithHelp.vue'
import { import {
giftCardAmountHelp, giftCardAmountHelp,
giftCardCountHelp, giftCardCountHelp,
+17 -9
View File
@@ -34,21 +34,29 @@
> >
<div v-if="isMobile" class="mb-3 text-base font-700">{{ discountSheetTitle }}</div> <div v-if="isMobile" class="mb-3 text-base font-700">{{ discountSheetTitle }}</div>
<a-form :model="discountForm" ref="discountFormRef" autocomplete="off" layout="vertical"> <a-form :model="discountForm" ref="discountFormRef" autocomplete="off" layout="vertical">
<a-form-item label="نام پویش" name="name" :rules="inputRules()"> <a-form-item name="name" :rules="inputRules()">
<template #label>
<AdminLabelWithHelp label="نام پویش" :help="discountNameHelp" />
</template>
<BasicInput v-model:value="discountForm.name" placeholder="نام پویش" /> <BasicInput v-model:value="discountForm.name" placeholder="نام پویش" />
<AdminFieldHelp :text="discountNameHelp" root-class="mt-1" />
</a-form-item> </a-form-item>
<a-form-item label="درصد تخفیف" name="amount" :rules="inputRulesNumber()"> <a-form-item name="amount" :rules="inputRulesNumber()">
<template #label>
<AdminLabelWithHelp label="درصد تخفیف" :help="discountPercentHelp" />
</template>
<BasicInput v-model:value="discountForm.amount" type="tel" placeholder="مثلاً 10" addon-before="%" /> <BasicInput v-model:value="discountForm.amount" type="tel" placeholder="مثلاً 10" addon-before="%" />
<AdminFieldHelp :text="discountPercentHelp" root-class="mt-1" />
</a-form-item> </a-form-item>
<a-form-item label="تعداد کدها" name="count" :rules="inputRulesNumber()"> <a-form-item name="count" :rules="inputRulesNumber()">
<template #label>
<AdminLabelWithHelp label="تعداد کدها" :help="discountCountHelp" />
</template>
<BasicInput v-model:value="discountForm.count" type="tel" /> <BasicInput v-model:value="discountForm.count" type="tel" />
<AdminFieldHelp :text="discountCountHelp" root-class="mt-1" />
</a-form-item> </a-form-item>
<a-form-item label="تاریخ انقضا" name="expiry_date" :rules="inputRules()"> <a-form-item name="expiry_date" :rules="inputRules()">
<template #label>
<AdminLabelWithHelp label="تاریخ انقضا" :help="discountExpiryHelp" />
</template>
<BaseDatePicker v-model:value="discountForm.expiry_date" format="YYYY-MM-DD" /> <BaseDatePicker v-model:value="discountForm.expiry_date" format="YYYY-MM-DD" />
<AdminFieldHelp :text="discountExpiryHelp" root-class="mt-1" />
</a-form-item> </a-form-item>
<a-form-item label="توضیحات" name="description"> <a-form-item label="توضیحات" name="description">
<BasicTextArea v-model:value="discountForm.description" place-holder="توضیحات اختیاری — فقط برای یادداشت شما" /> <BasicTextArea v-model:value="discountForm.description" place-holder="توضیحات اختیاری — فقط برای یادداشت شما" />
@@ -115,7 +123,7 @@ import { isStoreApp } from '@/utils/config'
import { inputRules, inputRulesNumber } from '@/utils/inputRules' import { inputRules, inputRulesNumber } from '@/utils/inputRules'
import BasicInput from '@/components/BasicInput.vue' import BasicInput from '@/components/BasicInput.vue'
import BasicTextArea from '@/components/BasicTextArea.vue' import BasicTextArea from '@/components/BasicTextArea.vue'
import AdminFieldHelp from '@/components/AdminFieldHelp.vue' import AdminLabelWithHelp from '@/components/AdminLabelWithHelp.vue'
import { import {
discountCountHelp, discountCountHelp,
discountExpiryHelp, discountExpiryHelp,
@@ -0,0 +1,65 @@
<template>
<a-form :model="form" ref="formRef" layout="vertical" autocomplete="off">
<a-form-item label="مشتری" name="customer_id" :rules="inputRules()">
<BasicSelect
v-model:value="form.customer_id"
:items="customerItems"
key-name="id"
value-name="label"
placeholder="انتخاب مشتری"
:disabled="Boolean(editId)"
/>
</a-form-item>
<a-form-item label="شرط قیمت" name="price_condition" :rules="inputRules()">
<BasicSelect
v-model:value="form.price_condition"
:items="conditionItems"
key-name="key"
value-name="value"
placeholder="بزرگتر یا کوچکتر"
/>
</a-form-item>
<a-form-item label="قیمت هدف" name="target_price" :rules="inputRulesNumber()">
<BasicInput v-model:value="form.target_price" type="price" addon-before="ریال" placeholder="مثلاً ۱۷۸۵۰۰۰۰۰" />
</a-form-item>
<a-form-item label="روش اطلاع‌رسانی" name="action" :rules="inputRules()">
<a-radio-group v-model:value="form.action" button-style="solid">
<a-radio-button value="sms">پیامک</a-radio-button>
<a-radio-button value="email">ایمیل</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item label="وضعیت" name="is_active">
<a-switch v-model:checked="form.is_active" checked-children="فعال" un-checked-children="غیرفعال" />
</a-form-item>
</a-form>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import type { FormInstance } from 'ant-design-vue'
import type { Customer } from '@/model/customer'
import type { PriceTriggers } from '@/model/priceTriggers'
import { inputRules, inputRulesNumber } from '@/utils/inputRules'
defineProps<{
form: Partial<PriceTriggers>
customerItems: Array<{ id: number; label: string }>
editId?: number
}>()
const conditionItems = [
{ value: 'بزرگتر از', key: '>' },
{ value: 'کوچکتر از', key: '<' },
]
const formRef = ref<FormInstance>()
defineExpose({
validateFields: () => formRef.value?.validateFields(),
resetFields: () => formRef.value?.resetFields(),
})
</script>
@@ -1,27 +1,52 @@
<template> <template>
<div> <div>
<BasicShowContext title="هشدار قیمت"> <BasicShowContext title="هشدار قیمت">
<AdminFieldHelp :text="priceTriggersPageIntro" root-class="mb-3 px-1" /> <template #extra>
<BaseTable v-bind="tblProps" @register="submit" ref="tbl"> <div class="flex items-center gap-2">
<!-- <template #extraHeader> <AdminFieldHelp :text="priceTriggersPageIntro" />
<BaseButton type="primary" size="small" class="flex" @click="emits('add')"> <BaseButton type="primary" size="small" @click="emits('add')">
<div>افزودن مدیر</div> <span>افزودن هشدار</span>
<BaseIcon icon="vuesax-linear:user-add" Size="16px" /> <BaseIcon icon="vuesax-linear:add" size="16" />
</BaseButton> </BaseButton>
</template>--> </div>
<template #action="{ column, record }">
<div class="gap-2 flex justify-center">
<a-tooltip placement="bottom" class="cursor-pointer" @click="emits('info', record)">
<template #title>
<span>مشخصات</span>
</template> </template>
<Icon icon="vuesax-linear:stickynote" color="#10913A" />
<BaseTable v-bind="tblProps" @register="submit" ref="tbl">
<template #extraHeader>
<BaseButton type="primary" size="small" class="hidden md:inline-flex" @click="emits('add')">
<span>افزودن هشدار</span>
<BaseIcon icon="vuesax-linear:add" size="16" />
</BaseButton>
</template>
<template #action="{ record }">
<div class="flex justify-center gap-2">
<a-tooltip placement="bottom">
<template #title>ویرایش</template>
<Icon
icon="vuesax-linear:edit"
color="#4572E9"
class="cursor-pointer"
@click="emits('edit', record)"
/>
</a-tooltip> </a-tooltip>
<a-tooltip placement="bottom" class="cursor-pointer" @click="emits('edit', record)"> <a-tooltip placement="bottom">
<template #title> <template #title>{{ record.is_active ? 'غیرفعال کردن' : 'فعال کردن' }}</template>
<span>ویرایش</span> <Icon
</template> :icon="record.is_active ? 'vuesax-linear:eye-slash' : 'vuesax-linear:eye'"
<Icon icon="vuesax-linear:edit" color="#4572E9" /> :color="record.is_active ? '#C53030' : '#25855A'"
class="cursor-pointer"
@click="emits('toggle', record)"
/>
</a-tooltip>
<a-tooltip placement="bottom">
<template #title>حذف</template>
<Icon
icon="vuesax-linear:trash"
color="#C53030"
class="cursor-pointer"
@click="emits('delete', record.id)"
/>
</a-tooltip> </a-tooltip>
</div> </div>
</template> </template>
@@ -33,67 +58,29 @@
<script lang="tsx" setup> <script lang="tsx" setup>
import type { TableInput } from '@core/type/baseTable' import type { TableInput } from '@core/type/baseTable'
import useBaseTable from '@core/components/BaseTableOld/hooks/useBaseTable' import useBaseTable from '@core/components/BaseTableOld/hooks/useBaseTable'
import { toJalali, toJalaliNoTime } from '@core/utils/useDataTime'
import { message, Tag } from 'ant-design-vue'
import CustomTag from '@/components/CustomTag.vue'
import Icon from '@core/components/BaseIcon/src/Icon.vue' import Icon from '@core/components/BaseIcon/src/Icon.vue'
import { changeUserActivation, getUserTable } from '@/api/user'
import { useUserStore } from '@/store/userStore'
import { cols } from '../hooks/priceTriggersColumns' import { cols } from '../hooks/priceTriggersColumns'
import { changeCustomerActivation } from '@/api/customer'
import { getPriceTriggersTable } from '@/api/priceTriggers' import { getPriceTriggersTable } from '@/api/priceTriggers'
import AdminFieldHelp from '@/components/AdminFieldHelp.vue' import AdminFieldHelp from '@/components/AdminFieldHelp.vue'
import { priceTriggersPageIntro } from '@/utils/adminHelpCopy' import { priceTriggersPageIntro } from '@/utils/adminHelpCopy'
const [submit, reload] = useBaseTable() const [submit, reload] = useBaseTable()
//emits const emits = defineEmits<{
const emits = defineEmits(['delete', 'edit', 'add', 'info']) add: []
edit: [record: Record<string, unknown>]
toggle: [record: Record<string, unknown>]
delete: [id: number]
}>()
//define table const tblProps = {
const table = ref(null)
//permission
function checkPermission(temp: string) {
const store = useUserStore()
return store.canAccess(temp)
}
const loading = ref<boolean>(false)
async function changeUserActivationFunction(id: number) {
try {
loading.value = true
await changeUserActivation(id)
reload()
message.success('تغییر وضعیت با موفقیت انجام شد')
} catch (e) {
console.error(e)
} finally {
loading.value = false
}
}
const tblProps = ref<any>({
columns: cols, columns: cols,
api: (params: TableInput) => { api: (params: TableInput) => getPriceTriggersTable(params),
return getPriceTriggersTable(params) }
}
//bordered: true
})
const fetch = () => { function fetch() {
// console.log('sss')
reload() reload()
} }
// const spinTable = (val: boolean) => {
// // console.log('sss')
// table?.value?.loading.value = val
// }
defineExpose({
fetch
// spinTable
})
</script>
<style scoped></style> defineExpose({ fetch })
</script>
@@ -29,6 +29,16 @@ export const cols: BaseTableColumn[] = [
return record.index + 1 return record.index + 1
} }
}, },
{
title: 'مشتری',
dataIndex: 'customer',
key: 'customer',
type: 'Text',
customRender: ({ record }) => {
const name = record.customer?.name?.trim()
return name ? `${name} (${record.customer_id})` : String(record.customer_id ?? '-')
},
},
{ {
title: 'کد مشتری', title: 'کد مشتری',
dataIndex: 'customer_id', dataIndex: 'customer_id',
@@ -203,6 +213,15 @@ export const cols: BaseTableColumn[] = [
{ {
title: 'روش ارسال', title: 'روش ارسال',
dataIndex: 'action', dataIndex: 'action',
key: 'action', key: 'notify_action',
} customRender: ({ record }) => (record.action === 'email' ? 'ایمیل' : 'پیامک'),
},
{
title: 'عملیات',
dataIndex: 'row_action',
key: 'row_action',
slot: 'action',
fixed: 'right',
filterable: false,
},
] ]
+135 -220
View File
@@ -1,248 +1,163 @@
<template> <template>
<div> <div class="flex h-full w-full">
<!-- <BasicMainCard>--> <tbl ref="tableRef" class="h-full w-full" @add="openAdd" @edit="openEdit" @toggle="toggleActive" @delete="deleteItem" />
<div class="flex w-full h-full">
<tbl ref="table" class="w-full h-full" @info="info" @edit="edit" @add="add" />
<!-- <BasicMenu :menu="menu" width="13rem" @onChange="tabSelected" /> <BasicActionSheet
<component :is="selectedComponent" class="mr-5" />--> v-model:visible="editorVisible"
<!-- <BasicShowContext title="پروژه ها"> <Search /> </BasicShowContext>--> :title="sheetTitle"
</div> modal-width="32rem"
<!-- </BasicMainCard>--> drawer-height="85vh"
<!-- <BasicModal
v-model:visible="addUserVisible"
width="40rem"
:title="`${editId ? 'ویرایش' : 'افزودن'} کاربر`"
@ok="ok"
:loading="loading" :loading="loading"
@close="addUserVisible = false" :show-button="true"
>--> :show-cancel="true"
<!-- <a-form :model="form" ref="formRef" autocomplete="off"> :show-ok="true"
<div class="grid md:grid-cols-2 gap-4"> ok-text="ذخیره"
<a-form-item label="نام" name="fname" :rules="inputRules()"> cancel-text="انصراف"
<BasicInput v-model:value="form.fname" /> @ok="submitForm"
</a-form-item> @close="closeEditor"
<a-form-item label="نام خانوادگی" name="lname" :rules="inputRules()"> @cancel="closeEditor"
<BasicInput v-model:value="form.lname" /> >
</a-form-item> <div v-if="isMobile" class="mb-3 text-base font-bold">{{ sheetTitle }}</div>
<a-form-item label="شماره همراه" name="mobile" :rules="inputLenRules(11)"> <PriceTriggerEditorFormBody
<BasicInput v-model:value="form.mobile" type="number" /> ref="editorRef"
</a-form-item> :form="form"
<a-form-item v-if="editId" label="رمز عبور" name="password"> :customer-items="customerItems"
<BasicInput v-model:value="form.password" is-password type="number" /> :edit-id="editId"
</a-form-item> />
<a-form-item v-if="editId" label="تکرار رمز عبور"> </BasicActionSheet>
<BasicInput v-model:value="form.repassword" is-password type="number" />
</a-form-item>
</div>
</a-form>-->
<!-- <BasicInfoModal
v-model:visible="userInfoVisible"
@close="userInfoVisible = false"
:title="infoParams.title"
:date="infoParams.date"
:time="infoParams.time"
:type="infoParams.type"
itemsTitle="اطلاعات فردی"
:name="infoParams.name"
:items="infoParams.items"
/>-->
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// import BasicInfoModal from '@/components/BasicInfoModal' import { computed, onMounted, ref } from 'vue'
import { inputLenRules, inputRules } from '@/utils/inputRules' import { message } from 'ant-design-vue'
import { useStaticApiListsStore } from '@/store/staticApisStore'
import type { Const } from '@/model/const'
import type { User } from '@/model/user'
import { createUser, deleteUser, updateUser } from '@/api/user'
import { ref } from 'vue'
import tbl from './components/table.vue'
import type { FormInstance } from 'ant-design-vue'
import BasicInfoModal from '@/components/BasicInfoModal/index.vue'
import { message } from 'ant-design-vue/es'
import type { Info } from '@/components/BasicInfoModal/model/Info'
import { toJalaliNoTime, toTime } from '@core/utils/useDataTime'
import { confirmDeleteItem } from '@/utils/confirmModal'
import { cloneDeep } from 'lodash' import { cloneDeep } from 'lodash'
import type { Item } from '@/model/Item' import tbl from './components/table.vue'
import BasicActionSheet from '@/components/BasicActionSheet.vue'
import PriceTriggerEditorFormBody from './components/PriceTriggerEditorFormBody.vue'
import type { PriceTriggers } from '@/model/priceTriggers'
import type { Customer } from '@/model/customer'
import { getCustomersList } from '@/api/customer'
import {
createPriceTrigger,
deletePriceTrigger,
togglePriceTriggerActive,
updatePriceTrigger,
} from '@/api/priceTriggers'
import { confirmDeleteItem } from '@/utils/confirmModal'
import { isMobile } from '@core/utils/is'
//loading const tableRef = ref<null | { fetch: () => void }>(null)
const loading = ref<boolean>(false) const editorRef = ref<{
validateFields?: () => Promise<unknown>
resetFields?: () => void
}>()
//define store const loading = ref(false)
const store = useStaticApiListsStore() const editorVisible = ref(false)
const editId = ref<number | undefined>()
const customers = ref<Customer[]>([])
//define tbl const defaultForm = (): Partial<PriceTriggers> => ({
const table = ref(null) customer_id: undefined,
const refreshTable = () => table?.value?.fetch() target_price: undefined,
price_condition: '>',
let updateKey = ref<number>(0) action: 'sms',
is_active: true,
//role confirm items
const confirmItems: Array<Item> = [
{
value: 'بله',
key: true
},
{
value: 'خیر',
key: false
}
]
//form
const form = ref<User>({
active: true
})
const formRef = ref<FormInstance>()
//info
let infoParams = reactive<Info>({})
function info(val: User) {
infoParams.name = val.name
infoParams.type = val.mobile
infoParams.date = toJalaliNoTime(val.created_at)
infoParams.time = toTime(val.created_at)
infoParams.itemsTitle = 'اطلاعات کاربری'
infoParams.items = [
{
key: 'تلفن همراه',
value: val.mobile ?? '-'
},
{
key: 'کد کاربری',
value: val.id ?? '-'
}
]
userInfoVisible.value = true
}
//edit
let editId: number | undefined = undefined
async function edit(record: User) {
addUserVisible.value = true
editId = record.id
form.value = cloneDeep(record)
}
//add
async function add() {
loading.value = true
editId = undefined
// await store.getUsersList()
addUserVisible.value = true
updateKey.value++
resetData()
loading.value = false
}
//reset data
const resetData = () => {
formRef.value?.resetFields()
form.value = {
fname: '',
lname: '',
active: false,
avatar_id: undefined,
sts: undefined,
id: undefined,
mobile: undefined
}
}
//delete
const deleteItem = (id: number) => confirmDeleteItem('کاربر', id, deleteUser, refreshTable)
/* Modal.confirm({
title: 'آیا از حذف کاربر اطمینان دارید؟',
icon: createVNode(ExclamationCircleOutlined),
okText: 'تایید',
okType: 'danger',
centered: true,
cancelText: 'انصراف',
async onOk() {
// console.log('OK');
await deleteItem(id)
},
onCancel() {
console.log('Cancel')
}
})*/
/*async function deleteItem(id: number) {
try {
loading.value = true
// table?.value?.spinTable(true)
await deleteUser(id)
message.success('با موفقیت حذف شد')
table?.value?.fetch()
} catch (e) {
console.log(e)
message.error('خطا در حذف')
} finally {
loading.value = false
// table?.value?.spinTable(false)
}
}*/
//define selects
onMounted(async () => {
loading.value = true
// await getResponsible()
// await getQC()
// await getManager()
loading.value = false
}) })
interface selectsItem { const form = ref<Partial<PriceTriggers>>(defaultForm())
responsibleItems: Array<User> | undefined
users: Array<Const> | undefined const sheetTitle = computed(() => (editId.value ? 'ویرایش هشدار قیمت' : 'افزودن هشدار قیمت'))
const customerItems = computed(() =>
customers.value.map((customer) => ({
id: customer.id,
label: `${customer.name || 'بدون نام'} — ${customer.mobile || customer.id}`,
}))
)
function refreshTable() {
tableRef.value?.fetch()
} }
//add project modal function closeEditor() {
const addUserVisible = ref<boolean>(false) editorVisible.value = false
const userInfoVisible = ref<boolean>(false) }
//submit user function openAdd() {
async function ok() { editId.value = undefined
formRef.value?.validateFields().then(async () => { form.value = defaultForm()
// console.log('salam', form.value) editorRef.value?.resetFields?.()
// return editorVisible.value = true
loading.value = true }
function openEdit(record: PriceTriggers) {
editId.value = record.id
form.value = cloneDeep({
customer_id: record.customer_id,
target_price: record.target_price,
price_condition: record.price_condition,
action: record.action,
is_active: record.is_active,
})
editorVisible.value = true
}
async function submitForm() {
try { try {
if (editId) { await editorRef.value?.validateFields?.()
if (form.value.password) { loading.value = true
if (form.value.password === form.value.repassword) {
await updateUser(editId, form.value) const payload = {
} else { customer_id: Number(form.value.customer_id),
message.error('پسورد وارد شده با تکرار پسورد وارد شده یکسان نیست') target_price: Number(form.value.target_price),
return price_condition: form.value.price_condition,
action: form.value.action,
is_active: Boolean(form.value.is_active),
} }
if (editId.value) {
await updatePriceTrigger(editId.value, payload)
} else { } else {
await updateUser(editId, form.value) await createPriceTrigger(payload)
} }
} else {
await createUser(form.value) message.success('هشدار با موفقیت ذخیره شد')
} editorVisible.value = false
addUserVisible.value = false
refreshTable() refreshTable()
resetData()
message.success('عملیات با موفقیت انجام شد')
// await getResponsible()
// await getQC()
// await getManager()
} catch (e) { } catch (e) {
console.error(e) console.error(e)
// message.error('خطا در بارگذاری اطلاعات')
} finally { } finally {
loading.value = false loading.value = false
} }
})
} }
</script>
<style scoped></style> async function toggleActive(record: PriceTriggers) {
if (!record.id) return
try {
loading.value = true
await togglePriceTriggerActive(record.id)
message.success(record.is_active ? 'هشدار غیرفعال شد' : 'هشدار فعال شد')
refreshTable()
} catch (e) {
console.error(e)
message.error('خطا در تغییر وضعیت')
} finally {
loading.value = false
}
}
function deleteItem(id: number) {
confirmDeleteItem('هشدار قیمت', id, deletePriceTrigger, refreshTable)
}
onMounted(async () => {
try {
customers.value = (await getCustomersList()) ?? []
} catch (e) {
console.error(e)
}
})
</script>
@@ -2,6 +2,8 @@
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden"> <div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
<BasicShowContext title="تاریخچه قیمت"> <BasicShowContext title="تاریخچه قیمت">
<template #extra> <template #extra>
<div class="flex items-center gap-2">
<AdminFieldHelp :text="pricingPageIntro" />
<BaseButton <BaseButton
v-if="isMobile" v-if="isMobile"
type="primary" type="primary"
@@ -12,13 +14,14 @@
> >
به‌روزرسانی قیمت به‌روزرسانی قیمت
</BaseButton> </BaseButton>
</div>
</template> </template>
<AdminFieldHelp :text="pricingPageIntro" root-class="mb-3 px-1" />
<div class="flex h-full min-h-0 flex-col"> <div class="flex h-full min-h-0 flex-col">
<BaseTable v-if="!isMobile" class="h-full min-h-0 min-w-0" v-bind="tblProps" @register="submit" ref="tbl"> <BaseTable v-if="!isMobile" class="h-full min-h-0 min-w-0" v-bind="tblProps" @register="submit" ref="tbl">
<template #extraHeader> <template #extraHeader>
<div class="flex items-center gap-2">
<AdminFieldHelp :text="pricingPageIntro" />
<BaseButton <BaseButton
type="primary" type="primary"
size="small" size="small"
@@ -28,6 +31,7 @@
> >
به‌روزرسانی قیمت به‌روزرسانی قیمت
</BaseButton> </BaseButton>
</div>
</template> </template>
<template #action="{ record }"> <template #action="{ record }">
<div class="flex justify-center gap-2"> <div class="flex justify-center gap-2">
+5 -2
View File
@@ -10,7 +10,10 @@ export const bannersAspectHelp =
'عکس باید افقی و با نسبت ۱۶:۹ باشد (مثل تصویر تلویزیون). اگر بلند و باریک یا مربع باشد، در اپ کشیده یا بریده می‌شود.' 'عکس باید افقی و با نسبت ۱۶:۹ باشد (مثل تصویر تلویزیون). اگر بلند و باریک یا مربع باشد، در اپ کشیده یا بریده می‌شود.'
export const bannersImageHelp = export const bannersImageHelp =
'روی «افزودن عکس» بزنید و یک تصویر با کیفیت مناسب انتخاب کنید. آیکون سطل زباله کل این بنر را از لیست حذف می‌کند.' 'روی «افزودن عکس» بزنید. آیکون سطل زباله فقط تصویر را پاک می‌کند؛ برای حذف کل اسلاید از دکمهٔ «حذف اسلاید» استفاده کنید.'
export const bannersImageSectionHelp =
'عکس افقی با نسبت ۱۶:۹ (مثل تلویزیون) بهترین نتیجه را می‌دهد. تصویر نامناسب در اپ کشیده یا بریده می‌شود.'
export const bannersEventHelp = export const bannersEventHelp =
'وقتی مشتری روی بنر ضربه می‌زند چه اتفاقی بیفتد؟ اگر مطمئن نیستید «بدون رویداد» را بگذارید.' 'وقتی مشتری روی بنر ضربه می‌زند چه اتفاقی بیفتد؟ اگر مطمئن نیستید «بدون رویداد» را بگذارید.'
@@ -61,4 +64,4 @@ export const discountExpiryHelp =
'بعد از این تاریخ کدهای این پویش دیگر کار نمی‌کنند.' 'بعد از این تاریخ کدهای این پویش دیگر کار نمی‌کنند.'
export const priceTriggersPageIntro = export const priceTriggersPageIntro =
'لیست درخواست‌های «هشدار قیمت» مشتریان. وقتی قیمت به عددی که مشتری گفته برسد، به او اطلاع داده می‌شود. از آیکون مداد می‌توانید وضعیت را ببینید یا ویرایش کنید.' 'لیست هشدارهای قیمت مشتریان. می‌توانید هشدار جدید بسازید، ویرایش کنید، فعال/غیرفعال کنید یا حذف کنید. وقتی قیمت به هدف برسد، به مشتری پیامک یا ایمیل می‌رود و هشدار خودکار غیرفعال می‌شود.'