From 02f6dd47c3b9dea6b5c221b5d0fcdf90818c8d5f Mon Sep 17 00:00:00 2001 From: xiaomai Date: Thu, 30 Apr 2026 15:24:19 +0800 Subject: [PATCH] refactor(skills): remove subcategory field from skills Drop subcategory column from database schema and update constraints Remove subcategory handling from backend queries and API endpoints Clean up frontend components and admin views to reflect the change --- DESIGN.md | 2 - backend/db/schema.sql | 7 ++- backend/src/queries.ts | 82 ++++++++----------------- frontend/src/components/EntityChips.vue | 5 +- frontend/src/components/TagsSelect.vue | 3 +- frontend/src/services/api.ts | 18 +++--- frontend/src/views/AdminView.vue | 24 +++----- frontend/src/views/HabitatEdit.vue | 2 +- frontend/src/views/ItemEdit.vue | 4 +- frontend/src/views/PokemonEdit.vue | 4 +- frontend/src/views/RecipeEdit.vue | 2 +- 11 files changed, 56 insertions(+), 97 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index 0681db8..4ecabb0 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -23,8 +23,6 @@ Pokemon 可配置: 特长 可配置: - 名称 -- 二级分类(可空,用于给乱撒这类特长做二级分类) -Eg: 名称:乱撒,二级分类:棉花 喜欢的环境 可配置: - 名称 diff --git a/backend/db/schema.sql b/backend/db/schema.sql index aaf3f89..21548ba 100644 --- a/backend/db/schema.sql +++ b/backend/db/schema.sql @@ -40,11 +40,12 @@ CREATE INDEX IF NOT EXISTS user_sessions_user_id_idx CREATE TABLE IF NOT EXISTS skills ( id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, - name text NOT NULL, - subcategory text, - UNIQUE (name, subcategory) + name text NOT NULL UNIQUE ); +ALTER TABLE skills DROP COLUMN IF EXISTS subcategory; +CREATE UNIQUE INDEX IF NOT EXISTS skills_name_key ON skills(name); + CREATE TABLE IF NOT EXISTS favorite_things ( id integer GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, name text NOT NULL UNIQUE diff --git a/backend/src/queries.ts b/backend/src/queries.ts index 477abc4..1a8c8d9 100644 --- a/backend/src/queries.ts +++ b/backend/src/queries.ts @@ -19,9 +19,7 @@ type ConfigType = type ConfigDefinition = { table: string; - select: string; order: string; - hasSubcategory?: boolean; }; type IdQuantity = { @@ -73,13 +71,13 @@ const timeOfDays = ['早晨', '中午', '傍晚', '晚上']; const weathers = ['晴天', '阴天', '雨天']; const configDefinitions: Record = { - skills: { table: 'skills', select: 'id, name, subcategory', order: 'name, subcategory', hasSubcategory: true }, - environments: { table: 'environments', select: 'id, name', order: 'name' }, - 'favorite-things': { table: 'favorite_things', select: 'id, name', order: 'name' }, - 'item-categories': { table: 'item_categories', select: 'id, name', order: 'name' }, - 'item-usages': { table: 'item_usages', select: 'id, name', order: 'name' }, - 'acquisition-methods': { table: 'acquisition_methods', select: 'id, name', order: 'name' }, - maps: { table: 'maps', select: 'id, name', order: 'name' } + skills: { table: 'skills', order: 'name' }, + environments: { table: 'environments', order: 'name' }, + 'favorite-things': { table: 'favorite_things', order: 'name' }, + 'item-categories': { table: 'item_categories', order: 'name' }, + 'item-usages': { table: 'item_usages', order: 'name' }, + 'acquisition-methods': { table: 'acquisition_methods', order: 'name' }, + maps: { table: 'maps', order: 'name' } }; function asString(value: QueryValue): string | undefined { @@ -112,10 +110,6 @@ function auditJoins(entityAlias: string, createdAlias = 'created_user', updatedA `; } -function configSelect(definition: ConfigDefinition): string { - return definition.hasSubcategory ? 'c.id, c.name, c.subcategory' : 'c.id, c.name'; -} - function configOrder(definition: ConfigDefinition): string { return definition.order .split(', ') @@ -215,7 +209,7 @@ const pokemonProjection = ` ${auditSelect('p', 'pokemon_created_user', 'pokemon_updated_user')}, json_build_object('id', e.id, 'name', e.name) AS environment, COALESCE(( - SELECT json_agg(json_build_object('id', s.id, 'name', s.name, 'subcategory', s.subcategory) ORDER BY s.name, s.subcategory) + SELECT json_agg(json_build_object('id', s.id, 'name', s.name) ORDER BY s.name) FROM pokemon_skills ps JOIN skills s ON s.id = ps.skill_id WHERE ps.pokemon_id = p.id @@ -241,9 +235,7 @@ export async function getOptions() { acquisitionMethods, maps ] = await Promise.all([ - query<{ id: number; name: string; subcategory: string | null }>( - 'SELECT id, name, subcategory FROM skills ORDER BY name, subcategory' - ), + optionSelect('skills'), optionSelect('environments'), optionSelect('favorite_things'), optionSelect('item_categories'), @@ -272,7 +264,7 @@ export async function listConfig(type: ConfigType) { const definition = configDefinitions[type]; return query( ` - SELECT ${configSelect(definition)}, ${auditSelect('c')} + SELECT c.id, c.name, ${auditSelect('c')} FROM ${definition.table} c ${auditJoins('c')} ORDER BY ${configOrder(definition)} @@ -284,7 +276,7 @@ async function getConfigById(type: ConfigType, id: number) { const definition = configDefinitions[type]; return queryOne( ` - SELECT ${configSelect(definition)}, ${auditSelect('c')} + SELECT c.id, c.name, ${auditSelect('c')} FROM ${definition.table} c ${auditJoins('c')} WHERE c.id = $1 @@ -296,26 +288,16 @@ async function getConfigById(type: ConfigType, id: number) { export async function createConfig(type: ConfigType, payload: Record, userId: number) { const definition = configDefinitions[type]; const name = cleanName(payload.name); - const subcategory = typeof payload.subcategory === 'string' && payload.subcategory.trim() ? payload.subcategory.trim() : null; const id = await withTransaction(async (client) => { - const result = definition.hasSubcategory - ? await client.query<{ id: number }>( - ` - INSERT INTO ${definition.table} (name, subcategory, created_by_user_id, updated_by_user_id) - VALUES ($1, $2, $3, $3) - RETURNING id - `, - [name, subcategory, userId] - ) - : await client.query<{ id: number }>( - ` - INSERT INTO ${definition.table} (name, created_by_user_id, updated_by_user_id) - VALUES ($1, $2, $2) - RETURNING id - `, - [name, userId] - ); + const result = await client.query<{ id: number }>( + ` + INSERT INTO ${definition.table} (name, created_by_user_id, updated_by_user_id) + VALUES ($1, $2, $2) + RETURNING id + `, + [name, userId] + ); const createdId = result.rows[0].id; await recordEditLog(client, type, createdId, 'create', userId); @@ -328,26 +310,16 @@ export async function createConfig(type: ConfigType, payload: Record, userId: number) { const definition = configDefinitions[type]; const name = cleanName(payload.name); - const subcategory = typeof payload.subcategory === 'string' && payload.subcategory.trim() ? payload.subcategory.trim() : null; const updated = await withTransaction(async (client) => { - const result = definition.hasSubcategory - ? await client.query( - ` - UPDATE ${definition.table} - SET name = $1, subcategory = $2, updated_by_user_id = $3, updated_at = now() - WHERE id = $4 - `, - [name, subcategory, userId, id] - ) - : await client.query( - ` - UPDATE ${definition.table} - SET name = $1, updated_by_user_id = $2, updated_at = now() - WHERE id = $3 - `, - [name, userId, id] - ); + const result = await client.query( + ` + UPDATE ${definition.table} + SET name = $1, updated_by_user_id = $2, updated_at = now() + WHERE id = $3 + `, + [name, userId, id] + ); if (result.rowCount === 0) { return false; diff --git a/frontend/src/components/EntityChips.vue b/frontend/src/components/EntityChips.vue index 0b53f78..7edd03c 100644 --- a/frontend/src/components/EntityChips.vue +++ b/frontend/src/components/EntityChips.vue @@ -2,15 +2,14 @@ import type { NamedEntity } from '../services/api'; defineProps<{ - items: Array; + items: Array; }>(); diff --git a/frontend/src/components/TagsSelect.vue b/frontend/src/components/TagsSelect.vue index efffab2..e3a19fc 100644 --- a/frontend/src/components/TagsSelect.vue +++ b/frontend/src/components/TagsSelect.vue @@ -5,7 +5,6 @@ export type TagsSelectOption = { id: number | string; name: string; label?: string; - subcategory?: string | null; }; type OptionRow = { @@ -56,7 +55,7 @@ const activeIndex = ref(-1); const optionRows = computed(() => props.options.map((option, index) => ({ value: String(option.id), - label: option.label ?? (option.subcategory ? `${option.name} · ${option.subcategory}` : option.name), + label: option.label ?? option.name, id: `${props.id}-option-${index}` })) ); diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index ee69019..fba7394 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -7,10 +7,6 @@ export interface NamedEntity { name: string; } -export interface Skill extends NamedEntity { - subcategory: string | null; -} - export interface UserSummary { id: number; displayName: string; @@ -27,7 +23,7 @@ export interface Pokemon extends EditInfo { id: number; name: string; environment: NamedEntity; - skills: Skill[]; + skills: NamedEntity[]; favorite_things: NamedEntity[]; } @@ -89,7 +85,7 @@ export interface RecipeDetail extends Recipe { } export interface Options { - skills: Skill[]; + skills: NamedEntity[]; environments: NamedEntity[]; favoriteThings: NamedEntity[]; itemCategories: NamedEntity[]; @@ -283,11 +279,11 @@ export const api = { me: () => getJson<{ user: AuthUser }>('/api/auth/me'), logout: () => postEmpty('/api/auth/logout'), options: () => getJson('/api/options'), - config: (type: ConfigType) => getJson>(`/api/admin/config/${type}`), - createConfig: (type: ConfigType, payload: { name: string; subcategory?: string | null }) => - sendJson(`/api/admin/config/${type}`, 'POST', payload), - updateConfig: (type: ConfigType, id: number, payload: { name: string; subcategory?: string | null }) => - sendJson(`/api/admin/config/${type}/${id}`, 'PUT', payload), + config: (type: ConfigType) => getJson(`/api/admin/config/${type}`), + createConfig: (type: ConfigType, payload: { name: string }) => + sendJson(`/api/admin/config/${type}`, 'POST', payload), + updateConfig: (type: ConfigType, id: number, payload: { name: string }) => + sendJson(`/api/admin/config/${type}/${id}`, 'PUT', payload), deleteConfig: (type: ConfigType, id: number) => deleteJson(`/api/admin/config/${type}/${id}`), pokemon: (params: Record) => getJson(`/api/pokemon${buildQuery(params)}`), diff --git a/frontend/src/views/AdminView.vue b/frontend/src/views/AdminView.vue index fdf9ac4..3c4512a 100644 --- a/frontend/src/views/AdminView.vue +++ b/frontend/src/views/AdminView.vue @@ -12,12 +12,11 @@ import { type Item, type NamedEntity, type Pokemon, - type Recipe, - type Skill + type Recipe } from '../services/api'; type AdminTab = 'config' | 'pokemon' | 'items' | 'recipes' | 'habitats'; -type EditableConfig = (NamedEntity | Skill) & { subcategory?: string | null }; +type EditableConfig = NamedEntity; const tabs: Array<{ key: AdminTab; label: string }> = [ { key: 'config', label: '系统配置' }, @@ -27,8 +26,8 @@ const tabs: Array<{ key: AdminTab; label: string }> = [ { key: 'habitats', label: '栖息地' } ]; -const configTypes: Array<{ key: ConfigType; label: string; hasSubcategory?: boolean }> = [ - { key: 'skills', label: '特长', hasSubcategory: true }, +const configTypes: Array<{ key: ConfigType; label: string }> = [ + { key: 'skills', label: '特长' }, { key: 'environments', label: '喜欢的环境' }, { key: 'favorite-things', label: '喜欢的东西 / 标签' }, { key: 'item-categories', label: '物品分类' }, @@ -48,7 +47,7 @@ const currentUser = ref(null); const busy = ref(false); const contentLoading = ref(false); const message = ref(''); -const configForm = ref({ id: 0, name: '', subcategory: '' }); +const configForm = ref({ id: 0, name: '' }); const selectedConfig = computed(() => configTypes.find((item) => item.key === activeConfigType.value) ?? configTypes[0]); const configTabs = computed(() => configTypes.map((item) => ({ value: item.key, label: item.label }))); @@ -87,18 +86,17 @@ async function loadConfig() { } function resetConfigForm() { - configForm.value = { id: 0, name: '', subcategory: '' }; + configForm.value = { id: 0, name: '' }; } function editConfig(item: EditableConfig) { - configForm.value = { id: item.id, name: item.name, subcategory: item.subcategory ?? '' }; + configForm.value = { id: item.id, name: item.name }; } async function saveConfig() { await run(async () => { const payload = { - name: configForm.value.name, - subcategory: selectedConfig.value.hasSubcategory ? configForm.value.subcategory || null : null + name: configForm.value.name }; if (configForm.value.id) { @@ -247,10 +245,6 @@ onMounted(() => { -
- - -
@@ -260,7 +254,7 @@ onMounted(() => {

{{ selectedConfig.label }}

  • - {{ item.name }} · {{ item.subcategory }} + {{ item.name }} diff --git a/frontend/src/views/HabitatEdit.vue b/frontend/src/views/HabitatEdit.vue index d577bed..2ba27ff 100644 --- a/frontend/src/views/HabitatEdit.vue +++ b/frontend/src/views/HabitatEdit.vue @@ -138,7 +138,7 @@ async function createMultiOption(selectKey: string, type: ConfigType, name: stri creatingSelect.value = selectKey; message.value = ''; try { - const created = await api.createConfig(type, { name: cleanName, subcategory: null }); + const created = await api.createConfig(type, { name: cleanName }); await loadOptions(); const value = String(created.id); if (!values.includes(value)) { diff --git a/frontend/src/views/ItemEdit.vue b/frontend/src/views/ItemEdit.vue index abbda18..da1300a 100644 --- a/frontend/src/views/ItemEdit.vue +++ b/frontend/src/views/ItemEdit.vue @@ -75,7 +75,7 @@ async function createSingleOption(selectKey: string, type: ConfigType, name: str creatingSelect.value = selectKey; message.value = ''; try { - const created = await api.createConfig(type, { name: cleanName, subcategory: null }); + const created = await api.createConfig(type, { name: cleanName }); await loadOptions(); assign(String(created.id)); } catch (error) { @@ -92,7 +92,7 @@ async function createMultiOption(selectKey: string, type: ConfigType, name: stri creatingSelect.value = selectKey; message.value = ''; try { - const created = await api.createConfig(type, { name: cleanName, subcategory: null }); + const created = await api.createConfig(type, { name: cleanName }); await loadOptions(); const value = String(created.id); if (!values.includes(value)) { diff --git a/frontend/src/views/PokemonEdit.vue b/frontend/src/views/PokemonEdit.vue index f5dc8da..095f77c 100644 --- a/frontend/src/views/PokemonEdit.vue +++ b/frontend/src/views/PokemonEdit.vue @@ -69,7 +69,7 @@ async function createSingleOption(selectKey: string, type: ConfigType, name: str creatingSelect.value = selectKey; message.value = ''; try { - const created = await api.createConfig(type, { name: cleanName, subcategory: null }); + const created = await api.createConfig(type, { name: cleanName }); await loadOptions(); assign(String(created.id)); } catch (error) { @@ -86,7 +86,7 @@ async function createMultiOption(selectKey: string, type: ConfigType, name: stri creatingSelect.value = selectKey; message.value = ''; try { - const created = await api.createConfig(type, { name: cleanName, subcategory: null }); + const created = await api.createConfig(type, { name: cleanName }); await loadOptions(); const value = String(created.id); if (!values.includes(value)) { diff --git a/frontend/src/views/RecipeEdit.vue b/frontend/src/views/RecipeEdit.vue index 33619e8..121b4b7 100644 --- a/frontend/src/views/RecipeEdit.vue +++ b/frontend/src/views/RecipeEdit.vue @@ -81,7 +81,7 @@ async function createMultiOption(selectKey: string, type: ConfigType, name: stri creatingSelect.value = selectKey; message.value = ''; try { - const created = await api.createConfig(type, { name: cleanName, subcategory: null }); + const created = await api.createConfig(type, { name: cleanName }); await loadOptions(); const value = String(created.id); if (!values.includes(value)) {