Files
web/src/pages/ProductVersionsPage.vue
T
MahdiandCursor df83af90e8 feat(auth): enforce admin permissions in UI
Add can()/router meta, HomePage module filtering, Admins permission
matrix, and hide write actions when the user lacks *:write.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-10 15:30:26 +03:30

200 lines
6.2 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'
import { useAuthStore } from '@/stores/auth'
type Tenant = {
id: string
product: 'hamkar' | 'khanegi'
slug: string
nameFa: string
nameEn?: string | null
domain: string
avatarUrl?: string | null
primaryColor?: string | null
isActive: boolean
nodeServer: string
notes?: string | null
hasAppUiConfig?: boolean
uiVariant?: 'modular' | 'newTheme'
}
const route = useRoute()
const router = useRouter()
const auth = useAuthStore()
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 canWrite = computed(() =>
product.value === 'hamkar' ? auth.can('hamkar:write') : auth.can('khanegi:write'),
)
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 v-if="canWrite" 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 v-if="item.nameEn" color="blue">{{ item.nameEn }}</a-tag>
<a-tag :color="item.isActive ? 'success' : 'default'">
{{ item.isActive ? 'فعال' : 'غیرفعال' }}
</a-tag>
<a-tag v-if="item.notes?.includes('classifyConfidence:low')" color="orange">
بررسی محصول
</a-tag>
<a-tag v-if="item.hasAppUiConfig" color="purple">ظاهر اپ</a-tag>
<a-tag v-if="product === 'khanegi'" :color="item.uiVariant === 'newTheme' ? 'geekblue' : 'cyan'">
{{ item.uiVariant === 'newTheme' ? 'New theme' : 'Modular' }}
</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
type="default"
block
style="flex: 1"
@click="
router.push({
name:
product === 'hamkar'
? 'version-ui-hamkar'
: item.uiVariant === 'newTheme'
? 'version-ui'
: 'version-ui-modular',
params: { id: item.id },
})
"
>
ظاهر اپ
</a-button>
<a-button
v-if="canWrite"
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 v-if="canWrite" type="primary" @click="addVersion">افزودن اولین نسخه</a-button>
</a-empty>
</a-spin>
</div>
</template>