This commit is contained in:
2026-02-01 18:45:34 +08:00
parent cc466f7bd4
commit 85f742ad2b
6 changed files with 2 additions and 3 deletions

View File

@@ -0,0 +1,61 @@
"use client"
import * as React from "react"
import { Calendar as CalendarIcon } from "lucide-react"
import dayjs, { type Dayjs } from "dayjs"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
interface DatePickerProps {
className?: string
value?: string | Dayjs
onChange?: (date: Dayjs) => void
}
export function DatePicker({ className, value, onChange }: DatePickerProps) {
const [date, setDate] = React.useState<Date | undefined>(
value ? new Date(typeof value === 'string' ? value : value.toISOString()) : undefined
)
React.useEffect(() => {
if (value) {
const dateValue = typeof value === 'string' ? value : value.toISOString()
setDate(new Date(dateValue))
}
}, [value])
const handleSelect = (selectedDate: Date | undefined) => {
setDate(selectedDate)
if (selectedDate && onChange) {
onChange(dayjs(selectedDate))
}
}
return (
<Popover>
<PopoverTrigger asChild>
<Button
variant={"outline"}
className={cn(
"w-full justify-start text-left font-normal",
!date && "text-muted-foreground",
className
)}
>
<CalendarIcon className="mr-2 h-4 w-4" />
{date ? dayjs(date).format("YYYY-MM-DD") : <span></span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<Calendar
mode="single"
selected={date}
onSelect={handleSelect}
/>
</PopoverContent>
</Popover>
)
}

View File

@@ -0,0 +1,149 @@
'use client';
import { useEffect, useState } from 'react';
import { KeyParse, getTips } from './key-parse';
import { DatePicker } from './DatePicker';
import { TagsInput } from './TagsInput';
import { HelpCircle } from 'lucide-react';
import clsx from 'clsx';
import { Button } from '@/components/ui/button';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Input } from "@/components/ui/input"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
export const KeyShareSelect = ({ value, onChange }: { value: string; onChange?: (value: string) => void }) => {
return (
<Select value={value || ''} onValueChange={(val) => onChange?.(val)}>
<SelectTrigger className="w-full">
<SelectValue placeholder="选择共享类型" />
</SelectTrigger>
<SelectContent>
<SelectItem value='public'></SelectItem>
<SelectItem value='protected'></SelectItem>
<SelectItem value='private'></SelectItem>
</SelectContent>
</Select>
);
};
export const KeyTextField = ({ name, value, onChange }: { name: string; value: string; onChange?: (value: string) => void }) => {
return (
<Input
name={name}
value={value}
onChange={(e) => onChange?.(e.target.value)}
className="w-full"
/>
);
};
type PermissionManagerProps = {
value: Record<string, any>;
onChange: (value: Record<string, any>) => void;
className?: string;
};
export const PermissionManager = ({ value, onChange, className }: PermissionManagerProps) => {
const [formData, setFormData] = useState<any>(value);
const [keys, setKeys] = useState<any>([]);
useEffect(() => {
const hasShare = value?.share && value?.share === 'protected';
setFormData(KeyParse.parse(value || {}));
if (hasShare) {
setKeys(['password', 'usernames', 'expiration-time']);
} else {
setKeys([]);
}
}, [value]);
const onChangeValue = (key: string, newValue: any) => {
let newFormData = { ...formData, [key]: newValue };
if (key === 'share') {
if (newValue === 'protected') {
newFormData = { ...newFormData, password: '', usernames: [], 'expiration-time': null };
onChange(KeyParse.stringify(newFormData));
setKeys(['password', 'usernames', 'expiration-time']);
} else {
delete newFormData.password;
delete newFormData.usernames;
delete newFormData['expiration-time'];
onChange(KeyParse.stringify(newFormData));
setKeys([]);
}
} else {
onChange(KeyParse.stringify(newFormData));
}
};
const tips = getTips('share');
return (
<TooltipProvider>
<form className={clsx('flex flex-col w-full gap-4', className)}>
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<label className="text-sm font-medium"></label>
{tips && (
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className="h-4 w-4 p-0">
<HelpCircle size={16} />
</Button>
</TooltipTrigger>
<TooltipContent className="whitespace-pre-wrap">
<p>{tips}</p>
</TooltipContent>
</Tooltip>
)}
</div>
<KeyShareSelect value={formData?.share} onChange={(value) => onChangeValue('share', value)} />
</div>
{keys.map((item: any) => {
const tips = getTips(item);
return (
<div key={item} className="flex flex-col gap-2">
<div className="flex items-center gap-2">
<label className="text-sm font-medium">{item}</label>
{tips && (
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className="h-4 w-4 p-0">
<HelpCircle size={16} />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{tips}</p>
</TooltipContent>
</Tooltip>
)}
</div>
{item === 'expiration-time' && (
<DatePicker value={formData[item] || ''} onChange={(date) => onChangeValue(item, date)} />
)}
{item === 'usernames' && (
<TagsInput value={formData[item] || []} onChange={(value: string[]) => onChangeValue(item, value)} />
)}
{item !== 'expiration-time' && item !== 'usernames' && (
<KeyTextField name={item} value={formData[item] || ''} onChange={(value) => onChangeValue(item, value)} />
)}
</div>
);
})}
</form>
</TooltipProvider>
);
};

