feat: 优化分类选择器与笔记移动功能
This commit is contained in:
@@ -23,6 +23,7 @@ public class MarkdownFile implements Serializable {
|
|||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
@Schema(description = "分组表id",implementation = Long.class)
|
@Schema(description = "分组表id",implementation = Long.class)
|
||||||
|
@JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||||
@TableField("grouping_id")
|
@TableField("grouping_id")
|
||||||
private Long groupingId;
|
private Long groupingId;
|
||||||
|
|
||||||
|
|||||||
@@ -9,4 +9,7 @@ import lombok.EqualsAndHashCode;
|
|||||||
public class MarkdownFileVO extends MarkdownFile {
|
public class MarkdownFileVO extends MarkdownFile {
|
||||||
@TableField(exist = false)
|
@TableField(exist = false)
|
||||||
private String groupingName;
|
private String groupingName;
|
||||||
|
|
||||||
|
@TableField(exist = false)
|
||||||
|
private Long groupingId;
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,7 @@ import org.apache.ibatis.annotations.Update;
|
|||||||
@Mapper
|
@Mapper
|
||||||
public interface MarkdownFileMapper extends BaseMapper<MarkdownFile> {
|
public interface MarkdownFileMapper extends BaseMapper<MarkdownFile> {
|
||||||
|
|
||||||
@Select("SELECT mf.id, mf.grouping_id, mf.`title`, mf.file_name, mf.`content`, mf.created_at, mf.updated_at, mf.is_deleted, mf.deleted_at, mf.deleted_by, mf.is_private, g.`grouping` as groupingName " +
|
@Select("SELECT mf.id, mf.grouping_id as groupingId, mf.`title`, mf.file_name, mf.`content`, mf.created_at, mf.updated_at, mf.is_deleted, mf.deleted_at, mf.deleted_by, mf.is_private, g.`grouping` as groupingName " +
|
||||||
"FROM `markdown_file` mf " +
|
"FROM `markdown_file` mf " +
|
||||||
"LEFT JOIN `grouping` g ON mf.grouping_id = g.id " +
|
"LEFT JOIN `grouping` g ON mf.grouping_id = g.id " +
|
||||||
"WHERE mf.is_deleted = 0 " +
|
"WHERE mf.is_deleted = 0 " +
|
||||||
@@ -23,7 +23,7 @@ public interface MarkdownFileMapper extends BaseMapper<MarkdownFile> {
|
|||||||
"LIMIT #{limit}")
|
"LIMIT #{limit}")
|
||||||
List<MarkdownFileVO> selectRecentWithGrouping(@Param("limit") int limit);
|
List<MarkdownFileVO> selectRecentWithGrouping(@Param("limit") int limit);
|
||||||
|
|
||||||
@Select("SELECT mf.id, mf.grouping_id, mf.`title`, mf.file_name, mf.`content`, mf.created_at, mf.updated_at, mf.is_deleted, mf.deleted_at, mf.deleted_by, mf.is_private, g.`grouping` as groupingName " +
|
@Select("SELECT mf.id, mf.grouping_id as groupingId, mf.`title`, mf.file_name, mf.`content`, mf.created_at, mf.updated_at, mf.is_deleted, mf.deleted_at, mf.deleted_by, mf.is_private, g.`grouping` as groupingName " +
|
||||||
"FROM `markdown_file` mf " +
|
"FROM `markdown_file` mf " +
|
||||||
"LEFT JOIN `grouping` g ON mf.grouping_id = g.id " +
|
"LEFT JOIN `grouping` g ON mf.grouping_id = g.id " +
|
||||||
"WHERE mf.grouping_id = #{groupingId} AND mf.is_deleted = 0 " +
|
"WHERE mf.grouping_id = #{groupingId} AND mf.is_deleted = 0 " +
|
||||||
@@ -51,7 +51,7 @@ public interface MarkdownFileMapper extends BaseMapper<MarkdownFile> {
|
|||||||
@Select("SELECT id, grouping_id, `title`, file_name, `content`, created_at, updated_at, is_deleted, deleted_at, deleted_by, is_private FROM `markdown_file` WHERE is_deleted = 0")
|
@Select("SELECT id, grouping_id, `title`, file_name, `content`, created_at, updated_at, is_deleted, deleted_at, deleted_by, is_private FROM `markdown_file` WHERE is_deleted = 0")
|
||||||
List<Integer> findAllIds();
|
List<Integer> findAllIds();
|
||||||
|
|
||||||
@Select("SELECT mf.id, mf.grouping_id, mf.`title`, mf.file_name, mf.`content`, mf.created_at, mf.updated_at, mf.is_deleted, mf.deleted_at, mf.deleted_by, mf.is_private, g.`grouping` as groupingName " +
|
@Select("SELECT mf.id, mf.grouping_id as groupingId, mf.`title`, mf.file_name, mf.`content`, mf.created_at, mf.updated_at, mf.is_deleted, mf.deleted_at, mf.deleted_by, mf.is_private, g.`grouping` as groupingName " +
|
||||||
"FROM `markdown_file` mf " +
|
"FROM `markdown_file` mf " +
|
||||||
"LEFT JOIN `grouping` g ON mf.grouping_id = g.id " +
|
"LEFT JOIN `grouping` g ON mf.grouping_id = g.id " +
|
||||||
"WHERE mf.id = #{id} AND mf.is_deleted = 0")
|
"WHERE mf.id = #{id} AND mf.is_deleted = 0")
|
||||||
|
|||||||
@@ -46,8 +46,33 @@ public class MarkdownFileServiceImpl
|
|||||||
markdownFile.setCreatedAt(new Date());
|
markdownFile.setCreatedAt(new Date());
|
||||||
this.save(markdownFile);
|
this.save(markdownFile);
|
||||||
id=l;
|
id=l;
|
||||||
|
} else {
|
||||||
|
MarkdownFile existingFile = this.getById(markdownFile.getId());
|
||||||
|
if (existingFile != null) {
|
||||||
|
LambdaUpdateWrapper<MarkdownFile> updateWrapper = new LambdaUpdateWrapper<>();
|
||||||
|
updateWrapper.eq(MarkdownFile::getId, markdownFile.getId())
|
||||||
|
.set(MarkdownFile::getUpdatedAt, new Date());
|
||||||
|
|
||||||
|
if (markdownFile.getTitle() != null) {
|
||||||
|
updateWrapper.set(MarkdownFile::getTitle, markdownFile.getTitle());
|
||||||
|
}
|
||||||
|
if (markdownFile.getContent() != null) {
|
||||||
|
updateWrapper.set(MarkdownFile::getContent, markdownFile.getContent());
|
||||||
|
}
|
||||||
|
if (markdownFile.getGroupingId() != null) {
|
||||||
|
updateWrapper.set(MarkdownFile::getGroupingId, markdownFile.getGroupingId());
|
||||||
|
}
|
||||||
|
if (markdownFile.getFileName() != null) {
|
||||||
|
updateWrapper.set(MarkdownFile::getFileName, markdownFile.getFileName());
|
||||||
|
}
|
||||||
|
if (markdownFile.getIsPrivate() != null) {
|
||||||
|
updateWrapper.set(MarkdownFile::getIsPrivate, markdownFile.getIsPrivate());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.update(updateWrapper);
|
||||||
} else {
|
} else {
|
||||||
this.updateById(markdownFile);
|
this.updateById(markdownFile);
|
||||||
|
}
|
||||||
id=markdownFile.getId();
|
id=markdownFile.getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +81,10 @@ public class MarkdownFileServiceImpl
|
|||||||
|
|
||||||
MarkdownFileVO result = markdownFileMapper.selectByIdWithGrouping(id);
|
MarkdownFileVO result = markdownFileMapper.selectByIdWithGrouping(id);
|
||||||
if (result != null) {
|
if (result != null) {
|
||||||
|
// 确保 groupingId 被正确设置
|
||||||
|
if (result.getGroupingId() == null && markdownFile.getGroupingId() != null) {
|
||||||
|
result.setGroupingId(markdownFile.getGroupingId());
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
return markdownFile;
|
return markdownFile;
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ const saveStatus = ref('');
|
|||||||
let saveTimeout = null;
|
let saveTimeout = null;
|
||||||
let lastSavedContent = ref('');
|
let lastSavedContent = ref('');
|
||||||
let isSaving = ref(false);
|
let isSaving = ref(false);
|
||||||
|
// 维护当前最新的笔记数据
|
||||||
|
const currentData = ref({ ...props.editData });
|
||||||
|
|
||||||
const initVditor = () => {
|
const initVditor = () => {
|
||||||
if (vditor.value) {
|
if (vditor.value) {
|
||||||
@@ -49,12 +51,12 @@ const initVditor = () => {
|
|||||||
},
|
},
|
||||||
after: () => {
|
after: () => {
|
||||||
isInitialized.value = true;
|
isInitialized.value = true;
|
||||||
if (props.editData && props.editData.content) {
|
if (currentData.value && currentData.value.content) {
|
||||||
vditor.value.setValue(props.editData.content);
|
vditor.value.setValue(currentData.value.content);
|
||||||
lastSavedContent.value = props.editData.content;
|
lastSavedContent.value = currentData.value.content;
|
||||||
}
|
}
|
||||||
if (props.editData && props.editData.id) {
|
if (currentData.value && currentData.value.id) {
|
||||||
currentId.value = props.editData.id;
|
currentId.value = currentData.value.id;
|
||||||
}
|
}
|
||||||
vditor.value.focus();
|
vditor.value.focus();
|
||||||
},
|
},
|
||||||
@@ -101,31 +103,42 @@ const save = async (value) => {
|
|||||||
try {
|
try {
|
||||||
saveStatus.value = '正在保存...';
|
saveStatus.value = '正在保存...';
|
||||||
|
|
||||||
|
// 确保groupingId不会丢失:优先使用currentData中的值
|
||||||
|
const groupingId = currentData.value.groupingId || props.editData.groupingId;
|
||||||
|
|
||||||
|
// 将ID转为字符串以避免JavaScript精度丢失
|
||||||
|
const idString = currentId.value ? String(currentId.value) : (currentData.value.id ? String(currentData.value.id) : null);
|
||||||
|
const groupingIdString = groupingId ? String(groupingId) : null;
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
id: currentId.value || props.editData.id || null,
|
id: idString,
|
||||||
content: content,
|
content: content,
|
||||||
title: props.editData.title,
|
title: currentData.value.title || props.editData.title,
|
||||||
groupingId: props.editData.groupingId,
|
groupingId: groupingIdString,
|
||||||
fileName: props.editData.fileName,
|
fileName: currentData.value.fileName || props.editData.fileName,
|
||||||
isPrivate: props.editData.isPrivate
|
isPrivate: currentData.value.isPrivate !== undefined ? currentData.value.isPrivate : props.editData.isPrivate
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const response = await updateMarkdown(payload);
|
const response = await updateMarkdown(payload);
|
||||||
|
|
||||||
if (response && response.id) {
|
if (response && response.id) {
|
||||||
currentId.value = response.id;
|
currentId.value = response.id;
|
||||||
lastSavedContent.value = content;
|
lastSavedContent.value = content;
|
||||||
|
|
||||||
|
// 使用后端返回的数据,但确保groupingId不会丢失
|
||||||
|
// 注意:后端返回的ID是字符串,保持字符串格式避免精度丢失
|
||||||
const updatedFile = {
|
const updatedFile = {
|
||||||
...props.editData,
|
...response,
|
||||||
id: response.id,
|
|
||||||
content: content,
|
content: content,
|
||||||
groupingId: response.groupingId,
|
// 如果后端返回的groupingId为空,使用原来的值(保持字符串格式)
|
||||||
groupingName: response.groupingName,
|
groupingId: response.groupingId || groupingIdString,
|
||||||
title: response.title,
|
groupingName: response.groupingName || currentData.value.groupingName
|
||||||
isPrivate: response.isPrivate
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 更新currentData为最新数据
|
||||||
|
currentData.value = updatedFile;
|
||||||
emit('update:editData', updatedFile);
|
emit('update:editData', updatedFile);
|
||||||
saveStatus.value = '已保存';
|
saveStatus.value = '已保存';
|
||||||
}
|
}
|
||||||
@@ -143,12 +156,17 @@ const handleBack = async () => {
|
|||||||
await save(content);
|
await save(content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 确保groupingId不会丢失(保持字符串格式)
|
||||||
|
const groupingId = currentData.value.groupingId || props.editData.groupingId;
|
||||||
|
const groupingName = currentData.value.groupingName || props.editData.groupingName;
|
||||||
|
|
||||||
const returnData = {
|
const returnData = {
|
||||||
|
...currentData.value,
|
||||||
...props.editData,
|
...props.editData,
|
||||||
id: currentId.value || props.editData.id,
|
id: currentId.value ? String(currentId.value) : (currentData.value.id ? String(currentData.value.id) : null),
|
||||||
content: content,
|
content: content,
|
||||||
groupingId: props.editData.groupingId,
|
groupingId: groupingId ? String(groupingId) : null,
|
||||||
groupingName: props.editData.groupingName
|
groupingName: groupingName
|
||||||
};
|
};
|
||||||
|
|
||||||
emit('back', returnData);
|
emit('back', returnData);
|
||||||
@@ -170,6 +188,8 @@ onBeforeUnmount(() => {
|
|||||||
|
|
||||||
watch(() => props.editData, (newVal, oldVal) => {
|
watch(() => props.editData, (newVal, oldVal) => {
|
||||||
if (vditor.value && isInitialized.value && newVal && newVal.id !== oldVal?.id) {
|
if (vditor.value && isInitialized.value && newVal && newVal.id !== oldVal?.id) {
|
||||||
|
// 更新currentData为最新的props数据
|
||||||
|
currentData.value = { ...newVal };
|
||||||
vditor.value.setValue(newVal.content || '');
|
vditor.value.setValue(newVal.content || '');
|
||||||
lastSavedContent.value = newVal.content || '';
|
lastSavedContent.value = newVal.content || '';
|
||||||
currentId.value = newVal.id;
|
currentId.value = newVal.id;
|
||||||
|
|||||||
@@ -2,23 +2,44 @@
|
|||||||
<el-dialog
|
<el-dialog
|
||||||
:model-value="visible"
|
:model-value="visible"
|
||||||
title="新建分类"
|
title="新建分类"
|
||||||
width="400px"
|
width="450px"
|
||||||
@close="handleClose"
|
@close="handleClose"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
>
|
>
|
||||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="80px">
|
<el-form :model="form" :rules="rules" ref="formRef" label-width="90px">
|
||||||
<el-form-item label="父级分类">
|
<el-form-item label="父级分类">
|
||||||
<el-cascader
|
<el-cascader
|
||||||
v-model="form.parentId"
|
v-model="form.parentId"
|
||||||
:options="categoryOptions"
|
:options="categoryOptions"
|
||||||
:props="{ checkStrictly: true, emitPath: false, value: 'id', label: 'grouping' }"
|
:props="cascaderProps"
|
||||||
clearable
|
clearable
|
||||||
placeholder="不选则为一级分类"
|
filterable
|
||||||
|
placeholder="不选则创建为一级分类"
|
||||||
style="width: 100%;"
|
style="width: 100%;"
|
||||||
></el-cascader>
|
>
|
||||||
|
<template #default="{ node, data }">
|
||||||
|
<span class="category-option">
|
||||||
|
<el-icon class="category-icon">
|
||||||
|
<Folder v-if="data.children && data.children.length > 0" />
|
||||||
|
<Folder-Opened v-else-if="node.isLeaf" />
|
||||||
|
<Folder v-else />
|
||||||
|
</el-icon>
|
||||||
|
<span class="category-name">{{ data.grouping }}</span>
|
||||||
|
<span v-if="data.children && data.children.length > 0" class="category-count">
|
||||||
|
({{ data.children.length }})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-cascader>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="分类名称" prop="name">
|
<el-form-item label="分类名称" prop="name">
|
||||||
<el-input v-model="form.name" autocomplete="off"></el-input>
|
<el-input
|
||||||
|
v-model="form.name"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="请输入新分类名称"
|
||||||
|
maxlength="50"
|
||||||
|
show-word-limit
|
||||||
|
></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -31,6 +52,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { Folder, FolderOpened } from '@element-plus/icons-vue';
|
||||||
import { addGroupings } from '@/api/CommonApi.js';
|
import { addGroupings } from '@/api/CommonApi.js';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -56,6 +78,15 @@ const rules = ref({
|
|||||||
name: [{ required: true, message: '请输入分类名称', trigger: 'blur' }],
|
name: [{ required: true, message: '请输入分类名称', trigger: 'blur' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 级联选择器配置
|
||||||
|
const cascaderProps = {
|
||||||
|
checkStrictly: true,
|
||||||
|
emitPath: false,
|
||||||
|
value: 'id',
|
||||||
|
label: 'grouping',
|
||||||
|
children: 'children'
|
||||||
|
};
|
||||||
|
|
||||||
// 当对话框关闭时,重置表单
|
// 当对话框关闭时,重置表单
|
||||||
watch(() => props.visible, (newVal) => {
|
watch(() => props.visible, (newVal) => {
|
||||||
if (!newVal && formRef.value) {
|
if (!newVal && formRef.value) {
|
||||||
@@ -88,3 +119,25 @@ const handleSubmit = async () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.category-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-icon {
|
||||||
|
color: #409eff;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-name {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-count {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -2,23 +2,43 @@
|
|||||||
<el-dialog
|
<el-dialog
|
||||||
:model-value="visible"
|
:model-value="visible"
|
||||||
title="新建笔记"
|
title="新建笔记"
|
||||||
width="400px"
|
width="450px"
|
||||||
@close="handleClose"
|
@close="handleClose"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
>
|
>
|
||||||
<el-form :model="form" :rules="rules" ref="formRef" label-width="80px">
|
<el-form :model="form" :rules="rules" ref="formRef" label-width="90px">
|
||||||
<el-form-item label="笔记标题" prop="title">
|
<el-form-item label="笔记标题" prop="title">
|
||||||
<el-input v-model="form.title" autocomplete="off"></el-input>
|
<el-input
|
||||||
|
v-model="form.title"
|
||||||
|
autocomplete="off"
|
||||||
|
placeholder="请输入笔记标题"
|
||||||
|
maxlength="100"
|
||||||
|
show-word-limit
|
||||||
|
></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="选择分类" prop="groupingId">
|
<el-form-item label="选择分类" prop="groupingId">
|
||||||
<el-cascader
|
<el-cascader
|
||||||
v-model="form.groupingId"
|
v-model="form.groupingId"
|
||||||
:options="categoryOptions"
|
:options="categoryOptions"
|
||||||
:props="{ checkStrictly: true, emitPath: false, value: 'id', label: 'grouping' }"
|
:props="cascaderProps"
|
||||||
clearable
|
clearable
|
||||||
|
filterable
|
||||||
placeholder="请选择笔记所属分类"
|
placeholder="请选择笔记所属分类"
|
||||||
style="width: 100%;"
|
style="width: 100%;"
|
||||||
></el-cascader>
|
>
|
||||||
|
<template #default="{ node, data }">
|
||||||
|
<span class="category-option">
|
||||||
|
<el-icon class="category-icon">
|
||||||
|
<Folder v-if="data.children && data.children.length > 0" />
|
||||||
|
<Document v-else />
|
||||||
|
</el-icon>
|
||||||
|
<span class="category-name">{{ data.grouping }}</span>
|
||||||
|
<span v-if="data.children && data.children.length > 0" class="category-count">
|
||||||
|
({{ data.children.length }})
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-cascader>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="私密笔记">
|
<el-form-item label="私密笔记">
|
||||||
<el-switch
|
<el-switch
|
||||||
@@ -41,6 +61,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { Folder, Document } from '@element-plus/icons-vue';
|
||||||
import { updateMarkdown } from '@/api/CommonApi.js';
|
import { updateMarkdown } from '@/api/CommonApi.js';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -68,6 +89,15 @@ const rules = ref({
|
|||||||
groupingId: [{ required: true, message: '请选择分类', trigger: 'change' }],
|
groupingId: [{ required: true, message: '请选择分类', trigger: 'change' }],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 级联选择器配置
|
||||||
|
const cascaderProps = {
|
||||||
|
checkStrictly: true,
|
||||||
|
emitPath: false,
|
||||||
|
value: 'id',
|
||||||
|
label: 'grouping',
|
||||||
|
children: 'children'
|
||||||
|
};
|
||||||
|
|
||||||
watch(() => props.visible, (newVal) => {
|
watch(() => props.visible, (newVal) => {
|
||||||
if (!newVal && formRef.value) {
|
if (!newVal && formRef.value) {
|
||||||
formRef.value.resetFields();
|
formRef.value.resetFields();
|
||||||
@@ -104,6 +134,26 @@ const handleSubmit = async () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.category-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-icon {
|
||||||
|
color: #409eff;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-name {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-count {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.form-item-help {
|
.form-item-help {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: #909399;
|
color: #909399;
|
||||||
|
|||||||
@@ -5,25 +5,56 @@
|
|||||||
width="400px"
|
width="400px"
|
||||||
@close="handleClose"
|
@close="handleClose"
|
||||||
:close-on-click-modal="false"
|
:close-on-click-modal="false"
|
||||||
|
:destroy-on-close="true"
|
||||||
>
|
>
|
||||||
<el-cascader
|
<div v-if="noteToMove" class="move-note-info">
|
||||||
|
<el-icon class="note-icon"><Document /></el-icon>
|
||||||
|
<span class="note-title" :title="noteToMove.title">{{ noteToMove.title }}</span>
|
||||||
|
<el-tag v-if="currentGroupName" type="info" size="small" effect="plain">{{ currentGroupName }}</el-tag>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="select-wrapper">
|
||||||
|
<el-select-v2
|
||||||
v-model="moveToGroupId"
|
v-model="moveToGroupId"
|
||||||
:options="categoryOptions"
|
:options="flattenOptions"
|
||||||
:props="{ checkStrictly: true, emitPath: false, value: 'id', label: 'grouping' }"
|
|
||||||
clearable
|
|
||||||
placeholder="请选择目标分类"
|
placeholder="请选择目标分类"
|
||||||
|
clearable
|
||||||
|
filterable
|
||||||
|
:height="200"
|
||||||
style="width: 100%;"
|
style="width: 100%;"
|
||||||
></el-cascader>
|
>
|
||||||
|
<template #default="{ item }">
|
||||||
|
<div class="select-option" :class="{ 'is-current': item.value === noteToMove?.groupingId }">
|
||||||
|
<span class="indent" :style="{ width: item.indent + 'px' }"></span>
|
||||||
|
<el-icon class="option-icon">
|
||||||
|
<Folder v-if="item.hasChildren" />
|
||||||
|
<Document v-else />
|
||||||
|
</el-icon>
|
||||||
|
<span class="option-label">{{ item.label }}</span>
|
||||||
|
<el-tag v-if="item.value === noteToMove?.groupingId" type="warning" size="small">当前</el-tag>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-select-v2>
|
||||||
|
</div>
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="handleClose">取消</el-button>
|
<el-button @click="handleClose">取消</el-button>
|
||||||
<el-button type="primary" @click="handleSubmit">确定</el-button>
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
@click="handleSubmit"
|
||||||
|
:disabled="!moveToGroupId || moveToGroupId === noteToMove?.groupingId"
|
||||||
|
:loading="isLoading"
|
||||||
|
>
|
||||||
|
确定
|
||||||
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch, computed } from 'vue';
|
||||||
import { ElMessage } from 'element-plus';
|
import { ElMessage } from 'element-plus';
|
||||||
|
import { Folder, Document } from '@element-plus/icons-vue';
|
||||||
import { updateMarkdown } from '@/api/CommonApi.js';
|
import { updateMarkdown } from '@/api/CommonApi.js';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -44,10 +75,53 @@ const props = defineProps({
|
|||||||
const emit = defineEmits(['update:visible', 'move-success']);
|
const emit = defineEmits(['update:visible', 'move-success']);
|
||||||
|
|
||||||
const moveToGroupId = ref(null);
|
const moveToGroupId = ref(null);
|
||||||
|
const isLoading = ref(false);
|
||||||
|
|
||||||
|
// 扁平化选项,用于虚拟选择器
|
||||||
|
const flattenOptions = computed(() => {
|
||||||
|
const result = [];
|
||||||
|
const flatten = (options, depth = 0) => {
|
||||||
|
for (const option of options) {
|
||||||
|
result.push({
|
||||||
|
value: option.id,
|
||||||
|
label: option.grouping,
|
||||||
|
indent: depth * 20,
|
||||||
|
hasChildren: option.children && option.children.length > 0
|
||||||
|
});
|
||||||
|
if (option.children && option.children.length > 0) {
|
||||||
|
flatten(option.children, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
flatten(props.categoryOptions);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 使用 Map 缓存分类名称,O(1) 查找
|
||||||
|
const groupNameMap = computed(() => {
|
||||||
|
const map = new Map();
|
||||||
|
const traverse = (options) => {
|
||||||
|
for (const option of options) {
|
||||||
|
map.set(option.id, option.grouping);
|
||||||
|
if (option.children && option.children.length > 0) {
|
||||||
|
traverse(option.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
traverse(props.categoryOptions);
|
||||||
|
return map;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 计算当前分类名称
|
||||||
|
const currentGroupName = computed(() => {
|
||||||
|
if (!props.noteToMove?.groupingId) return '';
|
||||||
|
return groupNameMap.value.get(props.noteToMove.groupingId) || '';
|
||||||
|
});
|
||||||
|
|
||||||
watch(() => props.visible, (newVal) => {
|
watch(() => props.visible, (newVal) => {
|
||||||
if (!newVal) {
|
if (!newVal) {
|
||||||
moveToGroupId.value = null;
|
moveToGroupId.value = null;
|
||||||
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -64,7 +138,12 @@ const handleSubmit = async () => {
|
|||||||
ElMessage.error('没有需要移动的笔记');
|
ElMessage.error('没有需要移动的笔记');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (moveToGroupId.value === props.noteToMove.groupingId) {
|
||||||
|
ElMessage.warning('不能移动到当前分类');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = true;
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
...props.noteToMove,
|
...props.noteToMove,
|
||||||
@@ -76,6 +155,69 @@ const handleSubmit = async () => {
|
|||||||
handleClose();
|
handleClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error('移动失败: ' + error.message);
|
ElMessage.error('移动失败: ' + error.message);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.move-note-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
background-color: #f5f7fa;
|
||||||
|
border-radius: 6px;
|
||||||
|
border-left: 3px solid #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-icon {
|
||||||
|
color: #409eff;
|
||||||
|
font-size: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-title {
|
||||||
|
flex: 1;
|
||||||
|
font-weight: 500;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-option.is-current {
|
||||||
|
color: #e6a23c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indent {
|
||||||
|
display: inline-block;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-icon {
|
||||||
|
color: #409eff;
|
||||||
|
font-size: 16px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.option-label {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -31,5 +31,13 @@ export const useUserStore = defineStore('user', {
|
|||||||
getters: {
|
getters: {
|
||||||
isLoggedIn: (state) => !!state.token,
|
isLoggedIn: (state) => !!state.token,
|
||||||
},
|
},
|
||||||
persist: true,
|
persist: {
|
||||||
|
enabled: true,
|
||||||
|
strategies: [
|
||||||
|
{
|
||||||
|
key: 'user-store',
|
||||||
|
storage: sessionStorage,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
@@ -15,10 +15,14 @@ const instance = axios.create({
|
|||||||
// 请求拦截器
|
// 请求拦截器
|
||||||
instance.interceptors.request.use(
|
instance.interceptors.request.use(
|
||||||
config => {
|
config => {
|
||||||
|
try {
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
if (userStore.token) {
|
if (userStore.token) {
|
||||||
config.headers['Authorization'] = `Bearer ${userStore.token}`
|
config.headers['Authorization'] = `Bearer ${userStore.token}`
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to get user store:', error)
|
||||||
|
}
|
||||||
return config
|
return config
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
@@ -43,10 +47,14 @@ instance.interceptors.response.use(
|
|||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
if (error.response && error.response.status === 401) {
|
if (error.response && error.response.status === 401) {
|
||||||
const userStore = useUserStore();
|
try {
|
||||||
|
const userStore = useUserStore()
|
||||||
userStore.logout();
|
userStore.logout();
|
||||||
ElMessage.error('登录已过期,请重新登录');
|
ElMessage.error('登录已过期,请重新登录');
|
||||||
router.push('/login');
|
router.push('/login');
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to get user store:', error)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ElMessage({
|
ElMessage({
|
||||||
message: error.message,
|
message: error.message,
|
||||||
|
|||||||
Reference in New Issue
Block a user