fix: complete first version issue remediation

This commit is contained in:
hectorzhao
2026-07-11 10:14:37 +08:00
parent 709ac97764
commit 208a6c23f8
73 changed files with 1549 additions and 245 deletions
+18 -1
View File
@@ -1,6 +1,9 @@
import { Download, Eye } from 'lucide-react';
import { useState } from 'react';
import { fileDownloadUrl, type FileRef } from '@/api/adminApi';
import { displayFileName } from '@/utils/fileName';
import { Button } from './Button';
import { Modal } from './Modal';
type FileActionsProps = {
file?: FileRef | null;
@@ -13,15 +16,18 @@ function isImageFile(file: FileRef) {
}
export function FileActions({ file }: FileActionsProps) {
const [previewOpen, setPreviewOpen] = useState(false);
if (!file?.fileObjectId) {
return null;
}
const previewUrl = fileDownloadUrl(file.fileObjectId, 'inline');
const downloadUrl = fileDownloadUrl(file.fileObjectId, 'attachment');
const fileName = displayFileName(file.fileName);
return (
<span className="file-actions" onClick={(event) => event.stopPropagation()}>
{isImageFile(file) ? (
<Button icon={<Eye size={14} />} onClick={() => window.open(previewUrl, '_blank', 'noopener,noreferrer')} size="sm" variant="ghost">
<Button icon={<Eye size={14} />} onClick={() => setPreviewOpen(true)} size="sm" variant="ghost">
</Button>
) : null}
@@ -29,6 +35,17 @@ export function FileActions({ file }: FileActionsProps) {
<Download size={14} />
</a>
{previewOpen ? (
<Modal
footer={<Button onClick={() => setPreviewOpen(false)}></Button>}
onClose={() => setPreviewOpen(false)}
open
size="xl"
title={fileName}
>
<img alt={fileName} className="file-preview-image" src={previewUrl} />
</Modal>
) : null}
</span>
);
}
+7 -1
View File
@@ -21,10 +21,16 @@ export function Input({
...props
}: InputProps) {
const inputId = id ?? props.name;
const required = Boolean(props.required);
return (
<label className={['ui-field', className].filter(Boolean).join(' ')} htmlFor={inputId}>
{label ? <span className="ui-field__label">{label}</span> : null}
{label ? (
<span className="ui-field__label">
{label}
{required ? <span aria-label="必填" className="ui-field__required">*</span> : null}
</span>
) : null}
<span className={['ui-input', error ? 'ui-input--error' : ''].filter(Boolean).join(' ')}>
{prefix ? <span className="ui-input__addon">{prefix}</span> : null}
<input id={inputId} {...props} />
+17 -1
View File
@@ -1,4 +1,4 @@
import type { ReactNode } from 'react';
import { useEffect, useState, type ReactNode } from 'react';
import { Button } from '@/components/ui/Button';
type QueryPanelProps = {
@@ -10,10 +10,12 @@ type QueryPanelProps = {
type PaginationProps = {
total?: number;
page?: number;
totalPages?: number;
previousDisabled?: boolean;
nextDisabled?: boolean;
onPrevious?: () => void;
onNext?: () => void;
onPageChange?: (page: number) => void;
};
type InlineTextPreviewProps = {
@@ -35,17 +37,31 @@ export function QueryPanel({ title, summary, children }: QueryPanelProps) {
export function Pagination({
total,
page = 1,
totalPages,
previousDisabled = true,
nextDisabled = true,
onPrevious,
onNext,
onPageChange,
}: PaginationProps) {
const pages = Math.max(1, totalPages ?? (typeof total === 'number' ? Math.ceil(total / 10) : page));
const [targetPage, setTargetPage] = useState(String(page));
useEffect(() => setTargetPage(String(page)), [page]);
function changePage(nextPage: number) {
onPageChange?.(Math.min(pages, Math.max(1, nextPage)));
}
return (
<div className="ui-pagination">
{typeof total === 'number' ? <span> {total} </span> : <span />}
<div>
<Button disabled={previousDisabled} onClick={onPrevious} size="sm" variant="ghost"></Button>
{onPageChange ? <Button disabled={page <= 1} onClick={() => changePage(1)} size="sm" variant="ghost"></Button> : null}
<Button size="sm" variant="secondary">{page}</Button>
{onPageChange ? <label className="ui-pagination__jump"> <input aria-label="跳转页码" min="1" max={pages} onChange={(event) => setTargetPage(event.target.value)} onKeyDown={(event) => { if (event.key === 'Enter') changePage(Number(targetPage)); }} type="number" value={targetPage} /> / {pages} </label> : null}
{onPageChange ? <Button disabled={page >= pages} onClick={() => changePage(pages)} size="sm" variant="ghost"></Button> : null}
<Button disabled={nextDisabled} onClick={onNext} size="sm" variant="ghost"></Button>
</div>
</div>
+7 -1
View File
@@ -35,6 +35,7 @@ export function Select({
onChange,
disabled,
placeholder,
required,
...props
}: SelectProps) {
const selectId = id ?? props.name;
@@ -70,7 +71,12 @@ export function Select({
htmlFor={selectId}
ref={rootRef}
>
{label ? <span className="ui-field__label">{label}</span> : null}
{label ? (
<span className="ui-field__label">
{label}
{required ? <span aria-label="必填" className="ui-field__required">*</span> : null}
</span>
) : null}
<span
className={[
'ui-select',
+3 -2
View File
@@ -9,13 +9,14 @@ export type TabItem = {
};
type TabsProps = {
className?: string;
items: TabItem[];
defaultValue?: string;
value?: string;
onChange?: (value: string) => void;
};
export function Tabs({ items, defaultValue, value, onChange }: TabsProps) {
export function Tabs({ className = '', items, defaultValue, value, onChange }: TabsProps) {
const [internalValue, setInternalValue] = useState(defaultValue ?? items[0]?.value);
const activeValue = value ?? internalValue;
const activeItem = items.find((item) => item.value === activeValue) ?? items[0];
@@ -26,7 +27,7 @@ export function Tabs({ items, defaultValue, value, onChange }: TabsProps) {
}
return (
<div className="ui-tabs">
<div className={['ui-tabs', className].filter(Boolean).join(' ')}>
<div className="ui-tabs__list" role="tablist">
{items.map((item) => (
<button
+7 -1
View File
@@ -8,10 +8,16 @@ type TextareaProps = TextareaHTMLAttributes<HTMLTextAreaElement> & {
export function Textarea({ className = '', label, hint, error, id, ...props }: TextareaProps) {
const textareaId = id ?? props.name;
const required = Boolean(props.required);
return (
<label className={['ui-field', className].filter(Boolean).join(' ')} htmlFor={textareaId}>
{label ? <span className="ui-field__label">{label}</span> : null}
{label ? (
<span className="ui-field__label">
{label}
{required ? <span aria-label="必填" className="ui-field__required">*</span> : null}
</span>
) : null}
<textarea
className={['ui-textarea', error ? 'ui-textarea--error' : ''].filter(Boolean).join(' ')}
id={textareaId}