Skip to content

Commit 0ca9ff8

Browse files
authored
feat: refactor app-download hook to support binding (#7)
Signed-off-by: Ryan Wang <i@ryanc.cc>
1 parent 527e902 commit 0ca9ff8

11 files changed

Lines changed: 365 additions & 121 deletions

File tree

console/postcss.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ module.exports = {
33
tailwindcss: {},
44
autoprefixer: {},
55
},
6-
}
6+
};
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<script lang="ts" setup>
2+
import { useAppCompare } from "@/composables/use-app-compare";
3+
import { useAppDownload } from "@/composables/use-app-download";
4+
import type { ApplicationSearchResult } from "@/types";
5+
import { VButton } from "@halo-dev/components";
6+
import { computed, toRefs } from "vue";
7+
8+
const props = withDefaults(
9+
defineProps<{
10+
app?: ApplicationSearchResult;
11+
size?: string;
12+
}>(),
13+
{
14+
app: undefined,
15+
size: "default",
16+
}
17+
);
18+
19+
const { app } = toRefs(props);
20+
21+
const { installing, handleInstall } = useAppDownload(app);
22+
const { isSatisfies, hasInstalled } = useAppCompare(app);
23+
24+
const actions = computed(() => {
25+
return [
26+
{
27+
label: installing?.value ? "安装中" : "安装",
28+
type: "default",
29+
available:
30+
!hasInstalled.value && isSatisfies.value && app.value?.application.spec.priceConfig?.mode !== "ONE_TIME",
31+
onClick: handleInstall,
32+
loading: installing?.value,
33+
disabled: false,
34+
},
35+
{
36+
label: `¥${(app.value?.application.spec.priceConfig?.oneTimePrice || 0) / 100}`,
37+
type: "default",
38+
// TODO: 需要判断是否已经购买
39+
available: app.value?.application.spec.priceConfig?.mode === "ONE_TIME" && !hasInstalled.value,
40+
onClick: () => {
41+
window.open(`https://halo.run/store/apps/${app.value?.application.metadata.name}/buy`);
42+
},
43+
loading: false,
44+
disabled: false,
45+
},
46+
{
47+
label: "已安装",
48+
type: "default",
49+
available: hasInstalled.value,
50+
onClick: undefined,
51+
loading: false,
52+
disabled: true,
53+
},
54+
{
55+
label: "版本不兼容",
56+
type: "default",
57+
available: !isSatisfies.value && !hasInstalled.value,
58+
onClick: undefined,
59+
loading: false,
60+
disabled: true,
61+
},
62+
];
63+
});
64+
65+
const action = computed(() => {
66+
return actions.value.find((action) => action.available);
67+
});
68+
</script>
69+
70+
<template>
71+
<VButton
72+
v-if="action"
73+
:size="size"
74+
:type="action.type"
75+
:disabled="action.disabled"
76+
:loading="action.loading"
77+
@click="action.onClick"
78+
>
79+
{{ action.label }}
80+
</VButton>
81+
</template>

