290 lines
8.3 KiB
Vue
290 lines
8.3 KiB
Vue
<template>
|
||
<div class="dev-page">
|
||
<!-- 页头 -->
|
||
<div class="page-header" style="margin-bottom: 24px">
|
||
<h2 style="font-size: 20px; font-weight: 600; margin: 0 0 6px">资源中心</h2>
|
||
<p style="color: #888; margin: 0; font-size: 14px">管理应用所需的基础设施资源,包括服务器、数据库、云存储、域名和 SSL 证书</p>
|
||
</div>
|
||
|
||
<!-- 资源统计卡片 -->
|
||
<a-row :gutter="[16, 16]" style="margin-bottom: 24px">
|
||
<a-col v-for="item in resourceCards" :key="item.key" :xs="24" :sm="12" :md="8" :lg="8" :xl="4">
|
||
<div class="resource-stat-card" @click="navigateTo(item.to)">
|
||
<div class="stat-icon">{{ item.icon }}</div>
|
||
<div class="stat-info">
|
||
<div class="stat-label">{{ item.label }}</div>
|
||
<div class="stat-count">
|
||
<span class="count-num">{{ item.count }}</span>
|
||
<span class="count-unit">个</span>
|
||
</div>
|
||
</div>
|
||
<div class="stat-arrow">›</div>
|
||
</div>
|
||
</a-col>
|
||
</a-row>
|
||
|
||
<!-- 快捷操作 -->
|
||
<div class="section-title">快速购买</div>
|
||
<a-row :gutter="[16, 16]" style="margin-bottom: 32px">
|
||
<a-col v-for="item in buyCards" :key="item.key" :xs="24" :sm="12" :md="8" :lg="6">
|
||
<div class="buy-card">
|
||
<div class="buy-icon">{{ item.icon }}</div>
|
||
<div class="buy-content">
|
||
<div class="buy-title">{{ item.title }}</div>
|
||
<div class="buy-desc">{{ item.desc }}</div>
|
||
</div>
|
||
<a-button type="primary" size="small" ghost @click="handleBuy(item.key)">
|
||
购买
|
||
</a-button>
|
||
</div>
|
||
</a-col>
|
||
</a-row>
|
||
|
||
<!-- 协作权限说明 -->
|
||
<div class="collab-notice">
|
||
<LockOutlined class="notice-icon" />
|
||
<span>资源信息按权限分级显示:<strong>创建者</strong>可查看完整信息(含密码/私钥);<strong>协作者</strong>可查看基础信息(IP、端口等),敏感信息不可见。</span>
|
||
</div>
|
||
|
||
<!-- 最近添加的资源 -->
|
||
<div class="section-title">最近添加</div>
|
||
<a-table
|
||
:columns="recentColumns"
|
||
:data-source="recentResources"
|
||
:pagination="false"
|
||
size="middle"
|
||
:loading="loading"
|
||
>
|
||
<template #bodyCell="{ column, record }">
|
||
<template v-if="column.key === 'type'">
|
||
<a-tag :color="typeColor[record.resourceType]">{{ typeLabel[record.resourceType] }}</a-tag>
|
||
</template>
|
||
<template v-if="column.key === 'status'">
|
||
<a-badge :status="record.status === 'running' ? 'success' : 'default'" :text="statusLabel[record.status]" />
|
||
</template>
|
||
<template v-if="column.key === 'permission'">
|
||
<a-tag v-if="record.isOwner" color="blue" size="small">创建者</a-tag>
|
||
<a-tag v-else color="orange" size="small"><LockOutlined /> 协作者</a-tag>
|
||
</template>
|
||
<template v-if="column.key === 'action'">
|
||
<a-button type="link" size="small" @click="navigateTo(typeRoute[record.resourceType])">管理</a-button>
|
||
</template>
|
||
</template>
|
||
</a-table>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { navigateTo } from '#app'
|
||
import { LockOutlined } from '@ant-design/icons-vue'
|
||
import { statsAppResource, pageAppResource } from '@/api/app/appResource'
|
||
import type { AppResource } from '@/api/app/appResource/model'
|
||
import { enrichResourcesWithPermission } from '@/composables/useResourceAccess'
|
||
|
||
definePageMeta({ layout: 'developer' })
|
||
useHead({ title: '资源总览 - 开发者中心' })
|
||
|
||
const loading = ref(false)
|
||
|
||
const resourceCards = ref([
|
||
{ key: 'server', label: '服务器', icon: '🖥️', count: 0, to: '/developer/resources/servers' },
|
||
{ key: 'database', label: '数据库', icon: '🗄️', count: 0, to: '/developer/resources/databases' },
|
||
{ key: 'storage', label: '云存储', icon: '☁️', count: 0, to: '/developer/resources/storage' },
|
||
{ key: 'domain', label: '域名', icon: '🌐', count: 0, to: '/developer/resources/domains' },
|
||
{ key: 'ssl', label: 'SSL 证书', icon: '🔒', count: 0, to: '/developer/resources/ssl' },
|
||
{ key: 'git', label: '代码仓库', icon: '🐙', count: 0, to: '/developer/resources/git' },
|
||
])
|
||
|
||
const buyCards = [
|
||
{ key: 'server', icon: '🖥️', title: '云服务器', desc: '高性能、稳定可靠的弹性计算服务' },
|
||
{ key: 'database', icon: '🗄️', title: '云数据库', desc: '支持 MySQL / PostgreSQL / Redis' },
|
||
{ key: 'storage', icon: '☁️', title: '对象存储', desc: '海量、安全、低成本的云端存储' },
|
||
{ key: 'domain', icon: '🌐', title: '域名注册', desc: '注册您的专属域名,快速备案' },
|
||
]
|
||
|
||
const typeLabel: Record<string, string> = {
|
||
server: '服务器',
|
||
database: '数据库',
|
||
storage: '云存储',
|
||
domain: '域名',
|
||
ssl: 'SSL证书',
|
||
git: '代码仓库',
|
||
}
|
||
|
||
const typeColor: Record<string, string> = {
|
||
server: 'blue',
|
||
database: 'purple',
|
||
storage: 'cyan',
|
||
domain: 'green',
|
||
ssl: 'orange',
|
||
git: 'geekblue',
|
||
}
|
||
|
||
const typeRoute: Record<string, string> = {
|
||
server: '/developer/resources/servers',
|
||
database: '/developer/resources/databases',
|
||
storage: '/developer/resources/storage',
|
||
domain: '/developer/resources/domains',
|
||
ssl: '/developer/resources/ssl',
|
||
git: '/developer/resources/git',
|
||
}
|
||
|
||
const statusLabel: Record<string, string> = {
|
||
running: '运行中',
|
||
stopped: '已停止',
|
||
expired: '已过期',
|
||
pending: '配置中',
|
||
}
|
||
|
||
const recentColumns = [
|
||
{ title: '资源名称', dataIndex: 'name', key: 'name' },
|
||
{ title: '类型', dataIndex: 'resourceType', key: 'type' },
|
||
{ title: '所属应用', dataIndex: 'appName', key: 'appName' },
|
||
{ title: '状态', dataIndex: 'status', key: 'status' },
|
||
{ title: '到期时间', dataIndex: 'expireAt', key: 'expireAt' },
|
||
{ title: '权限', key: 'permission', width: 90 },
|
||
{ title: '操作', key: 'action' },
|
||
]
|
||
|
||
// 最近添加的资源(最新 10 条)
|
||
const recentResources = ref<AppResource[]>([])
|
||
|
||
async function loadData() {
|
||
loading.value = true
|
||
try {
|
||
const [stats, recentResult] = await Promise.all([
|
||
statsAppResource(),
|
||
pageAppResource({ page: 1, limit: 10, sort: 'createTime', order: 'desc' }),
|
||
])
|
||
resourceCards.value.forEach(card => {
|
||
card.count = stats[card.key] ?? 0
|
||
})
|
||
recentResources.value = enrichResourcesWithPermission(recentResult?.list ?? [])
|
||
}
|
||
catch (e) {
|
||
console.error('加载资源数据失败', e)
|
||
}
|
||
finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function handleBuy(key: string) {
|
||
// TODO: 跳转到购买页或弹出购买引导
|
||
console.log('buy', key)
|
||
}
|
||
|
||
onMounted(() => loadData())
|
||
</script>
|
||
|
||
<style scoped>
|
||
.dev-page {
|
||
padding: 24px;
|
||
max-width: 1200px;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 15px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 12px;
|
||
padding-left: 10px;
|
||
border-left: 3px solid #1677ff;
|
||
}
|
||
|
||
/* 协作权限说明 */
|
||
.collab-notice {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
background: linear-gradient(90deg, #fffbe6 0%, #fff7e6 100%);
|
||
border: 1px solid #ffd591;
|
||
border-radius: 8px;
|
||
padding: 10px 16px;
|
||
margin-bottom: 20px;
|
||
font-size: 13px;
|
||
color: #7c4a00;
|
||
}
|
||
.collab-notice .notice-icon {
|
||
color: #faad14;
|
||
font-size: 15px;
|
||
flex-shrink: 0;
|
||
}
|
||
.collab-notice strong {
|
||
color: #d46b08;
|
||
}
|
||
|
||
/* 统计卡片 */
|
||
.resource-stat-card {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
background: #fff;
|
||
border: 1px solid #e8e8e8;
|
||
border-radius: 10px;
|
||
padding: 16px;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
.resource-stat-card:hover {
|
||
border-color: #1677ff;
|
||
box-shadow: 0 2px 10px rgba(22, 119, 255, 0.1);
|
||
}
|
||
.stat-icon {
|
||
font-size: 28px;
|
||
width: 44px;
|
||
text-align: center;
|
||
}
|
||
.stat-info {
|
||
flex: 1;
|
||
}
|
||
.stat-label {
|
||
font-size: 12px;
|
||
color: #888;
|
||
margin-bottom: 2px;
|
||
}
|
||
.count-num {
|
||
font-size: 22px;
|
||
font-weight: 700;
|
||
color: #1677ff;
|
||
}
|
||
.count-unit {
|
||
font-size: 12px;
|
||
color: #aaa;
|
||
margin-left: 2px;
|
||
}
|
||
.stat-arrow {
|
||
font-size: 18px;
|
||
color: #bbb;
|
||
}
|
||
|
||
/* 购买卡片 */
|
||
.buy-card {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
background: #fff;
|
||
border: 1px solid #e8e8e8;
|
||
border-radius: 10px;
|
||
padding: 14px 16px;
|
||
}
|
||
.buy-icon {
|
||
font-size: 24px;
|
||
width: 36px;
|
||
text-align: center;
|
||
}
|
||
.buy-content {
|
||
flex: 1;
|
||
}
|
||
.buy-title {
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #222;
|
||
}
|
||
.buy-desc {
|
||
font-size: 12px;
|
||
color: #999;
|
||
margin-top: 2px;
|
||
}
|
||
</style>
|