feat(nav): add in-dev sections and coming soon placeholders

Add navigation links for Dish, Events, Actions, Dream Island, and Clothes.
Implement StatusBadge component and ComingSoonView for future content.
This commit is contained in:
2026-05-02 07:55:04 +08:00
parent ec2a21bae6
commit f5ab96c2b1
9 changed files with 564 additions and 5 deletions

View File

@@ -5,7 +5,7 @@
- Pokopia Wiki 是一个面向 Pokopia 游戏资料的社区 Wiki。 - Pokopia Wiki 是一个面向 Pokopia 游戏资料的社区 Wiki。
- 所有人都可以浏览 Wiki 内容。 - 所有人都可以浏览 Wiki 内容。
- 已注册并完成邮箱验证的用户可以创建、编辑、删除 Wiki 内容。 - 已注册并完成邮箱验证的用户可以创建、编辑、删除 Wiki 内容。
- 前台以 Pokemon、栖息地、物品、材料单、每日 CheckList、Life 为主要浏览入口。 - 前台以 Pokemon、栖息地、物品、材料单、每日 CheckList、Life、Dish、Events、Actions、Dream Island、Clothes 为主要浏览入口。
- 管理入口用于维护全局配置、语言、列表排序和每日 CheckList。 - 管理入口用于维护全局配置、语言、列表排序和每日 CheckList。
## 技术栈 ## 技术栈
@@ -404,6 +404,18 @@ API 暴露边界:
- 非作者不能编辑或删除其他用户的 Life Post。 - 非作者不能编辑或删除其他用户的 Life Post。
- 非作者不能删除其他用户的 Life Comment。 - 非作者不能删除其他用户的 Life Comment。
## 开发中入口
以下前台公开入口当前仅展示“正在开发中”占位页,不提供数据模型、后端 API、编辑表单、管理入口或排序能力
- Dish
- Events
- Actions游戏内快捷动作例如挥手、跳舞等。
- Dream Island
- Clothes
这些开发中入口在主导航和占位页中显示状态 Badge便于用户识别当前功能状态。
## 前端交互与 UI ## 前端交互与 UI
- UI 风格以 `DesignGuidelines.html` 为准。 - UI 风格以 `DesignGuidelines.html` 为准。

View File

