Files
new-api/web/src/components/table/model-pricing/layout/header/SearchActions.jsx
t0ng7u 39a868faea 💱 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.
2025-09-29 23:23:31 +08:00

159 lines
4.7 KiB
JavaScript

/*
Copyright (C) 2025 QuantumNous
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
For commercial licensing, please contact support@quantumnous.com
*/
import React, { memo, useCallback } from 'react';
import { Input, Button, Switch, Select, Divider } from '@douyinfe/semi-ui';
import { IconSearch, IconCopy, IconFilter } from '@douyinfe/semi-icons';
const SearchActions = memo(
({
selectedRowKeys = [],
copyText,
handleChange,
handleCompositionStart,
handleCompositionEnd,
isMobile = false,
searchValue = '',
setShowFilterModal,
showWithRecharge,
setShowWithRecharge,
currency,
setCurrency,
showRatio,
setShowRatio,
viewMode,
setViewMode,
tokenUnit,
setTokenUnit,
t,
}) => {
const handleCopyClick = useCallback(() => {
if (copyText && selectedRowKeys.length > 0) {
copyText(selectedRowKeys);
}
}, [copyText, selectedRowKeys]);
const handleFilterClick = useCallback(() => {
setShowFilterModal?.(true);
}, [setShowFilterModal]);
const handleViewModeToggle = useCallback(() => {
setViewMode?.(viewMode === 'table' ? 'card' : 'table');
}, [viewMode, setViewMode]);
const handleTokenUnitToggle = useCallback(() => {
setTokenUnit?.(tokenUnit === 'K' ? 'M' : 'K');
}, [tokenUnit, setTokenUnit]);
return (
<div className='flex items-center gap-2 w-full'>
<div className='flex-1'>
<Input
prefix={<IconSearch />}
placeholder={t('模糊搜索模型名称')}
value={searchValue}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
onChange={handleChange}
showClear
/>
</div>
<Button
theme='outline'
type='primary'
icon={<IconCopy />}
onClick={handleCopyClick}
disabled={selectedRowKeys.length === 0}
className='!bg-blue-500 hover:!bg-blue-600 !text-white disabled:!bg-gray-300 disabled:!text-gray-500'
>
{t('复制')}
</Button>
{!isMobile && (
<>
<Divider layout='vertical' margin='8px' />
{/* 充值价格显示开关 */}
<div className='flex items-center gap-2'>
<span className='text-sm text-gray-600'>{t('充值价格显示')}</span>
<Switch
checked={showWithRecharge}
onChange={setShowWithRecharge}
/>
</div>
{/* 货币单位选择 */}
{showWithRecharge && (
<Select
value={currency}
onChange={setCurrency}
optionList={[
{ value: 'USD', label: 'USD' },
{ value: 'CNY', label: 'CNY' },
{ value: 'CUSTOM', label: t('自定义货币') },
]}
/>
)}
{/* 显示倍率开关 */}
<div className='flex items-center gap-2'>
<span className='text-sm text-gray-600'>{t('倍率')}</span>
<Switch checked={showRatio} onChange={setShowRatio} />
</div>
{/* 视图模式切换按钮 */}
<Button
theme={viewMode === 'table' ? 'solid' : 'outline'}
type={viewMode === 'table' ? 'primary' : 'tertiary'}
onClick={handleViewModeToggle}
>
{t('表格视图')}
</Button>
{/* Token单位切换按钮 */}
<Button
theme={tokenUnit === 'K' ? 'solid' : 'outline'}
type={tokenUnit === 'K' ? 'primary' : 'tertiary'}
onClick={handleTokenUnitToggle}
>
{tokenUnit}
</Button>
</>
)}
{isMobile && (
<Button
theme='outline'
type='tertiary'
icon={<IconFilter />}
onClick={handleFilterClick}
>
{t('筛选')}
</Button>
)}
</div>
);
},
);
SearchActions.displayName = 'SearchActions';
export default SearchActions;