@@ -1,10 +1,9 @@
< script setup lang = "ts" >
import { computed , onMounted , reactive , ref } from 'vue'
import { useRoute , useRouter } from 'vue-router'
import { message , Modal } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import {
ArrowRightOutlined ,
DeleteOutlined ,
GlobalOutlined ,
SearchOutlined ,
TeamOutlined ,
@@ -38,12 +37,9 @@ const router = useRouter()
const auth = useAuthStore ( )
const canWrite = computed ( ( ) => auth . can ( 'khanegi.settings:write' ) )
const loading = ref ( true )
const importing = ref ( false )
const purging = ref ( false )
const search = ref ( '' )
const filterType = ref < string | undefined > ( )
const tenant = ref < TenantMeta | null > ( null )
const apiBase = ref < string | null > ( null )
const items = ref < KhanegiSettingRow [ ] > ( [ ] )
const drafts = ref < Record < string , string > > ( { } )
const savingKeys = ref < Set < string > > ( new Set ( ) )
@@ -51,32 +47,11 @@ const savedFlashKeys = ref<Set<string>>(new Set())
const saveTimers = new Map < string , ReturnType < typeof setTimeout > > ( )
const flashTimers = new Map < string , ReturnType < typeof setTimeout > > ( )
const creds = reactive ( {
mobile : '' ,
password : '' ,
} )
type ImportPreviewRow = {
key : string
title _fa : string
khanegi _value : string
superadmin _value : string | null
status : 'new_in_khanegi' | 'both'
scope : 'none' | 'this' | 'all'
}
const previewRows = ref < ImportPreviewRow [ ] > ( [ ] )
const onlySaKeys = ref < string [ ] > ( [ ] )
const previewOpen = ref ( false )
const applying = ref ( false )
const typeSelectOptions = SETTING _TYPE _OPTIONS . map ( ( t ) => ( {
value : t . key ,
label : t . value ,
} ) )
const count = computed ( ( ) => items . value . length )
function asRow ( raw : Record < string , unknown > ) : KhanegiSettingRow {
return normalizeSettingRow ( {
id : typeof raw . id === 'number' ? raw . id : undefined ,
@@ -240,7 +215,6 @@ async function load() {
try {
const { data } = await api . get ( ` /tenants/ ${ route . params . id } /program-settings ` )
tenant . value = data . tenant
apiBase . value = data . apiBase || null
applyItems ( Array . isArray ( data . items ) ? data . items : [ ] )
} catch ( e : any ) {
message . error ( errText ( e ) || 'خطا در بارگذاری تنظیمات برنامه' )
@@ -252,99 +226,6 @@ async function load() {
}
}
async function importSettings ( ) {
if ( ! canWrite . value ) {
message . warning ( 'دسترسی نوشتن ندارید' )
return
}
if ( ! creds . mobile . trim ( ) || ! creds . password ) {
message . warning ( 'نام کاربری و رمز عبور ادمین خانگی را وارد کنید' )
return
}
importing . value = true
try {
const { data } = await api . post ( ` /tenants/ ${ route . params . id } /program-settings/import ` , {
mobile : creds . mobile . trim ( ) ,
password : creds . password ,
preview : true ,
} )
previewRows . value = ( data . decisionsNeeded || [ ] ) . map ( ( row : ImportPreviewRow ) => ( {
... row ,
scope : 'this' ,
} ) )
onlySaKeys . value = data . onlySuperAdminKeys || [ ]
apiBase . value = data . apiBase || apiBase . value
previewOpen . value = true
} catch ( e : any ) {
message . error ( errText ( e ) || 'پیشنمایش ایمپورت ناموفق بود' )
} finally {
importing . value = false
}
}
async function applyImport ( ) {
applying . value = true
try {
const { data } = await api . post ( ` /tenants/ ${ route . params . id } /program-settings/import ` , {
mobile : creds . mobile . trim ( ) ,
password : creds . password ,
preview : false ,
decisions : previewRows . value . map ( ( row ) => ( { key : row . key , scope : row . scope } ) ) ,
} )
tenant . value = data . tenant
apiBase . value = data . apiBase || apiBase . value
applyItems ( Array . isArray ( data . items ) ? data . items : [ ] )
creds . password = ''
previewOpen . value = false
message . success ( 'مقدارهای can_edit=0 خانگی روی این پروژه نوشته شد.' )
} catch ( e : any ) {
message . error ( errText ( e ) || 'اعمال ایمپورت ناموفق بود' )
} finally {
applying . value = false
}
}
/**
* بعد از restore لوکال روی سرور همان پروژه: بقیهٔ کلیدها (غیر دسترسی مشتری)
* را از جدول settings همین tenant در سوپرادمین حذف میکند.
*/
function confirmPurgeNonOwned ( ) {
if ( ! canWrite . value ) {
message . warning ( 'دسترسی نوشتن ندارید' )
return
}
Modal . confirm ( {
title : 'حذف تنظیمات غیر دسترسی مشتری؟' ,
content :
'فقط کلیدهای «دسترسی مشتری به قابلیتها» روی این پروژه در سوپرادمین میماند. ' +
'بقیه از جدول settings همین پروژه در سوپرادمین پاک میشود. ' +
'قبل از این کار باید روی سرور همان نصب، settings:restore-from-backup را اجرا کرده باشی.' ,
okText : 'حذف کن' ,
okType : 'danger' ,
cancelText : 'انصراف' ,
onOk : ( ) => purgeNonOwned ( ) ,
} )
}
async function purgeNonOwned ( ) {
purging . value = true
try {
const { data } = await api . post ( ` /tenants/ ${ route . params . id } /program-settings/purge-non-owned ` )
tenant . value = data . tenant
applyItems ( Array . isArray ( data . items ) ? data . items : [ ] )
const removed = Number ( data . removed || 0 )
message . success (
removed > 0
? ` ${ removed } کلید غیر دسترسی از این پروژه حذف شد. `
: 'چیزی برای حذف نبود؛ همین حالا فقط کلیدهای دسترسی مشتری روی این پروژه است.' ,
)
} catch ( e : any ) {
message . error ( errText ( e ) || 'حذف ناموفق بود' )
} finally {
purging . value = false
}
}
onMounted ( load )
< / script >
@@ -375,97 +256,6 @@ onMounted(load)
< / div >
< / div >
< section v-if = "canWrite" class="form-section purge-section" >
< h3 > مهاجرت مالکیت به دیتابیس نصب < / h3 >
< p class = "field-hint" >
ترتیب : ۱ ) روی سرور همان پروژه
< code dir = "ltr" > php artisan settings : restore - from - backup -- fill - from - superadmin < / code >
۲ ) همین دکمه را بزن تا همه ٔ کلیدها غیر از « دسترسی مشتری به قابلیت ها »
از جدول settings این پروژه در سوپرادمین حذف شوند .
< / p >
< a-button danger type = "primary" html -type = " button " size = "large" :loading = "purging" @click ="confirmPurgeNonOwned" >
< DeleteOutlined / >
حذف تنظیمات غیر دسترسی از این پروژه
< / a-button >
< / section >
< section class = "form-section" >
< h3 > انتقال از اپ خانگی < / h3 >
< p class = "field-hint" >
مقدار کلیدهای < b > جدید < / b > خانگی را می آورد — نه کلیدهایی که از قبل در قالب
< code dir = "ltr" > deploy _settings _const < / code > هستند .
اگر سوپرادمین کلیدی داشته باشد که خانگی ندارد ، پیش فرض قالب برای همین پروژه گذاشته می شود .
< / p >
< a-form :model = "creds" layout = "vertical" >
< div class = "import-grid" >
< a-form-item label = "نام کاربری / موبایل ادمین" name = "mobile" >
< a-input
v -model :value = "creds.mobile"
size = "large"
dir = "ltr"
autocomplete = "username"
placeholder = "09xxxxxxxxx"
/ >
< / a-form-item >
< a-form-item label = "رمز عبور ادمین" name = "password" >
< a-input-password
v -model :value = "creds.password"
size = "large"
autocomplete = "current-password"
/ >
< / a-form-item >
< / div >
< a-button type = "primary" html -type = " button " size = "large" :loading = "importing" @click ="importSettings" >
پیشنمایش ایمپورت
< / a -button >
< p v-if = "apiBase" class="import-meta" >
از < span dir = "ltr" > { { apiBase } } < / span > · { { count } } مورد
< / p >
< / a-form >
< / section >
< a-modal
v -model :open = "previewOpen"
title = "تصمیم برای هر کلید"
:confirm-loading = "applying"
ok -text = " اعمال "
cancel -text = " انصراف "
width = "760px"
@ok ="applyImport"
>
< p class = "field-hint" >
کلیدهای قالب
< code dir = "ltr" > deploy _settings _const < / code >
بدون سؤال روی همین پروژه نوشته می شوند ( مقدار
< code dir = "ltr" > can _edit = 0 < / code >
خانگی ) . اینجا فقط کلیدهایی می آیند که در آن فایل نیستند .
بدون انتخاب ، پیش فرض « فقط این پروژه » است
< template v-if = "onlySaKeys.length" >
· {{ onlySaKeys.length }} کلید قالب فقط در سوپرادمین با پیشفرض میماند
< / template > .
< / p >
< div v-if = "previewRows.length" class="preview-list" >
< div v-for = "row in previewRows" :key="row.key" class="preview-row" >
< div >
< code dir = "ltr" > { { row . key } } < / code >
< div class = "preview-title" > { { row . title _fa } } < / div >
< div class = "preview-vals" dir = "ltr" > خانگی : { { row . khanegi _value || '—' } } < / div >
< div v-if = "row.superadmin_value != null" class="preview-vals" dir="ltr" >
سوپرادمین : {{ row.superadmin_value | | ' — ' }}
< / div >
< / div >
< a -radio -group v -model :value = "row.scope" size = "small" >
< a-radio-button value = "none" > خیر < / a-radio-button >
< a-radio-button value = "this" > فقط این پروژه < / a-radio-button >
< a-radio-button value = "all" > همه پروژه ها < / a-radio-button >
< / a-radio-group >
< / div >
< / div >
< a-empty
v-else
description = "کلید جدیدی خارج از قالب نیست؛ اعمال همان can_edit=0 خانگی را طبق قالب روی این پروژه مینویسد"
/ >
< / a-modal >
< section class = "form-section" >
< div class = "settings-toolbar" >
@@ -578,20 +368,6 @@ onMounted(load)
font - size : 13 px ;
line - height : 1.7 ;
}
. import - grid {
display : grid ;
gap : 8 px 16 px ;
}
@ media ( min - width : 768 px ) {
. import - grid {
grid - template - columns : 1 fr 1 fr ;
}
}
. import - meta {
margin : 12 px 0 0 ;
color : # 64748 b ;
font - size : 13 px ;
}
. settings - toolbar {
display : flex ;
flex - wrap : wrap ;
@@ -702,29 +478,4 @@ onMounted(load)
. settings - grid {
margin - bottom : 8 px ;
}
. preview - list {
display : grid ;
gap : 10 px ;
max - height : 52 vh ;
overflow : auto ;
}
. preview - row {
display : flex ;
justify - content : space - between ;
gap : 12 px ;
align - items : center ;
padding : 10 px 12 px ;
border : 1 px solid var ( -- line , # e2e8f0 ) ;
border - radius : 12 px ;
background : # fff ;
}
. preview - title {
font - size : 13 px ;
color : # 334155 ;
}
. preview - vals {
font - size : 12 px ;
color : # 64748 b ;
margin - top : 4 px ;
}
< / style >