feat(api): Add getClient API endpoint to retrieve subscription applications

This commit is contained in:
web
2025-08-13 07:35:58 -07:00
parent 641ed5ec36
commit 7a279e6e30
32 changed files with 1109 additions and 354 deletions
@@ -5,8 +5,7 @@ import Renewal from '@/components/subscribe/renewal';
import ResetTraffic from '@/components/subscribe/reset-traffic';
import Unsubscribe from '@/components/subscribe/unsubscribe';
import useGlobalStore from '@/config/use-global';
import { getStat } from '@/services/common/common';
import { queryApplicationConfig } from '@/services/user/subscribe';
import { getClient, getStat } from '@/services/common/common';
import { queryUserSubscribe, resetUserSubscribeToken } from '@/services/user/user';
import { getPlatform } from '@/utils/common';
import { useQuery } from '@tanstack/react-query';
@@ -43,9 +42,9 @@ import CopyToClipboard from 'react-copy-to-clipboard';
import { toast } from 'sonner';
import Subscribe from '../subscribe/page';
const platforms: (keyof API.ApplicationPlatform)[] = [
const platforms: (keyof API.DownloadLink)[] = [
'windows',
'macos',
'mac',
'linux',
'ios',
'android',
@@ -70,13 +69,15 @@ export default function Content() {
},
});
const { data: applications } = useQuery({
queryKey: ['queryApplicationConfig'],
queryKey: ['getClient'],
queryFn: async () => {
const { data } = await queryApplicationConfig();
return data.data?.applications || [];
const { data } = await getClient();
return data.data?.list || [];
},
});
const [platform, setPlatform] = useState<keyof API.ApplicationPlatform>(getPlatform());
const [platform, setPlatform] = useState<keyof API.DownloadLink>(
getPlatform() === 'macos' ? 'mac' : (getPlatform() as keyof API.DownloadLink),
);
const { data } = useQuery({
queryKey: ['getStat'],
@@ -123,7 +124,7 @@ export default function Content() {
<div className='flex flex-wrap justify-between gap-4'>
<Tabs
value={platform}
onValueChange={(value) => setPlatform(value as keyof API.ApplicationPlatform)}
onValueChange={(value) => setPlatform(value as keyof API.DownloadLink)}
className='w-full max-w-full md:w-auto'
>
<TabsList className='flex *:flex-auto'>
@@ -133,7 +134,7 @@ export default function Content() {
icon={`${
{
windows: 'mdi:microsoft-windows',
macos: 'uil:apple',
mac: 'uil:apple',
linux: 'uil:linux',
ios: 'simple-icons:ios',
android: 'uil:android',
@@ -324,19 +325,18 @@ export default function Content() {
<div className='grid grid-cols-2 gap-4 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6'>
{applications
?.filter((application) => {
const platformApps = application.platform?.[platform];
return platformApps && platformApps.length > 0;
// 检查当前平台是否有下载链接或者有 scheme
if (!application.download_link && !application.scheme) return false;
return (
!!application.download_link?.[platform] || !!application.scheme
);
})
.map((application) => {
const platformApps = application.platform?.[platform];
const app =
platformApps?.find((item) => item.is_default) ||
platformApps?.[0];
if (!app) return null;
const downloadUrl = application.download_link?.[platform];
const handleCopy = (text: string, result: boolean) => {
if (result) {
const href = getAppSubLink(application.subscribe_type, url);
const href = getAppSubLink(url, application.scheme);
const showSuccessMessage = () => {
toast.success(
<>
@@ -379,23 +379,32 @@ export default function Content() {
/>
)}
<div className='flex'>
<Button
size='sm'
variant='secondary'
className='rounded-r-none px-1.5'
asChild
>
<Link href={app.url}>{t('download')}</Link>
</Button>
<CopyToClipboard
text={getAppSubLink(application.subscribe_type, url) || url}
onCopy={handleCopy}
>
<Button size='sm' className='rounded-l-none p-2'>
{t('import')}
{downloadUrl && (
<Button
size='sm'
variant='secondary'
className={
application.scheme ? 'rounded-r-none px-1.5' : 'px-1.5'
}
asChild
>
<Link href={downloadUrl}>{t('download')}</Link>
</Button>
</CopyToClipboard>
)}
{application.scheme && (
<CopyToClipboard
text={getAppSubLink(url, application.scheme)}
onCopy={handleCopy}
>
<Button
size='sm'
className={downloadUrl ? 'rounded-l-none p-2' : 'p-2'}
>
{t('import')}
</Button>
</CopyToClipboard>
)}
</div>
</div>
);
+42 -39
View File
@@ -10,7 +10,7 @@ export interface GlobalStore {
setUser: (user?: API.User) => void;
getUserInfo: () => Promise<void>;
getUserSubscribe: (uuid: string, type?: string) => string[];
getAppSubLink: (type: string, url: string) => string;
getAppSubLink: (url: string, schema?: string) => string;
}
export const useGlobalStore = create<GlobalStore>((set, get) => ({
@@ -107,45 +107,48 @@ export const useGlobalStore = create<GlobalStore>((set, get) => ({
}
});
},
getAppSubLink: (type: string, url: string) => {
getAppSubLink: (url: string, schema?: string) => {
const name = get().common?.site?.site_name || '';
switch (type) {
case 'Clash':
return `clash://install-config?url=${url}&name=${name}`;
case 'Hiddify':
return `hiddify://import/${url}#${name}`;
case 'Loon':
return `loon://import?sub=${encodeURIComponent(url)}`;
case 'NekoBox':
return `sn://subscription?url=${url}&name=${name}`;
case 'NekoRay':
return `sn://subscription?url=${url}&name=${name}`;
// case 'Netch':
// return ``;
case 'Quantumult X':
return `quantumult-x://add-resource?remote-resource=${encodeURIComponent(
JSON.stringify({
server_remote: [`${url}, tag=${name}`],
}),
)}`;
case 'Shadowrocket':
return `shadowrocket://add/sub://${window.btoa(url)}?remark=${encodeURIComponent(name)}`;
case 'Singbox':
return `sing-box://import-remote-profile?url=${encodeURIComponent(url)}#${name}`;
case 'Surfboard':
return `surfboard:///install-config?url=${encodeURIComponent(url)}`;
case 'Surge':
return `surge:///install-config?url=${encodeURIComponent(url)}`;
case 'V2box':
return `v2box://install-sub?url=${encodeURIComponent(url)}&name=${name}`;
// case 'V2rayN':
// return `v2rayn://install-sub?url=${encodeURIComponent(url)}&name=${name}`;
case 'V2rayNg':
return `v2rayng://install-sub?url=${encodeURIComponent(url)}#${name}`;
case 'Stash':
return `stash://install-config?url=${encodeURIComponent(url)}&name=${name}`;
default:
return '';
if (!schema) return url;
try {
let result = schema;
result = result.replace(/\${url}/g, url);
result = result.replace(/\${name}/g, name);
result = result.replace(/\${encodeURIComponent\(([^)]+)\)}/g, (match, expr) => {
if (expr === 'url') return encodeURIComponent(url);
if (expr === 'name') return encodeURIComponent(name);
return match;
});
result = result.replace(/\${window\.btoa\(([^)]+)\)}/g, (match, expr) => {
const btoa = typeof window !== 'undefined' ? window.btoa : (str: string) => str;
if (expr === 'url') return btoa(url);
if (expr === 'name') return btoa(name);
return match;
});
result = result.replace(/\${JSON\.stringify\(([^}]+)\)}/g, (match, expr) => {
try {
const processedExpr = expr.replace(/url/g, `"${url}"`).replace(/name/g, `"${name}"`);
if (processedExpr.includes('server_remote')) {
const serverRemoteValue = `${url}, tag=${name}`;
return JSON.stringify({ server_remote: [serverRemoteValue] });
}
const result = eval(`(${processedExpr})`);
return JSON.stringify(result);
} catch {
return match;
}
});
return result;
} catch (error) {
return url;
}
},
}));
+12 -1
View File
@@ -1,5 +1,5 @@
// @ts-ignore
/* eslint-disable */
import request from '@/utils/request';
/** Get Ads GET /v1/common/ads */
@@ -43,6 +43,17 @@ export async function checkVerificationCode(
);
}
/** Get Client GET /v1/common/client */
export async function getClient(options?: { [key: string]: any }) {
return request<API.Response & { data?: API.GetSubscribeApplicationListResponse }>(
'/v1/common/client',
{
method: 'GET',
...(options || {}),
},
);
}
/** Get verification code POST /v1/common/send_code */
export async function sendEmailCode(body: API.SendCodeRequest, options?: { [key: string]: any }) {
return request<API.Response & { data?: API.SendCodeResponse }>('/v1/common/send_code', {
+30
View File
@@ -213,6 +213,15 @@ declare namespace API {
updated_at: number;
};
type DownloadLink = {
ios?: string;
android?: string;
windows?: string;
mac?: string;
linux?: string;
harmony?: string;
};
type EmailAuthticateConfig = {
enable: boolean;
enable_verify: boolean;
@@ -284,6 +293,11 @@ declare namespace API {
protocol: string[];
};
type GetSubscribeApplicationListResponse = {
total: number;
list: SubscribeApplication[];
};
type GetTosResponse = {
tos_content: string;
};
@@ -765,6 +779,22 @@ declare namespace API {
updated_at: number;
};
type SubscribeApplication = {
id: number;
name: string;
description?: string;
icon?: string;
scheme?: string;
user_agent: string;
is_default: boolean;
proxy_template: string;
template: string;
output_format: string;
download_link?: DownloadLink;
created_at: number;
updated_at: number;
};
type SubscribeConfig = {
single_model: boolean;
subscribe_path: string;
+30
View File
@@ -214,6 +214,15 @@ declare namespace API {
updated_at: number;
};
type DownloadLink = {
ios?: string;
android?: string;
windows?: string;
mac?: string;
linux?: string;
harmony?: string;
};
type EmailAuthticateConfig = {
enable: boolean;
enable_verify: boolean;
@@ -266,6 +275,11 @@ declare namespace API {
methods: UserAuthMethod[];
};
type GetSubscribeApplicationListResponse = {
total: number;
list: SubscribeApplication[];
};
type GetSubscribeLogParams = {
page: number;
size: number;
@@ -863,6 +877,22 @@ declare namespace API {
updated_at: number;
};
type SubscribeApplication = {
id: number;
name: string;
description?: string;
icon?: string;
scheme?: string;
user_agent: string;
is_default: boolean;
proxy_template: string;
template: string;
output_format: string;
download_link?: DownloadLink;
created_at: number;
updated_at: number;
};
type SubscribeConfig = {
single_model: boolean;
subscribe_path: string;