console/src/components/AppCard.vue

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
<script lang="ts" setup>
2-
import { useAppControl } from "@/composables/use-app-control";
32
import type { ApplicationSearchResult } from "@/types";
43
import { relativeTimeTo } from "@/utils/date";
5-
import { VButton } from "@halo-dev/components";
64
import { toRefs } from "vue";
75
import { computed } from "vue";
86
import { prependDomain } from "@/utils/resource";
97
import AppVersionCheckBar from "./AppVersionCheckBar.vue";
8+
import AppActionButton from "./AppActionButton.vue";
109
1110
const props = withDefaults(
1211
defineProps<{
@@ -40,8 +39,6 @@ const vendor = computed(() => {
4039
logo: props.app.owner.avatar,
4140
};
4241
});
43-
44-
const { action } = useAppControl(app);
4542
</script>
4643

4744
<template>
@@ -155,16 +152,7 @@ const { action } = useAppControl(app);
155152
</span>
156153
</div>
157154
<div>
158-
<VButton
159-
v-if="action"
160-
size="sm"
161-
:type="action.type"
162-
:disabled="action.disabled"
163-
:loading="action.loading"
164-
@click="action.onClick"
165-
>
166-
{{ action.label }}
167-
</VButton>
155+
<AppActionButton :app="app" size="sm" />
168156
</div>
169157
</div>
170158
</div>

console/src/components/AppDetailModal.vue

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
<script lang="ts" setup>
2-
import { IconLink, VButton, VLoading, VModal, VSpace, VTabItem, VTabs } from "@halo-dev/components";
2+
import { IconLink, VLoading, VModal, VSpace, VTabItem, VTabs } from "@halo-dev/components";
33
import { useQuery } from "@tanstack/vue-query";
44
import { ref } from "vue";
55
import { computed } from "vue";
66
import { toRefs } from "vue";
77
import { prependDomain } from "@/utils/resource";
88
import type { ApplicationSearchResult } from "@/types";
99
import storeApiClient from "@/utils/store-api-client";
10-
import { useAppControl } from "@/composables/use-app-control";
1110
import DetailSidebar from "./detail/DetailSidebar.vue";
1211
import DetailReadme from "./detail/DetailReadme.vue";
1312
import DetailReleases from "./detail/DetailReleases.vue";
1413
import AppVersionCheckBar from "./AppVersionCheckBar.vue";
14+
import AppActionButton from "./AppActionButton.vue";
15+
1516
const props = withDefaults(
1617
defineProps<{
1718
visible: boolean;
@@ -65,8 +66,6 @@ const title = computed(() => {
6566
});
6667
6768
const activeId = ref(props.tab);
68-
69-
const { action } = useAppControl(app);
7069
</script>
7170

7271
<template>
@@ -141,15 +140,7 @@ const { action } = useAppControl(app);
141140
</div>
142141
<template #footer>
143142
<VSpace>
144-
<VButton
145-
v-if="action"
146-
:type="action.type"
147-
:disabled="action.disabled"
148-
:loading="action.loading"
149-
@click="action.onClick"
150-
>
151-
{{ action.label }}
152-
</VButton>
143+
<AppActionButton :app="app" />
153144
<AppVersionCheckBar :app="app" />
154145
</VSpace>
155146
</template>

console/src/components/AppVersionCheckBar.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script lang="ts" setup>
22
import AppDetailModal from "./AppDetailModal.vue";
33
import { nextTick, ref, toRefs } from "vue";
4-
import { useAppControl } from "@/composables/use-app-control";
4+
import { useAppDownload } from "@/composables/use-app-download";
55
import RiArrowUpCircleLine from "~icons/ri/arrow-up-circle-line";
66
import type { ApplicationSearchResult } from "@/types";
77
import { useAppCompare } from "@/composables/use-app-compare";
@@ -19,7 +19,7 @@ const { app } = toRefs(props);
1919
2020
const { hasUpdate, isSatisfies } = useAppCompare(app);
2121
22-
const { upgrading, handleUpgrade } = useAppControl(app);
22+
const { upgrading, handleUpgrade } = useAppDownload(app);
2323
2424
const detailModal = ref(false);
2525
const detailModalVisible = ref(false);

console/src/components/ThemeOrPluginVersionCheckBar.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { Plugin, Theme } from "@halo-dev/api-client";
33
import AppDetailModal from "./AppDetailModal.vue";
44
import { nextTick, ref, toRefs } from "vue";
55
import { usePluginVersion } from "@/composables/use-plugin-version";
6-
import { useAppControl } from "@/composables/use-app-control";
6+
import { useAppDownload } from "@/composables/use-app-download";
77
import RiArrowUpCircleLine from "~icons/ri/arrow-up-circle-line";
88
import { useThemeVersion } from "@/composables/use-theme-version";
99
import { useAppCompare } from "@/composables/use-app-compare";
@@ -38,7 +38,7 @@ function useVersion() {
3838
3939
const { hasUpdate, matchedApp } = useVersion();
4040
const { isSatisfies } = useAppCompare(matchedApp);
41-
const { upgrading, handleUpgrade } = useAppControl(matchedApp);
41+
const { upgrading, handleUpgrade } = useAppDownload(matchedApp);
4242
4343
const detailModal = ref(false);
4444
const detailModalVisible = ref(false);

console/src/components/detail/DetailReleaseItem.vue

Lines changed: 154 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
<script lang="ts" setup>
2-
import { useAppControl } from "@/composables/use-app-control";
3-
import type { ApplicationSearchResult, ReleaseDetail } from "@/types";
2+
import { useAppCompare } from "@/composables/use-app-compare";
3+
import { useAppDownload } from "@/composables/use-app-download";
4+
import { useHaloVersion } from "@/composables/use-halo-version";
5+
import type { ApplicationReleaseAsset, ApplicationSearchResult, ReleaseDetail } from "@/types";
46
import { relativeTimeTo } from "@/utils/date";
57
import prettyBytes from "pretty-bytes";
6-
import { toRefs } from "vue";
8+
import { computed, toRefs } from "vue";
79
import TablerCloudDownload from "~icons/tabler/cloud-download";
10+
import semver from "semver";
11+
import { useMutation } from "@tanstack/vue-query";
12+
import { apiClient } from "@/utils/api-client";
13+
import { Dialog, Toast } from "@halo-dev/components";
14+
import type { PluginInstallationErrorResponse, ThemeInstallationErrorResponse } from "@/types/core";
15+
import type { AxiosError } from "axios";
816
917
const props = withDefaults(
1018
defineProps<{
@@ -18,7 +26,145 @@ const props = withDefaults(
1826
1927
const { app } = toRefs(props);
2028
21-
const { handleInstall, installing } = useAppControl(app);
29+
const { haloVersion } = useHaloVersion();
30+
const {
31+
checkPluginUpgradeStatus,
32+
checkThemeUpgradeStatus,
33+
handleBindingPluginAppId,
34+
handleBindingThemeAppId,
35+
handleClearQueryCache,
36+
handleForceUpgradePlugin,
37+
handleForceUpgradeTheme,
38+
} = useAppDownload(app);
39+
40+
const { matchedPlugin, matchedTheme, appType, hasInstalled: appHasInstalled } = useAppCompare(app);
41+
42+
const hasInstalled = computed(() => {
43+
if (appType.value === "PLUGIN") {
44+
return matchedPlugin.value?.spec.version === props.release.release.spec.version;
45+
}
46+
if (appType.value === "THEME") {
47+
return matchedTheme.value?.spec.version === props.release.release.spec.version;
48+
}
49+
return false;
50+
});
51+
52+
const isSatisfies = computed(() => {
53+
const { requires } = props.release.release.spec;
54+
if (!haloVersion.value || !requires) return false;
55+
return semver.satisfies(haloVersion.value, requires, { includePrerelease: true });
56+
});
57+
58+
function getDownloadUrl(asset: ApplicationReleaseAsset) {
59+
return `${import.meta.env.VITE_APP_STORE_BACKEND}/store/apps/${
60+
app.value?.application.metadata.name
61+
}/releases/download/${props.release.release.metadata.name}/assets/${asset.metadata.name}`;
62+
}
63+
64+
const { isLoading: installing, mutate: handleInstall } = useMutation({
65+
mutationKey: ["install-app-from-release"],
66+
mutationFn: async ({ asset }: { asset: ApplicationReleaseAsset }) => {
67+
const { version: releaseVersion } = props.release.release.spec;
68+
const { version: currentVersion } = matchedPlugin.value?.spec || matchedTheme.value?.spec || {};
69+
70+
const downloadUrl = getDownloadUrl(asset);
71+
72+
if (appType.value === "PLUGIN") {
73+
if (appHasInstalled.value) {
74+
if (semver.gt(releaseVersion || "*", currentVersion || "*")) {
75+
await handleForceUpgradePlugin(
76+
matchedPlugin.value?.metadata.name as string,
77+
downloadUrl,
78+
props.release.release.spec.version
79+
);
80+
} else {
81+
Dialog.warning({
82+
title: "当前已安装较新的版本",
83+
description: "确定要安装一个旧版本吗?",
84+
async onConfirm() {
85+
await handleForceUpgradePlugin(
86+
matchedPlugin.value?.metadata.name as string,
87+
downloadUrl,
88+
props.release.release.spec.version
89+
);
90+
},
91+
});
92+
}
93+
} else {
94+
const { data: plugin } = await apiClient.plugin.installPluginFromUri({
95+
installFromUriRequest: { uri: downloadUrl },
96+
});
97+
if (await checkPluginUpgradeStatus(plugin, props.release.release.spec.version)) {
98+
await handleBindingPluginAppId({ plugin: plugin });
99+
Toast.success("安装成功");
100+
handleClearQueryCache();
101+
}
102+
}
103+
return;
104+
}
105+
106+
if (appType.value === "THEME") {
107+
if (appHasInstalled.value) {
108+
if (semver.gt(releaseVersion || "*", currentVersion || "*")) {
109+
await handleForceUpgradeTheme(
110+
matchedTheme.value?.metadata.name as string,
111+
downloadUrl,
112+
props.release.release.spec.version
113+
);
114+
} else {
115+
Dialog.warning({
116+
title: "当前已安装较新的版本",
117+
description: "确定要安装一个旧版本吗?",
118+
async onConfirm() {
119+
await handleForceUpgradeTheme(
120+
matchedTheme.value?.metadata.name as string,
121+
downloadUrl,
122+
props.release.release.spec.version
123+
);
124+
},
125+
});
126+
}
127+
} else {
128+
const { data: theme } = await apiClient.theme.installThemeFromUri({
129+
installFromUriRequest: { uri: downloadUrl },
130+
});
131+
if (await checkThemeUpgradeStatus(theme, props.release.release.spec.version)) {
132+
await handleBindingThemeAppId({ theme });
133+
Toast.success("安装成功");
134+
handleClearQueryCache();
135+
}
136+
}
137+
}
138+
},
139+
onError(error: AxiosError<PluginInstallationErrorResponse | ThemeInstallationErrorResponse>, variables) {
140+
Dialog.warning({
141+
title: `当前${appType.value === "PLUGIN" ? "插件" : "主题"}已经安装,是否重新安装?`,
142+
description:
143+
"请确认当前安装的应用是否和已存在的应用一致,重新安装之后会记录应用的安装来源,后续可以通过应用市场进行升级。",
144+
onConfirm: async () => {
145+
if (!error.response?.data) {
146+
return;
147+
}
148+
149+
const downloadUrl = getDownloadUrl(variables.asset);
150+
151+
if ("pluginName" in error.response.data) {
152+
await handleForceUpgradePlugin(
153+
error.response.data.pluginName,
154+
downloadUrl,
155+
props.release.release.spec.version
156+
);
157+
return;
158+
}
159+
160+
if ("themeName" in error.response.data) {
161+
await handleForceUpgradeTheme(error.response.data.themeName, downloadUrl, props.release.release.spec.version);
162+
return;
163+
}
164+
},
165+
});
166+
},
167+
});
22168
</script>
23169

24170
<template>
@@ -76,10 +222,13 @@ const { handleInstall, installing } = useAppControl(app);
76222
</span>
77223
</div>
78224
<div>
225+
<span v-if="hasInstalled" class="as-text-sm as-text-gray-600"> 已安装 </span>
226+
<span v-else-if="!isSatisfies" class="as-text-sm as-text-gray-600"> 不兼容 </span>
79227
<span
228+
v-else
80229
class="as-text-sm as-text-blue-600 hover:as-text-blue-500"
81230
:class="{ 'as-pointer-events-none': installing }"
82-
@click="handleInstall()"
231+
@click="handleInstall({ asset })"
83232
>
84233
{{ installing ? "安装中" : "安装" }}
85234
</span>

0 commit comments

Comments
 (0)