setting changed 3

This commit is contained in:
2026-06-05 22:01:10 +03:30
parent f89df02a90
commit c828c8f971
10 changed files with 1152 additions and 774 deletions
+469 -288
View File
@@ -1,69 +1,158 @@
<template>
<div>
<BasicShowContent title="تنظیمات">
<BaseButton class="mb-10" @click="add" type="primary" icon="vuesax-linear:add">
افزودن
</BaseButton>
<template v-if="items && hasAnyItems">
<div v-if="groupedItems.switch.length > 0" class="mb-4">
<div class="grid lg:grid-cols-2 xl:grid-cols-3 gap-2">
<SwitchConfigs v-for="item in groupedItems.switch" :key="item.id" :item="item"/>
</div>
</div>
<div class="h-full min-h-0">
<BasicShowContent title="تنظیمات" class="h-full">
<div class="settings-toolbar">
<a-input
v-model:value="search"
allow-clear
placeholder="جستجو کلید یا عنوان..."
class="settings-toolbar__search"
>
<template #prefix>
<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="h-1px bg-gray-300 my-4"></div>
<div class="grid lg:grid-cols-2 xl:grid-cols-3 gap-2">
<TextConfigs v-for="item in groupedItems.text" :key="item.id" :item="item"/>
</div>
</div>
<div v-if="groupedItems.longtext.length > 0">
<div class="h-1px bg-gray-300 my-4"></div>
<div class="gap-2">
<LongtextConfigs
v-for="item in groupedItems.longtext"
:key="item.id"
:item="item"
@set-val="(val) => (item.value = val)"
<div class="settings-toolbar__filters">
<label class="settings-filter-field">
<span class="settings-filter-field__label">وضعیت</span>
<a-select
v-model:value="filterActive"
allow-clear
placeholder="همه"
class="settings-filter-field__control"
:options="statusFilterOptions"
/>
</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>
</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>
<BasicModal
v-model:visible="addConfigVisible"
title="افزودن"
@close="addConfigVisible = false"
@ok="ok"
:loading="loading"
v-model:visible="addConfigVisible"
title="افزودن تنظیم"
:width="modalWidth"
@close="addConfigVisible = false"
@ok="ok"
:loading="loading"
>
<a-form ref="formRef" :model="form" autocomplete="off">
<a-form-item name="is_active">
<span class="ml-2">وضعیت :</span>
<a-checkbox v-model:checked="form.is_active">{{ !form.is_active ? 'غیرفعال' : 'فعال' }}</a-checkbox>
<a-form ref="formRef" :model="form" layout="vertical" autocomplete="off">
<a-form-item label="عنوان فارسی" name="title_fa">
<a-input v-model:value="form.title_fa" placeholder="عنوان در پنل" />
</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 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 name="type" label="نوع">
<BasicSelect
allowClear
v-model:value="form.type"
:items="typeItems"
valueName="value"
keyName="key"
<a-form-item
v-if="form.type === 'select'"
label="گزینه‌ها (با کاما)"
name="selectOptions"
>
<a-input
v-model:value="selectOptionsDraft"
placeholder="montzar,zaarbox,main_product"
/>
</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 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>
</BasicModal>
@@ -71,268 +160,360 @@
</template>
<script setup lang="ts">
import { debounce } from 'lodash'
import BasicShowContent from '@/components/BasicShowContext.vue'
import BasicModal from '@/components/BasicModal.vue'
import { inputRules } from '@/utils/inputRules'
import type { FormInstance } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import type { Const } from '@/model/const'
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 type { Const } from '@/model/configuration'
import { loading } from '@/utils'
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 items = ref<Array<any>>()
const baseData = ref<Array<Const>>()
const settingTypeOptions = SETTING_TYPE_OPTIONS
//modal
const addConfigVisible = ref<boolean>(false)
const statusFilterOptions = [
{ 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>({
key: '',
value: '',
title_fa:'',
type: 'text',
title_fa: '',
description: '',
is_active: true
type: 'text',
is_active: true,
can_edit: 1
})
const formRef = ref<FormInstance>()
const selectOptionsDraft = ref('')
const typeItems = [
{
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)
const effectiveType = computed(() =>
resolveSettingFieldType({ key: form.value.key, type: form.value.type })
)
async function resetData() {
formRef.value?.resetFields()
form.value = {
key: '',
value: '',
title_fa:'',
type: 'text',
description: '',
is_active: true
}
await initConfig()
}
const modalWidth = computed(() =>
effectiveType.value === 'longtext' ? '720px' : '520px'
)
async function add() {
await resetData()
addConfigVisible.value = true
}
const valueLabel = computed(() => form.value.title_fa || form.value.key || 'مقدار')
const valueHelp = computed(() => helpText(form.value))
async function ok() {
formRef.value?.validateFields().then(async () => {
try {
loading.value = true
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 sortRows(rows: Const[]): Const[] {
return [...rows].sort((a, b) => {
const ta = (a.title_fa || a.key || '').localeCompare(b.title_fa || b.key || '', 'fa')
return ta
})
}
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(() => {
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>
<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>