用户存储用量统计
Some checks failed
DevStar Studio Auto Test Pipeline / unit-frontend-test (pull_request) Failing after 41s
DevStar Studio Auto Test Pipeline / unit-backend-test (pull_request) Failing after 32s
DevStar Studio CI/CD Pipeline / build-and-push-x86-64-docker-image (pull_request) Failing after 37s
DevStar E2E Test / e2e-test (pull_request) Has been cancelled

This commit is contained in:
zhufengliang
2025-12-01 21:11:57 +08:00
parent 582c31e9a3
commit 85fc3665d4
8 changed files with 880 additions and 0 deletions

View File

@@ -698,6 +698,11 @@ following = Following
follow = Follow
unfollow = Unfollow
user_bio = Biography
user.stats = Data Statistics
user.repositories = Repositories
user.activity = Public Activity
user.starred = Starred
user.code = Code
disabled_public_activity = This user has disabled the public visibility of the activity.
email_visibility.limited = Your email address is visible to all authenticated users
email_visibility.private = Your email address is only visible to you and administrators
@@ -2688,6 +2693,20 @@ settings.rename_branch_from=old branch name
settings.rename_branch_to=new branch name
settings.rename_branch=Rename branch
settings.stats=Data Statistics
settings.stats.total=Total
settings.stats.storage=Storage
settings.stats.git_storage=Git Storage
settings.stats.lfs_storage=LFS Storage
settings.stats.assets_storage=Assets Storage
settings.stats.activity=Activity
settings.stats.active=Active
settings.stats.storage_breakdown=Storage Breakdown
settings.stats.no_data=No Data Available
settings.stats.no_data_description=The system is collecting your data statistics. Please try again later.
user.stats=Data Statistics
diff.browse_source = Browse Source
diff.parent = parent
diff.commit = commit

View File

@@ -692,6 +692,11 @@ following=关注中
follow=关注
unfollow=取消关注
user_bio=简历
user.stats=数据统计
user.repositories=仓库列表
user.activity=公开活动
user.starred=已点赞
user.code=代码
disabled_public_activity=该用户已隐藏活动记录。
email_visibility.limited=所有已认证用户均可看到您的邮箱地址
email_visibility.private=只有您本人和管理员可以看到您的邮箱地址
@@ -2677,6 +2682,20 @@ settings.rename_branch_from=旧分支名称
settings.rename_branch_to=新分支名称
settings.rename_branch=重命名分支
settings.stats=数据统计
settings.stats.total=总计
settings.stats.storage=存储空间
settings.stats.git_storage=Git 存储
settings.stats.lfs_storage=LFS 存储
settings.stats.assets_storage=资源存储
settings.stats.activity=活动统计
settings.stats.active=活跃
settings.stats.storage_breakdown=存储分布
settings.stats.no_data=暂无数据
settings.stats.no_data_description=系统正在收集您的数据统计信息,请稍后再试
user.stats=数据统计
diff.browse_source=浏览代码
diff.parent=父节点
diff.commit=当前提交

View File

