fix(plugins): show tenant domain and move store prices into a tab

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-08 11:01:00 +03:30
co-authored by Cursor
parent 2a9e8ea023
commit ca923c7d9b
3 changed files with 90 additions and 21 deletions
+35 -2
View File
@@ -7,6 +7,13 @@ import api from '@/api/http'
import MoneyInput from '@/components/MoneyInput.vue' import MoneyInput from '@/components/MoneyInput.vue'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
withDefaults(
defineProps<{
embedded?: boolean
}>(),
{ embedded: false },
)
type PriceRow = { type PriceRow = {
durationDays: number durationDays: number
enabled: boolean enabled: boolean
@@ -109,8 +116,11 @@ onMounted(load)
</script> </script>
<template> <template>
<div class="page-shell"> <div :class="embedded ? 'catalog-embed' : 'page-shell'">
<div style="display: flex; align-items: flex-start; gap: 8px; margin-bottom: 16px"> <div
v-if="!embedded"
style="display: flex; align-items: flex-start; gap: 8px; margin-bottom: 16px"
>
<a-button type="text" @click="router.push({ name: 'plugin-panel' })"> <a-button type="text" @click="router.push({ name: 'plugin-panel' })">
<ArrowRightOutlined /> <ArrowRightOutlined />
</a-button> </a-button>
@@ -122,6 +132,14 @@ onMounted(load)
<PlusOutlined /> تنظیم جدید <PlusOutlined /> تنظیم جدید
</a-button> </a-button>
</div> </div>
<div v-else class="catalog-embed-bar">
<p class="catalog-hint">
همهٔ کلیدهای تنظیمات خانگی سوپرادمین. مدت خاموش در ادمین ماژول دیده نمی‌شود.
</p>
<a-button v-if="canWrite" type="primary" size="small" @click="showCreate = true">
<PlusOutlined /> تنظیم جدید
</a-button>
</div>
<a-spin :spinning="loading"> <a-spin :spinning="loading">
<div class="catalog-grid"> <div class="catalog-grid">
@@ -193,6 +211,21 @@ onMounted(load)
</template> </template>
<style scoped> <style scoped>
.catalog-embed-bar {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: space-between;
gap: 10px;
margin-bottom: 12px;
}
.catalog-hint {
margin: 0;
flex: 1;
min-width: 12rem;
color: #64748b;
font-size: 0.85rem;
}
.catalog-grid { .catalog-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(100%, 320px), 1fr)); grid-template-columns: repeat(auto-fill, minmax(min(100%, 320px), 1fr));
+53 -15
View File
@@ -1,10 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { ArrowRightOutlined, AppstoreAddOutlined, SearchOutlined } from '@ant-design/icons-vue' import { ArrowRightOutlined, AppstoreAddOutlined, SearchOutlined } from '@ant-design/icons-vue'
import api from '@/api/http' import api from '@/api/http'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import PluginCatalogPage from '@/pages/PluginCatalogPage.vue'
type ExpiryLevel = 'healthy' | 'warning' | 'critical' | 'unknown' | 'none' type ExpiryLevel = 'healthy' | 'warning' | 'critical' | 'unknown' | 'none'
@@ -33,7 +34,10 @@ type RequestRow = {
createdAt: string createdAt: string
} }
type MainTab = 'khanegi' | 'hamkar' | 'requests' | 'catalog'
const router = useRouter() const router = useRouter()
const route = useRoute()
const auth = useAuthStore() const auth = useAuthStore()
const canWrite = computed(() => auth.can('plugins:write')) const canWrite = computed(() => auth.can('plugins:write'))
const storeBusyId = ref<string | null>(null) const storeBusyId = ref<string | null>(null)
@@ -41,7 +45,7 @@ const loadingList = ref(false)
const tenants = ref<TenantRow[]>([]) const tenants = ref<TenantRow[]>([])
const q = ref('') const q = ref('')
const toneFilter = ref<'healthy' | 'warning' | 'critical' | 'unknown' | null>(null) const toneFilter = ref<'healthy' | 'warning' | 'critical' | 'unknown' | null>(null)
const mainTab = ref<'khanegi' | 'hamkar' | 'requests'>('khanegi') const mainTab = ref<MainTab>('khanegi')
const requests = ref<RequestRow[]>([]) const requests = ref<RequestRow[]>([])
const loadingRequests = ref(false) const loadingRequests = ref(false)
const actingId = ref<string | null>(null) const actingId = ref<string | null>(null)
@@ -160,11 +164,29 @@ async function reject(row: RequestRow) {
} }
} }
function setTab(tab: 'khanegi' | 'hamkar' | 'requests') { function setTab(tab: MainTab) {
mainTab.value = tab mainTab.value = tab
const query = tab === 'khanegi' ? {} : { tab }
void router.replace({ query })
if (tab === 'requests') void loadRequests() if (tab === 'requests') void loadRequests()
} }
function tabFromQuery(raw: unknown): MainTab {
if (raw === 'hamkar' || raw === 'requests' || raw === 'catalog') return raw
return 'khanegi'
}
watch(
() => route.query.tab,
(tab) => {
const next = tabFromQuery(tab)
if (mainTab.value === next) return
mainTab.value = next
if (next === 'requests') void loadRequests()
},
{ immediate: true },
)
onMounted(() => { onMounted(() => {
loadTenants() loadTenants()
}) })
@@ -189,9 +211,10 @@ onMounted(() => {
<button type="button" :class="{ active: mainTab === 'khanegi' }" @click="setTab('khanegi')">خانگی</button> <button type="button" :class="{ active: mainTab === 'khanegi' }" @click="setTab('khanegi')">خانگی</button>
<button type="button" :class="{ active: mainTab === 'hamkar' }" @click="setTab('hamkar')">همکار</button> <button type="button" :class="{ active: mainTab === 'hamkar' }" @click="setTab('hamkar')">همکار</button>
<button type="button" :class="{ active: mainTab === 'requests' }" @click="setTab('requests')">درخواست‌ها</button> <button type="button" :class="{ active: mainTab === 'requests' }" @click="setTab('requests')">درخواست‌ها</button>
<button type="button" :class="{ active: mainTab === 'catalog' }" @click="setTab('catalog')">قیمت فروشگاه</button>
</div> </div>
<div v-if="mainTab !== 'requests'" class="toolbar"> <div v-if="mainTab === 'khanegi' || mainTab === 'hamkar'" class="toolbar">
<a-input <a-input
v-model:value="q" v-model:value="q"
allow-clear allow-clear
@@ -200,12 +223,9 @@ onMounted(() => {
> >
<template #prefix><SearchOutlined /></template> <template #prefix><SearchOutlined /></template>
</a-input> </a-input>
<a-button v-if="mainTab === 'khanegi'" type="primary" @click="router.push({ name: 'plugin-catalog' })">
قیمت فروشگاه
</a-button>
</div> </div>
<div v-if="mainTab !== 'requests'" class="legend" role="group" aria-label="فیلتر وضعیت افزونه"> <div v-if="mainTab === 'khanegi' || mainTab === 'hamkar'" class="legend" role="group" aria-label="فیلتر وضعیت افزونه">
<button <button
type="button" type="button"
class="chip all" class="chip all"
@@ -292,6 +312,11 @@ onMounted(() => {
</a-spin> </a-spin>
</section> </section>
<section v-else-if="mainTab === 'catalog'" class="sms-selector">
<div class="sms-side-head">قیمت فروشگاه خانگی</div>
<PluginCatalogPage embedded />
</section>
<section v-else class="sms-selector"> <section v-else class="sms-selector">
<div class="sms-side-head"><AppstoreAddOutlined /> نسخه‌ها</div> <div class="sms-side-head"><AppstoreAddOutlined /> نسخه‌ها</div>
<a-spin :spinning="loadingList"> <a-spin :spinning="loadingList">
@@ -332,14 +357,13 @@ onMounted(() => {
:class="toneClass(t.summary?.expiryLevel)" :class="toneClass(t.summary?.expiryLevel)"
@click="openTenant(t.id)" @click="openTenant(t.id)"
> >
<div <div class="sms-toggle" :class="{ off: !t.pluginStoreEnabled }" @click.stop>
v-if="canWrite" <div class="sms-toggle-text">
class="sms-toggle"
:class="{ off: !t.pluginStoreEnabled }"
@click.stop
>
<span class="sms-toggle-label">فروشگاه افزونه</span> <span class="sms-toggle-label">فروشگاه افزونه</span>
<small class="sms-toggle-domain" dir="ltr">{{ t.domain }}</small>
</div>
<a-switch <a-switch
v-if="canWrite"
:checked="t.pluginStoreEnabled === true" :checked="t.pluginStoreEnabled === true"
:loading="storeBusyId === t.id" :loading="storeBusyId === t.id"
checked-children="روشن" checked-children="روشن"
@@ -488,6 +512,20 @@ onMounted(() => {
color: #334155; color: #334155;
white-space: nowrap; white-space: nowrap;
} }
.sms-toggle-text {
display: grid;
gap: 2px;
min-width: 0;
flex: 1;
}
.sms-toggle-domain {
font-size: 0.7rem;
font-weight: 600;
color: #64748b;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.tenant-card strong { font-size: 0.95rem; } .tenant-card strong { font-size: 0.95rem; }
.tenant-card span, .tenant-card span,
+1 -3
View File
@@ -61,9 +61,7 @@ const router = createRouter({
}, },
{ {
path: 'plugins/catalog', path: 'plugins/catalog',
name: 'plugin-catalog', redirect: { name: 'plugin-panel', query: { tab: 'catalog' } },
component: () => import('@/pages/PluginCatalogPage.vue'),
meta: { permissions: ['plugins:read'] },
}, },
{ {
path: 'plugins/:tenantId', path: 'plugins/:tenantId',