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 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));
|
||||||
|
|||||||
@@ -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
@@ -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',
|
||||||
|
|||||||
Reference in New Issue
Block a user