Initial release of Web Tools 0.0.5
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
<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 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>
|
||||
Reference in New Issue
Block a user