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

846 lines
22 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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="dev-hero">
<div class="dev-hero-content">
<div class="dev-hero-left">
<div class="dev-hero-greeting">🛠 欢迎回来{{ userDisplayName }}</div>
<h1 class="dev-hero-title">开发者控制台</h1>
<p class="dev-hero-desc">管理你的应用API Key 和源码权限快速构建 AI 原生产品</p>
<a-space class="mt-4">
<a-button type="primary" @click="navigateTo('/developer/apikeys')">
🔑 获取 API Key
</a-button>
<a-button @click="navigateTo('/developer-center')">📖 查看文档</a-button>
</a-space>
</div>
<div class="dev-hero-right">
<div class="dev-code-snippet">
<div class="code-header">
<div class="code-dots">
<span class="dot red" /><span class="dot yellow" /><span class="dot green" />
</div>
<span class="code-filename">quickstart.ts</span>
</div>
<pre class="code-body"><code><span class="c">// 初始化 SDK</span>
<span class="kw">import</span> { <span class="cls">WebsopyClient</span> } <span class="kw">from</span> <span class="str">'@websopy/sdk'</span>
<span class="kw">const</span> <span class="var">client</span> = <span class="kw">new</span> <span class="cls">WebsopyClient</span>({
<span class="prop">apiKey</span>: <span class="str">'sk-xxxxxxxxxxxxxxxx'</span>
})
<span class="c">// 调用 AI 智能体</span>
<span class="kw">const</span> <span class="var">reply</span> = <span class="kw">await</span> <span class="var">client</span>.<span class="fn">agent</span>.<span class="fn">chat</span>({
<span class="prop">message</span>: <span class="str">'帮我分析本月销售数据'</span>
})</code></pre>
</div>
</div>
</div>
</div>
<div class="dev-body">
<!-- 数据统计卡片头部 -->
<div class="mb-4 flex items-center justify-between">
<div class="flex items-center gap-2">
<h3 class="text-base font-semibold text-gray-800">📊 数据概览</h3>
<a-tag v-if="!loading && statsData.totalUsage > 0" color="blue">
本月调用: {{ statsData.totalUsage }}
</a-tag>
<a-tag v-if="!loading && statsData.activeKeys > 0" color="green">
活跃 Key: {{ statsData.activeKeys }}
</a-tag>
</div>
<a-button size="small" :loading="loading" @click="loadStats">
<template #icon><reload-outlined /></template>
刷新数据
</a-button>
</div>
<!-- 数据统计卡片 -->
<a-row :gutter="[16, 16]" class="mb-6">
<a-col :xs="12" :md="6" v-for="stat in stats" :key="stat.label">
<a-spin :spinning="loading">
<div class="stat-card" :class="stat.color">
<div class="stat-icon">{{ stat.icon }}</div>
<div class="stat-info">
<div class="stat-value">{{ stat.value }}</div>
<div class="stat-label">{{ stat.label }}</div>
</div>
</div>
</a-spin>
</a-col>
</a-row>
<a-row :gutter="[16, 16]">
<!-- 快捷入口 -->
<a-col :xs="24" :lg="14">
<div class="panel">
<div class="panel-header">
<span class="panel-title"> 快捷入口</span>
</div>
<div class="quick-entries">
<div
v-for="entry in quickEntries"
:key="entry.label"
class="quick-entry"
@click="navigateTo(entry.to)"
>
<div class="quick-entry-icon" :class="entry.iconClass">{{ entry.icon }}</div>
<div class="quick-entry-text">
<div class="quick-entry-label">{{ entry.label }}</div>
<div class="quick-entry-desc">{{ entry.desc }}</div>
</div>
<div class="quick-entry-arrow"></div>
</div>
</div>
</div>
<!-- 最近动态时间线 -->
<div class="panel mt-4">
<div class="panel-header">
<span class="panel-title">🕐 最近动态</span>
<a-button size="small" type="link" @click="navigateTo('/developer/apps')">查看全部</a-button>
</div>
<div class="timeline-list">
<div v-if="recentActivities.length === 0" class="timeline-empty">
<div class="empty-icon">🗒</div>
<div class="empty-text">暂无操作记录</div>
</div>
<div v-for="(act, i) in recentActivities" :key="i" class="timeline-item">
<div class="timeline-dot" :class="act.type" />
<div class="timeline-line" v-if="i < recentActivities.length - 1" />
<div class="timeline-body">
<div class="timeline-title">{{ act.title }}</div>
<div class="timeline-meta">
<span class="timeline-app">{{ act.app }}</span>
<span class="timeline-sep">·</span>
<span class="timeline-time">{{ act.time }}</span>
</div>
</div>
</div>
</div>
</div>
</a-col>
<!-- 最新动态 / 开发者公告 -->
<a-col :xs="24" :lg="10">
<div class="panel">
<div class="panel-header">
<span class="panel-title">📢 开发者公告</span>
<a-tag color="red">NEW</a-tag>
</div>
<div class="notice-list">
<div v-for="notice in notices" :key="notice.title" class="notice-item">
<div class="notice-dot" :class="notice.type" />
<div class="notice-content">
<div class="notice-title">{{ notice.title }}</div>
<div class="notice-date">{{ notice.date }}</div>
</div>
</div>
</div>
</div>
<!-- 快速帮助 -->
<div class="panel mt-4">
<div class="panel-header">
<span class="panel-title">🆘 快速帮助</span>
</div>
<div class="help-links">
<a
v-for="link in helpLinks"
:key="link.label"
class="help-link"
@click="navigateTo(link.to)"
>
<span>{{ link.icon }}</span> {{ link.label }}
</a>
</div>
</div>
<!-- 服务状态 -->
<div class="panel mt-4">
<div class="panel-header">
<span class="panel-title">📡 服务状态</span>
<a-tag color="green"> 全部正常</a-tag>
</div>
<div class="srv-list">
<div v-for="srv in serviceStatus" :key="srv.name" class="srv-item">
<div class="srv-dot" :class="srv.status" />
<span class="srv-name">{{ srv.name }}</span>
<span class="srv-latency">{{ srv.latency }}</span>
</div>
</div>
</div>
</a-col>
</a-row>
<!-- SDK 支持状态 -->
<div class="panel mt-4">
<div class="panel-header">
<span class="panel-title">📦 SDK 支持状态</span>
<a-button size="small" type="link" @click="navigateTo('/developer/docs')">查看文档</a-button>
</div>
<div class="sdk-grid">
<div v-for="sdk in sdkStatus" :key="sdk.lang" class="sdk-item">
<span class="sdk-emoji">{{ sdk.emoji }}</span>
<div class="sdk-info">
<div class="sdk-lang">{{ sdk.lang }}</div>
<div class="sdk-desc">{{ sdk.desc }}</div>
</div>
<a-tag :color="sdk.tagColor" class="sdk-tag">{{ sdk.status }}</a-tag>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { getUserInfo } from '@/api/layout'
import { getToken } from '@/utils/token-util'
import { setAuthzFromUser } from '@/utils/permission'
// TODO: 后端接口就绪后解除注释
// import { getDeveloperStats } from '@/api/developer'
import { ReloadOutlined } from '@ant-design/icons-vue'
import type { User } from '@/api/system/user/model'
definePageMeta({ layout: 'developer' })
useHead({ title: '概览 - 开发者中心' })
const user = ref<User | null>(null)
const loading = ref(false)
const statsData = ref({
appCount: 0,
apiKeyCount: 0,
pendingRequests: 0,
repositoryAccess: 0,
totalUsage: 0,
activeKeys: 0
})
const userDisplayName = computed(() => {
const u = user.value
return u?.nickname?.trim() || u?.username?.trim() || u?.phone?.trim() || '开发者'
})
// 加载统计数据TODO: 后端接口就绪后替换 Mock
const loadStats = async () => {
loading.value = true
try {
// Mock 数据,后端接口就绪后替换为:
// const response = await getDeveloperStats()
// if (response.data?.success) { statsData.value = response.data.data }
statsData.value = { appCount: 12, apiKeyCount: 8, pendingRequests: 3, repositoryAccess: 5, totalUsage: 456200, activeKeys: 6 }
} catch (error) {
console.error('加载统计数据失败:', error)
} finally {
loading.value = false
}
}
onMounted(async () => {
const token = getToken()
if (!token) return
try {
const me = await getUserInfo()
user.value = me
setAuthzFromUser(me)
// 加载统计数据
await loadStats()
} catch { /* ignore */ }
})
const stats = computed(() => [
{
icon: '📦',
label: '可开发应用',
value: statsData.value.appCount > 0 ? statsData.value.appCount.toString() : '-',
color: 'blue'
},
{
icon: '🔑',
label: 'API Key 数量',
value: statsData.value.apiKeyCount > 0 ? statsData.value.apiKeyCount.toString() : '-',
color: 'purple'
},
{
icon: '📋',
label: '待处理申请',
value: statsData.value.pendingRequests > 0 ? statsData.value.pendingRequests.toString() : '-',
color: 'orange'
},
{
icon: '💻',
label: '已访问仓库',
value: statsData.value.repositoryAccess > 0 ? statsData.value.repositoryAccess.toString() : '-',
color: 'green'
},
])
const quickEntries = [
{
icon: '🔑', iconClass: 'purple',
label: 'API Key 管理',
desc: '创建、查看和管理你的 API Key',
to: '/developer/apikeys',
},
{
icon: '📦', iconClass: 'blue',
label: '应用中心',
desc: '查看订阅的应用与后台入口',
to: '/developer/apps',
},
{
icon: '💻', iconClass: 'cyan',
label: '源码与仓库',
desc: '申请仓库权限,获取完整源代码',
to: '/developer/source',
},
{
icon: '📚', iconClass: 'orange',
label: '开发文档',
desc: 'API 参考、SDK 使用、AI 功能与部署指南',
to: '/developer/docs',
},
{
icon: '🐙', iconClass: 'gray',
label: 'Git 账号绑定',
desc: '绑定 Gitea 账号,获取仓库访问权限',
to: '/developer/git',
},
{
icon: '💬', iconClass: 'green',
label: '支持与反馈',
desc: '遇到问题?提交工单或联系我们',
to: '/developer/support',
},
]
const notices = [
{ type: 'blue', title: 'TypeScript SDK v1.2.0 正式发布', date: '2026-03-25' },
{ type: 'green', title: 'OpenAPI 3.0 文档已全面更新', date: '2026-03-20' },
{ type: 'orange', title: 'AI Agent API 新增流式输出支持', date: '2026-03-15' },
{ type: 'gray', title: 'API Rate Limit 规则调整公告', date: '2026-03-10' },
]
const helpLinks = [
{ icon: '📘', label: '快速开始指南', to: '/developer/docs/getting-started/quickstart' },
{ icon: '🔌', label: 'REST API 参考', to: '/developer/docs/api/rest-api' },
{ icon: '🐙', label: '绑定 Git 账号', to: '/developer/git' },
{ icon: '📋', label: '权限申请流程', to: '/developer/source' },
]
const sdkStatus = [
{ emoji: '🟦', lang: 'TypeScript / JavaScript', desc: '官方维护,完整类型定义', status: '稳定版', tagColor: 'green' },
{ emoji: '🐍', lang: 'Python', desc: '支持 asyncio适合 AI 场景', status: '稳定版', tagColor: 'green' },
{ emoji: '☕', lang: 'Java', desc: '企业级 Spring Boot 接入', status: 'Beta', tagColor: 'orange' },
{ emoji: '🐹', lang: 'Go', desc: '高性能微服务场景', status: 'Beta', tagColor: 'orange' },
{ emoji: '🐘', lang: 'PHP', desc: 'Laravel / ThinkPHP 框架', status: '规划中', tagColor: 'default' },
]
// 最近动态(可替换为真实 API 数据)
const recentActivities = [
{ type: 'blue', title: '创建了 API Key', app: '人事管理系统', time: '10 分钟前' },
{ type: 'green', title: '应用上架审核通过', app: '电商平台', time: '2 小时前' },
{ type: 'orange', title: '提交了源码访问申请', app: '全局', time: '昨天 14:30' },
{ type: 'purple', title: '发布了新版本 v1.2.0', app: 'OA 系统', time: '2天前' },
{ type: 'gray', title: '绑定了 Gitea 账号', app: '全局', time: '3天前' },
]
// 服务状态
const serviceStatus = [
{ name: 'REST API', status: 'ok', latency: '28ms' },
{ name: 'AI Agent API', status: 'ok', latency: '312ms' },
{ name: 'Gitea 仓库', status: 'ok', latency: '45ms' },
{ name: 'SDK CDN', status: 'ok', latency: '18ms' },
{ name: 'Webhook', status: 'ok', latency: '—' },
]
</script>
<style scoped>
.dev-page {
min-height: 100%;
}
/* Hero 区域 */
.dev-hero {
background: linear-gradient(135deg, #1e1b4b 0%, #312e81 40%, #1e3a5f 100%);
padding: 32px 28px 28px;
position: relative;
overflow: hidden;
}
.dev-hero::before {
content: '';
position: absolute;
top: -80px;
right: -80px;
width: 300px;
height: 300px;
border-radius: 50%;
background: rgba(99, 102, 241, 0.2);
filter: blur(60px);
}
.dev-hero::after {
content: '';
position: absolute;
bottom: -60px;
left: 60px;
width: 200px;
height: 200px;
border-radius: 50%;
background: rgba(139, 92, 246, 0.15);
filter: blur(50px);
}
.dev-hero-content {
position: relative;
z-index: 1;
display: flex;
align-items: center;
gap: 24px;
}
.dev-hero-left {
flex: 1;
}
.dev-hero-greeting {
color: rgba(165, 180, 252, 0.9);
font-size: 14px;
margin-bottom: 8px;
}
.dev-hero-title {
color: #fff;
font-size: 26px;
font-weight: 700;
margin: 0 0 8px;
line-height: 1.3;
}
.dev-hero-desc {
color: rgba(199, 210, 254, 0.8);
font-size: 14px;
margin: 0;
line-height: 1.6;
}
.dev-hero-right {
flex-shrink: 0;
width: 360px;
}
@media (max-width: 900px) {
.dev-hero-right { display: none; }
}
/* 代码片段 */
.dev-code-snippet {
border-radius: 12px;
overflow: hidden;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(15, 15, 35, 0.8);
backdrop-filter: blur(10px);
}
.code-header {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(0, 0, 0, 0.2);
}
.code-dots { display: flex; gap: 5px; }
.dot {
width: 10px; height: 10px; border-radius: 50%;
}
.dot.red { background: #ff5f57; }
.dot.yellow { background: #febc2e; }
.dot.green { background: #28c840; }
.code-filename {
font-family: monospace;
font-size: 12px;
color: rgba(255, 255, 255, 0.4);
}
.code-body {
padding: 14px 16px;
margin: 0;
font-family: 'Fira Code', 'JetBrains Mono', monospace;
font-size: 12px;
line-height: 1.7;
overflow-x: auto;
color: #e2e8f0;
}
.code-body .c { color: #64748b; }
.code-body .kw { color: #93c5fd; }
.code-body .cls { color: #fde68a; }
.code-body .str { color: #86efac; }
.code-body .var { color: #a5b4fc; }
.code-body .prop { color: #cbd5e1; }
.code-body .fn { color: #fde68a; }
/* 主体内容 */
.dev-body {
padding: 20px 24px 24px;
}
/* 统计卡片 */
.stat-card {
display: flex;
align-items: center;
gap: 12px;
padding: 16px;
border-radius: 12px;
border: 1px solid transparent;
transition: all 0.2s;
}
.stat-card:hover { transform: translateY(-1px); }
.stat-card.blue { background: #eff6ff; border-color: #dbeafe; }
.stat-card.purple { background: #f5f3ff; border-color: #e9d5ff; }
.stat-card.orange { background: #fff7ed; border-color: #fed7aa; }
.stat-card.green { background: #f0fdf4; border-color: #bbf7d0; }
.stat-icon {
font-size: 28px;
flex-shrink: 0;
}
.stat-value {
font-size: 22px;
font-weight: 700;
color: rgba(0, 0, 0, 0.85);
line-height: 1.2;
}
.stat-label {
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
margin-top: 2px;
}
/* 面板通用 */
.panel {
background: #fff;
border: 1px solid #f0f0f0;
border-radius: 12px;
overflow: hidden;
}
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
border-bottom: 1px solid #f5f5f5;
}
.panel-title {
font-size: 14px;
font-weight: 600;
color: rgba(0, 0, 0, 0.85);
}
/* 快捷入口 */
.quick-entries {
padding: 8px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
}
.quick-entry {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
border-radius: 10px;
cursor: pointer;
transition: all 0.15s;
border: 1px solid transparent;
}
.quick-entry:hover {
background: #f5f7ff;
border-color: #e0e7ff;
}
.quick-entry-icon {
width: 36px;
height: 36px;
flex-shrink: 0;
border-radius: 9px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
.quick-entry-icon.blue { background: #eff6ff; }
.quick-entry-icon.purple { background: #f5f3ff; }
.quick-entry-icon.cyan { background: #ecfeff; }
.quick-entry-icon.orange { background: #fff7ed; }
.quick-entry-icon.gray { background: #f9fafb; }
.quick-entry-icon.green { background: #f0fdf4; }
.quick-entry-text { flex: 1; min-width: 0; }
.quick-entry-label {
font-size: 13px;
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
margin-bottom: 2px;
}
.quick-entry-desc {
font-size: 12px;
color: rgba(0, 0, 0, 0.4);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.quick-entry-arrow {
color: rgba(0, 0, 0, 0.25);
font-size: 14px;
flex-shrink: 0;
transition: transform 0.15s;
}
.quick-entry:hover .quick-entry-arrow {
color: #4f46e5;
transform: translateX(3px);
}
/* 公告列表 */
.notice-list {
padding: 8px 16px;
}
.notice-item {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 10px 0;
border-bottom: 1px solid #f9f9f9;
}
.notice-item:last-child { border-bottom: none; }
.notice-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
margin-top: 5px;
}
.notice-dot.blue { background: #4f46e5; }
.notice-dot.green { background: #16a34a; }
.notice-dot.orange { background: #ea580c; }
.notice-dot.gray { background: #9ca3af; }
.notice-title {
font-size: 13px;
color: rgba(0, 0, 0, 0.75);
line-height: 1.4;
}
.notice-date {
font-size: 11px;
color: rgba(0, 0, 0, 0.35);
margin-top: 2px;
}
/* 帮助链接 */
.help-links {
padding: 10px 14px 14px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
}
.help-link {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 10px;
border-radius: 8px;
font-size: 13px;
color: rgba(0, 0, 0, 0.65);
cursor: pointer;
transition: all 0.15s;
}
.help-link:hover {
background: #f5f7ff;
color: #4f46e5;
}
/* SDK 状态 */
.sdk-grid {
padding: 12px 16px;
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 10px;
}
.sdk-item {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 14px;
border-radius: 10px;
border: 1px solid #f0f0f0;
background: #fafafa;
transition: all 0.15s;
}
.sdk-item:hover {
border-color: #e0e7ff;
background: #f5f7ff;
}
.sdk-emoji {
font-size: 24px;
flex-shrink: 0;
}
.sdk-info { flex: 1; min-width: 0; }
.sdk-lang {
font-size: 13px;
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
}
.sdk-desc {
font-size: 12px;
color: rgba(0, 0, 0, 0.4);
margin-top: 2px;
}
.sdk-tag {
flex-shrink: 0;
font-size: 11px;
}
/* 最近动态时间线 */
.timeline-list {
padding: 12px 18px 16px;
position: relative;
}
.timeline-empty {
display: flex;
flex-direction: column;
align-items: center;
padding: 24px;
text-align: center;
}
.timeline-item {
display: flex;
align-items: flex-start;
gap: 12px;
position: relative;
padding-bottom: 14px;
}
.timeline-item:last-child { padding-bottom: 0; }
.timeline-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
margin-top: 5px;
position: relative;
z-index: 1;
}
.timeline-dot.blue { background: #4f46e5; }
.timeline-dot.green { background: #16a34a; }
.timeline-dot.orange { background: #ea580c; }
.timeline-dot.purple { background: #7c3aed; }
.timeline-dot.gray { background: #9ca3af; }
.timeline-line {
position: absolute;
left: 3px;
top: 14px;
bottom: 0;
width: 2px;
background: #f0f0f0;
}
.timeline-body { flex: 1; min-width: 0; }
.timeline-title {
font-size: 13px;
color: rgba(0, 0, 0, 0.8);
font-weight: 500;
margin-bottom: 3px;
}
.timeline-meta {
display: flex;
align-items: center;
gap: 5px;
font-size: 11px;
color: rgba(0, 0, 0, 0.38);
}
.timeline-sep { opacity: 0.5; }
.empty-icon { font-size: 28px; margin-bottom: 8px; }
.empty-text { font-size: 13px; color: rgba(0, 0, 0, 0.4); }
/* 服务状态 */
.srv-list {
padding: 8px 16px 12px;
}
.srv-item {
display: flex;
align-items: center;
gap: 10px;
padding: 7px 0;
border-bottom: 1px solid #f9f9f9;
}
.srv-item:last-child { border-bottom: none; }
.srv-dot {
width: 7px;
height: 7px;
border-radius: 50%;
flex-shrink: 0;
}
.srv-dot.ok { background: #16a34a; }
.srv-dot.warn { background: #f59e0b; }
.srv-dot.down { background: #dc2626; }
.srv-name {
flex: 1;
font-size: 13px;
color: rgba(0, 0, 0, 0.72);
}
.srv-latency {
font-size: 12px;
color: rgba(0, 0, 0, 0.35);
font-family: monospace;
}
</style>