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:
@@ -48,23 +48,11 @@ CREATE TABLE IF NOT EXISTS acquisition_methods (
|
|||||||
name text NOT NULL UNIQUE
|
name text NOT NULL UNIQUE
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS recipes (
|
|
||||||
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
|
||||||
name text NOT NULL UNIQUE
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS recipe_acquisition_methods (
|
|
||||||
recipe_id integer NOT NULL REFERENCES recipes(id) ON DELETE CASCADE,
|
|
||||||
acquisition_method_id integer NOT NULL REFERENCES acquisition_methods(id),
|
|
||||||
PRIMARY KEY (recipe_id, acquisition_method_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS items (
|
CREATE TABLE IF NOT EXISTS items (
|
||||||
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||||
name text NOT NULL UNIQUE,
|
name text NOT NULL UNIQUE,
|
||||||
category_id integer NOT NULL REFERENCES item_categories(id),
|
category_id integer NOT NULL REFERENCES item_categories(id),
|
||||||
usage_id integer REFERENCES item_usages(id),
|
usage_id integer REFERENCES item_usages(id),
|
||||||
recipe_id integer REFERENCES recipes(id),
|
|
||||||
dyeable boolean NOT NULL DEFAULT false,
|
dyeable boolean NOT NULL DEFAULT false,
|
||||||
dual_dyeable boolean NOT NULL DEFAULT false,
|
dual_dyeable boolean NOT NULL DEFAULT false,
|
||||||
pattern_editable boolean NOT NULL DEFAULT false
|
pattern_editable boolean NOT NULL DEFAULT false
|
||||||
@@ -72,6 +60,49 @@ CREATE TABLE IF NOT EXISTS items (
|
|||||||
|
|
||||||
ALTER TABLE items ALTER COLUMN usage_id DROP NOT NULL;
|
ALTER TABLE items ALTER COLUMN usage_id DROP NOT NULL;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS recipes (
|
||||||
|
id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||||
|
item_id integer NOT NULL UNIQUE REFERENCES items(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE recipes ADD COLUMN IF NOT EXISTS item_id integer REFERENCES items(id);
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM information_schema.columns
|
||||||
|
WHERE table_name = 'items'
|
||||||
|
AND column_name = 'recipe_id'
|
||||||
|
) THEN
|
||||||
|
EXECUTE '
|
||||||
|
UPDATE recipes r
|
||||||
|
SET item_id = linked.item_id
|
||||||
|
FROM (
|
||||||
|
SELECT DISTINCT ON (recipe_id) recipe_id, id AS item_id
|
||||||
|
FROM items
|
||||||
|
WHERE recipe_id IS NOT NULL
|
||||||
|
ORDER BY recipe_id, id
|
||||||
|
) linked
|
||||||
|
WHERE r.id = linked.recipe_id
|
||||||
|
AND r.item_id IS NULL
|
||||||
|
';
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DELETE FROM recipes WHERE item_id IS NULL;
|
||||||
|
|
||||||
|
ALTER TABLE recipes ALTER COLUMN item_id SET NOT NULL;
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS recipes_item_id_key ON recipes(item_id);
|
||||||
|
ALTER TABLE recipes DROP COLUMN IF EXISTS name;
|
||||||
|
ALTER TABLE items DROP COLUMN IF EXISTS recipe_id;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS recipe_acquisition_methods (
|
||||||
|
recipe_id integer NOT NULL REFERENCES recipes(id) ON DELETE CASCADE,
|
||||||
|
acquisition_method_id integer NOT NULL REFERENCES acquisition_methods(id),
|
||||||
|
PRIMARY KEY (recipe_id, acquisition_method_id)
|
||||||
|
);
|
||||||
|
|
||||||
DROP TABLE IF EXISTS item_item_tags;
|
DROP TABLE IF EXISTS item_item_tags;
|
||||||
DROP TABLE IF EXISTS item_tags;
|
DROP TABLE IF EXISTS item_tags;
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ type ItemPayload = {
|
|||||||
name: string;
|
name: string;
|
||||||
categoryId: number;
|
categoryId: number;
|
||||||
usageId: number | null;
|
usageId: number | null;
|
||||||
recipeId: number | null;
|
|
||||||
dyeable: boolean;
|
dyeable: boolean;
|
||||||
dualDyeable: boolean;
|
dualDyeable: boolean;
|
||||||
patternEditable: boolean;
|
patternEditable: boolean;
|
||||||
@@ -50,7 +49,7 @@ type ItemPayload = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type RecipePayload = {
|
type RecipePayload = {
|
||||||
name: string;
|
itemId: number;
|
||||||
acquisitionMethodIds: number[];
|
acquisitionMethodIds: number[];
|
||||||
materials: IdQuantity[];
|
materials: IdQuantity[];
|
||||||
};
|
};
|
||||||
@@ -649,7 +648,7 @@ export async function getItem(id: number) {
|
|||||||
`
|
`
|
||||||
SELECT
|
SELECT
|
||||||
r.id,
|
r.id,
|
||||||
r.name,
|
result_item.name,
|
||||||
COALESCE((
|
COALESCE((
|
||||||
SELECT json_agg(json_build_object('id', am.id, 'name', am.name) ORDER BY am.name)
|
SELECT json_agg(json_build_object('id', am.id, 'name', am.name) ORDER BY am.name)
|
||||||
FROM recipe_acquisition_methods ram
|
FROM recipe_acquisition_methods ram
|
||||||
@@ -661,10 +660,11 @@ export async function getItem(id: number) {
|
|||||||
FROM recipe_materials rm
|
FROM recipe_materials rm
|
||||||
JOIN items mi ON mi.id = rm.item_id
|
JOIN items mi ON mi.id = rm.item_id
|
||||||
WHERE rm.recipe_id = r.id
|
WHERE rm.recipe_id = r.id
|
||||||
), '[]'::json) AS materials
|
), '[]'::json) AS materials,
|
||||||
FROM items i
|
json_build_object('id', result_item.id, 'name', result_item.name) AS item
|
||||||
JOIN recipes r ON r.id = i.recipe_id
|
FROM recipes r
|
||||||
WHERE i.id = $1
|
JOIN items result_item ON result_item.id = r.item_id
|
||||||
|
WHERE r.item_id = $1
|
||||||
`,
|
`,
|
||||||
[id]
|
[id]
|
||||||
),
|
),
|
||||||
@@ -684,9 +684,6 @@ export async function getItem(id: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function cleanItemPayload(payload: Record<string, unknown>): ItemPayload {
|
function cleanItemPayload(payload: Record<string, unknown>): ItemPayload {
|
||||||
const recipeId = payload.recipeId === null || payload.recipeId === '' || payload.recipeId === undefined
|
|
||||||
? null
|
|
||||||
: requirePositiveInteger(payload.recipeId, '请选择材料单');
|
|
||||||
const usageId = payload.usageId === null || payload.usageId === '' || payload.usageId === undefined
|
const usageId = payload.usageId === null || payload.usageId === '' || payload.usageId === undefined
|
||||||
? null
|
? null
|
||||||
: requirePositiveInteger(payload.usageId, '请选择用途');
|
: requirePositiveInteger(payload.usageId, '请选择用途');
|
||||||
@@ -695,7 +692,6 @@ function cleanItemPayload(payload: Record<string, unknown>): ItemPayload {
|
|||||||
name: cleanName(payload.name, '请输入物品名字'),
|
name: cleanName(payload.name, '请输入物品名字'),
|
||||||
categoryId: requirePositiveInteger(payload.categoryId, '请选择分类'),
|
categoryId: requirePositiveInteger(payload.categoryId, '请选择分类'),
|
||||||
usageId,
|
usageId,
|
||||||
recipeId,
|
|
||||||
dyeable: Boolean(payload.dyeable),
|
dyeable: Boolean(payload.dyeable),
|
||||||
dualDyeable: Boolean(payload.dualDyeable),
|
dualDyeable: Boolean(payload.dualDyeable),
|
||||||
patternEditable: Boolean(payload.patternEditable),
|
patternEditable: Boolean(payload.patternEditable),
|
||||||
@@ -729,15 +725,14 @@ export async function createItem(payload: Record<string, unknown>) {
|
|||||||
const id = await withTransaction(async (client) => {
|
const id = await withTransaction(async (client) => {
|
||||||
const result = await client.query<{ id: number }>(
|
const result = await client.query<{ id: number }>(
|
||||||
`
|
`
|
||||||
INSERT INTO items (name, category_id, usage_id, recipe_id, dyeable, dual_dyeable, pattern_editable)
|
INSERT INTO items (name, category_id, usage_id, dyeable, dual_dyeable, pattern_editable)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
VALUES ($1, $2, $3, $4, $5, $6)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
cleanPayload.name,
|
cleanPayload.name,
|
||||||
cleanPayload.categoryId,
|
cleanPayload.categoryId,
|
||||||
cleanPayload.usageId,
|
cleanPayload.usageId,
|
||||||
cleanPayload.recipeId,
|
|
||||||
cleanPayload.dyeable,
|
cleanPayload.dyeable,
|
||||||
cleanPayload.dualDyeable,
|
cleanPayload.dualDyeable,
|
||||||
cleanPayload.patternEditable
|
cleanPayload.patternEditable
|
||||||
@@ -760,17 +755,15 @@ export async function updateItem(id: number, payload: Record<string, unknown>) {
|
|||||||
SET name = $1,
|
SET name = $1,
|
||||||
category_id = $2,
|
category_id = $2,
|
||||||
usage_id = $3,
|
usage_id = $3,
|
||||||
recipe_id = $4,
|
dyeable = $4,
|
||||||
dyeable = $5,
|
dual_dyeable = $5,
|
||||||
dual_dyeable = $6,
|
pattern_editable = $6
|
||||||
pattern_editable = $7
|
WHERE id = $7
|
||||||
WHERE id = $8
|
|
||||||
`,
|
`,
|
||||||
[
|
[
|
||||||
cleanPayload.name,
|
cleanPayload.name,
|
||||||
cleanPayload.categoryId,
|
cleanPayload.categoryId,
|
||||||
cleanPayload.usageId,
|
cleanPayload.usageId,
|
||||||
cleanPayload.recipeId,
|
|
||||||
cleanPayload.dyeable,
|
cleanPayload.dyeable,
|
||||||
cleanPayload.dualDyeable,
|
cleanPayload.dualDyeable,
|
||||||
cleanPayload.patternEditable,
|
cleanPayload.patternEditable,
|
||||||
@@ -795,7 +788,7 @@ export async function listRecipes() {
|
|||||||
return query(`
|
return query(`
|
||||||
SELECT
|
SELECT
|
||||||
r.id,
|
r.id,
|
||||||
r.name,
|
result_item.name,
|
||||||
COALESCE((
|
COALESCE((
|
||||||
SELECT json_agg(json_build_object('id', i.id, 'name', i.name, 'quantity', rm.quantity) ORDER BY i.name)
|
SELECT json_agg(json_build_object('id', i.id, 'name', i.name, 'quantity', rm.quantity) ORDER BY i.name)
|
||||||
FROM recipe_materials rm
|
FROM recipe_materials rm
|
||||||
@@ -803,7 +796,8 @@ export async function listRecipes() {
|
|||||||
WHERE rm.recipe_id = r.id
|
WHERE rm.recipe_id = r.id
|
||||||
), '[]'::json) AS materials
|
), '[]'::json) AS materials
|
||||||
FROM recipes r
|
FROM recipes r
|
||||||
ORDER BY r.name
|
JOIN items result_item ON result_item.id = r.item_id
|
||||||
|
ORDER BY result_item.name
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -812,7 +806,7 @@ export async function getRecipe(id: number) {
|
|||||||
`
|
`
|
||||||
SELECT
|
SELECT
|
||||||
r.id,
|
r.id,
|
||||||
r.name,
|
result_item.name,
|
||||||
COALESCE((
|
COALESCE((
|
||||||
SELECT json_agg(json_build_object('id', am.id, 'name', am.name) ORDER BY am.name)
|
SELECT json_agg(json_build_object('id', am.id, 'name', am.name) ORDER BY am.name)
|
||||||
FROM recipe_acquisition_methods ram
|
FROM recipe_acquisition_methods ram
|
||||||
@@ -824,8 +818,10 @@ export async function getRecipe(id: number) {
|
|||||||
FROM recipe_materials rm
|
FROM recipe_materials rm
|
||||||
JOIN items i ON i.id = rm.item_id
|
JOIN items i ON i.id = rm.item_id
|
||||||
WHERE rm.recipe_id = r.id
|
WHERE rm.recipe_id = r.id
|
||||||
), '[]'::json) AS materials
|
), '[]'::json) AS materials,
|
||||||
|
json_build_object('id', result_item.id, 'name', result_item.name) AS item
|
||||||
FROM recipes r
|
FROM recipes r
|
||||||
|
JOIN items result_item ON result_item.id = r.item_id
|
||||||
WHERE r.id = $1
|
WHERE r.id = $1
|
||||||
`,
|
`,
|
||||||
[id]
|
[id]
|
||||||
@@ -834,7 +830,7 @@ export async function getRecipe(id: number) {
|
|||||||
|
|
||||||
function cleanRecipePayload(payload: Record<string, unknown>): RecipePayload {
|
function cleanRecipePayload(payload: Record<string, unknown>): RecipePayload {
|
||||||
return {
|
return {
|
||||||
name: cleanName(payload.name, '请输入材料单名字'),
|
itemId: requirePositiveInteger(payload.itemId, '请选择物品'),
|
||||||
acquisitionMethodIds: cleanIds(payload.acquisitionMethodIds),
|
acquisitionMethodIds: cleanIds(payload.acquisitionMethodIds),
|
||||||
materials: cleanQuantities(payload.materials)
|
materials: cleanQuantities(payload.materials)
|
||||||
};
|
};
|
||||||
@@ -860,12 +856,20 @@ async function replaceRecipeRelations(client: DbClient, recipeId: number, payloa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function ensureItemExists(client: DbClient, itemId: number): Promise<void> {
|
||||||
|
const result = await client.query('SELECT 1 FROM items WHERE id = $1', [itemId]);
|
||||||
|
if (result.rowCount === 0) {
|
||||||
|
throw validationError('请选择物品');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function createRecipe(payload: Record<string, unknown>) {
|
export async function createRecipe(payload: Record<string, unknown>) {
|
||||||
const cleanPayload = cleanRecipePayload(payload);
|
const cleanPayload = cleanRecipePayload(payload);
|
||||||
|
|
||||||
const id = await withTransaction(async (client) => {
|
const id = await withTransaction(async (client) => {
|
||||||
const result = await client.query<{ id: number }>('INSERT INTO recipes (name) VALUES ($1) RETURNING id', [
|
await ensureItemExists(client, cleanPayload.itemId);
|
||||||
cleanPayload.name
|
const result = await client.query<{ id: number }>('INSERT INTO recipes (item_id) VALUES ($1) RETURNING id', [
|
||||||
|
cleanPayload.itemId
|
||||||
]);
|
]);
|
||||||
const recipeId = result.rows[0].id;
|
const recipeId = result.rows[0].id;
|
||||||
await replaceRecipeRelations(client, recipeId, cleanPayload);
|
await replaceRecipeRelations(client, recipeId, cleanPayload);
|
||||||
@@ -878,7 +882,8 @@ export async function updateRecipe(id: number, payload: Record<string, unknown>)
|
|||||||
const cleanPayload = cleanRecipePayload(payload);
|
const cleanPayload = cleanRecipePayload(payload);
|
||||||
|
|
||||||
const updated = await withTransaction(async (client) => {
|
const updated = await withTransaction(async (client) => {
|
||||||
const result = await client.query('UPDATE recipes SET name = $1 WHERE id = $2', [cleanPayload.name, id]);
|
await ensureItemExists(client, cleanPayload.itemId);
|
||||||
|
const result = await client.query('UPDATE recipes SET item_id = $1 WHERE id = $2', [cleanPayload.itemId, id]);
|
||||||
if (result.rowCount === 0) {
|
if (result.rowCount === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export interface Recipe {
|
|||||||
|
|
||||||
export interface RecipeDetail extends Recipe {
|
export interface RecipeDetail extends Recipe {
|
||||||
acquisition_methods: NamedEntity[];
|
acquisition_methods: NamedEntity[];
|
||||||
|
item: NamedEntity;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
@@ -105,7 +106,6 @@ export interface ItemPayload {
|
|||||||
name: string;
|
name: string;
|
||||||
categoryId: number;
|
categoryId: number;
|
||||||
usageId: number | null;
|
usageId: number | null;
|
||||||
recipeId: number | null;
|
|
||||||
dyeable: boolean;
|
dyeable: boolean;
|
||||||
dualDyeable: boolean;
|
dualDyeable: boolean;
|
||||||
patternEditable: boolean;
|
patternEditable: boolean;
|
||||||
@@ -114,7 +114,7 @@ export interface ItemPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface RecipePayload {
|
export interface RecipePayload {
|
||||||
name: string;
|
itemId: number;
|
||||||
acquisitionMethodIds: number[];
|
acquisitionMethodIds: number[];
|
||||||
materials: Array<{ itemId: number; quantity: number }>;
|
materials: Array<{ itemId: number; quantity: number }>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,7 +70,6 @@ const itemForm = ref({
|
|||||||
name: '',
|
name: '',
|
||||||
categoryId: '',
|
categoryId: '',
|
||||||
usageId: '',
|
usageId: '',
|
||||||
recipeId: '',
|
|
||||||
dyeable: false,
|
dyeable: false,
|
||||||
dualDyeable: false,
|
dualDyeable: false,
|
||||||
patternEditable: false,
|
patternEditable: false,
|
||||||
@@ -79,7 +78,7 @@ const itemForm = ref({
|
|||||||
});
|
});
|
||||||
const recipeForm = ref({
|
const recipeForm = ref({
|
||||||
id: 0,
|
id: 0,
|
||||||
name: '',
|
itemId: '',
|
||||||
acquisitionMethodIds: [] as string[],
|
acquisitionMethodIds: [] as string[],
|
||||||
materials: [] as Array<{ itemId: string; quantity: number }>
|
materials: [] as Array<{ itemId: string; quantity: number }>
|
||||||
});
|
});
|
||||||
@@ -255,7 +254,6 @@ function resetItemForm() {
|
|||||||
name: '',
|
name: '',
|
||||||
categoryId: '',
|
categoryId: '',
|
||||||
usageId: '',
|
usageId: '',
|
||||||
recipeId: '',
|
|
||||||
dyeable: false,
|
dyeable: false,
|
||||||
dualDyeable: false,
|
dualDyeable: false,
|
||||||
patternEditable: false,
|
patternEditable: false,
|
||||||
@@ -272,7 +270,6 @@ async function editItem(item: Item) {
|
|||||||
name: detail.name,
|
name: detail.name,
|
||||||
categoryId: String(detail.category.id),
|
categoryId: String(detail.category.id),
|
||||||
usageId: detail.usage ? String(detail.usage.id) : '',
|
usageId: detail.usage ? String(detail.usage.id) : '',
|
||||||
recipeId: detail.recipe ? String(detail.recipe.id) : '',
|
|
||||||
dyeable: detail.customization.dyeable,
|
dyeable: detail.customization.dyeable,
|
||||||
dualDyeable: detail.customization.dualDyeable,
|
dualDyeable: detail.customization.dualDyeable,
|
||||||
patternEditable: detail.customization.patternEditable,
|
patternEditable: detail.customization.patternEditable,
|
||||||
@@ -288,7 +285,6 @@ async function saveItem() {
|
|||||||
name: itemForm.value.name,
|
name: itemForm.value.name,
|
||||||
categoryId: Number(itemForm.value.categoryId),
|
categoryId: Number(itemForm.value.categoryId),
|
||||||
usageId: itemForm.value.usageId ? Number(itemForm.value.usageId) : null,
|
usageId: itemForm.value.usageId ? Number(itemForm.value.usageId) : null,
|
||||||
recipeId: itemForm.value.recipeId ? Number(itemForm.value.recipeId) : null,
|
|
||||||
dyeable: itemForm.value.dyeable,
|
dyeable: itemForm.value.dyeable,
|
||||||
dualDyeable: itemForm.value.dualDyeable,
|
dualDyeable: itemForm.value.dualDyeable,
|
||||||
patternEditable: itemForm.value.patternEditable,
|
patternEditable: itemForm.value.patternEditable,
|
||||||
@@ -313,7 +309,7 @@ async function removeItem(id: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resetRecipeForm() {
|
function resetRecipeForm() {
|
||||||
recipeForm.value = { id: 0, name: '', acquisitionMethodIds: [], materials: [] };
|
recipeForm.value = { id: 0, itemId: '', acquisitionMethodIds: [], materials: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
function addRecipeMaterial() {
|
function addRecipeMaterial() {
|
||||||
@@ -325,7 +321,7 @@ async function editRecipe(item: Recipe) {
|
|||||||
const detail = await api.recipeDetail(item.id);
|
const detail = await api.recipeDetail(item.id);
|
||||||
recipeForm.value = {
|
recipeForm.value = {
|
||||||
id: detail.id,
|
id: detail.id,
|
||||||
name: detail.name,
|
itemId: String(detail.item.id),
|
||||||
acquisitionMethodIds: detail.acquisition_methods.map((method) => String(method.id)),
|
acquisitionMethodIds: detail.acquisition_methods.map((method) => String(method.id)),
|
||||||
materials: detail.materials.map((material) => ({ itemId: String(material.id), quantity: material.quantity }))
|
materials: detail.materials.map((material) => ({ itemId: String(material.id), quantity: material.quantity }))
|
||||||
};
|
};
|
||||||
@@ -335,7 +331,7 @@ async function editRecipe(item: Recipe) {
|
|||||||
async function saveRecipe() {
|
async function saveRecipe() {
|
||||||
await run(async () => {
|
await run(async () => {
|
||||||
const payload: RecipePayload = {
|
const payload: RecipePayload = {
|
||||||
name: recipeForm.value.name,
|
itemId: Number(recipeForm.value.itemId),
|
||||||
acquisitionMethodIds: toIds(recipeForm.value.acquisitionMethodIds),
|
acquisitionMethodIds: toIds(recipeForm.value.acquisitionMethodIds),
|
||||||
materials: toQuantityRows(recipeForm.value.materials)
|
materials: toQuantityRows(recipeForm.value.materials)
|
||||||
};
|
};
|
||||||
@@ -345,7 +341,7 @@ async function saveRecipe() {
|
|||||||
await api.createRecipe(payload);
|
await api.createRecipe(payload);
|
||||||
}
|
}
|
||||||
resetRecipeForm();
|
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>
|
<option v-for="item in options.itemUsages" :key="item.id" :value="item.id">{{ item.name }}</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</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">
|
<div class="check-row">
|
||||||
<label><input v-model="itemForm.dyeable" type="checkbox" /> 可染色</label>
|
<label><input v-model="itemForm.dyeable" type="checkbox" /> 可染色</label>
|
||||||
<label><input v-model="itemForm.dualDyeable" 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">
|
<section v-if="activeTab === 'recipes' && options" class="admin-layout">
|
||||||
<form class="detail-section" @submit.prevent="saveRecipe">
|
<form class="detail-section" @submit.prevent="saveRecipe">
|
||||||
<h2>材料单</h2>
|
<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">
|
<div class="field">
|
||||||
<label for="recipe-methods">入手方式</label>
|
<label for="recipe-methods">入手方式</label>
|
||||||
<TagsSelect
|
<TagsSelect
|
||||||
|
|||||||
Reference in New Issue
Block a user