update
This commit is contained in:
61
src/app/apps/modules/permission/DatePicker.tsx
Normal file
61
src/app/apps/modules/permission/DatePicker.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
149
src/app/apps/modules/permission/PermissionManager.tsx
Normal file
149
src/app/apps/modules/permission/PermissionManager.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
61
src/app/apps/modules/permission/TagsInput.tsx
Normal file
61
src/app/apps/modules/permission/TagsInput.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
109
src/app/apps/modules/permission/key-parse.ts
Normal file
109
src/app/apps/modules/permission/key-parse.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user