Files
tiantian-system/app/pages/developer/resources/index.vue
2026-04-08 17:10:58 +08:00

290 lines
8.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>