fix(account): polish sidebar favorites and sync feedback

This commit is contained in:
2026-05-19 23:48:41 +08:00
parent 932e69fca7
commit 8d80a02316
18 changed files with 403 additions and 56 deletions
+11 -4
View File
@@ -129,7 +129,7 @@
import { computed, ref, watch } from "vue";
import type { SyncedAppListItem } from "@/global/typedefinition";
import { cloudItemKey } from "@/modules/appListSync";
import { cloudItemKey, cloudPackageKey } from "@/modules/appListSync";
const props = defineProps<{
show: boolean;
@@ -137,6 +137,7 @@ const props = defineProps<{
error: string;
items: SyncedAppListItem[];
installedKeys: Set<string>;
installedPackageKeys?: Set<string>;
}>();
const emit = defineEmits<{
@@ -147,7 +148,8 @@ const emit = defineEmits<{
const selectedKeys = ref<Set<string>>(new Set());
const isInstalled = (item: SyncedAppListItem): boolean =>
props.installedKeys.has(cloudItemKey(item));
props.installedKeys.has(cloudItemKey(item)) ||
Boolean(props.installedPackageKeys?.has(cloudPackageKey(item)));
const selectedItems = computed(() =>
props.items.filter(
@@ -157,7 +159,12 @@ const selectedItems = computed(() =>
const pruneSelectedKeys = (): void => {
selectedKeys.value = new Set(
[...selectedKeys.value].filter((key) => !props.installedKeys.has(key)),
[...selectedKeys.value].filter((key) => {
const item = props.items.find(
(candidate) => cloudItemKey(candidate) === key,
);
return item ? !isInstalled(item) : !props.installedKeys.has(key);
}),
);
};
@@ -179,7 +186,7 @@ watch(
);
watch(
() => props.installedKeys,
() => [props.installedKeys, props.installedPackageKeys] as const,
() => {
pruneSelectedKeys();
},
+24 -8
View File
@@ -20,24 +20,24 @@
:alt="accountLabel"
class="h-11 w-11 rounded-2xl object-cover shadow-sm ring-1 ring-slate-900/5"
/>
<div class="flex flex-col">
<div data-testid="account-text" class="flex min-w-0 flex-col">
<span
class="text-xs uppercase tracking-[0.3em] text-slate-500 dark:text-slate-400"
class="truncate text-xs uppercase tracking-[0.3em] text-slate-500 dark:text-slate-400"
>{{ currentUser ? currentUser.forumLevel : "Spark Store" }}</span
>
<span
class="text-lg font-semibold text-slate-900 dark:text-white"
class="truncate text-lg font-semibold text-slate-900 dark:text-white"
>{{ accountLabel }}</span
>
</div>
</button>
<AccountQuickMenu
v-if="currentUser && showAccountMenu"
@open-user-management="emit('open-user-management')"
@open-favorites="emit('open-favorites')"
@open-forum="emit('open-forum')"
@edit-profile="emit('edit-profile')"
@logout="emit('logout')"
@open-user-management="emitAccountAction('open-user-management')"
@open-favorites="emitAccountAction('open-favorites')"
@open-forum="emitAccountAction('open-forum')"
@edit-profile="emitAccountAction('edit-profile')"
@logout="emitAccountAction('logout')"
/>
</div>
<div class="flex items-center gap-1">
@@ -177,6 +177,22 @@ const handleAccountClick = () => {
showAccountMenu.value = !showAccountMenu.value;
};
const emitAccountAction = (
action:
| "open-user-management"
| "open-favorites"
| "open-forum"
| "edit-profile"
| "logout",
) => {
showAccountMenu.value = false;
if (action === "open-user-management") emit("open-user-management");
else if (action === "open-favorites") emit("open-favorites");
else if (action === "open-forum") emit("open-forum");
else if (action === "edit-profile") emit("edit-profile");
else emit("logout");
};
const toggleTheme = () => {
emit("toggle-theme");
};
+10 -6
View File
@@ -8,7 +8,9 @@
@click="selectCategory('all')"
>
<span>全部</span>
<span v-if="totalCount > 0" class="category-pill-count">{{ totalCount }}</span>
<span v-if="totalCount > 0" class="category-pill-count">{{
totalCount
}}</span>
</button>
<button
v-for="(category, key) in categories"
@@ -19,7 +21,9 @@
@click="selectCategory(key)"
>
<span>{{ category.zh }}</span>
<span v-if="categoryCounts[key]" class="category-pill-count">{{ categoryCounts[key] }}</span>
<span v-if="categoryCounts[key]" class="category-pill-count">{{
categoryCounts[key]
}}</span>
</button>
</div>
</div>
@@ -101,22 +105,22 @@ const selectCategory = (category: string) => {
}
.category-pill-active {
background: #0071e3;
background: #2b7fff;
color: #fff;
}
.category-pill-active:hover {
background: #0066cc;
background: #2b7fff;
color: #fff;
}
.dark .category-pill-active {
background: #409cff;
background: #2b7fff;
color: #fff;
}
.dark .category-pill-active:hover {
background: #0071e3;
background: #2b7fff;
color: #fff;
}
+40 -21
View File
@@ -48,7 +48,7 @@
>
当前收藏夹暂无应用
</div>
<label
<div
v-for="resolved in items"
:key="resolved.item.id"
class="flex items-center gap-3 rounded-2xl border border-slate-200 p-4 transition hover:bg-slate-50 dark:border-slate-800 dark:hover:bg-slate-800/60"
@@ -60,33 +60,45 @@
:value="resolved.item.id"
:aria-label="`选择 ${resolved.item.name || resolved.item.pkgname}`"
/>
<img
v-if="resolved.item.iconUrl"
:src="resolved.item.iconUrl"
alt=""
class="h-10 w-10 rounded-xl object-cover"
/>
<div
v-else
class="flex h-10 w-10 items-center justify-center rounded-xl bg-slate-100 text-sm font-semibold text-slate-500 dark:bg-slate-800"
<button
type="button"
class="flex min-w-0 flex-1 items-center gap-3 text-left"
:aria-label="`打开 ${resolved.item.name || resolved.item.pkgname} 详情`"
:disabled="!resolved.selectedApp"
@click="openFavoriteDetail(resolved)"
>
{{ (resolved.item.name || resolved.item.pkgname).slice(0, 1) }}
</div>
<div class="min-w-0 flex-1">
<p class="truncate font-medium text-slate-900 dark:text-white">
{{ resolved.item.name || resolved.item.pkgname }}
</p>
<p class="truncate text-xs text-slate-500 dark:text-slate-400">
{{ resolved.item.pkgname }} · {{ resolved.item.category }}
</p>
</div>
<img
v-if="resolved.item.iconUrl"
:src="resolved.item.iconUrl"
alt=""
class="h-10 w-10 rounded-xl object-cover"
/>
<span
v-else
class="flex h-10 w-10 items-center justify-center rounded-xl bg-slate-100 text-sm font-semibold text-slate-500 dark:bg-slate-800"
>
{{ (resolved.item.name || resolved.item.pkgname).slice(0, 1) }}
</span>
<span class="min-w-0 flex-1">
<span
class="block truncate font-medium text-slate-900 dark:text-white"
>
{{ resolved.item.name || resolved.item.pkgname }}
</span>
<span
class="block truncate text-xs text-slate-500 dark:text-slate-400"
>
{{ resolved.item.pkgname }} · {{ resolved.item.category }}
</span>
</span>
</button>
<span
class="rounded-full px-3 py-1 text-xs font-medium"
:class="statusClass(resolved.status)"
>
{{ resolved.reason }}
</span>
</label>
</div>
</div>
<div class="mt-6 flex flex-wrap gap-3">
@@ -121,6 +133,7 @@
import { computed, ref, watch } from "vue";
import type {
FavoriteAvailabilityStatus,
App,
FavoriteFolder,
ResolvedFavoriteItem,
} from "@/global/typedefinition";
@@ -138,6 +151,7 @@ const emit = defineEmits<{
"create-folder": [];
"remove-selected": [itemIds: number[]];
"install-selected": [items: ResolvedFavoriteItem[]];
"open-detail": [app: App];
}>();
const selectedIds = ref<number[]>([]);
@@ -163,6 +177,11 @@ const selectInstallable = () => {
.map((item) => item.item.id);
};
const openFavoriteDetail = (resolved: ResolvedFavoriteItem) => {
if (!resolved.selectedApp) return;
emit("open-detail", resolved.selectedApp);
};
const statusClass = (status: FavoriteAvailabilityStatus): string => {
if (status === "installable") {
return "bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300";
+15 -1
View File
@@ -18,6 +18,7 @@
</p>
<div class="mt-5 space-y-2">
<button
v-if="!hasDefaultFolder"
type="button"
class="w-full rounded-xl border border-slate-200 px-4 py-3 text-left text-sm font-medium text-slate-700 transition hover:bg-slate-50 dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800"
@click="emit('select-folder', 'default')"
@@ -34,6 +35,13 @@
{{ folder.name }}
</button>
</div>
<button
type="button"
class="mt-4 w-full rounded-xl border border-dashed border-slate-300 px-4 py-2 text-sm font-medium text-slate-600 transition hover:border-blue-400 hover:text-blue-600 dark:border-slate-700 dark:text-slate-300 dark:hover:border-blue-500 dark:hover:text-blue-300"
@click="emit('create-folder')"
>
新建收藏夹
</button>
<button
type="button"
class="mt-5 w-full rounded-xl bg-slate-900 px-4 py-2 text-sm font-medium text-white transition hover:bg-slate-700 dark:bg-slate-100 dark:text-slate-900"
@@ -46,15 +54,21 @@
</template>
<script setup lang="ts">
import { computed } from "vue";
import type { FavoriteFolder } from "@/global/typedefinition";
defineProps<{
const props = defineProps<{
show: boolean;
folders: FavoriteFolder[];
}>();
const hasDefaultFolder = computed(() =>
props.folders.some((folder) => folder.name === "默认收藏夹"),
);
const emit = defineEmits<{
close: [];
"select-folder": [folderId: number | "default"];
"create-folder": [];
}>();
</script>
-5
View File
@@ -21,11 +21,6 @@
<h2 class="text-2xl font-bold text-slate-900 dark:text-white">
登录星火账号
</h2>
<p
class="mt-2 text-sm leading-6 text-slate-500 dark:text-slate-400"
>
使用论坛账号登录密码仅直接提交到星火论坛用于换取论坛令牌不会发送给商店后端
</p>
</div>
<label class="mb-4 block">
+13 -6
View File
@@ -5,7 +5,7 @@
<div
class="flex flex-col gap-5 sm:flex-row sm:items-center sm:justify-between"
>
<div class="flex items-center gap-4">
<div class="flex min-w-0 items-center gap-4">
<img
v-if="user.avatarUrl"
:src="user.avatarUrl"
@@ -18,16 +18,16 @@
>
{{ userInitial }}
</div>
<div>
<div class="min-w-0">
<h1 class="text-2xl font-semibold text-slate-900 dark:text-white">
用户管理
</h1>
<p
class="mt-1 text-lg font-medium text-slate-800 dark:text-slate-100"
class="mt-1 truncate text-lg font-medium text-slate-800 dark:text-slate-100"
>
{{ user.displayName }}
</p>
<p class="text-sm text-slate-500 dark:text-slate-400">
<p class="truncate text-sm text-slate-500 dark:text-slate-400">
@{{ user.username }}
</p>
<p class="text-sm text-slate-500 dark:text-slate-400">
@@ -84,13 +84,18 @@
</label>
<button
type="button"
class="rounded-full border border-slate-200 px-4 py-2 text-sm font-medium text-slate-700 transition hover:border-sky-300 hover:text-sky-600 dark:border-slate-700 dark:text-slate-200 dark:hover:border-sky-500 dark:hover:text-sky-300"
class="rounded-full border border-slate-200 px-4 py-2 text-sm font-medium text-slate-700 transition hover:border-sky-300 hover:text-sky-600 disabled:cursor-not-allowed disabled:opacity-60 dark:border-slate-700 dark:text-slate-200 dark:hover:border-sky-500 dark:hover:text-sky-300"
:disabled="syncing"
@click="emit('sync-now')"
>
立即同步
{{ syncing ? "同步中..." : "立即同步" }}
</button>
</div>
<p v-if="syncMessage" class="text-sm text-sky-600 dark:text-sky-300">
{{ syncMessage }}
</p>
<section class="space-y-4">
<div class="flex items-center justify-between gap-4">
<div>
@@ -158,6 +163,8 @@ const props = defineProps<{
syncEnabled: boolean;
loading: boolean;
error: string;
syncing?: boolean;
syncMessage?: string;
}>();
const emit = defineEmits<{