@@ -3,7 +3,20 @@ import { computed, onMounted, onUnmounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import AppShell from './components/AppShell.vue'; import AppShell from './components/AppShell.vue';
import { iconAdmin, iconChecklist, iconHabitat, iconItem, iconLife, iconPokemon, iconRecipe } from './icons'; import {
iconAction,
iconAdmin,
iconChecklist,
iconClothes,
iconDish,
iconDreamIsland,
iconEvent,
iconHabitat,
iconItem,
iconLife,
iconPokemon,
iconRecipe
} from './icons';
import { getCurrentLocale, onLocaleChange, setCurrentLocale } from './i18n'; import { getCurrentLocale, onLocaleChange, setCurrentLocale } from './i18n';
import { api, getAuthToken, onAuthTokenChange, setAuthToken, type AuthUser, type Language } from './services/api'; import { api, getAuthToken, onAuthTokenChange, setAuthToken, type AuthUser, type Language } from './services/api';
@@ -18,11 +31,20 @@ const languages = ref<Language[]>([
let removeAuthListener: (() => void) | null = null; let removeAuthListener: (() => void) | null = null;
let removeLocaleListener: (() => void) | null = null; let removeLocaleListener: (() => void) | null = null;
function inDevBadge() {
return { label: t('common.inDev'), tone: 'info' as const };
}
const navItems = computed(() => [ const navItems = computed(() => [
{ label: t('nav.pokemon'), to: '/pokemon', icon: iconPokemon }, { label: t('nav.pokemon'), to: '/pokemon', icon: iconPokemon },
{ label: t('nav.habitats'), to: '/habitats', icon: iconHabitat }, { label: t('nav.habitats'), to: '/habitats', icon: iconHabitat },
{ label: t('nav.items'), to: '/items', icon: iconItem }, { label: t('nav.items'), to: '/items', icon: iconItem },
{ label: t('nav.recipes'), to: '/recipes', icon: iconRecipe }, { label: t('nav.recipes'), to: '/recipes', icon: iconRecipe },
{ label: t('nav.dish'), to: '/dish', icon: iconDish, badge: inDevBadge() },
{ label: t('nav.events'), to: '/events', icon: iconEvent, badge: inDevBadge() },
{ label: t('nav.actions'), to: '/actions', icon: iconAction, badge: inDevBadge() },
{ label: t('nav.dreamIsland'), to: '/dream-island', icon: iconDreamIsland, badge: inDevBadge() },
{ label: t('nav.clothes'), to: '/clothes', icon: iconClothes, badge: inDevBadge() },
{ label: t('nav.checklist'), to: '/checklist', icon: iconChecklist }, { label: t('nav.checklist'), to: '/checklist', icon: iconChecklist },
{ label: t('nav.life'), to: '/life', icon: iconLife }, { label: t('nav.life'), to: '/life', icon: iconLife },
{ label: t('nav.admin'), to: '/admin', icon: iconAdmin } { label: t('nav.admin'), to: '/admin', icon: iconAdmin }

View File

@@ -6,12 +6,21 @@ import { useRoute } from 'vue-router';
import { iconClose, iconLogin, iconLogout, iconMenu, iconRegister, iconTranslate, type AppIcon } from '../icons'; import { iconClose, iconLogin, iconLogout, iconMenu, iconRegister, iconTranslate, type AppIcon } from '../icons';
import type { AuthUser, Language } from '../services/api'; import type { AuthUser, Language } from '../services/api';
import PokeBallMark from './PokeBallMark.vue'; import PokeBallMark from './PokeBallMark.vue';
import StatusBadge from './StatusBadge.vue';
defineProps<{ defineProps<{
currentUser: AuthUser | null; currentUser: AuthUser | null;
languages: Language[]; languages: Language[];
locale: string; locale: string;
navItems: Array<{ label: string; to: string; icon?: AppIcon }>; navItems: Array<{
label: string;
to: string;
icon?: AppIcon;
badge?: {
label: string;
tone?: 'info' | 'success' | 'warning' | 'danger' | 'neutral';
};
}>;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@@ -132,7 +141,14 @@ onBeforeUnmount(() => {
@click="closeSidebar" @click="closeSidebar"
> >
<Icon v-if="item.icon" :icon="item.icon" class="ui-icon side-nav__icon" aria-hidden="true" /> <Icon v-if="item.icon" :icon="item.icon" class="ui-icon side-nav__icon" aria-hidden="true" />
<span>{{ item.label }}</span> <span class="side-nav__label">{{ item.label }}</span>
<StatusBadge
v-if="item.badge"
class="side-nav__badge"
:label="item.badge.label"
:tone="item.badge.tone"
compact
/>
</RouterLink> </RouterLink>
</nav> </nav>

View File

@@ -0,0 +1,20 @@
<script setup lang="ts">
withDefaults(
defineProps<{
label: string;
tone?: 'info' | 'success' | 'warning' | 'danger' | 'neutral';
compact?: boolean;
}>(),
{
tone: 'info',
compact: false
}
);
</script>
<template>
<span class="status-badge" :class="[`status-badge--${tone}`, { 'status-badge--compact': compact }]">
<span class="status-badge__dot" aria-hidden="true"></span>
<span class="status-badge__label">{{ label }}</span>
</span>
</template>

View File

@@ -35,6 +35,7 @@ const messages = {
noMatches: 'No matches', noMatches: 'No matches',
createNamed: 'Add "{name}"', createNamed: 'Add "{name}"',
creating: 'Adding', creating: 'Adding',
inDev: 'In-Dev',
removeNamed: 'Remove {name}', removeNamed: 'Remove {name}',
quantity: 'Quantity', quantity: 'Quantity',
required: 'Required' required: 'Required'
@@ -44,6 +45,11 @@ const messages = {
habitats: 'Habitats', habitats: 'Habitats',
items: 'Items', items: 'Items',
recipes: 'Recipes', recipes: 'Recipes',
dish: 'Dish',
events: 'Events',
actions: 'Actions',
dreamIsland: 'Dream Island',
clothes: 'Clothes',
checklist: 'CheckList', checklist: 'CheckList',
life: 'Life', life: 'Life',
admin: 'Admin', admin: 'Admin',
@@ -214,6 +220,68 @@ const messages = {
materials: 'Materials', materials: 'Materials',
addMaterial: 'Add material' addMaterial: 'Add material'
}, },
comingSoon: {
status: 'In development',
heading: 'This wiki section is being prepared.',
previewLabel: 'Section preview',
sections: {
dish: {
kicker: 'Dish',
title: 'Dish',
subtitle: 'A future home for cooked dishes and food discoveries.',
body: 'Dish pages are being shaped for clear browsing, source notes, and useful ingredient links.',
preview: {
one: 'Dish records will focus on names, effects, and discovery context.',
two: 'Ingredient relationships will connect back to items and recipes where useful.',
three: 'The page will stay browse-first so community edits can grow naturally.'
}
},
events: {
kicker: 'Events',
title: 'Events',
subtitle: 'Seasonal and limited-time game activity records are coming later.',
body: 'Events will collect timing, rewards, and participation details once the section is ready.',
preview: {
one: 'Event cards will make dates and active windows easy to scan.',
two: 'Rewards and related items will sit close to the event summary.',
three: 'Archived activities will remain readable after they end.'
}
},
actions: {
kicker: 'Actions',
title: 'Actions',
subtitle: 'Game shortcut actions such as waving and dancing will be documented here.',
body: 'Actions are being prepared as a quick reference for expressive in-game gestures and shortcuts.',
preview: {
one: 'Each action will describe the gesture or shortcut in player-facing language.',
two: 'Common examples include waving, dancing, and other social actions.',
three: 'Related unlock or usage details can be linked when the data model is ready.'
}
},
dreamIsland: {
kicker: 'Dream Island',
title: 'Dream Island',
subtitle: 'Dream Island information is being organized for future browsing.',
body: 'This area will present island details with a calm, destination-style layout when content is ready.',
preview: {
one: 'Island notes will prioritize location, availability, and notable discoveries.',
two: 'Related Pokemon, items, or activities can be connected from the page.',
three: 'The layout will support browsing without adding another management flow yet.'
}
},
clothes: {
kicker: 'Clothes',
title: 'Clothes',
subtitle: 'Outfit and clothing references are being prepared.',
body: 'Clothes pages will make it easy to compare appearance, acquisition, and customization details.',
preview: {
one: 'Clothing entries will focus on display names and visual categories.',
two: 'Acquisition and customization details can be connected when available.',
three: 'The page will keep item-like details readable without mixing them into the item list.'
}
}
}
},
checklist: { checklist: {
title: 'Daily checklist', title: 'Daily checklist',
subtitle: 'See what can be completed each day.', subtitle: 'See what can be completed each day.',
@@ -396,6 +464,7 @@ const messages = {
noMatches: '没有匹配项', noMatches: '没有匹配项',
createNamed: '添加「{name}」', createNamed: '添加「{name}」',
creating: '添加中', creating: '添加中',
inDev: '开发中',
removeNamed: '移除{name}', removeNamed: '移除{name}',
quantity: '数量', quantity: '数量',
required: '必填' required: '必填'
@@ -405,6 +474,11 @@ const messages = {
habitats: '栖息地', habitats: '栖息地',
items: '物品', items: '物品',
recipes: '材料单', recipes: '材料单',
dish: '料理',
events: '活动',
actions: '动作',
dreamIsland: 'Dream Island',
clothes: '服装',
checklist: 'CheckList', checklist: 'CheckList',
life: 'Life', life: 'Life',
admin: '管理', admin: '管理',
@@ -575,6 +649,68 @@ const messages = {
materials: '需要材料', materials: '需要材料',
addMaterial: '添加材料' addMaterial: '添加材料'
}, },
comingSoon: {
status: '正在开发中',
heading: '这个 Wiki 分区正在准备中。',
previewLabel: '分区预览',
sections: {
dish: {
kicker: 'Dish',
title: '料理',
subtitle: '未来会用于整理料理和食物相关发现。',
body: '料理页面会围绕清晰浏览、来源记录和材料关联来设计。',
preview: {
one: '料理记录会优先呈现名称、效果和发现方式。',
two: '需要时会把材料关系连接回物品和材料单。',
three: '页面会先保持浏览友好,后续再自然承接社区编辑内容。'
}
},
events: {
kicker: 'Events',
title: '活动',
subtitle: '季节活动和限时内容资料会在这里整理。',
body: '活动分区会在准备好后集中展示时间、奖励和参与信息。',
preview: {
one: '活动卡片会让日期和开放时间更容易浏览。',
two: '奖励与关联物品会靠近活动摘要展示。',
three: '活动结束后,历史记录也会保持可读。'
}
},
actions: {
kicker: 'Actions',
title: '动作',
subtitle: '挥手、跳舞等游戏内快捷动作会记录在这里。',
body: '动作分区会作为游戏内表情、社交动作和快捷动作的快速参考。',
preview: {
one: '每个动作会用面向玩家的语言说明动作或快捷方式。',
two: '常见内容包括挥手、跳舞和其他社交动作。',
three: '后续可在数据模型准备好后补充解锁或使用条件。'
}
},
dreamIsland: {
kicker: 'Dream Island',
title: 'Dream Island',
subtitle: 'Dream Island 相关资料正在整理。',
body: '这个区域未来会用更像目的地资料页的方式展示岛屿信息。',
preview: {
one: '岛屿记录会优先整理地点、开放状态和重要发现。',
two: '可关联的 Pokemon、物品或活动会从页面中连接出来。',
three: '目前先保持公开浏览入口,不额外增加管理流程。'
}
},
clothes: {
kicker: 'Clothes',
title: '服装',
subtitle: '外观和服装资料正在准备。',
body: '服装页面会用于对比外观、入手方式和自定义信息。',
preview: {
one: '服装条目会优先整理展示名称和视觉分类。',
two: '入手方式与自定义信息会在资料可用后接入。',
three: '页面会保持服装资料清晰,不和普通物品列表混在一起。'
}
}
}
},
checklist: { checklist: {
title: '每日清单', title: '每日清单',
subtitle: '查看每天可以完成的事项。', subtitle: '查看每天可以完成的事项。',

View File

@@ -2,6 +2,7 @@ export type AppIcon = string;
export const iconAdd: AppIcon = 'mdi:plus'; export const iconAdd: AppIcon = 'mdi:plus';
export const iconAdmin: AppIcon = 'mdi:tune-variant'; export const iconAdmin: AppIcon = 'mdi:tune-variant';
export const iconAction: AppIcon = 'mdi:gesture-tap-button';
export const iconBack: AppIcon = 'mdi:arrow-left'; export const iconBack: AppIcon = 'mdi:arrow-left';
export const iconCancel: AppIcon = 'mdi:close'; export const iconCancel: AppIcon = 'mdi:close';
export const iconCheck: AppIcon = 'mdi:check'; export const iconCheck: AppIcon = 'mdi:check';
@@ -10,13 +11,17 @@ export const iconChevronDown: AppIcon = 'mdi:chevron-down';
export const iconClose: AppIcon = 'mdi:close'; export const iconClose: AppIcon = 'mdi:close';
export const iconComment: AppIcon = 'mdi:comment-outline'; export const iconComment: AppIcon = 'mdi:comment-outline';
export const iconDelete: AppIcon = 'mdi:trash-can-outline'; export const iconDelete: AppIcon = 'mdi:trash-can-outline';
export const iconDish: AppIcon = 'mdi:silverware-fork-knife';
export const iconDragHandle: AppIcon = 'mdi:drag'; export const iconDragHandle: AppIcon = 'mdi:drag';
export const iconDreamIsland: AppIcon = 'mdi:palm-tree';
export const iconEdit: AppIcon = 'mdi:pencil-outline'; export const iconEdit: AppIcon = 'mdi:pencil-outline';
export const iconError: AppIcon = 'mdi:close-circle-outline'; export const iconError: AppIcon = 'mdi:close-circle-outline';
export const iconEvent: AppIcon = 'mdi:calendar-star';
export const iconHabitat: AppIcon = 'mdi:pine-tree'; export const iconHabitat: AppIcon = 'mdi:pine-tree';
export const iconInfo: AppIcon = 'mdi:information-outline'; export const iconInfo: AppIcon = 'mdi:information-outline';
export const iconItem: AppIcon = 'mdi:bag-personal-outline'; export const iconItem: AppIcon = 'mdi:bag-personal-outline';
export const iconLife: AppIcon = 'mdi:post-outline'; export const iconLife: AppIcon = 'mdi:post-outline';
export const iconClothes: AppIcon = 'mdi:tshirt-crew-outline';
export const iconLogin: AppIcon = 'mdi:login'; export const iconLogin: AppIcon = 'mdi:login';
export const iconLogout: AppIcon = 'mdi:logout'; export const iconLogout: AppIcon = 'mdi:logout';
export const iconMail: AppIcon = 'mdi:email-fast-outline'; export const iconMail: AppIcon = 'mdi:email-fast-outline';

View File

@@ -9,6 +9,7 @@ import RecipeList from '../views/RecipeList.vue';
import RecipeDetail from '../views/RecipeDetail.vue'; import RecipeDetail from '../views/RecipeDetail.vue';
import DailyChecklistView from '../views/DailyChecklistView.vue'; import DailyChecklistView from '../views/DailyChecklistView.vue';
import LifeView from '../views/LifeView.vue'; import LifeView from '../views/LifeView.vue';
import ComingSoonView from '../views/ComingSoonView.vue';
import AdminView from '../views/AdminView.vue'; import AdminView from '../views/AdminView.vue';
import LoginView from '../views/LoginView.vue'; import LoginView from '../views/LoginView.vue';
import RegisterView from '../views/RegisterView.vue'; import RegisterView from '../views/RegisterView.vue';
@@ -35,6 +36,11 @@ export const router = createRouter({
{ path: '/recipes/new', name: 'recipe-new', component: RecipeList, meta: { requiresVerified: true, editorModal: true } }, { path: '/recipes/new', name: 'recipe-new', component: RecipeList, meta: { requiresVerified: true, editorModal: true } },
{ path: '/recipes/:id/edit', name: 'recipe-edit', component: RecipeDetail, meta: { requiresVerified: true, editorModal: true } }, { path: '/recipes/:id/edit', name: 'recipe-edit', component: RecipeDetail, meta: { requiresVerified: true, editorModal: true } },
{ path: '/recipes/:id', name: 'recipe-detail', component: RecipeDetail }, { path: '/recipes/:id', name: 'recipe-detail', component: RecipeDetail },
{ path: '/dish', name: 'dish', component: ComingSoonView, props: { page: 'dish' } },
{ path: '/events', name: 'events', component: ComingSoonView, props: { page: 'events' } },
{ path: '/actions', name: 'actions', component: ComingSoonView, props: { page: 'actions' } },
{ path: '/dream-island', name: 'dream-island', component: ComingSoonView, props: { page: 'dreamIsland' } },
{ path: '/clothes', name: 'clothes', component: ComingSoonView, props: { page: 'clothes' } },
{ path: '/checklist', component: DailyChecklistView }, { path: '/checklist', component: DailyChecklistView },
{ path: '/life', component: LifeView }, { path: '/life', component: LifeView },
{ path: '/admin', component: AdminView, meta: { requiresVerified: true } }, { path: '/admin', component: AdminView, meta: { requiresVerified: true } },

View File

@@ -208,11 +208,32 @@ svg {
box-shadow: 0 2px 0 var(--line-strong); box-shadow: 0 2px 0 var(--line-strong);
} }
.side-nav__label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.side-nav__icon { .side-nav__icon {
width: 19px; width: 19px;
height: 19px; height: 19px;
} }
.side-nav__badge {
margin-left: auto;
flex: 0 0 auto;
}
.side-nav__link.router-link-active .status-badge {
border-color: rgba(255, 255, 255, 0.34);
background: rgba(255, 255, 255, 0.16);
color: #ffffff;
}
.side-nav__link.router-link-active .status-badge__dot {
background: var(--pokemon-yellow);
}
.auth-actions { .auth-actions {
display: grid; display: grid;
align-content: end; align-content: end;
@@ -1902,6 +1923,222 @@ button:disabled,
font-weight: 800; font-weight: 800;
} }
.status-badge {
--status-color: var(--muted);
width: fit-content;
min-height: 28px;
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
border: 1px solid color-mix(in srgb, var(--status-color) 34%, var(--line));
border-radius: 999px;
background: color-mix(in srgb, var(--status-color) 10%, var(--surface-soft));
color: var(--ink-soft);
font-size: 12px;
font-weight: 950;
line-height: 1.1;
text-transform: uppercase;
white-space: nowrap;
}
.status-badge--compact {
min-height: 22px;
gap: 4px;
padding: 3px 6px;
font-size: 10px;
letter-spacing: 0;
}
.status-badge__dot {
width: 8px;
height: 8px;
flex: 0 0 auto;
border-radius: 50%;
background: var(--status-color);
}
.status-badge__label {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.status-badge--info {
--status-color: var(--pokemon-blue);
}
.status-badge--success {
--status-color: var(--success);
}
.status-badge--warning {
--status-color: var(--warning);
}
.status-badge--danger {
--status-color: var(--danger);
}
.status-badge--neutral {
--status-color: var(--muted);
}
.coming-soon-panel {
--soon-accent: var(--pokemon-blue);
--soon-accent-soft: color-mix(in srgb, var(--soon-accent) 14%, var(--surface));
position: relative;
min-height: 300px;
display: grid;
grid-template-columns: auto minmax(0, 1fr) minmax(160px, 0.32fr);
align-items: center;
gap: 20px;
overflow: hidden;
padding: 24px;
border: 2px solid var(--line-strong);
border-radius: var(--radius-card);
background:
linear-gradient(135deg, var(--soon-accent-soft), transparent 62%),
linear-gradient(180deg, var(--surface) 0%, var(--surface-soft) 100%);
box-shadow: var(--shadow-control);
}
.coming-soon-panel--dish {
--soon-accent: var(--pokemon-yellow);
}
.coming-soon-panel--events {
--soon-accent: var(--pokemon-red);
}
.coming-soon-panel--actions {
--soon-accent: var(--pokemon-blue);
}
.coming-soon-panel--dream {
--soon-accent: var(--success);
}
.coming-soon-panel--clothes {
--soon-accent: var(--type-psychic);
}
.coming-soon-panel__icon {
width: clamp(76px, 11vw, 118px);
aspect-ratio: 1;
display: grid;
place-items: center;
border: 2px solid var(--line-strong);
border-radius: var(--radius-card);
background: var(--soon-accent);
box-shadow: 0 5px 0 var(--line-strong);
color: #172036;
}
.coming-soon-panel--events .coming-soon-panel__icon,
.coming-soon-panel--actions .coming-soon-panel__icon,
.coming-soon-panel--dream .coming-soon-panel__icon,
.coming-soon-panel--clothes .coming-soon-panel__icon {
color: #ffffff;
}
.coming-soon-panel__icon .ui-icon {
width: 54%;
height: 54%;
}
.coming-soon-panel__copy {
min-width: 0;
display: grid;
gap: 12px;
}
.coming-soon-panel__copy h2 {
margin: 0;
color: var(--ink);
font-family: var(--font-display);
font-size: clamp(28px, 4vw, 46px);
font-weight: 950;
line-height: 1.05;
}
.coming-soon-panel__copy p {
max-width: 62ch;
margin: 0;
color: var(--ink-soft);
font-size: 17px;
line-height: 1.6;
}
.coming-soon-panel__signal {
height: 100%;
min-height: 180px;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
align-items: end;
gap: 8px;
}
.coming-soon-panel__signal span {
display: block;
border: 2px solid var(--line-strong);
border-radius: var(--radius-small);
background:
linear-gradient(180deg, color-mix(in srgb, var(--soon-accent) 72%, #ffffff), var(--soon-accent)),
var(--soon-accent);
box-shadow: 0 3px 0 var(--line-strong);
}
.coming-soon-panel__signal span:nth-child(1) {
height: 46%;
}
.coming-soon-panel__signal span:nth-child(2) {
height: 72%;
}
.coming-soon-panel__signal span:nth-child(3) {
height: 58%;
}
.coming-soon-preview {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 14px;
}
.coming-soon-preview__item {
min-height: 128px;
display: grid;
align-content: start;
gap: 10px;
padding: 16px;
border: 1px solid var(--line);
border-radius: var(--radius-card);
background: var(--surface);
box-shadow: var(--shadow-soft);
}
.coming-soon-preview__index {
width: 42px;
min-height: 30px;
display: inline-grid;
place-items: center;
border: 2px solid var(--line-strong);
border-radius: var(--radius-small);
background: var(--surface-soft);
color: var(--pokemon-blue-deep);
font-size: 13px;
font-weight: 950;
}
.coming-soon-preview__item p {
margin: 0;
color: var(--ink-soft);
font-weight: 800;
line-height: 1.5;
}
.reorderable-row { .reorderable-row {
position: relative; position: relative;
flex-wrap: wrap; flex-wrap: wrap;
@@ -3077,6 +3314,19 @@ button:disabled,
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.coming-soon-panel {
grid-template-columns: auto minmax(0, 1fr);
}
.coming-soon-panel__signal {
grid-column: 1 / -1;
min-height: 94px;
}
.coming-soon-preview {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.appearance-row__main { .appearance-row__main {
grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-columns: repeat(2, minmax(0, 1fr));
} }
@@ -3106,7 +3356,8 @@ button:disabled,
.filter-panel, .filter-panel,
.toolbar, .toolbar,
.entity-grid, .entity-grid,
.grid { .grid,
.coming-soon-preview {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@@ -3114,6 +3365,15 @@ button:disabled,
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.coming-soon-panel {
grid-template-columns: 1fr;
padding: 18px;
}
.coming-soon-panel__signal {
min-height: 76px;
}
.life-post__header { .life-post__header {
grid-template-columns: auto minmax(0, 1fr); grid-template-columns: auto minmax(0, 1fr);
} }

View File

@@ -0,0 +1,82 @@
<script setup lang="ts">
import { Icon } from '@iconify/vue';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import PageHeader from '../components/PageHeader.vue';
import StatusBadge from '../components/StatusBadge.vue';
import {
iconAction,
iconClothes,
iconDish,
iconDreamIsland,
iconEvent,
type AppIcon
} from '../icons';
type ComingSoonPage = 'dish' | 'events' | 'actions' | 'dreamIsland' | 'clothes';
type ComingSoonConfig = {
icon: AppIcon;
accent: 'dish' | 'events' | 'actions' | 'dream' | 'clothes';
previewKeys: Array<'one' | 'two' | 'three'>;
};
const props = defineProps<{
page: ComingSoonPage;
}>();
const { t } = useI18n();
const pageConfigByPage: Record<ComingSoonPage, ComingSoonConfig> = {
dish: { icon: iconDish, accent: 'dish', previewKeys: ['one', 'two', 'three'] },
events: { icon: iconEvent, accent: 'events', previewKeys: ['one', 'two', 'three'] },
actions: { icon: iconAction, accent: 'actions', previewKeys: ['one', 'two', 'three'] },
dreamIsland: { icon: iconDreamIsland, accent: 'dream', previewKeys: ['one', 'two', 'three'] },
clothes: { icon: iconClothes, accent: 'clothes', previewKeys: ['one', 'two', 'three'] }
};
const pageConfig = computed(() => pageConfigByPage[props.page]);
const previewItems = computed(() =>
pageConfig.value.previewKeys.map((previewKey, index) => ({
code: String(index + 1).padStart(2, '0'),
text: t(pageMessageKey(`preview.${previewKey}`))
}))
);
function pageMessageKey(suffix: string) {
return `pages.comingSoon.sections.${props.page}.${suffix}`;
}
</script>
<template>
<section class="page-stack coming-soon-page">
<PageHeader :title="t(pageMessageKey('title'))" :subtitle="t(pageMessageKey('subtitle'))">
<template #kicker>{{ t(pageMessageKey('kicker')) }}</template>
</PageHeader>
<section class="coming-soon-panel" :class="`coming-soon-panel--${pageConfig.accent}`">
<div class="coming-soon-panel__icon" aria-hidden="true">
<Icon :icon="pageConfig.icon" class="ui-icon" />
</div>
<div class="coming-soon-panel__copy">
<StatusBadge :label="t('pages.comingSoon.status')" tone="info" />
<h2>{{ t('pages.comingSoon.heading') }}</h2>
<p>{{ t(pageMessageKey('body')) }}</p>
</div>
<div class="coming-soon-panel__signal" aria-hidden="true">
<span></span>
<span></span>
<span></span>
</div>
</section>
<section class="coming-soon-preview" :aria-label="t('pages.comingSoon.previewLabel')">
<article v-for="item in previewItems" :key="item.code" class="coming-soon-preview__item">
<span class="coming-soon-preview__index">{{ item.code }}</span>
<p>{{ item.text }}</p>
</article>
</section>
</section>
</template>