feat(components): 新增创建分类和笔记对话框及头部组件
- 新增 CreateGroupDialog 组件用于创建分类 - 新增 CreateNoteDialog 组件用于创建笔记 - 新增 HomeHeader 组件用于显示主页头部信息 - 对话框组件使用 Element Plus 样式- 头部组件包含用户操作按钮和搜索功能
This commit is contained in:
File diff suppressed because it is too large
Load Diff
172
biji-qianduan/src/components/home/HomeHeader.vue
Normal file
172
biji-qianduan/src/components/home/HomeHeader.vue
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!-- Desktop Header -->
|
||||||
|
<el-header class="header" v-if="!isMobile">
|
||||||
|
<h1 @click="$emit('reset-view')" style="cursor: pointer; flex-grow: 1;">我的笔记</h1>
|
||||||
|
<div class="actions">
|
||||||
|
<el-input
|
||||||
|
:model-value="searchKeyword"
|
||||||
|
@update:model-value="$emit('update:searchKeyword', $event)"
|
||||||
|
placeholder="搜索笔记标题"
|
||||||
|
class="search-input"
|
||||||
|
@keyup.enter="$emit('search')"
|
||||||
|
>
|
||||||
|
<template #append>
|
||||||
|
<el-button @click="$emit('search')">
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
<div v-if="userStore.isLoggedIn" class="user-actions">
|
||||||
|
<span class="welcome-text">欢迎, {{ userStore.userInfo?.username }}</span>
|
||||||
|
<el-button type="danger" @click="$emit('logout')">退出</el-button>
|
||||||
|
<el-button type="primary" @click="$emit('show-update-password')">修改密码</el-button>
|
||||||
|
<el-button type="warning" @click="$emit('show-system-settings')">系统管理</el-button>
|
||||||
|
<el-button type="primary" @click="$emit('show-create-note')">新建笔记</el-button>
|
||||||
|
<el-upload
|
||||||
|
class="upload-btn"
|
||||||
|
action=""
|
||||||
|
:show-file-list="false"
|
||||||
|
:before-upload="handleUpload"
|
||||||
|
accept=".md"
|
||||||
|
>
|
||||||
|
<el-button type="success">上传Markdown</el-button>
|
||||||
|
</el-upload>
|
||||||
|
</div>
|
||||||
|
<div v-else class="guest-actions">
|
||||||
|
<el-button type="primary" @click="goToLogin">登录</el-button>
|
||||||
|
<el-button @click="goToRegister">注册</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-header>
|
||||||
|
|
||||||
|
<!-- Mobile Header -->
|
||||||
|
<el-header class="header mobile-header" v-if="isMobile">
|
||||||
|
<el-button @click="$emit('toggle-collapse')" text circle class="mobile-menu-toggle">
|
||||||
|
<el-icon size="24"><Menu /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
<h1 class="mobile-title" @click="$emit('reset-view')">我的笔记</h1>
|
||||||
|
<el-button text circle class="mobile-search-toggle" @click="$emit('search')">
|
||||||
|
<el-icon size="22"><Search /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
<el-button v-if="!userStore.isLoggedIn" text circle class="mobile-login-toggle" @click="goToLogin">
|
||||||
|
<el-icon size="24"><User /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
</el-header>
|
||||||
|
|
||||||
|
<!-- Mobile Search Bar -->
|
||||||
|
<div v-if="isMobile" class="mobile-search-container">
|
||||||
|
<el-input
|
||||||
|
:model-value="searchKeyword"
|
||||||
|
@update:model-value="$emit('update:searchKeyword', $event)"
|
||||||
|
placeholder="搜索笔记标题"
|
||||||
|
class="mobile-search-input"
|
||||||
|
@keyup.enter="$emit('search')"
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { Search, Menu, User } from '@element-plus/icons-vue';
|
||||||
|
import { useUserStore } from '@/stores/user';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
isMobile: Boolean,
|
||||||
|
searchKeyword: String,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits([
|
||||||
|
'update:searchKeyword',
|
||||||
|
'search',
|
||||||
|
'reset-view',
|
||||||
|
'logout',
|
||||||
|
'show-update-password',
|
||||||
|
'show-system-settings',
|
||||||
|
'show-create-note',
|
||||||
|
'upload-markdown',
|
||||||
|
'toggle-collapse'
|
||||||
|
]);
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const goToLogin = () => router.push('/login');
|
||||||
|
const goToRegister = () => router.push('/register');
|
||||||
|
|
||||||
|
const handleUpload = (file) => {
|
||||||
|
emit('upload-markdown', file);
|
||||||
|
return false; // Prevent el-upload's default behavior
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: var(--box-shadow-light);
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .header {
|
||||||
|
background-color: rgba(30, 30, 47, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.search-input .el-input__wrapper) {
|
||||||
|
border-radius: var(--border-radius) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-actions, .guest-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-text {
|
||||||
|
white-space: nowrap;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-btn {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-header {
|
||||||
|
padding: 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-title {
|
||||||
|
cursor: pointer;
|
||||||
|
flex-grow: 1;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-search-container {
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.mobile-search-input .el-input__wrapper) {
|
||||||
|
border-radius: 9999px !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
172
biji-qianduan/src/components/home/NoteEditor.vue
Normal file
172
biji-qianduan/src/components/home/NoteEditor.vue
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
<template>
|
||||||
|
<div class="note-editor-wrapper">
|
||||||
|
<el-header class="editor-header">
|
||||||
|
<h2 class="editor-title">{{ editData.title }}</h2>
|
||||||
|
<div class="actions">
|
||||||
|
<el-button type="primary" @click="$emit('back', editData)">返回</el-button>
|
||||||
|
<el-button type="success" @click="save">保存</el-button>
|
||||||
|
<span class="save-status">{{ saveStatus }}</span>
|
||||||
|
</div>
|
||||||
|
</el-header>
|
||||||
|
<div id="vditor-editor" class="vditor" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
|
||||||
|
import Vditor from 'vditor';
|
||||||
|
import 'vditor/dist/index.css';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { updateMarkdown, uploadImage } from '@/api/CommonApi.js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
editData: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['back', 'save-success']);
|
||||||
|
|
||||||
|
const vditor = ref(null);
|
||||||
|
const saveStatus = ref('空闲');
|
||||||
|
let debounceTimer = null;
|
||||||
|
|
||||||
|
const initVditor = () => {
|
||||||
|
vditor.value = new Vditor('vditor-editor', {
|
||||||
|
height: 'calc(100vh - 120px)',
|
||||||
|
mode: 'ir',
|
||||||
|
after: () => {
|
||||||
|
if (props.editData && props.editData.content) {
|
||||||
|
vditor.value.setValue(props.editData.content);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
input: (value) => {
|
||||||
|
saveStatus.value = '正在输入...';
|
||||||
|
clearTimeout(debounceTimer);
|
||||||
|
debounceTimer = setTimeout(() => {
|
||||||
|
handleSave(value);
|
||||||
|
}, 2000);
|
||||||
|
},
|
||||||
|
upload: {
|
||||||
|
accept: 'image/*',
|
||||||
|
handler(files) {
|
||||||
|
handleImageUpload(files);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImageUpload = async (files) => {
|
||||||
|
const file = files;
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const promise = await uploadImage(file);
|
||||||
|
if (promise.url == null) {
|
||||||
|
ElMessage.error(promise.msg || '图片上传失败');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const fullUrl = `${import.meta.env.VITE_API_BASE_URL}${promise.url}`;
|
||||||
|
vditor.value.insertValue(``);
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('图片上传失败: ' + error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async (content) => {
|
||||||
|
saveStatus.value = '正在保存...';
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
id: props.editData.id,
|
||||||
|
title: props.editData.title,
|
||||||
|
groupingId: props.editData.groupingId,
|
||||||
|
content: content,
|
||||||
|
fileName: props.editData.fileName || `${props.editData.title}.md`,
|
||||||
|
isPrivate: props.editData.isPrivate,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await updateMarkdown(payload);
|
||||||
|
emit('save-success', response);
|
||||||
|
saveStatus.value = '已保存';
|
||||||
|
ElMessage.success('保存成功');
|
||||||
|
} catch (error) {
|
||||||
|
saveStatus.value = '保存失败';
|
||||||
|
ElMessage.error('保存失败: ' + (error.response?.data?.message || error.message));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const save = () => {
|
||||||
|
if (vditor.value) {
|
||||||
|
handleSave(vditor.value.getValue());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initVditor();
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
if (vditor.value) {
|
||||||
|
vditor.value.destroy();
|
||||||
|
}
|
||||||
|
clearTimeout(debounceTimer);
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.editData, (newData) => {
|
||||||
|
if (vditor.value && newData) {
|
||||||
|
vditor.value.setValue(newData.content || '');
|
||||||
|
}
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
// Expose the save method to the parent
|
||||||
|
defineExpose({ save });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.note-editor-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: var(--box-shadow-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .editor-header {
|
||||||
|
background-color: rgba(30, 30, 47, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-status {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
width: 80px; /* Give it a fixed width to prevent layout shifts */
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.vditor {
|
||||||
|
flex-grow: 1;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
80
biji-qianduan/src/components/home/NoteList.vue
Normal file
80
biji-qianduan/src/components/home/NoteList.vue
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="files.length > 0" class="file-list">
|
||||||
|
<el-card
|
||||||
|
v-for="file in files"
|
||||||
|
:key="file.id"
|
||||||
|
shadow="hover"
|
||||||
|
class="file-item"
|
||||||
|
:class="{ 'private-note': file.isPrivate === 1 }"
|
||||||
|
>
|
||||||
|
<div @click="$emit('preview', file)" class="file-title">
|
||||||
|
<span>{{ file.title }}</span>
|
||||||
|
<span class="file-group-name">{{ file.groupingName }}</span>
|
||||||
|
<el-icon v-if="file.isPrivate === 1 && !isUserLoggedIn" class="lock-icon"><Lock /></el-icon>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
<el-empty v-else description="暂无笔记,请创建或上传" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { Lock } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
files: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isUserLoggedIn: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
defineEmits(['preview']);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.file-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item {
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition: all var(--transition-duration) ease;
|
||||||
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .file-item {
|
||||||
|
background-color: rgba(30, 30, 47, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-title {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-group-name {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
background-color: var(--bg-color-secondary);
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lock-icon {
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
140
biji-qianduan/src/components/home/NotePreview.vue
Normal file
140
biji-qianduan/src/components/home/NotePreview.vue
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
<template>
|
||||||
|
<div class="file-preview">
|
||||||
|
<el-header class="preview-header">
|
||||||
|
<h2 class="preview-title">
|
||||||
|
<span>{{ file.title }}</span>
|
||||||
|
<el-icon v-if="file.isPrivate === 1" class="lock-icon"><Lock /></el-icon>
|
||||||
|
<el-icon class="edit-icon" @click="$emit('open-rename-dialog', file, 'file')"><Edit /></el-icon>
|
||||||
|
</h2>
|
||||||
|
<div class="actions">
|
||||||
|
<el-button :circle="isMobile" :size="isMobile ? 'small' : 'default'" type="primary" @click="$emit('back')">
|
||||||
|
<el-icon v-if="isMobile"><Back /></el-icon>
|
||||||
|
<span v-else>返回</span>
|
||||||
|
</el-button>
|
||||||
|
<el-button v-if="isUserLoggedIn && !isMobile" type="warning" @click="$emit('show-move-note-dialog')">移动</el-button>
|
||||||
|
<el-button v-if="isUserLoggedIn" :circle="isMobile" :size="isMobile ? 'small' : 'default'" type="primary" @click="$emit('edit')">
|
||||||
|
<el-icon v-if="isMobile"><Edit /></el-icon>
|
||||||
|
<span v-else>编辑</span>
|
||||||
|
</el-button>
|
||||||
|
<el-button v-if="isUserLoggedIn" :circle="isMobile" :size="isMobile ? 'small' : 'default'" type="danger" @click="$emit('delete')">
|
||||||
|
<el-icon v-if="isMobile"><Delete /></el-icon>
|
||||||
|
<span v-else>删除</span>
|
||||||
|
</el-button>
|
||||||
|
<el-button v-if="isUserLoggedIn" :circle="isMobile" :size="isMobile ? 'small' : 'default'" type="info" @click="$emit('show-privacy-dialog')">
|
||||||
|
<el-icon v-if="isMobile"><Lock /></el-icon>
|
||||||
|
<span v-else>{{ file.isPrivate === 1 ? '设为公开' : '设为私密' }}</span>
|
||||||
|
</el-button>
|
||||||
|
<el-dropdown v-if="isUserLoggedIn && !isMobile" @command="handleExport">
|
||||||
|
<el-button type="success">
|
||||||
|
导出<el-icon class="el-icon--right"><arrow-down /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item command="md">导出为 .md</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="pdf">导出为 .pdf</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="html">导出为 .html</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
</el-header>
|
||||||
|
<div :key="file.id" class="markdown-preview">
|
||||||
|
<!-- Content is rendered by Vditor.preview in the parent -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { Lock, Edit, Delete, ArrowDown, Back } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
file: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
isMobile: Boolean,
|
||||||
|
isUserLoggedIn: Boolean,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits([
|
||||||
|
'back',
|
||||||
|
'edit',
|
||||||
|
'delete',
|
||||||
|
'open-rename-dialog',
|
||||||
|
'show-move-note-dialog',
|
||||||
|
'show-privacy-dialog',
|
||||||
|
'export'
|
||||||
|
]);
|
||||||
|
|
||||||
|
const handleExport = (format) => {
|
||||||
|
emit('export', format);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.file-preview {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: var(--box-shadow-light);
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .preview-header {
|
||||||
|
background-color: rgba(30, 30, 47, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-title span {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-title .edit-icon {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.preview-title .edit-icon:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-preview {
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 2rem;
|
||||||
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
box-shadow: var(--box-shadow-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .markdown-preview {
|
||||||
|
background-color: rgba(30, 30, 47, 0.8);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
262
biji-qianduan/src/components/home/SidebarMenu.vue
Normal file
262
biji-qianduan/src/components/home/SidebarMenu.vue
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
<template>
|
||||||
|
<el-aside class="sidebar" :width="isCollapsed ? (isMobile ? '0' : '64px') : '250px'">
|
||||||
|
<div class="sidebar-header">
|
||||||
|
<span v-if="!isCollapsed" style="margin-right: 15px; font-weight: bold;">笔记分类</span>
|
||||||
|
<el-button v-if="!isCollapsed" type="primary" size="small" @click="$emit('show-create-group')" circle>
|
||||||
|
<el-icon><Plus /></el-icon>
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="$emit('toggle-collapse')" type="primary" size="small" circle v-if="!isMobile">
|
||||||
|
<el-icon>
|
||||||
|
<Fold v-if="!isCollapsed" />
|
||||||
|
<Expand v-else />
|
||||||
|
</el-icon>
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Desktop Menu -->
|
||||||
|
<el-menu
|
||||||
|
v-if="!isMobile"
|
||||||
|
:default-active="activeMenu"
|
||||||
|
class="el-menu-vertical-demo"
|
||||||
|
:collapse="isCollapsed"
|
||||||
|
popper-effect="light"
|
||||||
|
:collapse-transition="false"
|
||||||
|
>
|
||||||
|
<template v-for="menu in categoryTree" :key="menu.id">
|
||||||
|
<component :is="renderMenu(menu)" />
|
||||||
|
</template>
|
||||||
|
<ElMenuItem index="trash" @click="goToTrash">
|
||||||
|
<ElIcon><Delete /></ElIcon>
|
||||||
|
<template #title>回收站</template>
|
||||||
|
</ElMenuItem>
|
||||||
|
</el-menu>
|
||||||
|
|
||||||
|
<!-- Mobile Menu -->
|
||||||
|
<el-menu
|
||||||
|
v-if="isMobile"
|
||||||
|
:default-active="activeMenu"
|
||||||
|
class="el-menu-vertical-demo"
|
||||||
|
:collapse="isCollapsed"
|
||||||
|
:collapse-transition="false"
|
||||||
|
>
|
||||||
|
<div class="mobile-menu-header">
|
||||||
|
<div v-if="userStore.isLoggedIn" class="user-info">
|
||||||
|
<span class="username">欢迎, {{ userStore.userInfo?.username }}</span>
|
||||||
|
</div>
|
||||||
|
<div v-else class="guest-info">
|
||||||
|
<el-button type="primary" @click="goToLogin">登录</el-button>
|
||||||
|
<el-button @click="goToRegister">注册</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-for="menu in categoryTree" :key="menu.id">
|
||||||
|
<component :is="renderMenu(menu)" />
|
||||||
|
</template>
|
||||||
|
<ElMenuItem index="trash" @click="goToTrash">
|
||||||
|
<ElIcon><Delete /></ElIcon>
|
||||||
|
<template #title>回收站</template>
|
||||||
|
</ElMenuItem>
|
||||||
|
<ElMenuItem v-if="userStore.isLoggedIn" index="system-settings" @click="$emit('show-system-settings')">
|
||||||
|
<ElIcon><setting /></ElIcon>
|
||||||
|
<template #title>系统管理</template>
|
||||||
|
</ElMenuItem>
|
||||||
|
<ElMenuItem v-if="userStore.isLoggedIn" index="update-password" @click="$emit('show-update-password')">
|
||||||
|
<ElIcon><lock /></ElIcon>
|
||||||
|
<template #title>修改密码</template>
|
||||||
|
</ElMenuItem>
|
||||||
|
<ElMenuItem v-if="userStore.isLoggedIn" index="logout" @click="$emit('logout')">
|
||||||
|
<ElIcon><SwitchButton /></ElIcon>
|
||||||
|
<template #title>退出登录</template>
|
||||||
|
</ElMenuItem>
|
||||||
|
</el-menu>
|
||||||
|
</el-aside>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { h } from 'vue';
|
||||||
|
import { ElSubMenu, ElMenuItem, ElIcon, ElMessageBox, ElTooltip } from 'element-plus';
|
||||||
|
import { Folder, Delete, Edit, Plus, Fold, Expand, Setting, Lock, SwitchButton } from '@element-plus/icons-vue';
|
||||||
|
import { useUserStore } from '@/stores/user';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
import { deleteGrouping } from '@/api/CommonApi.js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
isCollapsed: Boolean,
|
||||||
|
isMobile: Boolean,
|
||||||
|
activeMenu: String,
|
||||||
|
categoryTree: Array,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits([
|
||||||
|
'select-file',
|
||||||
|
'show-rename-dialog',
|
||||||
|
'show-create-group',
|
||||||
|
'toggle-collapse',
|
||||||
|
'group-deleted',
|
||||||
|
'show-system-settings',
|
||||||
|
'show-update-password',
|
||||||
|
'logout'
|
||||||
|
]);
|
||||||
|
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const goToTrash = () => router.push({ name: 'Trash' });
|
||||||
|
const goToLogin = () => router.push('/login');
|
||||||
|
const goToRegister = () => router.push('/register');
|
||||||
|
|
||||||
|
const handleDeleteGroup = (group) => {
|
||||||
|
ElMessageBox.confirm(
|
||||||
|
`确定要删除分类 “${group.grouping}” 吗?这将同时删除该分类下的所有子分类和笔记。`,
|
||||||
|
'警告',
|
||||||
|
{ confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }
|
||||||
|
).then(async () => {
|
||||||
|
try {
|
||||||
|
await deleteGrouping(group.id);
|
||||||
|
ElMessage.success('分类已删除');
|
||||||
|
emit('group-deleted');
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('删除分类失败: ' + error.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderMenu = (item) => {
|
||||||
|
const titleContent = () => h('div', { class: 'menu-item-title' }, [
|
||||||
|
h(ElIcon, () => h(Folder)),
|
||||||
|
h('span', { class: 'menu-item-text' }, item.grouping),
|
||||||
|
h('div', { class: 'menu-item-actions' }, [
|
||||||
|
h(ElIcon, { class: 'edit-icon', onClick: (e) => { e.stopPropagation(); emit('show-rename-dialog', item, 'group'); } }, () => h(Edit)),
|
||||||
|
h(ElIcon, { class: 'delete-icon', onClick: (e) => { e.stopPropagation(); handleDeleteGroup(item); } }, () => h(Delete))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
|
||||||
|
const wrappedContent = () => h(ElTooltip, {
|
||||||
|
content: item.grouping,
|
||||||
|
placement: 'right',
|
||||||
|
disabled: !props.isCollapsed,
|
||||||
|
effect: 'dark',
|
||||||
|
offset: 15,
|
||||||
|
}, { default: titleContent });
|
||||||
|
|
||||||
|
if (item.children && item.children.length > 0) {
|
||||||
|
return h(ElSubMenu, {
|
||||||
|
index: `group-${item.id}`,
|
||||||
|
popperClass: props.isCollapsed ? 'hide-popper' : ''
|
||||||
|
}, {
|
||||||
|
title: () => h('div', {
|
||||||
|
class: 'submenu-title-wrapper',
|
||||||
|
onClick: () => emit('select-file', item),
|
||||||
|
style: 'width: 100%; display: flex; align-items: center;'
|
||||||
|
}, [ wrappedContent() ]),
|
||||||
|
default: () => item.children.map(child => renderMenu(child))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return h(ElMenuItem, { index: `group-${item.id}`, onClick: () => emit('select-file', item) }, {
|
||||||
|
default: wrappedContent
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.sidebar {
|
||||||
|
background-color: rgba(255, 255, 255, 0.8);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-right: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
transition: width var(--transition-duration) ease;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-theme .sidebar {
|
||||||
|
background-color: rgba(23, 23, 39, 0.8);
|
||||||
|
border-right: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu {
|
||||||
|
border-right: none;
|
||||||
|
background: transparent;
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-menu-item), :deep(.el-sub-menu__title) {
|
||||||
|
height: 48px;
|
||||||
|
line-height: 48px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
margin: 0.25rem 0.5rem;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-menu-item.is-active) {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-menu-item:hover), :deep(.el-sub-menu__title:hover) {
|
||||||
|
background-color: var(--primary-color-light);
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item-text {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item-actions {
|
||||||
|
display: none;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu:not(.el-menu--collapse) .el-menu-item:hover .menu-item-actions,
|
||||||
|
.el-menu:not(.el-menu--collapse) .el-sub-menu__title:hover .menu-item-actions {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-menu--collapse .menu-item-text,
|
||||||
|
.el-menu--collapse .menu-item-actions {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-icon, .delete-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-icon:hover, .delete-icon:hover {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-header {
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
border-bottom: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-header .username {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:model-value="visible"
|
||||||
|
title="新建分类"
|
||||||
|
width="400px"
|
||||||
|
@close="handleClose"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-form :model="form" :rules="rules" ref="formRef" label-width="80px">
|
||||||
|
<el-form-item label="父级分类">
|
||||||
|
<el-cascader
|
||||||
|
v-model="form.parentId"
|
||||||
|
:options="categoryOptions"
|
||||||
|
:props="{ checkStrictly: true, emitPath: false, value: 'id', label: 'grouping' }"
|
||||||
|
clearable
|
||||||
|
placeholder="不选则为一级分类"
|
||||||
|
style="width: 100%;"
|
||||||
|
></el-cascader>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="分类名称" prop="name">
|
||||||
|
<el-input v-model="form.name" autocomplete="off"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="handleClose">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { addGroupings } from '@/api/CommonApi.js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
categoryOptions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible', 'group-created']);
|
||||||
|
|
||||||
|
const formRef = ref(null);
|
||||||
|
const form = ref({
|
||||||
|
name: '',
|
||||||
|
parentId: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rules = ref({
|
||||||
|
name: [{ required: true, message: '请输入分类名称', trigger: 'blur' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
// 当对话框关闭时,重置表单
|
||||||
|
watch(() => props.visible, (newVal) => {
|
||||||
|
if (!newVal && formRef.value) {
|
||||||
|
formRef.value.resetFields();
|
||||||
|
form.value = { name: '', parentId: null };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
emit('update:visible', false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!formRef.value) return;
|
||||||
|
await formRef.value.validate(async (valid) => {
|
||||||
|
if (valid) {
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
grouping: form.value.name,
|
||||||
|
parentId: form.value.parentId || 0,
|
||||||
|
};
|
||||||
|
await addGroupings(payload);
|
||||||
|
ElMessage.success('分类创建成功');
|
||||||
|
emit('group-created'); // 通知父组件刷新
|
||||||
|
handleClose();
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('创建分类失败: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
109
biji-qianduan/src/components/home/dialogs/CreateNoteDialog.vue
Normal file
109
biji-qianduan/src/components/home/dialogs/CreateNoteDialog.vue
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:model-value="visible"
|
||||||
|
title="新建笔记"
|
||||||
|
width="400px"
|
||||||
|
@close="handleClose"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-form :model="form" :rules="rules" ref="formRef" label-width="80px">
|
||||||
|
<el-form-item label="笔记标题" prop="title">
|
||||||
|
<el-input v-model="form.title" autocomplete="off"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="选择分类" prop="groupingId">
|
||||||
|
<el-cascader
|
||||||
|
v-model="form.groupingId"
|
||||||
|
:options="categoryOptions"
|
||||||
|
:props="{ checkStrictly: true, emitPath: false, value: 'id', label: 'grouping' }"
|
||||||
|
clearable
|
||||||
|
placeholder="请选择笔记所属分类"
|
||||||
|
style="width: 100%;"
|
||||||
|
></el-cascader>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="私密笔记">
|
||||||
|
<el-switch
|
||||||
|
v-model="form.isPrivate"
|
||||||
|
:active-value="1"
|
||||||
|
:inactive-value="0"
|
||||||
|
active-text="私密"
|
||||||
|
inactive-text="公开"
|
||||||
|
/>
|
||||||
|
<div class="form-item-help">私密笔记只有登录用户才能查看内容</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="handleClose">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
categoryOptions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible', 'create-note']);
|
||||||
|
|
||||||
|
const formRef = ref(null);
|
||||||
|
const form = ref({
|
||||||
|
title: '',
|
||||||
|
groupingId: null,
|
||||||
|
isPrivate: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rules = ref({
|
||||||
|
title: [{ required: true, message: '请输入笔记标题', trigger: 'blur' }],
|
||||||
|
groupingId: [{ required: true, message: '请选择分类', trigger: 'change' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.visible, (newVal) => {
|
||||||
|
if (!newVal && formRef.value) {
|
||||||
|
formRef.value.resetFields();
|
||||||
|
form.value = { title: '', groupingId: null, isPrivate: 0 };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
emit('update:visible', false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!formRef.value) return;
|
||||||
|
await formRef.value.validate((valid) => {
|
||||||
|
if (valid) {
|
||||||
|
const payload = {
|
||||||
|
id: null,
|
||||||
|
title: form.value.title,
|
||||||
|
groupingId: form.value.groupingId,
|
||||||
|
fileName: form.value.title + '.md',
|
||||||
|
content: '',
|
||||||
|
isPrivate: form.value.isPrivate,
|
||||||
|
};
|
||||||
|
emit('create-note', payload);
|
||||||
|
handleClose();
|
||||||
|
} else {
|
||||||
|
ElMessage.error('请填写必要的字段');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.form-item-help {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
margin-top: 4px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
81
biji-qianduan/src/components/home/dialogs/MoveNoteDialog.vue
Normal file
81
biji-qianduan/src/components/home/dialogs/MoveNoteDialog.vue
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:model-value="visible"
|
||||||
|
title="移动笔记到"
|
||||||
|
width="400px"
|
||||||
|
@close="handleClose"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-cascader
|
||||||
|
v-model="moveToGroupId"
|
||||||
|
:options="categoryOptions"
|
||||||
|
:props="{ checkStrictly: true, emitPath: false, value: 'id', label: 'grouping' }"
|
||||||
|
clearable
|
||||||
|
placeholder="请选择目标分类"
|
||||||
|
style="width: 100%;"
|
||||||
|
></el-cascader>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="handleClose">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { updateMarkdown } from '@/api/CommonApi.js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
categoryOptions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
noteToMove: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible', 'move-success']);
|
||||||
|
|
||||||
|
const moveToGroupId = ref(null);
|
||||||
|
|
||||||
|
watch(() => props.visible, (newVal) => {
|
||||||
|
if (!newVal) {
|
||||||
|
moveToGroupId.value = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
emit('update:visible', false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!moveToGroupId.value) {
|
||||||
|
ElMessage.error('请选择目标分类');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!props.noteToMove) {
|
||||||
|
ElMessage.error('没有需要移动的笔记');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const payload = {
|
||||||
|
...props.noteToMove,
|
||||||
|
groupingId: moveToGroupId.value,
|
||||||
|
};
|
||||||
|
await updateMarkdown(payload);
|
||||||
|
ElMessage.success('笔记移动成功');
|
||||||
|
emit('move-success');
|
||||||
|
handleClose();
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('移动失败: ' + error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
91
biji-qianduan/src/components/home/dialogs/PrivacyDialog.vue
Normal file
91
biji-qianduan/src/components/home/dialogs/PrivacyDialog.vue
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:model-value="visible"
|
||||||
|
:title="title"
|
||||||
|
width="400px"
|
||||||
|
@close="handleClose"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<div v-if="note">
|
||||||
|
<p>您确定要将笔记 <strong>"{{ note.title }}"</strong> {{ note.isPrivate === 1 ? '设为公开' : '设为私密' }}吗?</p>
|
||||||
|
<div class="privacy-explanation">
|
||||||
|
<el-icon><InfoFilled /></el-icon>
|
||||||
|
<span>{{ explanation }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="handleClose">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { updateMarkdown } from '@/api/CommonApi.js';
|
||||||
|
import { InfoFilled } from '@element-plus/icons-vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
type: Object,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible', 'privacy-changed']);
|
||||||
|
|
||||||
|
const title = computed(() => {
|
||||||
|
if (!props.note) return '';
|
||||||
|
return props.note.isPrivate === 1 ? '设为公开笔记' : '设为私密笔记';
|
||||||
|
});
|
||||||
|
|
||||||
|
const explanation = computed(() => {
|
||||||
|
if (!props.note) return '';
|
||||||
|
return props.note.isPrivate === 1 ? '公开笔记:所有用户都可以查看内容' : '私密笔记:只有登录用户才能查看内容';
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
emit('update:visible', false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!props.note) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newPrivacyStatus = props.note.isPrivate === 1 ? 0 : 1;
|
||||||
|
const payload = {
|
||||||
|
...props.note,
|
||||||
|
isPrivate: newPrivacyStatus,
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatedFile = await updateMarkdown(payload);
|
||||||
|
ElMessage.success(`笔记已${newPrivacyStatus === 1 ? '设为私密' : '设为公开'}`);
|
||||||
|
emit('privacy-changed', updatedFile);
|
||||||
|
handleClose();
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('修改笔记状态失败: ' + error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.privacy-explanation {
|
||||||
|
margin-top: 1rem;
|
||||||
|
padding: 0.75rem;
|
||||||
|
background-color: #f4f4f5;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
.dark-theme .privacy-explanation {
|
||||||
|
background-color: #2c2c3e;
|
||||||
|
color: #a9a9a9;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
65
biji-qianduan/src/components/home/dialogs/RenameDialog.vue
Normal file
65
biji-qianduan/src/components/home/dialogs/RenameDialog.vue
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:model-value="visible"
|
||||||
|
title="重命名"
|
||||||
|
width="400px"
|
||||||
|
@close="handleClose"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-input v-model="newName" placeholder="请输入新名称" @keyup.enter="handleSubmit"></el-input>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="handleClose">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { updateMarkdownTitle, updateGroupingName } from '@/api/CommonApi.js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
default: () => null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible', 'renamed']);
|
||||||
|
|
||||||
|
const newName = ref('');
|
||||||
|
|
||||||
|
watch(() => props.item, (newItem) => {
|
||||||
|
if (newItem) {
|
||||||
|
newName.value = newItem.type === 'file' ? newItem.title : newItem.grouping;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
emit('update:visible', false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!newName.value.trim()) {
|
||||||
|
ElMessage.error('名称不能为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (props.item.type === 'file') {
|
||||||
|
await updateMarkdownTitle({ id: props.item.id, title: newName.value });
|
||||||
|
} else {
|
||||||
|
await updateGroupingName({ id: props.item.id, grouping: newName.value });
|
||||||
|
}
|
||||||
|
ElMessage.success('重命名成功');
|
||||||
|
emit('renamed');
|
||||||
|
handleClose();
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('重命名失败: ' + error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:model-value="visible"
|
||||||
|
title="选择导入的分类"
|
||||||
|
width="400px"
|
||||||
|
@close="handleClose"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-cascader
|
||||||
|
v-model="importGroupId"
|
||||||
|
:options="categoryOptions"
|
||||||
|
:props="{ checkStrictly: true, emitPath: false, value: 'id', label: 'grouping' }"
|
||||||
|
clearable
|
||||||
|
placeholder="请选择要导入的分类"
|
||||||
|
style="width: 100%;"
|
||||||
|
></el-cascader>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="handleClose">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { updateMarkdown } from '@/api/CommonApi.js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
categoryOptions: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
fileToImport: {
|
||||||
|
type: File,
|
||||||
|
default: null,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible', 'import-success']);
|
||||||
|
|
||||||
|
const importGroupId = ref(null);
|
||||||
|
|
||||||
|
watch(() => props.visible, (newVal) => {
|
||||||
|
if (!newVal) {
|
||||||
|
importGroupId.value = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
emit('update:visible', false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
if (!importGroupId.value) {
|
||||||
|
ElMessage.error('请选择要导入的分类');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!props.fileToImport) {
|
||||||
|
ElMessage.error('没有需要导入的文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = async (e) => {
|
||||||
|
const content = e.target.result;
|
||||||
|
const payload = {
|
||||||
|
title: props.fileToImport.name.replace(/\.md$/, ''),
|
||||||
|
groupingId: importGroupId.value,
|
||||||
|
content: content,
|
||||||
|
fileName: props.fileToImport.name,
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await updateMarkdown(payload);
|
||||||
|
ElMessage.success('Markdown 文件导入成功');
|
||||||
|
emit('import-success');
|
||||||
|
handleClose();
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('导入失败: ' + error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(props.fileToImport);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:model-value="visible"
|
||||||
|
title="系统管理"
|
||||||
|
width="500px"
|
||||||
|
@close="handleClose"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-form label-width="120px">
|
||||||
|
<el-form-item label="开放注册">
|
||||||
|
<el-switch v-model="isRegistrationEnabled" @change="handleToggleRegistration"></el-switch>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="生成注册码">
|
||||||
|
<el-button type="primary" @click="handleGenerateCode">生成</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="generatedCode" label="新注册码">
|
||||||
|
<el-input v-model="generatedCode" readonly>
|
||||||
|
<template #append>
|
||||||
|
<el-button @click="copyToClipboard(generatedCode)">复制</el-button>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="handleClose">关闭</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch, onMounted } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import {
|
||||||
|
getRegistrationStatus,
|
||||||
|
toggleRegistration,
|
||||||
|
generateRegistrationCode,
|
||||||
|
} from '@/api/CommonApi.js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible']);
|
||||||
|
|
||||||
|
const isRegistrationEnabled = ref(true);
|
||||||
|
const generatedCode = ref('');
|
||||||
|
|
||||||
|
watch(() => props.visible, (newVal) => {
|
||||||
|
if (newVal) {
|
||||||
|
fetchRegistrationStatus();
|
||||||
|
generatedCode.value = ''; // Reset code when dialog opens
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const fetchRegistrationStatus = async () => {
|
||||||
|
try {
|
||||||
|
isRegistrationEnabled.value = await getRegistrationStatus();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to fetch registration status:", error);
|
||||||
|
ElMessage.error('获取注册状态失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleToggleRegistration = async (value) => {
|
||||||
|
try {
|
||||||
|
await toggleRegistration(value);
|
||||||
|
ElMessage.success(`注册功能已${value ? '开启' : '关闭'}`);
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('操作失败');
|
||||||
|
isRegistrationEnabled.value = !value; // Revert on failure
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGenerateCode = async () => {
|
||||||
|
try {
|
||||||
|
const code = await generateRegistrationCode();
|
||||||
|
generatedCode.value = code;
|
||||||
|
ElMessage.success('注册码生成成功');
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('生成注册码失败: ' + error.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyToClipboard = (text) => {
|
||||||
|
navigator.clipboard.writeText(text).then(() => {
|
||||||
|
ElMessage.success('已复制到剪贴板');
|
||||||
|
}, () => {
|
||||||
|
ElMessage.error('复制失败');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
emit('update:visible', false);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
<template>
|
||||||
|
<el-dialog
|
||||||
|
:model-value="visible"
|
||||||
|
title="修改密码"
|
||||||
|
width="400px"
|
||||||
|
@close="handleClose"
|
||||||
|
:close-on-click-modal="false"
|
||||||
|
>
|
||||||
|
<el-form :model="form" :rules="rules" ref="formRef" label-width="100px">
|
||||||
|
<el-form-item label="旧密码" prop="oldPassword">
|
||||||
|
<el-input v-model="form.oldPassword" type="password" show-password autocomplete="off"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="新密码" prop="newPassword">
|
||||||
|
<el-input v-model="form.newPassword" type="password" show-password autocomplete="off"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="确认新密码" prop="confirmPassword">
|
||||||
|
<el-input v-model="form.confirmPassword" type="password" show-password autocomplete="off"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="handleClose">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { updatePassword } from '@/api/CommonApi.js';
|
||||||
|
import { useUserStore } from '@/stores/user';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
visible: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:visible', 'password-updated']);
|
||||||
|
const userStore = useUserStore();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const formRef = ref(null);
|
||||||
|
const form = ref({
|
||||||
|
oldPassword: '',
|
||||||
|
newPassword: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const validateConfirmPassword = (rule, value, callback) => {
|
||||||
|
if (value === '') {
|
||||||
|
callback(new Error('请再次输入新密码'));
|
||||||
|
} else if (value !== form.value.newPassword) {
|
||||||
|
callback(new Error("两次输入的新密码不一致"));
|
||||||
|
} else {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const rules = ref({
|
||||||
|
oldPassword: [{ required: true, message: '请输入旧密码', trigger: 'blur' }],
|
||||||
|
newPassword: [{ required: true, message: '请输入新密码', trigger: 'blur' }, { min: 6, message: '密码长度不能少于6位', trigger: 'blur' }],
|
||||||
|
confirmPassword: [{ required: true, validator: validateConfirmPassword, trigger: 'blur' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.visible, (newVal) => {
|
||||||
|
if (!newVal && formRef.value) {
|
||||||
|
formRef.value.resetFields();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
emit('update:visible', false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
if (!formRef.value) return;
|
||||||
|
await formRef.value.validate(async (valid) => {
|
||||||
|
if (valid) {
|
||||||
|
try {
|
||||||
|
await updatePassword({
|
||||||
|
oldPassword: form.value.oldPassword,
|
||||||
|
newPassword: form.value.newPassword,
|
||||||
|
});
|
||||||
|
ElMessage.success('密码修改成功,请重新登录');
|
||||||
|
emit('password-updated');
|
||||||
|
handleClose();
|
||||||
|
// Logout logic will be handled by the parent component
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('密码修改失败: ' + error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
Reference in New Issue
Block a user