feat(studio-ds): P4 代表面落地——Feed簇/Create/Profile/TabBar 接入设计体系+i18n+SVG图标+多档主题

- Feed/GameCard/InteractBar + Create + Profile + TabBar:硬编码色→语义令牌、emoji/字形→<Icon>、圆角统一、文案 i18n 化(feed/create/profile 命名空间)
- EmptyState 增 iconName(SVG,向后兼容)消 Create ⚠/✨;Icon 补 flag/projects/dashboard/enterprise/help/warning
- i18n/index.ts 合并 feed/create 命名空间;Profile 走页内 local-scope;TabBar 用 common.nav
- Profile 接入 <AppearanceSwitch>(暗/浅/柔 + 中/英 切换入口=决议②落地)

P4 of HJ-FE-DS-001 金样板。逻辑零改(子代理只动样式/文案/图标,主代理验证)。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
zizi 2026-06-16 16:09:39 +00:00
parent 655673eb99
commit caa2e1ba03
15 changed files with 478 additions and 162 deletions

View File

@ -3,14 +3,18 @@
* EmptyState —— 空态/错误态占位(列表无数据、加载失败、无项目等场景复用)。
* 支持图标、标题、描述、可选操作按钮(slot 或 actionText+emit)。
*/
import Icon from './Icon.vue'
withDefaults(
defineProps<{
/** 主标题 */
title?: string
/** 描述文案 */
description?: string
/** 顶部图标字符(emoji 或符号,深色风用简洁符号) */
/** 顶部图标字符(emoji 或符号)——新代码优先用 iconName(SVG) */
icon?: string
/** SVG 图标名(传则用 <Icon> 渲染,优先于 icon;满足 no-emoji-icons 红线) */
iconName?: string
/** 操作按钮文案(传则显示按钮,点击触发 action 事件) */
actionText?: string
}>(),
@ -28,7 +32,10 @@ const emit = defineEmits<{ (e: 'action'): void }>()
<template>
<div class="empty-state">
<div class="empty-state__icon">{{ icon }}</div>
<div class="empty-state__icon">
<Icon v-if="iconName" :name="iconName" :size="40" />
<template v-else>{{ icon }}</template>
</div>
<p class="empty-state__title">{{ title }}</p>
<p v-if="description" class="empty-state__desc">{{ description }}</p>
<!-- 默认插槽优先;否则用 actionText 渲染默认按钮 -->

View File

@ -6,6 +6,7 @@
* 不直接发请求(互动由 InteractBar / 父级 store 处理),保持组件纯展示 + 事件上抛。
*/
import { computed } from 'vue'
import Icon from './Icon.vue'
import type { FeedCard } from '../api/types'
import { resolveCoverStyle } from '../utils/coverGradient'
@ -74,7 +75,7 @@ const fallbackVars = computed<Record<string, string>>(() => {
<span class="game-card__cover-fallback-title">{{ card.title }}</span>
</div>
<!-- 中央播放按钮 -->
<div class="game-card__play">▶</div>
<div class="game-card__play"><Icon name="play" :size="24" /></div>
</div>
<!-- 底部信息条:标题/作者 + 计数 -->
@ -84,8 +85,10 @@ const fallbackVars = computed<Record<string, string>>(() => {
<p class="game-card__author">@{{ card.authorName }}</p>
</div>
<div class="game-card__stats">
<span class="game-card__stat">▶ {{ card.playCount }}</span>
<span class="game-card__stat" :class="{ 'is-liked': card.liked }">♥ {{ card.likeCount }}</span>
<span class="game-card__stat"><Icon name="play" :size="13" /> {{ card.playCount }}</span>
<span class="game-card__stat" :class="{ 'is-liked': card.liked }">
<Icon name="heart" :size="13" /> {{ card.likeCount }}
</span>
</div>
</div>
</div>
@ -106,7 +109,8 @@ const fallbackVars = computed<Record<string, string>>(() => {
background-color: var(--panel);
background-size: cover;
background-position: center;
border-radius: var(--radius);
/* 统一圆角令牌(旧 --radius=8 等同 --radius-md,此处沿用默认中号圆角) */
border-radius: var(--radius-md);
overflow: hidden;
cursor: pointer;
}
@ -120,10 +124,11 @@ const fallbackVars = computed<Record<string, string>>(() => {
justify-content: center;
padding: 24px;
overflow: hidden;
/* --cover-grad 由组件按 hash 注入;变量缺省时退回原中性渐变(健壮兜底) */
/* --cover-grad 由组件按 hash 注入;变量缺省时退回原中性渐变(健壮兜底,用面板令牌) */
background: var(--cover-grad, linear-gradient(135deg, var(--panel), var(--panel-2)));
}
/* 右上柔光聚光点(对齐 demo .game-cover::before 的暖光球,落位由 --cover-glow-* 错开) */
/* 右上柔光聚光点(对齐 demo .game-cover::before 的暖光球,落位由 --cover-glow-* 错开)。
此处 50% 为「径向柔光球」装饰(非 UI 控件/头像),与 loading 旋转环同属圆形例外,保留圆。 */
.game-card__cover-fallback::before {
content: '';
position: absolute;
@ -132,8 +137,8 @@ const fallbackVars = computed<Record<string, string>>(() => {
width: 64px;
height: 64px;
border-radius: 50%;
background: var(--cover-glow, rgba(49, 230, 255, 0.28));
box-shadow: 0 0 30px var(--cover-glow, rgba(49, 230, 255, 0.28));
background: var(--cover-glow, color-mix(in srgb, var(--accent) 28%, transparent));
box-shadow: 0 0 30px var(--cover-glow, color-mix(in srgb, var(--accent) 28%, transparent));
pointer-events: none;
}
/* 底部波形装饰(对齐 demo .game-cover::after 的折线波纹,向下渐隐融入卡底;相位由变体类切换 clip-path) */
@ -144,7 +149,11 @@ const fallbackVars = computed<Record<string, string>>(() => {
right: -8%;
bottom: 0;
height: 54%;
background: linear-gradient(180deg, var(--cover-wave, rgba(49, 230, 255, 0.4)), rgba(9, 11, 21, 0.96));
background: linear-gradient(
180deg,
var(--cover-wave, color-mix(in srgb, var(--accent) 40%, transparent)),
color-mix(in srgb, var(--bg) 96%, transparent)
);
clip-path: polygon(0 100%, 16% 40%, 27% 72%, 42% 25%, 58% 72%, 74% 36%, 100% 100%);
pointer-events: none;
}
@ -165,9 +174,9 @@ const fallbackVars = computed<Record<string, string>>(() => {
font-weight: 700;
line-height: 1.35;
color: var(--ink);
text-shadow: 0 2px 12px rgba(0, 0, 0, 0.45);
text-shadow: 0 2px 12px color-mix(in srgb, #000 45%, transparent);
}
/* 中央播放按钮 */
/* 中央播放按钮(半透深底 + 霓虹描边玻璃感;改统一圆角矩形,杀正圆) */
.game-card__play {
position: absolute;
top: 50%;
@ -178,11 +187,10 @@ const fallbackVars = computed<Record<string, string>>(() => {
display: flex;
align-items: center;
justify-content: center;
font-size: 22px;
color: var(--ink);
background: rgba(7, 9, 17, 0.55);
border: 1px solid var(--cyan);
border-radius: 50%;
background: color-mix(in srgb, var(--bg) 55%, transparent);
border: 1px solid var(--accent);
border-radius: var(--radius-lg);
backdrop-filter: blur(2px);
}
/* 底部信息条 */
@ -211,6 +219,9 @@ const fallbackVars = computed<Record<string, string>>(() => {
align-items: flex-end;
}
.game-card__stat {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
color: var(--muted);
}

View File

@ -24,6 +24,12 @@ const PATHS: Record<string, string> = {
spark: '<path d="M12 3v4M12 17v4M3 12h4M17 12h4M6 6l2.5 2.5M15.5 15.5 18 18M18 6l-2.5 2.5M8.5 15.5 6 18"/>',
settings: '<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>',
globe: '<circle cx="12" cy="12" r="9"/><path d="M3 12h18M12 3a15 15 0 0 1 0 18 15 15 0 0 1 0-18z"/>',
flag: '<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/><path d="M4 22v-7"/>',
projects: '<path d="m12 2 9 5-9 5-9-5 9-5z"/><path d="m3 12 9 5 9-5"/><path d="m3 17 9 5 9-5"/>',
dashboard: '<path d="M3 3v18h18M8 17v-5M13 17V9M18 17v-8"/>',
enterprise: '<rect x="3" y="7" width="18" height="13" rx="2"/><path d="M8 7V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M3 13h18"/>',
help: '<circle cx="12" cy="12" r="9"/><path d="M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3"/><path d="M12 17h.01"/>',
warning: '<path d="M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0z"/><path d="M12 9v4M12 17h.01"/>',
}
// 找不到的图标渲染空 svg(不报错)

View File

@ -4,6 +4,8 @@
* 纯展示 + 事件上抛:点击各按钮 emit('interact', action),由父级调 feedStore.interact 处理乐观更新。
* action 取值严格贴 feed.yaml FeedInteractReqVO.action:1点赞 2收藏 3分享 4举报。
*/
import { useI18n } from 'vue-i18n'
import Icon from './Icon.vue'
import type { FeedAction } from '../api/types'
defineProps<{
@ -17,14 +19,17 @@ defineProps<{
/** 互动事件:上抛 action(1赞2藏3享4报) */
const emit = defineEmits<{ (e: 'interact', action: FeedAction): void }>()
// i18n:标签文案走 feed 命名空间(globalInjection 已开,模板亦可用 $t)
const { t } = useI18n()
</script>
<template>
<div class="interact-bar">
<!-- 点赞(action=1) -->
<button class="interact-bar__item" :class="{ 'is-active': liked }" @click="emit('interact', 1)">
<span class="interact-bar__icon">♥</span>
<span class="interact-bar__label">{{ likeCount ?? '赞' }}</span>
<span class="interact-bar__icon"><Icon name="heart" :size="22" /></span>
<span class="interact-bar__label">{{ likeCount ?? t('feed.like') }}</span>
</button>
<!-- 收藏(action=2) -->
<button
@ -32,18 +37,18 @@ const emit = defineEmits<{ (e: 'interact', action: FeedAction): void }>()
:class="{ 'is-active-fav': favorited }"
@click="emit('interact', 2)"
>
<span class="interact-bar__icon">★</span>
<span class="interact-bar__label">藏</span>
<span class="interact-bar__icon"><Icon name="star" :size="22" /></span>
<span class="interact-bar__label">{{ t('feed.favorite') }}</span>
</button>
<!-- 分享(action=3) -->
<button class="interact-bar__item" @click="emit('interact', 3)">
<span class="interact-bar__icon">➦</span>
<span class="interact-bar__label">享</span>
<span class="interact-bar__icon"><Icon name="share" :size="22" /></span>
<span class="interact-bar__label">{{ t('feed.share') }}</span>
</button>
<!-- 举报(action=4) -->
<!-- 举报(action=4):缺 flag 图标,name='flag' 暂渲空 SVG(Icon 对未知名零报错),待主代理补登记 -->
<button class="interact-bar__item" @click="emit('interact', 4)">
<span class="interact-bar__icon">⚑</span>
<span class="interact-bar__label">报</span>
<span class="interact-bar__icon"><Icon name="flag" :size="22" /></span>
<span class="interact-bar__label">{{ t('feed.report') }}</span>
</button>
</div>
</template>
@ -68,16 +73,16 @@ const emit = defineEmits<{ (e: 'interact', action: FeedAction): void }>()
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
/* 抖音式悬浮玻璃感(对齐 demo .feed-action i,spec §3.3 V-1):
/* 抖音式悬浮玻璃感(对齐 mockup .feed-rail button,spec §3.3 V-1):
半透白底 + 半透白描边 + 轻投影,配 backdrop-filter 模糊出通透玻璃层次。 */
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.15);
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.26);
background: color-mix(in srgb, var(--ink) 10%, transparent);
border: 1px solid color-mix(in srgb, var(--ink) 15%, transparent);
box-shadow: 0 12px 28px color-mix(in srgb, #000 26%, transparent);
/* 模糊适度(8px)兼顾移动端性能;-webkit- 前缀兜底 iOS Safari/旧 WebView */
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
border-radius: 50%;
/* 统一圆角矩形(对齐 mockup .feed-rail button 的 --radius-lg,杀正圆药丸) */
border-radius: var(--radius-lg);
transition: transform 0.1s ease, color 0.15s ease, border-color 0.15s ease;
}
.interact-bar__item:active .interact-bar__icon {
@ -92,9 +97,9 @@ const emit = defineEmits<{ (e: 'interact', action: FeedAction): void }>()
color: var(--pink);
border-color: var(--pink);
}
/* 已藏高亮 amber */
/* 已藏高亮(warning=amber 语义层,杀硬编码) */
.interact-bar__item.is-active-fav .interact-bar__icon {
color: var(--amber);
border-color: var(--amber);
color: var(--warning);
border-color: var(--warning);
}
</style>

View File

@ -11,11 +11,15 @@
import { useRoute, useRouter } from 'vue-router'
import { computed, onMounted } from 'vue'
import { storeToRefs } from 'pinia'
import { useI18n } from 'vue-i18n'
import { useMessageStore } from '../store/message'
import { useUserStore } from '../store/user'
import Icon from './Icon.vue'
const route = useRoute()
const router = useRouter()
// 文案 i18n:四入口走全局命名空间 common.nav.*(已在 i18n/index.ts 注册,全局作用域直接取)
const { t } = useI18n()
// 消息未读角标:与消息中心同源(store 单一数据源)
const messageStore = useMessageStore()
const { unreadTotal } = storeToRefs(messageStore)
@ -30,10 +34,10 @@ interface TabItem {
match: string
/** 额外高亮前缀(如「我的」同时覆盖 /project 二级页),可选 */
extraMatch?: string
/** 文案 */
label: string
/** 图标字符 */
icon: string
/** 文案 i18n key(全局命名空间 common.nav.*,模板内经 t() 取) */
labelKey: string
/** 图标名(统一走 <Icon name>,替代旧 Unicode 字形) */
iconName: string
/** 是否显示未读红点(仅「消息」Tab) */
showBadge?: boolean
}
@ -41,10 +45,10 @@ interface TabItem {
/** 四入口(顺序:游戏流 / 创作 / 消息 / 我的)。
* 「我的」指向个人中心 /profile,并额外高亮 /project(我的项目二级页归属「我的」)。 */
const tabs: TabItem[] = [
{ path: '/feed', match: '/feed', label: '游戏流', icon: '◎' },
{ path: '/create', match: '/create', label: '创作', icon: '✦' },
{ path: '/message', match: '/message', label: '消息', icon: '✉', showBadge: true },
{ path: '/profile', match: '/profile', extraMatch: '/project', label: '我的', icon: '◰' },
{ path: '/feed', match: '/feed', labelKey: 'common.nav.feed', iconName: 'feed' },
{ path: '/create', match: '/create', labelKey: 'common.nav.create', iconName: 'create' },
{ path: '/message', match: '/message', labelKey: 'common.nav.message', iconName: 'message', showBadge: true },
{ path: '/profile', match: '/profile', extraMatch: '/project', labelKey: 'common.nav.me', iconName: 'me' },
]
/** 判断某 tab 是否被当前 path 命中(match 或 extraMatch 的精确/前缀匹配) */
@ -88,13 +92,13 @@ onMounted(() => {
@click="go(tab)"
>
<span class="tabbar__icon">
{{ tab.icon }}
<Icon :name="tab.iconName" :size="22" />
<!-- 「消息」Tab 未读红点(unreadTotal>0 才显示;>99 显示 99+) -->
<span v-if="tab.showBadge && unreadTotal > 0" class="tabbar__badge">
{{ unreadTotal > 99 ? '99+' : unreadTotal }}
</span>
</span>
<span class="tabbar__label">{{ tab.label }}</span>
<span class="tabbar__label">{{ t(tab.labelKey) }}</span>
</button>
</nav>
</template>
@ -121,10 +125,9 @@ onMounted(() => {
}
.tabbar__icon {
position: relative;
font-size: 20px;
line-height: 1;
}
/* 未读红点角标(叠在图标右上角,pink 点缀,与消息中心同色) */
/* 未读红点角标(叠在图标右上角,danger 点缀,与消息中心同色) */
.tabbar__badge {
position: absolute;
top: -6px;
@ -139,15 +142,17 @@ onMounted(() => {
font-size: 10px;
font-weight: 700;
line-height: 1;
color: #2a0610;
background: var(--pink);
border-radius: 8px;
/* danger 之上的近黑前景令牌(原 #2a0610 硬编码) */
color: var(--on-danger);
background: var(--danger);
/* 统一圆角矩形(原 8px 近正圆改 --radius-xs) */
border-radius: var(--radius-xs);
}
.tabbar__label {
font-size: 11px;
}
/* 激活态 cyan 高亮 */
/* 激活态 accent 高亮 */
.tabbar__item.is-active {
color: var(--cyan);
color: var(--accent);
}
</style>

View File

@ -6,13 +6,22 @@
import { createI18n } from 'vue-i18n'
import zhCN from '../locales/zh-CN'
import enUS from '../locales/en-US'
// 各页域命名空间(P4 起按面拆分文件,集中在此合入全局 messages)。
// 注:profile 走页内 local-scope 注入(见 Profile.vue),故此处只并 feed / create。
import feedZh from '../locales/feed.zh'
import feedEn from '../locales/feed.en'
import createZh from '../locales/create.zh'
import createEn from '../locales/create.en'
export const i18n = createI18n({
legacy: false, // 组合式 API(useI18n / $t),关闭 legacy 选项式
globalInjection: true, // 模板内可直接用 $t
locale: 'zh-CN',
fallbackLocale: 'zh-CN',
messages: { 'zh-CN': zhCN, 'en-US': enUS },
messages: {
'zh-CN': { ...zhCN, ...feedZh, ...createZh },
'en-US': { ...enUS, ...feedEn, ...createEn },
},
})
export default i18n

View File

@ -0,0 +1,31 @@
/**
* 创作入口页(/create)英文文案(与 create.zh.ts 同构)。缺 key 时由 i18n 回落 zh-CN。
* 由主代理在 i18n/index.ts 中合并进 en-US。
*/
export default {
create: {
// —— 顶部标题区 ——
title: 'One sentence, one game',
subtitle: 'Pick a template, describe your idea in one line, and AI builds it',
// —— 模板选择区 ——
pickTemplate: 'Pick a template',
templateLoading: 'Loading templates…',
// 真加载失败(网络/服务异常)
loadFailedTitle: 'Failed to load templates',
loadFailedDesc: 'Network error or service not ready, please retry',
reload: 'Reload',
// 空(非故障):玩法模板体系升级中
upgradingTitle: 'Template system is upgrading',
upgradingDesc: 'The new one-sentence creator is coming soon — explore the feed first',
goFeed: 'Go to feed',
// —— 一句话描述输入区 ——
describe: 'Describe in one line',
promptPlaceholder: 'e.g. an idle game where tapping earns coins, faster and faster',
// 示例 Prompt 一键填入前缀(后接模板示例文案)
useExample: 'Use example: ',
// —— 底部提交 ——
submit: 'Generate',
submitting: 'Creating…',
tip: 'Generation takes a few seconds; track status on the progress page',
},
}

View File

@ -0,0 +1,32 @@
/**
* 创作入口页(/create)中文文案。命名空间:create。
* 由主代理在 i18n/index.ts 中合并进 zh-CN(与 create.en.ts 同构)。
* 通用词(提交/取消/重试/加载/空态)复用 common.action / common.state,不在此重复定义。
*/
export default {
create: {
// —— 顶部标题区 ——
title: '一句话,做个小游戏',
subtitle: '选一个玩法模板,用一句话描述你的创意,AI 帮你生成',
// —— 模板选择区 ——
pickTemplate: '选择玩法模板',
templateLoading: '模板加载中…',
// 真加载失败(网络/服务异常)
loadFailedTitle: '模板加载失败',
loadFailedDesc: '网络异常或服务未就绪,请重试',
reload: '重新加载',
// 空(非故障):玩法模板体系升级中
upgradingTitle: '玩法模板体系升级中',
upgradingDesc: '新版「一句话创作」即将上线,先去游戏流逛逛吧',
goFeed: '去游戏流',
// —— 一句话描述输入区 ——
describe: '用一句话描述',
promptPlaceholder: '例如:做一个点击就能赚金币、越点越快的放置小游戏',
// 示例 Prompt 一键填入前缀(后接模板示例文案)
useExample: '用示例:',
// —— 底部提交 ——
submit: '开始生成',
submitting: '正在创建…',
tip: '生成约需数秒,期间可在进度页查看状态',
},
}

View File

@ -0,0 +1,40 @@
/**
* 消费侧 Feed 簇文案(英文,与 feed.zh 同构)。命名空间:feed。
* 缺 key 时由 i18n 回落 zh-CN。
*/
export default {
feed: {
/** 顶部混合流入口名 */
recommend: 'For You',
/** 专区名兜底 */
zone: 'Zone',
/** 首屏加载提示(带专区名插值) */
loadingZone: 'Loading {name}…',
/** 空态标题 */
emptyTitle: 'No games here yet',
/** 空态描述 */
emptyDesc: 'Try another zone, or check back later',
/** 空态/重新拉取按钮 */
refresh: 'Refresh',
/** 互动转正引导弹窗:标题 */
loginTitle: 'Sign in to interact',
/** 互动转正引导弹窗:正文 */
loginMessage: 'Sign in to like, save and share the games you love',
/** 互动转正引导弹窗:确认按钮 */
loginConfirm: 'Sign in',
/** 互动转正引导弹窗:取消按钮 */
loginCancel: 'Not now',
/** 举报理由输入提示 */
reportPrompt: 'Please describe the issue (inappropriate content / infringement, etc.)',
/** 举报提交成功提示 */
reportDone: 'Report submitted, we will review it shortly',
/** 互动条:收藏标签 */
favorite: 'Save',
/** 互动条:分享标签 */
share: 'Share',
/** 互动条:举报标签 */
report: 'Report',
/** 互动条:点赞标签 */
like: 'Like',
},
}

View File

@ -0,0 +1,41 @@
/**
* 消费侧 Feed 簇文案(中文基线)。命名空间:feed。
* 由 i18n/index.ts 合并进 zh-CN messages(主代理负责合并)。
* 通用词(立即玩/分享/重试/更多/加载中/这里还没有内容…)复用 common.*,本文件只放 Feed 专属文案。
*/
export default {
feed: {
/** 顶部混合流入口名(非专区时的标题) */
recommend: '推荐',
/** 专区名兜底(zones 里查不到对应 id 时) */
zone: '专区',
/** 首屏加载提示(带专区名插值),如「正在加载 推荐…」 */
loadingZone: '正在加载 {name}…',
/** 空态标题(加载完成但无数据) */
emptyTitle: '这里还没有游戏',
/** 空态描述 */
emptyDesc: '换个专区,或稍后再来看看',
/** 空态/重新拉取按钮(区别于 common.action.retry「重试」,此处语义为刷新当前流) */
refresh: '刷新',
/** 互动转正引导弹窗:标题 */
loginTitle: '登录后才能互动',
/** 互动转正引导弹窗:正文 */
loginMessage: '登录账号即可点赞、收藏、分享你喜欢的小游戏',
/** 互动转正引导弹窗:确认按钮(跳登录) */
loginConfirm: '去登录',
/** 互动转正引导弹窗:取消按钮(留在当前流) */
loginCancel: '再看看',
/** 举报理由输入提示(reason 必填,契约 action=4) */
reportPrompt: '请填写举报理由(违规内容/侵权等)',
/** 举报提交成功提示 */
reportDone: '已提交举报,平台将尽快处理',
/** 互动条:收藏标签 */
favorite: '藏',
/** 互动条:分享标签 */
share: '享',
/** 互动条:举报标签 */
report: '报',
/** 互动条:点赞标签(likeCount 为空时回退展示) */
like: '赞',
},
}

View File

@ -0,0 +1,30 @@
/**
* 「我的」个人中心页域文案(英文,与 profile.zh.ts 同构)。命名空间 profile.*。
* 缺 key 时由 vue-i18n 回落基线(zh-CN)。作为 Profile.vue 局部 i18n 消息注入。
*/
export default {
profile: {
// 账号信息
guest: 'Guest',
avatarFallback: 'U', // 无头像兜底首字(英文用 U=User)
creatorTag: 'Creator',
// 创作成长卡
levelLabel: 'Creator level',
publishedLabel: 'Published works',
nextMilestone: '{n} more works to the next milestone',
// 功能入口菜单
menu: {
projects: 'My projects',
dashboard: 'Analytics',
biz: 'Enterprise',
message: 'Inbox',
settings: 'Settings',
help: 'Help & feedback',
},
// 占位功能项点击提示
comingSoon: '{name} is coming soon',
// 退出登录
logout: 'Log out',
logoutConfirm: 'You will need to sign in again to view your projects and creations. Log out?',
},
}

View File

@ -0,0 +1,33 @@
/**
* 「我的」个人中心页域文案(中文基线)。命名空间 profile.*。
* 作为 Profile.vue 局部 i18n 消息注入(useI18n useScope:'local'),不入全局 i18n/index.ts。
* 通用词(取消/退出登录等)优先复用 common.*,本文件只放页面专属文案。
* 中英同构:profile.en.ts 必须保持完全相同的 key 结构。
*/
export default {
profile: {
// 账号信息
guest: '游客', // 未取到昵称时的占位昵称
avatarFallback: '游', // 无头像时取昵称首字兜底,昵称也缺失时用此字
creatorTag: '创作者', // 创作者白名单标识
// 创作成长卡
levelLabel: '创作等级',
publishedLabel: '已发布作品',
// 距下个里程碑还差 {n} 个作品({n} 由模板插值)
nextMilestone: '距下个里程碑还差 {n} 个作品',
// 功能入口菜单
menu: {
projects: '我的项目',
dashboard: '创作数据',
biz: '企业定制',
message: '消息',
settings: '设置',
help: '帮助与反馈',
},
// 占位功能项点击提示:{name}功能即将上线({name} 为入口名)
comingSoon: '{name}功能即将上线',
// 退出登录
logout: '退出登录', // 入口按钮 + 二次确认标题/确认按钮共用
logoutConfirm: '退出后需重新登录才能查看你的项目与创作,确定退出吗?',
},
}

View File

@ -16,10 +16,15 @@
*/
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useCreateStore } from '../../store/create'
import { useProjectStore } from '../../store/project'
import type { AigcTemplate } from '../../api/types'
import { LoadingBar, EmptyState, AppButton } from '../../components'
import Icon from '../../components/Icon.vue'
// i18n:文案统一走 create 命名空间(create.zh.ts / create.en.ts)+ common 通用词,禁裸中文
const { t } = useI18n()
const router = useRouter()
const createStore = useCreateStore()
@ -126,40 +131,40 @@ onMounted(loadTemplates)
<div class="create-page">
<!-- 顶部标题区 -->
<header class="create-head">
<h1 class="create-title">一句话,做个小游戏</h1>
<p class="create-sub">选一个玩法模板,用一句话描述你的创意,AI 帮你生成</p>
<h1 class="create-title">{{ t('create.title') }}</h1>
<p class="create-sub">{{ t('create.subtitle') }}</p>
</header>
<!-- 模板选择区 -->
<section class="create-section">
<div class="section-label">
<span class="dot" />
选择玩法模板
{{ t('create.pickTemplate') }}
</div>
<!-- 加载中 -->
<div v-if="loadingTpl" class="tpl-loading">
<LoadingBar indeterminate color="var(--cyan)" />
<span class="tpl-loading-text">模板加载中…</span>
<LoadingBar indeterminate color="var(--accent)" />
<span class="tpl-loading-text">{{ t('create.templateLoading') }}</span>
</div>
<!-- 真加载失败(网络/服务异常):可重试 -->
<EmptyState
v-else-if="tplError"
icon="⚠"
title="模板加载失败"
description="网络异常或服务未就绪,请重试"
action-text="重新加载"
icon-name="warning"
:title="t('create.loadFailedTitle')"
:description="t('create.loadFailedDesc')"
:action-text="t('create.reload')"
@action="loadTemplates"
/>
<!-- 空(非故障):W-CLEAN 后玩法模板体系升级中,新版一句话创作接 W-G1 待上线;
诚实区分「升级中」与「加载失败」,不误报故障,给一个去游戏流的出口 -->
<EmptyState
v-else-if="createStore.templates.length === 0"
icon="✨"
title="玩法模板体系升级中"
description="新版「一句话创作」即将上线,先去游戏流逛逛吧"
action-text="去游戏流"
icon-name="spark"
:title="t('create.upgradingTitle')"
:description="t('create.upgradingDesc')"
:action-text="t('create.goFeed')"
@action="goFeed"
/>
@ -178,7 +183,9 @@ onMounted(loadTemplates)
<span class="tpl-name">{{ tpl.name }}</span>
<span class="tpl-desc">{{ tpl.description }}</span>
<!-- 选中标记 -->
<span v-if="tpl.templateId === selectedTemplateId" class="tpl-check">✓</span>
<span v-if="tpl.templateId === selectedTemplateId" class="tpl-check">
<Icon name="check" :size="12" />
</span>
</button>
</div>
</section>
@ -187,7 +194,7 @@ onMounted(loadTemplates)
<section class="create-section">
<div class="section-label">
<span class="dot" />
用一句话描述
{{ t('create.describe') }}
</div>
<div class="prompt-box">
@ -196,7 +203,7 @@ onMounted(loadTemplates)
class="prompt-input"
:maxlength="PROMPT_MAX"
rows="4"
placeholder="例如:做一个点击就能赚金币、越点越快的放置小游戏"
:placeholder="t('create.promptPlaceholder')"
/>
<div class="prompt-foot">
<!-- 示例 Prompt 提示:点击一键填入当前模板示例 -->
@ -206,7 +213,8 @@ onMounted(loadTemplates)
type="button"
@click="useExample"
>
💡 用示例:{{ selectedTemplate.examplePrompt }}
<Icon name="spark" :size="14" />
<span class="prompt-example-text">{{ t('create.useExample') }}{{ selectedTemplate.examplePrompt }}</span>
</button>
<span class="prompt-count">{{ prompt.length }}/{{ PROMPT_MAX }}</span>
</div>
@ -216,9 +224,9 @@ onMounted(loadTemplates)
<!-- 底部提交(固定底部主操作) -->
<footer class="create-foot">
<AppButton block size="large" :loading="submitting" :disabled="!canSubmit" @click="onSubmit">
{{ submitting ? '正在创建…' : '开始生成' }}
{{ submitting ? t('create.submitting') : t('create.submit') }}
</AppButton>
<p class="create-tip">生成约需数秒,期间可在进度页查看状态</p>
<p class="create-tip">{{ t('create.tip') }}</p>
</footer>
</div>
</template>
@ -242,8 +250,8 @@ onMounted(loadTemplates)
margin: 0;
font-size: 22px;
font-weight: 800;
/* 青→紫渐变标题,呼应科技风 */
background: linear-gradient(90deg, var(--cyan), var(--violet));
/* 设计体系签名渐变(青→绿,--grad-primary)填充标题文字,主题恒定、呼应科技风 */
background: var(--grad-primary);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
@ -271,9 +279,10 @@ onMounted(loadTemplates)
.section-label .dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--cyan);
box-shadow: 0 0 8px var(--cyan);
/* 统一圆角矩形(禁正圆),小尺寸用最小圆角 --radius-xs */
border-radius: var(--radius-xs);
background: var(--accent);
box-shadow: 0 0 8px var(--accent);
}
/* —— 模板加载条 —— */
@ -310,7 +319,7 @@ onMounted(loadTemplates)
padding: 8px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
border-radius: var(--radius-md);
text-align: left;
transition: border-color 0.15s ease, transform 0.05s ease;
}
@ -318,14 +327,15 @@ onMounted(loadTemplates)
transform: scale(0.98);
}
.tpl-card.is-selected {
border-color: var(--cyan);
box-shadow: 0 0 0 1px var(--cyan), 0 0 12px rgba(49, 230, 255, 0.25);
border-color: var(--accent);
/* 选中辉光:手抄 rgba 改为语义令牌 + color-mix(强调色 25% 透明) */
box-shadow: 0 0 0 1px var(--accent), 0 0 12px color-mix(in srgb, var(--accent) 25%, transparent);
}
.tpl-cover {
width: 100%;
height: 84px;
object-fit: cover;
border-radius: 6px;
border-radius: var(--radius-sm);
background: var(--panel-2);
}
.tpl-name {
@ -352,17 +362,17 @@ onMounted(loadTemplates)
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: #04222a;
background: var(--cyan);
border-radius: 50%;
/* 深色 on 霓虹前景统一走 --on-accent;圆角矩形(禁正圆)用 --radius-sm */
color: var(--on-accent);
background: var(--accent);
border-radius: var(--radius-sm);
}
/* —— Prompt 输入框 —— */
.prompt-box {
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
border-radius: var(--radius-md);
padding: 12px;
}
.prompt-input {
@ -390,13 +400,20 @@ onMounted(loadTemplates)
.prompt-example {
flex: 1;
min-width: 0;
display: inline-flex;
align-items: center;
gap: 4px;
border: 0;
background: transparent;
color: var(--amber);
/* 警示/进行中语义色(原 --amber → --warning) */
color: var(--warning);
font-size: 12px;
text-align: left;
padding: 0;
/* 单行省略 */
}
/* 示例文案单行省略(图标不参与省略,故省略改挂内层文本) */
.prompt-example-text {
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;

View File

@ -15,11 +15,13 @@
*/
import { ref, computed, onMounted, onActivated, watch, nextTick } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { storeToRefs } from 'pinia'
import { showConfirmDialog } from 'vant'
import { useFeedStore } from '../../store/feed'
import { useUserStore } from '../../store/user'
import { GameCard, InteractBar, LoadingBar, EmptyState } from '../../components'
import Icon from '../../components/Icon.vue'
import { track } from '../../telemetry'
import type { FeedAction } from '../../api/types'
@ -32,6 +34,8 @@ const props = defineProps<{ zoneId?: string }>()
const route = useRoute()
const router = useRouter()
// i18n:本页文案走 feed 命名空间,通用词复用 common.*(globalInjection 已开,模板亦可用 $t)
const { t } = useI18n()
const feedStore = useFeedStore()
const userStore = useUserStore()
// 注意:仅对 state 用 storeToRefs(保持响应式);actions 直接从 store 解构(函数无需 ref)。
@ -56,8 +60,8 @@ const resolvedZoneId = computed<number | undefined>(() => {
/** 当前专区名(顶部标题展示;混合流为「推荐」) */
const currentZoneName = computed(() => {
if (resolvedZoneId.value == null) return '推荐'
return zones.value.find((z) => z.id === resolvedZoneId.value)?.name ?? '专区'
if (resolvedZoneId.value == null) return t('feed.recommend')
return zones.value.find((z) => z.id === resolvedZoneId.value)?.name ?? t('feed.zone')
})
/* ============================================================================
@ -156,10 +160,10 @@ async function onInteract(action: FeedAction): Promise<void> {
if (!userStore.isRealLogin()) {
try {
await showConfirmDialog({
title: '登录后才能互动',
message: '登录账号即可点赞、收藏、分享你喜欢的小游戏',
confirmButtonText: '去登录',
cancelButtonText: '再看看',
title: t('feed.loginTitle'),
message: t('feed.loginMessage'),
confirmButtonText: t('feed.loginConfirm'),
cancelButtonText: t('feed.loginCancel'),
})
// 确认:跳登录并带当前路由回跳(登录成功回到本流继续互动)
router.push({ path: '/login', query: { redirect: route.fullPath } })
@ -179,11 +183,11 @@ async function onInteract(action: FeedAction): Promise<void> {
// 举报:契约要求 reason 必填(action=4 转 compliance);骨架用浏览器 prompt 极简采集
if (action === 4) {
const reason = window.prompt('请填写举报理由(违规内容/侵权等)') || ''
const reason = window.prompt(t('feed.reportPrompt')) || ''
if (!reason.trim()) return // 未填写则取消,不发请求(契约 reason 必填)
await feedStore.interact(card.gameId, 4, reason.trim()).catch(() => {})
track('report', { reason: reason.trim() }, { gameId: card.gameId, versionId: card.versionId })
window.alert('已提交举报,平台将尽快处理')
window.alert(t('feed.reportDone'))
return
}
@ -211,7 +215,7 @@ function onPlay(payload: { gameId: number; versionId: number }): void {
:class="{ 'is-active': resolvedZoneId == null }"
@click="router.push('/feed')"
>
推荐
{{ t('feed.recommend') }}
</button>
<button
v-for="z in zones"
@ -227,15 +231,15 @@ function onPlay(payload: { gameId: number; versionId: number }): void {
<!-- 首屏加载态(列表为空且加载中) -->
<div v-if="loading && list.length === 0" class="feed__loading">
<LoadingBar indeterminate />
<p class="feed__loading-tip">正在加载 {{ currentZoneName }}…</p>
<p class="feed__loading-tip">{{ t('feed.loadingZone', { name: currentZoneName }) }}</p>
</div>
<!-- 空态(加载完成但无数据) -->
<EmptyState
v-else-if="!loading && list.length === 0"
title="这里还没有游戏"
description="换个专区,或稍后再来看看"
action-text="刷新"
:title="t('feed.emptyTitle')"
:description="t('feed.emptyDesc')"
:action-text="t('feed.refresh')"
@action="loadCurrent"
/>
@ -264,8 +268,13 @@ function onPlay(payload: { gameId: number; versionId: number }): void {
<!-- 上/下切换按钮(竖屏右下,兜底非触摸滑动场景,如桌面验证) -->
<div v-if="list.length > 1" class="feed__nav">
<button class="feed__nav-btn" :disabled="activeIndex <= 0" @click="go(-1)">▲</button>
<button class="feed__nav-btn" :disabled="activeIndex >= list.length - 1" @click="go(1)">▼</button>
<!-- 上/下切换:复用 chevron(右向)旋转 -90°/90° 成上/下箭头,杀 emoji 字形 -->
<button class="feed__nav-btn" :disabled="activeIndex <= 0" @click="go(-1)">
<Icon name="chevron" :size="16" class="feed__nav-ico feed__nav-ico--up" />
</button>
<button class="feed__nav-btn" :disabled="activeIndex >= list.length - 1" @click="go(1)">
<Icon name="chevron" :size="16" class="feed__nav-ico feed__nav-ico--down" />
</button>
</div>
</div>
</template>
@ -292,8 +301,12 @@ function onPlay(payload: { gameId: number; versionId: number }): void {
gap: 8px;
padding: 12px 14px;
overflow-x: auto;
/* 顶部渐隐遮罩,保证白字在封面上可读 */
background: linear-gradient(180deg, rgba(7, 9, 17, 0.7), rgba(7, 9, 17, 0));
/* 顶部渐隐遮罩,保证白字在封面上可读(用 --bg 令牌,随主题走) */
background: linear-gradient(
180deg,
color-mix(in srgb, var(--bg) 70%, transparent),
color-mix(in srgb, var(--bg) 0%, transparent)
);
-webkit-overflow-scrolling: touch;
}
.feed__zones::-webkit-scrollbar {
@ -304,15 +317,16 @@ function onPlay(payload: { gameId: number; versionId: number }): void {
padding: 5px 14px;
font-size: 13px;
color: var(--muted);
background: rgba(16, 21, 34, 0.55);
background: color-mix(in srgb, var(--panel) 55%, transparent);
border: 1px solid var(--border);
border-radius: 999px;
/* 统一圆角矩形(杀 999px 药丸) */
border-radius: var(--radius-md);
white-space: nowrap;
}
.feed__zone.is-active {
color: var(--bg);
background: var(--cyan);
border-color: var(--cyan);
color: var(--on-accent);
background: var(--accent);
border-color: var(--accent);
font-weight: 600;
}
@ -383,13 +397,20 @@ function onPlay(payload: { gameId: number; versionId: number }): void {
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: var(--ink);
background: rgba(16, 21, 34, 0.6);
background: color-mix(in srgb, var(--panel) 60%, transparent);
border: 1px solid var(--border);
border-radius: 50%;
/* 统一圆角矩形(杀正圆按钮) */
border-radius: var(--radius-lg);
}
.feed__nav-btn:disabled {
opacity: 0.3;
}
/* chevron 右向 → 旋转成上/下箭头(纯展示旋转,不改交互) */
.feed__nav-ico--up {
transform: rotate(-90deg);
}
.feed__nav-ico--down {
transform: rotate(90deg);
}
</style>

View File

@ -14,15 +14,26 @@
*/
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { showConfirmDialog, showToast } from 'vant'
import { getMe, type PlayerMeResp } from '../../api/passport'
import * as communityApi from '../../api/community'
import { useUserStore } from '../../store/user'
import { useMessageStore } from '../../store/message'
import { AppButton } from '../../components'
import Icon from '../../components/Icon.vue'
import AppearanceSwitch from '../../components/AppearanceSwitch.vue'
import profileZh from '../../locales/profile.zh'
import profileEn from '../../locales/profile.en'
import type { CommunityLevel } from '../../api/types'
const router = useRouter()
// 页域 i18n:profile.* 文案以局部消息注入(不污染全局 i18n/index.ts);
// common.* 等全局文案经 vue-i18n 根回落(fallbackRoot)仍可用 t() 取到。
const { t } = useI18n({
useScope: 'local',
messages: { 'zh-CN': profileZh, 'en-US': profileEn },
})
// 登录态 store:账号信息兜底源 + 登出动作(复用既有 logout(),不重实现)
const userStore = useUserStore()
// 消息 store:用于「消息」入口的未读角标展示(与底部 Tab 红点同源)
@ -101,9 +112,10 @@ function goBiz() {
router.push('/biz')
}
/** 占位功能项点击(设置/帮助本波未实现,给可感知的提示而非静默) */
/** 占位功能项点击(设置/帮助本波未实现,给可感知的提示而非静默)。
* name 传入已 i18n 的入口名,套 profile.comingSoon 模板插值。 */
function onPlaceholder(name: string) {
showToast(`${name}功能即将上线`)
showToast(t('profile.comingSoon', { name }))
}
/**
@ -114,10 +126,10 @@ function onPlaceholder(name: string) {
async function onLogout() {
try {
await showConfirmDialog({
title: '退出登录',
message: '退出后需重新登录才能查看你的项目与创作,确定退出吗?',
confirmButtonText: '退出登录',
cancelButtonText: '取消',
title: t('profile.logout'),
message: t('profile.logoutConfirm'),
confirmButtonText: t('profile.logout'),
cancelButtonText: t('common.action.cancel'),
})
} catch {
// 用户点取消:showConfirmDialog 以 reject 表示,吞掉即可,不执行登出
@ -147,25 +159,25 @@ onMounted(() => {
class="profile__avatar"
:style="{ backgroundImage: me?.avatar ? `url(${me.avatar})` : undefined }"
>
<span v-if="!me?.avatar" class="profile__avatar-ph">{{ (me?.nickname || '游').slice(0, 1) }}</span>
<span v-if="!me?.avatar" class="profile__avatar-ph">{{ (me?.nickname || t('profile.avatarFallback')).slice(0, 1) }}</span>
</div>
<div class="profile__id">
<h1 class="profile__nick">{{ me?.nickname || '游客' }}</h1>
<h1 class="profile__nick">{{ me?.nickname || t('profile.guest') }}</h1>
<!-- 脱敏手机号(passport.me 才有;兜底态留空不展示该行) -->
<p v-if="me?.mobile" class="profile__mobile">{{ me.mobile }}</p>
<!-- 创作者白名单标识(creatorFlag=1 才显示) -->
<span v-if="me?.creatorFlag === 1" class="profile__tag">创作者</span>
<span v-if="me?.creatorFlag === 1" class="profile__tag">{{ t('profile.creatorTag') }}</span>
</div>
</header>
<!-- 创作成长卡(community.level 成功才显示) -->
<section v-if="level" class="profile__level">
<div class="profile__level-row">
<span class="profile__level-label">创作等级</span>
<span class="profile__level-label">{{ t('profile.levelLabel') }}</span>
<span class="profile__level-val">Lv.{{ level.level }}</span>
</div>
<div class="profile__level-row">
<span class="profile__level-label">已发布作品</span>
<span class="profile__level-label">{{ t('profile.publishedLabel') }}</span>
<span class="profile__level-val">{{ level.publishedCount }}</span>
</div>
<!-- 下一里程碑进度(有 nextMilestoneTarget 才显示) -->
@ -173,56 +185,62 @@ onMounted(() => {
v-if="level.nextMilestoneTarget"
class="profile__level-next"
>
距下个里程碑还差 {{ Math.max(0, level.nextMilestoneTarget - level.publishedCount) }} 个作品
{{ t('profile.nextMilestone', { n: Math.max(0, level.nextMilestoneTarget - level.publishedCount) }) }}
</p>
</section>
<!-- 外观设置卡(主题暗/浅/柔 + 语言中/英,分段切换,即时持久化全局生效)。
HJ-FE-DS-001 决议②落地入口:AppearanceSwitch 内部已实现切换+持久化,本页仅放置。 -->
<section class="profile__appearance">
<AppearanceSwitch />
</section>
<!-- 功能入口列表 -->
<nav class="profile__menu">
<!-- 我的项目(保留既有项目列表全功能) -->
<button class="profile__item" @click="goProjects">
<span class="profile__item-icon">◰</span>
<span class="profile__item-label">我的项目</span>
<span class="profile__item-chevron">›</span>
<Icon class="profile__item-icon" name="projects" :size="18" />
<span class="profile__item-label">{{ t('profile.menu.projects') }}</span>
<Icon class="profile__item-chevron" name="chevron" :size="18" />
</button>
<!-- 创作数据看板(P-OPS-01/02:累计播放/点赞/作品/奖励 + 七日趋势占位) -->
<button class="profile__item" @click="goDashboard">
<span class="profile__item-icon">▤</span>
<span class="profile__item-label">创作数据</span>
<span class="profile__item-chevron">›</span>
<Icon class="profile__item-icon" name="dashboard" :size="18" />
<span class="profile__item-label">{{ t('profile.menu.dashboard') }}</span>
<Icon class="profile__item-chevron" name="chevron" :size="18" />
</button>
<!-- 企业定制(B 端定制承接:提单/进度看板/试玩验收,P-BIZ-01~04) -->
<button class="profile__item" @click="goBiz">
<span class="profile__item-icon">◳</span>
<span class="profile__item-label">企业定制</span>
<span class="profile__item-chevron">›</span>
<Icon class="profile__item-icon" name="enterprise" :size="18" />
<span class="profile__item-label">{{ t('profile.menu.biz') }}</span>
<Icon class="profile__item-chevron" name="chevron" :size="18" />
</button>
<!-- 消息(与底部 Tab 同入口,带未读角标,方便从个人中心进入) -->
<button class="profile__item" @click="goMessages">
<span class="profile__item-icon">✉</span>
<span class="profile__item-label">消息</span>
<Icon class="profile__item-icon" name="message" :size="18" />
<span class="profile__item-label">{{ t('profile.menu.message') }}</span>
<span v-if="messageStore.unreadTotal > 0" class="profile__item-badge">
{{ messageStore.unreadTotal > 99 ? '99+' : messageStore.unreadTotal }}
</span>
<span class="profile__item-chevron">›</span>
<Icon class="profile__item-chevron" name="chevron" :size="18" />
</button>
<!-- 设置(占位,本波未实现) -->
<button class="profile__item" @click="onPlaceholder('设置')">
<span class="profile__item-icon">⚙</span>
<span class="profile__item-label">设置</span>
<span class="profile__item-chevron">›</span>
<button class="profile__item" @click="onPlaceholder(t('profile.menu.settings'))">
<Icon class="profile__item-icon" name="settings" :size="18" />
<span class="profile__item-label">{{ t('profile.menu.settings') }}</span>
<Icon class="profile__item-chevron" name="chevron" :size="18" />
</button>
<!-- 帮助与反馈(占位,本波未实现) -->
<button class="profile__item" @click="onPlaceholder('帮助与反馈')">
<span class="profile__item-icon">?</span>
<span class="profile__item-label">帮助与反馈</span>
<span class="profile__item-chevron">›</span>
<button class="profile__item" @click="onPlaceholder(t('profile.menu.help'))">
<Icon class="profile__item-icon" name="help" :size="18" />
<span class="profile__item-label">{{ t('profile.menu.help') }}</span>
<Icon class="profile__item-chevron" name="chevron" :size="18" />
</button>
</nav>
<!-- 退出登录(仅真实登录态显示;匿名/批跑 env-token 态不渲染) -->
<div v-if="showLogout" class="profile__logout">
<AppButton type="ghost" block @click="onLogout">退出登录</AppButton>
<AppButton type="ghost" block @click="onLogout">{{ t('profile.logout') }}</AppButton>
</div>
</div>
</template>
@ -247,7 +265,8 @@ onMounted(() => {
flex: 0 0 60px;
width: 60px;
height: 60px;
border-radius: 50%;
/* 统一圆角矩形(设计体系禁正圆/药丸;原 50% 正圆改 --radius-lg 圆角方) */
border-radius: var(--radius-lg);
background-color: var(--panel-2);
background-size: cover;
background-position: center;
@ -283,10 +302,19 @@ onMounted(() => {
margin-top: 6px;
padding: 0 8px;
font-size: 11px;
color: var(--cyan);
border: 1px solid var(--cyan);
border-radius: var(--radius);
background: color-mix(in srgb, var(--cyan) 10%, transparent);
color: var(--accent);
border: 1px solid var(--accent);
border-radius: var(--radius-sm);
background: color-mix(in srgb, var(--accent) 10%, transparent);
}
/* 外观设置卡(主题 + 语言切换;与下方菜单卡同一视觉规格) */
.profile__appearance {
margin: 0 12px 12px;
padding: 14px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
}
/* 创作成长卡 */
@ -295,7 +323,7 @@ onMounted(() => {
padding: 14px;
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
border-radius: var(--radius-lg);
}
.profile__level-row {
display: flex;
@ -310,7 +338,7 @@ onMounted(() => {
.profile__level-val {
font-size: 15px;
font-weight: 700;
color: var(--green);
color: var(--success);
}
.profile__level-next {
margin: 6px 0 0;
@ -325,7 +353,7 @@ onMounted(() => {
flex-direction: column;
background: var(--panel);
border: 1px solid var(--border);
border-radius: var(--radius);
border-radius: var(--radius-lg);
overflow: hidden;
}
.profile__item {
@ -344,11 +372,10 @@ onMounted(() => {
.profile__item:active {
background: var(--panel-2);
}
/* 行首图标(已改为 <Icon> SVG,currentColor 跟随此处 color;尺寸由组件 :size 控制) */
.profile__item-icon {
flex: 0 0 22px;
font-size: 17px;
color: var(--cyan);
text-align: center;
color: var(--accent);
}
.profile__item-label {
flex: 1;
@ -365,15 +392,16 @@ onMounted(() => {
font-size: 11px;
font-weight: 700;
line-height: 1;
color: #2a0610;
background: var(--pink);
border-radius: 9px;
/* danger/pink 之上的近黑前景令牌(原 #2a0610 硬编码) */
color: var(--on-danger);
background: var(--danger);
/* 统一圆角矩形(原 9px 近正圆药丸改 --radius-xs) */
border-radius: var(--radius-xs);
}
/* 行尾箭头(已改为 <Icon name="chevron">;color 跟随 muted) */
.profile__item-chevron {
flex: 0 0 auto;
font-size: 20px;
color: var(--muted);
line-height: 1;
}
/* 退出登录 */