Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions web/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
<script setup>
import { RouterView } from 'vue-router'
import NavBar from './components/NavBar.vue'
import ConfirmModal from './components/ConfirmModal.vue'
import { useToast } from './composables/useToast'
import { useConfirm } from './composables/useConfirm'

const { toast } = useToast()
const { state: confirmState, handleConfirm, handleCancel } = useConfirm()
</script>

<template>
Expand All @@ -13,6 +16,16 @@ const { toast } = useToast()
<RouterView class="animate-fade-in" />
</main>
<ToastMessage v-model="toast.show" :message="toast.message" :type="toast.type" />
<ConfirmModal
v-model:show="confirmState.show"
:title="confirmState.title"
:message="confirmState.message"
:confirm-text="confirmState.confirmText"
:cancel-text="confirmState.cancelText"
:type="confirmState.type"
@confirm="handleConfirm"
@cancel="handleCancel"
/>
</div>
</template>

Expand Down
92 changes: 92 additions & 0 deletions web/src/components/ConfirmModal.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<script setup>
import { ref, watch, computed } from 'vue'

const props = defineProps({
show: {
type: Boolean,
default: false,
},
title: {
type: String,
default: '确认',
},
message: {
type: String,
required: true,
},
confirmText: {
type: String,
default: '确认',
},
cancelText: {
type: String,
default: '取消',
},
type: {
type: String,
default: 'info', // info, warning, error, success
validator: (value) => ['info', 'warning', 'error', 'success'].includes(value),
},
})

const emit = defineEmits(['confirm', 'cancel', 'update:show'])

const modalRef = ref(null)

const confirmButtonClass = computed(() => {
const typeMap = {
info: 'btn-primary',
warning: 'btn-warning',
error: 'btn-error',
success: 'btn-success',
}
return typeMap[props.type] || 'btn-primary'
})

watch(
() => props.show,
(newVal) => {
if (newVal && modalRef.value) {
modalRef.value.showModal()
} else if (!newVal && modalRef.value) {
modalRef.value.close()
}
},
)

const handleConfirm = () => {
emit('confirm')
emit('update:show', false)
if (modalRef.value) {
modalRef.value.close()
}
}

const handleCancel = () => {
emit('cancel')
emit('update:show', false)
if (modalRef.value) {
modalRef.value.close()
}
}
</script>

<template>
<dialog ref="modalRef" class="modal modal-bottom sm:modal-middle">
<div class="modal-box">
<h3 class="text-lg font-bold">{{ title }}</h3>
<p class="py-4">{{ message }}</p>
<div class="modal-action">
<button type="button" class="btn" @click="handleCancel">
{{ cancelText }}
</button>
<button type="button" :class="['btn', confirmButtonClass]" @click="handleConfirm">
{{ confirmText }}
</button>
</div>
</div>
<form method="dialog" class="modal-backdrop" @click="handleCancel">
<button type="button">close</button>
</form>
</dialog>
</template>
26 changes: 18 additions & 8 deletions web/src/components/NoteEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ref, watch, computed, onBeforeUnmount } from 'vue'
import noteService from '../api/noteService'
import FileFolderSelector from './FileFolderSelector.vue'
import { useToast } from '@/composables/useToast'
import { useConfirm } from '@/composables/useConfirm'
import { getFileIcon, getFileTypeColor } from '@/utils/file'
import { formatSize } from '@/utils/format'

Expand All @@ -27,6 +28,7 @@ let autoSaveTimer = null
const currentNoteId = ref(null)

const { showToast } = useToast()
const { confirm: showConfirm } = useConfirm()

