feat(components): 新增创建分类和笔记对话框及头部组件

- 新增 CreateGroupDialog 组件用于创建分类
- 新增 CreateNoteDialog 组件用于创建笔记
- 新增 HomeHeader 组件用于显示主页头部信息
- 对话框组件使用 Element Plus 样式- 头部组件包含用户操作按钮和搜索功能
This commit is contained in:
ikmkj
2025-08-08 20:19:52 +08:00
parent f00b60ddb7
commit c28b12ecd1
14 changed files with 1852 additions and 1425 deletions

View File

@@ -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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>