用户存储用量统计
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
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:
@@ -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
|
||||
|
||||
@@ -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=当前提交
|
||||
|
||||
@@ -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 {
|
||||
|
||||
33
routers/web/user/setting/stats.go
Normal file
33
routers/web/user/setting/stats.go
Normal 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
237
services/user/stats.go
Normal 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
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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" .}}
|
||||
|
||||
295
templates/user/settings/stats.tmpl
Normal file
295
templates/user/settings/stats.tmpl
Normal 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" .}}
|
||||
Reference in New Issue
Block a user