fix(glass): port material accents to v3 (#660)

This commit is contained in:
InfinityPacer
2026-08-10 15:01:08 +08:00
committed by GitHub
parent 6a4af66683
commit 1a303b4323
22 changed files with 742 additions and 86 deletions
+10 -11
View File
@@ -3,7 +3,7 @@ import api from '@/api'
import type { ApiResponse, Plugin } from '@/api/types'
import { getLogoUrl, getProxyImageUrl } from '@/utils/imageUtils'
import { useGlobalSettingsStore } from '@/stores'
import { getCardAccentRgbFromImage } from '@/composables/useCardAccentColor'
import { usePluginCardAccent } from '@/composables/usePluginCardAccent'
import { isNullOrEmptyObject } from '@/@core/utils'
import { formatDownloadCount } from '@/@core/utils/formatters'
import { useToast } from 'vue-toastification'
@@ -37,11 +37,7 @@ const $toast = useToast()
const createConfirm = useConfirm()
// 卡片头部染色所用的图标主色(CSS 变量可直接消费的 RGB 通道值)
const accentRgb = ref('40, 169, 225')
// 图片对象
const imageRef = ref<{ $el: HTMLElement } | null>(null)
const { accentStyle, imageRef, resetAccentColor, updateAccentColor } = usePluginCardAccent()
// 获取当前插件的标签
const pluginLabels = computed(() => {
@@ -73,9 +69,12 @@ function closeInstallProgress() {
// 图片加载完成
async function imageLoaded() {
const imageElement = imageRef.value?.$el.querySelector('img') as HTMLImageElement
// 从图标中提取主色,作为卡片头部染色玻璃的色相来源
accentRgb.value = await getCardAccentRgbFromImage(imageElement, '#28A9E1')
await updateAccentColor()
}
function imageFailed() {
imageLoadError.value = true
resetAccentColor()
}
// 计算图标路径
@@ -240,7 +239,7 @@ onUnmounted(() => {
:class="{
'app-hover-lift-card--hovering': hover.isHovering,
}"
:style="{ '--plugin-card-accent-rgb': accentRgb }"
:style="accentStyle"
>
<div class="plugin-card__banner flex-grow">
<VCardText class="px-2 pt-2 pb-0">
@@ -283,7 +282,7 @@ onUnmounted(() => {
aspect-ratio="4/3"
cover
@load="imageLoaded"
@error="imageLoadError = true"
@error="imageFailed"
/>
</VAvatar>
</div>
+10 -11
View File
@@ -4,7 +4,7 @@ import { useConfirm } from '@/composables/useConfirm'
import api from '@/api'
import type { ApiResponse, Plugin, PluginRating } from '@/api/types'
import { getLogoUrl, getProxyImageUrl } from '@/utils/imageUtils'
import { getCardAccentRgbFromImage } from '@/composables/useCardAccentColor'
import { usePluginCardAccent } from '@/composables/usePluginCardAccent'
import { formatDownloadCount } from '@/@core/utils/formatters'
import { useDisplay } from 'vuetify'
import { useI18n } from 'vue-i18n'
@@ -54,11 +54,7 @@ const cardRatingSummary = computed(() =>
// 显示器宽度
const display = useDisplay()
// 卡片头部染色所用的图标主色(CSS 变量可直接消费的 RGB 通道值)
const accentRgb = ref('40, 169, 225')
// 图片对象
const imageRef = ref<{ $el: HTMLElement } | null>(null)
const { accentStyle, imageRef, resetAccentColor, updateAccentColor } = usePluginCardAccent()
// 提示框
const $toast = useToast()
@@ -110,9 +106,12 @@ watch(
// 图片加载完成
async function imageLoaded() {
const imageElement = imageRef.value?.$el.querySelector('img') as HTMLImageElement
// 从图标中提取主色,作为卡片头部染色玻璃的色相来源
accentRgb.value = await getCardAccentRgbFromImage(imageElement, '#28A9E1')
await updateAccentColor()
}
function imageFailed() {
imageLoadError.value = true
resetAccentColor()
}
// 显示更新日志
@@ -651,7 +650,7 @@ watch(
'app-hover-lift-card--hovering': hover.isHovering && !props.sortable,
'cursor-move': props.sortable,
}"
:style="{ '--plugin-card-accent-rgb': accentRgb }"
:style="accentStyle"
:ripple="!props.sortable"
>
<div class="plugin-card__banner flex-grow">
@@ -682,7 +681,7 @@ watch(
aspect-ratio="4/3"
cover
@load="imageLoaded"
@error="imageLoadError = true"
@error="imageFailed"
/>
</VAvatar>
</div>
@@ -1,5 +1,6 @@
import type { Plugin } from '@/api/types'
import PluginAppCard from '@/components/cards/PluginAppCard.vue'
import { normalizePluginAccentColor } from '@/utils/glassColor'
import { renderWithProviders } from '@tests/support/render'
import { fireEvent, screen, waitFor } from '@testing-library/vue'
import { defineComponent } from 'vue'
@@ -31,8 +32,8 @@ vi.mock('vue-toastification', () => ({
useToast: () => ({ error: mocks.toastError, success: mocks.toastSuccess }),
}))
vi.mock('@/composables/useCardAccentColor', () => ({
getCardAccentRgbFromImage: mocks.accentFromImage,
vi.mock('@/@core/utils/image', () => ({
extractDominantColor: mocks.accentFromImage,
}))
const plugin: Plugin = {
@@ -47,12 +48,13 @@ const plugin: Plugin = {
const ImageStub = defineComponent({
name: 'VImg',
emits: ['error', 'load'],
template: '<button data-testid="plugin-image" @click="$emit(\'load\')" @contextmenu.prevent="$emit(\'error\')" />',
template:
'<button data-testid="plugin-image" @click="$emit(\'load\')" @contextmenu.prevent="$emit(\'error\')"><img /></button>',
})
describe('PluginAppCard rating badge', () => {
beforeEach(() => {
mocks.accentFromImage.mockReset().mockResolvedValue('12, 34, 56')
mocks.accentFromImage.mockReset().mockResolvedValue('#123456')
mocks.apiGet.mockReset()
mocks.confirm.mockReset().mockResolvedValue(true)
mocks.dialogCloses.length = 0
@@ -232,7 +234,20 @@ describe('PluginAppCard rating badge', () => {
const image = screen.getByTestId('plugin-image')
await fireEvent.click(image)
await waitFor(() => expect(mocks.accentFromImage).toHaveBeenCalled())
expect(
container.querySelector<HTMLElement>('.plugin-card')?.style.getPropertyValue('--plugin-card-accent-rgb'),
).toBe(normalizePluginAccentColor('#123456')?.rgb)
await fireEvent.contextMenu(image)
mocks.accentFromImage.mockResolvedValueOnce('#654321')
await fireEvent.click(image)
await waitFor(() => expect(mocks.accentFromImage).toHaveBeenCalledTimes(2))
expect(
container.querySelector<HTMLElement>('.plugin-card')?.style.getPropertyValue('--plugin-card-accent-rgb'),
).toBe(normalizePluginAccentColor('#654321')?.rgb)
await fireEvent.contextMenu(image)
expect(
container.querySelector<HTMLElement>('.plugin-card')?.style.getPropertyValue('--plugin-card-accent-rgb'),
).toBe('')
await fireEvent.click(container.querySelector<HTMLButtonElement>('.v-card .v-btn')!)
await fireEvent.click(await screen.findByText('项目主页'))
@@ -1,6 +1,7 @@
import type { Plugin } from '@/api/types'
import PluginCard from '@/components/cards/PluginCard.vue'
import { usePluginSidebarNavStore } from '@/stores/pluginSidebarNav'
import { normalizePluginAccentColor } from '@/utils/glassColor'
import { renderWithProviders } from '@tests/support/render'
import { fireEvent, screen, waitFor } from '@testing-library/vue'
import { defineComponent } from 'vue'
@@ -34,8 +35,8 @@ vi.mock('@/composables/useSharedDialog', () => ({
openSharedDialog: (...args: unknown[]) => mocks.openSharedDialog(...args),
}))
vi.mock('@/composables/useCardAccentColor', () => ({
getCardAccentRgbFromImage: mocks.accentFromImage,
vi.mock('@/@core/utils/image', () => ({
extractDominantColor: mocks.accentFromImage,
}))
vi.mock('vue-toastification', () => ({
@@ -55,12 +56,13 @@ const plugin: Plugin = {
const ImageStub = defineComponent({
name: 'VImg',
emits: ['error', 'load'],
template: '<button data-testid="plugin-image" @click="$emit(\'load\')" @contextmenu.prevent="$emit(\'error\')" />',
template:
'<button data-testid="plugin-image" @click="$emit(\'load\')" @contextmenu.prevent="$emit(\'error\')"><img /></button>',
})
describe('PluginCard lifecycle actions', () => {
beforeEach(() => {
mocks.accentFromImage.mockReset().mockResolvedValue('12, 34, 56')
mocks.accentFromImage.mockReset().mockResolvedValue('#123456')
mocks.apiDelete.mockReset()
mocks.apiGet.mockReset()
mocks.apiPost.mockReset()
@@ -362,6 +364,7 @@ describe('PluginCard lifecycle actions', () => {
})
it('handles image lifecycle and ignores card clicks while sorting', async () => {
mocks.accentFromImage.mockResolvedValueOnce(undefined).mockResolvedValueOnce('#123456')
const { container } = await renderWithProviders(PluginCard, {
props: {
plugin: { ...plugin, plugin_icon: 'https://example.com/plugin.png' },
@@ -372,7 +375,19 @@ describe('PluginCard lifecycle actions', () => {
const [image, authorImage] = screen.getAllByTestId('plugin-image')
await fireEvent.click(image)
await waitFor(() => expect(mocks.accentFromImage).toHaveBeenCalled())
expect(
container.querySelector<HTMLElement>('.plugin-card')?.style.getPropertyValue('--plugin-card-accent-rgb'),
).toBe('')
await fireEvent.contextMenu(image)
await fireEvent.click(image)
await waitFor(() => expect(mocks.accentFromImage).toHaveBeenCalledTimes(2))
expect(
container.querySelector<HTMLElement>('.plugin-card')?.style.getPropertyValue('--plugin-card-accent-rgb'),
).toBe(normalizePluginAccentColor('#123456')?.rgb)
await fireEvent.contextMenu(image)
expect(
container.querySelector<HTMLElement>('.plugin-card')?.style.getPropertyValue('--plugin-card-accent-rgb'),
).toBe('')
await fireEvent.click(authorImage)
await fireEvent.click(container.querySelector('.v-card')!)
expect(mocks.openSharedDialog).not.toHaveBeenCalled()
@@ -25,8 +25,8 @@ vi.mock('@/composables/useSharedDialog', () => ({
openSharedDialog: mocks.openSharedDialog,
}))
vi.mock('@/composables/useCardAccentColor', () => ({
getCardAccentRgbFromImage: vi.fn().mockResolvedValue('40, 169, 225'),
vi.mock('@/@core/utils/image', () => ({
extractDominantColor: vi.fn().mockResolvedValue(undefined),
}))
vi.mock('vue-toastification', () => ({
+1 -1
View File
@@ -37,7 +37,7 @@ const props = defineProps<{
translationStrength: number
/** 路由变化标识,用于在页面内容稳定后重新发现高价值表面。 */
routeKey: string
/** 当前主题主色,用于同步色调材质的光学高光。 */
/** 由用户主色派生的大面积玻璃材料色,用于同步色调材质的光学高光。 */
tintColor: string
/** 外层壁纸交叉淡化的时长,shader 使用同一时钟混合双纹理。 */
transitionDuration: number
@@ -126,7 +126,7 @@ afterEach(() => {
})
describe('GlassOpticalLayer', () => {
it('uses exactly two visible presentation contexts with one interaction source', () => {
it('uses exactly two visible presentation contexts with one interaction source', async () => {
rendererCalls.length = 0
rendererResults.length = 0
setRendererState.mockClear()
@@ -159,6 +159,7 @@ describe('GlassOpticalLayer', () => {
expect(rendererCalls.every(options => options.wallpaperSourceCache === wallpaperSourceCache)).toBe(true)
expect(rendererCalls.every(options => options.syncDocumentState === false)).toBe(true)
expect(rendererCalls.every(options => (options.dynamicsActive as { value: boolean }).value)).toBe(true)
expect(rendererCalls.map(options => (options.tintColor as () => string)())).toEqual(['#8D51F9', '#8D51F9'])
expect(rendererCalls[0].pageMotion).toBeUndefined()
expect(rendererCalls[1].pageMotion).toEqual(
expect.objectContaining({
@@ -167,6 +168,9 @@ describe('GlassOpticalLayer', () => {
}),
)
await wrapper.setProps({ tintColor: '#00A6B8' })
expect(rendererCalls.map(options => (options.tintColor as () => string)())).toEqual(['#00A6B8', '#00A6B8'])
wrapper.unmount()
expect(setRendererState).toHaveBeenLastCalledWith(expect.any(Object), 'fallback')
})