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
This commit is contained in:
2026-04-30 15:24:19 +08:00
parent 3e8265e0c8
commit 02f6dd47c3
11 changed files with 56 additions and 97 deletions

View File

@@ -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<AuthUser | null>(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<TabOption[]>(() => 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(() => {
<label for="config-name">名称</label>
<input id="config-name" v-model="configForm.name" required />
</div>
<div v-if="selectedConfig.hasSubcategory" class="field">
<label for="config-subcategory">二级分类</label>
<input id="config-subcategory" v-model="configForm.subcategory" />
</div>
<div class="form-actions">
<button type="submit" class="link-button" :disabled="busy">{{ busy ? '保存中' : '保存' }}</button>
<button type="button" class="plain-button" :disabled="busy" @click="resetConfigForm">新建</button>
@@ -260,7 +254,7 @@ onMounted(() => {
<h3 class="section-subtitle">{{ selectedConfig.label }}</h3>
<ul v-if="configRows.length" class="row-list">
<li v-for="item in configRows" :key="item.id">
<span>{{ item.name }}<span v-if="item.subcategory"> · {{ item.subcategory }}</span></span>
<span>{{ item.name }}</span>
<span class="row-actions">
<button type="button" @click="editConfig(item)">编辑</button>
<button type="button" @click="removeConfig(item.id)">删除</button>

View File

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

View File

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

View File

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

View File

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