Просмотр исходного кода

✨ refactor: Restructure model pricing components and improve UX consistency

- **Fix SideSheet double-click issue**: Remove early return for null modelData to prevent rendering blockage during async state updates
- **Component modularization**:
  - Split ModelDetailSideSheet into focused sub-components (ModelHeader, ModelBasicInfo, ModelEndpoints, ModelPricingTable)
  - Refactor PricingFilterModal with FilterModalContent and FilterModalFooter components
  - Remove unnecessary FilterSection wrapper for cleaner interface
- **Improve visual consistency**:
  - Unify avatar/icon logic between ModelHeader and PricingCardView components
  - Standardize tag colors across all pricing components (violet/teal for billing types)
  - Apply consistent dashed border styling using Semi UI theme colors
- **Enhance data accuracy**:
  - Display raw endpoint type names (e.g., "openai", "anthropic") instead of translated descriptions
  - Remove text alignment classes for better responsive layout
  - Add proper null checks to prevent runtime errors
- **Code quality improvements**:
  - Reduce component complexity by 52-74% through modularization
  - Improve maintainability with single responsibility principle
  - Add comprehensive error handling for edge cases

This refactoring improves component reusability, reduces bundle size, and provides a more consistent user experience across the model pricing interface.
t0ng7u 7 месяцев назад
Родитель
Сommit
0b1a1ca064

+ 1 - 9
web/src/components/table/model-pricing/filter/PricingEndpointTypes.jsx

