From a1ffdaeb0357d2b550b07991d538086cf6b24b96 Mon Sep 17 00:00:00 2001 From: shiyu Date: Mon, 25 Aug 2025 11:08:02 +0800 Subject: [PATCH] feat: enhance sharing experience with password protection --- api/routes/share.py | 14 ++- web/src/api/client.ts | 2 +- web/src/api/share.ts | 6 +- .../components/Modals/ShareModal.tsx | 102 +++++++++++++----- 4 files changed, 91 insertions(+), 33 deletions(-) diff --git a/api/routes/share.py b/api/routes/share.py index d94c218..540eec6 100644 --- a/api/routes/share.py +++ b/api/routes/share.py @@ -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]) diff --git a/web/src/api/client.ts b/web/src/api/client.ts index 70e5ce3..7440389 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -72,5 +72,5 @@ async function request(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; diff --git a/web/src/api/share.ts b/web/src/api/share.ts index 98efba7..db89eae 100644 --- a/web/src/api/share.ts +++ b/web/src/api/share.ts @@ -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('/shares', { method: 'POST', json: payload }), + create: (payload: ShareCreatePayload) => request('/shares', { method: 'POST', json: payload }), list: () => request('/shares'), remove: (shareId: number) => request(`/shares/${shareId}`, { method: 'DELETE' }), get: (token: string) => request(`/s/${token}`), diff --git a/web/src/pages/FileExplorerPage/components/Modals/ShareModal.tsx b/web/src/pages/FileExplorerPage/components/Modals/ShareModal.tsx index 033bb79..efeabd2 100644 --- a/web/src/pages/FileExplorerPage/components/Modals/ShareModal.tsx +++ b/web/src/pages/FileExplorerPage/components/Modals/ShareModal.tsx @@ -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(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 = () => ( +
+ + + + + setAccessType(e.target.value)}> + 公开 + 密码访问 + + + {accessType === 'password' && ( + + + + )} + + + +
+ ); + + const renderSuccess = () => ( +
+ 分享链接已成功创建! +
+ +
+ + +
+
+ {createdShare?.password && ( + +
+ + +
+
+ )} +
+ + 有效期至: {createdShare?.expires_at ? new Date(createdShare.expires_at).toLocaleString() : '永久有效'} + +
+ ); return ( -
- - - - - setAccessType(e.target.value)}> - 公开 - 密码访问 - - - {accessType === 'password' && ( - - - - )} - - - -
+ {createdShare ? renderSuccess() : renderForm()}
); }); \ No newline at end of file