Explorar el Código

fix:页面一些样式修改,补充部分国际化

韩洋 hace 1 mes
padre
commit
494571137f

+ 6 - 3
default/src/components/data-table/toolbar/view-options.tsx

@@ -67,6 +67,11 @@ export function DataTableViewOptions<TData>({
         <DropdownMenuGroup>
           <DropdownMenuLabel>{t('Toggle columns')}</DropdownMenuLabel>
           {hideableColumns.map((column) => {
+            const columnLabel =
+              typeof column.columnDef.header === 'string'
+                ? column.columnDef.header
+                : (column.columnDef.meta?.label ?? column.id)
+
             return (
               <DropdownMenuCheckboxItem
                 key={column.id}
@@ -74,9 +79,7 @@ export function DataTableViewOptions<TData>({
                 checked={column.getIsVisible()}
                 onCheckedChange={(value) => column.toggleVisibility(!!value)}
               >
-                {typeof column.columnDef.header === 'string'
-                  ? column.columnDef.header
-                  : (column.columnDef.meta?.label ?? column.id)}
+                {t(columnLabel)}
               </DropdownMenuCheckboxItem>
             )
           })}

+ 3 - 2
default/src/components/layout/components/mobile-drawer.tsx

@@ -81,6 +81,7 @@ function MobileUserProfile({ user, onNavigate }: MobileUserProfileProps) {
   const { t } = useTranslation()
   const [signOutOpen, setSignOutOpen] = useDialogState()
   const { displayName, initials, roleLabel } = useUserDisplay(user)
+  const visibleGroup = user?.group && user.group !== 'default' ? user.group : ''
 
   if (!user) return null
 
@@ -100,11 +101,11 @@ function MobileUserProfile({ user, onNavigate }: MobileUserProfileProps) {
             </p>
             <div className='flex items-center gap-1.5'>
               <span className='text-muted-foreground text-xs'>{roleLabel}</span>
-              {user.group && (
+              {visibleGroup && (
                 <>
                   <span className='text-muted-foreground text-xs'>·</span>
                   <span className='text-muted-foreground text-xs'>
-                    {String(user.group)}
+                    {String(visibleGroup)}
                   </span>
                 </>
               )}

+ 3 - 2
default/src/components/profile-dropdown.tsx

@@ -54,6 +54,7 @@ export function ProfileDropdown() {
     () => getUserAvatarStyle(avatarName),
     [avatarName]
   )
+  const visibleGroup = user?.group && user.group !== 'default' ? user.group : ''
 
   return (
     <>
@@ -88,11 +89,11 @@ export function ProfileDropdown() {
                 <span className='text-muted-foreground text-xs'>
                   {roleLabel}
                 </span>
-                {user?.group && (
+                {visibleGroup && (
                   <>
                     <span className='text-muted-foreground text-xs'>·</span>
                     <span className='text-muted-foreground truncate text-xs'>
-                      {String(user.group)}
+                      {String(visibleGroup)}
                     </span>
                   </>
                 )}

+ 5 - 1
default/src/features/groups/components/groups-columns.tsx

@@ -46,6 +46,7 @@ export function useGroupsColumns(): ColumnDef<Group>[] {
     {
       accessorKey: 'group_name',
       header: t('Group Name'),
+      meta: { label: 'Group Name' },
       cell: ({ row }) => {
         const group = row.original
         return (
@@ -74,6 +75,7 @@ export function useGroupsColumns(): ColumnDef<Group>[] {
     {
       accessorKey: 'magnification',
       header: t('Magnification'),
+      meta: { label: 'Magnification' },
       cell: ({ row }) => {
         const magnification = row.getValue('magnification') as number
         return (
@@ -89,6 +91,7 @@ export function useGroupsColumns(): ColumnDef<Group>[] {
     {
       accessorKey: 'models',
       header: t('Models'),
+      meta: { label: 'Models' },
       cell: ({ row }) => {
         const models = row.original.models || []
         const visibleCount = Math.min(models.length, 2)
@@ -172,6 +175,7 @@ export function useGroupsColumns(): ColumnDef<Group>[] {
     {
       accessorKey: 'created_at',
       header: t('Created At'),
+      meta: { label: 'Created At' },
       cell: ({ row }) => {
         const createdAt = row.getValue('created_at') as string | undefined
         return (
@@ -185,6 +189,7 @@ export function useGroupsColumns(): ColumnDef<Group>[] {
     {
       accessorKey: 'updated_at',
       header: t('Updated At'),
+      meta: { label: 'Updated At', mobileHidden: true },
       cell: ({ row }) => {
         const updatedAt = row.getValue('updated_at') as string | undefined
         return (
@@ -194,7 +199,6 @@ export function useGroupsColumns(): ColumnDef<Group>[] {
         )
       },
       size: 200,
-      meta: { mobileHidden: true },
     },
     {
       id: 'actions',

+ 9 - 1
default/src/features/groups/components/groups-primary-buttons.tsx

@@ -18,15 +18,22 @@ For commercial licensing, please contact support@quantumnous.com
 */
 
 import { Plus } from 'lucide-react'
+import type { Table } from '@tanstack/react-table'
 import { useTranslation } from 'react-i18next'
 
+import { DataTableViewOptions } from '@/components/data-table'
 import { Button } from '@/components/ui/button'
 import { PERMISSION_CODES, hasPermissionCode } from '@/lib/admin-permissions'
 import { useAuthStore } from '@/stores/auth-store'
 
+import type { Group } from '../types'
 import { useGroups } from './groups-provider'
 
-export function GroupsPrimaryButtons() {
+type GroupsPrimaryButtonsProps = {
+  table: Table<Group>
+}
+
+export function GroupsPrimaryButtons(props: GroupsPrimaryButtonsProps) {
   const { t } = useTranslation()
   const { setOpen, setCurrentRow } = useGroups()
   const currentUser = useAuthStore((s) => s.auth.user)
@@ -42,6 +49,7 @@ export function GroupsPrimaryButtons() {
 
   return (
     <div className='flex gap-2'>
+      <DataTableViewOptions table={props.table} />
       {canCreateGroup && (
         <Button size='sm' onClick={handleCreate}>
           <Plus className='h-4 w-4' />

+ 18 - 1
default/src/features/pricing/hooks/use-pricing-data.ts

@@ -19,8 +19,10 @@ For commercial licensing, please contact support@quantumnous.com
 import { useQuery } from '@tanstack/react-query'
 import { useMemo } from 'react'
 
+import { getGroups } from '@/features/groups/api'
 import { useStatus } from '@/hooks/use-status'
 
+import { EXCLUDED_GROUPS } from '../constants'
 import { getPricing } from '../api'
 
 export function usePricingData() {
@@ -32,6 +34,12 @@ export function usePricingData() {
     staleTime: 5 * 60 * 1000,
   })
 
+  const { data: groupsData, isLoading: isGroupsLoading } = useQuery({
+    queryKey: ['groups', 'pricing-filter'],
+    queryFn: () => getGroups({ p: 1, page_size: 10000 }),
+    staleTime: 5 * 60 * 1000,
+  })
+
   // Ensure rates never reach zero to prevent division errors
   const priceRate = useMemo(
     () => Math.max((status?.price as number) ?? 1, 0.001),
@@ -62,14 +70,23 @@ export function usePricingData() {
     })
   }, [data])
 
+  const availableGroups = useMemo(
+    () =>
+      (groupsData?.data?.items ?? [])
+        .map((group) => group.group_name)
+        .filter((group) => !EXCLUDED_GROUPS.includes(group)),
+    [groupsData]
+  )
+
   return {
     models,
     vendors: data?.vendors ?? [],
+    availableGroups,
     groupRatio: data?.group_ratio ?? {},
     usableGroup: data?.usable_group ?? {},
     endpointMap: data?.supported_endpoint ?? {},
     autoGroups: data?.auto_groups ?? [],
-    isLoading,
+    isLoading: isLoading || isGroupsLoading,
     error,
     refetch,
     priceRate,

+ 2 - 9
default/src/features/pricing/index.tsx

@@ -32,7 +32,7 @@ import {
   ModelCardGrid,
   ModelDetailsDrawer,
 } from './components'
-import { EXCLUDED_GROUPS, VIEW_MODES } from './constants'
+import { VIEW_MODES } from './constants'
 import { useFilters } from './hooks/use-filters'
 import { usePricingData } from './hooks/use-pricing-data'
 
@@ -51,6 +51,7 @@ export function PricingContent({
   const {
     models,
     vendors,
+    availableGroups,
     groupRatio,
     usableGroup,
     endpointMap,
@@ -99,14 +100,6 @@ export function PricingContent({
     [models, selectedModelName]
   )
 
-  const availableGroups = useMemo(
-    () =>
-      Object.keys(usableGroup || {}).filter(
-        (g) => !EXCLUDED_GROUPS.includes(g)
-      ),
-    [usableGroup]
-  )
-
   const handleClearAll = useCallback(() => {
     clearFilters()
     clearSearch()

+ 3 - 2
default/src/features/profile/components/profile-header.tsx

@@ -84,6 +84,7 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
   const avatarFallback = getUserAvatarFallback(avatarName)
   const avatarFallbackStyle = getUserAvatarStyle(avatarName)
   const roleLabel = getRoleLabel(profile.role)
+  const visibleGroup = profile.group && profile.group !== 'default' ? profile.group : ''
   const stats: {
     label: string
     value: string
@@ -151,10 +152,10 @@ export function ProfileHeader({ profile, loading }: ProfileHeaderProps) {
                   <span className='truncate'>{profile.email}</span>
                 </>
               )}
-              {profile.group && (
+              {visibleGroup && (
                 <>
                   <span>•</span>
-                  <span className='truncate'>{profile.group}</span>
+                  <span className='truncate'>{visibleGroup}</span>
                 </>
               )}
             </div>

+ 10 - 1
default/src/features/roles/index.tsx

@@ -26,7 +26,11 @@ import { useTranslation } from 'react-i18next'
 import { toast } from 'sonner'
 import { z } from 'zod'
 
-import { DataTablePage, useDataTable } from '@/components/data-table'
+import {
+  DataTablePage,
+  DataTableViewOptions,
+  useDataTable,
+} from '@/components/data-table'
 import {
   SideDrawerSection,
   sideDrawerContentClassName,
@@ -681,6 +685,7 @@ export function RolesSettings() {
     {
       accessorKey: 'name',
       header: t('Role Name'),
+      meta: { label: 'Role Name' },
       cell: ({ row }) => {
         const name = row.getValue('name') as string
         return <span className='font-medium'>{name}</span>
@@ -691,6 +696,7 @@ export function RolesSettings() {
     {
       accessorKey: 'description',
       header: t('Role Description'),
+      meta: { label: 'Role Description' },
       cell: ({ row }) => {
         const description = row.getValue('description') as string
         return (
@@ -705,6 +711,7 @@ export function RolesSettings() {
       id: 'permission_modules_count',
       accessorKey: 'permission_modules_count',
       header: t('Permission Modules'),
+      meta: { label: 'Permission Modules' },
       cell: ({ row }) => {
         const count = row.getValue('permission_modules_count') as number
         return (
@@ -719,6 +726,7 @@ export function RolesSettings() {
       id: 'permission_actions_count',
       accessorKey: 'permission_actions_count',
       header: t('Permission Actions'),
+      meta: { label: 'Permission Actions' },
       cell: ({ row }) => {
         const count = row.getValue('permission_actions_count') as number
         return (
@@ -1328,6 +1336,7 @@ export function RolesSettings() {
               className='w-64'
             />
           </div>
+          <DataTableViewOptions table={table} />
           {canCreateRole && (
             <Sheet open={createOpen} onOpenChange={handleCreateOpenChange}>
               <SheetTrigger render={<Button />}>

+ 5 - 1
default/src/features/system-settings/billing/group-management-section.tsx

@@ -15,6 +15,9 @@ import { GroupsPrimaryButtons } from '@/features/groups/components/groups-primar
 import { useGroupsColumns } from '@/features/groups/components/groups-columns'
 import { GroupsProvider, useGroups } from '@/features/groups/components/groups-provider'
 
+const BILLING_GROUPS_COLUMN_VISIBILITY_STORAGE_KEY =
+  'system-settings:billing:groups:column-visibility'
+
 function BillingGroupManagementContent() {
   const { t } = useTranslation()
   const columns = useGroupsColumns()
@@ -59,6 +62,7 @@ function BillingGroupManagementContent() {
   const { table } = useDataTable({
     data: data?.items || [],
     columns,
+    columnVisibilityStorageKey: BILLING_GROUPS_COLUMN_VISIBILITY_STORAGE_KEY,
     totalCount,
     pagination,
     getRowId: (row) => row.id,
@@ -70,7 +74,7 @@ function BillingGroupManagementContent() {
   return (
     <>
       <SettingsPageActionsPortal>
-        <GroupsPrimaryButtons />
+        <GroupsPrimaryButtons table={table} />
       </SettingsPageActionsPortal>
 
       <DataTablePage

+ 58 - 35
default/src/features/system-settings/general/system-info-section.tsx

@@ -17,7 +17,10 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
 For commercial licensing, please contact support@quantumnous.com
 */
 import { zodResolver } from '@hookform/resolvers/zod'
+import { ImageUpload01Icon } from '@hugeicons/core-free-icons'
+import { HugeiconsIcon } from '@hugeicons/react'
 import type { Resolver } from 'react-hook-form'
+import { useState } from 'react'
 import { useTranslation } from 'react-i18next'
 import * as z from 'zod'
 
@@ -31,6 +34,9 @@ import {
   FormMessage,
 } from '@/components/ui/form'
 import { Input } from '@/components/ui/input'
+import { Spinner } from '@/components/ui/spinner'
+import { DEFAULT_LOGO } from '@/lib/constants'
+import { cn } from '@/lib/utils'
 
 import { FormDirtyIndicator } from '../components/form-dirty-indicator'
 import { FormNavigationGuard } from '../components/form-navigation-guard'
@@ -165,7 +171,9 @@ export function SystemInfoSection({ defaultValues }: SystemInfoSectionProps) {
       },
     })
 
-  const { handleAppIconUpload } = useIconUpload()
+  const { handleAppIconUpload, isUploading } = useIconUpload()
+  const [isDragActive, setIsDragActive] = useState(false)
+  const logoUrl = normalizedDefaults.Logo || DEFAULT_LOGO
 
   return (
     <>
@@ -267,57 +275,72 @@ export function SystemInfoSection({ defaultValues }: SystemInfoSectionProps) {
                 )}
               />
 
-              <div className='grid grid-cols-1 gap-6 md:grid-cols-2'>
-                <div className='space-y-2'>
-                  <label className='text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70'>
-                    {t('App Icon')}
-                  </label>
+              <div className='space-y-2'>
+                <label className='text-sm leading-none font-medium peer-disabled:cursor-not-allowed peer-disabled:opacity-70'>
+                  {t('App Icon')}
+                </label>
+                <div className='flex items-stretch gap-4'>
+                  <div className='relative size-20 shrink-0 overflow-hidden rounded-2xl border border-border/80 bg-muted/30 shadow-sm sm:size-24'>
+                    <img
+                      src={logoUrl}
+                      alt={t('App Icon')}
+                      className='size-full object-cover'
+                    />
+                    {isUploading && (
+                      <div className='absolute inset-0 flex items-center justify-center bg-background/70 backdrop-blur-sm'>
+                        <Spinner className='size-5 text-primary' />
+                      </div>
+                    )}
+                  </div>
                   <div
-                    className='cursor-pointer rounded-lg border-2 border-dashed border-gray-300 p-6 text-center transition-colors hover:border-gray-400'
+                    role='button'
+                    tabIndex={0}
+                    className={cn(
+                      'group relative flex flex-1 cursor-pointer flex-col items-center justify-center gap-2 rounded-2xl border-2 border-dashed border-border/80 bg-muted/20 px-4 py-5 text-center transition-all hover:border-primary/40 hover:bg-primary/5 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/45 focus-visible:outline-none',
+                      isDragActive && 'border-primary bg-primary/5',
+                      isUploading && 'pointer-events-none opacity-60'
+                    )}
                     onClick={() =>
+                      !isUploading &&
                       document.getElementById('app-icon-input')?.click()
                     }
+                    onKeyDown={(e) => {
+                      if (
+                        !isUploading &&
+                        (e.key === 'Enter' || e.key === ' ')
+                      ) {
+                        e.preventDefault()
+                        document.getElementById('app-icon-input')?.click()
+                      }
+                    }}
                     onDragOver={(e) => {
                       e.preventDefault()
-                      e.currentTarget.classList.add(
-                        'border-blue-400',
-                        'bg-blue-50'
-                      )
+                      setIsDragActive(true)
                     }}
                     onDragLeave={(e) => {
-                      e.currentTarget.classList.remove(
-                        'border-blue-400',
-                        'bg-blue-50'
-                      )
+                      e.preventDefault()
+                      setIsDragActive(false)
                     }}
                     onDrop={(e) => {
                       e.preventDefault()
-                      e.currentTarget.classList.remove(
-                        'border-blue-400',
-                        'bg-blue-50'
-                      )
+                      setIsDragActive(false)
                       const file = e.dataTransfer.files?.[0]
                       if (file && file.type === 'image/png') {
                         handleAppIconUpload(file)
                       }
                     }}
                   >
-                    <div className='space-y-2'>
-                      <svg
-                        className='mx-auto h-10 w-10 text-gray-400'
-                        fill='none'
-                        stroke='currentColor'
-                        viewBox='0 0 24 24'
-                      >
-                        <path
-                          strokeLinecap='round'
-                          strokeLinejoin='round'
-                          strokeWidth={2}
-                          d='M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z'
-                        />
-                      </svg>
-                      <p className='text-sm text-gray-600'>{t('Click or drag to upload')}</p>
-                      <p className='text-xs text-gray-400'>{t('PNG only')}</p>
+                    <HugeiconsIcon
+                      icon={ImageUpload01Icon}
+                      className='size-7 text-muted-foreground/70 transition-colors group-hover:text-primary'
+                    />
+                    <div className='space-y-0.5'>
+                      <p className='text-sm font-medium text-foreground'>
+                        {t('Click or drag to upload')}
+                      </p>
+                      <p className='text-xs text-muted-foreground'>
+                        {t('PNG only')}
+                      </p>
                     </div>
                     <input
                       id='app-icon-input'

+ 16 - 1
default/src/features/tenants/index.tsx

@@ -36,7 +36,12 @@ import { useTranslation } from 'react-i18next'
 import { toast } from 'sonner'
 import { z } from 'zod'
 
-import { BadgeCell, DataTablePage, useDataTable } from '@/components/data-table'
+import {
+  BadgeCell,
+  DataTablePage,
+  DataTableViewOptions,
+  useDataTable,
+} from '@/components/data-table'
 import { DatePicker } from '@/components/date-picker'
 import {
   SideDrawerSection,
@@ -114,6 +119,8 @@ const tenantStatusConfig: Record<
   disabled: { labelKey: 'Disabled', variant: 'neutral' },
 }
 
+const TENANTS_COLUMN_VISIBILITY_STORAGE_KEY = 'tenants:column-visibility'
+
 const tenantPermissionModules = [
   {
     key: 'channels',
@@ -977,6 +984,7 @@ export function Tenants() {
     {
       accessorKey: 'name',
       header: t('Tenant Name'),
+      meta: { label: 'Tenant Name' },
       cell: ({ row }) => {
         const tenant = row.original
         return (
@@ -1001,6 +1009,7 @@ export function Tenants() {
     {
       accessorKey: 'status',
       header: t('Status'),
+      meta: { label: 'Status' },
       cell: ({ row }) => {
         const status = row.getValue('status') as TenantStatus
         const config = tenantStatusConfig[status]
@@ -1017,6 +1026,7 @@ export function Tenants() {
     {
       accessorKey: 'groups',
       header: t('Group'),
+      meta: { label: 'Group' },
       cell: ({ row }) => {
         const tenant = row.original
         const groups = tenant.groups
@@ -1054,6 +1064,7 @@ export function Tenants() {
     {
       accessorKey: 'quota_limit',
       header: t('Quota Limit'),
+      meta: { label: 'Quota Limit' },
       cell: ({ row }) => {
         const quotaLimit = row.getValue('quota_limit') as number
         return (
@@ -1067,6 +1078,7 @@ export function Tenants() {
     {
       id: 'time_range',
       header: t('Authorization Time'),
+      meta: { label: 'Authorization Time' },
       cell: ({ row }) => {
         const tenant = row.original
         return (
@@ -1082,6 +1094,7 @@ export function Tenants() {
     {
       accessorKey: 'admin_registered',
       header: t('Tenant Status'),
+      meta: { label: 'Tenant Status' },
       cell: ({ row }) => {
         const registered = row.getValue('admin_registered') as boolean
         return (
@@ -1465,6 +1478,7 @@ export function Tenants() {
   const { table } = useDataTable({
     data: filteredTenants,
     columns,
+    columnVisibilityStorageKey: TENANTS_COLUMN_VISIBILITY_STORAGE_KEY,
     pagination,
     onPaginationChange: setPagination,
     manualPagination: true,
@@ -1486,6 +1500,7 @@ export function Tenants() {
             onChange={(event) => setSearchValue(event.target.value)}
             className='w-72'
           />
+          <DataTableViewOptions table={table} />
           {canCreateTenant && (
             <Sheet open={createOpen} onOpenChange={handleCreateOpenChange}>
               <SheetTrigger render={<Button />}>

+ 2 - 1
default/src/features/usage-logs/components/columns/common-logs-columns.tsx

@@ -496,7 +496,8 @@ export function useCommonLogsColumns(
     },
     {
       accessorKey: 'prompt_tokens',
-      header: 'Tokens',
+      header: t('Tokens'),
+      meta: { label: 'Tokens' },
       cell: ({ row }) => {
         const log = row.original
         if (!isDisplayableLogType(log.type)) return null

+ 14 - 2
default/src/features/users/components/users-table.tsx

@@ -31,7 +31,7 @@ import { useMediaQuery } from '@/hooks'
 import { useTableUrlState } from '@/hooks/use-table-url-state'
 
 import { getRoles } from '../../roles/api'
-import { getUsers, searchUsers } from '../api'
+import { getGroups, getUsers, searchUsers } from '../api'
 import { USER_STATUS, getUserStatusOptions, isUserDeleted } from '../constants'
 import type { User } from '../types'
 import { DataTableBulkActions } from './data-table-bulk-actions'
@@ -62,6 +62,18 @@ export function UsersTable() {
       label: role.name || role.role_name || String(role.id),
     })) || []
 
+  const { data: groupsData } = useQuery({
+    queryKey: ['groups', 'users-filter'],
+    queryFn: getGroups,
+    staleTime: 5 * 60 * 1000,
+  })
+
+  const groupOptions =
+    groupsData?.data?.items.map((group) => ({
+      value: group.group_name,
+      label: group.group_name,
+    })) || []
+
   const {
     globalFilter,
     onGlobalFilterChange,
@@ -201,7 +213,7 @@ export function UsersTable() {
           {
             columnId: 'groups',
             title: t('Group'),
-            options: [],
+            options: groupOptions,
             singleSelect: true,
           },
         ],