2026-08-17 20:14:17 +03:30
|
|
|
|
<script setup lang="ts">
|
|
|
|
|
|
import { reactive, ref } from 'vue'
|
2026-08-17 21:39:50 +03:30
|
|
|
|
import { message } from 'ant-design-vue'
|
2026-08-17 20:14:17 +03:30
|
|
|
|
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 }>()
|
|
|
|
|
|
|
2026-08-17 21:39:50 +03:30
|
|
|
|
const addOpen = ref(false)
|
|
|
|
|
|
const deleteOpen = ref(false)
|
2026-08-17 20:14:17 +03:30
|
|
|
|
const saving = ref(false)
|
|
|
|
|
|
const deleting = ref(false)
|
|
|
|
|
|
const activeTab = ref('form')
|
2026-08-17 21:39:50 +03:30
|
|
|
|
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]',
|
|
|
|
|
|
)
|
2026-08-17 20:14:17 +03:30
|
|
|
|
const deleteKey = ref('')
|
|
|
|
|
|
|
|
|
|
|
|
const form = reactive({
|
|
|
|
|
|
key: '',
|
|
|
|
|
|
title_fa: '',
|
|
|
|
|
|
value: '',
|
|
|
|
|
|
description: '',
|
|
|
|
|
|
type: 'text',
|
|
|
|
|
|
type_data: '',
|
|
|
|
|
|
can_edit: 0,
|
|
|
|
|
|
is_active: 1,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const typeOptions = SETTING_TYPE_OPTIONS.map((t) => ({ value: t.key, label: t.value }))
|
|
|
|
|
|
|
|
|
|
|
|
function parseJsonItems(raw: string): Array<Record<string, unknown>> {
|
|
|
|
|
|
const parsed = JSON.parse(raw)
|
|
|
|
|
|
if (Array.isArray(parsed)) return parsed
|
|
|
|
|
|
if (parsed && typeof parsed === 'object' && Array.isArray(parsed.items)) return parsed.items
|
|
|
|
|
|
throw new Error('JSON باید آرایه باشد: [{"key":"name_fa",...}]')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 21:39:50 +03:30
|
|
|
|
function errText(e: any) {
|
|
|
|
|
|
const msg = e?.response?.data?.message
|
|
|
|
|
|
return Array.isArray(msg) ? msg.join('، ') : msg || 'خطا'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function submitAdd() {
|
2026-08-17 20:14:17 +03:30
|
|
|
|
if (!props.canWrite) {
|
|
|
|
|
|
message.warning('دسترسی نوشتن ندارید')
|
2026-08-17 21:39:50 +03:30
|
|
|
|
return Promise.reject()
|
2026-08-17 20:14:17 +03:30
|
|
|
|
}
|
|
|
|
|
|
let items: Array<Record<string, unknown>> = []
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (activeTab.value === 'json') {
|
|
|
|
|
|
items = parseJsonItems(jsonText.value)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
if (!form.key.trim()) {
|
|
|
|
|
|
message.warning('اسم کلید لازم است (مثلاً name_fa)')
|
2026-08-17 21:39:50 +03:30
|
|
|
|
return Promise.reject()
|
2026-08-17 20:14:17 +03:30
|
|
|
|
}
|
|
|
|
|
|
items = [{ ...form, key: form.key.trim() }]
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (e: any) {
|
|
|
|
|
|
message.error(e?.message || 'JSON نامعتبر است')
|
2026-08-17 21:39:50 +03:30
|
|
|
|
return Promise.reject()
|
2026-08-17 20:14:17 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 21:39:50 +03:30
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-08-17 20:14:17 +03:30
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-17 21:39:50 +03:30
|
|
|
|
async function submitDelete() {
|
2026-08-17 20:14:17 +03:30
|
|
|
|
const key = deleteKey.value.trim()
|
|
|
|
|
|
if (!key) {
|
|
|
|
|
|
message.warning('اسم کلید را بنویس (مثلاً name_fa)')
|
2026-08-17 21:39:50 +03:30
|
|
|
|
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
|
2026-08-17 20:14:17 +03:30
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-08-17 21:39:50 +03:30
|
|
|
|
<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"
|
|
|
|
|
|
>
|
2026-08-17 20:14:17 +03:30
|
|
|
|
<p class="field-hint">
|
2026-08-17 21:39:50 +03:30
|
|
|
|
با ثبت، کلید به قالب نسخههای بعدی میرود و روی پروژههایی که این کلید را ندارند اعمال میشود.
|
2026-08-17 20:14:17 +03:30
|
|
|
|
اسم کلید مثل <code dir="ltr">name_fa</code> است نه عنوان فارسی.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<a-tabs v-model:activeKey="activeTab">
|
|
|
|
|
|
<a-tab-pane key="form" tab="فرم">
|
|
|
|
|
|
<a-form layout="vertical">
|
|
|
|
|
|
<div class="catalog-grid">
|
|
|
|
|
|
<a-form-item label="کلید (key)" required>
|
|
|
|
|
|
<a-input v-model:value="form.key" dir="ltr" placeholder="name_fa" size="large" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item label="عنوان فارسی">
|
|
|
|
|
|
<a-input v-model:value="form.title_fa" size="large" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item label="مقدار پیشفرض">
|
|
|
|
|
|
<a-input v-model:value="form.value" dir="ltr" size="large" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
<a-form-item label="نوع">
|
|
|
|
|
|
<a-select v-model:value="form.type" :options="typeOptions" size="large" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<a-form-item label="توضیح">
|
|
|
|
|
|
<a-textarea v-model:value="form.description" :rows="2" />
|
|
|
|
|
|
</a-form-item>
|
|
|
|
|
|
</a-form>
|
|
|
|
|
|
</a-tab-pane>
|
|
|
|
|
|
<a-tab-pane key="json" tab="JSON">
|
|
|
|
|
|
<a-textarea v-model:value="jsonText" :rows="10" dir="ltr" class="json-box" />
|
|
|
|
|
|
</a-tab-pane>
|
|
|
|
|
|
</a-tabs>
|
2026-08-17 21:39:50 +03:30
|
|
|
|
</a-modal>
|
2026-08-17 20:14:17 +03:30
|
|
|
|
|
2026-08-17 21:39:50 +03:30
|
|
|
|
<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>
|
2026-08-17 20:14:17 +03:30
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2026-08-17 21:39:50 +03:30
|
|
|
|
.catalog-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
}
|
2026-08-17 20:14:17 +03:30
|
|
|
|
.field-hint {
|
|
|
|
|
|
margin: 0 0 12px;
|
|
|
|
|
|
color: #64748b;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
}
|
|
|
|
|
|
.catalog-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
|
|
|
gap: 0 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
@media (max-width: 720px) {
|
2026-08-17 21:39:50 +03:30
|
|
|
|
.catalog-grid {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
2026-08-17 20:14:17 +03:30
|
|
|
|
}
|
|
|
|
|
|
.json-box {
|
|
|
|
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
|
|
|
|
font-size: 13px;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|