@@ -55,15 +55,7 @@ const PricingEndpointTypes = ({ filterEndpointType, setFilterEndpointType, model
 
   // 端点类型显示名称映射
   const getEndpointTypeLabel = (endpointType) => {
-    const labelMap = {
-      'openai': 'OpenAI',
-      'openai-response': 'OpenAI Response',
-      'anthropic': 'Anthropic',
-      'gemini': 'Gemini',
-      'jina-rerank': 'Jina Rerank',
-      'image-generation': t('图像生成'),
-    };
-    return labelMap[endpointType] || endpointType;
+    return endpointType;
   };
 
   const availableEndpointTypes = getAllEndpointTypes();

+ 15 - 0
web/src/components/table/model-pricing/layout/PricingPage.jsx

@@ -21,6 +21,7 @@ import React from 'react';
 import { Layout, ImagePreview } from '@douyinfe/semi-ui';
 import PricingSidebar from './PricingSidebar';
 import PricingContent from './content/PricingContent';
+import ModelDetailSideSheet from '../modal/ModelDetailSideSheet';
 import { useModelPricingData } from '../../../../hooks/model-pricing/useModelPricingData';
 import { useIsMobile } from '../../../../hooks/common/useIsMobile';
 
@@ -66,6 +67,20 @@ const PricingPage = () => {
         visible={pricingData.isModalOpenurl}
         onVisibleChange={(visible) => pricingData.setIsModalOpenurl(visible)}
       />
+
+      <ModelDetailSideSheet
+        visible={pricingData.showModelDetail}
+        onClose={pricingData.closeModelDetail}
+        modelData={pricingData.selectedModel}
+        selectedGroup={pricingData.selectedGroup}
+        groupRatio={pricingData.groupRatio}
+        usableGroup={pricingData.usableGroup}
+        currency={pricingData.currency}
+        tokenUnit={pricingData.tokenUnit}
+        displayPrice={pricingData.displayPrice}
+        showRatio={allProps.showRatio}
+        t={pricingData.t}
+      />
     </div>
   );
 };

+ 103 - 0
web/src/components/table/model-pricing/modal/ModelDetailSideSheet.jsx

@@ -0,0 +1,103 @@
+/*
+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 from 'react';
+import {
+  SideSheet,
+  Typography,
+  Button,
+} from '@douyinfe/semi-ui';
+import {
+  IconClose,
+} from '@douyinfe/semi-icons';
+
+import { useIsMobile } from '../../../../hooks/common/useIsMobile';
+import ModelHeader from './components/ModelHeader';
+import ModelBasicInfo from './components/ModelBasicInfo';
+import ModelEndpoints from './components/ModelEndpoints';
+import ModelPricingTable from './components/ModelPricingTable';
+
+const { Text } = Typography;
+
+const ModelDetailSideSheet = ({
+  visible,
+  onClose,
+  modelData,
+  selectedGroup,
+  groupRatio,
+  currency,
+  tokenUnit,
+  displayPrice,
+  showRatio,
+  usableGroup,
+  t,
+}) => {
+  const isMobile = useIsMobile();
+
+  return (
+    <SideSheet
+      placement="right"
+      title={<ModelHeader modelData={modelData} t={t} />}
+      bodyStyle={{
+        padding: '0',
+        display: 'flex',
+        flexDirection: 'column',
+        borderBottom: '1px solid var(--semi-color-border)'
+      }}
+      visible={visible}
+      width={isMobile ? '100%' : 600}
+      closeIcon={
+        <Button
+          className="semi-button-tertiary semi-button-size-small semi-button-borderless"
+          type="button"
+          icon={<IconClose />}
+          onClick={onClose}
+        />
+      }
+      onCancel={onClose}
+    >
+      <div className="p-2">
+        {!modelData && (
+          <div className="flex justify-center items-center py-10">
+            <Text type="secondary">{t('加载中...')}</Text>
+          </div>
+        )}
+        {modelData && (
+          <>
+            <ModelBasicInfo modelData={modelData} t={t} />
+            <ModelEndpoints modelData={modelData} t={t} />
+            <ModelPricingTable
+              modelData={modelData}
+              selectedGroup={selectedGroup}
+              groupRatio={groupRatio}
+              currency={currency}
+              tokenUnit={tokenUnit}
+              displayPrice={displayPrice}
+              showRatio={showRatio}
+              usableGroup={usableGroup}
+              t={t}
+            />
+          </>
+        )}
+      </div>
+    </SideSheet>
+  );
+};
+
+export default ModelDetailSideSheet; 

+ 21 - 103
web/src/components/table/model-pricing/modal/PricingFilterModal.jsx

@@ -18,13 +18,10 @@ For commercial licensing, please contact support@quantumnous.com
 */
 
 import React from 'react';
-import { Modal, Button } from '@douyinfe/semi-ui';
-import PricingCategories from '../filter/PricingCategories';
-import PricingGroups from '../filter/PricingGroups';
-import PricingQuotaTypes from '../filter/PricingQuotaTypes';
-import PricingEndpointTypes from '../filter/PricingEndpointTypes';
-import PricingDisplaySettings from '../filter/PricingDisplaySettings';
+import { Modal } from '@douyinfe/semi-ui';
 import { resetPricingFilters } from '../../../../helpers/utils';
+import FilterModalContent from './components/FilterModalContent';
+import FilterModalFooter from './components/FilterModalFooter';
 
 const PricingFilterModal = ({
   visible,
@@ -32,64 +29,28 @@ const PricingFilterModal = ({
   sidebarProps,
   t
 }) => {
-  const {
-    showWithRecharge,
-    setShowWithRecharge,
-    currency,
-    setCurrency,
-    handleChange,
-    setActiveKey,
-    showRatio,
-    setShowRatio,
-    viewMode,
-    setViewMode,
-    filterGroup,
-    setFilterGroup,
-    filterQuotaType,
-    setFilterQuotaType,
-    filterEndpointType,
-    setFilterEndpointType,
-    currentPage,
-    setCurrentPage,
-    tokenUnit,
-    setTokenUnit,
-    loading,
-    ...categoryProps
-  } = sidebarProps;
-
   const handleResetFilters = () =>
     resetPricingFilters({
-      handleChange,
-      setActiveKey,
-      availableCategories: categoryProps.availableCategories,
-      setShowWithRecharge,
-      setCurrency,
-      setShowRatio,
-      setViewMode,
-      setFilterGroup,
-      setFilterQuotaType,
-      setFilterEndpointType,
-      setCurrentPage,
-      setTokenUnit,
+      handleChange: sidebarProps.handleChange,
+      setActiveKey: sidebarProps.setActiveKey,
+      availableCategories: sidebarProps.availableCategories,
+      setShowWithRecharge: sidebarProps.setShowWithRecharge,
+      setCurrency: sidebarProps.setCurrency,
+      setShowRatio: sidebarProps.setShowRatio,
+      setViewMode: sidebarProps.setViewMode,
+      setFilterGroup: sidebarProps.setFilterGroup,
+      setFilterQuotaType: sidebarProps.setFilterQuotaType,
+      setFilterEndpointType: sidebarProps.setFilterEndpointType,
+      setCurrentPage: sidebarProps.setCurrentPage,
+      setTokenUnit: sidebarProps.setTokenUnit,
     });
 
   const footer = (
-    <div className="flex justify-end">
-      <Button
-        theme="outline"
-        type='tertiary'
-        onClick={handleResetFilters}
-      >
-        {t('重置')}
-      </Button>
-      <Button
-        theme="solid"
-        type="primary"
-        onClick={onClose}
-      >
-        {t('确定')}
-      </Button>
-    </div>
+    <FilterModalFooter
+      onReset={handleResetFilters}
+      onConfirm={onClose}
+      t={t}
+    />
   );
 
   return (
@@ -107,50 +68,7 @@ const PricingFilterModal = ({
         msOverflowStyle: 'none'
       }}
     >
-      <div className="p-2">
-        <PricingDisplaySettings
-          showWithRecharge={showWithRecharge}
-          setShowWithRecharge={setShowWithRecharge}
-          currency={currency}
-          setCurrency={setCurrency}
-          showRatio={showRatio}
-          setShowRatio={setShowRatio}
-          viewMode={viewMode}
-          setViewMode={setViewMode}
-          tokenUnit={tokenUnit}
-          setTokenUnit={setTokenUnit}
-          loading={loading}
-          t={t}
-        />
-
-        <PricingCategories {...categoryProps} setActiveKey={setActiveKey} loading={loading} t={t} />
-
-        <PricingGroups
-          filterGroup={filterGroup}
-          setFilterGroup={setFilterGroup}
-          usableGroup={categoryProps.usableGroup}
-          groupRatio={categoryProps.groupRatio}
-          models={categoryProps.models}
-          loading={loading}
-          t={t}
-        />
-
-        <PricingQuotaTypes
-          filterQuotaType={filterQuotaType}
-          setFilterQuotaType={setFilterQuotaType}
-          models={categoryProps.models}
-          loading={loading}
-          t={t}
-        />
-
-        <PricingEndpointTypes
-          filterEndpointType={filterEndpointType}
-          setFilterEndpointType={setFilterEndpointType}
-          models={categoryProps.models}
-          loading={loading}
-          t={t}
-        />
-      </div>
+      <FilterModalContent sidebarProps={sidebarProps} t={t} />
     </Modal>
   );
 };

+ 99 - 0
web/src/components/table/model-pricing/modal/components/FilterModalContent.jsx

@@ -0,0 +1,99 @@
+/*
+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 from 'react';
+import PricingDisplaySettings from '../../filter/PricingDisplaySettings';
+import PricingCategories from '../../filter/PricingCategories';
+import PricingGroups from '../../filter/PricingGroups';
+import PricingQuotaTypes from '../../filter/PricingQuotaTypes';
+import PricingEndpointTypes from '../../filter/PricingEndpointTypes';
+
+const FilterModalContent = ({ sidebarProps, t }) => {
+  const {
+    showWithRecharge,
+    setShowWithRecharge,
+    currency,
+    setCurrency,
+    handleChange,
+    setActiveKey,
+    showRatio,
+    setShowRatio,
+    viewMode,
+    setViewMode,
+    filterGroup,
+    setFilterGroup,
+    filterQuotaType,
+    setFilterQuotaType,
+    filterEndpointType,
+    setFilterEndpointType,
+    tokenUnit,
+    setTokenUnit,
+    loading,
+    ...categoryProps
+  } = sidebarProps;
+
+  return (
+    <div className="p-2">
+      <PricingDisplaySettings
+        showWithRecharge={showWithRecharge}
+        setShowWithRecharge={setShowWithRecharge}
+        currency={currency}
+        setCurrency={setCurrency}
+        showRatio={showRatio}
+        setShowRatio={setShowRatio}
+        viewMode={viewMode}
+        setViewMode={setViewMode}
+        tokenUnit={tokenUnit}
+        setTokenUnit={setTokenUnit}
+        loading={loading}
+        t={t}
+      />
+
+      <PricingCategories {...categoryProps} setActiveKey={setActiveKey} loading={loading} t={t} />
+
+      <PricingGroups
+        filterGroup={filterGroup}
+        setFilterGroup={setFilterGroup}
+        usableGroup={categoryProps.usableGroup}
+        groupRatio={categoryProps.groupRatio}
+        models={categoryProps.models}
+        loading={loading}
+        t={t}
+      />
+
+      <PricingQuotaTypes
+        filterQuotaType={filterQuotaType}
+        setFilterQuotaType={setFilterQuotaType}
+        models={categoryProps.models}
+        loading={loading}
+        t={t}
+      />
+
+      <PricingEndpointTypes
+        filterEndpointType={filterEndpointType}
+        setFilterEndpointType={setFilterEndpointType}
+        models={categoryProps.models}
+        loading={loading}
+        t={t}
+      />
+    </div>
+  );
+};
+
+export default FilterModalContent; 

+ 44 - 0
web/src/components/table/model-pricing/modal/components/FilterModalFooter.jsx

@@ -0,0 +1,44 @@
+/*
+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 from 'react';
+import { Button } from '@douyinfe/semi-ui';
+
+const FilterModalFooter = ({ onReset, onConfirm, t }) => {
+  return (
+    <div className="flex justify-end">
+      <Button
+        theme="outline"
+        type='tertiary'
+        onClick={onReset}
+      >
+        {t('重置')}
+      </Button>
+      <Button
+        theme="solid"
+        type="primary"
+        onClick={onConfirm}
+      >
+        {t('确定')}
+      </Button>
+    </div>
+  );
+};
+
+export default FilterModalFooter; 

+ 55 - 0
web/src/components/table/model-pricing/modal/components/ModelBasicInfo.jsx

@@ -0,0 +1,55 @@
+/*
+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 from 'react';
+import { Card, Avatar, Typography } from '@douyinfe/semi-ui';
+import { IconInfoCircle } from '@douyinfe/semi-icons';
+
+const { Text } = Typography;
+
+const ModelBasicInfo = ({ modelData, t }) => {
+  // 获取模型描述
+  const getModelDescription = () => {
+    if (!modelData) return t('暂无模型描述');
+    // 这里可以根据模型名称返回不同的描述
+    if (modelData.model_name?.includes('gpt-4o-image')) {
+      return t('逆向plus账号的可绘图的gpt-4o模型,由于OpenAI限制绘图数量,并非每次都能绘图成功,由于是逆向模型,只要请求成功,即使绘图失败也会扣费,请谨慎使用。推荐使用正式版的 gpt-image-1模型。');
+    }
+    return modelData.description || t('暂无模型描述');
+  };
+
+  return (
+    <Card className="!rounded-2xl shadow-sm border-0 mb-6">
+      <div className="flex items-center mb-4">
+        <Avatar size="small" color="blue" className="mr-2 shadow-md">
+          <IconInfoCircle size={16} />
+        </Avatar>
+        <div>
+          <Text className="text-lg font-medium">{t('基本信息')}</Text>
+          <div className="text-xs text-gray-600">{t('模型的详细描述和基本特性')}</div>
+        </div>
+      </div>
+      <div className="text-gray-600">
+        <p>{getModelDescription()}</p>
+      </div>
+    </Card>
+  );
+};
+
+export default ModelBasicInfo; 

+ 69 - 0
web/src/components/table/model-pricing/modal/components/ModelEndpoints.jsx

@@ -0,0 +1,69 @@
+/*
+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 from 'react';
+import { Card, Avatar, Typography, Badge } from '@douyinfe/semi-ui';
+import { IconLink } from '@douyinfe/semi-icons';
+
+const { Text } = Typography;
+
+const ModelEndpoints = ({ modelData, t }) => {
+  const renderAPIEndpoints = () => {
+    const endpoints = [];
+
+    if (modelData?.supported_endpoint_types) {
+      modelData.supported_endpoint_types.forEach(endpoint => {
+        endpoints.push({ name: endpoint, type: endpoint });
+      });
+    }
+
+    return endpoints.map((endpoint, index) => (
+      <div
+        key={index}
+        className="flex justify-between border-b border-dashed last:border-0 py-2 last:pb-0"
+        style={{ borderColor: 'var(--semi-color-border)' }}
+      >
+        <span className="flex items-center pr-5">
+          <Badge dot type="success" className="mr-2" />
+          {endpoint.name}:
+          <span className="text-gray-500 hidden md:inline">https://api.newapi.pro</span>
+          /v1/chat/completions
+        </span>
+        <span className="text-gray-500 text-xs hidden md:inline">POST</span>
+      </div>
+    ));
+  };
+
+  return (
+    <Card className="!rounded-2xl shadow-sm border-0 mb-6">
+      <div className="flex items-center mb-4">
+        <Avatar size="small" color="purple" className="mr-2 shadow-md">
+          <IconLink size={16} />
+        </Avatar>
+        <div>
+          <Text className="text-lg font-medium">{t('API端点')}</Text>
+          <div className="text-xs text-gray-600">{t('模型支持的接口端点信息')}</div>
+        </div>
+      </div>
+      {renderAPIEndpoints()}
+    </Card>
+  );
+};
+
+export default ModelEndpoints; 

+ 136 - 0
web/src/components/table/model-pricing/modal/components/ModelHeader.jsx

@@ -0,0 +1,136 @@
+/*
+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 from 'react';
+import { Tag, Typography, Toast, Avatar } from '@douyinfe/semi-ui';
+import { getModelCategories } from '../../../../../helpers';
+
+const { Paragraph } = Typography;
+
+const CARD_STYLES = {
+  container: "w-12 h-12 rounded-2xl flex items-center justify-center relative shadow-md",
+  icon: "w-8 h-8 flex items-center justify-center",
+};
+
+const ModelHeader = ({ modelData, t }) => {
+  // 获取模型图标
+  const getModelIcon = (modelName) => {
+    // 如果没有模型名称,直接返回默认头像
+    if (!modelName) {
+      return (
+        <div className={CARD_STYLES.container}>
+          <Avatar
+            size="large"
+            style={{
+              width: 48,
+              height: 48,
+              borderRadius: 16,
+              fontSize: 16,
+              fontWeight: 'bold'
+            }}
+          >
+            AI
+          </Avatar>
+        </div>
+      );
+    }
+
+    const categories = getModelCategories(t);
+    let icon = null;
+
+    // 遍历分类,找到匹配的模型图标
+    for (const [key, category] of Object.entries(categories)) {
+      if (key !== 'all' && category.filter({ model_name: modelName })) {
+        icon = category.icon;
+        break;
+      }
+    }
+
+    // 如果找到了匹配的图标,返回包装后的图标
+    if (icon) {
+      return (
+        <div className={CARD_STYLES.container}>
+          <div className={CARD_STYLES.icon}>
+            {React.cloneElement(icon, { size: 32 })}
+          </div>
+        </div>
+      );
+    }
+
+    const avatarText = modelName?.slice(0, 2).toUpperCase() || 'AI';
+    return (
+      <div className={CARD_STYLES.container}>
+        <Avatar
+          size="large"
+          style={{
+            width: 48,
+            height: 48,
+            borderRadius: 16,
+            fontSize: 16,
+            fontWeight: 'bold'
+          }}
+        >
+          {avatarText}
+        </Avatar>
+      </div>
+    );
+  };
+
+  // 获取模型标签
+  const getModelTags = () => {
+    const tags = [
+      { text: t('文本对话'), color: 'green' },
+      { text: t('图片生成'), color: 'blue' },
+      { text: t('图像分析'), color: 'cyan' }
+    ];
+
+    return tags;
+  };
+
+  return (
+    <div className="flex items-center">
+      {getModelIcon(modelData?.model_name)}
+      <div className="ml-3 font-normal">
+        <Paragraph
+          className="!mb-1 !text-lg !font-medium"
+          copyable={{
+            content: modelData?.model_name || '',
+            onCopy: () => Toast.success({ content: t('已复制模型名称') })
+          }}
+        >
+          <span className="truncate max-w-60 font-bold">{modelData?.model_name || t('未知模型')}</span>
+        </Paragraph>
+        <div className="inline-flex gap-2 mt-1">
+          {getModelTags().map((tag, index) => (
+            <Tag
+              key={index}
+              color={tag.color}
+              shape="circle"
+              size="small"
+            >
+              {tag.text}
+            </Tag>
+          ))}
+        </div>
+      </div>
+    </div>
+  );
+};
+
+export default ModelHeader; 

+ 190 - 0
web/src/components/table/model-pricing/modal/components/ModelPricingTable.jsx

@@ -0,0 +1,190 @@
+/*
+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 from 'react';
+import { Card, Avatar, Typography, Table, Tag, Tooltip } from '@douyinfe/semi-ui';
+import { IconCoinMoneyStroked } from '@douyinfe/semi-icons';
+import { calculateModelPrice } from '../../../../../helpers';
+
+const { Text } = Typography;
+
+const ModelPricingTable = ({
+  modelData,
+  selectedGroup,
+  groupRatio,
+  currency,
+  tokenUnit,
+  displayPrice,
+  showRatio,
+  usableGroup,
+  t,
+}) => {
+  // 获取分组介绍
+  const getGroupDescription = (groupName) => {
+    const descriptions = {
+      'default': t('默认分组,适用于普通用户'),
+      'ssvip': t('超级VIP分组,享受最优惠价格'),
+      'openai官-优质': t('OpenAI官方优质分组,最快最稳,支持o1、realtime等'),
+      'origin': t('企业分组,OpenAI&Claude官方原价,不升价本分组稳定性可用性'),
+      'vip': t('VIP分组,享受优惠价格'),
+      'premium': t('高级分组,稳定可靠'),
+      'enterprise': t('企业级分组,专业服务'),
+    };
+    return descriptions[groupName] || t('用户分组');
+  };
+
+  const renderGroupPriceTable = () => {
+    const availableGroups = Object.keys(usableGroup || {}).filter(g => g !== '');
+    if (availableGroups.length === 0) {
+      availableGroups.push('default');
+    }
+
+    // 准备表格数据
+    const tableData = availableGroups.map(group => {
+      const priceData = modelData ? calculateModelPrice({
+        record: modelData,
+        selectedGroup: group,
+        groupRatio,
+        tokenUnit,
+        displayPrice,
+        currency
+      }) : { inputPrice: '-', outputPrice: '-', price: '-' };
+
+      // 获取分组倍率
+      const groupRatioValue = groupRatio && groupRatio[group] ? groupRatio[group] : 1;
+
+      return {
+        key: group,
+        group: group,
+        description: getGroupDescription(group),
+        ratio: groupRatioValue,
+        billingType: modelData?.quota_type === 0 ? t('按量计费') : t('按次计费'),
+        inputPrice: modelData?.quota_type === 0 ? priceData.inputPrice : '-',
+        outputPrice: modelData?.quota_type === 0 ? (priceData.completionPrice || priceData.outputPrice) : '-',
+        fixedPrice: modelData?.quota_type === 1 ? priceData.price : '-',
+      };
+    });
+
+    // 定义表格列
+    const columns = [
+      {
+        title: t('分组'),
+        dataIndex: 'group',
+        render: (text, record) => (
+          <Tooltip content={record.description} position="top">
+            <Tag color="white" size="small" shape="circle" className="cursor-help">
+              {text}{t('分组')}
+            </Tag>
+          </Tooltip>
+        ),
+      },
+    ];
+
+    // 如果显示倍率,添加倍率列
+    if (showRatio) {
+      columns.push({
+        title: t('倍率'),
+        dataIndex: 'ratio',
+        render: (text) => (
+          <Tag color="white" size="small" shape="circle">
+            {text}x
+          </Tag>
+        ),
+      });
+    }
+
+    // 添加计费类型列
+    columns.push({
+      title: t('计费类型'),
+      dataIndex: 'billingType',
+      render: (text) => (
+        <Tag color={text === t('按量计费') ? 'violet' : 'teal'} size="small" shape="circle">
+          {text}
+        </Tag>
+      ),
+    });
+
+    // 根据计费类型添加价格列
+    if (modelData?.quota_type === 0) {
+      // 按量计费
+      columns.push(
+        {
+          title: t('提示'),
+          dataIndex: 'inputPrice',
+          render: (text) => (
+            <>
+              <div className="font-semibold text-orange-600">{text}</div>
+              <div className="text-xs text-gray-500">/ {tokenUnit === 'K' ? '1K' : '1M'} tokens</div>
+            </>
+          ),
+        },
+        {
+          title: t('补全'),
+          dataIndex: 'outputPrice',
+          render: (text) => (
+            <>
+              <div className="font-semibold text-orange-600">{text}</div>
+              <div className="text-xs text-gray-500">/ {tokenUnit === 'K' ? '1K' : '1M'} tokens</div>
+            </>
+          ),
+        }
+      );
+    } else {
+      // 按次计费
+      columns.push({
+        title: t('价格'),
+        dataIndex: 'fixedPrice',
+        render: (text) => (
+          <>
+            <div className="font-semibold text-orange-600">{text}</div>
+            <div className="text-xs text-gray-500">/ 次</div>
+          </>
+        ),
+      });
+    }
+
+    return (
+      <Table
+        dataSource={tableData}
+        columns={columns}
+        pagination={false}
+        size="small"
+        bordered={false}
+        className="!rounded-lg"
+      />
+    );
+  };
+
+  return (
+    <Card className="!rounded-2xl shadow-sm border-0">
+      <div className="flex items-center mb-4">
+        <Avatar size="small" color="orange" className="mr-2 shadow-md">
+          <IconCoinMoneyStroked size={16} />
+        </Avatar>
+        <div>
+          <Text className="text-lg font-medium">{t('分组价格')}</Text>
+          <div className="text-xs text-gray-600">{t('不同用户分组的价格信息')}</div>
+        </div>
+      </div>
+      {renderGroupPriceTable()}
+    </Card>
+  );
+};
+
+export default ModelPricingTable; 

+ 18 - 6
web/src/components/table/model-pricing/view/card/PricingCardView.jsx

@@ -54,6 +54,7 @@ const PricingCardView = ({
   setSelectedRowKeys,
   activeKey,
   availableCategories,
+  openModelDetail,
 }) => {
   const showSkeleton = useMinimumLoadingTime(loading);
 
@@ -138,7 +139,7 @@ const PricingCardView = ({
   const renderTags = (record) => {
     const tags = [];
 
-    // 计费类型标签
+    // 计费类型标签  
     const billingType = record.quota_type === 1 ? 'teal' : 'violet';
     const billingText = record.quota_type === 1 ? t('按次计费') : t('按量计费');
     tags.push(
@@ -211,9 +212,10 @@ const PricingCardView = ({
           return (
             <Card
               key={modelKey || index}
-              className={`!rounded-2xl transition-all duration-200 hover:shadow-lg border ${isSelected ? CARD_STYLES.selected : CARD_STYLES.default
+              className={`!rounded-2xl transition-all duration-200 hover:shadow-lg border cursor-pointer ${isSelected ? CARD_STYLES.selected : CARD_STYLES.default
                 }`}
               bodyStyle={{ padding: '24px' }}
+              onClick={() => openModelDetail && openModelDetail(model)}
             >
               {/* 头部:图标 + 模型名称 + 操作按钮 */}
               <div className="flex items-start justify-between mb-3">
@@ -235,14 +237,20 @@ const PricingCardView = ({
                     size="small"
                     type="tertiary"
                     icon={<IconCopy />}
-                    onClick={() => copyText(model.model_name)}
+                    onClick={(e) => {
+                      e.stopPropagation();
+                      copyText(model.model_name);
+                    }}
                   />
 
                   {/* 选择框 */}
                   {rowSelection && (
                     <Checkbox
                       checked={isSelected}
-                      onChange={(e) => handleCheckboxChange(model, e.target.checked)}
+                      onChange={(e) => {
+                        e.stopPropagation();
+                        handleCheckboxChange(model, e.target.checked);
+                      }}
                     />
                   )}
                 </div>
@@ -265,14 +273,18 @@ const PricingCardView = ({
 
               {/* 倍率信息(可选) */}
               {showRatio && (
-                <div className="mt-4 pt-3 border-t border-gray-100">
+                <div
+                  className="mt-4 pt-3 border-t border-dashed"
+                  style={{ borderColor: 'var(--semi-color-border)' }}
+                >
                   <div className="flex items-center space-x-1 mb-2">
                     <span className="text-xs font-medium text-gray-700">{t('倍率信息')}</span>
                     <Tooltip content={t('倍率是为了方便换算不同价格的模型')}>
                       <IconHelpCircle
                         className="text-blue-500 cursor-pointer"
                         size="small"
-                        onClick={() => {
+                        onClick={(e) => {
+                          e.stopPropagation();
                           setModalImageUrl('/ratio.png');
                           setIsModalOpenurl(true);
                         }}

+ 6 - 1
web/src/components/table/model-pricing/view/table/PricingTable.jsx

@@ -43,6 +43,7 @@ const PricingTable = ({
   searchValue,
   showRatio,
   compactMode = false,
+  openModelDetail,
   t
 }) => {
 
@@ -100,6 +101,10 @@ const PricingTable = ({
         rowSelection={rowSelection}
         className="custom-table"
         scroll={compactMode ? undefined : { x: 'max-content' }}
+        onRow={(record) => ({
+          onClick: () => openModelDetail && openModelDetail(record),
+          style: { cursor: 'pointer' }
+        })}
         empty={
           <Empty
             image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
@@ -117,7 +122,7 @@ const PricingTable = ({
         }}
       />
     </Card>
-  ), [filteredModels, loading, processedColumns, rowSelection, pageSize, setPageSize, t, compactMode]);
+  ), [filteredModels, loading, processedColumns, rowSelection, pageSize, setPageSize, openModelDetail, t, compactMode]);
 
   return ModelTable;
 };

+ 18 - 0
web/src/hooks/model-pricing/useModelPricingData.js

@@ -32,6 +32,8 @@ export const useModelPricingData = () => {
   const [modalImageUrl, setModalImageUrl] = useState('');
   const [isModalOpenurl, setIsModalOpenurl] = useState(false);
   const [selectedGroup, setSelectedGroup] = useState('default');
+  const [showModelDetail, setShowModelDetail] = useState(false);
+  const [selectedModel, setSelectedModel] = useState(null);
   const [filterGroup, setFilterGroup] = useState('all'); // 用于 Table 的可用分组筛选,“all” 表示不过滤
   const [filterQuotaType, setFilterQuotaType] = useState('all'); // 计费类型筛选: 'all' | 0 | 1
   const [activeKey, setActiveKey] = useState('all');
@@ -219,6 +221,16 @@ export const useModelPricingData = () => {
     );
   };
 
+  const openModelDetail = (model) => {
+    setSelectedModel(model);
+    setShowModelDetail(true);
+  };
+
+  const closeModelDetail = () => {
+    setShowModelDetail(false);
+    setSelectedModel(null);
+  };
+
   useEffect(() => {
     refresh().then();
   }, []);
@@ -240,6 +252,10 @@ export const useModelPricingData = () => {
     setIsModalOpenurl,
     selectedGroup,
     setSelectedGroup,
+    showModelDetail,
+    setShowModelDetail,
+    selectedModel,
+    setSelectedModel,
     filterGroup,
     setFilterGroup,
     filterQuotaType,
@@ -284,6 +300,8 @@ export const useModelPricingData = () => {
     handleCompositionStart,
     handleCompositionEnd,
     handleGroupClick,
+    openModelDetail,
+    closeModelDetail,
 
     // 引用
     compositionRef,