fix(plugins): show tenant domain and move store prices into a tab
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7,6 +7,13 @@ import api from '@/api/http'
|
||||
import MoneyInput from '@/components/MoneyInput.vue'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
embedded?: boolean
|
||||
}>(),
|
||||
{ embedded: false },
|
||||
)
|
||||
|
||||
type PriceRow = {
|
||||
durationDays: number
|
||||
enabled: boolean
|
||||
@@ -109,8 +116,11 @@ onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-shell">
|
||||
<div style="display: flex; align-items: flex-start; gap: 8px; margin-bottom: 16px">
|
||||
<div :class="embedded ? 'catalog-embed' : 'page-shell'">
|
||||
<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' })">
|
||||
<ArrowRightOutlined />
|
||||
</a-button>
|
||||
@@ -122,6 +132,14 @@ onMounted(load)
|
||||
<PlusOutlined /> تنظیم جدید
|
||||
</a-button>
|
||||
</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">
|
||||
<div class="catalog-grid">
|
||||
@@ -193,6 +211,21 @@ onMounted(load)
|
||||
</template>
|
||||
|
||||
<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 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(100%, 320px), 1fr));
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { ArrowRightOutlined, AppstoreAddOutlined, SearchOutlined } from '@ant-design/icons-vue'
|
||||
import api from '@/api/http'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import PluginCatalogPage from '@/pages/PluginCatalogPage.vue'
|
||||
|
||||
type ExpiryLevel = 'healthy' | 'warning' | 'critical' | 'unknown' | 'none'
|
||||
|
||||
@@ -33,7 +34,10 @@ type RequestRow = {
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
type MainTab = 'khanegi' | 'hamkar' | 'requests' | 'catalog'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const auth = useAuthStore()
|
||||
const canWrite = computed(() => auth.can('plugins:write'))
|
||||
const storeBusyId = ref<string | null>(null)
|
||||
@@ -41,7 +45,7 @@ const loadingList = ref(false)
|
||||
const tenants = ref<TenantRow[]>([])
|
||||
const q = ref('')
|
||||
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 loadingRequests = ref(false)
|
||||
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
|
||||
const query = tab === 'khanegi' ? {} : { tab }
|
||||
void router.replace({ query })
|
||||
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(() => {
|
||||
loadTenants()
|
||||
})
|
||||
@@ -189,9 +211,10 @@ onMounted(() => {
|
||||
<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 === 'requests' }" @click="setTab('requests')">درخواستها</button>
|
||||
<button type="button" :class="{ active: mainTab === 'catalog' }" @click="setTab('catalog')">قیمت فروشگاه</button>
|
||||
</div>
|
||||
|
||||
<div v-if="mainTab !== 'requests'" class="toolbar">
|
||||
<div v-if="mainTab === 'khanegi' || mainTab === 'hamkar'" class="toolbar">
|
||||
<a-input
|
||||
v-model:value="q"
|
||||
allow-clear
|
||||
@@ -200,12 +223,9 @@ onMounted(() => {
|
||||
>
|
||||
<template #prefix><SearchOutlined /></template>
|
||||
</a-input>
|
||||
<a-button v-if="mainTab === 'khanegi'" type="primary" @click="router.push({ name: 'plugin-catalog' })">
|
||||
قیمت فروشگاه
|
||||
</a-button>
|
||||
</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
|
||||
type="button"
|
||||
class="chip all"
|
||||
@@ -292,6 +312,11 @@ onMounted(() => {
|
||||
</a-spin>
|
||||
</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">
|
||||
<div class="sms-side-head"><AppstoreAddOutlined /> نسخهها</div>
|
||||
<a-spin :spinning="loadingList">
|
||||
@@ -332,14 +357,13 @@ onMounted(() => {
|
||||
:class="toneClass(t.summary?.expiryLevel)"
|
||||
@click="openTenant(t.id)"
|
||||
>
|
||||
<div
|
||||
v-if="canWrite"
|
||||
class="sms-toggle"
|
||||
:class="{ off: !t.pluginStoreEnabled }"
|
||||
@click.stop
|
||||
>
|
||||
<span class="sms-toggle-label">فروشگاه افزونه</span>
|
||||
<div class="sms-toggle" :class="{ off: !t.pluginStoreEnabled }" @click.stop>
|
||||
<div class="sms-toggle-text">
|
||||
<span class="sms-toggle-label">فروشگاه افزونه</span>
|
||||
<small class="sms-toggle-domain" dir="ltr">{{ t.domain }}</small>
|
||||
</div>
|
||||
<a-switch
|
||||
v-if="canWrite"
|
||||
:checked="t.pluginStoreEnabled === true"
|
||||
:loading="storeBusyId === t.id"
|
||||
checked-children="روشن"
|
||||
@@ -488,6 +512,20 @@ onMounted(() => {
|
||||
color: #334155;
|
||||
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 span,
|
||||
|
||||
+1
-3
@@ -61,9 +61,7 @@ const router = createRouter({
|
||||
},
|
||||
{
|
||||
path: 'plugins/catalog',
|
||||
name: 'plugin-catalog',
|
||||
component: () => import('@/pages/PluginCatalogPage.vue'),
|
||||
meta: { permissions: ['plugins:read'] },
|
||||
redirect: { name: 'plugin-panel', query: { tab: 'catalog' } },
|
||||
},
|
||||
{
|
||||
path: 'plugins/:tenantId',
|
||||
|
||||
Reference in New Issue
Block a user