Skip to content

Commit f1cd8f2

Browse files
committed
feat: 优化导出订阅/文件逻辑
1 parent 8bfdcd4 commit f1cd8f2

7 files changed

Lines changed: 113 additions & 61 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "sub-store-front-end",
3-
"version": "2.29.3",
3+
"version": "2.29.4",
44
"private": true,
55
"packageManager": "pnpm@11.0.9",
66
"scripts": {

src/api/files/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ export function useFilesApi() {
2727
method: 'get',
2828
});
2929
},
30+
exportFile: (name: string): AxiosPromise<Blob> => {
31+
return request({
32+
url: `/api/wholeFile/${encodeURIComponent(name)}`,
33+
method: 'get',
34+
params: { raw: 1 },
35+
responseType: 'blob',
36+
});
37+
},
3038
createFile: (data: any): AxiosPromise<MyAxiosRes> => {
3139
return request({
3240
url: '/api/files',

src/api/subs/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ export function useSubsApi() {
2121
method: 'get',
2222
});
2323
},
24+
exportOne: (type: 'sub' | 'collection', name: string): AxiosPromise<Blob> => {
25+
return request({
26+
url: `/api/${type}/${encodeURIComponent(name)}`,
27+
method: 'get',
28+
params: { raw: 1 },
29+
responseType: 'blob',
30+
});
31+
},
2432
downloadOne: (name: string, params?: any): AxiosPromise<MyAxiosRes> => {
2533
return request({
2634
url: `/download/${encodeURIComponent(name)}`,

src/components/FileListItem.vue

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -162,10 +162,6 @@
162162
</nut-button>
163163
</div>
164164
<div class="sub-item-swipe-btn-wrapper">
165-
<!-- <a
166-
:href="`${host}/api/wholeFile/${encodeURIComponent(name)}?raw=1`"
167-
target="_blank"
168-
> -->
169165
<nut-button
170166
shape="square"
171167
type="success"
@@ -272,6 +268,7 @@
272268
import clashmetaIcon from '@/assets/icons/clashmeta_color.png';
273269
import { isMihomoConfigFileType } from "@/utils/fileType";
274270
import { formatPreviewError } from "@/utils/previewError";
271+
import { downloadBlobResponse } from '@/utils/download';
275272
276273
const { copy, isSupported } = useClipboard();
277274
const { toClipboard: copyFallback } = useV3Clipboard();
@@ -476,10 +473,6 @@
476473
}
477474
};
478475
479-
const swipeClose = () => {
480-
swipe.value.close();
481-
};
482-
483476
const swipeController = () => {
484477
if (swipeIsOpen.value) {
485478
swipe.value.close();
@@ -620,10 +613,26 @@
620613
return env.value?.feature?.share;
621614
});
622615
623-
const onClickExportFile = (name) => {
624-
const url = `${host.value}/api/wholeFile/${encodeURIComponent(name)}?raw=1`;
625-
console.log('url', url);
626-
window.open(url, '_blank'); // 在新窗口中打开链接
616+
const onClickExportFile = async (name: string) => {
617+
Toast.loading(t('subPage.exportConfigNotify.loading'), { id: 'exportConfig' });
618+
try {
619+
downloadBlobResponse(
620+
await filesApi.exportFile(name),
621+
`sub-store_file_${name}.json`,
622+
);
623+
showNotify({ title: t('subPage.exportConfigNotify.succeed') });
624+
} catch (e) {
625+
console.error(e);
626+
showNotify({
627+
type: 'danger',
628+
title: t('subPage.exportConfigNotify.failed', {
629+
e: e instanceof Error ? e.message : e,
630+
}),
631+
});
632+
} finally {
633+
Toast.hide('exportConfig');
634+
closeExpandedMenu();
635+
}
627636
};
628637
629638
const onClickShareLink = async () => {

src/components/SubListItem.vue

Lines changed: 36 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -247,14 +247,14 @@
247247
</nut-button>
248248
</div>
249249
<div class="sub-item-swipe-btn-wrapper">
250-
<a
251-
:href="`${host}/api/${props.type}/${encodeURIComponent(name)}?raw=1`"
252-
target="_blank"
250+
<nut-button
251+
shape="square"
252+
type="success"
253+
class="sub-item-swipe-btn"
254+
@click="onClickExport"
253255
>
254-
<nut-button shape="square" type="success" class="sub-item-swipe-btn">
255-
<font-awesome-icon icon="fa-solid fa-file-export" />
256-
</nut-button>
257-
</a>
256+
<font-awesome-icon icon="fa-solid fa-file-export" />
257+
</nut-button>
258258
</div>
259259
<!-- preview -->
260260
<!-- <div class="sub-item-swipe-btn-wrapper">
@@ -287,14 +287,14 @@
287287
</nut-button>
288288
</div>
289289
<div class="sub-item-swipe-btn-wrapper">
290-
<a
291-
:href="`${host}/api/${props.type}/${encodeURIComponent(name)}?raw=1`"
292-
target="_blank"
290+
<nut-button
291+
shape="square"
292+
type="success"
293+
class="sub-item-swipe-btn"
294+
@click="onClickExport"
293295
>
294-
<nut-button shape="square" type="success" class="sub-item-swipe-btn">
295-
<font-awesome-icon icon="fa-solid fa-file-export" />
296-
</nut-button>
297-
</a>
296+
<font-awesome-icon icon="fa-solid fa-file-export" />
297+
</nut-button>
298298
</div>
299299
<div class="sub-item-swipe-btn-wrapper">
300300
<nut-button
@@ -345,6 +345,7 @@ import { createGithubProxyUrlRewriter } from "@/utils/githubProxy";
345345
import { resolveImageFit } from "@/utils/iconFit";
346346
import { isMobile } from "@/utils/isMobile";
347347
import { openManagedDeleteDialog } from "@/utils/archive";
348+
import { downloadBlobResponse } from "@/utils/download";
348349
import CompareTable from "@/views/CompareTable.vue";
349350
350351
const props = defineProps<{
@@ -924,26 +925,27 @@ const onClickCopyConfig = async () => {
924925
showNotify({ title: t("subPage.copyConfigNotify.succeed") });
925926
closeExpandedMenu();
926927
};
927-
// const onClickExport = async () => {
928-
// swipeController()
929-
// let data: Sub | Collection;
930-
// switch (props.type) {
931-
// case "sub":
932-
// data = JSON.parse(JSON.stringify(toRaw(props.sub)));
933-
// break;
934-
// case "collection":
935-
// data = JSON.parse(JSON.stringify(toRaw(props.collection)));
936-
// break;
937-
// }
938-
// data.name += `-exportedAt${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}`;
939-
940-
// Toast.loading(t("subPage.copyConfigNotify.loading"), { id: "exportConfig" });
941-
// // await subsApi.createSub(props.type + "s", data);
942-
// // await subsStore.fetchSubsData();
943-
// Toast.hide("exportConfig");
944-
// showNotify({ title: t("subPage.copyConfigNotify.succeed") });
945-
// swipe.value.close();
946-
// };
928+
const onClickExport = async () => {
929+
Toast.loading(t("subPage.exportConfigNotify.loading"), { id: "exportConfig" });
930+
try {
931+
downloadBlobResponse(
932+
await subsApi.exportOne(props.type, name),
933+
`sub-store_${props.type}_${name}.json`,
934+
);
935+
showNotify({ title: t("subPage.exportConfigNotify.succeed") });
936+
} catch (e) {
937+
console.error(e);
938+
showNotify({
939+
type: "danger",
940+
title: t("subPage.exportConfigNotify.failed", {
941+
e: e instanceof Error ? e.message : e,
942+
}),
943+
});
944+
} finally {
945+
Toast.hide("exportConfig");
946+
closeExpandedMenu();
947+
}
948+
};
947949
948950
const onClickEdit = () => {
949951
router.push(`/edit/${props.type}s/${encodeURIComponent(name)}`);

src/utils/download.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
const getFilename = (disposition: string | null, fallback: string) => {
2+
const encoded = disposition?.match(/filename\*?=(?:UTF-8''|")?([^";]+)/i)?.[1];
3+
if (!encoded) return fallback;
4+
5+
try {
6+
return decodeURIComponent(encoded);
7+
} catch {
8+
return encoded;
9+
}
10+
};
11+
12+
type BlobResponse = {
13+
data: Blob;
14+
status: number;
15+
headers?: Record<string, string | undefined>;
16+
};
17+
18+
export const downloadBlobResponse = (
19+
response: BlobResponse | undefined,
20+
fallbackFilename: string,
21+
) => {
22+
if (!response || response.status < 200 || response.status >= 300) {
23+
throw new Error(`HTTP ${response?.status || 0}`);
24+
}
25+
26+
const objectUrl = URL.createObjectURL(response.data);
27+
const link = document.createElement('a');
28+
link.href = objectUrl;
29+
link.download = getFilename(
30+
response.headers?.['content-disposition'] || null,
31+
fallbackFilename,
32+
);
33+
document.body.appendChild(link);
34+
link.click();
35+
link.remove();
36+
setTimeout(() => URL.revokeObjectURL(objectUrl), 1000);
37+
};

src/views/My.vue

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -683,6 +683,7 @@ import { useAppNotifyStore } from "@/store/appNotify";
683683
import { useGlobalStore } from "@/store/global";
684684
import { useSettingsStore } from "@/store/settings";
685685
import { butifyDate } from "@/utils/butifyDate";
686+
import { downloadBlobResponse } from "@/utils/download";
686687
import { createGithubProxyUrlRewriter } from "@/utils/githubProxy";
687688
import { initStores } from "@/utils/initApp";
688689
import { storeToRefs } from "pinia";
@@ -1149,20 +1150,7 @@ const downloadBackup = async () => {
11491150
backupIsLoading.value = true;
11501151
try {
11511152
const res = await useSettingsApi().downloadBackup();
1152-
if (!res || res.status < 200 || res.status >= 300) throw new Error('backup failed');
1153-
1154-
const disposition = res.headers?.['content-disposition'] || '';
1155-
const filename = decodeURIComponent(
1156-
disposition.match(/filename="?([^";]+)"?/i)?.[1] || 'sub-store_data.json',
1157-
);
1158-
const url = URL.createObjectURL(new Blob([res.data], { type: 'application/octet-stream' }));
1159-
const link = document.createElement('a');
1160-
link.href = url;
1161-
link.download = filename;
1162-
document.body.appendChild(link);
1163-
link.click();
1164-
link.remove();
1165-
setTimeout(() => URL.revokeObjectURL(url), 1000);
1153+
downloadBlobResponse(res, 'sub-store_data.json');
11661154
showNotify({
11671155
type: "success",
11681156
title: t(`myPage.notify.download.succeed`),

0 commit comments

Comments
 (0)