change support setting

This commit is contained in:
2026-07-06 18:15:57 +02:00
15 changed files with 1187 additions and 399 deletions
+3
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']
@@ -52,6 +54,7 @@ declare module '@vue/runtime-core' {
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}`)
+60
View File
@@ -0,0 +1,60 @@
<template>
<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 {
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>
+440 -89
View File
@@ -1,84 +1,172 @@
<template>
<div class="p-4">
<BaseButton size="small" type="primary" @click="addRow">
<span> افزودن بنر</span>
<BaseIcon icon="vuesax-linear:add" size="17"/>
</BaseButton>
<div class="flex my-4 flex-col gap-2">
<div class="grid md:grid-cols-3 grid-cols-1 gap-4 overflow-y-scroll max-h-52vh md:max-h-75vh " >
<Card v-for="(item, index) in benefitsList">
<div class="grid grid-cols-1 gap-4 mt-3">
<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 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="flex items-center justify-between gap-2 my-2">
<div class="text">(16 در 9)</div>
<div class="flex items-center gap-2">
<span>نمایش:</span>
<a-switch v-model:checked="item.show"/>
</div>
</div>
<div class="text-center">
<UploadPic
:compressed="true"
@upload="handleUpload($event, index, 'icon')"
@remove="handelRemoveFile(index, 'icon')"
:imageUrl="item.icon"
/>
</div>
</div>
<div class="flex items-center justify-between ">
<div class="w-full gap-1 flex flex-col ">
<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>
<BasicSelect v-model:value="item.typeClick" :items="typeList" value-name="lable" key-name="value"></BasicSelect>
</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>
<div v-if="item.typeClick=='link'" class="flex flex-col gap-1 mt-1">
<div>آدرس لینک</div>
<BasicInput v-model:value="item.link"></BasicInput>
</div>
<div class="flex flex-col gap-1" v-if="item.typeClick=='modal'">
<div>متن پیام</div>
<QuillEditor
placeholder=" متن پیام"
v-model:content="item.modalText"
toolbar="full"
theme="snow"
content-type="html"
class="editor-box2"
/>
</div>
</Card>
</article>
</div>
</div>
<div class="text-left mt-5 md:mt-0">
<BaseButton type="primary" @click="handleSubmit">
<span>ذخیره تغییرات</span>
</BaseButton>
</div>
</div>
</template>
<script setup lang="ts">
import {Card, message} from "ant-design-vue"
import type {IBenefits} from "@/model/banner"
import UploadPic from "@/components/UploadPic.vue"
import {updateBannerConst, getHeaderBannerConst} from "@/api/consts"
import '@vueup/vue-quill/dist/vue-quill.snow.css'
import {QuillEditor} from '@vueup/vue-quill'
import {normalizeUploadFileResponse} from '@/utils/file'
import { message } from 'ant-design-vue'
import type { IBenefits } from '@/model/banner'
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 {
bannersEventHelp,
bannersImageSectionHelp,
bannersLinkHelp,
bannersModalHelp,
bannersNoEventHelp,
bannersPageIntro,
bannersShareHelp,
bannersShowHelp,
} from '@/utils/adminHelpCopy'
const benefitsList = ref<IBenefits[]>([])
const saving = ref(false)
let id: number | null = null
const typeList = ref([
{lable: 'باز شدن لینک', value: 'link'},
{lable: ' پنجره بازشو', value: 'modal'},
{lable: 'اشتراک گذاری', value: 'share'},
{lable: 'بدون رویداد', value: null},
const typeList = ref([
{ lable: 'بدون رویداد', value: null },
{ lable: 'باز شدن لینک', value: 'link' },
{ lable: 'پنجره بازشو', value: 'modal' },
{ 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 {
@@ -93,28 +181,20 @@ function addRow() {
if (!Array.isArray(benefitsList.value)) {
benefitsList.value = []
}
benefitsList.value.push({icon: null, show: true, caption: null, link: null, typeClick: null, modalText: null})
benefitsList.value.push({
icon: null,
show: true,
caption: null,
link: null,
typeClick: null,
modalText: null,
})
}
function removeRow(index: number) {
benefitsList.value.splice(index, 1)
}
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
}
async function handleSubmit() {
if (!id) {
message.error('شناسه بنر یافت نشد؛ صفحه را رفرش کنید')
@@ -122,6 +202,7 @@ async function handleSubmit() {
}
try {
saving.value = true
await updateBannerConst({
id,
value: JSON.stringify(benefitsList.value),
@@ -130,6 +211,8 @@ async function handleSubmit() {
message.success('اطلاعات با موفقیت ذخیره شد')
} catch (error) {
message.error('خطا در ذخیره اطلاعات')
} finally {
saving.value = false
}
}
@@ -148,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>
@@ -277,6 +277,8 @@ export function switchLabels(key: string | undefined): { on: string; off: string
const KEY_HELP_FALLBACK: Record<string, string> = {
name_fa:
'نام فارسی برند که در پیامک‌ها، عنوان صفحات و بخش‌های عمومی اپ به مشتری نشان داده می‌شود.',
banner_enabled:
'روشن: منوی «بنرها» در تنظیمات فعال می‌شود و می‌توانید اسلایدر بالای صفحهٔ اصلی اپ را مدیریت کنید. خاموش: بنرها در اپ نشان داده نمی‌شوند.',
open_register:
'روشن: کاربران جدید می‌توانند ثبت‌نام کنند. خاموش: فقط ورود کاربران قبلی مجاز است.',
sms_panel:
@@ -360,9 +362,9 @@ const KEY_HELP_FALLBACK: Record<string, string> = {
node_shared_secret:
'کلید مشترک امضای درخواست‌ها به سرویس سفارش خارجی. محرمانه نگه دارید.',
support_messenger:
'پیام‌رسانی که دکمه پشتیبانی در اپ مشتری (درباره ما، قوانین، آموزش، دکمه شناور و راهنمای واریز) به آن هدایت می‌کند.',
'پیام‌رسانی که آیکون کناری و دکمه شناور پشتیبانی در اپ مشتری (درباره ما، قوانین، آموزش و راهنمای واریز) به آن هدایت می‌کند.',
support_messenger_id:
'شناسه تماس پشتیبانی: یوزرنیم، شماره یا آیدی بسته به پیام‌رسان انتخاب‌شده. روی دکمه «تماس با پشتیبانی» و آیکون پیام‌رسان اعمال می‌شود.',
'شناسه تماس در پیام‌رسان انتخاب‌شده: یوزرنیم، آیدی یا لینک کامل. فقط روی آیکون پیام‌رسان و دکمه شناور اعمال می‌شود؛ تماس تلفنی از فیلد «شماره پشتیبانی» خوانده می‌شود.',
support_messenger_text:
'متن راهنمای پشتیبانی در مودال شارژ حساب (واریز).',
deposit_sms_support_number:
@@ -1,30 +1,44 @@
<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" />
</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="نام و نام خانوادگی مشتری" />
</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>
</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="تعداد کدهای کارت هدیه" />
</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' ? 'گرم' : 'ریال'"
@@ -33,12 +47,15 @@
/>
</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" />
</a-form-item>
<a-form-item label="توضیحات" name="description" class="mb-0">
<BasicTextArea v-model:value="form.description" place-holder="توضیحات اختیاری" />
<BasicTextArea v-model:value="form.description" place-holder="توضیحات اختیاری — فقط برای یادداشت شما در پنل" />
</a-form-item>
</div>
</a-form>
@@ -47,6 +64,15 @@
<script setup lang="ts">
import type { FormInstance } from 'ant-design-vue'
import type { GiftcardInput } from '@/model/giftCard'
import AdminLabelWithHelp from '@/components/AdminLabelWithHelp.vue'
import {
giftCardAmountHelp,
giftCardCountHelp,
giftCardExpiryHelp,
giftCardMobileHelp,
giftCardNameHelp,
giftCardUnitHelp,
} from '@/utils/adminHelpCopy'
import { inputLenRules, inputRules, inputRulesNumber } from '@/utils/inputRules'
import { metalLabel } from '@/utils/adminMetalLabels'
@@ -58,6 +84,6 @@ const formRef = ref<FormInstance>()
defineExpose({
validateFields: () => formRef.value?.validateFields(),
resetFields: () => formRef.value?.resetFields()
resetFields: () => formRef.value?.resetFields(),
})
</script>
+24 -5
View File
@@ -34,20 +34,32 @@
>
<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="نام پویش" />
</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="%" />
</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" />
</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" />
</a-form-item>
<a-form-item label="توضیحات" name="description">
<BasicTextArea v-model:value="discountForm.description" place-holder="توضیحات اختیاری" />
<BasicTextArea v-model:value="discountForm.description" place-holder="توضیحات اختیاری — فقط برای یادداشت شما" />
</a-form-item>
</a-form>
</BasicActionSheet>
@@ -111,6 +123,13 @@ import { isStoreApp } from '@/utils/config'
import { inputRules, inputRulesNumber } from '@/utils/inputRules'
import BasicInput from '@/components/BasicInput.vue'
import BasicTextArea from '@/components/BasicTextArea.vue'
import AdminLabelWithHelp from '@/components/AdminLabelWithHelp.vue'
import {
discountCountHelp,
discountExpiryHelp,
discountNameHelp,
discountPercentHelp,
} from '@/utils/adminHelpCopy'
import BaseDatePicker from '@core/components/BaseDatePicker'
//loading
@@ -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,26 +1,45 @@
<template>
<div>
<BasicShowContext title="هشدار قیمت">
<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 #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>
@@ -32,65 +51,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 { 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,12 +14,15 @@
>
به‌روزرسانی قیمت
</BaseButton>
</div>
</template>
<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"
@@ -26,6 +31,7 @@
>
به‌روزرسانی قیمت
</BaseButton>
</div>
</template>
<template #action="{ record }">
<div class="flex justify-center gap-2">
@@ -58,6 +64,8 @@ import { storePriceColumns } from '../hooks/storePriceColumns'
import { usePrefix } from '@core/composable/usePrefix'
import { getPriceSyncLogTable } from '@/api/price'
import PriceMobileCardList from './PriceMobileCardList.vue'
import AdminFieldHelp from '@/components/AdminFieldHelp.vue'
import { pricingPageIntro } from '@/utils/adminHelpCopy'
import { isMobile } from '@core/utils/is'
import type { PriceSyncLog } from '@/model/priceSyncLog'
+67
View File
@@ -0,0 +1,67 @@
/** توضیحات ساده فارسی برای فرم‌های ادمین — قابل فهم بدون دانش فنی */
export const bannersPageIntro =
'بنرها همان تصاویر اسلایدر بالای صفحهٔ اصلی اپ مشتری هستند. هر کارت = یک اسلاید. بعد از هر تغییر حتماً «ذخیره تغییرات» را بزنید؛ تا آن لحظه در اپ دیده نمی‌شود.'
export const bannersShowHelp =
'روشن = این بنر در اپ مشتری نشان داده می‌شود. خاموش = موقتاً مخفی می‌شود (حذف نمی‌شود).'
export const bannersAspectHelp =
'عکس باید افقی و با نسبت ۱۶:۹ باشد (مثل تصویر تلویزیون). اگر بلند و باریک یا مربع باشد، در اپ کشیده یا بریده می‌شود.'
export const bannersImageHelp =
'روی «افزودن عکس» بزنید. آیکون سطل زباله فقط تصویر را پاک می‌کند؛ برای حذف کل اسلاید از دکمهٔ «حذف اسلاید» استفاده کنید.'
export const bannersImageSectionHelp =
'عکس افقی با نسبت ۱۶:۹ (مثل تلویزیون) بهترین نتیجه را می‌دهد. تصویر نامناسب در اپ کشیده یا بریده می‌شود.'
export const bannersEventHelp =
'وقتی مشتری روی بنر ضربه می‌زند چه اتفاقی بیفتد؟ اگر مطمئن نیستید «بدون رویداد» را بگذارید.'
export const bannersLinkHelp =
'آدرس کامل صفحه‌ای که باید باز شود. مثال: https://bazretala.ir یا tel:02112345678'
export const bannersModalHelp =
'متنی که داخل خود اپ به مشتری نشان داده می‌شود. می‌توانید بولد، لیست و لینک هم بگذارید.'
export const bannersShareHelp =
'با انتخاب این گزینه، منوی اشتراک‌گذاری گوشی مشتری باز می‌شود (واتساپ، تلگرام و...). نیازی به پر کردن فیلد دیگر نیست.'
export const bannersNoEventHelp =
'فقط تصویر دیده می‌شود؛ کلیک هیچ کاری نمی‌کند. برای بنرهای تزئینی مناسب است.'
export const pricingPageIntro =
'اینجا تاریخچهٔ همگام‌سازی قیمت را می‌بینید. دکمهٔ «به‌روزرسانی قیمت» همان لحظه قیمت را از منبعی که در تنظیمات مشخص کرده‌اید می‌گیرد و روی محصولات اعمال می‌کند. اگر قیمت عوض نشده باشد پیام «تغییری نکرد» می‌آید.'
export const giftCardMobileHelp =
'شماره موبایلی که کارت هدیه برایش ساخته می‌شود. باید ۱۱ رقم و با ۰۹ شروع شود.'
export const giftCardNameHelp =
'نام مشتری فقط برای شناسایی در پنل شماست؛ روی اپ مشتری تأثیر مستقیم ندارد.'
export const giftCardUnitHelp =
'ریالی = اعتبار کیف پول (ریال). طلا = وزن طلا به گرم که به کیف مشتری واریز می‌شود.'
export const giftCardCountHelp =
'چند کد یکتا ساخته شود. هر کد فقط یک‌بار قابل استفاده است.'
export const giftCardAmountHelp =
'مقدار هر کد: اگر ریالی انتخاب کردید مبلغ به ریال؛ اگر طلا انتخاب کردید وزن به گرم.'
export const giftCardExpiryHelp =
'بعد از این تاریخ کدها دیگر قابل استفاده نیستند. تاریخ را با دقت انتخاب کنید.'
export const discountNameHelp =
'نامی که در لیست پویش‌های تخفیف در پنل می‌بینید؛ فقط برای مدیریت داخلی است.'
export const discountPercentHelp =
'درصد تخفیف روی سفارش. مثلاً ۱۰ یعنی ده درصد کمتر از مبلغ اصلی.'
export const discountCountHelp =
'چند کد تخفیف یکتا ساخته شود. هر کد یک‌بار مصرف می‌شود.'
export const discountExpiryHelp =
'بعد از این تاریخ کدهای این پویش دیگر کار نمی‌کنند.'
export const priceTriggersPageIntro =
'لیست هشدارهای قیمت مشتریان. می‌توانید هشدار جدید بسازید، ویرایش کنید، فعال/غیرفعال کنید یا حذف کنید. وقتی قیمت به هدف برسد، به مشتری پیامک یا ایمیل می‌رود و هشدار خودکار غیرفعال می‌شود.'