feat: enhance sharing experience with password protection

This commit is contained in:
shiyu
2025-08-25 11:08:02 +08:00
parent 9f22ed6771
commit a1ffdaeb03
4 changed files with 91 additions and 33 deletions

View File

@@ -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])

View File

@@ -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;

View File

@@ -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}`),

View File

@@ -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>
);
});