337 lines
8.6 KiB
Vue
337 lines
8.6 KiB
Vue
<template>
|
||
<div class="space-y-4">
|
||
<a-page-header title="管理中心" sub-title="产品开通、使用与续费" :ghost="false" class="page-header">
|
||
<template #extra>
|
||
<a-segmented
|
||
:value="active"
|
||
:options="[
|
||
{ label: '已开通', value: 'index' },
|
||
{ label: '未开通', value: 'unopened' }
|
||
]"
|
||
@update:value="onSwitch"
|
||
/>
|
||
</template>
|
||
</a-page-header>
|
||
|
||
<!-- 产品分类 -->
|
||
<a-card :bordered="false" class="card">
|
||
<a-spin :spinning="loading">
|
||
<a-tabs v-model:activeKey="activeCategory" @change="loadProducts">
|
||
<a-tab-pane key="all" tab="全部产品" />
|
||
<a-tab-pane key="website" tab="企业官网" />
|
||
<a-tab-pane key="shop" tab="电商系统" />
|
||
<a-tab-pane key="mp" tab="小程序/公众号" />
|
||
<a-tab-pane key="plugin" tab="插件工具" />
|
||
</a-tabs>
|
||
|
||
<div v-if="!loading && filteredProducts.length === 0" class="empty-state">
|
||
<a-empty description="暂无可用产品">
|
||
<template #image>
|
||
<div class="empty-icon">🛒</div>
|
||
</template>
|
||
<a-button type="primary" @click="navigateTo('/market')">
|
||
前往应用商店
|
||
</a-button>
|
||
</a-empty>
|
||
</div>
|
||
|
||
<div v-else class="product-grid">
|
||
<div
|
||
v-for="product in filteredProducts"
|
||
:key="product.productId"
|
||
class="product-card"
|
||
>
|
||
<div class="product-icon-wrap">
|
||
<img v-if="product.icon" :src="product.icon" class="product-icon-img" />
|
||
<div v-else class="product-icon-placeholder" :style="{ background: iconBgColor(product.productName) }">
|
||
{{ (product.productName || 'P').charAt(0) }}
|
||
</div>
|
||
</div>
|
||
<div class="product-info">
|
||
<div class="product-name">{{ product.productName }}</div>
|
||
<div class="product-desc">{{ product.description || '暂无描述' }}</div>
|
||
<div class="product-tags">
|
||
<a-tag v-if="product.priceType === 'free'" color="green" size="small">免费</a-tag>
|
||
<a-tag v-else color="orange" size="small">
|
||
¥{{ (product.price || 0) / 100 }}
|
||
</a-tag>
|
||
<a-tag v-if="product.appType" size="small">
|
||
{{ typeName(product.appType) }}
|
||
</a-tag>
|
||
</div>
|
||
</div>
|
||
<div class="product-actions">
|
||
<a-button type="primary" size="small" @click="handleOpen(product)">
|
||
立即开通
|
||
</a-button>
|
||
<a-button size="small" @click="handlePreview(product)">
|
||
了解详情
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</a-spin>
|
||
</a-card>
|
||
|
||
<!-- 开通引导 -->
|
||
<a-card :bordered="false" class="card guide-card">
|
||
<div class="guide-content">
|
||
<div class="guide-info">
|
||
<h3 class="guide-title">💡 需要定制化方案?</h3>
|
||
<p class="guide-desc">如果以上产品无法满足您的需求,可以联系我们的技术团队获取专属定制方案。</p>
|
||
</div>
|
||
<div class="guide-actions">
|
||
<a-button type="primary" @click="navigateTo('/console/tickets')">
|
||
提交工单
|
||
</a-button>
|
||
<a-button @click="navigateTo('/market')">
|
||
浏览应用商店
|
||
</a-button>
|
||
</div>
|
||
</div>
|
||
</a-card>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { computed, onMounted, ref } from 'vue'
|
||
import { message } from 'ant-design-vue'
|
||
import { pageAppProductAll } from '@/api/app/appProduct'
|
||
import type { AppProduct } from '@/api/app/appProduct/model'
|
||
|
||
definePageMeta({ layout: 'console' })
|
||
|
||
const route = useRoute()
|
||
const active = computed(() => (route.path.includes('/console/tenant/unopened') ? 'unopened' : ''))
|
||
|
||
function onSwitch(value: string | number) {
|
||
navigateTo(`/console/tenant/${String(value)}`)
|
||
}
|
||
|
||
// ─── 数据 ──────────────────────────────────────────────────────
|
||
const loading = ref(false)
|
||
const products = ref<AppProduct[]>([])
|
||
const activeCategory = ref('all')
|
||
|
||
const TYPE_NAME: Record<string, string> = {
|
||
web: 'Web 应用',
|
||
miniprogram: '小程序',
|
||
mobile: '移动 App',
|
||
api: 'API 服务',
|
||
plugin: '插件',
|
||
}
|
||
|
||
const TYPE_CATEGORY_MAP: Record<string, string> = {
|
||
web: 'website',
|
||
miniprogram: 'mp',
|
||
plugin: 'plugin',
|
||
}
|
||
|
||
function typeName(type?: string) {
|
||
return TYPE_NAME[type || ''] || type || '应用'
|
||
}
|
||
|
||
function getCategoryForProduct(product: AppProduct): string {
|
||
if (product.appType) {
|
||
return TYPE_CATEGORY_MAP[product.appType] || 'all'
|
||
}
|
||
// 根据 keywords 或 description 推断分类
|
||
const name = (product.productName || '').toLowerCase()
|
||
const desc = (product.description || '').toLowerCase()
|
||
if (name.includes('官网') || name.includes('企业') || desc.includes('官网') || desc.includes('企业站')) return 'website'
|
||
if (name.includes('电商') || name.includes('商城') || name.includes('shop') || desc.includes('电商')) return 'shop'
|
||
if (name.includes('小程序') || name.includes('公众号') || name.includes('mp') || desc.includes('小程序')) return 'mp'
|
||
if (name.includes('插件') || name.includes('plugin') || desc.includes('插件')) return 'plugin'
|
||
return 'all'
|
||
}
|
||
|
||
const filteredProducts = computed(() => {
|
||
if (activeCategory.value === 'all') return products.value
|
||
return products.value.filter(p => getCategoryForProduct(p) === activeCategory.value)
|
||
})
|
||
|
||
const PALETTE = ['#4e6ef2', '#f4a261', '#e76f51', '#2a9d8f', '#e9c46a', '#264653']
|
||
|
||
function iconBgColor(name?: string) {
|
||
if (!name) return PALETTE[0]
|
||
let h = 0
|
||
for (let i = 0; i < name.length; i++) h = (h * 31 + name.charCodeAt(i)) & 0xffffffff
|
||
return PALETTE[Math.abs(h) % PALETTE.length]
|
||
}
|
||
|
||
async function loadProducts() {
|
||
loading.value = true
|
||
try {
|
||
const res = await pageAppProductAll({
|
||
current: 1,
|
||
size: 50,
|
||
status: 1, // 正常状态
|
||
})
|
||
products.value = res?.list || []
|
||
} catch (e) {
|
||
console.error('加载产品列表失败', e)
|
||
products.value = []
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function handleOpen(product: AppProduct) {
|
||
navigateTo(`/market?app=${product.productId}`)
|
||
}
|
||
|
||
function handlePreview(product: AppProduct) {
|
||
navigateTo(`/market?app=${product.productId}`)
|
||
}
|
||
|
||
onMounted(() => {
|
||
loadProducts()
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.page-header {
|
||
border-radius: 12px;
|
||
}
|
||
|
||
.card {
|
||
border-radius: 12px;
|
||
}
|
||
|
||
/* 产品网格 */
|
||
.product-grid {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 14px;
|
||
}
|
||
|
||
.product-card {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
padding: 18px 20px;
|
||
border-radius: 12px;
|
||
border: 1px solid #f0f0f0;
|
||
background: #fff;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.product-card:hover {
|
||
border-color: #d6e4ff;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
|
||
}
|
||
|
||
.product-icon-wrap {
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.product-icon-img {
|
||
width: 52px;
|
||
height: 52px;
|
||
border-radius: 12px;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.product-icon-placeholder {
|
||
width: 52px;
|
||
height: 52px;
|
||
border-radius: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 20px;
|
||
font-weight: 700;
|
||
color: #fff;
|
||
}
|
||
|
||
.product-info {
|
||
flex: 1;
|
||
min-width: 0;
|
||
}
|
||
|
||
.product-name {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: rgba(0, 0, 0, 0.85);
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.product-desc {
|
||
font-size: 13px;
|
||
color: rgba(0, 0, 0, 0.45);
|
||
margin-bottom: 8px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.product-tags {
|
||
display: flex;
|
||
gap: 6px;
|
||
}
|
||
|
||
.product-actions {
|
||
flex-shrink: 0;
|
||
display: flex;
|
||
gap: 8px;
|
||
}
|
||
|
||
/* 空状态 */
|
||
.empty-state {
|
||
padding: 60px 20px;
|
||
text-align: center;
|
||
}
|
||
|
||
.empty-icon {
|
||
font-size: 48px;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
/* 引导卡片 */
|
||
.guide-card {
|
||
background: linear-gradient(135deg, #f5f3ff, #ede9fe);
|
||
border-color: #ede9fe;
|
||
}
|
||
|
||
.guide-content {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 20px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.guide-info { flex: 1; min-width: 200px; }
|
||
|
||
.guide-title {
|
||
font-size: 16px;
|
||
font-weight: 600;
|
||
color: rgba(0, 0, 0, 0.85);
|
||
margin: 0 0 6px;
|
||
}
|
||
|
||
.guide-desc {
|
||
font-size: 13px;
|
||
color: rgba(0, 0, 0, 0.5);
|
||
margin: 0;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.guide-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
.product-card {
|
||
flex-direction: column;
|
||
text-align: center;
|
||
}
|
||
.product-actions {
|
||
width: 100%;
|
||
justify-content: center;
|
||
}
|
||
}
|
||
</style>
|