refactor(admin): redesign system wording layout with sidebar and tabs

Replace module and surface dropdowns with a sidebar and tabbed interface.
Improve navigation and usability in the admin wording section.
This commit is contained in:
2026-05-02 11:57:59 +08:00
parent 976a2a2482
commit 475e3577dd
2 changed files with 199 additions and 47 deletions

View File

@@ -2409,15 +2409,98 @@ button:disabled,
font-weight: 850;
}
.system-wording-toolbar {
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
.system-wording-header {
align-items: end;
}
.system-wording-header__locale {
width: min(260px, 100%);
}
.system-wording-layout {
display: grid;
grid-template-columns: minmax(180px, 240px) minmax(0, 1fr);
gap: 16px;
align-items: start;
}
.system-wording-sidebar {
min-width: 0;
display: grid;
align-content: start;
gap: 6px;
padding: 10px;
border: 1px solid var(--line);
border-radius: var(--radius-card);
background: var(--surface-soft);
}
.system-wording-sidebar__title {
padding: 2px 4px 4px;
color: var(--muted);
font-size: 13px;
font-weight: 900;
}
.system-wording-sidebar__button {
width: 100%;
min-height: 44px;
display: flex;
align-items: center;
justify-content: flex-start;
padding: 9px 10px;
border: 1px solid transparent;
border-radius: var(--radius-control);
background: transparent;
color: var(--ink-soft);
font-weight: 850;
text-align: left;
overflow-wrap: anywhere;
cursor: pointer;
}
.system-wording-sidebar__button:hover {
border-color: rgba(42, 117, 187, 0.24);
background: rgba(255, 203, 5, 0.2);
color: var(--pokemon-blue-deep);
}
.system-wording-sidebar__button.active {
border-color: var(--line-strong);
background: var(--pokemon-blue);
color: #ffffff;
box-shadow: 0 2px 0 var(--line-strong);
}
.system-wording-sidebar__button:disabled {
cursor: not-allowed;
opacity: 0.54;
}
.system-wording-content {
min-width: 0;
display: grid;
gap: 12px;
}
.system-wording-controls {
display: flex;
align-items: start;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
}
.system-wording-controls .tabs--component {
flex: 1 1 320px;
min-width: 0;
}
.system-wording-toolbar__check {
min-height: 44px;
display: flex;
align-items: center;
justify-content: flex-end;
}
.system-wording-list li {
@@ -3795,10 +3878,22 @@ button:disabled,
.pokemon-profile-grid,
.pokemon-profile-row,
.pokemon-related-grid,
.system-wording-layout,
.admin-layout {
grid-template-columns: 1fr;
}
.system-wording-sidebar {
display: flex;
overflow-x: auto;
}
.system-wording-sidebar__button {
width: auto;
flex: 0 0 auto;
white-space: nowrap;
}
.coming-soon-panel {
grid-template-columns: auto minmax(0, 1fr);
}
@@ -3884,6 +3979,32 @@ button:disabled,
width: 100%;
}
.system-wording-header {
align-items: stretch;
flex-direction: column;
}
.system-wording-header__locale {
width: 100%;
}
.system-wording-controls .tabs--component {
flex-basis: 100%;
}
.system-wording-toolbar__check {
justify-content: flex-start;
}
.system-wording-list li {
align-items: stretch;
flex-direction: column;
}
.system-wording-list .row-actions {
justify-content: flex-start;
}
.life-feed__list {
width: 100%;
}

View File

@@ -161,6 +161,18 @@ const wordingLocaleOptions = computed(() =>
]
);
const wordingModules = computed(() => [...new Set(wordingRows.value.map((item) => item.module))].sort((a, b) => a.localeCompare(b)));
const wordingSurfaceTabs = computed<TabOption[]>(() => [
{ value: '', label: t('pages.admin.allSurfaces') },
{ value: 'frontend', label: t('pages.admin.surfaceFrontend') },
{ value: 'backend', label: t('pages.admin.surfaceBackend') },
{ value: 'email', label: t('pages.admin.surfaceEmail') }
]);
const activeWordingSurfaceTab = computed({
get: () => wordingSurface.value,
set: (value: string) => {
wordingSurface.value = value === 'frontend' || value === 'backend' || value === 'email' ? value : '';
}
});
const filteredWordingRows = computed(() =>
wordingRows.value.filter((item) => {
if (wordingModule.value && item.module !== wordingModule.value) return false;
@@ -230,6 +242,10 @@ function resetWordingForm() {
wordingForm.value = { key: '', locale: wordingLocale.value || defaultLocale, value: '', defaultValue: '', placeholders: [] };
}
function selectWordingModule(module: string) {
wordingModule.value = module;
}
function openNewConfig() {
resetConfigForm();
configModalOpen.value = true;
@@ -817,11 +833,9 @@ onMounted(() => {
</section>
<section v-else-if="canEdit && activeTab === 'wordings'" class="detail-section">
<div class="detail-section__header">
<div class="detail-section__header system-wording-header">
<h2>{{ t('pages.admin.wordings') }}</h2>
</div>
<div class="toolbar system-wording-toolbar">
<div class="field">
<div class="field system-wording-header__locale">
<label for="wording-locale">{{ t('pages.admin.wordingLocale') }}</label>
<select id="wording-locale" v-model="wordingLocale" :disabled="busy" @change="reloadWordings">
<option v-for="language in wordingLocaleOptions" :key="language.code" :value="language.code">
@@ -829,49 +843,66 @@ onMounted(() => {
</option>
</select>
</div>
<div class="field">
<label for="wording-module">{{ t('pages.admin.wordingModule') }}</label>
<select id="wording-module" v-model="wordingModule" :disabled="busy">
<option value="">{{ t('pages.admin.allModules') }}</option>
<option v-for="module in wordingModules" :key="module" :value="module">{{ module }}</option>
</select>
</div>
<div class="field">
<label for="wording-surface">{{ t('pages.admin.wordingSurface') }}</label>
<select id="wording-surface" v-model="wordingSurface" :disabled="busy">
<option value="">{{ t('pages.admin.allSurfaces') }}</option>
<option value="frontend">{{ t('pages.admin.surfaceFrontend') }}</option>
<option value="backend">{{ t('pages.admin.surfaceBackend') }}</option>
<option value="email">{{ t('pages.admin.surfaceEmail') }}</option>
</select>
</div>
<div class="check-row system-wording-toolbar__check">
<label>
<input v-model="wordingMissingOnly" type="checkbox" :disabled="busy" />
{{ t('pages.admin.wordingMissingOnly') }}
</label>
</div>
<div class="system-wording-layout">
<nav class="system-wording-sidebar" :aria-label="t('pages.admin.wordingModule')">
<span class="system-wording-sidebar__title">{{ t('pages.admin.wordingModule') }}</span>
<button
type="button"
class="system-wording-sidebar__button"
:class="{ active: !wordingModule }"
:aria-current="!wordingModule ? 'true' : undefined"
:disabled="busy"
@click="selectWordingModule('')"
>
{{ t('pages.admin.allModules') }}
</button>
<button
v-for="module in wordingModules"
:key="module"
type="button"
class="system-wording-sidebar__button"
:class="{ active: wordingModule === module }"
:aria-current="wordingModule === module ? 'true' : undefined"
:disabled="busy"
@click="selectWordingModule(module)"
>
{{ module }}
</button>
</nav>
<div class="system-wording-content">
<div class="system-wording-controls">
<Tabs id="admin-wording-surface" v-model="activeWordingSurfaceTab" :tabs="wordingSurfaceTabs" :label="t('pages.admin.wordingSurface')" />
<div class="check-row system-wording-toolbar__check">
<label>
<input v-model="wordingMissingOnly" type="checkbox" :disabled="busy" />
{{ t('pages.admin.wordingMissingOnly') }}
</label>
</div>
</div>
<ul v-if="filteredWordingRows.length" class="row-list system-wording-list">
<li v-for="item in filteredWordingRows" :key="item.key">
<span class="system-wording-row">
<strong>{{ item.key }}</strong>
<span class="system-wording-row__meta">
<span class="config-flag">{{ item.module }}</span>
<span class="config-flag">{{ t(`pages.admin.surface${item.surface.charAt(0).toUpperCase()}${item.surface.slice(1)}`) }}</span>
<span v-if="item.missing" class="config-flag">{{ t('pages.admin.missingTranslation') }}</span>
</span>
<span class="system-wording-row__value">{{ item.value || item.defaultValue }}</span>
</span>
<span class="row-actions">
<button type="button" :disabled="busy" @click="editWording(item)">
<Icon :icon="iconEdit" class="ui-icon" aria-hidden="true" />
{{ t('common.edit') }}
</button>
</span>
</li>
</ul>
<p v-else class="meta-line">{{ t('common.noRecords') }}</p>
</div>
</div>
<ul v-if="filteredWordingRows.length" class="row-list system-wording-list">
<li v-for="item in filteredWordingRows" :key="item.key">
<span class="system-wording-row">
<strong>{{ item.key }}</strong>
<span class="system-wording-row__meta">
<span class="config-flag">{{ item.module }}</span>
<span class="config-flag">{{ t(`pages.admin.surface${item.surface.charAt(0).toUpperCase()}${item.surface.slice(1)}`) }}</span>
<span v-if="item.missing" class="config-flag">{{ t('pages.admin.missingTranslation') }}</span>
</span>
<span class="system-wording-row__value">{{ item.value || item.defaultValue }}</span>
</span>
<span class="row-actions">
<button type="button" :disabled="busy" @click="editWording(item)">
<Icon :icon="iconEdit" class="ui-icon" aria-hidden="true" />
{{ t('common.edit') }}
</button>
</span>
</li>
</ul>
<p v-else class="meta-line">{{ t('common.noRecords') }}</p>
</section>
<section v-else-if="canEdit && activeTab === 'pokemon'" class="detail-section">