Files
WebTools/src/views/SettingView.vue
T

163 lines
7.1 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { AlertCircle, ArrowDown, ArrowUp, CheckCircle2, GripVertical, RotateCcw, Save } from '@lucide/vue'
import { computed, onMounted, ref } from 'vue'
import { categories, categoryById, toolById } from '@/tools/registry'
import { useNavigation } from '@/composables/useNavigation'
import {
cloneNavigation,
moveCategoryBy,
moveToolBy,
moveToolToCategory,
relocateTool,
reorderCategory,
} from '@/lib/navigationOrder'
import type { NavigationConfig } from '@/types'
const { navigation, loadError, loadNavigation, resetLocalNavigation, saveNavigation } = useNavigation()
const draft = ref<NavigationConfig>(resetLocalNavigation())
const original = ref('')
const ready = ref(false)
const saving = ref(false)
const error = ref('')
const success = ref('')
const draggedTool = ref<{ categoryIndex: number; toolIndex: number }>()
const draggedCategory = ref<number>()
const dirty = computed(() => ready.value && JSON.stringify(draft.value) !== original.value)
function cloneCurrent() {
draft.value = cloneNavigation(navigation.value as NavigationConfig)
original.value = JSON.stringify(draft.value)
}
function moveCategory(index: number, delta: number) {
draft.value = moveCategoryBy(draft.value, index, delta)
}
function moveTool(categoryIndex: number, toolIndex: number, delta: number) {
draft.value = moveToolBy(draft.value, categoryIndex, toolIndex, delta)
}
function changeToolCategory(toolId: string, sourceCategoryIndex: number, targetCategoryId: string) {
draft.value = moveToolToCategory(draft.value, toolId, sourceCategoryIndex, targetCategoryId)
}
function startToolDrag(categoryIndex: number, toolIndex: number) {
draggedCategory.value = undefined
draggedTool.value = { categoryIndex, toolIndex }
}
function startCategoryDrag(categoryIndex: number) {
draggedTool.value = undefined
draggedCategory.value = categoryIndex
}
function dropTool(targetCategoryIndex: number, targetToolIndex: number) {
const source = draggedTool.value
draggedTool.value = undefined
if (!source) return
draft.value = relocateTool(draft.value, source, { categoryIndex: targetCategoryIndex, toolIndex: targetToolIndex })
}
function dropCategory(targetIndex: number) {
const sourceIndex = draggedCategory.value
draggedCategory.value = undefined
if (sourceIndex === undefined || sourceIndex === targetIndex) return
draft.value = reorderCategory(draft.value, sourceIndex, targetIndex)
}
function restoreDefaults() {
if (!ready.value) return
draft.value = resetLocalNavigation()
error.value = ''
success.value = '已恢复默认顺序,保存后生效'
}
async function save() {
if (!ready.value || !dirty.value) return
saving.value = true
error.value = ''
success.value = ''
try {
const result = await saveNavigation(cloneNavigation(draft.value))
draft.value = cloneNavigation(result)
original.value = JSON.stringify(result)
success.value = '导航顺序已保存'
} catch (reason) {
error.value = reason instanceof Error ? reason.message : '保存失败'
} finally {
saving.value = false
}
}
onMounted(async () => {
await loadNavigation()
cloneCurrent()
if (loadError.value) error.value = loadError.value
ready.value = true
})
</script>
<template>
<section class="setting-page">
<header class="setting-heading">
<div>
<p class="setting-category">工具管理</p>
<h1>导航顺序</h1>
<p class="setting-description">拖动分类或工具调整顺序,也可以用方向按钮精确移动。</p>
</div>
<div class="setting-heading-actions">
<button class="action-button" type="button" :disabled="!ready || saving" @click="restoreDefaults"><RotateCcw :size="15" />恢复默认</button>
<button class="action-button primary" type="button" :disabled="!ready || saving || !dirty" @click="save"><Save :size="15" />{{ saving ? '保存中…' : '保存顺序' }}</button>
</div>
</header>
<div v-if="!ready" class="setting-loading" role="status">正在加载导航配置…</div>
<div v-else class="setting-list">
<section
v-for="(navigationCategory, categoryIndex) in draft.categories"
:key="navigationCategory.id"
class="setting-category-block"
@dragover.prevent
@drop="draggedTool ? dropTool(categoryIndex, navigationCategory.tools.length) : dropCategory(categoryIndex)"
>
<header class="setting-category-header" draggable="true" @dragstart="startCategoryDrag(categoryIndex)" @dragend="draggedCategory = undefined">
<GripVertical class="drag-handle" :size="16" aria-hidden="true" />
<h2>{{ categoryById.get(navigationCategory.id)?.title }}</h2>
<small>{{ navigationCategory.tools.length }} 个工具</small>
<span class="row-actions">
<button class="row-action" type="button" aria-label="分类上移" :disabled="categoryIndex === 0" @click="moveCategory(categoryIndex, -1)"><ArrowUp :size="15" /></button>
<button class="row-action" type="button" aria-label="分类下移" :disabled="categoryIndex === draft.categories.length - 1" @click="moveCategory(categoryIndex, 1)"><ArrowDown :size="15" /></button>
</span>
</header>
<div class="setting-tool-list">
<div
v-for="(toolId, toolIndex) in navigationCategory.tools"
:key="toolId"
class="setting-tool-row"
:class="{ 'is-dragging': draggedTool?.categoryIndex === categoryIndex && draggedTool?.toolIndex === toolIndex }"
draggable="true"
@dragstart="startToolDrag(categoryIndex, toolIndex)"
@dragend="draggedTool = undefined"
@dragover.prevent
@drop.stop="dropTool(categoryIndex, toolIndex)"
>
<GripVertical class="drag-handle" :size="16" aria-hidden="true" />
<span class="tool-row-icon"><component :is="toolById.get(toolId)?.iconComponent" :size="15" /></span>
<strong>{{ toolById.get(toolId)?.title }}</strong>
<select :id="`setting-tool-category-${toolId}`" :name="`setting-tool-category-${toolId}`" class="select-input" :value="navigationCategory.id" aria-label="所属分类" @change="changeToolCategory(toolId, categoryIndex, ($event.target as HTMLSelectElement).value)">
<option v-for="category in categories" :key="category.id" :value="category.id">{{ category.title }}</option>
</select>
<span class="row-actions">
<button class="row-action" type="button" aria-label="工具上移" :disabled="toolIndex === 0" @click="moveTool(categoryIndex, toolIndex, -1)"><ArrowUp :size="15" /></button>
<button class="row-action" type="button" aria-label="工具下移" :disabled="toolIndex === navigationCategory.tools.length - 1" @click="moveTool(categoryIndex, toolIndex, 1)"><ArrowDown :size="15" /></button>
</span>
</div>
</div>
</section>
</div>
<div v-if="error" class="status-message error save-state"><AlertCircle :size="16" />{{ error }}</div>
<div v-else-if="success" class="status-message success save-state"><CheckCircle2 :size="16" />{{ success }}</div>
</section>
</template>