思维导图详情页增加重复项和重命名菜单

This commit is contained in:
jingrow 2026-04-19 17:26:50 +08:00
parent eac89e8f0f
commit 3979ba7ba7

View File

@ -70,6 +70,28 @@
{{ t('Back') }}
</n-button>
<!-- 三点菜单 -->
<n-dropdown
v-if="menuOptions.length > 0"
:options="menuOptions"
trigger="click"
:render-icon="renderMenuIcon"
@select="handleMenuSelect"
>
<n-button
type="default"
size="medium"
:title="t('More')"
class="header-action-btn"
>
<template #icon>
<n-icon>
<Icon icon="tabler:dots" />
</n-icon>
</template>
</n-button>
</n-dropdown>
<!-- 保存按钮 -->
<n-button
type="primary"
@ -86,16 +108,42 @@
</template>
{{ t('Save') }}
</n-button>
<!-- 重命名对话框 -->
<n-modal
v-model:show="showRenameDialog"
preset="dialog"
:title="t('Rename')"
:positive-text="t('OK')"
:negative-text="t('Cancel')"
:positive-button-props="{ type: 'primary' }"
:loading="renaming"
@positive-click="handleRenameConfirm"
style="width: 450px;"
>
<n-form ref="renameFormRef" :model="renameForm" :rules="renameRules" label-placement="top">
<n-form-item :label="t('New Name')" path="newName">
<n-input v-model:value="renameForm.newName" :placeholder="t('Enter new name')" />
</n-form-item>
<n-form-item :label="t('Merge with existing')" path="merge">
<n-checkbox v-model:checked="renameForm.merge">
<span>{{ t('Merge with existing') }}</span>
<n-text depth="3" style="margin-left: 4px;">({{ t('This cannot be undone') }})</n-text>
</n-checkbox>
</n-form-item>
</n-form>
</n-modal>
</n-space>
</template>
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
import { NButton, NSpace, NIcon } from 'naive-ui'
import { ref, computed, watch, onMounted, h } from 'vue'
import { NButton, NSpace, NIcon, NDropdown, NModal, NForm, NFormItem, NInput, NCheckbox, NText, useMessage, type FormInst, type FormRules, type DropdownOption } from 'naive-ui'
import { Icon } from '@iconify/vue'
import { useRouter } from 'vue-router'
import { t } from '@/shared/i18n'
import { pageTypeToSlug } from '@/shared/utils/slug'
import { renameRecord, duplicateRecord } from '@/shared/api/common'
interface Props {
entity: string
@ -104,6 +152,7 @@ interface Props {
canEdit: boolean
loading: boolean
sidebarPosition?: 'left' | 'right'
pageMeta?: any
}
interface Emits {
@ -112,14 +161,17 @@ interface Emits {
(e: 'delete'): void
(e: 'go-back'): void
(e: 'save'): void
(e: 'rename', newName: string): void
(e: 'duplicate', newName: string): void
}
const props = withDefaults(defineProps<Props>(), {
sidebarPosition: 'left',
record: () => ({})
record: () => ({}),
pageMeta: () => ({})
})
defineEmits<Emits>()
const emit = defineEmits<Emits>()
const router = useRouter()
@ -155,6 +207,123 @@ function handleMindMap() {
const pagetype = pageTypeToSlug(props.entity)
router.push({ name: 'MindMapEditor', query: { pagetype, id: props.id } })
}
// ===== =====
const canRename = computed(() => {
if (isNew.value) return false
if (!props.canEdit) return false
return !!(props.pageMeta?.allow_rename)
})
// ===== =====
const menuOptions = computed<DropdownOption[]>(() => {
const options: DropdownOption[] = []
if (!isNew.value && props.canEdit && !props.pageMeta?.allow_copy) {
options.push({
label: t('Duplicate'),
key: 'duplicate',
icon: 'tabler:copy'
})
}
if (canRename.value) {
options.push({
label: t('Rename'),
key: 'rename',
icon: 'tabler:edit'
})
}
return options
})
function renderMenuIcon(option: DropdownOption) {
const iconName = option.icon as string
if (!iconName) return null
return h(NIcon, null, { default: () => h(Icon, { icon: iconName }) })
}
function handleMenuSelect(key: string) {
if (key === 'rename') {
openRenameDialog()
} else if (key === 'duplicate') {
handleDuplicate()
}
}
// ===== =====
const duplicating = ref(false)
const message = useMessage()
async function handleDuplicate() {
if (duplicating.value || isNew.value) return
duplicating.value = true
try {
const result = await duplicateRecord(props.entity, props.id)
if (result.success && result.data) {
const storageKey = `duplicate:${props.entity}`
try {
sessionStorage.setItem(storageKey, JSON.stringify(result.data))
} catch {}
emit('duplicate', '')
} else {
message.error(result.message || t('Failed to duplicate record'))
}
} catch (err: any) {
message.error(err?.message || t('Failed to duplicate record'))
} finally {
duplicating.value = false
}
}
// ===== =====
const showRenameDialog = ref(false)
const renaming = ref(false)
const renameFormRef = ref<FormInst | null>(null)
const renameForm = ref({
newName: '',
merge: false
})
const renameRules: FormRules = {
newName: [
{ required: true, message: t('New name is required'), trigger: 'blur' }
]
}
function openRenameDialog() {
renameForm.value.newName = props.id
renameForm.value.merge = false
showRenameDialog.value = true
}
async function handleRenameConfirm() {
try {
await renameFormRef.value?.validate()
} catch {
return false
}
const { newName, merge } = renameForm.value
if (!newName || newName === props.id) {
message.info(t('No changes made'))
return false
}
renaming.value = true
try {
const result = await renameRecord(props.entity, props.id, newName, merge)
if (result.success) {
showRenameDialog.value = false
emit('rename', result.data || newName)
} else {
message.error(result.message || t('Failed to rename document'))
return false
}
} catch (err: any) {
message.error(err?.message || t('Failed to rename document'))
return false
} finally {
renaming.value = false
}
}
</script>
<style scoped>