yunzer_go/pc/src/views/apps/knowledge/components/edit.vue
2025-11-02 23:53:41 +08:00

563 lines
14 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="knowledge-edit">
<!-- 顶部标题栏 -->
<div class="edit-header">
<el-button type="primary" link @click="goBack">
<el-icon><ArrowLeft /></el-icon>
返回
</el-button>
<h2 class="edit-title">{{ isEdit ? "编辑知识" : "新建知识" }}</h2>
</div>
<!-- 基本信息 -->
<div class="edit-meta">
<div class="meta-card">
<h3 class="meta-title">基本信息</h3>
<el-divider />
<el-form
:model="formData"
:rules="rules"
ref="formRef"
label-width="80px"
size="default"
>
<el-row :gutter="20">
<el-col :span="24">
<el-form-item label="标题" prop="title">
<el-input v-model="formData.title" placeholder="请输入标题" />
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="20">
<el-col :span="6">
<el-form-item label="分类" prop="category">
<el-cascader
v-model="cascaderValue"
:options="categoryTreeOptions"
:props="cascaderProps"
placeholder="请选择分类"
clearable
style="width: 100%"
@change="handleCategoryChange"
/>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="标签" prop="tags">
<el-select
v-model="formData.tags"
multiple
placeholder="请选择标签"
style="width: 100%"
allow-create
default-first-option
>
<el-option
v-for="tag in tagList"
:key="tag"
:label="tag"
:value="tag"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="作者" prop="author">
<el-input v-model="formData.author" placeholder="请输入作者" />
</el-form-item>
</el-col>
<el-col :span="6">
<el-form-item label="权限" prop="share">
<el-radio-group v-model="formData.share">
<el-radio-button :value="0">个人</el-radio-button>
<el-radio-button :value="1">共享</el-radio-button>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
<div class="meta-actions">
<el-button type="primary" @click="handleSubmit">
<el-icon><Check /></el-icon>
保存
</el-button>
<el-button @click="goBack">取消</el-button>
</div>
</div>
</div>
<!-- 正文内容区 - 左右结构 -->
<div class="edit-body">
<!-- 左侧编辑器 -->
<div class="editor-panel">
<div class="editor-card">
<h3 class="editor-title">编辑正文</h3>
<el-divider />
<div class="editor-content">
<WangEditor v-model="formData.content" />
</div>
</div>
</div>
<!-- 右侧预览 -->
<div class="preview-panel">
<div class="preview-card">
<h3 class="preview-title">预览效果</h3>
<el-divider />
<div class="markdown-body" v-html="compiledMarkdown"></div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, computed, onMounted, watch, nextTick } from "vue";
import { useRouter, useRoute } from "vue-router";
import { marked } from "marked";
import { ArrowLeft, Check } from '@element-plus/icons-vue'
// @ts-ignore
import { getKnowledgeDetail, createKnowledge, updateKnowledge, getCategoryList, getTagList } from "@/api/knowledge";
import { ElMessage, ElMessageBox } from "element-plus";
import type { FormInstance, FormRules } from "element-plus";
import WangEditor from '@/views/components/WangEditor.vue';
const formRef = ref<FormInstance>();
const router = useRouter();
const route = useRoute();
const formData = reactive<{
title: string;
category: string;
categoryId: number;
tags: string[];
author: string;
content: string;
share: number;
}>({
title: "",
category: "",
categoryId: 0,
tags: [],
author: "",
content: "",
share: 0,
});
const rules = reactive<FormRules>({
title: [{ required: true, message: "请输入标题", trigger: "blur" }],
category: [{ required: true, message: "请选择分类", trigger: "change" }],
author: [{ required: true, message: "请输入作者", trigger: "blur" }],
content: [{ required: true, message: "请输入正文", trigger: "blur" }],
});
interface CategoryItem {
categoryId: number;
categoryName: string;
parentId?: number;
children?: CategoryItem[];
}
const categoryList = ref<CategoryItem[]>([]);
const categoryTreeOptions = computed(() => {
return buildCategoryTree(categoryList.value);
});
const cascaderValue = ref<number | null>(null);
const cascaderProps = {
value: 'categoryId',
label: 'categoryName',
children: 'children',
checkStrictly: true,
emitPath: false,
};
const tagList = ref<string[]>([]);
const id = computed(() => route.params.id || route.query.id);
const isEdit = computed(() => {
const currentId = id.value;
return !!currentId && currentId !== "new" && currentId !== "";
});
const compiledMarkdown = computed(() => marked(formData.content || ""));
watch(
() => formData.content,
() => {
// 内容变化时自动更新预览
}
);
const getLoginUser = () => {
const userStr = localStorage.getItem("user");
if (userStr) {
try {
const user = JSON.parse(userStr);
return user.username || user.name || user.userName || "";
} catch (e) {
return "";
}
}
return "";
};
const fetchDetail = async () => {
try {
const currentId = id.value as string;
if (currentId && currentId !== "new") {
const res = await getKnowledgeDetail(parseInt(currentId as string));
const data = res.code === 0 && res.data ? res.data : res.data || res;
formData.title = data.title || "";
formData.category = data.categoryName || "";
formData.categoryId = data.categoryId || 0;
formData.author = data.author || "";
formData.content = data.content || "";
formData.share = data.share || 0;
// 设置级联选择器的值
// 注意:这里需要等待 categoryList 加载完成后再设置
// 所以使用 nextTick 确保数据已更新
nextTick(() => {
if (formData.categoryId) {
cascaderValue.value = formData.categoryId;
} else if (formData.category) {
// 如果没有 categoryId尝试通过 categoryName 查找
const foundCategory = categoryList.value.find(
(item) => item.categoryName === formData.category
);
if (foundCategory) {
formData.categoryId = foundCategory.categoryId;
cascaderValue.value = foundCategory.categoryId;
}
} else {
cascaderValue.value = null;
}
});
if (data.tags) {
try {
const parsed =
typeof data.tags === "string" ? JSON.parse(data.tags) : data.tags;
formData.tags = Array.isArray(parsed) ? parsed : [];
} catch {
formData.tags = Array.isArray(data.tags) ? data.tags : [];
}
} else {
formData.tags = [];
}
}
} catch (e) {
ElMessage.error("获取详情失败");
}
};
const loadCategoryAndTag = async () => {
try {
const [catRes, tagRes] = await Promise.all([
getCategoryList(),
getTagList(),
]);
const categories =
catRes.code === 0 && catRes.data ? catRes.data : catRes.data || [];
const tags =
tagRes.code === 0 && tagRes.data ? tagRes.data : tagRes.data || [];
categoryList.value = Array.isArray(categories) ? categories : [];
tagList.value = Array.isArray(tags)
? tags.map((tag) => tag.tagName || tag)
: [];
} catch (e: any) {
console.error("加载分类和标签失败:", e);
categoryList.value = [];
tagList.value = [];
}
};
// 构建分类树形结构
const buildCategoryTree = (list: CategoryItem[]): CategoryItem[] => {
if (!list || list.length === 0) return [];
const map = new Map<number, CategoryItem>();
const roots: CategoryItem[] = [];
// 创建节点映射
list.forEach(item => {
map.set(item.categoryId, {
...item,
children: [],
});
});
// 构建树形结构
list.forEach(item => {
const node = map.get(item.categoryId);
const parentId = item.parentId || 0;
if (parentId === 0 || !map.has(parentId)) {
// 根节点
roots.push(node!);
} else {
// 子节点
const parent = map.get(parentId);
if (parent && node) {
if (!parent.children) {
parent.children = [];
}
parent.children.push(node);
}
}
});
return roots;
};
const handleCategoryChange = (value: number | null) => {
if (value) {
formData.categoryId = value;
// 根据 categoryId 查找 categoryName
const findCategoryName = (list: CategoryItem[], id: number): string | null => {
for (const item of list) {
if (item.categoryId === id) {
return item.categoryName;
}
if (item.children) {
const found = findCategoryName(item.children, id);
if (found) return found;
}
}
return null;
};
const categoryName = findCategoryName(categoryTreeOptions.value, value);
if (categoryName) {
formData.category = categoryName;
}
} else {
formData.categoryId = 0;
formData.category = '';
}
};
const goBack = () => {
router.push("/apps/knowledge");
};
const handleSubmit = () => {
if (!formRef.value) return;
formRef.value.validate(async (valid: boolean) => {
if (!valid) return;
const currentId = id.value as string;
if (!formData.categoryId) {
ElMessage.warning("请选择分类");
return;
}
const submitData: any = {
title: formData.title || "",
categoryId: Number(formData.categoryId) || 0,
author: formData.author || "",
content: formData.content || "",
tags:
Array.isArray(formData.tags) && formData.tags.length > 0
? JSON.stringify(formData.tags)
: "[]",
status: 1,
share: formData.share,
};
if (isEdit.value && currentId !== "new") {
const knowledgeId = parseInt(currentId as string);
if (isNaN(knowledgeId) || knowledgeId <= 0) {
ElMessage.error("知识ID无效");
return;
}
try {
const res = await updateKnowledge(knowledgeId, submitData);
if (res.code === 0) {
ElMessage.success("保存成功");
goBack();
} else {
ElMessage.error(res.message || "保存失败");
}
} catch (e: any) {
console.error("保存失败:", e);
const errorMessage =
e.response?.data?.message || e.message || "保存失败";
ElMessage.error(errorMessage);
}
} else {
try {
const res = await createKnowledge(submitData);
if (res.code === 0) {
ElMessage.success("创建成功");
goBack();
} else {
ElMessage.error(res.message || "创建失败");
}
} catch (e: any) {
console.error("创建失败:", e);
const errorMessage =
e.response?.data?.message || e.message || "创建失败";
ElMessage.error(errorMessage);
}
}
});
};
onMounted(async () => {
const author = getLoginUser();
if (author && !isEdit.value) {
formData.author = author;
}
await loadCategoryAndTag();
if (isEdit.value) {
await fetchDetail();
}
// 新建时不需要设置默认分类,让用户自己选择
});
defineExpose({
formRef,
});
</script>
<style scoped lang="less">
.knowledge-edit {
padding: 24px;
min-height: 100%;
background-color: var(--el-bg-color-page);
}
.edit-header {
background: var(--el-bg-color);
border-radius: 12px;
padding: 20px 24px;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
border: 1px solid var(--el-border-color-lighter);
.edit-title {
margin: 0;
font-size: 20px;
font-weight: 600;
color: var(--el-text-color-primary);
}
}
.edit-meta {
margin-bottom: 20px;
.meta-card {
background: var(--el-bg-color);
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
border: 1px solid var(--el-border-color-lighter);
.meta-title {
margin: 0 0 16px;
font-size: 18px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.meta-actions {
margin-top: 24px;
text-align: right;
}
}
}
.edit-body {
display: flex;
gap: 20px;
min-height: 600px;
}
.editor-panel,
.preview-panel {
flex: 1;
min-width: 0;
}
.editor-card,
.preview-card {
background: var(--el-bg-color);
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
border: 1px solid var(--el-border-color-lighter);
height: 100%;
display: flex;
flex-direction: column;
.editor-title,
.preview-title {
font-size: 18px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.editor-content {
flex: 1;
min-height: 400px;
}
}
.preview-card {
.markdown-body {
flex: 1;
overflow-y: auto;
font-size: 14px;
line-height: 1.8;
color: var(--el-text-color-primary);
padding: 16px;
background: var(--el-fill-color-lighter);
border-radius: 8px;
:deep(h1), :deep(h2), :deep(h3) {
margin-top: 16px;
margin-bottom: 8px;
}
:deep(p), :deep(li) {
margin: 8px 0;
}
:deep(code) {
background-color: var(--el-fill-color-light);
padding: 2px 6px;
border-radius: 3px;
}
:deep(pre) {
background-color: var(--el-fill-color-light);
padding: 12px;
border-radius: 8px;
overflow-x: auto;
}
}
}
/* 响应式布局 */
@media (max-width: 768px) {
.edit-body {
flex-direction: column;
height: auto;
}
.editor-panel,
.preview-panel {
width: 100%;
}
}
</style>