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:
2026-05-01 07:59:29 +08:00
parent 14b13e479d
commit 60cad3f5e8
9 changed files with 866 additions and 197 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>