165 lines
5.0 KiB
Vue
165 lines
5.0 KiB
Vue
<script setup lang="ts">
|
|||
|
|
import { computed, onMounted, ref, watch } from 'vue'
|
||
|
|
import { useRoute, useRouter } from 'vue-router'
|
||
|
|
import { message, Modal } from 'ant-design-vue'
|
||
|
|
import { ArrowRightOutlined, PlusOutlined, SearchOutlined } from '@ant-design/icons-vue'
|
||
|
|
import api from '@/api/http'
|
||
|
|
|
||
|
|
type Tenant = {
|
||
|
|
id: string
|
||
|
|
product: 'hamkar' | 'khanegi'
|
||
|
|
slug: string
|
||
|
|
nameFa: string
|
||
|
|
domain: string
|
||
|
|
avatarUrl?: string | null
|
||
|
|
primaryColor?: string | null
|
||
|
|
isActive: boolean
|
||
|
|
nodeServer: string
|
||
|
|
}
|
||
|
|
|
||
|
|
const route = useRoute()
|
||
|
|
const router = useRouter()
|
||
|
|
const loading = ref(false)
|
||
|
|
const rows = ref<Tenant[]>([])
|
||
|
|
const q = ref('')
|
||
|
|
|
||
|
|
const product = computed(() => {
|
||
|
|
const p = String(route.params.product || '')
|
||
|
|
return p === 'khanegi' ? 'khanegi' : 'hamkar'
|
||
|
|
})
|
||
|
|
|
||
|
|
const title = computed(() => (product.value === 'hamkar' ? 'همکار' : 'خانگی'))
|
||
|
|
const subtitle = computed(() =>
|
||
|
|
product.value === 'hamkar'
|
||
|
|
? 'نسخههای فروشی پروژه همکار'
|
||
|
|
: 'نسخههای فروشی پروژه خانگی (Modular)',
|
||
|
|
)
|
||
|
|
|
||
|
|
const filtered = computed(() => {
|
||
|
|
const term = q.value.trim().toLowerCase()
|
||
|
|
if (!term) return rows.value
|
||
|
|
return rows.value.filter(
|
||
|
|
(t) =>
|
||
|
|
t.nameFa.toLowerCase().includes(term) ||
|
||
|
|
t.domain.toLowerCase().includes(term) ||
|
||
|
|
t.slug.toLowerCase().includes(term),
|
||
|
|
)
|
||
|
|
})
|
||
|
|
|
||
|
|
async function load() {
|
||
|
|
loading.value = true
|
||
|
|
try {
|
||
|
|
const { data } = await api.get('/tenants', { params: { product: product.value } })
|
||
|
|
rows.value = data
|
||
|
|
} catch (e: any) {
|
||
|
|
message.error(e?.response?.data?.message || 'خطا در دریافت نسخهها')
|
||
|
|
} finally {
|
||
|
|
loading.value = false
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function addVersion() {
|
||
|
|
router.push({ name: 'version-create', query: { product: product.value } })
|
||
|
|
}
|
||
|
|
|
||
|
|
function edit(id: string) {
|
||
|
|
router.push({ name: 'version-edit', params: { id } })
|
||
|
|
}
|
||
|
|
|
||
|
|
function remove(record: Tenant) {
|
||
|
|
Modal.confirm({
|
||
|
|
title: `حذف نسخه «${record.nameFa}»؟`,
|
||
|
|
content: 'فقط از رجیستری سوپرادمین حذف میشود؛ سرور مشتری را پاک نمیکند.',
|
||
|
|
okText: 'حذف',
|
||
|
|
okType: 'danger',
|
||
|
|
cancelText: 'انصراف',
|
||
|
|
async onOk() {
|
||
|
|
await api.delete(`/tenants/${record.id}`)
|
||
|
|
message.success('حذف شد')
|
||
|
|
await load()
|
||
|
|
},
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
watch(product, load, { immediate: true })
|
||
|
|
onMounted(load)
|
||
|
|
</script>
|
||
|
|
|
||
|
|
<template>
|
||
|
|
<div class="page-shell">
|
||
|
|
<div style="display: flex; align-items: flex-start; gap: 8px; margin-bottom: 4px">
|
||
|
|
<a-button type="text" @click="router.push({ name: 'home' })">
|
||
|
|
<ArrowRightOutlined />
|
||
|
|
</a-button>
|
||
|
|
<div style="flex: 1">
|
||
|
|
<h1 class="page-title">{{ title }}</h1>
|
||
|
|
<p class="page-sub">{{ subtitle }} · {{ filtered.length }} مورد</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="toolbar">
|
||
|
|
<a-input
|
||
|
|
v-model:value="q"
|
||
|
|
allow-clear
|
||
|
|
placeholder="جستجو نام، دامنه یا slug"
|
||
|
|
style="flex: 1; min-width: 180px"
|
||
|
|
>
|
||
|
|
<template #prefix><SearchOutlined /></template>
|
||
|
|
</a-input>
|
||
|
|
<a-button type="primary" @click="addVersion">
|
||
|
|
<PlusOutlined /> نسخه جدید
|
||
|
|
</a-button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<a-spin :spinning="loading">
|
||
|
|
<div v-if="filtered.length" class="version-list">
|
||
|
|
<article v-for="item in filtered" :key="item.id" class="version-card">
|
||
|
|
<div class="version-card-head">
|
||
|
|
<img
|
||
|
|
v-if="item.avatarUrl"
|
||
|
|
class="version-avatar"
|
||
|
|
:src="item.avatarUrl"
|
||
|
|
:alt="item.nameFa"
|
||
|
|
/>
|
||
|
|
<div
|
||
|
|
v-else
|
||
|
|
class="version-avatar fallback"
|
||
|
|
:style="{ background: item.primaryColor || '#1d4ed8' }"
|
||
|
|
>
|
||
|
|
{{ item.nameFa.slice(0, 1) }}
|
||
|
|
</div>
|
||
|
|
<div class="version-meta">
|
||
|
|
<h3>{{ item.nameFa }}</h3>
|
||
|
|
<div class="domain">{{ item.domain }}</div>
|
||
|
|
<div style="margin-top: 8px">
|
||
|
|
<a-tag>{{ item.slug }}</a-tag>
|
||
|
|
<a-tag :color="item.isActive ? 'success' : 'default'">
|
||
|
|
{{ item.isActive ? 'فعال' : 'غیرفعال' }}
|
||
|
|
</a-tag>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<div class="version-actions">
|
||
|
|
<a-button type="primary" ghost block style="flex: 1" @click="edit(item.id)">
|
||
|
|
مدیریت
|
||
|
|
</a-button>
|
||
|
|
<a-button
|
||
|
|
v-if="product === 'khanegi'"
|
||
|
|
type="default"
|
||
|
|
block
|
||
|
|
style="flex: 1"
|
||
|
|
@click="router.push({ name: 'version-ui', params: { id: item.id } })"
|
||
|
|
>
|
||
|
|
ظاهر اپ
|
||
|
|
</a-button>
|
||
|
|
<a-button danger ghost style="flex: 0 0 auto" @click="remove(item)">حذف</a-button>
|
||
|
|
</div>
|
||
|
|
</article>
|
||
|
|
</div>
|
||
|
|
<a-empty v-else description="نسخهای ثبت نشده" style="margin: 32px 0">
|
||
|
|
<a-button type="primary" @click="addVersion">افزودن اولین نسخه</a-button>
|
||
|
|
</a-empty>
|
||
|
|
</a-spin>
|
||
|
|
</div>
|
||
|
|
</template>
|