refactor(ui): unify top level pages structure, use standard language codes and time zones (#13573)

### What problem does this PR solve?

- Unify top level pages structure
- Standardize locale language codes (BCP 47) and time zones (IANA tz)


> **Note:** 
> Newly created user info brings non-standard default values `timezone:
"UTC+8\tAsia/Shanghai"` and `language: "English"`.


### Type of change

- [x] Refactoring
This commit is contained in:
Jimmy Ben Klieve
2026-03-12 21:01:09 +08:00
committed by GitHub
parent 35cd56f990
commit 1a4dee4313
42 changed files with 673 additions and 1364 deletions

View File

@ -26,8 +26,9 @@ const ApiContent = ({ id, idKey }: { id?: string; idKey: string }) => {
const isDarkTheme = useIsDarkTheme();
return (
<div className="pb-2 flex flex-col w-full">
<BackendServiceApi show={showApiKeyModal}></BackendServiceApi>
<div className="flex flex-col w-full">
<BackendServiceApi show={showApiKeyModal} />
<div className="text-left py-4">
<Button onClick={tocVisible ? hideToc : showToc}>
{tocVisible ? t('hideToc') : t('showToc')}

View File

@ -1,3 +1,4 @@
import { combineRefs } from '@/lib/utils';
import { transformFile2Base64 } from '@/utils/file-util';
import { LucidePencil, LucidePlus, LucideX } from 'lucide-react';
import {
@ -11,7 +12,6 @@ import {
import { useTranslation } from 'react-i18next';
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Modal } from './ui/modal/modal';
type AvatarUploadProps = {
@ -42,6 +42,7 @@ export const AvatarUpload = forwardRef<HTMLInputElement, AvatarUploadProps>(
const [isCropModalOpen, setIsCropModalOpen] = useState(false);
const [imageToCrop, setImageToCrop] = useState<string | null>(null);
const [cropArea, setCropArea] = useState({ x: 0, y: 0, size: 200 });
const innerInputRef = useRef<HTMLInputElement | null>(null);
const imageRef = useRef<HTMLImageElement>(null);
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
@ -210,7 +211,6 @@ export const AvatarUpload = forwardRef<HTMLInputElement, AvatarUploadProps>(
const handleWheel = useCallback((e: React.WheelEvent) => {
if (!imageRef.current) return;
e.preventDefault();
const image = imageRef.current;
const delta = e.deltaY > 0 ? 0.9 : 1.1; // Zoom factor
@ -245,10 +245,10 @@ export const AvatarUpload = forwardRef<HTMLInputElement, AvatarUploadProps>(
}
}, [value]);
/*
useEffect(() => {
const container = containerRef.current;
setTimeout(() => {
console.log('container', container);
// initCropArea();
if (imageToCrop && container && isCropModalOpen) {
container.addEventListener(
@ -265,34 +265,59 @@ export const AvatarUpload = forwardRef<HTMLInputElement, AvatarUploadProps>(
}
}, 100);
}, [handleWheel, imageToCrop, isCropModalOpen]);
*/
return (
<div className="flex justify-start items-end space-x-2">
<div className="relative group">
<input
placeholder=""
type="file"
title=""
accept="image/*"
className="peer/input size-0 absolute top-0 left-0 opacity-0 pointer-events-none"
onChange={handleChange}
ref={combineRefs(ref, innerInputRef)}
data-testid={uploadInputTestId}
tabIndex={-1}
/>
{!avatarBase64Str ? (
<div
className="
border border-dashed border-borer-button rounded-md size-16
flex flex-col gap-1 justify-center items-center text-sm text-text-secondary transition-colors
group-has-[input:focus-visible]:border-accent-primary group-has-[input:focus-visible]:text-text-primary"
<Button
variant="dashed"
size="icon"
className="size-16 flex flex-col items-center gap-1 !bg-transparent"
onClick={() => {
innerInputRef.current?.click();
}}
>
<LucidePlus className="size-4" />
<span>{t('common.upload')}</span>
</div>
</Button>
) : (
<div className="size-16 relative grid place-content-center">
<Avatar className="size-16 rounded-md">
<AvatarImage className="block" src={avatarBase64Str} alt="" />
<AvatarFallback></AvatarFallback>
</Avatar>
<div
className="
absolute inset-0 bg-black/50 flex items-center justify-center
transition-opacity opacity-0 group-hover:opacity-100 group-has-[input:focus-visible]:opacity-100"
<Button
variant="transparent"
size="icon"
className="group/button size-full p-0 transition-all relative gap-0 overflow-hidden"
onClick={() => {
innerInputRef.current?.click();
}}
>
<LucidePencil className="size-5 opacity-75" />
</div>
<Avatar className="size-full rounded-none">
<AvatarImage className="block" src={avatarBase64Str} alt="" />
<AvatarFallback />
</Avatar>
<div
className="
absolute inset-0 flex items-center justify-center
bg-black/40 opacity-0 transition-opacity
group-hover/button:opacity-100 group-focus-visible/button:opacity-100"
>
<LucidePencil className="size-5 opacity-75" />
</div>
</Button>
<Button
onClick={handleRemove}
@ -306,18 +331,8 @@ export const AvatarUpload = forwardRef<HTMLInputElement, AvatarUploadProps>(
</Button>
</div>
)}
<Input
placeholder=""
type="file"
title=""
accept="image/*"
className="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer"
onChange={handleChange}
ref={ref}
data-testid={uploadInputTestId}
/>
</div>
<div className="ms-1 text-xs text-text-secondary">
{tips ?? t('knowledgeConfiguration.photoTip')}
</div>
@ -357,7 +372,7 @@ export const AvatarUpload = forwardRef<HTMLInputElement, AvatarUploadProps>(
height: '300px',
touchAction: 'none',
}}
// onWheel={handleWheel}
onWheel={handleWheel}
>
<img
ref={imageRef}

View File

@ -5,13 +5,13 @@ type CardContainerProps = { className?: string } & PropsWithChildren;
export function CardContainer({ children, className }: CardContainerProps) {
return (
<section
<div
className={cn(
'grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5',
'grid gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 auto-rows-auto content-start',
className,
)}
>
{children}
</section>
</div>
);
}

View File

@ -1,5 +1,5 @@
import { cn } from '@/lib/utils';
import { isValidElement, PropsWithChildren, ReactNode } from 'react';
import { PropsWithChildren } from 'react';
import './index.less';
type CardContainerProps = { className?: string } & PropsWithChildren;
@ -8,26 +8,6 @@ export function CardSineLineContainer({
children,
className,
}: CardContainerProps) {
const flattenChildren = (children: ReactNode): ReactNode[] => {
const result: ReactNode[] = [];
const traverse = (child: ReactNode) => {
if (Array.isArray(child)) {
child.forEach(traverse);
} else if (isValidElement(child) && child.props.children) {
result.push(child);
} else {
result.push(child);
}
};
traverse(children);
return result;
};
const childArray = flattenChildren(children);
const childCount = childArray.length;
console.log(childArray, childCount);
return (
<section
className={cn(

View File

@ -83,8 +83,10 @@ export const EmptyAppCard = (props: {
size?: 'small' | 'large';
children?: React.ReactNode;
testId?: string;
tabIndex?: number;
}) => {
const { type, showIcon, className, isSearch, children, testId } = props;
const { type, showIcon, className, isSearch, children, testId, tabIndex } =
props;
const { t } = useTranslation();
let defaultClass = '';
let style = {};
@ -110,10 +112,10 @@ export const EmptyAppCard = (props: {
<EmptyCard
onClick={isSearch ? undefined : props.onClick}
data-testid={testId}
tabIndex={isSearch ? undefined : 0}
tabIndex={tabIndex ?? (isSearch ? undefined : 0)}
icon={showIcon ? cardData.icon : undefined}
title={isSearch ? notFound : title}
className={cn('cursor-pointer', className)}
className={cn(!isSearch && 'cursor-pointer', className)}
style={style}
// description={EmptyCardData[type].description}
>

View File

@ -94,7 +94,7 @@ export default function ListFilterBar({
return (
<div className={cn('flex justify-between items-center', className)}>
<div className="text-2xl font-semibold flex items-center gap-2.5">
<h1 className="text-2xl font-semibold flex items-center gap-2.5">
{typeof icon === 'string' ? (
// <IconFont name={icon} className="size-6"></IconFont>
<HomeIcon
@ -105,11 +105,11 @@ export default function ListFilterBar({
icon
)}
{leftPanel || title}
</div>
</h1>
<div className="flex gap-4 items-center" role="toolbar">
{preChildren}
{showFilter && (
{filters?.length && showFilter && (
<FilterPopover
value={value}
onChange={onChange}

View File

@ -165,10 +165,8 @@ export const SelectWithSearch = forwardRef<
)}
>
{selectLabel || value ? (
<span className="flex min-w-0 options-center gap-2">
<span className="leading-none truncate">
{selectLabel || value}
</span>
<span className="flex min-w-0 options-center gap-2 truncate">
{selectLabel || value}
</span>
) : (
<span className="text-text-disabled">{placeholder}</span>
@ -209,10 +207,10 @@ export const SelectWithSearch = forwardRef<
<CommandEmpty>
<div dangerouslySetInnerHTML={{ __html: emptyData }}></div>
</CommandEmpty>
{options.map((group, idx) => {
{options.map((group) => {
if (group.options) {
return (
<Fragment key={idx}>
<Fragment key={group.value}>
<CommandGroup heading={group.label} className="mb-1">
{group.options.map((option) => (
<CommandItem

View File

@ -93,7 +93,8 @@ const buttonVariants = cva(
// Static
// Button has no interaction transitions
static: 'text-text-secondary',
static:
'text-text-secondary hover:text-text-primary focus-visible:text-text-primary',
},
size: {
auto: '',

View File

@ -9,8 +9,8 @@ import {
} from '@/components/ui/pagination';
import { RAGFlowSelect, RAGFlowSelectOptionType } from '@/components/ui/select';
import { cn } from '@/lib/utils';
import { t } from 'i18next';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
export type RAGFlowPaginationType = {
showQuickJumper?: boolean;
@ -28,6 +28,7 @@ export function RAGFlowPagination({
onChange,
showSizeChanger = true,
}: RAGFlowPaginationType) {
const { t } = useTranslation();
const [currentPage, setCurrentPage] = useState(1);
const [currentPageSize, setCurrentPageSize] = useState('10');
@ -36,7 +37,7 @@ export function RAGFlowPagination({
label: <span>{t('pagination.page', { page: x })}</span>,
value: x.toString(),
}));
}, []);
}, [t]);
const pages = useMemo(() => {
const num = Math.ceil(total / pageSize);
@ -134,7 +135,7 @@ export function RAGFlowPagination({
}, [pages, currentPage]);
return (
<section className="flex items-center justify-end text-text-sub-title-invert">
<div className="flex items-center justify-end text-text-sub-title-invert">
<span className="mr-4 text-text-primary">
{t('pagination.total', { total: total })}
</span>
@ -181,6 +182,6 @@ export function RAGFlowPagination({
triggerClassName="bg-bg-card border-transparent"
/>
)}
</section>
</div>
);
}

View File

@ -105,8 +105,6 @@ const Segmented = React.forwardRef<HTMLDivElement, SegmentedProps>(
const isObject = typeof option === 'object';
const actualValue = isObject ? option.value : option;
console.log(actualValue);
return (
<Button
key={actualValue}