## 当前状态 - 插件界面已完成重命名 (cursorpro → hummingbird) - 双账号池 UI 已实现 (Auto/Pro 卡片) - 后端已切换到 MySQL 数据库 - 添加了 Cursor 官方用量 API 文档 ## 已知问题 (待修复) 1. 激活时检查账号导致无账号时激活失败 2. 未启用无感换号时不应获取账号 3. 账号用量模块不显示 (seamless 未启用时应隐藏) 4. 积分显示为 0 (后端未正确返回) 5. Auto/Pro 双密钥逻辑混乱,状态不同步 6. 账号添加后无自动分析功能 ## 下一版本计划 - 重构数据模型,优化账号状态管理 - 实现 Cursor API 自动分析账号 - 修复激活流程,不依赖账号 - 启用无感时才分配账号 - 完善账号用量实时显示 ## 文件说明 - docs/系统设计文档.md - 完整架构设计 - cursor 官方用量接口.md - Cursor API 文档 - 参考计费/ - Vibeviewer 开源项目参考 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
116 lines
4.1 KiB
Swift
116 lines
4.1 KiB
Swift
import SwiftUI
|
||
import VibeviewerModel
|
||
import VibeviewerCore
|
||
import VibeviewerShareUI
|
||
|
||
struct TotalCreditsUsageView: View {
|
||
let snapshot: DashboardSnapshot?
|
||
|
||
@State private var isModelsUsageExpanded: Bool = false
|
||
|
||
var body: some View {
|
||
VStack(alignment: .trailing, spacing: 6) {
|
||
if let billingCycleText {
|
||
Text(billingCycleText)
|
||
.font(.app(.satoshiRegular, size: 10))
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
|
||
headerView
|
||
|
||
if isModelsUsageExpanded, let modelsUsageSummary = snapshot?.modelsUsageSummary {
|
||
modelsUsageDetailView(modelsUsageSummary)
|
||
}
|
||
|
||
Text(snapshot?.displayTotalUsageCents.dollarStringFromCents ?? "0")
|
||
.font(.app(.satoshiBold, size: 16))
|
||
.foregroundStyle(.primary)
|
||
.contentTransition(.numericText())
|
||
}
|
||
.maxFrame(true, false, alignment: .trailing)
|
||
}
|
||
|
||
private var headerView: some View {
|
||
HStack(alignment: .center, spacing: 4) {
|
||
Text("Total Credits Usage")
|
||
.font(.app(.satoshiRegular, size: 12))
|
||
.foregroundStyle(.secondary)
|
||
|
||
// 如果有模型用量数据,显示展开/折叠箭头
|
||
if snapshot?.modelsUsageSummary != nil {
|
||
Image(systemName: "chevron.down")
|
||
.font(.system(size: 10, weight: .semibold))
|
||
.foregroundStyle(.secondary)
|
||
.rotationEffect(.degrees(isModelsUsageExpanded ? 180 : 0))
|
||
}
|
||
}
|
||
.onTapGesture {
|
||
if snapshot?.modelsUsageSummary != nil {
|
||
isModelsUsageExpanded.toggle()
|
||
}
|
||
}
|
||
.maxFrame(true, false, alignment: .trailing)
|
||
}
|
||
|
||
private func modelsUsageDetailView(_ summary: ModelsUsageSummary) -> some View {
|
||
VStack(alignment: .leading, spacing: 12) {
|
||
ForEach(summary.modelsSortedByCost.prefix(5), id: \.modelName) { model in
|
||
UsageEventView.EventItemView(event: makeAggregateEvent(from: model))
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
/// 将模型聚合数据映射为一个“虚构”的 UsageEvent,供 UsageEventView.EventItemView 复用 UI
|
||
private func makeAggregateEvent(from model: ModelUsageInfo) -> UsageEvent {
|
||
let tokenUsage = TokenUsage(
|
||
outputTokens: model.outputTokens,
|
||
inputTokens: model.inputTokens,
|
||
totalCents: model.costCents,
|
||
cacheWriteTokens: model.cacheWriteTokens,
|
||
cacheReadTokens: model.cacheReadTokens
|
||
)
|
||
|
||
// occurredAtMs 使用 "0" 即可,这里不会参与分组和排序,仅用于展示
|
||
return UsageEvent(
|
||
occurredAtMs: "0",
|
||
modelName: model.modelName,
|
||
kind: "aggregate",
|
||
requestCostCount: 0,
|
||
usageCostDisplay: model.formattedCost,
|
||
usageCostCents: Int(model.costCents.rounded()),
|
||
isTokenBased: true,
|
||
userDisplayName: "",
|
||
cursorTokenFee: 0,
|
||
tokenUsage: tokenUsage
|
||
)
|
||
}
|
||
|
||
/// 当前计费周期展示文案(如 "Billing cycle: Oct 1 – Oct 31")
|
||
private var billingCycleText: String? {
|
||
guard
|
||
let startMs = snapshot?.billingCycleStartMs,
|
||
let endMs = snapshot?.billingCycleEndMs,
|
||
let startDate = Date.fromMillisecondsString(startMs),
|
||
let endDate = Date.fromMillisecondsString(endMs)
|
||
else {
|
||
return nil
|
||
}
|
||
|
||
let formatter = DateFormatter()
|
||
formatter.dateFormat = "MMM d"
|
||
let start = formatter.string(from: startDate)
|
||
let end = formatter.string(from: endDate)
|
||
|
||
return "\(start) – \(end)"
|
||
}
|
||
|
||
private func formatNumber(_ number: Int) -> String {
|
||
let formatter = NumberFormatter()
|
||
formatter.numberStyle = .decimal
|
||
formatter.groupingSeparator = ","
|
||
return formatter.string(from: NSNumber(value: number)) ?? "\(number)"
|
||
}
|
||
}
|
||
|