feat(frontend): 前端界面优化与使用统计功能增强 (#46)
* feat(frontend): 前端界面优化与使用统计功能增强
主要改动:
1. 表格布局统一优化
- 新增 TablePageLayout 通用布局组件
- 统一所有管理页面的表格样式和交互
- 优化 DataTable、Pagination、Select 等通用组件
2. 使用统计功能增强
- 管理端: 添加完整的筛选和显示功能
- 用户端: 完善 API Key 列显示
- 后端: 优化使用统计数据结构和查询
3. 账户组件优化
- 优化 AccountStatsModal、AccountUsageCell 等组件
- 统一进度条和统计显示样式
4. 其他改进
- 完善中英文国际化
- 统一页面样式和交互体验
- 优化各视图页面的响应式布局
* fix(test): 修复 stubUsageLogRepo.ListWithFilters 测试 stub
测试用例 GET /api/v1/usage 返回 500 是因为 stub 方法未实现,
现在正确返回基于 UserID 过滤的日志数据。
* feat(frontend): 统一日期时间显示格式
**主要改动**:
1. 增强 utils/format.ts:
- 新增 formatDateOnly() - 格式: YYYY-MM-DD
- 新增 formatDateTime() - 格式: YYYY-MM-DD HH:mm:ss
2. 全局替换视图中的格式化函数:
- 移除各视图中的自定义 formatDate 函数
- 统一导入使用 @/utils/format 中的函数
- created_at/updated_at 使用 formatDateTime
- expires_at 使用 formatDateOnly
3. 受影响的视图 (8个):
- frontend/src/views/user/KeysView.vue
- frontend/src/views/user/DashboardView.vue
- frontend/src/views/user/UsageView.vue
- frontend/src/views/user/RedeemView.vue
- frontend/src/views/admin/UsersView.vue
- frontend/src/views/admin/UsageView.vue
- frontend/src/views/admin/RedeemView.vue
- frontend/src/views/admin/SubscriptionsView.vue
**效果**:
- 日期统一显示为 YYYY-MM-DD
- 时间统一显示为 YYYY-MM-DD HH:mm:ss
- 提升可维护性,避免格式不一致
* fix(frontend): 补充遗漏的时间格式化统一
**补充修复**(基于 code review 发现的遗漏):
1. 增强 utils/format.ts:
- 新增 formatTime() - 格式: HH:mm
2. 修复 4 个遗漏的文件:
- src/views/admin/UsersView.vue
* 删除 formatExpiresAt(),改用 formatDateTime()
* 修复订阅过期时间 tooltip 显示格式不一致问题
- src/views/user/ProfileView.vue
* 删除 formatMemberSince(),改用 formatDate(date, 'YYYY-MM')
* 统一会员起始时间显示格式
- src/views/user/SubscriptionsView.vue
* 修改 formatExpirationDate() 使用 formatDateOnly()
* 保留天数计算逻辑
- src/components/account/AccountStatusIndicator.vue
* 删除本地 formatTime(),改用 utils/format 中的统一函数
* 修复 rate limit 和 overload 重置时间显示
**验证**:
- TypeScript 类型检查通过 ✓
- 前端构建成功 ✓
- 所有剩余的 toLocaleString() 都是数字格式化,属于正确用法 ✓
**效果**:
- 订阅过期时间统一为 YYYY-MM-DD HH:mm:ss
- 会员起始时间统一为 YYYY-MM
- 重置时间统一为 HH:mm
- 消除所有不规范的原生 locale 方法调用
This commit is contained in:
@@ -30,13 +30,56 @@ export default {
|
||||
title: 'Supported Providers',
|
||||
description: 'Unified API interface for AI services',
|
||||
supported: 'Supported',
|
||||
soon: 'Soon'
|
||||
soon: 'Soon',
|
||||
claude: 'Claude',
|
||||
gemini: 'Gemini',
|
||||
more: 'More'
|
||||
},
|
||||
footer: {
|
||||
allRightsReserved: 'All rights reserved.'
|
||||
}
|
||||
},
|
||||
|
||||
// Setup Wizard
|
||||
setup: {
|
||||
title: 'Sub2API Setup',
|
||||
description: 'Configure your Sub2API instance',
|
||||
database: {
|
||||
title: 'Database Configuration',
|
||||
host: 'Host',
|
||||
port: 'Port',
|
||||
username: 'Username',
|
||||
password: 'Password',
|
||||
databaseName: 'Database Name',
|
||||
sslMode: 'SSL Mode',
|
||||
ssl: {
|
||||
disable: 'Disable',
|
||||
require: 'Require',
|
||||
verifyCa: 'Verify CA',
|
||||
verifyFull: 'Verify Full'
|
||||
}
|
||||
},
|
||||
redis: {
|
||||
title: 'Redis Configuration',
|
||||
host: 'Host',
|
||||
port: 'Port',
|
||||
password: 'Password (optional)',
|
||||
database: 'Database'
|
||||
},
|
||||
admin: {
|
||||
title: 'Admin Account',
|
||||
email: 'Email',
|
||||
password: 'Password',
|
||||
confirmPassword: 'Confirm Password'
|
||||
},
|
||||
ready: {
|
||||
title: 'Ready to Install',
|
||||
database: 'Database',
|
||||
redis: 'Redis',
|
||||
adminEmail: 'Admin Email'
|
||||
}
|
||||
},
|
||||
|
||||
// Common
|
||||
common: {
|
||||
loading: 'Loading...',
|
||||
@@ -142,7 +185,20 @@ export default {
|
||||
accountCreatedSuccess: 'Account created successfully! Welcome to {siteName}.',
|
||||
turnstileExpired: 'Verification expired, please try again',
|
||||
turnstileFailed: 'Verification failed, please try again',
|
||||
completeVerification: 'Please complete the verification'
|
||||
completeVerification: 'Please complete the verification',
|
||||
verifyYourEmail: 'Verify Your Email',
|
||||
sessionExpired: 'Session expired',
|
||||
sessionExpiredDesc: 'Please go back to the registration page and start again.',
|
||||
verificationCode: 'Verification Code',
|
||||
verificationCodeHint: 'Enter the 6-digit code sent to your email',
|
||||
sendingCode: 'Sending...',
|
||||
clickToResend: 'Click to resend code',
|
||||
resendCode: 'Resend verification code',
|
||||
oauth: {
|
||||
code: 'Code',
|
||||
state: 'State',
|
||||
fullUrl: 'Full URL'
|
||||
}
|
||||
},
|
||||
|
||||
// Dashboard
|
||||
@@ -377,6 +433,12 @@ export default {
|
||||
noData: 'No data found'
|
||||
},
|
||||
|
||||
// Table
|
||||
table: {
|
||||
expandActions: 'Expand More Actions',
|
||||
collapseActions: 'Collapse Actions'
|
||||
},
|
||||
|
||||
// Pagination
|
||||
pagination: {
|
||||
showing: 'Showing',
|
||||
@@ -584,6 +646,7 @@ export default {
|
||||
actions: 'Actions',
|
||||
billingType: 'Billing Type'
|
||||
},
|
||||
rateAndAccounts: '{rate}x rate · {count} accounts',
|
||||
accountsCount: '{count} accounts',
|
||||
form: {
|
||||
name: 'Name',
|
||||
@@ -742,6 +805,13 @@ export default {
|
||||
openai: 'OpenAI',
|
||||
gemini: 'Gemini'
|
||||
},
|
||||
types: {
|
||||
oauth: 'OAuth',
|
||||
chatgptOauth: 'ChatGPT OAuth',
|
||||
responsesApi: 'Responses API',
|
||||
googleOauth: 'Google OAuth',
|
||||
codeAssist: 'Code Assist'
|
||||
},
|
||||
columns: {
|
||||
name: 'Name',
|
||||
platformType: 'Platform/Type',
|
||||
@@ -1022,6 +1092,7 @@ export default {
|
||||
todayOverview: 'Today Overview',
|
||||
cost: 'Cost',
|
||||
requests: 'Requests',
|
||||
tokens: 'Tokens',
|
||||
highestCostDay: 'Highest Cost Day',
|
||||
highestRequestDay: 'Highest Request Day',
|
||||
date: 'Date',
|
||||
@@ -1037,6 +1108,9 @@ export default {
|
||||
todayCost: 'Today Cost',
|
||||
usageTrend: '30-Day Cost & Request Trend',
|
||||
noData: 'No usage data available for this account'
|
||||
},
|
||||
usageWindow: {
|
||||
statsTitle: '5-Hour Window Usage Statistics'
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1070,6 +1144,10 @@ export default {
|
||||
enterProxyName: 'Enter proxy name',
|
||||
leaveEmptyToKeep: 'Leave empty to keep current',
|
||||
optionalAuth: 'Optional authentication',
|
||||
form: {
|
||||
hostPlaceholder: 'proxy.example.com',
|
||||
portPlaceholder: '8080'
|
||||
},
|
||||
noProxiesYet: 'No proxies yet',
|
||||
createFirstProxy: 'Create your first proxy to route traffic through it.',
|
||||
// Batch import
|
||||
@@ -1174,6 +1252,18 @@ export default {
|
||||
searchUserPlaceholder: 'Search user by email...',
|
||||
selectedUser: 'Selected',
|
||||
user: 'User',
|
||||
account: 'Account',
|
||||
group: 'Group',
|
||||
requestId: 'Request ID',
|
||||
allModels: 'All Models',
|
||||
allAccounts: 'All Accounts',
|
||||
allGroups: 'All Groups',
|
||||
allTypes: 'All Types',
|
||||
allBillingTypes: 'All Billing',
|
||||
inputCost: 'Input Cost',
|
||||
outputCost: 'Output Cost',
|
||||
cacheCreationCost: 'Cache Creation Cost',
|
||||
cacheReadCost: 'Cache Read Cost',
|
||||
failedToLoad: 'Failed to load usage records'
|
||||
},
|
||||
|
||||
@@ -1211,16 +1301,20 @@ export default {
|
||||
title: 'Site Settings',
|
||||
description: 'Customize site branding',
|
||||
siteName: 'Site Name',
|
||||
siteNamePlaceholder: 'Sub2API',
|
||||
siteNameHint: 'Displayed in emails and page titles',
|
||||
siteSubtitle: 'Site Subtitle',
|
||||
siteSubtitlePlaceholder: 'Subscription to API Conversion Platform',
|
||||
siteSubtitleHint: 'Displayed on login and register pages',
|
||||
apiBaseUrl: 'API Base URL',
|
||||
apiBaseUrlPlaceholder: 'https://api.example.com',
|
||||
apiBaseUrlHint:
|
||||
'Used for "Use Key" and "Import to CC Switch" features. Leave empty to use current site URL.',
|
||||
contactInfo: 'Contact Info',
|
||||
contactInfoPlaceholder: 'e.g., QQ: 123456789',
|
||||
contactInfoHint: 'Customer support contact info, displayed on redeem page, profile, etc.',
|
||||
docUrl: 'Documentation URL',
|
||||
docUrlPlaceholder: 'https://docs.example.com',
|
||||
docUrlHint: 'Link to your documentation site. Leave empty to hide the documentation link.',
|
||||
siteLogo: 'Site Logo',
|
||||
uploadImage: 'Upload Image',
|
||||
@@ -1236,12 +1330,18 @@ export default {
|
||||
testConnection: 'Test Connection',
|
||||
testing: 'Testing...',
|
||||
host: 'SMTP Host',
|
||||
hostPlaceholder: 'smtp.gmail.com',
|
||||
port: 'SMTP Port',
|
||||
portPlaceholder: '587',
|
||||
username: 'SMTP Username',
|
||||
usernamePlaceholder: 'your-email@gmail.com',
|
||||
password: 'SMTP Password',
|
||||
passwordPlaceholder: '********',
|
||||
passwordHint: 'Leave empty to keep existing password',
|
||||
fromEmail: 'From Email',
|
||||
fromEmailPlaceholder: 'noreply@example.com',
|
||||
fromName: 'From Name',
|
||||
fromNamePlaceholder: 'Sub2API',
|
||||
useTls: 'Use TLS',
|
||||
useTlsHint: 'Enable TLS encryption for SMTP connection'
|
||||
},
|
||||
@@ -1249,6 +1349,7 @@ export default {
|
||||
title: 'Send Test Email',
|
||||
description: 'Send a test email to verify your SMTP configuration',
|
||||
recipientEmail: 'Recipient Email',
|
||||
recipientEmailPlaceholder: 'test@example.com',
|
||||
sendTestEmail: 'Send Test Email',
|
||||
sending: 'Sending...',
|
||||
enterRecipientHint: 'Please enter a recipient email address'
|
||||
|
||||
@@ -27,13 +27,56 @@ export default {
|
||||
title: '支持的服务商',
|
||||
description: 'AI 服务的统一 API 接口',
|
||||
supported: '已支持',
|
||||
soon: '即将推出'
|
||||
soon: '即将推出',
|
||||
claude: 'Claude',
|
||||
gemini: 'Gemini',
|
||||
more: '更多'
|
||||
},
|
||||
footer: {
|
||||
allRightsReserved: '保留所有权利。'
|
||||
}
|
||||
},
|
||||
|
||||
// Setup Wizard
|
||||
setup: {
|
||||
title: 'Sub2API 安装向导',
|
||||
description: '配置您的 Sub2API 实例',
|
||||
database: {
|
||||
title: '数据库配置',
|
||||
host: '主机',
|
||||
port: '端口',
|
||||
username: '用户名',
|
||||
password: '密码',
|
||||
databaseName: '数据库名称',
|
||||
sslMode: 'SSL 模式',
|
||||
ssl: {
|
||||
disable: '禁用',
|
||||
require: '要求',
|
||||
verifyCa: '验证 CA',
|
||||
verifyFull: '完全验证'
|
||||
}
|
||||
},
|
||||
redis: {
|
||||
title: 'Redis 配置',
|
||||
host: '主机',
|
||||
port: '端口',
|
||||
password: '密码(可选)',
|
||||
database: '数据库'
|
||||
},
|
||||
admin: {
|
||||
title: '管理员账户',
|
||||
email: '邮箱',
|
||||
password: '密码',
|
||||
confirmPassword: '确认密码'
|
||||
},
|
||||
ready: {
|
||||
title: '准备安装',
|
||||
database: '数据库',
|
||||
redis: 'Redis',
|
||||
adminEmail: '管理员邮箱'
|
||||
}
|
||||
},
|
||||
|
||||
// Common
|
||||
common: {
|
||||
loading: '加载中...',
|
||||
@@ -139,7 +182,20 @@ export default {
|
||||
accountCreatedSuccess: '账户创建成功!欢迎使用 {siteName}。',
|
||||
turnstileExpired: '验证已过期,请重试',
|
||||
turnstileFailed: '验证失败,请重试',
|
||||
completeVerification: '请完成验证'
|
||||
completeVerification: '请完成验证',
|
||||
verifyYourEmail: '验证您的邮箱',
|
||||
sessionExpired: '会话已过期',
|
||||
sessionExpiredDesc: '请返回注册页面重新开始。',
|
||||
verificationCode: '验证码',
|
||||
verificationCodeHint: '请输入发送到您邮箱的6位验证码',
|
||||
sendingCode: '发送中...',
|
||||
clickToResend: '点击重新发送验证码',
|
||||
resendCode: '重新发送验证码',
|
||||
oauth: {
|
||||
code: '授权码',
|
||||
state: '状态',
|
||||
fullUrl: '完整URL'
|
||||
}
|
||||
},
|
||||
|
||||
// Dashboard
|
||||
@@ -373,6 +429,12 @@ export default {
|
||||
noData: '暂无数据'
|
||||
},
|
||||
|
||||
// Table
|
||||
table: {
|
||||
expandActions: '展开更多操作',
|
||||
collapseActions: '收起操作'
|
||||
},
|
||||
|
||||
// Pagination
|
||||
pagination: {
|
||||
showing: '显示',
|
||||
@@ -689,6 +751,7 @@ export default {
|
||||
exclusiveFilter: '独占',
|
||||
nonExclusive: '非独占',
|
||||
public: '公开',
|
||||
rateAndAccounts: '{rate}x 费率 · {count} 个账号',
|
||||
accountsCount: '{count} 个账号',
|
||||
enterGroupName: '请输入分组名称',
|
||||
optionalDescription: '可选描述',
|
||||
@@ -848,6 +911,10 @@ export default {
|
||||
},
|
||||
types: {
|
||||
oauth: 'OAuth',
|
||||
chatgptOauth: 'ChatGPT OAuth',
|
||||
responsesApi: 'Responses API',
|
||||
googleOauth: 'Google OAuth',
|
||||
codeAssist: 'Code Assist',
|
||||
api_key: 'API Key',
|
||||
cookie: 'Cookie'
|
||||
},
|
||||
@@ -857,6 +924,9 @@ export default {
|
||||
error: '错误',
|
||||
cooldown: '冷却中'
|
||||
},
|
||||
usageWindow: {
|
||||
statsTitle: '5小时窗口用量统计'
|
||||
},
|
||||
form: {
|
||||
nameLabel: '账号名称',
|
||||
namePlaceholder: '请输入账号名称',
|
||||
@@ -1125,6 +1195,7 @@ export default {
|
||||
todayOverview: '今日概览',
|
||||
cost: '费用',
|
||||
requests: '请求',
|
||||
tokens: 'Token',
|
||||
highestCostDay: '最高费用日',
|
||||
highestRequestDay: '最高请求日',
|
||||
date: '日期',
|
||||
@@ -1364,6 +1435,18 @@ export default {
|
||||
searchUserPlaceholder: '按邮箱搜索用户...',
|
||||
selectedUser: '已选择',
|
||||
user: '用户',
|
||||
account: '账户',
|
||||
group: '分组',
|
||||
requestId: '请求ID',
|
||||
allModels: '全部模型',
|
||||
allAccounts: '全部账户',
|
||||
allGroups: '全部分组',
|
||||
allTypes: '全部类型',
|
||||
allBillingTypes: '全部计费',
|
||||
inputCost: '输入成本',
|
||||
outputCost: '输出成本',
|
||||
cacheCreationCost: '缓存创建成本',
|
||||
cacheReadCost: '缓存读取成本',
|
||||
failedToLoad: '加载使用记录失败'
|
||||
},
|
||||
|
||||
@@ -1402,15 +1485,19 @@ export default {
|
||||
description: '自定义站点品牌',
|
||||
siteName: '站点名称',
|
||||
siteNameHint: '显示在邮件和页面标题中',
|
||||
siteNamePlaceholder: 'Sub2API',
|
||||
siteSubtitle: '站点副标题',
|
||||
siteSubtitleHint: '显示在登录和注册页面',
|
||||
siteSubtitlePlaceholder: '订阅转 API 转换平台',
|
||||
apiBaseUrl: 'API 端点地址',
|
||||
apiBaseUrlHint: '用于"使用密钥"和"导入到 CC Switch"功能,留空则使用当前站点地址',
|
||||
apiBaseUrlPlaceholder: 'https://api.example.com',
|
||||
contactInfo: '客服联系方式',
|
||||
contactInfoPlaceholder: '例如:QQ: 123456789',
|
||||
contactInfoHint: '填写客服联系方式,将展示在兑换页面、个人资料等位置',
|
||||
docUrl: '文档链接',
|
||||
docUrlHint: '文档网站的链接。留空则隐藏文档链接。',
|
||||
docUrlPlaceholder: 'https://docs.example.com',
|
||||
siteLogo: '站点Logo',
|
||||
uploadImage: '上传图片',
|
||||
remove: '移除',
|
||||
@@ -1425,12 +1512,18 @@ export default {
|
||||
testConnection: '测试连接',
|
||||
testing: '测试中...',
|
||||
host: 'SMTP 主机',
|
||||
hostPlaceholder: 'smtp.gmail.com',
|
||||
port: 'SMTP 端口',
|
||||
portPlaceholder: '587',
|
||||
username: 'SMTP 用户名',
|
||||
usernamePlaceholder: 'your-email@gmail.com',
|
||||
password: 'SMTP 密码',
|
||||
passwordPlaceholder: '********',
|
||||
passwordHint: '留空以保留现有密码',
|
||||
fromEmail: '发件人邮箱',
|
||||
fromEmailPlaceholder: 'noreply@example.com',
|
||||
fromName: '发件人名称',
|
||||
fromNamePlaceholder: 'Sub2API',
|
||||
useTls: '使用 TLS',
|
||||
useTlsHint: '为 SMTP 连接启用 TLS 加密'
|
||||
},
|
||||
@@ -1438,6 +1531,7 @@ export default {
|
||||
title: '发送测试邮件',
|
||||
description: '发送测试邮件以验证 SMTP 配置',
|
||||
recipientEmail: '收件人邮箱',
|
||||
recipientEmailPlaceholder: 'test@example.com',
|
||||
sendTestEmail: '发送测试邮件',
|
||||
sending: '发送中...',
|
||||
enterRecipientHint: '请输入收件人邮箱地址'
|
||||
|
||||
Reference in New Issue
Block a user