@@ -28,6 +28,7 @@ import (
shared_user "code.gitea.io/gitea/routers/web/shared/user"
"code.gitea.io/gitea/services/context"
feed_service "code.gitea.io/gitea/services/feed"
user_service "code.gitea.io/gitea/services/user"
)
const (
@@ -160,6 +161,14 @@ func prepareUserProfileTabData(ctx *context.Context, profileDbRepo *repo_model.R
case "following":
ctx.Data["Cards"] = following
total = int(numFollowing)
case "userstats":
// Get user statistics
stats, err := user_service.GetUserStatistics(ctx, ctx.ContextUser.ID)
if err != nil {
ctx.ServerError("GetUserStatistics", err)
return
}
ctx.Data["UserStats"] = stats
case "activity":
// prepare heatmap data
if setting.Service.EnableUserHeatmap {

View File

@@ -0,0 +1,33 @@
// Copyright 2024 The DevStar Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"net/http"
"code.gitea.io/gitea/modules/templates"
user_service "code.gitea.io/gitea/services/user"
gitea_context "code.gitea.io/gitea/services/context"
)
const (
tplSettingsStats templates.TplName = "user/settings/stats"
)
// Stats render user's data statistics page
func Stats(ctx *gitea_context.Context) {
ctx.Data["Title"] = ctx.Tr("settings.stats")
ctx.Data["PageIsSettingsStats"] = true
// Get user statistics
stats, err := user_service.GetUserStatistics(ctx, ctx.ContextUser.ID)
if err != nil {
ctx.ServerError("GetUserStatistics", err)
return
}
ctx.Data["UserStats"] = stats
ctx.HTML(http.StatusOK, tplSettingsStats)
}

237
services/user/stats.go Normal file
View File

@@ -0,0 +1,237 @@
// Copyright 2024 The DevStar Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package user
import (
"context"
"code.gitea.io/gitea/models/db"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
devcontainer_model "code.gitea.io/gitea/models/devcontainer"
"code.gitea.io/gitea/modules/log"
)
// UserStats represents the user's data statistics
type UserStats struct {
UserID int64 `json:"user_id"`
// Repository statistics
TotalRepositories int `json:"total_repositories"`
PublicRepositories int `json:"public_repositories"`
PrivateRepositories int `json:"private_repositories"`
// Storage statistics
TotalStorage int64 `json:"total_storage"`
GitStorage int64 `json:"git_storage"`
LFSStorage int64 `json:"lfs_storage"`
AssetsStorage int64 `json:"assets_storage"`
// DevContainer statistics
TotalDevContainers int `json:"total_devcontainers"`
ActiveDevContainers int `json:"active_devcontainers"`
// Activity statistics
TotalCommits int `json:"total_commits"`
TotalIssues int `json:"total_issues"`
PullRequests int `json:"pull_requests"`
ClosedIssues int `json:"closed_issues"`
MergedPullRequests int `json:"merged_pull_requests"`
}
// GetUserStatistics calculates and returns comprehensive statistics for a user
func GetUserStatistics(ctx context.Context, userID int64) (*UserStats, error) {
stats := &UserStats{
UserID: userID,
}
// Get repository statistics
if err := getRepoStats(ctx, userID, stats); err != nil {
log.Error("Failed to get repository statistics for user %d: %v", userID, err)
return nil, err
}
// Get devcontainer statistics
if err := getDevContainerStats(ctx, userID, stats); err != nil {
log.Error("Failed to get devcontainer statistics for user %d: %v", userID, err)
return nil, err
}
// Get activity statistics (commits, issues, PRs)
if err := getActivityStats(ctx, userID, stats); err != nil {
log.Error("Failed to get activity statistics for user %d: %v", userID, err)
return nil, err
}
return stats, nil
}
// getRepoStats calculates repository-related statistics
func getRepoStats(ctx context.Context, userID int64, stats *UserStats) error {
// Count total repositories for the user
count, err := repo_model.CountRepositories(ctx, repo_model.CountRepositoryOptions{
OwnerID: userID,
})
if err != nil {
return err
}
stats.TotalRepositories = int(count)
// Get all repositories to calculate storage and visibility breakdown
repos, _, err := repo_model.GetUserRepositories(ctx, repo_model.SearchRepoOptions{
ListOptions: db.ListOptions{
PageSize: 1000, // Get all repos
},
Actor: &user_model.User{ID: userID},
})
if err != nil {
return err
}
var totalSize, gitSize, lfsSize int64
publicCount, privateCount := 0, 0
for _, repo := range repos {
// Count by visibility
if repo.IsPrivate {
privateCount++
} else {
publicCount++
}
// Sum storage sizes
totalSize += repo.Size
gitSize += repo.GitSize
lfsSize += repo.LFSSize
}
stats.PublicRepositories = publicCount
stats.PrivateRepositories = privateCount
stats.TotalStorage = totalSize
stats.GitStorage = gitSize
stats.LFSStorage = lfsSize
// Assets storage would need additional calculation from attachments, releases, etc.
stats.AssetsStorage = 0 // TODO: Implement assets storage calculation
return nil
}
// getDevContainerStats calculates devcontainer-related statistics
func getDevContainerStats(ctx context.Context, userID int64, stats *UserStats) error {
// Count total devcontainers for the user
count, err := db.GetEngine(ctx).
Where("user_id = ?", userID).
Count(&devcontainer_model.Devcontainer{})
if err != nil {
return err
}
stats.TotalDevContainers = int(count)
// Count active devcontainers (assuming status 1 means active)
activeCount, err := db.GetEngine(ctx).
Where("user_id = ? AND devcontainer_status = ?", userID, 1).
Count(&devcontainer_model.Devcontainer{})
if err != nil {
return err
}
stats.ActiveDevContainers = int(activeCount)
return nil
}
// getActivityStats calculates activity-related statistics (commits, issues, PRs)
func getActivityStats(ctx context.Context, userID int64, stats *UserStats) error {
// Get user's commits count across all their repositories
// This is a simplified version - in reality you'd need to query git logs
commitCount, err := getUserCommitCount(ctx, userID)
if err != nil {
log.Warn("Failed to get commit count for user %d: %v", userID, err)
commitCount = 0 // Continue without commit stats
}
stats.TotalCommits = commitCount
// Count issues created by user
issueCount, err := getUserIssueCount(ctx, userID)
if err != nil {
log.Warn("Failed to get issue count for user %d: %v", userID, err)
issueCount = 0
}
stats.TotalIssues = issueCount
// Count pull requests created by user
prCount, err := getUserPullRequestCount(ctx, userID)
if err != nil {
log.Warn("Failed to get PR count for user %d: %v", userID, err)
prCount = 0
}
stats.PullRequests = prCount
// Count closed issues and merged PRs
closedIssues, err := getUserClosedIssueCount(ctx, userID)
if err != nil {
log.Warn("Failed to get closed issue count for user %d: %v", userID, err)
closedIssues = 0
}
stats.ClosedIssues = closedIssues
mergedPRs, err := getUserMergedPullRequestCount(ctx, userID)
if err != nil {
log.Warn("Failed to get merged PR count for user %d: %v", userID, err)
mergedPRs = 0
}
stats.MergedPullRequests = mergedPRs
return nil
}
// getUserCommitCount gets the total number of commits made by the user
func getUserCommitCount(ctx context.Context, userID int64) (int, error) {
// This is a placeholder implementation
// In a real implementation, you would:
// 1. Get all repositories owned by the user
// 2. For each repository, query git log for commits by this user's email
// 3. Sum up all commits
user, err := user_model.GetUserByID(ctx, userID)
if err != nil {
return 0, err
}
// For now, return 0 as this would require git log parsing
// TODO: Implement actual commit counting
log.Info("Commit counting not yet implemented for user %s (%s)", user.Name, user.Email)
return 0, nil
}
// getUserIssueCount gets the total number of issues created by the user
func getUserIssueCount(ctx context.Context, userID int64) (int, error) {
// This would query the issues table
// For now, return a placeholder value
// TODO: Implement actual issue counting
return 0, nil
}
// getUserPullRequestCount gets the total number of pull requests created by the user
func getUserPullRequestCount(ctx context.Context, userID int64) (int, error) {
// This would query the pull_requests table
// For now, return a placeholder value
// TODO: Implement actual PR counting
return 0, nil
}
// getUserClosedIssueCount gets the number of closed issues created by the user
func getUserClosedIssueCount(ctx context.Context, userID int64) (int, error) {
// This would query the issues table for closed issues
// For now, return a placeholder value
// TODO: Implement actual closed issue counting
return 0, nil
}
// getUserMergedPullRequestCount gets the number of merged pull requests created by the user
func getUserMergedPullRequestCount(ctx context.Context, userID int64) (int, error) {
// This would query the pull_requests table for merged PRs
// For now, return a placeholder value
// TODO: Implement actual merged PR counting
return 0, nil
}

View File

@@ -30,6 +30,9 @@
</a>
{{end}}
{{if .ContextUser.IsIndividual}}
<a class="{{if eq .TabName "userstats"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=userstats">
{{svg "octicon-graph"}} 数据统计
</a>
<a class="{{if eq .TabName "activity"}}active {{end}}item" href="{{.ContextUser.HomeLink}}?tab=activity">
{{svg "octicon-rss"}} {{ctx.Locale.Tr "user.activity"}}
</a>

View File

@@ -29,6 +29,271 @@
<div id="readme_profile" class="render-content markup">{{.ProfileReadmeContent}}</div>
{{else if eq .TabName "organizations"}}
{{template "repo/user_cards" .}}
{{else if eq .TabName "userstats"}}
<div class="user-stats-container">
<style>
.ui.sub.header {
font-size: 0.85em !important;
font-weight: normal !important;
color: rgba(0, 0, 0, 0.6) !important;
}
</style>
{{if .UserStats}}
<!-- 概览统计 -->
<div class="flex-list">
<div class="flex-item">
<div class="flex-item-leading">
{{svg "octicon-repo" 32}}
</div>
<div class="flex-item-main">
<div class="flex-item-header">
<div class="flex-item-title">代码仓库</div>
</div>
<div class="flex-item-body">
<span class="text primary">{{.UserStats.TotalRepositories}}</span>
个仓库
<div class="tw-mt-1 text small">
<span class="text blue">{{.UserStats.PublicRepositories}} 个公开</span> •
<span class="text orange">{{.UserStats.PrivateRepositories}} 个私有</span>
</div>
</div>
</div>
</div>
<div class="flex-item">
<div class="flex-item-leading">
{{svg "octicon-database" 32}}
</div>
<div class="flex-item-main">
<div class="flex-item-header">
<div class="flex-item-title">存储空间</div>
</div>
<div class="flex-item-body">
<span class="text primary">{{FileSize .UserStats.TotalStorage}}</span>
总容量
<div class="tw-mt-1 text small">
<span class="text blue">{{FileSize .UserStats.GitStorage}} Git存储</span> •
<span class="text green">{{FileSize .UserStats.LFSStorage}} LFS存储</span> •
<span class="text orange">{{FileSize .UserStats.AssetsStorage}} 资源存储</span>
</div>
</div>
</div>
</div>
<div class="flex-item">
<div class="flex-item-leading">
{{svg "octicon-container" 32}}
</div>
<div class="flex-item-main">
<div class="flex-item-header">
<div class="flex-item-title">开发容器</div>
</div>
<div class="flex-item-body">
<span class="text primary">{{.UserStats.TotalDevContainers}}</span>
个容器
<div class="tw-mt-1 text small">
<span class="text green">{{.UserStats.ActiveDevContainers}} 个运行中</span>
</div>
</div>
</div>
</div>
</div>
<!-- 存储分布 -->
<div class="ui divider"></div>
<h4 class="ui header">
{{svg "octicon-graph" 16}}
<div class="content">
<div class="ui sub header">存储空间分布</div>
</div>
</h4>
<div class="flex-list">
<div class="flex-item">
<div class="flex-item-leading">
<div class="storage-chart-container">
<canvas id="storageChart" width="120" height="120"></canvas>
</div>
</div>
<div class="flex-item-main">
<div class="flex-item-header">
<div class="flex-item-title">存储空间详情</div>
</div>
<div class="flex-item-body">
<div class="tw-space-y-1">
<div class="text small">
<span class="text blue">●</span> Git仓库: {{FileSize .UserStats.GitStorage}}
</div>
<div class="text small">
<span class="text green">●</span> 大文件存储: {{FileSize .UserStats.LFSStorage}}
</div>
<div class="text small">
<span class="text orange">●</span> 资源文件: {{FileSize .UserStats.AssetsStorage}}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 活动统计 -->
<div class="ui divider"></div>
<h4 class="ui header">
{{svg "octicon-graph" 16}}
<div class="content">
<div class="ui sub header">活动统计</div>
</div>
</h4>
<div class="flex-list">
<div class="flex-item">
<div class="flex-item-leading">
{{svg "octicon-git-commit" 24}}
</div>
<div class="flex-item-main">
<div class="flex-item-header">
<div class="flex-item-title">{{.UserStats.TotalCommits}}</div>
</div>
<div class="flex-item-body text small">代码提交</div>
</div>
</div>
<div class="flex-item">
<div class="flex-item-leading">
{{svg "octicon-issue-opened" 24}}
</div>
<div class="flex-item-main">
<div class="flex-item-header">
<div class="flex-item-title">{{.UserStats.TotalIssues}}</div>
</div>
<div class="flex-item-body text small">问题反馈</div>
</div>
</div>
<div class="flex-item">
<div class="flex-item-leading">
{{svg "octicon-git-pull-request" 24}}
</div>
<div class="flex-item-main">
<div class="flex-item-header">
<div class="flex-item-title">{{.UserStats.PullRequests}}</div>
</div>
<div class="flex-item-body text small">合并请求</div>
</div>
</div>
</div>
<!-- 问题与PR进度 -->
<div class="ui divider"></div>
<div class="flex-list">
<div class="flex-item">
<div class="flex-item-leading">
{{svg "octicon-issue-closed" 24}}
</div>
<div class="flex-item-main">
<div class="flex-item-header">
<div class="flex-item-title">问题完成情况</div>
</div>
<div class="flex-item-body">
<div class="ui progress tiny" data-value="{{.UserStats.ClosedIssues}}" data-total="{{.UserStats.TotalIssues}}">
<div class="bar">
<div class="progress"></div>
</div>
<div class="label text small">
已关闭: {{.UserStats.ClosedIssues}} / 总计: {{.UserStats.TotalIssues}}
</div>
</div>
</div>
</div>
</div>
<div class="flex-item">
<div class="flex-item-leading">
{{svg "octicon-git-merge" 24}}
</div>
<div class="flex-item-main">
<div class="flex-item-header">
<div class="flex-item-title">合并请求完成情况</div>
</div>
<div class="flex-item-body">
<div class="ui progress tiny" data-value="{{.UserStats.MergedPullRequests}}" data-total="{{.UserStats.PullRequests}}">
<div class="bar">
<div class="progress"></div>
</div>
<div class="label text small">
已合并: {{.UserStats.MergedPullRequests}} / 总计: {{.UserStats.PullRequests}}
</div>
</div>
</div>
</div>
</div>
</div>
{{else}}
<div class="ui info message">
<div class="header">暂无统计数据</div>
<p>系统正在收集您的数据统计信息,请稍后再试</p>
</div>
{{end}}
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
{{if .UserStats}}
// Initialize progress bars
const progressBars = document.querySelectorAll('.ui.progress');
progressBars.forEach(function(bar) {
const value = bar.getAttribute('data-value');
const total = bar.getAttribute('data-total');
if (value && total) {
const percentage = (parseFloat(value) / parseFloat(total)) * 100;
const progressBar = bar.querySelector('.bar .progress');
if (progressBar) {
progressBar.style.width = percentage + '%';
}
}
});
// Storage breakdown chart
const ctx = document.getElementById('storageChart');
if (ctx) {
const storageData = {
git: {{.UserStats.GitStorage}},
lfs: {{.UserStats.LFSStorage}},
assets: {{.UserStats.AssetsStorage}}
};
// Simple pie chart using canvas
const total = storageData.git + storageData.lfs + storageData.assets;
if (total > 0) {
const canvas = ctx;
const width = canvas.width;
const height = canvas.height;
const centerX = width / 2;
const centerY = height / 2;
const radius = Math.min(width, height) / 2 - 5;
const context = canvas.getContext('2d');
let currentAngle = -Math.PI / 2;
const segments = [
{ value: storageData.git, color: '#4285f4' },
{ value: storageData.lfs, color: '#34a853' },
{ value: storageData.assets, color: '#ea4335' }
];
segments.forEach(function(segment) {
if (segment.value > 0) {
const sliceAngle = (segment.value / total) * 2 * Math.PI;
// Draw pie slice
context.beginPath();
context.arc(centerX, centerY, radius, currentAngle, currentAngle + sliceAngle);
context.lineTo(centerX, centerY);
context.fillStyle = segment.color;
context.fill();
currentAngle += sliceAngle;
}
});
}
}
{{end}}
});
</script>
{{else}}
{{template "shared/repo/search" .}}
{{template "shared/repo/list" .}}

