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:
parent
655673eb99
commit
caa2e1ba03
@ -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 渲染默认按钮 -->
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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(不报错)
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
31
game-studio/src/locales/create.en.ts
Normal file
31
game-studio/src/locales/create.en.ts
Normal 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',
|
||||
},
|
||||
}
|
||||
32
game-studio/src/locales/create.zh.ts
Normal file
32
game-studio/src/locales/create.zh.ts
Normal 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: '生成约需数秒,期间可在进度页查看状态',
|
||||
},
|
||||
}
|
||||
40
game-studio/src/locales/feed.en.ts
Normal file
40
game-studio/src/locales/feed.en.ts
Normal 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',
|
||||
},
|
||||
}
|
||||
41
game-studio/src/locales/feed.zh.ts
Normal file
41
game-studio/src/locales/feed.zh.ts
Normal 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: '赞',
|
||||
},
|
||||
}
|
||||
30
game-studio/src/locales/profile.en.ts
Normal file
30
game-studio/src/locales/profile.en.ts
Normal 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?',
|
||||
},
|
||||
}
|
||||
33
game-studio/src/locales/profile.zh.ts
Normal file
33
game-studio/src/locales/profile.zh.ts
Normal 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: '退出后需重新登录才能查看你的项目与创作,确定退出吗?',
|
||||
},
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
/* 退出登录 */
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user