feat(versions): shared identity/UI/settings tabs and denser desktop form

Keep the same pill nav on all three Khanegi version pages, tighten the identity form on desktop, and split catalog add/delete into separate modals.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-17 21:39:50 +03:30
co-authored by Cursor
parent b879d085d3
commit 6dbb15c6e1
8 changed files with 512 additions and 281 deletions
+94 -79
View File
@@ -1,16 +1,20 @@
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { message, Modal } from 'ant-design-vue'
import { message } from 'ant-design-vue'
import { DeleteOutlined, PlusOutlined } from '@ant-design/icons-vue'
import api from '@/api/http'
import { SETTING_TYPE_OPTIONS } from '@/config/khanegiSettingCatalog'
const props = defineProps<{ canWrite: boolean }>()
const addOpen = ref(false)
const deleteOpen = ref(false)
const saving = ref(false)
const deleting = ref(false)
const activeTab = ref('form')
const jsonText = ref('[\n {\n "key": "name_fa",\n "value": "",\n "title_fa": "نام فارسی",\n "description": "",\n "type": "text",\n "can_edit": 0,\n "is_active": 1\n }\n]')
const jsonText = ref(
'[\n {\n "key": "name_fa",\n "value": "",\n "title_fa": "نام فارسی",\n "description": "",\n "type": "text",\n "can_edit": 0,\n "is_active": 1\n }\n]',
)
const deleteKey = ref('')
const form = reactive({
@@ -33,10 +37,15 @@ function parseJsonItems(raw: string): Array<Record<string, unknown>> {
throw new Error('JSON باید آرایه باشد: [{"key":"name_fa",...}]')
}
async function submit() {
function errText(e: any) {
const msg = e?.response?.data?.message
return Array.isArray(msg) ? msg.join('، ') : msg || 'خطا'
}
async function submitAdd() {
if (!props.canWrite) {
message.warning('دسترسی نوشتن ندارید')
return
return Promise.reject()
}
let items: Array<Record<string, unknown>> = []
try {
@@ -45,78 +54,77 @@ async function submit() {
} else {
if (!form.key.trim()) {
message.warning('اسم کلید لازم است (مثلاً name_fa)')
return
return Promise.reject()
}
items = [{ ...form, key: form.key.trim() }]
}
} catch (e: any) {
message.error(e?.message || 'JSON نامعتبر است')
return
return Promise.reject()
}
Modal.confirm({
title: 'ثبت کلید روی همهٔ نسخه‌ها؟',
content: `${items.length} کلید به قالب اضافه می‌شود و روی پروژه‌هایی که این کلید را ندارند اعمال می‌شود.`,
okText: 'ثبت',
cancelText: 'انصراف',
async onOk() {
saving.value = true
try {
const { data } = await api.post('/setting-templates/keys', { items })
message.success(`${data.count ?? items.length} کلید ثبت شد`)
form.key = ''
form.title_fa = ''
form.value = ''
form.description = ''
} catch (e: any) {
const msg = e?.response?.data?.message
message.error(Array.isArray(msg) ? msg.join('، ') : msg || 'ثبت ناموفق')
throw e
} finally {
saving.value = false
}
},
})
saving.value = true
try {
const { data } = await api.post('/setting-templates/keys', { items })
message.success(`${data.count ?? items.length} کلید ثبت شد`)
form.key = ''
form.title_fa = ''
form.value = ''
form.description = ''
addOpen.value = false
} catch (e: any) {
message.error(errText(e) || 'ثبت ناموفق')
return Promise.reject()
} finally {
saving.value = false
}
}
function removeKey() {
async function submitDelete() {
const key = deleteKey.value.trim()
if (!key) {
message.warning('اسم کلید را بنویس (مثلاً name_fa)')
return
return Promise.reject()
}
deleting.value = true
try {
await api.delete('/setting-templates/keys', { params: { key } })
message.success('کلید حذف شد')
deleteKey.value = ''
deleteOpen.value = false
} catch (e: any) {
message.error(errText(e) || 'حذف ناموفق')
return Promise.reject()
} finally {
deleting.value = false
}
Modal.confirm({
title: `حذف کلید «${key}» از همهٔ پروژه‌ها؟`,
content: 'از قالب و تنظیمات همهٔ نسخه‌های خانگی پاک می‌شود.',
okText: 'حذف',
okType: 'danger',
cancelText: 'انصراف',
async onOk() {
deleting.value = true
try {
await api.delete('/setting-templates/keys', { params: { key } })
message.success('کلید حذف شد')
deleteKey.value = ''
} catch (e: any) {
const msg = e?.response?.data?.message
message.error(Array.isArray(msg) ? msg.join('، ') : msg || 'حذف ناموفق')
throw e
} finally {
deleting.value = false
}
},
})
}
</script>
<template>
<section class="form-section catalog-panel">
<h3>کلید تنظیمات (همهٔ نسخه‌ها)</h3>
<div class="catalog-actions">
<a-button v-if="canWrite" @click="addOpen = true">
<PlusOutlined /> افزودن کلید
</a-button>
<a-button v-if="canWrite" danger ghost @click="deleteOpen = true">
<DeleteOutlined /> حذف کلید
</a-button>
</div>
<a-modal
v-model:open="addOpen"
title="افزودن کلید تنظیمات"
ok-text="ثبت کلید"
cancel-text="انصراف"
:confirm-loading="saving"
:width="720"
destroy-on-close
@ok="submitAdd"
>
<p class="field-hint">
کلید جدید با ثبت هم به قالب نسخه‌های بعدی می‌رود هم روی پروژه‌های فعلی اعمال می‌شود.
با ثبت، کلید به قالب نسخه‌های بعدی می‌رود و روی پروژه‌هایی که این کلید را ندارند اعمال می‌شود.
اسم کلید مثل <code dir="ltr">name_fa</code> است نه عنوان فارسی.
</p>
<a-tabs v-model:activeKey="activeTab">
<a-tab-pane key="form" tab="فرم">
<a-form layout="vertical">
@@ -143,26 +151,37 @@ function removeKey() {
<a-textarea v-model:value="jsonText" :rows="10" dir="ltr" class="json-box" />
</a-tab-pane>
</a-tabs>
</a-modal>
<a-button v-if="canWrite" type="primary" size="large" :loading="saving" @click="submit">
<PlusOutlined /> ثبت کلید
</a-button>
<div class="delete-row">
<a-input
v-model:value="deleteKey"
dir="ltr"
size="large"
placeholder="حذف با اسم کلید — مثلاً name_fa"
/>
<a-button v-if="canWrite" danger :loading="deleting" @click="removeKey">
<DeleteOutlined /> حذف از همه
</a-button>
</div>
</section>
<a-modal
v-model:open="deleteOpen"
title="حذف کلید از همهٔ نسخه‌ها"
ok-text="حذف از همه"
ok-type="danger"
cancel-text="انصراف"
:confirm-loading="deleting"
:width="520"
destroy-on-close
@ok="submitDelete"
>
<p class="field-hint">
این کلید از قالب و تنظیمات همهٔ پروژه‌های خانگی پاک می‌شود. اسم کلید را بنویس، نه عنوان فارسی.
</p>
<a-input
v-model:value="deleteKey"
dir="ltr"
size="large"
placeholder="مثلاً name_fa"
/>
</a-modal>
</template>
<style scoped>
.catalog-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.field-hint {
margin: 0 0 12px;
color: #64748b;
@@ -175,13 +194,9 @@ function removeKey() {
gap: 0 12px;
}
@media (max-width: 720px) {
.catalog-grid { grid-template-columns: 1fr; }
}
.delete-row {
display: flex;
gap: 8px;
margin-top: 16px;
align-items: center;
.catalog-grid {
grid-template-columns: 1fr;
}
}
.json-box {
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;