Merge branch 'main' into fix/announcement-display-and-preview

解决 frontend/pnpm-lock.yaml 冲突:采用 main 的 frontend/package.json 与
frontend/pnpm-lock.yaml。本 PR 附带的 postcss ^8.4.32 -> ^8.5.22 并非安全降级
(高于 main 的 pnpm.overrides 下限 >=8.5.18),但与本 PR 的公告弹窗主题无关,
回退后 PR 范围回归其本职改动。
本 PR 的功能改动(AnnouncementBell/Popup、announcement-markdown.css、
AnnouncementsView、i18n)未受影响。
This commit is contained in:
shaw
2026-07-25 14:33:58 +08:00
61 changed files with 1489 additions and 99 deletions
@@ -225,6 +225,7 @@ const formatScopeName = (scope: string): string => {
'claude-opus-4-6-thinking': 'COpus46T',
'claude-opus-4-7': 'COpus47',
'claude-opus-4-8': 'COpus48',
'claude-opus-5': 'COpus5',
'claude-sonnet-4-6': 'CSon46',
'claude-sonnet-4-5': 'CSon45',
'claude-sonnet-4-5-thinking': 'CSon45T',
@@ -32,6 +32,7 @@ export const claudeModels = [
'claude-opus-4-6',
'claude-opus-4-7',
'claude-opus-4-8',
'claude-opus-5',
'claude-sonnet-4-6',
'claude-sonnet-5',
'claude-fable-5'
@@ -267,6 +268,7 @@ const anthropicPresetMappings = [
{ label: 'Opus 4.6', from: 'claude-opus-4-6', to: 'claude-opus-4-6', color: 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400' },
{ label: 'Opus 4.7', from: 'claude-opus-4-7', to: 'claude-opus-4-7', color: 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400' },
{ label: 'Opus 4.8', from: 'claude-opus-4-8', to: 'claude-opus-4-8', color: 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400' },
{ label: 'Opus 5', from: 'claude-opus-5', to: 'claude-opus-5', color: 'bg-purple-100 text-purple-700 hover:bg-purple-200 dark:bg-purple-900/30 dark:text-purple-400' },
{ label: 'Haiku 3.5', from: 'claude-3-5-haiku-20241022', to: 'claude-3-5-haiku-20241022', color: 'bg-green-100 text-green-700 hover:bg-green-200 dark:bg-green-900/30 dark:text-green-400' },
{ label: 'Haiku 4.5', from: 'claude-haiku-4-5-20251001', to: 'claude-haiku-4-5-20251001', color: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400' },
{ label: 'Opus->Sonnet', from: 'claude-opus-4-6', to: 'claude-sonnet-4-5-20250929', color: 'bg-amber-100 text-amber-700 hover:bg-amber-200 dark:bg-amber-900/30 dark:text-amber-400' }
@@ -360,6 +362,7 @@ const bedrockPresetMappings = [
{ label: 'Opus 4.6', from: 'claude-opus-4-6', to: 'us.anthropic.claude-opus-4-6-v1', color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400' },
{ label: 'Opus 4.7', from: 'claude-opus-4-7', to: 'us.anthropic.claude-opus-4-7-v1', color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400' },
{ label: 'Opus 4.8', from: 'claude-opus-4-8', to: 'us.anthropic.claude-opus-4-8-v1', color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400' },
{ label: 'Opus 5', from: 'claude-opus-5', to: 'us.anthropic.claude-opus-5-v1', color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400' },
{ label: 'Sonnet 5', from: 'claude-sonnet-5', to: 'us.anthropic.claude-sonnet-5-v1', color: 'bg-indigo-100 text-indigo-700 hover:bg-indigo-200 dark:bg-indigo-900/30 dark:text-indigo-400' },
{ label: 'Sonnet 4.6', from: 'claude-sonnet-4-6', to: 'us.anthropic.claude-sonnet-4-6', color: 'bg-cyan-100 text-cyan-700 hover:bg-cyan-200 dark:bg-cyan-900/30 dark:text-cyan-400' },
{ label: 'Opus 4.5', from: 'claude-opus-4-5-thinking', to: 'us.anthropic.claude-opus-4-5-20251101-v1:0', color: 'bg-pink-100 text-pink-700 hover:bg-pink-200 dark:bg-pink-900/30 dark:text-pink-400' },
+6 -6
View File
@@ -51,9 +51,9 @@
<div class="mt-5 grid gap-4 md:grid-cols-2">
<div class="space-y-2">
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('affiliate.yourCode') }}</p>
<div class="flex items-center gap-2 rounded-xl border border-gray-200 bg-gray-50 px-3 py-2 dark:border-dark-700 dark:bg-dark-900">
<code class="flex-1 truncate text-sm font-semibold text-gray-900 dark:text-white">{{ detail.aff_code }}</code>
<button class="btn btn-secondary btn-sm" @click="copyCode">
<div class="flex flex-col items-stretch gap-2 rounded-xl border border-gray-200 bg-gray-50 px-3 py-2 dark:border-dark-700 dark:bg-dark-900 sm:flex-row sm:items-center">
<code class="min-w-0 break-all text-sm font-semibold text-gray-900 dark:text-white sm:flex-1 sm:truncate">{{ detail.aff_code }}</code>
<button class="btn btn-secondary btn-sm w-full sm:w-auto sm:shrink-0" @click="copyCode">
<Icon name="copy" size="sm" />
<span>{{ t('affiliate.copyCode') }}</span>
</button>
@@ -62,9 +62,9 @@
<div class="space-y-2">
<p class="text-sm font-medium text-gray-700 dark:text-gray-300">{{ t('affiliate.inviteLink') }}</p>
<div class="flex items-center gap-2 rounded-xl border border-gray-200 bg-gray-50 px-3 py-2 dark:border-dark-700 dark:bg-dark-900">
<code class="flex-1 truncate text-sm text-gray-700 dark:text-gray-300">{{ inviteLink }}</code>
<button class="btn btn-secondary btn-sm" @click="copyInviteLink">
<div class="flex flex-col items-stretch gap-2 rounded-xl border border-gray-200 bg-gray-50 px-3 py-2 dark:border-dark-700 dark:bg-dark-900 sm:flex-row sm:items-center">
<code class="min-w-0 break-all text-sm text-gray-700 dark:text-gray-300 sm:flex-1 sm:truncate">{{ inviteLink }}</code>
<button class="btn btn-secondary btn-sm w-full sm:w-auto sm:shrink-0" @click="copyInviteLink">
<Icon name="copy" size="sm" />
<span>{{ t('affiliate.copyLink') }}</span>
</button>
@@ -0,0 +1,116 @@
import { flushPromises, mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import AffiliateView from '../AffiliateView.vue'
const { copyToClipboard, getAffiliateDetail } = vi.hoisted(() => ({
copyToClipboard: vi.fn(),
getAffiliateDetail: vi.fn(),
}))
vi.mock('@/api/user', () => ({
default: {
getAffiliateDetail,
transferAffiliateQuota: vi.fn(),
},
}))
vi.mock('@/stores/app', () => ({
useAppStore: () => ({
showError: vi.fn(),
showSuccess: vi.fn(),
}),
}))
vi.mock('@/stores/auth', () => ({
useAuthStore: () => ({
refreshUser: vi.fn(),
}),
}))
vi.mock('@/composables/useClipboard', () => ({
useClipboard: () => ({ copyToClipboard }),
}))
vi.mock('vue-i18n', async (importOriginal) => {
const actual = await importOriginal<typeof import('vue-i18n')>()
return {
...actual,
useI18n: () => ({
t: (key: string) => key,
}),
}
})
describe('AffiliateView', () => {
const affiliateCode = 'affiliate-code-that-is-long-enough-to-overflow-a-mobile-viewport'
beforeEach(() => {
vi.clearAllMocks()
copyToClipboard.mockResolvedValue(true)
getAffiliateDetail.mockResolvedValue({
user_id: 1,
aff_code: affiliateCode,
inviter_id: null,
aff_count: 0,
aff_quota: 0,
aff_frozen_quota: 0,
aff_history_quota: 0,
effective_rebate_rate_percent: 10,
invitees: [],
})
})
it('stacks long values and copy controls on mobile while retaining desktop rows', async () => {
const wrapper = mount(AffiliateView, {
global: {
stubs: {
AppLayout: { template: '<main><slot /></main>' },
Icon: true,
},
},
})
await flushPromises()
const values = wrapper.findAll('code')
expect(values).toHaveLength(2)
for (const value of values) {
expect(value.classes()).toEqual(expect.arrayContaining([
'min-w-0',
'break-all',
'sm:flex-1',
'sm:truncate',
]))
expect(Array.from(value.element.parentElement?.classList ?? [])).toEqual(expect.arrayContaining([
'flex-col',
'items-stretch',
'sm:flex-row',
'sm:items-center',
]))
}
const copyButtons = wrapper.findAll('button').filter((button) =>
['affiliate.copyCode', 'affiliate.copyLink'].includes(button.text()),
)
expect(copyButtons).toHaveLength(2)
for (const button of copyButtons) {
expect(button.classes()).toEqual(expect.arrayContaining([
'w-full',
'sm:w-auto',
'sm:shrink-0',
]))
}
await copyButtons[0].trigger('click')
await copyButtons[1].trigger('click')
await flushPromises()
expect(copyToClipboard).toHaveBeenNthCalledWith(1, affiliateCode, 'affiliate.codeCopied')
expect(copyToClipboard).toHaveBeenNthCalledWith(
2,
`${window.location.origin}/register?aff=${encodeURIComponent(affiliateCode)}`,
'affiliate.linkCopied',
)
})
})