🚀 feat: Introduce full Model & Vendor Management suite (backend + frontend) and UI refinements

Backend
• Add `model/model_meta.go` and `model/vendor_meta.go` defining Model & Vendor entities with CRUD helpers, soft-delete and time stamps
• Create corresponding controllers `controller/model_meta.go`, `controller/vendor_meta.go` and register routes in `router/api-router.go`
• Auto-migrate new tables in DB startup logic

Frontend
• Build complete “Model Management” module under `/console/models`
  - New pages, tables, filters, actions, hooks (`useModelsData`) and dynamic vendor tabs
  - Modals `EditModelModal.jsx` & unified `EditVendorModal.jsx`; latter now uses default confirm/cancel footer and mobile-friendly modal sizing (`full-width` / `small`) via `useIsMobile`
• Update sidebar (`SiderBar.js`) and routing (`App.js`) to surface the feature
• Add helper updates (`render.js`) incl. `stringToColor`, dynamic LobeHub icon retrieval, and tag color palettes

Table UX improvements
• Replace separate status column with inline Enable / Disable buttons in operation column (matching channel table style)
• Limit visible tags to max 3; overflow represented as “+x” tag with padded `Popover` showing remaining tags
• Color all tags deterministically using `stringToColor` for consistent theming
• Change vendor column tag color to white for better contrast

Misc
• Minor layout tweaks, compact-mode toggle relocation, lint fixes and TypeScript/ESLint clean-up

These changes collectively deliver end-to-end model & vendor administration while unifying visual language across management tables.
This commit is contained in:
t0ng7u
2025-07-31 22:28:09 +08:00
parent 82bf149ade
commit af59b61f8a
21 changed files with 2392 additions and 3 deletions

View File

@@ -18,10 +18,11 @@ For commercial licensing, please contact support@quantumnous.com
*/
import i18next from 'i18next';
import { Modal, Tag, Typography } from '@douyinfe/semi-ui';
import { Modal, Tag, Typography, Avatar } from '@douyinfe/semi-ui';
import { copy, showSuccess } from './utils';
import { MOBILE_BREAKPOINT } from '../hooks/common/useIsMobile.js';
import { visit } from 'unist-util-visit';
import * as LobeIcons from '@lobehub/icons';
import {
OpenAI,
Claude,
@@ -85,6 +86,7 @@ export const sidebarIconColors = {
gift: '#F43F5E', // 玫红色
user: '#10B981', // 绿色
settings: '#F97316', // 橙色
models: '#10B981', // 绿色
};
// 获取侧边栏Lucide图标组件
@@ -177,6 +179,13 @@ export function getLucideIcon(key, selected = false) {
color={selected ? sidebarIconColors.user : 'currentColor'}
/>
);
case 'models':
return (
<Layers
{...commonProps}
color={selected ? sidebarIconColors.models : 'currentColor'}
/>
);
case 'setting':
return (
<Settings
@@ -422,6 +431,37 @@ export function getChannelIcon(channelType) {
}
}
/**
* 根据图标名称动态获取 LobeHub 图标组件
* @param {string} iconName - 图标名称
* @param {number} size - 图标大小,默认为 14
* @returns {JSX.Element} - 对应的图标组件或 Avatar
*/
export function getLobeHubIcon(iconName, size = 14) {
if (typeof iconName === 'string') iconName = iconName.trim();
// 如果没有图标名称,返回 Avatar
if (!iconName) {
return <Avatar size="extra-extra-small">?</Avatar>;
}
let IconComponent;
if (iconName.includes('.')) {
const [base, variant] = iconName.split('.');
const BaseIcon = LobeIcons[base];
IconComponent = BaseIcon ? BaseIcon[variant] : undefined;
} else {
IconComponent = LobeIcons[iconName];
}
if (IconComponent && (typeof IconComponent === 'function' || typeof IconComponent === 'object')) {
return <IconComponent size={size} />;
}
const firstLetter = iconName.charAt(0).toUpperCase();
return <Avatar size="extra-extra-small">{firstLetter}</Avatar>;
}
// 颜色列表
const colors = [
'amber',
@@ -891,13 +931,13 @@ export function renderQuota(quota, digits = 2) {
if (displayInCurrency) {
const result = quota / quotaPerUnit;
const fixedResult = result.toFixed(digits);
// 如果 toFixed 后结果为 0 但原始值不为 0显示最小值
if (parseFloat(fixedResult) === 0 && quota > 0 && result > 0) {
const minValue = Math.pow(10, -digits);
return '$' + minValue.toFixed(digits);
}
return '$' + fixedResult;
}
return renderNumber(quota);