mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-05-06 18:22:44 +08:00
feat: enhance sharing experience with password protection
This commit is contained in:
@@ -41,9 +41,14 @@ class ShareInfo(BaseModel):
|
||||
access_type=obj.access_type,
|
||||
)
|
||||
|
||||
|
||||
class ShareInfoWithPassword(ShareInfo):
|
||||
password: Optional[str] = None
|
||||
|
||||
|
||||
# --- Management Routes ---
|
||||
|
||||
@router.post("", response_model=ShareInfo)
|
||||
@router.post("", response_model=ShareInfoWithPassword)
|
||||
async def create_share(
|
||||
payload: ShareCreate,
|
||||
current_user: User = Depends(get_current_active_user),
|
||||
@@ -60,7 +65,12 @@ async def create_share(
|
||||
access_type=payload.access_type,
|
||||
password=payload.password,
|
||||
)
|
||||
return ShareInfo.from_orm(share)
|
||||
share_info_base = ShareInfo.from_orm(share)
|
||||
response_data = share_info_base.model_dump()
|
||||
if payload.access_type == "password" and payload.password:
|
||||
response_data['password'] = payload.password
|
||||
|
||||
return response_data
|
||||
|
||||
|
||||
@router.get("", response_model=List[ShareInfo])
|
||||
|
||||
@@ -72,5 +72,5 @@ async function request<T = any>(url: string, options: RequestOptions = {}): Prom
|
||||
|
||||
export { vfsApi, type VfsEntry, type DirListing } from './vfs';
|
||||
export { adaptersApi, type AdapterItem, type AdapterTypeField, type AdapterTypeMeta } from './adapters';
|
||||
export { shareApi } from './share';
|
||||
export { shareApi, type ShareInfo, type ShareInfoWithPassword } from './share';
|
||||
export default request;
|
||||
|
||||
@@ -11,6 +11,10 @@ export interface ShareInfo {
|
||||
access_type: 'public' | 'password';
|
||||
}
|
||||
|
||||
export interface ShareInfoWithPassword extends ShareInfo {
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export interface ShareCreatePayload {
|
||||
name: string;
|
||||
paths: string[];
|
||||
@@ -20,7 +24,7 @@ export interface ShareCreatePayload {
|
||||
}
|
||||
|
||||
export const shareApi = {
|
||||
create: (payload: ShareCreatePayload) => request<ShareInfo>('/shares', { method: 'POST', json: payload }),
|
||||
create: (payload: ShareCreatePayload) => request<ShareInfoWithPassword>('/shares', { method: 'POST', json: payload }),
|
||||
list: () => request<ShareInfo[]>('/shares'),
|
||||
remove: (shareId: number) => request<void>(`/shares/${shareId}`, { method: 'DELETE' }),
|
||||
get: (token: string) => request<ShareInfo>(`/s/${token}`),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { memo, useState, useEffect } from 'react';
|
||||
import { Modal, Form, Input, Radio, InputNumber, message } from 'antd';
|
||||
import type { VfsEntry } from '../../../../api/client';
|
||||
import { Modal, Form, Input, Radio, InputNumber, message, Button, Typography } from 'antd';
|
||||
import { CopyOutlined } from '@ant-design/icons';
|
||||
import type { VfsEntry, ShareInfoWithPassword } from '../../../../api/client';
|
||||
import { shareApi } from '../../../../api/share';
|
||||
|
||||
interface ShareModalProps {
|
||||
@@ -15,13 +16,15 @@ export const ShareModal = memo(function ShareModal({ entries, path, open, onOk,
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [accessType, setAccessType] = useState('public');
|
||||
const [createdShare, setCreatedShare] = useState<ShareInfoWithPassword | null>(null);
|
||||
|
||||
const defaultName = entries.length > 1
|
||||
? `分享 ${entries.length} 个项目`
|
||||
const defaultName = entries.length > 1
|
||||
? `分享 ${entries.length} 个项目`
|
||||
: (entries.length === 1 ? entries[0].name : '');
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setCreatedShare(null);
|
||||
form.setFieldsValue({
|
||||
name: defaultName,
|
||||
accessType: 'public',
|
||||
@@ -36,13 +39,13 @@ export const ShareModal = memo(function ShareModal({ entries, path, open, onOk,
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
setLoading(true);
|
||||
|
||||
|
||||
const fullPaths = entries.map(e => {
|
||||
const p = path === '/' ? '' : path;
|
||||
return `${p}/${e.name}`;
|
||||
});
|
||||
|
||||
await shareApi.create({
|
||||
const result = await shareApi.create({
|
||||
name: values.name,
|
||||
paths: fullPaths,
|
||||
access_type: values.accessType,
|
||||
@@ -50,42 +53,83 @@ export const ShareModal = memo(function ShareModal({ entries, path, open, onOk,
|
||||
expires_in_days: values.expiresInDays,
|
||||
});
|
||||
message.success('分享链接已创建');
|
||||
onOk();
|
||||
setCreatedShare(result);
|
||||
} catch (e: any) {
|
||||
message.error(e.message || '创建失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopy = (text: string) => {
|
||||
navigator.clipboard.writeText(text);
|
||||
message.success('已复制到剪贴板');
|
||||
};
|
||||
|
||||
const shareUrl = createdShare ? `${window.location.origin}/s/${createdShare.token}` : '';
|
||||
|
||||
const renderForm = () => (
|
||||
<Form form={form} layout="vertical" initialValues={{ name: defaultName, accessType: 'public', expiresInDays: 7 }}>
|
||||
<Form.Item name="name" label="分享名称" rules={[{ required: true }]} >
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="accessType" label="访问权限">
|
||||
<Radio.Group onChange={(e) => setAccessType(e.target.value)}>
|
||||
<Radio value="public">公开</Radio>
|
||||
<Radio value="password">密码访问</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
{accessType === 'password' && (
|
||||
<Form.Item name="password" label="访问密码" rules={[{ required: true, message: '请输入密码' }]} >
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item name="expiresInDays" label="有效期 (天)" help="设置为 0 或负数表示永久有效">
|
||||
<InputNumber min={-1} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
|
||||
const renderSuccess = () => (
|
||||
<div>
|
||||
<Typography.Paragraph>分享链接已成功创建!</Typography.Paragraph>
|
||||
<Form layout="vertical">
|
||||
<Form.Item label="分享链接">
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Input readOnly value={shareUrl} style={{ flex: 1 }} />
|
||||
<Button icon={<CopyOutlined />} onClick={() => handleCopy(shareUrl)}>
|
||||
复制
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
{createdShare?.password && (
|
||||
<Form.Item label="访问密码">
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<Input readOnly value={createdShare.password} style={{ flex: 1 }} />
|
||||
<Button icon={<CopyOutlined />} onClick={() => handleCopy(createdShare.password!)}>
|
||||
复制
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form>
|
||||
<Typography.Text type="secondary">
|
||||
有效期至: {createdShare?.expires_at ? new Date(createdShare.expires_at).toLocaleString() : '永久有效'}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title="创建分享"
|
||||
title={createdShare ? "分享创建成功" : "创建分享"}
|
||||
open={open}
|
||||
onOk={handleOk}
|
||||
onOk={createdShare ? onOk : handleOk}
|
||||
onCancel={onCancel}
|
||||
confirmLoading={loading}
|
||||
destroyOnClose
|
||||
destroyOnHidden
|
||||
okText={createdShare ? "完成" : "创建"}
|
||||
>
|
||||
<Form form={form} layout="vertical" initialValues={{ name: defaultName, accessType: 'public', expiresInDays: 7 }}>
|
||||
<Form.Item name="name" label="分享名称" rules={[{ required: true }]} >
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item name="accessType" label="访问权限">
|
||||
<Radio.Group onChange={(e) => setAccessType(e.target.value)}>
|
||||
<Radio value="public">公开</Radio>
|
||||
<Radio value="password">密码访问</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
{accessType === 'password' && (
|
||||
<Form.Item name="password" label="访问密码" rules={[{ required: true, message: '请输入密码' }]} >
|
||||
<Input.Password />
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item name="expiresInDays" label="有效期 (天)" help="设置为 0 或负数表示永久有效">
|
||||
<InputNumber min={-1} style={{ width: '100%' }} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
{createdShare ? renderSuccess() : renderForm()}
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user