diff --git a/backend/internal/repository/account_repo.go b/backend/internal/repository/account_repo.go index fe6053ee..1908dd61 100644 --- a/backend/internal/repository/account_repo.go +++ b/backend/internal/repository/account_repo.go @@ -186,7 +186,7 @@ func (r *accountRepository) BatchUpdateLastUsed(ctx context.Context, updates map ids = append(ids, id) } - caseSql += " END WHERE id IN ?" + caseSql += " END WHERE id IN ? AND deleted_at IS NULL" args = append(args, ids) return r.db.WithContext(ctx).Exec(caseSql, args...).Error diff --git a/backend/internal/repository/proxy_repo.go b/backend/internal/repository/proxy_repo.go index 423584fb..ea0577c0 100644 --- a/backend/internal/repository/proxy_repo.go +++ b/backend/internal/repository/proxy_repo.go @@ -119,6 +119,7 @@ func (r *proxyRepository) CountAccountsByProxyID(ctx context.Context, proxyID in var count int64 err := r.db.WithContext(ctx).Table("accounts"). Where("proxy_id = ?", proxyID). + Where("deleted_at IS NULL"). Count(&count).Error return count, err } @@ -134,6 +135,7 @@ func (r *proxyRepository) GetAccountCountsForProxies(ctx context.Context) (map[i Table("accounts"). Select("proxy_id, COUNT(*) as count"). Where("proxy_id IS NOT NULL"). + Where("deleted_at IS NULL"). Group("proxy_id"). Scan(&results).Error if err != nil { diff --git a/backend/internal/repository/usage_log_repo.go b/backend/internal/repository/usage_log_repo.go index 84c44a91..5ee84877 100644 --- a/backend/internal/repository/usage_log_repo.go +++ b/backend/internal/repository/usage_log_repo.go @@ -182,6 +182,7 @@ func (r *usageLogRepository) GetDashboardStats(ctx context.Context) (*DashboardS COUNT(CASE WHEN rate_limited_at IS NOT NULL AND rate_limit_reset_at > ? THEN 1 END) as ratelimit_accounts, COUNT(CASE WHEN overload_until IS NOT NULL AND overload_until > ? THEN 1 END) as overload_accounts FROM accounts + WHERE deleted_at IS NULL `, service.StatusActive, service.StatusError, now, now).Scan(&accountStats).Error; err != nil { return nil, err } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 5267ad67..6563ee0c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -12,13 +12,16 @@ "axios": "^1.6.2", "chart.js": "^4.4.1", "driver.js": "^1.4.0", + "file-saver": "^2.0.5", "pinia": "^2.1.7", "vue": "^3.4.0", "vue-chartjs": "^5.3.0", "vue-i18n": "^9.14.5", - "vue-router": "^4.2.5" + "vue-router": "^4.2.5", + "xlsx": "^0.18.5" }, "devDependencies": { + "@types/file-saver": "^2.0.7", "@types/node": "^20.10.5", "@vitejs/plugin-vue": "^5.2.3", "autoprefixer": "^10.4.16", @@ -936,6 +939,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/file-saver": { + "version": "2.0.7", + "resolved": "https://registry.npmmirror.com/@types/file-saver/-/file-saver-2.0.7.tgz", + "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.19.27", "resolved": "https://registry.npmmirror.com/@types/node/-/node-20.19.27.tgz", @@ -1173,6 +1183,15 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/alien-signals": { "version": "1.0.13", "resolved": "https://registry.npmmirror.com/alien-signals/-/alien-signals-1.0.13.tgz", @@ -1406,6 +1425,19 @@ ], "license": "CC-BY-4.0" }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chart.js": { "version": "4.5.1", "resolved": "https://registry.npmmirror.com/chart.js/-/chart.js-4.5.1.tgz", @@ -1456,6 +1488,15 @@ "node": ">= 6" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmmirror.com/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1478,6 +1519,18 @@ "node": ">= 6" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmmirror.com/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz", @@ -1504,15 +1557,6 @@ "dev": true, "license": "MIT" }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -1705,15 +1749,6 @@ "node": ">= 6" } }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.19.1.tgz", @@ -1724,6 +1759,12 @@ "reusify": "^1.0.4" } }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==", + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", @@ -1773,6 +1814,15 @@ "node": ">= 6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fraction.js": { "version": "5.3.4", "resolved": "https://registry.npmmirror.com/fraction.js/-/fraction.js-5.3.4.tgz", @@ -2001,22 +2051,6 @@ "dev": true, "license": "MIT" }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/lilconfig": { "version": "3.1.3", "resolved": "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.3.tgz", @@ -2207,26 +2241,6 @@ "node": ">= 6" } }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", @@ -2477,18 +2491,6 @@ "dev": true, "license": "MIT" }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -2646,6 +2648,18 @@ "node": ">=0.10.0" } }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmmirror.com/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/strip-ansi": { "version": "7.1.2", "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.2.tgz", @@ -2834,21 +2848,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/typescript": { "version": "5.6.3", "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.6.3.tgz", @@ -3202,16 +3201,43 @@ "typescript": ">=5.0.0" } }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", "engines": { - "node": ">=0.10.0" + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmmirror.com/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmmirror.com/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" } } } diff --git a/frontend/package.json b/frontend/package.json index 2713c0f1..e4c047d5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -15,13 +15,16 @@ "axios": "^1.6.2", "chart.js": "^4.4.1", "driver.js": "^1.4.0", + "file-saver": "^2.0.5", "pinia": "^2.1.7", "vue": "^3.4.0", "vue-chartjs": "^5.3.0", "vue-i18n": "^9.14.5", - "vue-router": "^4.2.5" + "vue-router": "^4.2.5", + "xlsx": "^0.18.5" }, "devDependencies": { + "@types/file-saver": "^2.0.7", "@types/node": "^20.10.5", "@vitejs/plugin-vue": "^5.2.3", "autoprefixer": "^10.4.16", diff --git a/frontend/src/components/account/AccountTestModal.vue b/frontend/src/components/account/AccountTestModal.vue index 44befcdf..6424cbe4 100644 --- a/frontend/src/components/account/AccountTestModal.vue +++ b/frontend/src/components/account/AccountTestModal.vue @@ -362,6 +362,10 @@ const resetState = () => { } const handleClose = () => { + // 防止在连接测试进行中关闭对话框 + if (status.value === 'connecting') { + return + } closeEventSource() emit('close') } diff --git a/frontend/src/components/account/CreateAccountModal.vue b/frontend/src/components/account/CreateAccountModal.vue index 247c87f2..416f27a0 100644 --- a/frontend/src/components/account/CreateAccountModal.vue +++ b/frontend/src/components/account/CreateAccountModal.vue @@ -2,7 +2,7 @@ diff --git a/frontend/src/components/account/EditAccountModal.vue b/frontend/src/components/account/EditAccountModal.vue index 82daf6b9..5badf512 100644 --- a/frontend/src/components/account/EditAccountModal.vue +++ b/frontend/src/components/account/EditAccountModal.vue @@ -2,7 +2,7 @@
diff --git a/frontend/src/components/account/SyncFromCrsModal.vue b/frontend/src/components/account/SyncFromCrsModal.vue index 5c3349b0..4bd0320a 100644 --- a/frontend/src/components/account/SyncFromCrsModal.vue +++ b/frontend/src/components/account/SyncFromCrsModal.vue @@ -151,6 +151,10 @@ watch( ) const handleClose = () => { + // 防止在同步进行中关闭对话框 + if (syncing.value) { + return + } emit('close') } diff --git a/frontend/src/components/common/BaseDialog.vue b/frontend/src/components/common/BaseDialog.vue index 73374ac4..fab48fe0 100644 --- a/frontend/src/components/common/BaseDialog.vue +++ b/frontend/src/components/common/BaseDialog.vue @@ -1,53 +1,63 @@ diff --git a/frontend/src/components/common/ExportProgressDialog.vue b/frontend/src/components/common/ExportProgressDialog.vue new file mode 100644 index 00000000..f8712a47 --- /dev/null +++ b/frontend/src/components/common/ExportProgressDialog.vue @@ -0,0 +1,68 @@ + + + diff --git a/frontend/src/components/common/Modal.vue b/frontend/src/components/common/Modal.vue deleted file mode 100644 index 89304de1..00000000 --- a/frontend/src/components/common/Modal.vue +++ /dev/null @@ -1,120 +0,0 @@ - - - diff --git a/frontend/src/components/common/index.ts b/frontend/src/components/common/index.ts index 16b5c6ef..754034a2 100644 --- a/frontend/src/components/common/index.ts +++ b/frontend/src/components/common/index.ts @@ -1,7 +1,6 @@ // Export all common components export { default as DataTable } from './DataTable.vue' export { default as Pagination } from './Pagination.vue' -export { default as Modal } from './Modal.vue' export { default as BaseDialog } from './BaseDialog.vue' export { default as ConfirmDialog } from './ConfirmDialog.vue' export { default as StatCard } from './StatCard.vue' @@ -9,6 +8,7 @@ export { default as Toast } from './Toast.vue' export { default as LoadingSpinner } from './LoadingSpinner.vue' export { default as EmptyState } from './EmptyState.vue' export { default as LocaleSwitcher } from './LocaleSwitcher.vue' +export { default as ExportProgressDialog } from './ExportProgressDialog.vue' // Export types export type { Column } from './types' diff --git a/frontend/src/i18n/locales/en.ts b/frontend/src/i18n/locales/en.ts index e62691d9..8f1e9fa2 100644 --- a/frontend/src/i18n/locales/en.ts +++ b/frontend/src/i18n/locales/en.ts @@ -326,7 +326,8 @@ export default { customKeyHint: 'Only letters, numbers, underscores and hyphens allowed. Minimum 16 characters.', customKeyTooShort: 'Custom key must be at least 16 characters', customKeyInvalidChars: 'Custom key can only contain letters, numbers, underscores, and hyphens', - customKeyRequired: 'Please enter a custom key' + customKeyRequired: 'Please enter a custom key', + ccSwitchNotInstalled: 'CC-Switch is not installed or the protocol handler is not registered. Please install CC-Switch first or manually copy the API key.' }, // Usage @@ -345,6 +346,12 @@ export default { allApiKeys: 'All API Keys', timeRange: 'Time Range', exportCsv: 'Export CSV', + exportExcel: 'Export Excel', + exportingProgress: 'Exporting data...', + exportedCount: 'Exported {current}/{total} records', + estimatedTime: 'Estimated time remaining: {time}', + cancelExport: 'Cancel Export', + exportCancelled: 'Export cancelled', exporting: 'Exporting...', preparingExport: 'Preparing export...', model: 'Model', @@ -368,6 +375,8 @@ export default { noDataToExport: 'No data to export', exportSuccess: 'Usage data exported successfully', exportFailed: 'Failed to export usage data', + exportExcelSuccess: 'Usage data exported successfully (Excel format)', + exportExcelFailed: 'Failed to export usage data', billingType: 'Billing', balance: 'Balance', subscription: 'Subscription' @@ -1291,6 +1300,7 @@ export default { account: 'Account', group: 'Group', requestId: 'Request ID', + requestIdCopied: 'Request ID copied', allModels: 'All Models', allAccounts: 'All Accounts', allGroups: 'All Groups', @@ -1300,6 +1310,10 @@ export default { outputCost: 'Output Cost', cacheCreationCost: 'Cache Creation Cost', cacheReadCost: 'Cache Read Cost', + inputTokens: 'Input Tokens', + outputTokens: 'Output Tokens', + cacheCreationTokens: 'Cache Creation Tokens', + cacheReadTokens: 'Cache Read Tokens', failedToLoad: 'Failed to load usage records' }, diff --git a/frontend/src/i18n/locales/zh.ts b/frontend/src/i18n/locales/zh.ts index 2bb27eec..2e4c7673 100644 --- a/frontend/src/i18n/locales/zh.ts +++ b/frontend/src/i18n/locales/zh.ts @@ -322,7 +322,8 @@ export default { customKeyHint: '仅允许字母、数字、下划线和连字符,最少16个字符。', customKeyTooShort: '自定义密钥至少需要16个字符', customKeyInvalidChars: '自定义密钥只能包含字母、数字、下划线和连字符', - customKeyRequired: '请输入自定义密钥' + customKeyRequired: '请输入自定义密钥', + ccSwitchNotInstalled: 'CC-Switch 未安装或协议处理程序未注册。请先安装 CC-Switch 或手动复制 API 密钥。' }, // Usage @@ -341,6 +342,12 @@ export default { allApiKeys: '全部密钥', timeRange: '时间范围', exportCsv: '导出 CSV', + exportExcel: '导出 Excel', + exportingProgress: '正在导出数据...', + exportedCount: '已导出 {current}/{total} 条', + estimatedTime: '预计剩余时间:{time}', + cancelExport: '取消导出', + exportCancelled: '导出已取消', exporting: '导出中...', preparingExport: '正在准备导出...', model: '模型', @@ -364,6 +371,8 @@ export default { noDataToExport: '没有可导出的数据', exportSuccess: '使用数据导出成功', exportFailed: '使用数据导出失败', + exportExcelSuccess: '使用数据导出成功(Excel格式)', + exportExcelFailed: '使用数据导出失败', billingType: '消费类型', balance: '余额', subscription: '订阅' @@ -1490,6 +1499,7 @@ export default { account: '账户', group: '分组', requestId: '请求ID', + requestIdCopied: '请求ID已复制', allModels: '全部模型', allAccounts: '全部账户', allGroups: '全部分组', @@ -1499,6 +1509,10 @@ export default { outputCost: '输出成本', cacheCreationCost: '缓存创建成本', cacheReadCost: '缓存读取成本', + inputTokens: '输入 Token', + outputTokens: '输出 Token', + cacheCreationTokens: '缓存创建 Token', + cacheReadTokens: '缓存读取 Token', failedToLoad: '加载使用记录失败' }, diff --git a/frontend/src/style.css b/frontend/src/style.css index f7c2e6ae..bd86ab6d 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -79,6 +79,20 @@ @apply hover:from-red-600 hover:to-red-700 hover:shadow-lg hover:shadow-red-500/30; } + .btn-success { + @apply bg-gradient-to-r from-emerald-500 to-emerald-600; + @apply text-white shadow-md shadow-emerald-500/25; + @apply hover:from-emerald-600 hover:to-emerald-700 hover:shadow-lg hover:shadow-emerald-500/30; + @apply dark:shadow-emerald-500/20; + } + + .btn-warning { + @apply bg-gradient-to-r from-amber-500 to-amber-600; + @apply text-white shadow-md shadow-amber-500/25; + @apply hover:from-amber-600 hover:to-amber-700 hover:shadow-lg hover:shadow-amber-500/30; + @apply dark:shadow-amber-500/20; + } + .btn-sm { @apply rounded-lg px-3 py-1.5 text-xs; } @@ -130,6 +144,20 @@ -moz-appearance: textfield; } + /* ============ 玻璃效果 ============ */ + .glass { + @apply bg-white/80 backdrop-blur-xl dark:bg-dark-800/80; + } + + .glass-card { + @apply bg-white/70 dark:bg-dark-800/70; + @apply backdrop-blur-xl; + @apply rounded-2xl; + @apply border border-white/20 dark:border-dark-700/50; + @apply shadow-glass; + @apply transition-all duration-300; + } + /* ============ 卡片样式 ============ */ .card { @apply bg-white dark:bg-dark-800/50; @@ -151,6 +179,20 @@ @apply shadow-glass; } + .card-header { + @apply border-b border-gray-100 dark:border-dark-700; + @apply px-6 py-4; + } + + .card-body { + @apply p-6; + } + + .card-footer { + @apply border-t border-gray-100 dark:border-dark-700; + @apply px-6 py-4; + } + /* ============ 统计卡片 ============ */ .stat-card { @apply card p-5; @@ -256,6 +298,10 @@ @apply bg-gray-100 text-gray-700 dark:bg-dark-700 dark:text-dark-300; } + .badge-purple { + @apply bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400; + } + /* ============ 下拉菜单 ============ */ .dropdown { @apply absolute z-50; @@ -283,15 +329,19 @@ } .modal-content { + @apply w-full; + @apply max-h-[95vh] sm:max-h-[90vh]; @apply bg-white dark:bg-dark-800; @apply rounded-2xl shadow-2xl; - @apply w-full; - @apply max-h-[90vh] overflow-y-auto; + @apply border border-gray-200 dark:border-dark-700; + @apply flex flex-col; } .modal-header { - @apply border-b border-gray-100 px-6 py-4 dark:border-dark-700; + @apply border-b border-gray-200 px-4 py-3 dark:border-dark-700; + @apply sm:px-6 sm:py-4; @apply flex items-center justify-between; + @apply flex-shrink-0; } .modal-title { @@ -299,12 +349,69 @@ } .modal-body { - @apply px-6 py-4; + @apply px-4 py-3; + @apply sm:px-6 sm:py-4; + @apply flex-1 overflow-y-auto; } .modal-footer { - @apply border-t border-gray-100 px-6 py-4 dark:border-dark-700; + @apply border-t border-gray-200 px-4 py-3 dark:border-dark-700; + @apply sm:px-6 sm:py-4; @apply flex items-center justify-end gap-3; + @apply flex-shrink-0; + } + + /* 防止body滚动的工具类 */ + body.modal-open { + overflow: hidden; + } + + .modal-enter-active { + transition: opacity 250ms ease-out; + } + + .modal-leave-active { + transition: opacity 200ms ease-in; + } + + .modal-enter-from, + .modal-leave-to { + opacity: 0; + } + + .modal-enter-active .modal-content { + transition: transform 250ms ease-out, opacity 250ms ease-out; + } + + .modal-leave-active .modal-content { + transition: transform 200ms ease-in, opacity 200ms ease-in; + } + + .modal-enter-from .modal-content, + .modal-leave-to .modal-content { + transform: scale(0.95); + opacity: 0; + } + + .modal-enter-to .modal-content, + .modal-leave-from .modal-content { + transform: scale(1); + opacity: 1; + } + + @media (prefers-reduced-motion: reduce) { + .modal-enter-active, + .modal-leave-active, + .modal-enter-active .modal-content, + .modal-leave-active .modal-content { + transition-duration: 1ms; + transition-delay: 0ms; + } + + .modal-enter-from .modal-content, + .modal-leave-to .modal-content { + transform: none; + } } /* ============ Dialog ============ */ diff --git a/frontend/src/views/admin/AccountsView.vue b/frontend/src/views/admin/AccountsView.vue index 1d5f7653..916f6fb7 100644 --- a/frontend/src/views/admin/AccountsView.vue +++ b/frontend/src/views/admin/AccountsView.vue @@ -105,65 +105,65 @@