feat: Add application domain and file domain configuration

This commit is contained in:
shiyu
2025-08-31 12:38:21 +08:00
parent 3f3f192d53
commit b50f19bcb4
9 changed files with 30 additions and 12 deletions
+3 -1
View File
@@ -41,7 +41,9 @@ async def get_system_status():
"version": VERSION, "version": VERSION,
"title": await ConfigCenter.get("APP_NAME", "Foxel"), "title": await ConfigCenter.get("APP_NAME", "Foxel"),
"logo": await ConfigCenter.get("APP_LOGO", "/logo.svg"), "logo": await ConfigCenter.get("APP_LOGO", "/logo.svg"),
"is_initialized": await has_users() "is_initialized": await has_users(),
"app_domain": await ConfigCenter.get("APP_DOMAIN"),
"file_domain": await ConfigCenter.get("FILE_DOMAIN"),
} }
return success(system_info) return success(system_info)
+8 -1
View File
@@ -19,6 +19,7 @@ from services.virtual_fs import (
from services.thumbnail import is_image_filename, get_or_create_thumb, is_raw_filename from services.thumbnail import is_image_filename, get_or_create_thumb, is_raw_filename
from schemas import MkdirRequest, MoveRequest from schemas import MkdirRequest, MoveRequest
from api.response import success from api.response import success
from services.config import ConfigCenter
router = APIRouter(prefix='/api/fs', tags=["virtual-fs"]) router = APIRouter(prefix='/api/fs', tags=["virtual-fs"])
@@ -151,7 +152,13 @@ async def get_temp_link(
"""获取文件的临时公开访问令牌""" """获取文件的临时公开访问令牌"""
full_path = '/' + full_path if not full_path.startswith('/') else full_path full_path = '/' + full_path if not full_path.startswith('/') else full_path
token = await generate_temp_link_token(full_path, expires_in=expires_in) token = await generate_temp_link_token(full_path, expires_in=expires_in)
return success({"token": token, "path": full_path}) file_domain = await ConfigCenter.get("FILE_DOMAIN")
if file_domain:
file_domain = file_domain.rstrip('/')
url = f"{file_domain}/api/fs/public/{token}"
else:
url = f"/api/fs/public/{token}"
return success({"token": token, "path": full_path, "url": url})
@router.get("/public/{token}") @router.get("/public/{token}")
+2
View File
@@ -20,6 +20,8 @@ export interface SystemStatus {
title: string; title: string;
logo: string; logo: string;
is_initialized: boolean; is_initialized: boolean;
app_domain?: string;
file_domain?: string;
} }
export async function status() { export async function status() {
+1 -1
View File
@@ -51,7 +51,7 @@ export const vfsApi = {
streamUrl: (path: string) => `${API_BASE_URL}/fs/stream/${encodeURI(path.replace(/^\/+/, ''))}`, streamUrl: (path: string) => `${API_BASE_URL}/fs/stream/${encodeURI(path.replace(/^\/+/, ''))}`,
stat: (path: string) => request(`/fs/stat/${encodeURI(path.replace(/^\/+/, ''))}`), stat: (path: string) => request(`/fs/stat/${encodeURI(path.replace(/^\/+/, ''))}`),
getTempLinkToken: (path: string, expiresIn: number = 3600) => getTempLinkToken: (path: string, expiresIn: number = 3600) =>
request<{token: string}>(`/fs/temp-link/${encodeURI(path.replace(/^\/+/, ''))}?expires_in=${expiresIn}`), request<{token: string, path: string, url: string}>(`/fs/temp-link/${encodeURI(path.replace(/^\/+/, ''))}?expires_in=${expiresIn}`),
getTempPublicUrl: (token: string) => `${API_BASE_URL}/fs/public/${token}`, getTempPublicUrl: (token: string) => `${API_BASE_URL}/fs/public/${token}`,
uploadStream: (fullPath: string, file: File, overwrite: boolean = true, onProgress?: (loaded: number, total: number) => void) => { uploadStream: (fullPath: string, file: File, overwrite: boolean = true, onProgress?: (loaded: number, total: number) => void) => {
const enc = encodeURI(fullPath.replace(/^\/+/, '')); const enc = encodeURI(fullPath.replace(/^\/+/, ''));
+4 -2
View File
@@ -2,8 +2,10 @@ import React, { useEffect, useState } from 'react';
import { vfsApi } from '../../api/client'; import { vfsApi } from '../../api/client';
import type { AppComponentProps } from '../types'; import type { AppComponentProps } from '../types';
import { Spin, Result, Button } from 'antd'; import { Spin, Result, Button } from 'antd';
import { useSystemStatus } from '../../contexts/SystemContext';
export const OfficeViewerApp: React.FC<AppComponentProps> = ({ filePath, onRequestClose }) => { export const OfficeViewerApp: React.FC<AppComponentProps> = ({ filePath, onRequestClose }) => {
const systemStatus = useSystemStatus();
const [url, setUrl] = useState<string>(); const [url, setUrl] = useState<string>();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [err, setErr] = useState<string>(); const [err, setErr] = useState<string>();
@@ -17,8 +19,8 @@ export const OfficeViewerApp: React.FC<AppComponentProps> = ({ filePath, onReque
vfsApi.getTempLinkToken(filePath.replace(/^\/+/, '')) vfsApi.getTempLinkToken(filePath.replace(/^\/+/, ''))
.then(res => { .then(res => {
if (cancelled) return; if (cancelled) return;
// 注意:vfsApi.getTempPublicUrl 返回的是相对路径,我们需要构建完整的 URL const baseUrl = systemStatus?.file_domain || window.location.origin;
const fullUrl = new URL(vfsApi.getTempPublicUrl(res.token), window.location.origin).href; const fullUrl = new URL(res.url, baseUrl).href;
const officeUrl = `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(fullUrl)}`; const officeUrl = `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(fullUrl)}`;
setUrl(officeUrl); setUrl(officeUrl);
}) })
@@ -29,8 +29,7 @@ export const DirectLinkModal = memo(function DirectLinkModal({ entry, path, open
try { try {
const fullPath = (path === '/' ? '' : path) + '/' + entry.name; const fullPath = (path === '/' ? '' : path) + '/' + entry.name;
const res = await vfsApi.getTempLinkToken(fullPath, expiresIn); const res = await vfsApi.getTempLinkToken(fullPath, expiresIn);
const tempLink = `${window.location.origin}/api/fs/public/${res.token}`; setLink(res.url);
setLink(tempLink);
} catch (e: any) { } catch (e: any) {
message.error(e.message || '生成链接失败'); message.error(e.message || '生成链接失败');
} finally { } finally {
@@ -3,6 +3,7 @@ import { Modal, Form, Input, Radio, InputNumber, message, Button, Typography } f
import { CopyOutlined } from '@ant-design/icons'; import { CopyOutlined } from '@ant-design/icons';
import type { VfsEntry, ShareInfoWithPassword } from '../../../../api/client'; import type { VfsEntry, ShareInfoWithPassword } from '../../../../api/client';
import { shareApi } from '../../../../api/share'; import { shareApi } from '../../../../api/share';
import { useSystemStatus } from '../../../../contexts/SystemContext';
interface ShareModalProps { interface ShareModalProps {
entries: VfsEntry[]; entries: VfsEntry[];
@@ -13,6 +14,7 @@ interface ShareModalProps {
} }
export const ShareModal = memo(function ShareModal({ entries, path, open, onOk, onCancel }: ShareModalProps) { export const ShareModal = memo(function ShareModal({ entries, path, open, onOk, onCancel }: ShareModalProps) {
const systemStatus = useSystemStatus();
const [form] = Form.useForm(); const [form] = Form.useForm();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [accessType, setAccessType] = useState('public'); const [accessType, setAccessType] = useState('public');
@@ -66,7 +68,8 @@ export const ShareModal = memo(function ShareModal({ entries, path, open, onOk,
message.success('已复制到剪贴板'); message.success('已复制到剪贴板');
}; };
const shareUrl = createdShare ? `${window.location.origin}/share/${createdShare.token}` : ''; const baseUrl = systemStatus?.app_domain || window.location.origin;
const shareUrl = createdShare ? new URL(`/share/${createdShare.token}`, baseUrl).href : '';
const renderForm = () => ( const renderForm = () => (
<Form form={form} layout="vertical" initialValues={{ name: defaultName, accessType: 'public', expiresInDays: 7 }}> <Form form={form} layout="vertical" initialValues={{ name: defaultName, accessType: 'public', expiresInDays: 7 }}>
+4 -1
View File
@@ -4,8 +4,10 @@ import PageCard from '../components/PageCard';
import { shareApi, type ShareInfo } from '../api/share'; import { shareApi, type ShareInfo } from '../api/share';
import { format, parseISO } from 'date-fns'; import { format, parseISO } from 'date-fns';
import { LinkOutlined, CopyOutlined, DeleteOutlined } from '@ant-design/icons'; import { LinkOutlined, CopyOutlined, DeleteOutlined } from '@ant-design/icons';
import { useSystemStatus } from '../contexts/SystemContext';
const SharePage = memo(function SharePage() { const SharePage = memo(function SharePage() {
const systemStatus = useSystemStatus();
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [data, setData] = useState<ShareInfo[]>([]); const [data, setData] = useState<ShareInfo[]>([]);
@@ -24,7 +26,8 @@ const SharePage = memo(function SharePage() {
useEffect(() => { fetchList(); }, [fetchList]); useEffect(() => { fetchList(); }, [fetchList]);
const doCopy = (rec: ShareInfo) => { const doCopy = (rec: ShareInfo) => {
const shareUrl = `${window.location.origin}/share/${rec.token}`; const baseUrl = systemStatus?.app_domain || window.location.origin;
const shareUrl = new URL(`/share/${rec.token}`, baseUrl).href;
navigator.clipboard.writeText(shareUrl); navigator.clipboard.writeText(shareUrl);
message.success('链接已复制'); message.success('链接已复制');
}; };
@@ -2,13 +2,13 @@ import { Form, Input, Button, message, Tabs, Space, Card } from 'antd';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import PageCard from '../../components/PageCard'; import PageCard from '../../components/PageCard';
import { getAllConfig, setConfig } from '../../api/config'; import { getAllConfig, setConfig } from '../../api/config';
import { API_BASE_URL } from '../../api/client';
import { AppstoreOutlined, RobotOutlined } from '@ant-design/icons'; import { AppstoreOutlined, RobotOutlined } from '@ant-design/icons';
const APP_CONFIG_KEYS = [ const APP_CONFIG_KEYS: {key: string, label: string, default?: string}[] = [
{ key: 'APP_NAME', label: '应用名称' }, { key: 'APP_NAME', label: '应用名称' },
{ key: 'APP_LOGO', label: 'LOGO地址' }, { key: 'APP_LOGO', label: 'LOGO地址' },
{ key: 'SERVER_URL', label: '服务端URL', default: API_BASE_URL }, { key: 'APP_DOMAIN', label: '应用域名' },
{ key: 'FILE_DOMAIN', label: '文件域名' },
]; ];
const VISION_CONFIG_KEYS = [ const VISION_CONFIG_KEYS = [