2026-07-23 10:24:50 +03:30
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
|
|
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
|
|
|
|
import { message } from 'ant-design-vue'
|
2026-07-30 18:19:54 +03:30
|
|
|
|
import { ArrowRightOutlined, PlusOutlined, DeleteOutlined } from '@ant-design/icons-vue'
|
2026-07-23 10:24:50 +03:30
|
|
|
|
import api from '@/api/http'
|
2026-08-17 21:39:50 +03:30
|
|
|
|
import VersionPageTabs from '@/components/VersionPageTabs.vue'
|
2026-08-10 15:30:26 +03:30
|
|
|
|
import { useAuthStore } from '@/stores/auth'
|
2026-08-15 15:44:44 +03:30
|
|
|
|
import { formatIranDateTime } from '@/utils/datetime'
|
2026-07-23 10:24:50 +03:30
|
|
|
|
|
|
|
|
|
|
const route = useRoute()
|
|
|
|
|
|
const router = useRouter()
|
2026-08-10 15:30:26 +03:30
|
|
|
|
const auth = useAuthStore()
|
2026-07-23 10:24:50 +03:30
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
const saving = ref(false)
|
2026-07-30 18:19:54 +03:30
|
|
|
|
const uploading = ref(false)
|
2026-08-15 15:44:44 +03:30
|
|
|
|
const createdAt = ref<string | null>(null)
|
2026-07-23 10:24:50 +03:30
|
|
|
|
|
|
|
|
|
|
const isEdit = computed(() => Boolean(route.params.id))
|
|
|
|
|
|
|
|
|
|
|
|
const form = reactive({
|
|
|
|
|
|
product: 'hamkar' as 'hamkar' | 'khanegi',
|
|
|
|
|
|
slug: '',
|
|
|
|
|
|
nameFa: '',
|
|
|
|
|
|
nameEn: '',
|
|
|
|
|
|
domain: '',
|
2026-08-17 20:14:17 +03:30
|
|
|
|
domains: [] as string[],
|
2026-07-23 10:24:50 +03:30
|
|
|
|
avatarUrl: '',
|
2026-07-30 18:19:54 +03:30
|
|
|
|
appVersion: '1',
|
2026-08-09 12:58:44 +03:30
|
|
|
|
uiVariant: 'modular' as 'modular' | 'newTheme',
|
2026-07-23 10:24:50 +03:30
|
|
|
|
nodeServer: '',
|
|
|
|
|
|
nodeWs: '',
|
|
|
|
|
|
appUrl: '',
|
|
|
|
|
|
adminUrl: '',
|
|
|
|
|
|
remoteApiToken: '',
|
|
|
|
|
|
isActive: true,
|
|
|
|
|
|
notes: '',
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const productLabel = computed(() => (form.product === 'hamkar' ? 'همکار' : 'خانگی'))
|
2026-08-10 15:30:26 +03:30
|
|
|
|
const canWrite = computed(() =>
|
|
|
|
|
|
form.product === 'hamkar' ? auth.can('hamkar:write') : auth.can('khanegi:write'),
|
|
|
|
|
|
)
|
2026-07-23 10:24:50 +03:30
|
|
|
|
|
|
|
|
|
|
function backTarget() {
|
|
|
|
|
|
return {
|
|
|
|
|
|
name: 'product-versions' as const,
|
|
|
|
|
|
params: { product: form.product },
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function load() {
|
|
|
|
|
|
if (!isEdit.value) {
|
|
|
|
|
|
const q = String(route.query.product || '')
|
|
|
|
|
|
form.product = q === 'khanegi' ? 'khanegi' : 'hamkar'
|
2026-08-17 20:14:17 +03:30
|
|
|
|
form.domains = []
|
2026-08-15 15:44:44 +03:30
|
|
|
|
createdAt.value = null
|
2026-07-23 10:24:50 +03:30
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const { data } = await api.get(`/tenants/${route.params.id}`)
|
2026-08-15 15:44:44 +03:30
|
|
|
|
createdAt.value = data.createdAt || null
|
2026-07-23 10:24:50 +03:30
|
|
|
|
Object.assign(form, {
|
|
|
|
|
|
product: data.product,
|
|
|
|
|
|
slug: data.slug,
|
|
|
|
|
|
nameFa: data.nameFa,
|
|
|
|
|
|
nameEn: data.nameEn || '',
|
|
|
|
|
|
domain: data.domain,
|
2026-08-17 20:14:17 +03:30
|
|
|
|
domains: Array.isArray(data.domains) ? data.domains.map(String) : [],
|
2026-07-23 10:24:50 +03:30
|
|
|
|
avatarUrl: data.avatarUrl || '',
|
2026-07-30 18:19:54 +03:30
|
|
|
|
appVersion: data.appVersion || '1',
|
2026-08-09 12:58:44 +03:30
|
|
|
|
uiVariant: data.uiVariant === 'newTheme' ? 'newTheme' : 'modular',
|
2026-07-23 10:24:50 +03:30
|
|
|
|
nodeServer: data.nodeServer,
|
|
|
|
|
|
nodeWs: data.nodeWs || '',
|
|
|
|
|
|
appUrl: data.appUrl || '',
|
|
|
|
|
|
adminUrl: data.adminUrl || '',
|
|
|
|
|
|
remoteApiToken: '',
|
|
|
|
|
|
isActive: data.isActive,
|
|
|
|
|
|
notes: data.notes || '',
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
message.error(e?.response?.data?.message || 'خطا در بارگذاری')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-30 18:19:54 +03:30
|
|
|
|
async function uploadLogo(options: {
|
|
|
|
|
|
file: string | Blob | File
|
|
|
|
|
|
onSuccess?: (body?: unknown) => void
|
|
|
|
|
|
onError?: (e: Error) => void
|
|
|
|
|
|
}) {
|
|
|
|
|
|
const file = options.file
|
|
|
|
|
|
if (!(file instanceof File) && !(file instanceof Blob)) return
|
|
|
|
|
|
|
|
|
|
|
|
uploading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const body = new FormData()
|
|
|
|
|
|
body.append('file', file)
|
|
|
|
|
|
const { data } = await api.post<{ url: string }>('/tenants/upload-avatar', body)
|
|
|
|
|
|
form.avatarUrl = data.url
|
|
|
|
|
|
options.onSuccess?.(data)
|
|
|
|
|
|
message.success('لوگو آپلود شد')
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
const msg = e?.response?.data?.message
|
|
|
|
|
|
options.onError?.(e instanceof Error ? e : new Error(String(msg || 'upload failed')))
|
|
|
|
|
|
message.error(Array.isArray(msg) ? msg.join('، ') : msg || 'آپلود ناموفق')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
uploading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function clearLogo() {
|
|
|
|
|
|
form.avatarUrl = ''
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-23 10:24:50 +03:30
|
|
|
|
async function save() {
|
2026-08-10 15:30:26 +03:30
|
|
|
|
if (!canWrite.value) {
|
|
|
|
|
|
message.warning('دسترسی نوشتن ندارید')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2026-07-23 10:24:50 +03:30
|
|
|
|
saving.value = true
|
|
|
|
|
|
try {
|
2026-08-17 20:14:17 +03:30
|
|
|
|
const payload = {
|
|
|
|
|
|
...form,
|
|
|
|
|
|
domains: form.domains.map((item) => item.trim()).filter(Boolean),
|
|
|
|
|
|
}
|
2026-07-23 10:24:50 +03:30
|
|
|
|
if (!payload.remoteApiToken) {
|
|
|
|
|
|
delete (payload as any).remoteApiToken
|
|
|
|
|
|
}
|
|
|
|
|
|
if (isEdit.value) {
|
|
|
|
|
|
await api.patch(`/tenants/${route.params.id}`, payload)
|
|
|
|
|
|
message.success('بهروزرسانی شد')
|
|
|
|
|
|
} else {
|
|
|
|
|
|
await api.post('/tenants', payload)
|
|
|
|
|
|
message.success('نسخه ایجاد شد')
|
|
|
|
|
|
}
|
|
|
|
|
|
router.push(backTarget())
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
const msg = e?.response?.data?.message
|
|
|
|
|
|
message.error(Array.isArray(msg) ? msg.join('، ') : msg || 'ذخیره ناموفق')
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
saving.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
watch(() => route.fullPath, load)
|
|
|
|
|
|
onMounted(load)
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-08-17 21:39:50 +03:30
|
|
|
|
<div class="page-shell tenant-form" v-if="!loading">
|
|
|
|
|
|
<div class="page-head">
|
|
|
|
|
|
<a-button type="text" class="back-btn" @click="router.push(backTarget())">
|
2026-07-23 10:24:50 +03:30
|
|
|
|
<ArrowRightOutlined />
|
|
|
|
|
|
</a-button>
|
2026-08-17 21:39:50 +03:30
|
|
|
|
<div class="page-head-text">
|
2026-07-23 10:24:50 +03:30
|
|
|
|
<h1 class="page-title">{{ isEdit ? 'مدیریت نسخه' : 'نسخه فروشی جدید' }}</h1>
|
|
|
|
|
|
<p class="page-sub">محصول: {{ productLabel }} · تنظیمات رجیستری برند (/get_user)</p>
|
|
|
|
|
|
</div>
|
2026-08-17 21:39:50 +03:30
|
|
|
|
<VersionPageTabs
|
|
|
|
|
|
v-if="isEdit && form.product === 'khanegi'"
|
|
|
|
|
|
class="page-nav-slot"
|
|
|
|
|
|
active="identity"
|
|
|
|
|
|
:tenant-id="String(route.params.id)"
|
|
|
|
|
|
:ui-route-name="form.uiVariant === 'newTheme' ? 'version-ui' : 'version-ui-modular'"
|
|
|
|
|
|
/>
|
2026-07-23 10:24:50 +03:30
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-08-17 21:39:50 +03:30
|
|
|
|
<a-form :model="form" layout="vertical" class="tenant-form-body" @finish="save">
|
|
|
|
|
|
<div class="form-top">
|
|
|
|
|
|
<section class="form-section">
|
|
|
|
|
|
<h3>هویت نسخه</h3>
|
|
|
|
|
|
<div class="field-grid">
|
|
|
|
|
|
<a-form-item label="محصول" name="product">
|
|
|
|
|
|
<a-select v-model:value="form.product" :disabled="isEdit" size="large">
|
|
|
|
|
|
<a-select-option value="hamkar">همکار</a-select-option>
|
|
|
|
|
|
<a-select-option value="khanegi">خانگی</a-select-option>
|
|
|
|
|
|
</a-select>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item label="نام فارسی" name="nameFa" :rules="[{ required: true, message: 'لازم است' }]">
|
|
|
|
|
|
<a-input v-model:value="form.nameFa" size="large" placeholder="مثلاً باختر گلد" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item label="slug لاتین" name="slug" :rules="[{ required: true, message: 'لازم است' }]">
|
|
|
|
|
|
<a-input v-model:value="form.slug" size="large" placeholder="bakhtargold" dir="ltr" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item label="نام لاتین (اختیاری)" name="nameEn">
|
|
|
|
|
|
<a-input v-model:value="form.nameEn" size="large" dir="ltr" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item label="دامنه فروشگاه" name="domain" :rules="[{ required: true, message: 'لازم است' }]">
|
|
|
|
|
|
<a-input v-model:value="form.domain" size="large" placeholder="example.com" dir="ltr" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item v-if="isEdit" label="تاریخ ایجاد">
|
|
|
|
|
|
<a-input :value="formatIranDateTime(createdAt)" size="large" disabled />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item label="دامنهها و IPهای اضافی" class="field-span-2">
|
|
|
|
|
|
<p class="field-hint">
|
|
|
|
|
|
برای /get_user. داخل همان محصول (خانگی یا همکار) هر دامنه/IP فقط روی یک نسخه میماند؛
|
|
|
|
|
|
اگر از قبل روی نسخه دیگری از همین محصول باشد، از آنجا برداشته و به این نسخه اضافه میشود.
|
|
|
|
|
|
خانگی و همکار روی هم اثر ندارند.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div class="alias-list">
|
|
|
|
|
|
<div v-for="(_, index) in form.domains" :key="index" class="alias-row">
|
|
|
|
|
|
<a-input
|
|
|
|
|
|
v-model:value="form.domains[index]"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
dir="ltr"
|
|
|
|
|
|
placeholder="مثلاً 192.168.1.10 یا shop.example.com"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<a-button danger @click="form.domains.splice(index, 1)">
|
|
|
|
|
|
<DeleteOutlined />
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<a-button class="alias-add" @click="form.domains.push('')">
|
|
|
|
|
|
<PlusOutlined /> افزودن دامنه / IP
|
|
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<section class="form-section logo-card">
|
|
|
|
|
|
<h3>لوگو</h3>
|
2026-08-17 20:14:17 +03:30
|
|
|
|
<p class="field-hint">
|
2026-08-17 21:39:50 +03:30
|
|
|
|
از طریق /get_user بهعنوان <code dir="ltr">avatar</code> فرستاده میشود.
|
|
|
|
|
|
<template v-if="form.product === 'khanegi'">
|
|
|
|
|
|
رنگ و تم از «ظاهر اپ»
|
|
|
|
|
|
({{ form.uiVariant === 'newTheme' ? 'New theme' : 'Modular' }}).
|
|
|
|
|
|
</template>
|
2026-08-17 20:14:17 +03:30
|
|
|
|
</p>
|
2026-08-17 21:39:50 +03:30
|
|
|
|
<a-form-item name="avatarUrl" class="logo-item">
|
|
|
|
|
|
<div class="logo-upload-row">
|
|
|
|
|
|
<a-upload
|
|
|
|
|
|
name="file"
|
|
|
|
|
|
list-type="picture-card"
|
|
|
|
|
|
:show-upload-list="false"
|
|
|
|
|
|
accept="image/jpeg,image/png,image/gif,image/webp,image/svg+xml"
|
|
|
|
|
|
:custom-request="uploadLogo"
|
|
|
|
|
|
:disabled="uploading"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div v-if="form.avatarUrl" class="logo-preview">
|
|
|
|
|
|
<img :src="form.avatarUrl" alt="لوگو" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div v-else class="logo-placeholder">
|
|
|
|
|
|
<PlusOutlined />
|
|
|
|
|
|
<div>{{ uploading ? 'در حال آپلود…' : 'آپلود لوگو' }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a-upload>
|
|
|
|
|
|
<a-button v-if="form.avatarUrl" danger @click="clearLogo">
|
|
|
|
|
|
<DeleteOutlined /> حذف لوگو
|
2026-08-17 20:14:17 +03:30
|
|
|
|
</a-button>
|
|
|
|
|
|
</div>
|
2026-08-17 21:39:50 +03:30
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
2026-07-23 10:24:50 +03:30
|
|
|
|
|
|
|
|
|
|
<section class="form-section">
|
|
|
|
|
|
<h3>اتصال سرور این نسخه</h3>
|
2026-08-17 21:39:50 +03:30
|
|
|
|
<div class="field-grid">
|
|
|
|
|
|
<a-form-item label="آدرس API (node_server)" name="nodeServer" :rules="[{ required: true, message: 'لازم است' }]">
|
|
|
|
|
|
<a-input v-model:value="form.nodeServer" size="large" dir="ltr" placeholder="https://example.com/server" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item
|
|
|
|
|
|
:label="form.product === 'hamkar' ? 'WebSocket (node_ws)' : 'WebSocket اختیاری (خانگی بیشتر SSE است)'"
|
|
|
|
|
|
name="nodeWs"
|
|
|
|
|
|
>
|
|
|
|
|
|
<a-input v-model:value="form.nodeWs" size="large" dir="ltr" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item label="app_url" name="appUrl">
|
|
|
|
|
|
<a-input v-model:value="form.appUrl" size="large" dir="ltr" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item label="admin_url" name="adminUrl">
|
|
|
|
|
|
<a-input v-model:value="form.adminUrl" size="large" dir="ltr" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item label="توکن ریموت سوپر (برای تنظیمات داخل سرور نسخه)" name="remoteApiToken" class="field-span-2">
|
|
|
|
|
|
<a-input-password v-model:value="form.remoteApiToken" size="large" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</div>
|
2026-07-23 10:24:50 +03:30
|
|
|
|
</section>
|
|
|
|
|
|
|
2026-08-17 21:39:50 +03:30
|
|
|
|
<section class="form-section form-section--last">
|
2026-07-23 10:24:50 +03:30
|
|
|
|
<h3>وضعیت</h3>
|
2026-08-17 21:39:50 +03:30
|
|
|
|
<div class="field-grid">
|
|
|
|
|
|
<a-form-item label="فعال در رجیستری">
|
|
|
|
|
|
<a-switch v-model:checked="form.isActive" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item label="ورژن اپ" name="appVersion">
|
|
|
|
|
|
<a-input
|
|
|
|
|
|
v-model:value="form.appVersion"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
dir="ltr"
|
|
|
|
|
|
placeholder="مثلاً 2.0.1"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<template #extra>
|
|
|
|
|
|
از طریق <code dir="ltr">/get_version</code> به اپ فرستاده میشود. با تغییرش، کلاینت کش کانفیگ را رفرش و صفحه را reload میکند.
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item label="یادداشت داخلی" class="field-span-2">
|
|
|
|
|
|
<a-textarea v-model:value="form.notes" :rows="3" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</div>
|
2026-07-23 10:24:50 +03:30
|
|
|
|
</section>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="sticky-actions">
|
2026-08-10 15:30:26 +03:30
|
|
|
|
<a-button
|
|
|
|
|
|
v-if="canWrite"
|
2026-08-17 21:39:50 +03:30
|
|
|
|
class="save-btn"
|
2026-08-10 15:30:26 +03:30
|
|
|
|
type="primary"
|
|
|
|
|
|
html-type="submit"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
:loading="saving"
|
|
|
|
|
|
>
|
|
|
|
|
|
ذخیره
|
|
|
|
|
|
</a-button>
|
2026-07-23 10:24:50 +03:30
|
|
|
|
<a-button size="large" @click="router.push(backTarget())">انصراف</a-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</a-form>
|
|
|
|
|
|
</div>
|
2026-08-17 21:39:50 +03:30
|
|
|
|
<div v-else class="page-shell tenant-form-loading">
|
2026-07-23 10:24:50 +03:30
|
|
|
|
<a-spin />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
2026-07-30 18:19:54 +03:30
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-08-17 21:39:50 +03:30
|
|
|
|
.page-head {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
margin-bottom: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.back-btn {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.page-head-text {
|
|
|
|
|
|
flex: 1 1 180px;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.page-nav-slot {
|
|
|
|
|
|
margin-inline-start: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-30 18:19:54 +03:30
|
|
|
|
.field-hint {
|
|
|
|
|
|
margin: 0 0 12px;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 21:39:50 +03:30
|
|
|
|
.field-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
column-gap: 16px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.field-grid :deep(.ant-form-item) {
|
|
|
|
|
|
margin-bottom: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.logo-item {
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-30 18:19:54 +03:30
|
|
|
|
.logo-upload-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.logo-preview,
|
|
|
|
|
|
.logo-placeholder {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.logo-preview img {
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
max-height: 100%;
|
|
|
|
|
|
object-fit: contain;
|
|
|
|
|
|
}
|
2026-08-17 20:14:17 +03:30
|
|
|
|
|
|
|
|
|
|
.alias-list {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.alias-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.alias-row :deep(.ant-input) {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
2026-08-17 21:39:50 +03:30
|
|
|
|
|
|
|
|
|
|
.alias-add {
|
|
|
|
|
|
justify-self: start;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.save-btn {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tenant-form-loading {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
place-items: center;
|
|
|
|
|
|
min-height: 200px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (min-width: 768px) {
|
|
|
|
|
|
.field-grid {
|
|
|
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.field-span-2 {
|
|
|
|
|
|
grid-column: 1 / -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.save-btn {
|
|
|
|
|
|
width: auto;
|
|
|
|
|
|
min-width: 168px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tenant-form :deep(.sticky-actions) {
|
|
|
|
|
|
justify-content: flex-start;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (min-width: 960px) {
|
|
|
|
|
|
.form-top {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: minmax(0, 1fr) 240px;
|
|
|
|
|
|
gap: 20px;
|
|
|
|
|
|
align-items: start;
|
|
|
|
|
|
margin-bottom: 18px;
|
|
|
|
|
|
padding-bottom: 14px;
|
|
|
|
|
|
border-bottom: 1px solid var(--line);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-top .form-section {
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
padding-bottom: 0;
|
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.logo-card.form-section {
|
|
|
|
|
|
padding: 16px;
|
|
|
|
|
|
border: 1px solid var(--line);
|
|
|
|
|
|
border-radius: 18px;
|
|
|
|
|
|
background: #f8fafc;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-section--last {
|
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-30 18:19:54 +03:30
|
|
|
|
</style>
|