View File

@@ -0,0 +1,61 @@
"use client"
import * as React from "react"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
import { Input } from "@/components/ui/input"
type TagsInputProps = {
value: string[];
onChange: (value: string[]) => void;
placeholder?: string;
className?: string;
};
export function TagsInput({ value, onChange, placeholder = "输入用户名,按回车添加", className }: TagsInputProps) {
const [inputValue, setInputValue] = React.useState("")
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter" || e.key === ",") {
e.preventDefault()
const newValue = inputValue.trim()
if (newValue && !value.includes(newValue)) {
onChange([...value, newValue])
}
setInputValue("")
} else if (e.key === "Backspace" && !inputValue && value.length > 0) {
onChange(value.slice(0, -1))
}
}
const removeTag = (tagToRemove: string) => {
onChange(value.filter((tag) => tag !== tagToRemove))
}
return (
<div className={cn("flex flex-wrap gap-2 w-full", className)}>
{value.map((tag, index) => (
<div
key={`${tag}-${index}`}
className="flex items-center gap-1 px-3 py-1 text-sm bg-secondary text-secondary-foreground rounded-md border border-border"
>
<span>{tag}</span>
<button
type="button"
onClick={() => removeTag(tag)}
className="flex items-center justify-center w-4 h-4 rounded hover:bg-muted transition-colors"
>
<X className="w-3 h-3" />
</button>
</div>
))}
<Input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={value.length === 0 ? placeholder : ""}
className="flex-1 min-w-[120px] h-8"
/>
</div>
)
}

View File

@@ -0,0 +1,109 @@
'use client';
import dayjs from 'dayjs';
export const getTips = (key: string, lang?: string) => {
const tip = keysTips.find((item) => item.key === key);
if (tip) {
if (lang === 'en') {
return tip.enTips;
}
return tip.tips;
}
return '';
};
export const keysTips = [
{
key: 'share',
tips: `共享设置
1. 设置公共可以直接访问
2. 设置受保护需要登录后访问
3. 设置私有只有自己可以访问。\n
受保护可以设置密码,设置访问的用户名。切换共享状态后,需要重新设置密码和用户名。 不设置,默认是只能自己访问。`,
enTips: `1. Set public to directly access
2. Set protected to access after login
3. Set private to access only yourself.
Protected can set a password and set the username for access. After switching the shared state, you need to reset the password and username. If not set, it defaults to only being accessible to yourself.`,
},
{
key: 'content-type',
tips: `内容类型,设置文件的内容类型。默认不要修改。`,
enTips: `Content type, set the content type of the file. Default do not modify.`,
},
{
key: 'app-source',
tips: `应用来源,上传方式。默认不要修改。`,
enTips: `App source, upload method. Default do not modify.`,
},
{
key: 'cache-control',
tips: `缓存控制,设置文件的缓存控制。默认不要修改。`,
enTips: `Cache control, set the cache control of the file. Default do not modify.`,
},
{
key: 'password',
tips: `密码,设置文件的密码。不设置默认是所有人都可以访问。`,
enTips: `Password, set the password of the file. If not set, it defaults to everyone can access.`,
},
{
key: 'usernames',
tips: `用户名,设置文件的用户名。不设置默认是所有人都可以访问。`,
enTips: `Username, set the username of the file. If not set, it defaults to everyone can access.`,
parse: (value: string) => {
if (!value) {
return [];
}
return value.split(',');
},
stringify: (value: string[]) => {
if (!value) {
return '';
}
return value.join(',');
},
},
{
key: 'expiration-time',
tips: `过期时间,设置文件的过期时间。不设置默认是永久。`,
enTips: `Expiration time, set the expiration time of the file. If not set, it defaults to permanent.`,
parse: (value: Date) => {
if (!value) {
return null;
}
return dayjs(value);
},
stringify: (value?: dayjs.Dayjs) => {
if (!value) {
return '';
}
return value.toISOString();
},
},
];
export class KeyParse {
static parse(metadata: Record<string, any>) {
const keys = Object.keys(metadata);
const newMetadata = {};
keys.forEach((key) => {
const tip = keysTips.find((item) => item.key === key);
if (tip && tip.parse) {
newMetadata[key] = tip.parse(metadata[key]);
} else {
newMetadata[key] = metadata[key];
}
});
return newMetadata;
}
static stringify(metadata: Record<string, any>) {
const keys = Object.keys(metadata);
const newMetadata = {};
keys.forEach((key) => {
const tip = keysTips.find((item) => item.key === key);
if (tip && tip.stringify) {
newMetadata[key] = tip.stringify(metadata[key]);
} else {
newMetadata[key] = metadata[key];
}
});
return newMetadata;
}
}