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)) {