💱 feat(settings): introduce site-wide quota display type (USD/CNY/TOKENS/CUSTOM)

Replace the legacy boolean “DisplayInCurrencyEnabled” with an injected, type-safe
configuration `general_setting.quota_display_type`, and wire it through the
backend and frontend.

Backend
- Add `QuotaDisplayType` to `operation_setting.GeneralSetting` with injected
  registration via `config.GlobalConfig.Register("general_setting", ...)`.
  Helpers: `IsCurrencyDisplay()`, `IsCNYDisplay()`, `GetQuotaDisplayType()`.
- Expose `quota_display_type` in `/api/status` and keep legacy
  `display_in_currency` for backward compatibility.
- Logger: update `LogQuota` and `FormatQuota` to support USD/CNY/TOKENS. When
  CNY is selected, convert using `operation_setting.USDExchangeRate`.
- Controllers:
  - `billing`: compute subscription/usage amounts based on the selected type
    (USD: divide by `QuotaPerUnit`; CNY: USD→CNY; TOKENS: keep raw tokens).
  - `topup` / `topup_stripe`: treat inputs as “amount” for USD/CNY and as
    token-count for TOKENS; adjust min topup and pay money accordingly.
  - `misc`: include `quota_display_type` in status payload.
- Compatibility: in `model/option.UpdateOption`, map updates to
  `DisplayInCurrencyEnabled` → `general_setting.quota_display_type`
  (true→USD, false→TOKENS). Keep exporting the legacy key in `OptionMap`.

Frontend
- Settings: replace the “display in currency” switch with a Select
  (`general_setting.quota_display_type`) offering USD / CNY / Tokens.
  Provide fallback mapping from legacy `DisplayInCurrencyEnabled`.
- Persist `quota_display_type` to localStorage (keep `display_in_currency`
  for legacy components).
- Rendering helpers: base all quota/price rendering on `quota_display_type`;
  use `usd_exchange_rate` for CNY symbol/values.
- Pricing page: default view currency follows site display type (USD/CNY),
  while TOKENS mode still allows per-view currency toggling when needed.

Notes
- No database migrations required.
- Legacy clients remain functional via compatibility fields.
This commit is contained in:
t0ng7u
2025-09-29 23:23:31 +08:00
parent 41ea93883b
commit 8294a76bc2
46 changed files with 1268 additions and 601 deletions

View File

@@ -60,38 +60,54 @@ const ContentModal = ({
if (videoError) {
return (
<div style={{ textAlign: 'center', padding: '40px' }}>
<Text type="tertiary" style={{ display: 'block', marginBottom: '16px' }}>
<Text
type='tertiary'
style={{ display: 'block', marginBottom: '16px' }}
>
视频无法在当前浏览器中播放这可能是由于
</Text>
<Text type="tertiary" style={{ display: 'block', marginBottom: '8px', fontSize: '12px' }}>
<Text
type='tertiary'
style={{ display: 'block', marginBottom: '8px', fontSize: '12px' }}
>
视频服务商的跨域限制
</Text>
<Text type="tertiary" style={{ display: 'block', marginBottom: '8px', fontSize: '12px' }}>
<Text
type='tertiary'
style={{ display: 'block', marginBottom: '8px', fontSize: '12px' }}
>
需要特定的请求头或认证
</Text>
<Text type="tertiary" style={{ display: 'block', marginBottom: '16px', fontSize: '12px' }}>
<Text
type='tertiary'
style={{ display: 'block', marginBottom: '16px', fontSize: '12px' }}
>
防盗链保护机制
</Text>
<div style={{ marginTop: '20px' }}>
<Button
<Button
icon={<IconExternalOpen />}
onClick={handleOpenInNewTab}
style={{ marginRight: '8px' }}
>
在新标签页中打开
</Button>
<Button
icon={<IconCopy />}
onClick={handleCopyUrl}
>
<Button icon={<IconCopy />} onClick={handleCopyUrl}>
复制链接
</Button>
</div>
<div style={{ marginTop: '16px', padding: '8px', backgroundColor: '#f8f9fa', borderRadius: '4px' }}>
<Text
type="tertiary"
<div
style={{
marginTop: '16px',
padding: '8px',
backgroundColor: '#f8f9fa',
borderRadius: '4px',
}}
>
<Text
type='tertiary'
style={{ fontSize: '10px', wordBreak: 'break-all' }}
>
{modalContent}
@@ -104,22 +120,24 @@ const ContentModal = ({
return (
<div style={{ position: 'relative' }}>
{isLoading && (
<div style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
zIndex: 10
}}>
<Spin size="large" />
<div
style={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
zIndex: 10,
}}
>
<Spin size='large' />
</div>
)}
<video
src={modalContent}
controls
style={{ width: '100%' }}
<video
src={modalContent}
controls
style={{ width: '100%' }}
autoPlay
crossOrigin="anonymous"
crossOrigin='anonymous'
onError={handleVideoError}
onLoadedData={handleVideoLoaded}
onLoadStart={() => setIsLoading(true)}
@@ -134,10 +152,10 @@ const ContentModal = ({
onOk={() => setIsModalOpen(false)}
onCancel={() => setIsModalOpen(false)}
closable={null}
bodyStyle={{
height: isVideo ? '450px' : '400px',
bodyStyle={{
height: isVideo ? '450px' : '400px',
overflow: 'auto',
padding: isVideo && videoError ? '0' : '24px'
padding: isVideo && videoError ? '0' : '24px',
}}
width={800}
>