Files
spark-store/src/components/update-center/UpdateCenterItem.vue
T
xiyidaiwa 81567f8351 refactor(display): 移除字体大小设置,仅保留界面整体缩放
用户实测发现仅调字体到特大/超大时主页面卡片布局遮挡,且字体大小
与界面缩放功能效果高度重叠。故采用方案1:删除字体大小入口,放大
统一由 Electron setZoomFactor 处理(整体等比缩放,不破坏布局)。

- displaySettings.ts:移除 FontSizeOption/FONT_SIZE_OPTIONS/applyFontSize/
  setFontSize/initFontSize 等全部字体相关导出,仅保留界面缩放
- App.vue:移除 initFontSize 导入与调用
- SettingsModal.vue:删除「字体大小」卡片及所有相关逻辑,仅留「界面缩放」
- 还原 8 个组件中当初为字体联动改写的 rem 为原始 px(10/11/12/13/15px),
  消除对 html font-size 的依赖,卡片布局不再因字号变化溢出
2026-08-18 00:15:24 +08:00

272 lines
8.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<label
class="relative flex items-center gap-3 rounded-xl border p-2.5 shadow-sm transition"
:class="
item.ignored === true
? 'border-slate-200/50 bg-slate-50/60 opacity-70 dark:border-slate-800/50 dark:bg-slate-900/40'
: item.held === true
? 'border-amber-300/70 bg-amber-50/70 dark:border-amber-500/30 dark:bg-amber-500/10'
: 'border-slate-200/70 bg-white/90 dark:border-slate-800/70 dark:bg-slate-900/70'
"
>
<input
type="checkbox"
class="h-4 w-4 shrink-0 rounded border-slate-300 accent-brand focus:ring-brand"
:checked="selected"
:disabled="isSelectionLocked"
@change="$emit('toggle-selection')"
/>
<div
class="flex h-9 w-9 shrink-0 items-center justify-center overflow-hidden rounded-md bg-slate-100 text-slate-400 dark:bg-slate-800 dark:text-slate-500"
:class="item.ignored === true || item.held === true ? 'opacity-60' : ''"
>
<img
:src="iconSrc"
:alt="`${item.displayName} 图标`"
class="h-full w-full object-cover"
@error="handleIconError"
/>
</div>
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-2">
<p
class="truncate font-semibold"
:class="
item.ignored === true
? 'text-sm text-slate-400 dark:text-slate-500'
: 'text-sm text-slate-900 dark:text-white'
"
>
{{ item.displayName }}
</p>
<span
class="shrink-0 rounded-full bg-slate-100 px-2 py-0.5 text-[10px] font-semibold text-slate-600 dark:bg-slate-800 dark:text-slate-200"
>
{{ sourceLabel }}
</span>
<span
v-if="item.ignored === true"
class="shrink-0 rounded-full bg-slate-200 px-2 py-0.5 text-[10px] font-semibold text-slate-500 dark:bg-slate-800 dark:text-slate-300"
>
已忽略
</span>
<span
v-else-if="item.held === true && !forced"
class="shrink-0 rounded-full bg-amber-200 px-2 py-0.5 text-[10px] font-semibold text-amber-700 dark:bg-amber-500/20 dark:text-amber-200"
>
已锁定
</span>
<span
v-else-if="item.held === true && forced"
class="shrink-0 rounded-full bg-amber-500 px-2 py-0.5 text-[10px] font-semibold text-white dark:bg-amber-500"
>
将强制
</span>
</div>
<p class="mt-0.5 truncate text-xs text-slate-500 dark:text-slate-400">
{{ item.packageName }} · {{ item.currentVersion }} →
{{ item.newVersion }}
</p>
</div>
<div
v-if="task"
class="shrink-0 text-right text-xs font-semibold text-slate-600 dark:text-slate-300"
>
<p>{{ statusLabel }}</p>
<p v-if="showProgress" class="mt-0.5">{{ progressText }}</p>
</div>
<div
v-if="item.ignored !== true && !task"
class="flex shrink-0 flex-col items-end gap-0.5"
>
<span
class="text-[11px] font-semibold text-brand dark:text-amber-300"
>{{ timeLabel }}</span
>
<span class="text-[10px] text-slate-400">{{ sizeLabel }}</span>
</div>
<div v-else-if="!task" class="flex shrink-0 flex-col items-end gap-0.5">
<span class="text-[11px] font-semibold text-slate-400">{{
timeLabel
}}</span>
<span class="text-[10px] text-slate-400">{{ sizeLabel }}</span>
</div>
<!-- 强制安装开关:仅对被系统锁定(held)的包显示 -->
<div
v-if="item.held === true && !task"
class="flex shrink-0 items-center gap-1"
:title="forced ? '已开启强制安装' : '强制安装被锁定的软件'"
>
<button
type="button"
class="relative inline-flex h-[18px] w-[34px] items-center rounded-full transition"
:class="forced ? 'bg-brand' : 'bg-slate-300 dark:bg-slate-600'"
role="switch"
:aria-checked="forced"
aria-label="强制安装"
@click.stop="$emit('toggle-force')"
>
<span
class="inline-block h-3.5 w-3.5 transform rounded-full bg-white transition"
:class="forced ? 'translate-x-[16px]' : 'translate-x-[2px]'"
></span>
</button>
<span class="text-[10px] text-slate-400">强制</span>
</div>
<button
v-if="item.ignored === true"
type="button"
class="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg border border-slate-300/80 text-slate-500 transition hover:bg-slate-50 dark:border-slate-700 dark:text-slate-300 dark:hover:bg-slate-800"
aria-label="取消忽略"
@click.stop="$emit('unignore-item')"
>
<i class="fas fa-rotate-left text-xs"></i>
</button>
<button
v-else
type="button"
class="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-lg text-amber-600 transition hover:bg-amber-50 dark:text-amber-300 dark:hover:bg-amber-500/10"
aria-label="忽略更新"
@click.stop="$emit('ignore-item')"
>
<i class="fas fa-eye-slash text-xs"></i>
</button>
<div
v-if="showProgress"
class="absolute inset-x-0 bottom-0 h-1 overflow-hidden rounded-b-xl bg-slate-200 dark:bg-slate-800"
>
<div
class="h-full rounded-full bg-gradient-to-r from-brand to-brand-dark"
:style="progressStyle"
></div>
</div>
</label>
</template>
<script setup lang="ts">
import { computed, ref, watch } from "vue";
import type {
UpdateCenterItem,
UpdateCenterTaskState,
} from "@/global/typedefinition";
const props = defineProps<{
item: UpdateCenterItem;
task?: UpdateCenterTaskState;
selected: boolean;
// 是否已开启强制安装(仅对 held 项有意义)
forced?: boolean;
}>();
const PLACEHOLDER_ICON =
'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"%3E%3Crect width="48" height="48" rx="12" fill="%23e2e8f0"/%3E%3Cpath d="M17 31h14v2H17zm3-12h8a2 2 0 0 1 2 2v8H18v-8a2 2 0 0 1 2-2" fill="%2394a3b8"/%3E%3C/svg%3E';
const iconIndex = ref(0);
defineEmits<{
(e: "toggle-selection"): void;
(e: "ignore-item"): void;
(e: "unignore-item"): void;
(e: "toggle-force"): void;
}>();
// 被锁定且未强制时,复选框禁用(与「已忽略」一样不可被选中)
const isSelectionLocked = computed(
() =>
props.item.ignored === true || (props.item.held === true && !props.forced),
);
const normalizeIconSrc = (icon: string): string => {
if (/^[a-z]+:\/\//i.test(icon)) {
return icon;
}
return icon.startsWith("/") ? `file://${icon}` : icon;
};
const iconCandidates = computed(() => {
return [props.item.localIcon, props.item.remoteIcon].filter(
(icon): icon is string => Boolean(icon),
);
});
const handleIconError = () => {
if (iconIndex.value < iconCandidates.value.length) {
iconIndex.value += 1;
}
};
watch(
[() => props.item, () => props.item.localIcon, () => props.item.remoteIcon],
() => {
iconIndex.value = 0;
},
);
const iconSrc = computed(() => {
const icon = iconCandidates.value[iconIndex.value];
return icon ? normalizeIconSrc(icon) : PLACEHOLDER_ICON;
});
const sourceLabel = computed(() => {
return props.item.source === "apm" ? "APM" : "传统deb";
});
const statusLabel = computed(() => {
switch (props.task?.status) {
case "downloading":
return "下载中";
case "installing":
return "安装中";
case "completed":
return "已完成";
case "failed":
return "失败";
case "cancelled":
return "已取消";
default:
return "待处理";
}
});
const showProgress = computed(() => {
return (
props.task?.status === "downloading" || props.task?.status === "installing"
);
});
const progressText = computed(() => `${props.task?.progress ?? 0}%`);
const progressStyle = computed(() => ({ width: progressText.value }));
// 相对时间(如「3天前」),无数据降级为 「—」
const timeLabel = computed(() => {
const t = props.item.updateTime;
if (!t || typeof t !== "number") return "—";
const diff = Date.now() - t;
const day = 24 * 60 * 60 * 1000;
if (diff < 0) return "刚刚";
if (diff < day) return "今天";
if (diff < 2 * day) return "昨天";
if (diff < 30 * day) return `${Math.floor(diff / day)}天前`;
if (diff < 365 * day) return `${Math.floor(diff / (30 * day))}个月前`;
return `${Math.floor(diff / (365 * day))}年前`;
});
// 大小格式化(如 45 MB / 1.2 GB)
const sizeLabel = computed(() => {
const size = props.item.size;
if (!size || size <= 0) return "—";
const mb = size / (1024 * 1024);
if (mb >= 1024) return `${(mb / 1024).toFixed(1)} GB`;
return `${Math.round(mb)} MB`;
});
</script>