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