View File

@@ -0,0 +1,295 @@
{{template "base/head" .}}
<div class="user setting stats">
<div class="ui container">
<div class="ui grid">
{{template "user/settings/navbar" .}}
<div class="twelve wide column content">
{{template "base/alert" .}}
<h4 class="ui top attached header">
{{ctx.Locale.Tr "settings.stats"}}
</h4>
<div class="ui attached segment">
{{if .UserStats}}
<div class="ui stackable three column grid">
<!-- Repository Statistics -->
<div class="column">
<div class="ui card">
<div class="content">
<div class="header">
<i class="book icon"></i>
{{ctx.Locale.Tr "repo.repositories"}}
</div>
<div class="description">
<div class="ui list">
<div class="item">
<strong>{{ctx.Locale.Tr "settings.stats.total"}}:</strong> {{.UserStats.TotalRepositories}}
</div>
<div class="item">
<strong>{{ctx.Locale.Tr "repo.issues"}}:</strong> {{.UserStats.PublicRepositories}}
</div>
<div class="item">
<strong>{{ctx.Locale.Tr "repo.private"}}:</strong> {{.UserStats.PrivateRepositories}}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Storage Statistics -->
<div class="column">
<div class="ui card">
<div class="content">
<div class="header">
<i class="database icon"></i>
{{ctx.Locale.Tr "settings.stats.storage"}}
</div>
<div class="description">
<div class="ui list">
<div class="item">
<strong>{{ctx.Locale.Tr "settings.stats.total"}}:</strong> {{FileSize .UserStats.TotalStorage}}
</div>
<div class="item">
<strong>{{ctx.Locale.Tr "settings.stats.git_storage"}}:</strong> {{FileSize .UserStats.GitStorage}}
</div>
<div class="item">
<strong>{{ctx.Locale.Tr "settings.stats.lfs_storage"}}:</strong> {{FileSize .UserStats.LFSStorage}}
</div>
<div class="item">
<strong>{{ctx.Locale.Tr "settings.stats.assets_storage"}}:</strong> {{FileSize .UserStats.AssetsStorage}}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- DevContainer Statistics -->
<div class="column">
<div class="ui card">
<div class="content">
<div class="header">
<i class="cube icon"></i>
{{ctx.Locale.Tr "admin.devcontainer"}}
</div>
<div class="description">
<div class="ui list">
<div class="item">
<strong>{{ctx.Locale.Tr "settings.stats.total"}}:</strong> {{.UserStats.TotalDevContainers}}
</div>
<div class="item">
<strong>{{ctx.Locale.Tr "settings.stats.active"}}:</strong> {{.UserStats.ActiveDevContainers}}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Activity Statistics -->
<div class="ui divider"></div>
<h5 class="ui header">
<i class="chart line icon"></i>
{{ctx.Locale.Tr "settings.stats.activity"}}
</h5>
<div class="ui stackable three column grid">
<div class="column">
<div class="ui statistic">
<div class="value">
{{.UserStats.TotalCommits}}
</div>
<div class="label">
<i class="code commit icon"></i>
{{ctx.Locale.Tr "repo.commits"}}
</div>
</div>
</div>
<div class="column">
<div class="ui statistic">
<div class="value">
{{.UserStats.TotalIssues}}
</div>
<div class="label">
<i class="bug icon"></i>
{{ctx.Locale.Tr "repo.issues"}}
</div>
</div>
</div>
<div class="column">
<div class="ui statistic">
<div class="value">
{{.UserStats.PullRequests}}
</div>
<div class="label">
<i class="code branch icon"></i>
{{ctx.Locale.Tr "repo.pulls"}}
</div>
</div>
</div>
</div>
<!-- Issues and PR Statistics -->
<div class="ui stackable two column grid" style="margin-top: 20px;">
<div class="column">
<div class="ui card">
<div class="content">
<div class="header">
<i class="bug icon"></i>
{{ctx.Locale.Tr "repo.issues"}}
</div>
<div class="description">
<div class="ui progress" data-value="{{.UserStats.ClosedIssues}}" data-total="{{.UserStats.TotalIssues}}">
<div class="bar">
<div class="progress"></div>
</div>
<div class="label">
{{ctx.Locale.Tr "repo.issues.closed"}}: {{.UserStats.ClosedIssues}} / {{.UserStats.TotalIssues}}
</div>
</div>
</div>
</div>
</div>
</div>
<div class="column">
<div class="ui card">
<div class="content">
<div class="header">
<i class="code branch icon"></i>
{{ctx.Locale.Tr "repo.pulls"}}
</div>
<div class="description">
<div class="ui progress" data-value="{{.UserStats.MergedPullRequests}}" data-total="{{.UserStats.PullRequests}}">
<div class="bar">
<div class="progress"></div>
</div>
<div class="label">
{{ctx.Locale.Tr "repo.pulls.merged"}}: {{.UserStats.MergedPullRequests}} / {{.UserStats.PullRequests}}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Storage Usage Chart -->
<div class="ui divider"></div>
<h5 class="ui header">
<i class="pie chart icon"></i>
{{ctx.Locale.Tr "settings.stats.storage_breakdown"}}
</h5>
<div class="ui stackable two column grid">
<div class="column">
<div class="ui small统计">
<canvas id="storageChart" width="400" height="200"></canvas>
</div>
</div>
<div class="column">
<div class="ui list">
<div class="item">
<div class="ui horizontal statistic">
<div class="value">
{{FileSize .UserStats.GitStorage}}
</div>
<div class="label">
<i class="git icon"></i>
{{ctx.Locale.Tr "settings.stats.git_storage"}}
</div>
</div>
</div>
<div class="item">
<div class="ui horizontal statistic">
<div class="value">
{{FileSize .UserStats.LFSStorage}}
</div>
<div class="label">
<i class="file archive icon"></i>
{{ctx.Locale.Tr "settings.stats.lfs_storage"}}
</div>
</div>
</div>
<div class="item">
<div class="ui horizontal statistic">
<div class="value">
{{FileSize .UserStats.AssetsStorage}}
</div>
<div class="label">
<i class="file icon"></i>
{{ctx.Locale.Tr "settings.stats.assets_storage"}}
</div>
</div>
</div>
</div>
</div>
</div>
{{else}}
<div class="ui message">
<div class="header">
{{ctx.Locale.Tr "settings.stats.no_data"}}
</div>
<p>{{ctx.Locale.Tr "settings.stats.no_data_description"}}</p>
</div>
{{end}}
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
{{if .UserStats}}
// Initialize progress bars
$('.ui.progress').progress();
// Storage breakdown chart
const ctx = document.getElementById('storageChart');
if (ctx) {
const storageData = {
git: {{.UserStats.GitStorage}},
lfs: {{.UserStats.LFSStorage}},
assets: {{.UserStats.AssetsStorage}}
};
// Simple pie chart using canvas
const total = storageData.git + storageData.lfs + storageData.assets;
if (total > 0) {
const canvas = ctx;
const width = canvas.width;
const height = canvas.height;
const centerX = width / 2;
const centerY = height / 2;
const radius = Math.min(width, height) / 2 - 10;
const context = canvas.getContext('2d');
let currentAngle = -Math.PI / 2;
const segments = [
{ value: storageData.git, color: '#4285f4', label: '{{ctx.Locale.Tr "settings.stats.git_storage"}}' },
{ value: storageData.lfs, color: '#ea4335', label: '{{ctx.Locale.Tr "settings.stats.lfs_storage"}}' },
{ value: storageData.assets, color: '#34a853', label: '{{ctx.Locale.Tr "settings.stats.assets_storage"}}' }
];
segments.forEach(function(segment) {
if (segment.value > 0) {
const sliceAngle = (segment.value / total) * 2 * Math.PI;
// Draw pie slice
context.beginPath();
context.arc(centerX, centerY, radius, currentAngle, currentAngle + sliceAngle);
context.lineTo(centerX, centerY);
context.fillStyle = segment.color;
context.fill();
currentAngle += sliceAngle;
}
});
}
}
{{end}}
});
</script>
{{template "base/footer" .}}