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']
AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
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']
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
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']
ASwitch: typeof import('ant-design-vue/es')['Switch']
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']
ATimePicker: typeof import('ant-design-vue/es')['TimePicker']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
+16 -4
View File
@@ -1,6 +1,18 @@
import type {TableInput} from "@core/type/baseTable";
import useAxios from "@core/utils/useAxios";
import type {PriceTriggers} from "@/model/priceTriggers";
import type { TableInput } from '@core/type/baseTable'
import useAxios from '@core/utils/useAxios'
import type { PriceTriggers } from '@/model/priceTriggers'
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>
<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>
<script setup lang="ts">
import IosInfoBadge from '@/pages/dashboard/components/IosInfoBadge.vue'
import { TOOLTIP_ABOVE_MODAL_Z_INDEX } from '@/constants/overlayZIndex'
withDefaults(
defineProps<{
text?: string
rootClass?: string
size?: 'xs' | 'sm' | 'md'
}>(),
{
text: '',
rootClass: '',
size: 'xs',
}
)
</script>
<style scoped lang="less">
.admin-field-help {
margin: 0;
font-size: 11px;
color: var(--admin-text-secondary);
line-height: 1.5;
display: inline-flex;
align-items: center;
line-height: 0;
cursor: help;
vertical-align: middle;
}
</style>
<style>
.ant-tooltip.admin-field-help-tooltip {
z-index: 1250 !important;
}
</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>
+424 -116
View File
@@ -1,104 +1,126 @@
<template>
<div class="p-4">
<AdminFieldHelp :text="bannersPageIntro" root-class="mb-4 text-xs md:text-sm" />
<BaseButton size="small" type="primary" @click="addRow">
<span>افزودن بنر</span>
<BaseIcon icon="vuesax-linear:add" size="17" />
</BaseButton>
<AdminFieldHelp
text="هر بار «افزودن بنر» یک اسلاید جدید به انتهای لیست اضافه می‌کند."
root-class="mt-2"
/>
<div class="my-4 flex flex-col gap-2">
<div
class="grid max-h-52vh grid-cols-1 gap-4 overflow-y-scroll md:max-h-75vh md:grid-cols-3"
>
<Card v-for="(item, index) in benefitsList" :key="index">
<div class="mt-3 grid grid-cols-1 gap-4">
<div class="my-2 flex items-center justify-between gap-2">
<div class="flex flex-col gap-0.5">
<div class="text-sm font-medium">نسبت تصویر ۱۶:۹</div>
<AdminFieldHelp :text="bannersAspectHelp" />
</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 class="text-center">
<AdminFieldHelp :text="bannersImageHelp" root-class="mb-2 text-center" />
<UploadPic
:compressed="true"
@upload="handleUpload($event, index, 'icon')"
@remove="handelRemoveFile(index, 'icon')"
:imageUrl="item.icon"
/>
</div>
</div>
<div class="mt-3 flex flex-col gap-1">
<div class="text-sm font-medium">انتخاب رویداد (پس از کلیک):</div>
<AdminFieldHelp :text="bannersEventHelp" />
<BasicSelect
v-model:value="item.typeClick"
:items="typeList"
value-name="lable"
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 v-if="item.typeClick == 'link'" class="mt-2 flex flex-col gap-1">
<div class="text-sm font-medium">آدرس لینک</div>
<BasicInput v-model:value="item.link" placeholder="https://..." />
</div>
<div v-if="item.typeClick == 'modal'" class="mt-2 flex flex-col gap-1">
<div class="text-sm font-medium">متن پیام</div>
<QuillEditor
placeholder="متن پیام"
v-model:content="item.modalText"
toolbar="full"
theme="snow"
content-type="html"
class="editor-box2"
/>
</div>
</Card>
<div class="banners-page">
<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>
<div class="banners-page__header-actions">
<BaseButton size="small" type="primary" @click="addRow">
<span>افزودن اسلاید</span>
<BaseIcon icon="vuesax-linear:add" size="17" />
</BaseButton>
<BaseButton type="primary" :loading="saving" @click="handleSubmit">
ذخیره تغییرات
</BaseButton>
</div>
</header>
<div class="mt-5 text-left md:mt-0">
<BaseButton type="primary" @click="handleSubmit">
<span>ذخیره تغییرات</span>
</BaseButton>
<AdminFieldHelp
text="بدون زدن این دکمه، هیچ‌کدام از تغییرات بالا در اپ مشتری دیده نمی‌شود."
root-class="mt-2"
/>
<div class="banners-page__body">
<div v-if="!benefitsList.length" class="banners-page__empty">
<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 }"
>
<header class="banner-card__head">
<div class="banner-card__head-start">
<span class="banner-card__index">{{ index + 1 }}</span>
<div>
<div class="banner-card__title">اسلاید {{ index + 1 }}</div>
<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>
<div class="banner-card__head-end">
<label class="banner-card__switch">
<span>نمایش</span>
<a-switch v-model:checked="item.show" size="small" />
<AdminFieldHelp :text="bannersShowHelp" />
</label>
<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 class="banner-card__fields" :class="{ 'banner-card__fields--stack': item.typeClick === 'modal' }">
<div class="banner-card__field">
<AdminLabelWithHelp label="بعد از کلیک مشتری" :help="clickEventHelp(item.typeClick)" />
<BasicSelect
v-model:value="item.typeClick"
:items="typeList"
value-name="lable"
key-name="value"
/>
</div>
<div v-if="item.typeClick === 'link'" class="banner-card__field banner-card__field--wide">
<AdminLabelWithHelp label="آدرس لینک" :help="bannersLinkHelp" />
<BasicInput v-model:value="item.link" placeholder="https://example.com" />
</div>
<div v-if="item.typeClick === 'modal'" class="banner-card__field banner-card__field--wide">
<AdminLabelWithHelp label="متن پیام" :help="bannersModalHelp" />
<a-textarea
:value="htmlToPlain(item.modalText)"
:rows="7"
class="banner-card__message"
placeholder="متن پیامی که مشتری در پنجرهٔ بازشو می‌بیند..."
@update:value="item.modalText = plainToHtml($event)"
/>
</div>
</div>
</div>
</article>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Card, message } from 'ant-design-vue'
import { message } from 'ant-design-vue'
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 AdminLabelWithHelp from '@/components/AdminLabelWithHelp.vue'
import { updateBannerConst, getHeaderBannerConst } from '@/api/consts'
import {
bannersAspectHelp,
bannersEventHelp,
bannersImageHelp,
bannersImageSectionHelp,
bannersLinkHelp,
bannersModalHelp,
bannersNoEventHelp,
@@ -106,19 +128,45 @@ import {
bannersShareHelp,
bannersShowHelp,
} 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 saving = ref(false)
let id: number | null = null
const typeList = ref([
{ lable: 'بدون رویداد', value: null },
{ lable: 'باز شدن لینک', value: 'link' },
{ lable: 'پنجره بازشو', value: 'modal' },
{ lable: 'اشتراک گذاری', value: 'share' },
{ lable: 'بدون رویداد', value: null },
{ lable: 'اشتراک‌گذاری', value: 'share' },
])
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[] {
if (!value) return []
try {
@@ -143,19 +191,8 @@ function addRow() {
})
}
function handleUpload(response: unknown, index: number, field: keyof IBenefits) {
const fileResource = normalizeUploadFileResponse(response)
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
function removeRow(index: number) {
benefitsList.value.splice(index, 1)
}
async function handleSubmit() {
@@ -165,6 +202,7 @@ async function handleSubmit() {
}
try {
saving.value = true
await updateBannerConst({
id,
value: JSON.stringify(benefitsList.value),
@@ -173,6 +211,8 @@ async function handleSubmit() {
message.success('اطلاعات با موفقیت ذخیره شد')
} catch (error) {
message.error('خطا در ذخیره اطلاعات')
} finally {
saving.value = false
}
}
@@ -191,18 +231,286 @@ onMounted(async () => {
})
</script>
<style lang="less">
.editor-box2 {
max-height: 40vh;
overflow-y: auto;
border: 1px solid #ddd;
border-radius: 8px;
padding: 10px;
<style scoped lang="less">
.banners-page {
display: flex;
flex-direction: column;
width: 100%;
min-width: 0;
height: 100%;
min-height: 0;
}
.editor-box2 .ql-editor {
min-height: 10vh;
.banners-page__header {
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;
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>
@@ -1,46 +1,57 @@
<template>
<a-form :model="form" ref="formRef" autocomplete="off">
<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" />
<AdminFieldHelp :text="giftCardMobileHelp" root-class="mt-1" />
</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="نام و نام خانوادگی مشتری" />
<AdminFieldHelp :text="giftCardNameHelp" root-class="mt-1" />
</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-button value="1">ریالی</a-radio-button>
<a-radio-button value="2">{{ metalLabel.goldWalletShort() }}</a-radio-button>
</a-radio-group>
<AdminFieldHelp :text="giftCardUnitHelp" root-class="mt-1" />
</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="تعداد کدهای کارت هدیه" />
<AdminFieldHelp :text="giftCardCountHelp" root-class="mt-1" />
</a-form-item>
<a-form-item
:label="`مقدار ${form.unit === '2' ? '(گرم)' : '(ریال)'}`"
name="amount"
:rules="inputRulesNumber()"
>
<a-form-item name="amount" :rules="inputRulesNumber()">
<template #label>
<AdminLabelWithHelp
:label="`مقدار ${form.unit === '2' ? '(گرم)' : '(ریال)'}`"
:help="giftCardAmountHelp"
/>
</template>
<BasicInput
v-model:value="form.amount"
:addon-before="form.unit === '2' ? 'گرم' : 'ریال'"
type="tel"
placeholder="مقدار کارت هدیه"
/>
<AdminFieldHelp :text="giftCardAmountHelp" root-class="mt-1" />
</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" />
<AdminFieldHelp :text="giftCardExpiryHelp" root-class="mt-1" />
</a-form-item>
<a-form-item label="توضیحات" name="description" class="mb-0">
@@ -53,7 +64,7 @@
<script setup lang="ts">
import type { FormInstance } from 'ant-design-vue'
import type { GiftcardInput } from '@/model/giftCard'
import AdminFieldHelp from '@/components/AdminFieldHelp.vue'
import AdminLabelWithHelp from '@/components/AdminLabelWithHelp.vue'
import {
giftCardAmountHelp,
giftCardCountHelp,
+17 -9
View File
@@ -34,21 +34,29 @@
>
<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-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="نام پویش" />
<AdminFieldHelp :text="discountNameHelp" root-class="mt-1" />
</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="%" />
<AdminFieldHelp :text="discountPercentHelp" root-class="mt-1" />
</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" />
<AdminFieldHelp :text="discountCountHelp" root-class="mt-1" />
</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" />
<AdminFieldHelp :text="discountExpiryHelp" root-class="mt-1" />
</a-form-item>
<a-form-item label="توضیحات" name="description">
<BasicTextArea v-model:value="discountForm.description" place-holder="توضیحات اختیاری — فقط برای یادداشت شما" />
@@ -115,7 +123,7 @@ import { isStoreApp } from '@/utils/config'
import { inputRules, inputRulesNumber } from '@/utils/inputRules'
import BasicInput from '@/components/BasicInput.vue'
import BasicTextArea from '@/components/BasicTextArea.vue'
import AdminFieldHelp from '@/components/AdminFieldHelp.vue'
import AdminLabelWithHelp from '@/components/AdminLabelWithHelp.vue'
import {
discountCountHelp,
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>
<div>
<BasicShowContext title="هشدار قیمت">
<AdminFieldHelp :text="priceTriggersPageIntro" root-class="mb-3 px-1" />
<BaseTable v-bind="tblProps" @register="submit" ref="tbl">
<!-- <template #extraHeader>
<BaseButton type="primary" size="small" class="flex" @click="emits('add')">
<div>افزودن مدیر</div>
<BaseIcon icon="vuesax-linear:user-add" Size="16px" />
<template #extra>
<div class="flex items-center gap-2">
<AdminFieldHelp :text="priceTriggersPageIntro" />
<BaseButton type="primary" size="small" @click="emits('add')">
<span>افزودن هشدار</span>
<BaseIcon icon="vuesax-linear:add" size="16" />
</BaseButton>
</template>-->
<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>
<Icon icon="vuesax-linear:stickynote" color="#10913A" />
</div>
</template>
<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 placement="bottom" class="cursor-pointer" @click="emits('edit', record)">
<template #title>
<span>ویرایش</span>
</template>
<Icon icon="vuesax-linear:edit" color="#4572E9" />
<a-tooltip placement="bottom">
<template #title>{{ record.is_active ? 'غیرفعال کردن' : 'فعال کردن' }}</template>
<Icon
:icon="record.is_active ? 'vuesax-linear:eye-slash' : 'vuesax-linear:eye'"
: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>
</div>
</template>
@@ -33,67 +58,29 @@
<script lang="tsx" setup>
import type { TableInput } from '@core/type/baseTable'
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 { changeUserActivation, getUserTable } from '@/api/user'
import { useUserStore } from '@/store/userStore'
import { cols } from '../hooks/priceTriggersColumns'
import { changeCustomerActivation } from '@/api/customer'
import { getPriceTriggersTable } from '@/api/priceTriggers'
import AdminFieldHelp from '@/components/AdminFieldHelp.vue'
import { priceTriggersPageIntro } from '@/utils/adminHelpCopy'
const [submit, reload] = useBaseTable()
//emits
const emits = defineEmits(['delete', 'edit', 'add', 'info'])
const emits = defineEmits<{
add: []
edit: [record: Record<string, unknown>]
toggle: [record: Record<string, unknown>]
delete: [id: number]
}>()
//define table
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>({
const tblProps = {
columns: cols,
api: (params: TableInput) => {
return getPriceTriggersTable(params)
}
//bordered: true
})
api: (params: TableInput) => getPriceTriggersTable(params),
}
const fetch = () => {
// console.log('sss')
function fetch() {
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
}
},
{
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: 'کد مشتری',
dataIndex: 'customer_id',
@@ -203,6 +213,15 @@ export const cols: BaseTableColumn[] = [
{
title: 'روش ارسال',
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,
},
]
+129 -214
View File
@@ -1,248 +1,163 @@
<template>
<div>
<!-- <BasicMainCard>-->
<div class="flex w-full h-full">
<tbl ref="table" class="w-full h-full" @info="info" @edit="edit" @add="add" />
<div class="flex h-full w-full">
<tbl ref="tableRef" class="h-full w-full" @add="openAdd" @edit="openEdit" @toggle="toggleActive" @delete="deleteItem" />
<!-- <BasicMenu :menu="menu" width="13rem" @onChange="tabSelected" />
<component :is="selectedComponent" class="mr-5" />-->
<!-- <BasicShowContext title="پروژه ها"> <Search /> </BasicShowContext>-->
</div>
<!-- </BasicMainCard>-->
<!-- <BasicModal
v-model:visible="addUserVisible"
width="40rem"
:title="`${editId ? 'ویرایش' : 'افزودن'} کاربر`"
@ok="ok"
<BasicActionSheet
v-model:visible="editorVisible"
:title="sheetTitle"
modal-width="32rem"
drawer-height="85vh"
:loading="loading"
@close="addUserVisible = false"
>-->
<!-- <a-form :model="form" ref="formRef" autocomplete="off">
<div class="grid md:grid-cols-2 gap-4">
<a-form-item label="نام" name="fname" :rules="inputRules()">
<BasicInput v-model:value="form.fname" />
</a-form-item>
<a-form-item label="نام خانوادگی" name="lname" :rules="inputRules()">
<BasicInput v-model:value="form.lname" />
</a-form-item>
<a-form-item label="شماره همراه" name="mobile" :rules="inputLenRules(11)">
<BasicInput v-model:value="form.mobile" type="number" />
</a-form-item>
<a-form-item v-if="editId" label="رمز عبور" name="password">
<BasicInput v-model:value="form.password" is-password type="number" />
</a-form-item>
<a-form-item v-if="editId" label="تکرار رمز عبور">
<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"
/>-->
:show-button="true"
:show-cancel="true"
:show-ok="true"
ok-text="ذخیره"
cancel-text="انصراف"
@ok="submitForm"
@close="closeEditor"
@cancel="closeEditor"
>
<div v-if="isMobile" class="mb-3 text-base font-bold">{{ sheetTitle }}</div>
<PriceTriggerEditorFormBody
ref="editorRef"
:form="form"
:customer-items="customerItems"
:edit-id="editId"
/>
</BasicActionSheet>
</div>
</template>
<script setup lang="ts">
// import BasicInfoModal from '@/components/BasicInfoModal'
import { inputLenRules, inputRules } from '@/utils/inputRules'
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 { computed, onMounted, ref } from 'vue'
import { message } from 'ant-design-vue'
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 loading = ref<boolean>(false)
const tableRef = ref<null | { fetch: () => void }>(null)
const editorRef = ref<{
validateFields?: () => Promise<unknown>
resetFields?: () => void
}>()
//define store
const store = useStaticApiListsStore()
const loading = ref(false)
const editorVisible = ref(false)
const editId = ref<number | undefined>()
const customers = ref<Customer[]>([])
//define tbl
const table = ref(null)
const refreshTable = () => table?.value?.fetch()
let updateKey = ref<number>(0)
//role confirm items
const confirmItems: Array<Item> = [
{
value: 'بله',
key: true
},
{
value: 'خیر',
key: false
}
]
//form
const form = ref<User>({
active: true
const defaultForm = (): Partial<PriceTriggers> => ({
customer_id: undefined,
target_price: undefined,
price_condition: '>',
action: 'sms',
is_active: true,
})
const formRef = ref<FormInstance>()
//info
let infoParams = reactive<Info>({})
const form = ref<Partial<PriceTriggers>>(defaultForm())
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
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()
}
//edit
let editId: number | undefined = undefined
async function edit(record: User) {
addUserVisible.value = true
editId = record.id
form.value = cloneDeep(record)
function closeEditor() {
editorVisible.value = false
}
//add
async function add() {
loading.value = true
editId = undefined
// await store.getUsersList()
addUserVisible.value = true
updateKey.value++
resetData()
loading.value = false
function openAdd() {
editId.value = undefined
form.value = defaultForm()
editorRef.value?.resetFields?.()
editorVisible.value = true
}
//reset data
const resetData = () => {
formRef.value?.resetFields()
form.value = {
fname: '',
lname: '',
active: false,
avatar_id: undefined,
sts: undefined,
id: undefined,
mobile: undefined
}
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
}
//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) {
async function submitForm() {
try {
await editorRef.value?.validateFields?.()
loading.value = true
// table?.value?.spinTable(true)
await deleteUser(id)
message.success('با موفقیت حذف شد')
table?.value?.fetch()
const payload = {
customer_id: Number(form.value.customer_id),
target_price: Number(form.value.target_price),
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 {
await createPriceTrigger(payload)
}
message.success('هشدار با موفقیت ذخیره شد')
editorVisible.value = false
refreshTable()
} catch (e) {
console.log(e)
message.error('خطا در حذف')
console.error(e)
} 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 {
responsibleItems: Array<User> | undefined
users: Array<Const> | undefined
}
//add project modal
const addUserVisible = ref<boolean>(false)
const userInfoVisible = ref<boolean>(false)
async function toggleActive(record: PriceTriggers) {
if (!record.id) return
//submit user
async function ok() {
formRef.value?.validateFields().then(async () => {
// console.log('salam', form.value)
// return
try {
loading.value = true
try {
if (editId) {
if (form.value.password) {
if (form.value.password === form.value.repassword) {
await updateUser(editId, form.value)
} else {
message.error('پسورد وارد شده با تکرار پسورد وارد شده یکسان نیست')
return
}
} else {
await updateUser(editId, form.value)
}
} else {
await createUser(form.value)
}
addUserVisible.value = false
refreshTable()
resetData()
message.success('عملیات با موفقیت انجام شد')
// await getResponsible()
// await getQC()
// await getManager()
} catch (e) {
console.error(e)
// message.error('خطا در بارگذاری اطلاعات')
} finally {
loading.value = false
}
})
await togglePriceTriggerActive(record.id)
message.success(record.is_active ? 'هشدار غیرفعال شد' : 'هشدار فعال شد')
refreshTable()
} catch (e) {
console.error(e)
message.error('خطا در تغییر وضعیت')
} finally {
loading.value = false
}
}
</script>
<style scoped></style>
function deleteItem(id: number) {
confirmDeleteItem('هشدار قیمت', id, deletePriceTrigger, refreshTable)
}
onMounted(async () => {
try {
customers.value = (await getCustomersList()) ?? []
} catch (e) {
console.error(e)
}
})
</script>
@@ -2,7 +2,9 @@
<div class="h-full min-h-0 min-w-0 max-w-full overflow-hidden">
<BasicShowContext title="تاریخچه قیمت">
<template #extra>
<BaseButton
<div class="flex items-center gap-2">
<AdminFieldHelp :text="pricingPageIntro" />
<BaseButton
v-if="isMobile"
type="primary"
size="small"
@@ -12,14 +14,15 @@
>
به‌روزرسانی قیمت
</BaseButton>
</div>
</template>
<AdminFieldHelp :text="pricingPageIntro" root-class="mb-3 px-1" />
<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">
<template #extraHeader>
<BaseButton
<div class="flex items-center gap-2">
<AdminFieldHelp :text="pricingPageIntro" />
<BaseButton
type="primary"
size="small"
icon="vuesax-linear:refresh"
@@ -28,6 +31,7 @@
>
به‌روزرسانی قیمت
</BaseButton>
</div>
</template>
<template #action="{ record }">
<div class="flex justify-center gap-2">
+5 -2
View File
@@ -10,7 +10,10 @@ export const bannersAspectHelp =
'عکس باید افقی و با نسبت ۱۶:۹ باشد (مثل تصویر تلویزیون). اگر بلند و باریک یا مربع باشد، در اپ کشیده یا بریده می‌شود.'
export const bannersImageHelp =
'روی «افزودن عکس» بزنید و یک تصویر با کیفیت مناسب انتخاب کنید. آیکون سطل زباله کل این بنر را از لیست حذف می‌کند.'
'روی «افزودن عکس» بزنید. آیکون سطل زباله فقط تصویر را پاک می‌کند؛ برای حذف کل اسلاید از دکمهٔ «حذف اسلاید» استفاده کنید.'
export const bannersImageSectionHelp =
'عکس افقی با نسبت ۱۶:۹ (مثل تلویزیون) بهترین نتیجه را می‌دهد. تصویر نامناسب در اپ کشیده یا بریده می‌شود.'
export const bannersEventHelp =
'وقتی مشتری روی بنر ضربه می‌زند چه اتفاقی بیفتد؟ اگر مطمئن نیستید «بدون رویداد» را بگذارید.'
@@ -61,4 +64,4 @@ export const discountExpiryHelp =
'بعد از این تاریخ کدهای این پویش دیگر کار نمی‌کنند.'
export const priceTriggersPageIntro =
'لیست درخواست‌های «هشدار قیمت» مشتریان. وقتی قیمت به عددی که مشتری گفته برسد، به او اطلاع داده می‌شود. از آیکون مداد می‌توانید وضعیت را ببینید یا ویرایش کنید.'
'لیست هشدارهای قیمت مشتریان. می‌توانید هشدار جدید بسازید، ویرایش کنید، فعال/غیرفعال کنید یا حذف کنید. وقتی قیمت به هدف برسد، به مشتری پیامک یا ایمیل می‌رود و هشدار خودکار غیرفعال می‌شود.'