mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-08-07 11:53:57 +08:00
fix(account): polish sidebar favorites and sync feedback
This commit is contained in:
@@ -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();
|
||||
},
|
||||
|
||||
@@ -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");
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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<{
|
||||
|
||||
Reference in New Issue
Block a user