setting changed 3
This commit is contained in:
Vendored
+4
@@ -15,13 +15,16 @@ declare module '@vue/runtime-core' {
|
|||||||
AButton: typeof import('ant-design-vue/es')['Button']
|
AButton: typeof import('ant-design-vue/es')['Button']
|
||||||
ACard: typeof import('ant-design-vue/es')['Card']
|
ACard: typeof import('ant-design-vue/es')['Card']
|
||||||
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
|
ACheckbox: typeof import('ant-design-vue/es')['Checkbox']
|
||||||
|
ACol: typeof import('ant-design-vue/es')['Col']
|
||||||
AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
|
AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
|
||||||
ADivider: typeof import('ant-design-vue/es')['Divider']
|
ADivider: typeof import('ant-design-vue/es')['Divider']
|
||||||
ADrawer: typeof import('ant-design-vue/es')['Drawer']
|
ADrawer: typeof import('ant-design-vue/es')['Drawer']
|
||||||
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
|
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
|
||||||
|
AEmpty: typeof import('ant-design-vue/es')['Empty']
|
||||||
AForm: typeof import('ant-design-vue/es')['Form']
|
AForm: typeof import('ant-design-vue/es')['Form']
|
||||||
AFormItem: typeof import('ant-design-vue/es')['FormItem']
|
AFormItem: typeof import('ant-design-vue/es')['FormItem']
|
||||||
AInput: typeof import('ant-design-vue/es')['Input']
|
AInput: typeof import('ant-design-vue/es')['Input']
|
||||||
|
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
|
||||||
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
|
AInputPassword: typeof import('ant-design-vue/es')['InputPassword']
|
||||||
ALayout: typeof import('ant-design-vue/es')['Layout']
|
ALayout: typeof import('ant-design-vue/es')['Layout']
|
||||||
ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent']
|
ALayoutContent: typeof import('ant-design-vue/es')['LayoutContent']
|
||||||
@@ -36,6 +39,7 @@ declare module '@vue/runtime-core' {
|
|||||||
ARadio: typeof import('ant-design-vue/es')['Radio']
|
ARadio: typeof import('ant-design-vue/es')['Radio']
|
||||||
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
|
ARadioButton: typeof import('ant-design-vue/es')['RadioButton']
|
||||||
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
|
ARadioGroup: typeof import('ant-design-vue/es')['RadioGroup']
|
||||||
|
ARow: typeof import('ant-design-vue/es')['Row']
|
||||||
ASelect: typeof import('ant-design-vue/es')['Select']
|
ASelect: typeof import('ant-design-vue/es')['Select']
|
||||||
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
|
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
|
||||||
ASpin: typeof import('ant-design-vue/es')['Spin']
|
ASpin: typeof import('ant-design-vue/es')['Spin']
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { Const } from '@/model/const'
|
import type { Const } from '@/model/configuration'
|
||||||
import useAxios from '@core/utils/useAxios'
|
import useAxios from "@core/utils/useAxios"
|
||||||
|
|
||||||
export const setConfiguration = async (obj: Const) => await useAxios.post<Const>(`/admin/get_settings`, obj)
|
export const setConfiguration = async (obj: Const) => await useAxios.post<Const>(`/admin/get_settings`, obj)
|
||||||
|
|
||||||
export const getConfiguration = async () =>
|
export const getConfiguration = async () =>
|
||||||
await useAxios.get<Array<Const>>(`/admin/get_settings`)
|
await useAxios.get<Array<Const>>(`/admin/get_settings`, { params: { editable_only: 1 } })
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
<div class="truncate text-black font-bold">{{ props.title }}</div>
|
<div class="truncate text-black font-bold">{{ props.title }}</div>
|
||||||
<slot name="extra" />
|
<slot name="extra" />
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2 min-h-0 flex-1 overflow-hidden p-1">
|
<div class="mt-2 min-h-0 flex-1 overflow-x-hidden overflow-y-auto p-1">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,17 +3,10 @@ export interface Const {
|
|||||||
key?: string
|
key?: string
|
||||||
value?: string | undefined
|
value?: string | undefined
|
||||||
description?: string
|
description?: string
|
||||||
type: string
|
title_fa?: string
|
||||||
title_fa: string
|
type?: string
|
||||||
}
|
is_active: boolean | number
|
||||||
|
can_edit?: number | boolean
|
||||||
export interface SupportLink {
|
|
||||||
show_to_users_goftino: string
|
|
||||||
show_to_users_eitaa: string
|
|
||||||
show_to_users_telegram: string
|
|
||||||
show_to_users_whatsapp: string
|
|
||||||
show_to_users_bale: string
|
|
||||||
show_to_users_phone: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface itemConfig {
|
export interface itemConfig {
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="setting-card"
|
||||||
|
:class="{
|
||||||
|
'setting-card--readonly': readonly,
|
||||||
|
'setting-card--saving': saving,
|
||||||
|
'setting-card--inactive': !isActive && fieldType !== 'switch'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="setting-card__head">
|
||||||
|
<div class="setting-card__title-row">
|
||||||
|
<span v-if="readonly" class="setting-card__lock" title="فقطخواندنی">🔒</span>
|
||||||
|
<span class="setting-card__title">{{ title }}</span>
|
||||||
|
<a-switch
|
||||||
|
v-if="fieldType !== 'switch'"
|
||||||
|
:checked="isActive"
|
||||||
|
size="small"
|
||||||
|
class="setting-card__active-switch"
|
||||||
|
@change="onActiveChange"
|
||||||
|
>
|
||||||
|
<template #checkedChildren>فعال</template>
|
||||||
|
<template #unCheckedChildren>غیرفعال</template>
|
||||||
|
</a-switch>
|
||||||
|
</div>
|
||||||
|
<div class="setting-card__key">کلید: {{ settingKey }}</div>
|
||||||
|
<div v-if="help" class="setting-card__help">{{ help }}</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="setting-card__body"
|
||||||
|
:class="{ 'setting-card__body--switch': fieldType === 'switch' }"
|
||||||
|
>
|
||||||
|
<SettingValueField
|
||||||
|
:model-value="modelValue"
|
||||||
|
:setting-key="settingKey"
|
||||||
|
:setting-type="settingType"
|
||||||
|
:description="description"
|
||||||
|
:disabled="readonly"
|
||||||
|
:rows="fieldType === 'longtext' ? 4 : 6"
|
||||||
|
@update:value="emit('update:value', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="saving" class="setting-card__status">
|
||||||
|
<a-spin size="small" />
|
||||||
|
<span>در حال ذخیره...</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="savedFlash" class="setting-card__status setting-card__status--ok">ذخیره شد</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import SettingValueField from './SettingValueField.vue'
|
||||||
|
import { helpText, resolveSettingFieldType, type SettingFieldType } from './settingTypes'
|
||||||
|
import type { Const } from '@/model/configuration'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
row: Const
|
||||||
|
modelValue: string
|
||||||
|
readonly?: boolean
|
||||||
|
saving?: boolean
|
||||||
|
savedFlash?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:value': [value: string]
|
||||||
|
'update:isActive': [value: boolean]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const settingKey = computed(() => props.row.key ?? '')
|
||||||
|
const settingType = computed(() => props.row.type)
|
||||||
|
const description = computed(() => props.row.description)
|
||||||
|
const title = computed(() => props.row.title_fa || props.row.key || '—')
|
||||||
|
|
||||||
|
const fieldType = computed<SettingFieldType>(() =>
|
||||||
|
resolveSettingFieldType({ key: props.row.key, type: props.row.type })
|
||||||
|
)
|
||||||
|
|
||||||
|
const isActive = computed(() => props.row.is_active !== false && props.row.is_active !== 0)
|
||||||
|
|
||||||
|
function onActiveChange(checked: boolean) {
|
||||||
|
emit('update:isActive', checked)
|
||||||
|
}
|
||||||
|
|
||||||
|
const help = computed(() => helpText(props.row))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.setting-card {
|
||||||
|
border: 1px solid #e8e8e8;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: #fff;
|
||||||
|
min-height: 108px;
|
||||||
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&:hover:not(.setting-card--readonly) {
|
||||||
|
border-color: #91caff;
|
||||||
|
box-shadow: 0 2px 8px rgba(22, 119, 255, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--readonly {
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--saving {
|
||||||
|
border-color: #91caff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--inactive {
|
||||||
|
opacity: 0.72;
|
||||||
|
background: #fafafa;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__head {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__active-switch {
|
||||||
|
margin-right: auto;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__lock {
|
||||||
|
flex-shrink: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__key {
|
||||||
|
margin-top: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
direction: ltr;
|
||||||
|
text-align: right;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__help {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__body {
|
||||||
|
margin-top: 12px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
&--switch {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__status {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-height: 18px;
|
||||||
|
|
||||||
|
&--ok {
|
||||||
|
color: #52c41a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
<template>
|
||||||
|
<div class="setting-value-field">
|
||||||
|
<a-switch
|
||||||
|
v-if="fieldType === 'switch'"
|
||||||
|
:checked="switchChecked"
|
||||||
|
:disabled="disabled"
|
||||||
|
@change="onSwitchChange"
|
||||||
|
>
|
||||||
|
<template #checkedChildren>فعال</template>
|
||||||
|
<template #unCheckedChildren>غیرفعال</template>
|
||||||
|
</a-switch>
|
||||||
|
|
||||||
|
<a-select
|
||||||
|
v-else-if="fieldType === 'select'"
|
||||||
|
:value="modelValue || undefined"
|
||||||
|
:disabled="disabled"
|
||||||
|
allow-clear
|
||||||
|
placeholder="انتخاب کنید"
|
||||||
|
class="w-full"
|
||||||
|
@update:value="onSelectChange"
|
||||||
|
>
|
||||||
|
<a-select-option v-for="opt in selectItems" :key="opt.key" :value="opt.key">
|
||||||
|
{{ opt.value }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
|
||||||
|
<a-input-number
|
||||||
|
v-else-if="fieldType === 'number'"
|
||||||
|
:value="numberValue"
|
||||||
|
:disabled="disabled"
|
||||||
|
class="w-full"
|
||||||
|
:formatter="(v: string) => `${v}`.replace(/\B(?=(\d{3})+(?!\d))/g, ',')"
|
||||||
|
:parser="(v: string) => v.replace(/,/g, '')"
|
||||||
|
@update:value="onNumberChange"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<a-textarea
|
||||||
|
v-else-if="fieldType === 'longtext'"
|
||||||
|
:value="modelValue"
|
||||||
|
:disabled="disabled"
|
||||||
|
:rows="rows"
|
||||||
|
class="w-full font-mono text-sm"
|
||||||
|
@update:value="emit('update:value', $event)"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<a-input
|
||||||
|
v-else
|
||||||
|
:value="modelValue"
|
||||||
|
:disabled="disabled"
|
||||||
|
allow-clear
|
||||||
|
@update:value="emit('update:value', $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
isSwitchOn,
|
||||||
|
parseSelectOptions,
|
||||||
|
resolveSettingFieldType,
|
||||||
|
switchToString,
|
||||||
|
type SettingFieldType
|
||||||
|
} from './settingTypes'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
modelValue: string
|
||||||
|
settingKey?: string
|
||||||
|
settingType?: string
|
||||||
|
description?: string
|
||||||
|
disabled?: boolean
|
||||||
|
rows?: number
|
||||||
|
}>(),
|
||||||
|
{ disabled: false, rows: 10 }
|
||||||
|
)
|
||||||
|
|
||||||
|
const emit = defineEmits<{ 'update:value': [value: string] }>()
|
||||||
|
|
||||||
|
const fieldType = computed<SettingFieldType>(() =>
|
||||||
|
resolveSettingFieldType({
|
||||||
|
key: props.settingKey,
|
||||||
|
type: props.settingType
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const selectItems = computed(() =>
|
||||||
|
parseSelectOptions({
|
||||||
|
key: props.settingKey,
|
||||||
|
description: props.description,
|
||||||
|
type: props.settingType
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const switchChecked = computed(() => isSwitchOn(props.modelValue))
|
||||||
|
|
||||||
|
function onSwitchChange(checked: boolean) {
|
||||||
|
emit('update:value', switchToString(checked))
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSelectChange(val: string | undefined) {
|
||||||
|
emit('update:value', val ?? '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const numberValue = computed(() => {
|
||||||
|
const n = parseFloat(String(props.modelValue || '').replace(/,/g, ''))
|
||||||
|
return Number.isFinite(n) ? n : undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
function onNumberChange(val: number | string | null) {
|
||||||
|
if (val === null || val === undefined || val === '') {
|
||||||
|
emit('update:value', '')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
emit('update:value', String(val))
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.setting-value-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.setting-value-field :deep(.ant-input-number) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,69 +1,158 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="h-full min-h-0">
|
||||||
<BasicShowContent title="تنظیمات">
|
<BasicShowContent title="تنظیمات" class="h-full">
|
||||||
<BaseButton class="mb-10" @click="add" type="primary" icon="vuesax-linear:add">
|
<div class="settings-toolbar">
|
||||||
افزودن
|
<a-input
|
||||||
</BaseButton>
|
v-model:value="search"
|
||||||
<template v-if="items && hasAnyItems">
|
allow-clear
|
||||||
<div v-if="groupedItems.switch.length > 0" class="mb-4">
|
placeholder="جستجو کلید یا عنوان..."
|
||||||
<div class="grid lg:grid-cols-2 xl:grid-cols-3 gap-2">
|
class="settings-toolbar__search"
|
||||||
<SwitchConfigs v-for="item in groupedItems.switch" :key="item.id" :item="item"/>
|
>
|
||||||
</div>
|
<template #prefix>
|
||||||
</div>
|
<BaseIcon icon="vuesax-linear:search-normal-1" size="16" class="text-gray-400" />
|
||||||
|
</template>
|
||||||
|
</a-input>
|
||||||
|
|
||||||
<div v-if="groupedItems.text.length > 0" class="mb-4">
|
<div class="settings-toolbar__filters">
|
||||||
<div class="h-1px bg-gray-300 my-4"></div>
|
<label class="settings-filter-field">
|
||||||
<div class="grid lg:grid-cols-2 xl:grid-cols-3 gap-2">
|
<span class="settings-filter-field__label">وضعیت</span>
|
||||||
<TextConfigs v-for="item in groupedItems.text" :key="item.id" :item="item"/>
|
<a-select
|
||||||
</div>
|
v-model:value="filterActive"
|
||||||
</div>
|
allow-clear
|
||||||
|
placeholder="همه"
|
||||||
<div v-if="groupedItems.longtext.length > 0">
|
class="settings-filter-field__control"
|
||||||
<div class="h-1px bg-gray-300 my-4"></div>
|
:options="statusFilterOptions"
|
||||||
<div class="gap-2">
|
|
||||||
<LongtextConfigs
|
|
||||||
v-for="item in groupedItems.longtext"
|
|
||||||
:key="item.id"
|
|
||||||
:item="item"
|
|
||||||
@set-val="(val) => (item.value = val)"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</label>
|
||||||
|
<label class="settings-filter-field">
|
||||||
|
<span class="settings-filter-field__label">نوع فیلد</span>
|
||||||
|
<a-select
|
||||||
|
v-model:value="filterType"
|
||||||
|
allow-clear
|
||||||
|
placeholder="همه"
|
||||||
|
class="settings-filter-field__control"
|
||||||
|
:options="typeSelectOptions"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
|
||||||
|
<!-- <a-button type="primary" :loading="loading" @click="add">افزودن تنظیم</a-button>-->
|
||||||
|
<a-button class="settings-toolbar__action" :loading="loading" @click="initConfig">
|
||||||
|
بروزرسانی
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-spin :spinning="loading && !baseData.length">
|
||||||
|
<template v-if="switchRows.length">
|
||||||
|
<div class="settings-section-title">روشن / خاموش</div>
|
||||||
|
<a-row :gutter="[16, 16]" class="settings-grid">
|
||||||
|
<a-col
|
||||||
|
v-for="row in switchRows"
|
||||||
|
:key="row.id"
|
||||||
|
:xs="24"
|
||||||
|
:sm="12"
|
||||||
|
:lg="8"
|
||||||
|
>
|
||||||
|
<SettingCard
|
||||||
|
:row="row"
|
||||||
|
:model-value="draftValue(row)"
|
||||||
|
:readonly="isReadonly(row)"
|
||||||
|
:saving="isSaving(row.id)"
|
||||||
|
:saved-flash="isSavedFlash(row.id)"
|
||||||
|
@update:value="onValueChange(row, $event)"
|
||||||
|
@update:is-active="onActiveChange(row, $event)"
|
||||||
|
/>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="otherRows.length">
|
||||||
|
<div class="settings-section-title settings-section-title--spaced">سایر تنظیمات</div>
|
||||||
|
<a-row :gutter="[16, 16]" class="settings-grid">
|
||||||
|
<a-col
|
||||||
|
v-for="row in otherRows"
|
||||||
|
:key="row.id"
|
||||||
|
:xs="24"
|
||||||
|
:sm="12"
|
||||||
|
:lg="rowColSpan(row)"
|
||||||
|
>
|
||||||
|
<SettingCard
|
||||||
|
:row="row"
|
||||||
|
:model-value="draftValue(row)"
|
||||||
|
:readonly="isReadonly(row)"
|
||||||
|
:saving="isSaving(row.id)"
|
||||||
|
:saved-flash="isSavedFlash(row.id)"
|
||||||
|
@update:value="onValueChange(row, $event)"
|
||||||
|
@update:is-active="onActiveChange(row, $event)"
|
||||||
|
/>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<a-empty
|
||||||
|
v-if="!loading && !switchRows.length && !otherRows.length"
|
||||||
|
description="تنظیمی یافت نشد"
|
||||||
|
class="mt-8"
|
||||||
|
/>
|
||||||
|
</a-spin>
|
||||||
</BasicShowContent>
|
</BasicShowContent>
|
||||||
|
|
||||||
<BasicModal
|
<BasicModal
|
||||||
v-model:visible="addConfigVisible"
|
v-model:visible="addConfigVisible"
|
||||||
title="افزودن"
|
title="افزودن تنظیم"
|
||||||
@close="addConfigVisible = false"
|
:width="modalWidth"
|
||||||
@ok="ok"
|
@close="addConfigVisible = false"
|
||||||
:loading="loading"
|
@ok="ok"
|
||||||
|
:loading="loading"
|
||||||
>
|
>
|
||||||
<a-form ref="formRef" :model="form" autocomplete="off">
|
<a-form ref="formRef" :model="form" layout="vertical" autocomplete="off">
|
||||||
<a-form-item name="is_active">
|
<a-form-item label="عنوان فارسی" name="title_fa">
|
||||||
<span class="ml-2">وضعیت :</span>
|
<a-input v-model:value="form.title_fa" placeholder="عنوان در پنل" />
|
||||||
<a-checkbox v-model:checked="form.is_active">{{ !form.is_active ? 'غیرفعال' : 'فعال' }}</a-checkbox>
|
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item name="title_fa" label="عنوان">
|
|
||||||
<BasicInput v-model:value="form.title_fa" :placeHolder="form.title_fa" />
|
<a-form-item label="کلید (key)" name="key" :rules="[inputRules()]">
|
||||||
|
<a-input v-model:value="form.key" placeholder="مثلاً price_provider" />
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item name="key" label="کلید">
|
|
||||||
<BasicInput v-model:value="form.key" :placeHolder="form.key" />
|
<a-form-item label="نوع فیلد" name="type">
|
||||||
|
<a-select v-model:value="form.type" placeholder="نوع ورودی">
|
||||||
|
<a-select-option v-for="t in settingTypeOptions" :key="t.key" :value="t.key">
|
||||||
|
{{ t.value }}
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item name="type" label="نوع">
|
|
||||||
<BasicSelect
|
<a-form-item
|
||||||
allowClear
|
v-if="form.type === 'select'"
|
||||||
v-model:value="form.type"
|
label="گزینهها (با کاما)"
|
||||||
:items="typeItems"
|
name="selectOptions"
|
||||||
valueName="value"
|
>
|
||||||
keyName="key"
|
<a-input
|
||||||
|
v-model:value="selectOptionsDraft"
|
||||||
|
placeholder="montzar,zaarbox,main_product"
|
||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item name="value" label="مقدار">
|
|
||||||
<BasicInput v-model:value="form.value" :placeHolder="form.value" />
|
<a-form-item name="is_active">
|
||||||
|
<a-checkbox v-model:checked="form.is_active">فعال در سیستم</a-checkbox>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item name="description" label="توضیحات">
|
|
||||||
<BasicTextArea v-model:value="form.description" :placeHolder="form.description" />
|
<a-form-item name="value" :label="valueLabel" :extra="valueHelp">
|
||||||
|
<SettingValueField
|
||||||
|
v-model:value="form.value"
|
||||||
|
:setting-key="form.key"
|
||||||
|
:setting-type="effectiveType"
|
||||||
|
:description="form.description"
|
||||||
|
:rows="effectiveType === 'longtext' ? 12 : 6"
|
||||||
|
/>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item label="توضیحات / راهنما" name="description">
|
||||||
|
<a-textarea
|
||||||
|
v-model:value="form.description"
|
||||||
|
:rows="2"
|
||||||
|
placeholder="برای select: گزینهها با کاما — montzar,zaarbox"
|
||||||
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
</BasicModal>
|
</BasicModal>
|
||||||
@@ -71,268 +160,360 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { debounce } from 'lodash'
|
||||||
import BasicShowContent from '@/components/BasicShowContext.vue'
|
import BasicShowContent from '@/components/BasicShowContext.vue'
|
||||||
import BasicModal from '@/components/BasicModal.vue'
|
import BasicModal from '@/components/BasicModal.vue'
|
||||||
|
import { inputRules } from '@/utils/inputRules'
|
||||||
import type { FormInstance } from 'ant-design-vue'
|
import type { FormInstance } from 'ant-design-vue'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import type { Const } from '@/model/const'
|
import type { Const } from '@/model/configuration'
|
||||||
import BasicInput from '@/components/BasicInput.vue'
|
|
||||||
import BasicTextArea from '@/components/BasicTextArea.vue'
|
|
||||||
import TextConfigs from '@/pages/menuPages/configuration/components/TextConfigs.vue'
|
|
||||||
import LongtextConfigs from '@/pages/menuPages/configuration/components/LongtextConfigs.vue'
|
|
||||||
import { loading } from '@/utils'
|
import { loading } from '@/utils'
|
||||||
import { getConfiguration, setConfiguration } from '@/api/configuration'
|
import { getConfiguration, setConfiguration } from '@/api/configuration'
|
||||||
import SwitchConfigs from "@/pages/menuPages/configuration/components/SwitchConfigs.vue";
|
import SettingValueField from './SettingValueField.vue'
|
||||||
|
import SettingCard from './SettingCard.vue'
|
||||||
|
import {
|
||||||
|
SETTING_TYPE_OPTIONS,
|
||||||
|
formatValueForSave,
|
||||||
|
helpText,
|
||||||
|
resolveSettingFieldType
|
||||||
|
} from './settingTypes'
|
||||||
|
|
||||||
//data Arrays
|
const settingTypeOptions = SETTING_TYPE_OPTIONS
|
||||||
const items = ref<Array<any>>()
|
|
||||||
const baseData = ref<Array<Const>>()
|
|
||||||
|
|
||||||
//modal
|
const statusFilterOptions = [
|
||||||
const addConfigVisible = ref<boolean>(false)
|
{ value: 'active', label: 'فعال' },
|
||||||
|
{ value: 'inactive', label: 'غیرفعال' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const typeSelectOptions = SETTING_TYPE_OPTIONS.map((t) => ({
|
||||||
|
value: t.key,
|
||||||
|
label: t.value
|
||||||
|
}))
|
||||||
|
|
||||||
|
const baseData = ref<Array<Const>>([])
|
||||||
|
const drafts = ref<Record<number, string>>({})
|
||||||
|
const addConfigVisible = ref(false)
|
||||||
|
const search = ref('')
|
||||||
|
const filterType = ref<string | undefined>()
|
||||||
|
const filterActive = ref<string | undefined>()
|
||||||
|
|
||||||
|
function isRowActive(row: Const): boolean {
|
||||||
|
return row.is_active !== false && row.is_active !== 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const savingIds = ref<Set<number>>(new Set())
|
||||||
|
const savedFlashIds = ref<Set<number>>(new Set())
|
||||||
|
const saveTimers = new Map<number, ReturnType<typeof setTimeout>>()
|
||||||
|
const debouncedSave = new Map<number, ReturnType<typeof debounce>>()
|
||||||
|
|
||||||
const form = ref<Const>({
|
const form = ref<Const>({
|
||||||
key: '',
|
key: '',
|
||||||
value: '',
|
value: '',
|
||||||
title_fa:'',
|
title_fa: '',
|
||||||
type: 'text',
|
|
||||||
description: '',
|
description: '',
|
||||||
is_active: true
|
type: 'text',
|
||||||
|
is_active: true,
|
||||||
|
can_edit: 1
|
||||||
})
|
})
|
||||||
|
|
||||||
const formRef = ref<FormInstance>()
|
const formRef = ref<FormInstance>()
|
||||||
|
const selectOptionsDraft = ref('')
|
||||||
|
|
||||||
const typeItems = [
|
const effectiveType = computed(() =>
|
||||||
{
|
resolveSettingFieldType({ key: form.value.key, type: form.value.type })
|
||||||
value: 'متنی کوتاه',
|
|
||||||
key: 'text'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'متنی بلند',
|
|
||||||
key: 'longtext'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: 'سوییچی',
|
|
||||||
key: 'switch'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const settingsConfig = {
|
|
||||||
// اطلاعات پایه
|
|
||||||
'name_fa': {
|
|
||||||
title: 'نام فارسی',
|
|
||||||
list: []
|
|
||||||
},
|
|
||||||
'name_en': {
|
|
||||||
title: 'نام انگلیسی',
|
|
||||||
list: []
|
|
||||||
},
|
|
||||||
'app_url': {
|
|
||||||
title: 'آدرس اپلیکیشن',
|
|
||||||
list: []
|
|
||||||
},
|
|
||||||
'bace_url': {
|
|
||||||
title: 'آدرس پایه',
|
|
||||||
list: []
|
|
||||||
},
|
|
||||||
'admin_url': {
|
|
||||||
title: 'آدرس پنل ادمین',
|
|
||||||
list: []
|
|
||||||
},
|
|
||||||
'server_url': {
|
|
||||||
title: 'آدرس سرور',
|
|
||||||
list: []
|
|
||||||
},
|
|
||||||
|
|
||||||
// تهحساب
|
|
||||||
'tahesab_token': {
|
|
||||||
title: 'توکن تهحساب',
|
|
||||||
list: []
|
|
||||||
},
|
|
||||||
'tahesab_db': {
|
|
||||||
title: 'دیتابیس تهحساب',
|
|
||||||
list: []
|
|
||||||
},
|
|
||||||
'send_to_tahesab': {
|
|
||||||
title: 'ارسال اسناد به ته حساب',
|
|
||||||
list: ['true', 'false']
|
|
||||||
},
|
|
||||||
|
|
||||||
// تنظیمات صفحهها
|
|
||||||
'has_page_earn_money': {
|
|
||||||
title: 'صفحه کسب درآمد',
|
|
||||||
list: ['true', 'false']
|
|
||||||
},
|
|
||||||
'has_page_fund': {
|
|
||||||
title: 'صفحه سرمایهگذاری',
|
|
||||||
list: ['true', 'false']
|
|
||||||
},
|
|
||||||
'has_page_permission': {
|
|
||||||
title: 'صفحه دسترسیها',
|
|
||||||
list: ['true', 'false']
|
|
||||||
},
|
|
||||||
'has_page_price_trigger': {
|
|
||||||
title: 'صفحه هشدار قیمت',
|
|
||||||
list: ['true', 'false']
|
|
||||||
},
|
|
||||||
'has_page_physical_gold': {
|
|
||||||
title: 'صفحه طلای فیزیکی',
|
|
||||||
list: ['true', 'false']
|
|
||||||
},
|
|
||||||
|
|
||||||
// رنگها
|
|
||||||
'color_primary': {
|
|
||||||
title: 'رنگ اصلی',
|
|
||||||
list: []
|
|
||||||
},
|
|
||||||
'color_secondary': {
|
|
||||||
title: 'رنگ ثانویه',
|
|
||||||
list: []
|
|
||||||
},
|
|
||||||
|
|
||||||
// کارمزدها
|
|
||||||
'fee_service_charge_percentage': {
|
|
||||||
title: 'کارمزد درصدی',
|
|
||||||
list: []
|
|
||||||
},
|
|
||||||
'fee_cash': {
|
|
||||||
title: 'کارمزد ریالی',
|
|
||||||
list: []
|
|
||||||
},
|
|
||||||
'fee_single': {
|
|
||||||
title: 'اختلاف با قیمت واقعی',
|
|
||||||
list: []
|
|
||||||
},
|
|
||||||
'fee_free': {
|
|
||||||
title: 'تعداد سفارش اول بدون کارمزد',
|
|
||||||
list: []
|
|
||||||
},
|
|
||||||
|
|
||||||
// تنظیمات عمومی
|
|
||||||
'has_chat': {
|
|
||||||
title: 'فعال بودن چت',
|
|
||||||
list: ['true', 'false']
|
|
||||||
},
|
|
||||||
'is_requaire_referral': {
|
|
||||||
title: 'نیازمند معرف',
|
|
||||||
list: ['true', 'false']
|
|
||||||
},
|
|
||||||
'auto_active_customer': {
|
|
||||||
title: 'فعالسازی خودکار حساب کاربران',
|
|
||||||
list: ['true', 'false']
|
|
||||||
},
|
|
||||||
'open_register': {
|
|
||||||
title: 'فعالسازی ثبت نام',
|
|
||||||
list: ['true', 'false']
|
|
||||||
},
|
|
||||||
'charge_notif': {
|
|
||||||
title: 'ارسال پیام موقع شارژ از سمت ادمین',
|
|
||||||
list: ['true', 'false']
|
|
||||||
},
|
|
||||||
'display_montzar': {
|
|
||||||
title: 'نمایش منتظر',
|
|
||||||
list: ['true', 'false']
|
|
||||||
},
|
|
||||||
|
|
||||||
// توکنها
|
|
||||||
'liman_token': {
|
|
||||||
title: 'توکن لیمان',
|
|
||||||
list: []
|
|
||||||
},
|
|
||||||
'key': {
|
|
||||||
title: 'کلید',
|
|
||||||
list: []
|
|
||||||
},
|
|
||||||
|
|
||||||
// متفرقه
|
|
||||||
'logo_login': {
|
|
||||||
title: 'لوگو صفحه ورود',
|
|
||||||
list: []
|
|
||||||
},
|
|
||||||
'logo_register': {
|
|
||||||
title: 'لوگو صفحه ثبتنام',
|
|
||||||
list: []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
async function initConfig() {
|
|
||||||
try {
|
|
||||||
loading.value = true
|
|
||||||
baseData.value = await getConfiguration()
|
|
||||||
items.value = baseData.value?.map((x: Const) => {
|
|
||||||
const title_fa = x?.title_fa || settingsConfig[x.key]?.title || x?.key
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: x.id,
|
|
||||||
name: title_fa,
|
|
||||||
title_fa,
|
|
||||||
value: x.value,
|
|
||||||
key: x.key,
|
|
||||||
type: x.type || 'text',
|
|
||||||
description: x.description,
|
|
||||||
is_active: x.is_active,
|
|
||||||
list: settingsConfig[x.key]?.list ?? []
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupedItems = computed(() => {
|
|
||||||
const result = { switch: [], text: [], longtext: [] } as Record<string, any[]>
|
|
||||||
items.value?.forEach((item) => {
|
|
||||||
const type = item.type && result[item.type] ? item.type : 'text'
|
|
||||||
result[type].push(item)
|
|
||||||
})
|
|
||||||
// console.log(result)
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
|
|
||||||
const hasAnyItems = computed(() =>
|
|
||||||
Object.values(groupedItems.value).some((list) => list.length > 0)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async function resetData() {
|
const modalWidth = computed(() =>
|
||||||
formRef.value?.resetFields()
|
effectiveType.value === 'longtext' ? '720px' : '520px'
|
||||||
form.value = {
|
)
|
||||||
key: '',
|
|
||||||
value: '',
|
|
||||||
title_fa:'',
|
|
||||||
type: 'text',
|
|
||||||
description: '',
|
|
||||||
is_active: true
|
|
||||||
}
|
|
||||||
await initConfig()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function add() {
|
const valueLabel = computed(() => form.value.title_fa || form.value.key || 'مقدار')
|
||||||
await resetData()
|
const valueHelp = computed(() => helpText(form.value))
|
||||||
addConfigVisible.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
async function ok() {
|
function sortRows(rows: Const[]): Const[] {
|
||||||
formRef.value?.validateFields().then(async () => {
|
return [...rows].sort((a, b) => {
|
||||||
try {
|
const ta = (a.title_fa || a.key || '').localeCompare(b.title_fa || b.key || '', 'fa')
|
||||||
loading.value = true
|
return ta
|
||||||
console.log(form.value)
|
|
||||||
await setConfiguration(form.value)
|
|
||||||
await resetData()
|
|
||||||
message.success('عملیات با موفقیت انجام شد')
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
addConfigVisible.value = false
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isEditable(row: Const): boolean {
|
||||||
|
return row.can_edit !== 0 && row.can_edit !== false
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesFilter(row: Const): boolean {
|
||||||
|
const q = search.value.trim().toLowerCase()
|
||||||
|
if (filterActive.value === 'active' && !isRowActive(row)) return false
|
||||||
|
if (filterActive.value === 'inactive' && isRowActive(row)) return false
|
||||||
|
if (filterType.value) {
|
||||||
|
const t = resolveSettingFieldType(row)
|
||||||
|
if (t !== filterType.value) return false
|
||||||
|
}
|
||||||
|
if (!q) return true
|
||||||
|
return (
|
||||||
|
(row.key || '').toLowerCase().includes(q) ||
|
||||||
|
(row.title_fa || '').toLowerCase().includes(q) ||
|
||||||
|
(row.value || '').toLowerCase().includes(q) ||
|
||||||
|
(drafts.value[row.id!] || '').toLowerCase().includes(q)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredRows = computed(() =>
|
||||||
|
sortRows(
|
||||||
|
(baseData.value || []).filter(
|
||||||
|
(row) => row.id != null && isEditable(row) && matchesFilter(row)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const switchRows = computed(() =>
|
||||||
|
filteredRows.value.filter((row) => resolveSettingFieldType(row) === 'switch')
|
||||||
|
)
|
||||||
|
|
||||||
|
const otherRows = computed(() =>
|
||||||
|
filteredRows.value.filter((row) => resolveSettingFieldType(row) !== 'switch')
|
||||||
|
)
|
||||||
|
|
||||||
|
function rowColSpan(row: Const): number {
|
||||||
|
return resolveSettingFieldType(row) === 'longtext' ? 24 : 8
|
||||||
|
}
|
||||||
|
|
||||||
|
function isReadonly(row: Const): boolean {
|
||||||
|
return row.can_edit === 0 || row.can_edit === false
|
||||||
|
}
|
||||||
|
|
||||||
|
function draftValue(row: Const): string {
|
||||||
|
const id = row.id!
|
||||||
|
if (drafts.value[id] !== undefined) return drafts.value[id]
|
||||||
|
return row.value ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSaving(id?: number): boolean {
|
||||||
|
return id != null && savingIds.value.has(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSavedFlash(id?: number): boolean {
|
||||||
|
return id != null && savedFlashIds.value.has(id) && !savingIds.value.has(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncDraftsFromBase() {
|
||||||
|
const next: Record<number, string> = {}
|
||||||
|
for (const row of baseData.value || []) {
|
||||||
|
if (row.id != null) next[row.id] = row.value ?? ''
|
||||||
|
}
|
||||||
|
drafts.value = next
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initConfig()
|
initConfig()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
async function initConfig() {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
baseData.value = (await getConfiguration()) ?? []
|
||||||
|
syncDraftsFromBase()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
message.error('بارگذاری تنظیمات ناموفق بود')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onValueChange(row: Const, value: string) {
|
||||||
|
if (row.id == null || isReadonly(row)) return
|
||||||
|
drafts.value[row.id] = value
|
||||||
|
scheduleSave(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onActiveChange(row: Const, isActive: boolean) {
|
||||||
|
if (row.id == null) return
|
||||||
|
if (isRowActive(row) === isActive) return
|
||||||
|
await persistRow(row, { isActive })
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleSave(row: Const) {
|
||||||
|
const id = row.id!
|
||||||
|
if (!debouncedSave.has(id)) {
|
||||||
|
debouncedSave.set(
|
||||||
|
id,
|
||||||
|
debounce(() => persistRowValue(id), resolveSettingFieldType(row) === 'switch' ? 200 : 600)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
debouncedSave.get(id)!()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function persistRowValue(id: number) {
|
||||||
|
const row = baseData.value.find((x) => x.id === id)
|
||||||
|
if (!row || isReadonly(row)) return
|
||||||
|
await saveSettingRow(row)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function persistRow(row: Const, overrides?: { isActive?: boolean }) {
|
||||||
|
await saveSettingRow(row, overrides)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveSettingRow(row: Const, overrides?: { isActive?: boolean }) {
|
||||||
|
if (row.id == null) return
|
||||||
|
const id = row.id
|
||||||
|
|
||||||
|
const type = resolveSettingFieldType(row)
|
||||||
|
const raw = drafts.value[id] ?? row.value ?? ''
|
||||||
|
const value = formatValueForSave(raw, type)
|
||||||
|
const isActive =
|
||||||
|
overrides?.isActive !== undefined ? overrides.isActive : isRowActive(row)
|
||||||
|
|
||||||
|
if (overrides?.isActive === undefined && value === (row.value ?? '')) return
|
||||||
|
|
||||||
|
savingIds.value.add(id)
|
||||||
|
savedFlashIds.value.delete(id)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await setConfiguration({
|
||||||
|
id: row.id,
|
||||||
|
key: row.key,
|
||||||
|
value,
|
||||||
|
description: row.description ?? '',
|
||||||
|
is_active: isActive,
|
||||||
|
title_fa: row.title_fa ?? '',
|
||||||
|
type
|
||||||
|
})
|
||||||
|
row.value = value
|
||||||
|
row.is_active = isActive
|
||||||
|
drafts.value[id] = value
|
||||||
|
savedFlashIds.value.add(id)
|
||||||
|
if (saveTimers.has(id)) clearTimeout(saveTimers.get(id))
|
||||||
|
saveTimers.set(
|
||||||
|
id,
|
||||||
|
setTimeout(() => {
|
||||||
|
savedFlashIds.value.delete(id)
|
||||||
|
savedFlashIds.value = new Set(savedFlashIds.value)
|
||||||
|
}, 2000)
|
||||||
|
)
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
drafts.value[id] = row.value ?? ''
|
||||||
|
message.error(`ذخیره «${row.title_fa || row.key}» ناموفق بود`)
|
||||||
|
} finally {
|
||||||
|
savingIds.value.delete(id)
|
||||||
|
savingIds.value = new Set(savingIds.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function add() {
|
||||||
|
form.value = {
|
||||||
|
key: '',
|
||||||
|
value: '',
|
||||||
|
title_fa: '',
|
||||||
|
description: '',
|
||||||
|
type: 'text',
|
||||||
|
is_active: true,
|
||||||
|
can_edit: 1
|
||||||
|
}
|
||||||
|
selectOptionsDraft.value = ''
|
||||||
|
formRef.value?.resetFields()
|
||||||
|
addConfigVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ok() {
|
||||||
|
const type = effectiveType.value
|
||||||
|
let description = form.value.description ?? ''
|
||||||
|
if (type === 'select' && selectOptionsDraft.value.trim()) {
|
||||||
|
description = selectOptionsDraft.value.trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
key: form.value.key,
|
||||||
|
value: formatValueForSave(form.value.value ?? '', type),
|
||||||
|
description,
|
||||||
|
is_active: form.value.is_active,
|
||||||
|
title_fa: form.value.title_fa ?? '',
|
||||||
|
type
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await formRef.value?.validateFields()
|
||||||
|
loading.value = true
|
||||||
|
await setConfiguration(payload)
|
||||||
|
message.success('تنظیم جدید اضافه شد')
|
||||||
|
addConfigVisible.value = false
|
||||||
|
await initConfig()
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less"></style>
|
<style lang="less" scoped>
|
||||||
|
.settings-toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: flex-end;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: #fafafa;
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
&__search {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
max-width: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__filters {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__action {
|
||||||
|
margin-inline-start: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-filter-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 148px;
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: rgba(0, 0, 0, 0.55);
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__control {
|
||||||
|
width: 148px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section-title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: rgba(0, 0, 0, 0.55);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
border-bottom: 1px solid #f0f0f0;
|
||||||
|
|
||||||
|
&--spaced {
|
||||||
|
margin-top: 28px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-grid {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-form-item-extra) {
|
||||||
|
font-size: 12px;
|
||||||
|
color: rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import type { Const } from '@/model/configuration'
|
||||||
|
|
||||||
|
export type SettingFieldType = 'text' | 'number' | 'switch' | 'select' | 'longtext'
|
||||||
|
|
||||||
|
export const SETTING_TYPE_OPTIONS = [
|
||||||
|
{ key: 'text', value: 'متن کوتاه' },
|
||||||
|
{ key: 'number', value: 'عدد' },
|
||||||
|
{ key: 'switch', value: 'روشن/خاموش' },
|
||||||
|
{ key: 'select', value: 'انتخابی' },
|
||||||
|
{ key: 'longtext', value: 'متن بلند' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const KEY_TYPE_FALLBACK: Record<string, SettingFieldType> = {
|
||||||
|
price_provider: 'select',
|
||||||
|
referral_id: 'select',
|
||||||
|
payping_gateway_name: 'select',
|
||||||
|
text_rule: 'longtext',
|
||||||
|
text_about: 'longtext',
|
||||||
|
fee_service_charge_percentage: 'number',
|
||||||
|
fee_single: 'number',
|
||||||
|
buy_fee: 'number',
|
||||||
|
sell_fee: 'number',
|
||||||
|
fee_cash: 'number',
|
||||||
|
min_withdraw: 'number',
|
||||||
|
min_first_buy: 'number',
|
||||||
|
minimum_sell: 'number',
|
||||||
|
gift_for_referrer: 'number',
|
||||||
|
gift_for_referral: 'number',
|
||||||
|
referral_bonus_amount: 'text',
|
||||||
|
referral_count_for_gift: 'number',
|
||||||
|
montzar_product_id: 'number',
|
||||||
|
balance_report_exclude_customer_id: 'number',
|
||||||
|
jibit_verification: 'switch',
|
||||||
|
register_jibit_verify: 'switch',
|
||||||
|
open_register: 'switch',
|
||||||
|
auto_active_customer: 'switch',
|
||||||
|
close_price: 'switch',
|
||||||
|
montzar_price: 'switch',
|
||||||
|
main_product_price: 'switch',
|
||||||
|
tabanGohar_price: 'switch',
|
||||||
|
show_last_price: 'switch',
|
||||||
|
send_to_tahesab: 'switch',
|
||||||
|
profit_gold_api_enabled: 'switch',
|
||||||
|
payping_round_toman: 'switch',
|
||||||
|
payping_strict_failed_withdraw: 'switch',
|
||||||
|
has_page_earn_money: 'switch',
|
||||||
|
has_page_fund: 'switch',
|
||||||
|
has_page_permission: 'switch',
|
||||||
|
has_page_price_trigger: 'switch',
|
||||||
|
has_page_physical_gold: 'switch',
|
||||||
|
has_chat: 'switch',
|
||||||
|
charge_notif: 'switch',
|
||||||
|
group_sms: 'switch',
|
||||||
|
display_montzar: 'switch',
|
||||||
|
credit: 'switch',
|
||||||
|
credit_daily: 'switch',
|
||||||
|
credit_error_in_orders: 'switch',
|
||||||
|
credit_rial_dynamic: 'switch',
|
||||||
|
open_closed_messages: 'switch',
|
||||||
|
new_style_create_order: 'switch',
|
||||||
|
new_style_admin_layout: 'switch',
|
||||||
|
register: 'switch',
|
||||||
|
sharh: 'switch',
|
||||||
|
multi_source: 'switch',
|
||||||
|
get_orders_pdf: 'switch',
|
||||||
|
get_orders_excel: 'switch',
|
||||||
|
auto_order_buy_sell: 'switch',
|
||||||
|
invite_link: 'switch',
|
||||||
|
montzar_closed: 'switch',
|
||||||
|
montzar_open_buy: 'switch',
|
||||||
|
montzar_open_sell: 'switch',
|
||||||
|
is_requaire_referral: 'switch',
|
||||||
|
product_update_price_on_list: 'switch',
|
||||||
|
product_auto_close_open_on_list: 'switch',
|
||||||
|
payping_fin_sync_enabled: 'switch',
|
||||||
|
external_order_api_enabled: 'switch',
|
||||||
|
store_status: 'select',
|
||||||
|
credit_rial_price_unit: 'select',
|
||||||
|
node_client_id: 'text',
|
||||||
|
node_shared_secret: 'text'
|
||||||
|
}
|
||||||
|
|
||||||
|
const SELECT_OPTIONS_BY_KEY: Record<string, string> = {
|
||||||
|
price_provider: 'montzar,zaarbox,main_product,taban_gohar',
|
||||||
|
referral_id: 'required,',
|
||||||
|
payping_gateway_name: 'sepal,',
|
||||||
|
credit_rial_price_unit: 'gram,mesghal,unit',
|
||||||
|
store_status: 'باز,بسته'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveSettingFieldType(row: Pick<Const, 'key' | 'type'>): SettingFieldType {
|
||||||
|
const raw = (row.type || '').trim().toLowerCase()
|
||||||
|
if (['text', 'number', 'switch', 'select', 'longtext'].includes(raw)) {
|
||||||
|
return raw as SettingFieldType
|
||||||
|
}
|
||||||
|
if (row.key && KEY_TYPE_FALLBACK[row.key]) {
|
||||||
|
return KEY_TYPE_FALLBACK[row.key]
|
||||||
|
}
|
||||||
|
return 'text'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** گزینههای select: اول از description (با | یا کاما)، بعد از نقشه کلید */
|
||||||
|
export function parseSelectOptions(
|
||||||
|
row: Pick<Const, 'key' | 'description' | 'type'>
|
||||||
|
): Array<{ key: string; value: string }> {
|
||||||
|
const preset = row.key ? SELECT_OPTIONS_BY_KEY[row.key] : ''
|
||||||
|
let source = preset || row.description || ''
|
||||||
|
|
||||||
|
if (source.includes('|')) {
|
||||||
|
source = source.split('|').pop() || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const opts = source.split(',').map((s) => s.trim())
|
||||||
|
return opts.map((opt) => ({
|
||||||
|
key: opt,
|
||||||
|
value: opt === '' ? '(خالی)' : opt
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSwitchOn(value: string | undefined): boolean {
|
||||||
|
const v = (value ?? '').trim().toLowerCase()
|
||||||
|
return v === 'true' || v === '1' || v === 'yes'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function switchToString(checked: boolean): string {
|
||||||
|
return checked ? 'true' : 'false'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatValueForSave(value: string, type: SettingFieldType): string {
|
||||||
|
if (type === 'switch') {
|
||||||
|
return switchToString(isSwitchOn(value))
|
||||||
|
}
|
||||||
|
if (type === 'number') {
|
||||||
|
return value === '' ? '0' : String(value).replace(/,/g, '')
|
||||||
|
}
|
||||||
|
return value ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function helpText(row: Const): string {
|
||||||
|
const desc = (row.description || '').trim()
|
||||||
|
if (!desc) return ''
|
||||||
|
if (resolveSettingFieldType(row) === 'select' && desc.includes(',')) {
|
||||||
|
const parts = desc.split('|')
|
||||||
|
return parts.length > 1 ? parts[0].trim() : ''
|
||||||
|
}
|
||||||
|
return desc
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<BasicMainCard>
|
<BasicMainCard>
|
||||||
<div class="flex flex-col md:flex-row md:h-full">
|
<div class="flex h-full min-h-0 flex-col md:flex-row">
|
||||||
<!-- {{ menu }}-->
|
<!-- {{ menu }}-->
|
||||||
<!-- <BasicMenu width="13rem" :menu="menu" class="overflow-auto min-w-[13.5rem]" />-->
|
<!-- <BasicMenu width="13rem" :menu="menu" class="overflow-auto min-w-[13.5rem]" />-->
|
||||||
<BasicMenu width="11rem" :menu="menu" mode="horizontal" class="md:hidden overflow-auto" />
|
<BasicMenu width="11rem" :menu="menu" mode="horizontal" class="md:hidden overflow-auto" />
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
class="hidden md:inline overflow-auto min-w-[13.5rem]"
|
class="hidden md:inline overflow-auto min-w-[13.5rem]"
|
||||||
/>
|
/>
|
||||||
<!-- <component :is="selectedComponent" class="mr-5" />-->
|
<!-- <component :is="selectedComponent" class="mr-5" />-->
|
||||||
<RouterView class="md:mr-5 md:mt-0 mt-5 w-full"></RouterView>
|
<RouterView class="mt-5 min-h-0 w-full flex-1 overflow-hidden md:mr-5 md:mt-0" />
|
||||||
</div>
|
</div>
|
||||||
</BasicMainCard>
|
</BasicMainCard>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user