Initial release of Web Tools 0.0.5

This commit is contained in:
mid
2026-08-07 21:51:15 +08:00
commit fa14e9e5b9
71 changed files with 6365 additions and 0 deletions
+162
View File
@@ -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>