fix: complete first version issue remediation
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user