const isDirty = computed(() => {
return (
Expand Down Expand Up @@ -177,28 +179,36 @@ const handleAttachItems = async ({ files: fileIds, folders: folderIds }) => {
const handleDetachFile = async (fileId) => {
if (!props.note || !props.note.id) return

if (!confirm('确定要移除这个文件的关联吗?')) return

try {
await showConfirm('确定要移除这个文件的关联吗?', {
title: '确认移除',
type: 'warning',
})
await noteService.detachFiles(props.note.id, [fileId])
attachedFiles.value = attachedFiles.value.filter((file) => file.id !== fileId)
} catch (error) {
console.error('Failed to detach file', error)
showToast('移除文件关联失败', 'error')
if (error !== false) {
console.error('Failed to detach file', error)
showToast('移除文件关联失败', 'error')
}
}
}

const handleDetachFolder = async (folderId) => {
if (!props.note || !props.note.id) return

if (!confirm('确定要移除这个文件夹的关联吗?')) return

try {
await showConfirm('确定要移除这个文件夹的关联吗?', {
title: '确认移除',
type: 'warning',
})
await noteService.detachFolders(props.note.id, [folderId])
attachedFolders.value = attachedFolders.value.filter((folder) => folder.id !== folderId)
} catch (error) {
console.error('Failed to detach folder', error)
showToast('移除文件夹关联失败', 'error')
if (error !== false) {
console.error('Failed to detach folder', error)
showToast('移除文件夹关联失败', 'error')
}
}
}
</script>
Expand Down
26 changes: 20 additions & 6 deletions web/src/components/UnifiedNotes.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ref, watch, computed } from 'vue'
import { marked } from 'marked'
import DOMPurify from 'dompurify'
import noteService from '../api/noteService'
import { useConfirm } from '@/composables/useConfirm'
import { formatDate } from '@/utils/format'
import { getFileIcon, getFileTypeColor } from '@/utils/file'

Expand All @@ -24,6 +25,8 @@ const props = defineProps({

const emit = defineEmits(['close', 'note-created'])

const { confirm: showConfirm } = useConfirm()

const notes = ref([])
const selectedNote = ref(null)
const loading = ref(false)
Expand Down Expand Up @@ -133,9 +136,12 @@ const renderedContent = computed(() => {
})

const detachNote = async (noteId) => {
if (!confirm('确定要取消此笔记的关联吗?(笔记本身不会被删除)')) return

try {
await showConfirm('确定要取消此笔记的关联吗?(笔记本身不会被删除)', {
title: '确认取消关联',
type: 'warning',
})

if (props.itemType === 'folder') {
await noteService.detachFolders(noteId, [props.item.id])
} else {
Expand All @@ -148,22 +154,30 @@ const detachNote = async (noteId) => {
}
emit('note-created')
} catch (error) {
console.error('Failed to detach note', error)
if (error !== false) {
console.error('Failed to detach note', error)
}
}
}

const deleteNote = async (noteId) => {
if (!confirm('确定要永久删除这条笔记吗?此操作无法撤销!')) return

try {
await showConfirm('确定要永久删除这条笔记吗?此操作无法撤销!', {
title: '确认删除',
type: 'error',
confirmText: '删除',
})

await noteService.deleteNote(noteId)
await loadNotes()
if (selectedNote.value?.id === noteId) {
selectedNote.value = null
}
emit('note-created')
} catch (error) {
console.error('Failed to delete note', error)
if (error !== false) {
console.error('Failed to delete note', error)
}
}
}

Expand Down
57 changes: 57 additions & 0 deletions web/src/composables/useConfirm.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { reactive } from 'vue'

const state = reactive({
show: false,
title: '确认',
message: '',
confirmText: '确认',
cancelText: '取消',
type: 'info',
resolvePromise: null,
rejectPromise: null,
})

export function useConfirm() {
const confirm = (message, options = {}) => {
return new Promise((resolve, reject) => {
state.show = true
state.message = message
state.title = options.title || '确认'
state.confirmText = options.confirmText || '确认'
state.cancelText = options.cancelText || '取消'
state.type = options.type || 'info'
state.resolvePromise = resolve
state.rejectPromise = reject
})
}

const handleConfirm = () => {
if (state.resolvePromise) {
state.resolvePromise(true)
}
resetState()
}

const handleCancel = () => {
if (state.rejectPromise) {
state.rejectPromise(false)
}
resetState()
}

const resetState = () => {
state.show = false
state.resolvePromise = null
state.rejectPromise = null
}

return {
state,
confirm,
handleConfirm,
handleCancel,
}
}

// 导出一个全局单例
export const confirmState = state
18 changes: 13 additions & 5 deletions web/src/views/admin/StorageBackendsView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import { ref, reactive, onMounted, watch } from 'vue'
import storageBackendService from '@/api/storageBackendService'
import { useToast } from '@/composables/useToast'
import { useConfirm } from '@/composables/useConfirm'

const BACKEND_TYPES = [
{
Expand All @@ -28,15 +29,16 @@ const BACKEND_TYPES = [

const S3_BACKEND_TYPES = ['aws_s3', 'aliyun_oss', 'minio', 's3']

const { showToast } = useToast()
const { confirm: showConfirm } = useConfirm()

const backends = ref([])
const loading = ref(true)
const submitting = ref(false)
const testingId = ref(null)
const isEditing = ref(false)
const currentId = ref(null)

const { showToast } = useToast()

const importing = ref(false)
const selectedFile = ref(null)
const importFileInput = ref(null)
Expand Down Expand Up @@ -179,14 +181,20 @@ const handleSubmit = async () => {
}

const handleDelete = async (backend) => {
if (!confirm(`确定要删除 "${backend.name}" 吗?此操作不可恢复。`)) return

try {
await showConfirm(`确定要删除 "${backend.name}" 吗?此操作不可恢复。`, {
title: '确认删除',
type: 'error',
confirmText: '删除',
})

await storageBackendService.deleteBackend(backend.id)
showToast('删除成功')
fetchBackends()
} catch (error) {
showToast(error.response?.data?.detail || '删除失败', 'error')
if (error !== false) {
showToast(error.response?.data?.detail || '删除失败', 'error')
}
}
}

Expand Down
13 changes: 10 additions & 3 deletions web/src/views/files/FileDetailView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@ import fileService from '@/api/fileService.js'
import UnifiedNotes from '@/components/UnifiedNotes.vue'
import PDFViewer from '@/components/PDFViewer.vue'
import TextViewer from '@/components/TextViewer.vue'
import { useConfirm } from '@/composables/useConfirm'

const route = useRoute()
const router = useRouter()
const { confirm: showConfirm } = useConfirm()

const file = ref(null)
const loading = ref(false)
Expand Down Expand Up @@ -79,13 +81,18 @@ const handleRename = async () => {
}

const handleDelete = async () => {
if (!confirm(`确定要删除文件 "${file.value.filename}" 吗?`)) return
try {
await showConfirm(`确定要删除文件 "${file.value.filename}" 吗?`, {
title: '确认删除',
type: 'error',
})
await fileService.deleteFile(file.value.id)
router.push({ name: 'files' })
} catch (error) {
console.error('删除失败:', error)
alert('删除失败')
if (error !== false) {
console.error('删除失败:', error)
alert('删除失败')
}
}
}

Expand Down
Loading
Loading