refactor(schema): invert relationship between items and recipes

Move item_id to recipes table and drop recipe_id from items
Update backend queries and admin UI to reflect the new domain model
This commit is contained in:
2026-04-30 09:50:11 +08:00
parent be6902333d
commit b94450d3ba
4 changed files with 90 additions and 59 deletions

View File

@@ -71,6 +71,7 @@ export interface Recipe {
export interface RecipeDetail extends Recipe {
acquisition_methods: NamedEntity[];
item: NamedEntity;
}
export interface Options {
@@ -105,7 +106,6 @@ export interface ItemPayload {
name: string;
categoryId: number;
usageId: number | null;
recipeId: number | null;
dyeable: boolean;
dualDyeable: boolean;
patternEditable: boolean;
@@ -114,7 +114,7 @@ export interface ItemPayload {
}
export interface RecipePayload {
name: string;
itemId: number;
acquisitionMethodIds: number[];
materials: Array<{ itemId: number; quantity: number }>;
}

View File

@@ -70,7 +70,6 @@ const itemForm = ref({
name: '',
categoryId: '',
usageId: '',
recipeId: '',
dyeable: false,
dualDyeable: false,
patternEditable: false,
@@ -79,7 +78,7 @@ const itemForm = ref({
});
const recipeForm = ref({
id: 0,
name: '',
itemId: '',
acquisitionMethodIds: [] as string[],
materials: [] as Array<{ itemId: string; quantity: number }>
});
@@ -255,7 +254,6 @@ function resetItemForm() {
name: '',
categoryId: '',
usageId: '',
recipeId: '',
dyeable: false,
dualDyeable: false,
patternEditable: false,
@@ -272,7 +270,6 @@ async function editItem(item: Item) {
name: detail.name,
categoryId: String(detail.category.id),
usageId: detail.usage ? String(detail.usage.id) : '',
recipeId: detail.recipe ? String(detail.recipe.id) : '',
dyeable: detail.customization.dyeable,
dualDyeable: detail.customization.dualDyeable,
patternEditable: detail.customization.patternEditable,
@@ -288,7 +285,6 @@ async function saveItem() {
name: itemForm.value.name,
categoryId: Number(itemForm.value.categoryId),
usageId: itemForm.value.usageId ? Number(itemForm.value.usageId) : null,
recipeId: itemForm.value.recipeId ? Number(itemForm.value.recipeId) : null,
dyeable: itemForm.value.dyeable,
dualDyeable: itemForm.value.dualDyeable,
patternEditable: itemForm.value.patternEditable,
@@ -313,7 +309,7 @@ async function removeItem(id: number) {
}
function resetRecipeForm() {
recipeForm.value = { id: 0, name: '', acquisitionMethodIds: [], materials: [] };
recipeForm.value = { id: 0, itemId: '', acquisitionMethodIds: [], materials: [] };
}
function addRecipeMaterial() {
@@ -325,7 +321,7 @@ async function editRecipe(item: Recipe) {
const detail = await api.recipeDetail(item.id);
recipeForm.value = {
id: detail.id,
name: detail.name,
itemId: String(detail.item.id),
acquisitionMethodIds: detail.acquisition_methods.map((method) => String(method.id)),
materials: detail.materials.map((material) => ({ itemId: String(material.id), quantity: material.quantity }))
};
@@ -335,7 +331,7 @@ async function editRecipe(item: Recipe) {
async function saveRecipe() {
await run(async () => {
const payload: RecipePayload = {
name: recipeForm.value.name,
itemId: Number(recipeForm.value.itemId),
acquisitionMethodIds: toIds(recipeForm.value.acquisitionMethodIds),
materials: toQuantityRows(recipeForm.value.materials)
};
@@ -345,7 +341,7 @@ async function saveRecipe() {
await api.createRecipe(payload);
}
resetRecipeForm();
await loadRecipes();
await Promise.all([loadRecipes(), loadItems()]);
});
}
@@ -576,13 +572,6 @@ onMounted(() => {
<option v-for="item in options.itemUsages" :key="item.id" :value="item.id">{{ item.name }}</option>
</select>
</div>
<div class="field">
<label for="item-recipe">材料单</label>
<select id="item-recipe" v-model="itemForm.recipeId">
<option value=""></option>
<option v-for="item in recipeRows" :key="item.id" :value="item.id">{{ item.name }}</option>
</select>
</div>
<div class="check-row">
<label><input v-model="itemForm.dyeable" type="checkbox" /> 可染色</label>
<label><input v-model="itemForm.dualDyeable" type="checkbox" /> 可双区染色</label>
@@ -635,7 +624,13 @@ onMounted(() => {
<section v-if="activeTab === 'recipes' && options" class="admin-layout">
<form class="detail-section" @submit.prevent="saveRecipe">
<h2>材料单</h2>
<div class="field"><label for="recipe-name">名称</label><input id="recipe-name" v-model="recipeForm.name" /></div>
<div class="field">
<label for="recipe-item">物品</label>
<select id="recipe-item" v-model="recipeForm.itemId">
<option value="">请选择</option>
<option v-for="item in itemRows" :key="item.id" :value="String(item.id)">{{ item.name }}</option>
</select>
</div>
<div class="field">
<label for="recipe-methods">入手方式</label>
<TagsSelect