feat(history): add detailed edit history tracking and display panel
Record field-level before/after changes in wiki_edit_logs Replace EditMeta with EditHistoryPanel on entity detail pages Update detail views to use a sidebar layout for history
This commit is contained in:
112
frontend/src/components/EditHistoryPanel.vue
Normal file
112
frontend/src/components/EditHistoryPanel.vue
Normal file
@@ -0,0 +1,112 @@
|
||||
<script setup lang="ts">
|
||||
import type { EditHistoryAction, EditHistoryEntry, EditInfo, UserSummary } from '../services/api';
|
||||
|
||||
defineProps<{
|
||||
entity: EditInfo;
|
||||
history: EditHistoryEntry[];
|
||||
}>();
|
||||
|
||||
const actionLabels: Record<EditHistoryAction, string> = {
|
||||
create: '创建',
|
||||
update: '编辑',
|
||||
delete: '删除'
|
||||
};
|
||||
|
||||
function displayName(user: UserSummary | null): string {
|
||||
return user?.displayName ?? '系统';
|
||||
}
|
||||
|
||||
function actionLabel(action: EditHistoryAction): string {
|
||||
return actionLabels[action];
|
||||
}
|
||||
|
||||
function actionMark(action: EditHistoryAction): string {
|
||||
return actionLabels[action].charAt(0);
|
||||
}
|
||||
|
||||
function historySummary(entry: EditHistoryEntry): string {
|
||||
if (!entry.changes.length) {
|
||||
return actionLabel(entry.action);
|
||||
}
|
||||
|
||||
return entry.changes.map((change) => change.label).join('、');
|
||||
}
|
||||
|
||||
function formatDateTime(value: string): string {
|
||||
return new Intl.DateTimeFormat('zh-CN', {
|
||||
dateStyle: 'medium',
|
||||
timeStyle: 'short'
|
||||
}).format(new Date(value));
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="edit-history-panel" aria-labelledby="edit-history-panel-title">
|
||||
<div class="edit-history-panel__header">
|
||||
<h2 id="edit-history-panel-title">贡献记录</h2>
|
||||
</div>
|
||||
|
||||
<dl class="edit-history-summary">
|
||||
<div>
|
||||
<dt>由谁创建</dt>
|
||||
<dd>
|
||||
<strong>{{ displayName(entity.createdBy) }}</strong>
|
||||
<time :datetime="entity.createdAt">{{ formatDateTime(entity.createdAt) }}</time>
|
||||
</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>最后编辑</dt>
|
||||
<dd>
|
||||
<strong>{{ displayName(entity.updatedBy) }}</strong>
|
||||
<time :datetime="entity.updatedAt">{{ formatDateTime(entity.updatedAt) }}</time>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<section class="edit-history-list" aria-labelledby="edit-history-list-title">
|
||||
<h3 id="edit-history-list-title">编辑历史</h3>
|
||||
<ol v-if="history.length" class="edit-timeline">
|
||||
<li v-for="entry in history" :key="`${entry.action}-${entry.createdAt}-${entry.user?.id ?? 'system'}`">
|
||||
<span class="edit-timeline__avatar" aria-hidden="true">{{ actionMark(entry.action) }}</span>
|
||||
<div class="edit-timeline__body">
|
||||
<details class="edit-history-entry">
|
||||
<summary>
|
||||
<span class="edit-history-entry__title">{{ historySummary(entry) }}</span>
|
||||
</summary>
|
||||
|
||||
<div class="edit-history-entry__content">
|
||||
<dl v-if="entry.changes.length" class="edit-change-list">
|
||||
<div v-for="change in entry.changes" :key="`${change.label}-${change.before}-${change.after}`">
|
||||
<dt>{{ change.label }}</dt>
|
||||
<dd>
|
||||
<span class="edit-change-list__label">修改前</span>
|
||||
<span>{{ change.before }}</span>
|
||||
<span class="edit-change-list__label">修改后</span>
|
||||
<span>{{ change.after }}</span>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<dl class="edit-history-detail-meta">
|
||||
<div>
|
||||
<dt>作者</dt>
|
||||
<dd>{{ displayName(entry.user) }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>时间</dt>
|
||||
<dd><time :datetime="entry.createdAt">{{ formatDateTime(entry.createdAt) }}</time></dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>动作</dt>
|
||||
<dd>{{ actionLabel(entry.action) }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
<p v-else class="meta-line">暂无编辑历史</p>
|
||||
</section>
|
||||
</aside>
|
||||
</template>
|
||||
@@ -23,6 +23,21 @@ export interface EditInfo {
|
||||
updatedBy: UserSummary | null;
|
||||
}
|
||||
|
||||
export type EditHistoryAction = 'create' | 'update' | 'delete';
|
||||
|
||||
export interface EditChange {
|
||||
label: string;
|
||||
before: string;
|
||||
after: string;
|
||||
}
|
||||
|
||||
export interface EditHistoryEntry {
|
||||
action: EditHistoryAction;
|
||||
changes: EditChange[];
|
||||
createdAt: string;
|
||||
user: UserSummary | null;
|
||||
}
|
||||
|
||||
export interface Pokemon extends EditInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -34,6 +49,7 @@ export interface Pokemon extends EditInfo {
|
||||
export interface PokemonDetail extends Pokemon {
|
||||
skills: Array<Skill & { itemDrop: NamedEntity | null }>;
|
||||
favoriteThingItems: Array<NamedEntity & { category: NamedEntity; tags: NamedEntity[] }>;
|
||||
editHistory: EditHistoryEntry[];
|
||||
habitats: Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -52,6 +68,7 @@ export interface Habitat extends EditInfo {
|
||||
}
|
||||
|
||||
export interface HabitatDetail extends Habitat {
|
||||
editHistory: EditHistoryEntry[];
|
||||
pokemon: Array<NamedEntity & {
|
||||
time_of_day: string;
|
||||
weather: string;
|
||||
@@ -96,6 +113,7 @@ export interface ItemDetail extends Item {
|
||||
recipe: RecipeDetail | null;
|
||||
relatedRecipes: RecipeUsage[];
|
||||
relatedHabitats: HabitatUsage[];
|
||||
editHistory: EditHistoryEntry[];
|
||||
droppedByPokemon: Array<{
|
||||
pokemon: NamedEntity;
|
||||
skill: NamedEntity;
|
||||
@@ -110,6 +128,7 @@ export interface Recipe extends EditInfo {
|
||||
|
||||
export interface RecipeDetail extends Recipe {
|
||||
acquisition_methods: NamedEntity[];
|
||||
editHistory: EditHistoryEntry[];
|
||||
item: NamedEntity;
|
||||
}
|
||||
|
||||
|
||||
@@ -917,6 +917,13 @@ button:disabled,
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.detail-with-sidebar {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(260px, 320px);
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.habitat-detail-stack {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
@@ -966,6 +973,255 @@ button:disabled,
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.edit-history-panel {
|
||||
position: sticky;
|
||||
top: 92px;
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
padding: 18px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-card);
|
||||
background: var(--surface);
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.edit-history-panel__header h2,
|
||||
.edit-history-list h3 {
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
font-family: var(--font-display);
|
||||
font-weight: 950;
|
||||
line-height: 1.12;
|
||||
}
|
||||
|
||||
.edit-history-panel__header h2 {
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
.edit-history-list h3 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.edit-history-summary {
|
||||
display: grid;
|
||||
gap: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.edit-history-summary div {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
padding: 11px 0;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.edit-history-summary div:first-child {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.edit-history-summary div:last-child {
|
||||
padding-bottom: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.edit-history-summary dt {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.edit-history-summary dd {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
margin: 0;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.edit-history-summary time,
|
||||
.edit-timeline time {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.edit-history-list {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.edit-timeline {
|
||||
display: grid;
|
||||
gap: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.edit-timeline li {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 38px minmax(0, 1fr);
|
||||
gap: 10px;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.edit-timeline li:not(:last-child)::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 38px;
|
||||
bottom: 0;
|
||||
left: 17px;
|
||||
width: 2px;
|
||||
background: var(--line);
|
||||
}
|
||||
|
||||
.edit-timeline__avatar {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 2px solid var(--line-strong);
|
||||
border-radius: 50%;
|
||||
background: var(--pokemon-yellow);
|
||||
box-shadow: 0 2px 0 var(--line-strong);
|
||||
color: #172036;
|
||||
font-size: 13px;
|
||||
font-weight: 950;
|
||||
}
|
||||
|
||||
.edit-timeline__body {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
padding-bottom: 13px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.edit-timeline li:last-child .edit-timeline__body {
|
||||
padding-bottom: 0;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.edit-history-entry {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.edit-history-entry summary {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 18px;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
min-height: 34px;
|
||||
margin: 0;
|
||||
color: var(--ink-soft);
|
||||
cursor: pointer;
|
||||
font-weight: 850;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.edit-history-entry summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.edit-history-entry summary::after {
|
||||
content: "";
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
justify-self: center;
|
||||
border-right: 2px solid var(--muted);
|
||||
border-bottom: 2px solid var(--muted);
|
||||
transform: rotate(-45deg);
|
||||
transition: transform 0.16s ease;
|
||||
}
|
||||
|
||||
.edit-history-entry[open] summary::after {
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.edit-history-entry__title {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.edit-history-entry__content {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.edit-change-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.edit-change-list div {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-small);
|
||||
background: var(--surface-soft);
|
||||
}
|
||||
|
||||
.edit-change-list dt {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.edit-change-list dd {
|
||||
display: grid;
|
||||
grid-template-columns: 52px minmax(0, 1fr);
|
||||
gap: 3px 8px;
|
||||
margin: 0;
|
||||
color: var(--ink-soft);
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.edit-change-list dd span {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.edit-change-list__label {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.edit-history-detail-meta {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
margin: 0;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.edit-history-detail-meta div {
|
||||
display: grid;
|
||||
grid-template-columns: 42px minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.edit-history-detail-meta dt,
|
||||
.edit-history-detail-meta dd {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.edit-history-detail-meta dt {
|
||||
color: var(--muted);
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.edit-history-detail-meta dd {
|
||||
color: var(--ink-soft);
|
||||
font-weight: 800;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.row-list {
|
||||
display: grid;
|
||||
gap: 0;
|
||||
@@ -1429,10 +1685,15 @@ button:disabled,
|
||||
}
|
||||
|
||||
.detail-grid,
|
||||
.detail-with-sidebar,
|
||||
.admin-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.edit-history-panel {
|
||||
position: static;
|
||||
}
|
||||
|
||||
.appearance-row__main {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import DetailSection from '../components/DetailSection.vue';
|
||||
import EditMeta from '../components/EditMeta.vue';
|
||||
import EditHistoryPanel from '../components/EditHistoryPanel.vue';
|
||||
import EntityChips from '../components/EntityChips.vue';
|
||||
import PageHeader from '../components/PageHeader.vue';
|
||||
import Skeleton from '../components/Skeleton.vue';
|
||||
@@ -129,45 +129,46 @@ onMounted(async () => {
|
||||
<section v-else class="page-stack">
|
||||
<PageHeader :title="habitat.name" subtitle="栖息地详情">
|
||||
<template #kicker>Habitat Detail</template>
|
||||
<template #meta>
|
||||
<EditMeta :entity="habitat" />
|
||||
</template>
|
||||
<template #actions>
|
||||
<RouterLink class="ui-button ui-button--primary ui-button--small" :to="`/habitats/${habitat.id}/edit`">编辑</RouterLink>
|
||||
<RouterLink class="ui-button ui-button--blue ui-button--small" to="/habitats">返回列表</RouterLink>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<div class="habitat-detail-stack">
|
||||
<DetailSection title="配方列表">
|
||||
<EntityChips :items="habitat.recipe" />
|
||||
</DetailSection>
|
||||
<div class="detail-with-sidebar">
|
||||
<div class="habitat-detail-stack">
|
||||
<DetailSection title="配方列表">
|
||||
<EntityChips :items="habitat.recipe" />
|
||||
</DetailSection>
|
||||
|
||||
<DetailSection title="可能出现的宝可梦">
|
||||
<ul class="row-list appearance-list">
|
||||
<li v-for="item in pokemonRows" :key="`${item.id}-${item.rarity}`">
|
||||
<RouterLink class="appearance-name" :to="`/pokemon/${item.id}`">{{ item.name }}</RouterLink>
|
||||
<dl class="appearance-summary">
|
||||
<div>
|
||||
<dt>时段</dt>
|
||||
<dd>{{ item.timeOfDays.join(' / ') }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>天气</dt>
|
||||
<dd>{{ item.weathers.join(' / ') }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>稀有度</dt>
|
||||
<dd>{{ item.rarity }} 星</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>出现地图</dt>
|
||||
<dd>{{ item.maps.join(' / ') }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</li>
|
||||
</ul>
|
||||
</DetailSection>
|
||||
<DetailSection title="可能出现的宝可梦">
|
||||
<ul class="row-list appearance-list">
|
||||
<li v-for="item in pokemonRows" :key="`${item.id}-${item.rarity}`">
|
||||
<RouterLink class="appearance-name" :to="`/pokemon/${item.id}`">{{ item.name }}</RouterLink>
|
||||
<dl class="appearance-summary">
|
||||
<div>
|
||||
<dt>时段</dt>
|
||||
<dd>{{ item.timeOfDays.join(' / ') }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>天气</dt>
|
||||
<dd>{{ item.weathers.join(' / ') }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>稀有度</dt>
|
||||
<dd>{{ item.rarity }} 星</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>出现地图</dt>
|
||||
<dd>{{ item.maps.join(' / ') }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</li>
|
||||
</ul>
|
||||
</DetailSection>
|
||||
</div>
|
||||
|
||||
<EditHistoryPanel :entity="habitat" :history="habitat.editHistory" />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import DetailSection from '../components/DetailSection.vue';
|
||||
import EditMeta from '../components/EditMeta.vue';
|
||||
import EditHistoryPanel from '../components/EditHistoryPanel.vue';
|
||||
import EntityChips from '../components/EntityChips.vue';
|
||||
import PageHeader from '../components/PageHeader.vue';
|
||||
import Skeleton from '../components/Skeleton.vue';
|
||||
@@ -84,74 +84,75 @@ onMounted(async () => {
|
||||
<section v-else class="page-stack">
|
||||
<PageHeader :title="item.name" :subtitle="item.usage ? `${item.category.name} · ${item.usage.name}` : item.category.name">
|
||||
<template #kicker>Item Detail</template>
|
||||
<template #meta>
|
||||
<EditMeta :entity="item" />
|
||||
</template>
|
||||
<template #actions>
|
||||
<RouterLink class="ui-button ui-button--primary ui-button--small" :to="`/items/${item.id}/edit`">编辑</RouterLink>
|
||||
<RouterLink class="ui-button ui-button--blue ui-button--small" to="/items">返回列表</RouterLink>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<div class="detail-grid">
|
||||
<DetailSection title="入手方式">
|
||||
<EntityChips :items="item.acquisitionMethods" />
|
||||
</DetailSection>
|
||||
<div class="detail-with-sidebar">
|
||||
<div class="detail-grid">
|
||||
<DetailSection title="入手方式">
|
||||
<EntityChips :items="item.acquisitionMethods" />
|
||||
</DetailSection>
|
||||
|
||||
<DetailSection title="自定义">
|
||||
<div v-if="customization.length" class="chips">
|
||||
<span v-for="entry in customization" :key="entry" class="chip">{{ entry }}</span>
|
||||
</div>
|
||||
<p v-else class="meta-line">无</p>
|
||||
</DetailSection>
|
||||
<DetailSection title="自定义">
|
||||
<div v-if="customization.length" class="chips">
|
||||
<span v-for="entry in customization" :key="entry" class="chip">{{ entry }}</span>
|
||||
</div>
|
||||
<p v-else class="meta-line">无</p>
|
||||
</DetailSection>
|
||||
|
||||
<DetailSection title="标签">
|
||||
<EntityChips :items="item.tags" />
|
||||
</DetailSection>
|
||||
<DetailSection title="标签">
|
||||
<EntityChips :items="item.tags" />
|
||||
</DetailSection>
|
||||
|
||||
<DetailSection title="材料单信息">
|
||||
<template v-if="item.recipe">
|
||||
<RouterLink :to="`/recipes/${item.recipe.id}`">{{ item.recipe.name }}</RouterLink>
|
||||
<EntityChips :items="item.recipe.materials" />
|
||||
</template>
|
||||
<p v-else-if="item.noRecipe" class="meta-line">无材料单</p>
|
||||
<template v-else>
|
||||
<p class="meta-line">无</p>
|
||||
<RouterLink class="ui-button ui-button--primary ui-button--small" :to="`/recipes/new?itemId=${item.id}`">
|
||||
创建材料单
|
||||
</RouterLink>
|
||||
</template>
|
||||
</DetailSection>
|
||||
<DetailSection title="材料单信息">
|
||||
<template v-if="item.recipe">
|
||||
<RouterLink :to="`/recipes/${item.recipe.id}`">{{ item.recipe.name }}</RouterLink>
|
||||
<EntityChips :items="item.recipe.materials" />
|
||||
</template>
|
||||
<p v-else-if="item.noRecipe" class="meta-line">无材料单</p>
|
||||
<template v-else>
|
||||
<p class="meta-line">无</p>
|
||||
<RouterLink class="ui-button ui-button--primary ui-button--small" :to="`/recipes/new?itemId=${item.id}`">
|
||||
创建材料单
|
||||
</RouterLink>
|
||||
</template>
|
||||
</DetailSection>
|
||||
|
||||
<DetailSection title="相关材料单">
|
||||
<ul v-if="item.relatedRecipes.length" class="row-list">
|
||||
<li v-for="recipe in item.relatedRecipes" :key="recipe.id">
|
||||
<RouterLink :to="`/recipes/${recipe.id}`">{{ recipe.name }}</RouterLink>
|
||||
<EntityChips :items="recipe.materials" />
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="meta-line">无</p>
|
||||
</DetailSection>
|
||||
<DetailSection title="相关材料单">
|
||||
<ul v-if="item.relatedRecipes.length" class="row-list">
|
||||
<li v-for="recipe in item.relatedRecipes" :key="recipe.id">
|
||||
<RouterLink :to="`/recipes/${recipe.id}`">{{ recipe.name }}</RouterLink>
|
||||
<EntityChips :items="recipe.materials" />
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="meta-line">无</p>
|
||||
</DetailSection>
|
||||
|
||||
<DetailSection title="相关栖息地">
|
||||
<ul v-if="item.relatedHabitats.length" class="row-list">
|
||||
<li v-for="habitat in item.relatedHabitats" :key="habitat.id">
|
||||
<RouterLink :to="`/habitats/${habitat.id}`">{{ habitat.name }}</RouterLink>
|
||||
<EntityChips :items="habitat.recipe" />
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="meta-line">无</p>
|
||||
</DetailSection>
|
||||
<DetailSection title="相关栖息地">
|
||||
<ul v-if="item.relatedHabitats.length" class="row-list">
|
||||
<li v-for="habitat in item.relatedHabitats" :key="habitat.id">
|
||||
<RouterLink :to="`/habitats/${habitat.id}`">{{ habitat.name }}</RouterLink>
|
||||
<EntityChips :items="habitat.recipe" />
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="meta-line">无</p>
|
||||
</DetailSection>
|
||||
|
||||
<DetailSection title="Pokemon 掉落">
|
||||
<ul v-if="item.droppedByPokemon.length" class="row-list">
|
||||
<li v-for="entry in item.droppedByPokemon" :key="`${entry.pokemon.id}-${entry.skill.id}`">
|
||||
<RouterLink :to="`/pokemon/${entry.pokemon.id}`">#{{ entry.pokemon.id }} {{ entry.pokemon.name }}</RouterLink>
|
||||
<span>{{ entry.skill.name }}掉落物</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="meta-line">无</p>
|
||||
</DetailSection>
|
||||
<DetailSection title="Pokemon 掉落">
|
||||
<ul v-if="item.droppedByPokemon.length" class="row-list">
|
||||
<li v-for="entry in item.droppedByPokemon" :key="`${entry.pokemon.id}-${entry.skill.id}`">
|
||||
<RouterLink :to="`/pokemon/${entry.pokemon.id}`">#{{ entry.pokemon.id }} {{ entry.pokemon.name }}</RouterLink>
|
||||
<span>{{ entry.skill.name }}掉落物</span>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="meta-line">无</p>
|
||||
</DetailSection>
|
||||
</div>
|
||||
|
||||
<EditHistoryPanel :entity="item" :history="item.editHistory" />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import DetailSection from '../components/DetailSection.vue';
|
||||
import EditMeta from '../components/EditMeta.vue';
|
||||
import EditHistoryPanel from '../components/EditHistoryPanel.vue';
|
||||
import EntityChips from '../components/EntityChips.vue';
|
||||
import PageHeader from '../components/PageHeader.vue';
|
||||
import Skeleton from '../components/Skeleton.vue';
|
||||
@@ -165,78 +165,79 @@ onMounted(async () => {
|
||||
<section v-else class="page-stack">
|
||||
<PageHeader :title="`#${pokemon.id} ${pokemon.name}`" :subtitle="`喜欢的环境:${pokemon.environment.name}`">
|
||||
<template #kicker>Pokédex Detail</template>
|
||||
<template #meta>
|
||||
<EditMeta :entity="pokemon" />
|
||||
</template>
|
||||
<template #actions>
|
||||
<RouterLink class="ui-button ui-button--primary ui-button--small" :to="`/pokemon/${pokemon.id}/edit`">编辑</RouterLink>
|
||||
<RouterLink class="ui-button ui-button--blue ui-button--small" to="/pokemon">返回列表</RouterLink>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<div class="detail-grid detail-grid--stack">
|
||||
<DetailSection title="特长">
|
||||
<EntityChips :items="pokemon.skills" />
|
||||
</DetailSection>
|
||||
<div class="detail-with-sidebar">
|
||||
<div class="detail-grid detail-grid--stack">
|
||||
<DetailSection title="特长">
|
||||
<EntityChips :items="pokemon.skills" />
|
||||
</DetailSection>
|
||||
|
||||
<DetailSection v-if="skillDropRows.length" title="特长掉落物">
|
||||
<ul class="row-list skill-drop-summary">
|
||||
<li v-for="skill in skillDropRows" :key="skill.id">
|
||||
<span>{{ skill.name }}掉落物</span>
|
||||
<RouterLink v-if="skill.itemDrop" :to="`/items/${skill.itemDrop.id}`">{{ skill.itemDrop.name }}</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
</DetailSection>
|
||||
|
||||
<DetailSection title="喜欢的东西">
|
||||
<EntityChips :items="pokemon.favorite_things" />
|
||||
</DetailSection>
|
||||
|
||||
<DetailSection title="关联物品">
|
||||
<template v-if="pokemon.favoriteThingItems.length">
|
||||
<Tabs
|
||||
v-if="itemCategoryTabs.length"
|
||||
id="pokemon-favorite-items"
|
||||
v-model="itemCategoryTab"
|
||||
:tabs="itemCategoryTabs"
|
||||
label="关联物品分类"
|
||||
/>
|
||||
<ul v-if="favoriteThingItems.length" class="row-list">
|
||||
<li v-for="item in favoriteThingItems" :key="item.id">
|
||||
<RouterLink :to="`/items/${item.id}`">{{ item.name }}</RouterLink>
|
||||
<EntityChips :items="item.tags" />
|
||||
<DetailSection v-if="skillDropRows.length" title="特长掉落物">
|
||||
<ul class="row-list skill-drop-summary">
|
||||
<li v-for="skill in skillDropRows" :key="skill.id">
|
||||
<span>{{ skill.name }}掉落物</span>
|
||||
<RouterLink v-if="skill.itemDrop" :to="`/items/${skill.itemDrop.id}`">{{ skill.itemDrop.name }}</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="meta-line">无</p>
|
||||
</template>
|
||||
<p v-else class="meta-line">无</p>
|
||||
</DetailSection>
|
||||
</DetailSection>
|
||||
|
||||
<DetailSection title="栖息地">
|
||||
<ul class="row-list appearance-list">
|
||||
<li v-for="habitat in habitatRows" :key="`${habitat.id}-${habitat.rarity}`">
|
||||
<RouterLink class="appearance-name" :to="`/habitats/${habitat.id}`">{{ habitat.name }}</RouterLink>
|
||||
<dl class="appearance-summary">
|
||||
<div>
|
||||
<dt>时段</dt>
|
||||
<dd>{{ habitat.timeOfDays.join(' / ') }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>天气</dt>
|
||||
<dd>{{ habitat.weathers.join(' / ') }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>稀有度</dt>
|
||||
<dd>{{ habitat.rarity }} 星</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>出现地图</dt>
|
||||
<dd>{{ habitat.maps.join(' / ') }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</li>
|
||||
</ul>
|
||||
</DetailSection>
|
||||
<DetailSection title="喜欢的东西">
|
||||
<EntityChips :items="pokemon.favorite_things" />
|
||||
</DetailSection>
|
||||
|
||||
<DetailSection title="关联物品">
|
||||
<template v-if="pokemon.favoriteThingItems.length">
|
||||
<Tabs
|
||||
v-if="itemCategoryTabs.length"
|
||||
id="pokemon-favorite-items"
|
||||
v-model="itemCategoryTab"
|
||||
:tabs="itemCategoryTabs"
|
||||
label="关联物品分类"
|
||||
/>
|
||||
<ul v-if="favoriteThingItems.length" class="row-list">
|
||||
<li v-for="item in favoriteThingItems" :key="item.id">
|
||||
<RouterLink :to="`/items/${item.id}`">{{ item.name }}</RouterLink>
|
||||
<EntityChips :items="item.tags" />
|
||||
</li>
|
||||
</ul>
|
||||
<p v-else class="meta-line">无</p>
|
||||
</template>
|
||||
<p v-else class="meta-line">无</p>
|
||||
</DetailSection>
|
||||
|
||||
<DetailSection title="栖息地">
|
||||
<ul class="row-list appearance-list">
|
||||
<li v-for="habitat in habitatRows" :key="`${habitat.id}-${habitat.rarity}`">
|
||||
<RouterLink class="appearance-name" :to="`/habitats/${habitat.id}`">{{ habitat.name }}</RouterLink>
|
||||
<dl class="appearance-summary">
|
||||
<div>
|
||||
<dt>时段</dt>
|
||||
<dd>{{ habitat.timeOfDays.join(' / ') }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>天气</dt>
|
||||
<dd>{{ habitat.weathers.join(' / ') }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>稀有度</dt>
|
||||
<dd>{{ habitat.rarity }} 星</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>出现地图</dt>
|
||||
<dd>{{ habitat.maps.join(' / ') }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</li>
|
||||
</ul>
|
||||
</DetailSection>
|
||||
</div>
|
||||
|
||||
<EditHistoryPanel :entity="pokemon" :history="pokemon.editHistory" />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import DetailSection from '../components/DetailSection.vue';
|
||||
import EditMeta from '../components/EditMeta.vue';
|
||||
import EditHistoryPanel from '../components/EditHistoryPanel.vue';
|
||||
import EntityChips from '../components/EntityChips.vue';
|
||||
import PageHeader from '../components/PageHeader.vue';
|
||||
import Skeleton from '../components/Skeleton.vue';
|
||||
@@ -46,23 +46,24 @@ onMounted(async () => {
|
||||
<section v-else class="page-stack">
|
||||
<PageHeader :title="recipe.name" subtitle="材料单详情">
|
||||
<template #kicker>Recipe Detail</template>
|
||||
<template #meta>
|
||||
<EditMeta :entity="recipe" />
|
||||
</template>
|
||||
<template #actions>
|
||||
<RouterLink class="ui-button ui-button--primary ui-button--small" :to="`/recipes/${recipe.id}/edit`">编辑</RouterLink>
|
||||
<RouterLink class="ui-button ui-button--blue ui-button--small" to="/recipes">返回列表</RouterLink>
|
||||
</template>
|
||||
</PageHeader>
|
||||
|
||||
<div class="detail-grid">
|
||||
<DetailSection title="入手方式">
|
||||
<EntityChips :items="recipe.acquisition_methods" />
|
||||
</DetailSection>
|
||||
<div class="detail-with-sidebar">
|
||||
<div class="detail-grid">
|
||||
<DetailSection title="入手方式">
|
||||
<EntityChips :items="recipe.acquisition_methods" />
|
||||
</DetailSection>
|
||||
|
||||
<DetailSection title="需要材料">
|
||||
<EntityChips :items="recipe.materials" />
|
||||
</DetailSection>
|
||||
<DetailSection title="需要材料">
|
||||
<EntityChips :items="recipe.materials" />
|
||||
</DetailSection>
|
||||
</div>
|
||||
|
||||
<EditHistoryPanel :entity="recipe" :history="recipe.editHistory" />
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user