初始化本地仓库
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
<!-- 留言管理页面 -->
|
||||
<template>
|
||||
<div>
|
||||
<h1 class="text-4xl font-medium mt-5">留言墙</h1>
|
||||
<p class="mt-3.5 text-g-600">每一份留言都记录了您的想法,也为我们提供了珍贵的回忆</p>
|
||||
|
||||
<ul
|
||||
class="mt-10 grid grid-cols-5 gap-5 max-2xl:grid-cols-4 max-xl:grid-cols-3 max-lg:grid-cols-2 max-sm:grid-cols-1 mb-5"
|
||||
>
|
||||
<li
|
||||
class="relative p-4 c-p aspect-16/12 duration-300 hover:-translate-y-1.5"
|
||||
:style="{ background: item.color }"
|
||||
v-for="item in commentsWithColors"
|
||||
:key="item.id"
|
||||
@click="openDrawer(item)"
|
||||
>
|
||||
<p class="text-g-600 text-sm">{{ item.date }}</p>
|
||||
<p class="mt-4 text-sm text-gray-800">{{ item.content }}</p>
|
||||
<div class="absolute bottom-4 left-0 px-4 flex-cb w-full">
|
||||
<div class="flex-c">
|
||||
<div class="flex-c mr-5 text-xs text-g-600">
|
||||
<ArtSvgIcon icon="ri:heart-line" class="mr-1 text-base" />
|
||||
<span>{{ item.collection }}</span>
|
||||
</div>
|
||||
<div class="flex-c mr-5 text-xs text-g-600">
|
||||
<ArtSvgIcon icon="ri:message-3-line" class="mr-1 text-base" />
|
||||
<span>{{ item.comment }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<span class="text-sm text-gray-700">{{ item.userName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ElDrawer
|
||||
lDrawer
|
||||
v-model="showDrawer"
|
||||
:lock-scroll="false"
|
||||
:size="360"
|
||||
modal-class="comment-modal"
|
||||
>
|
||||
<template #header>
|
||||
<h4>详情</h4>
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="drawer-default">
|
||||
<div class="relative p-4 aspect-16/12" :style="{ background: clickItem.color }">
|
||||
<p class="text-g-500 text-sm">{{ clickItem.date }}</p>
|
||||
<p class="mt-4 text-sm text-gray-800">{{ clickItem.content }}</p>
|
||||
<div class="absolute bottom-4 left-0 px-4 flex-cb w-full">
|
||||
<div class="flex-c">
|
||||
<div class="flex-c mr-5 text-xs text-g-600">
|
||||
<ArtSvgIcon icon="ri:heart-line" class="mr-1 text-base" />
|
||||
<span>{{ clickItem.collection }}</span>
|
||||
</div>
|
||||
<div class="flex-c mr-5 text-xs text-g-600">
|
||||
<ArtSvgIcon icon="ri:message-3-line" class="mr-1 text-base" />
|
||||
<span>{{ clickItem.comment }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-sm text-gray-700">{{ clickItem.userName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 评论组件 -->
|
||||
<CommentWidget />
|
||||
</div>
|
||||
</template>
|
||||
</ElDrawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { commentList } from "@/apps/demo/mock/temp/commentList";
|
||||
|
||||
defineOptions({ name: "ArticleComment" });
|
||||
|
||||
interface CommentItem {
|
||||
id: number;
|
||||
date: string;
|
||||
content: string;
|
||||
collection: number;
|
||||
comment: number;
|
||||
userName: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
const COLOR_LIST = [
|
||||
"#D8F8FF",
|
||||
"#FDDFD9",
|
||||
"#FCE6F0",
|
||||
"#D3F8F0",
|
||||
"#FFEABC",
|
||||
"#F5E1FF",
|
||||
"#E1E6FE",
|
||||
];
|
||||
|
||||
const showDrawer = ref(false);
|
||||
const clickItem = ref<CommentItem>({
|
||||
id: 1,
|
||||
date: "2024-9-3",
|
||||
content: "加油!学好Node 自己写个小Demo",
|
||||
collection: 5,
|
||||
comment: 8,
|
||||
userName: "匿名",
|
||||
color: COLOR_LIST[0],
|
||||
});
|
||||
|
||||
/**
|
||||
* 为评论列表分配随机颜色
|
||||
*/
|
||||
const commentsWithColors = computed(() => {
|
||||
let lastColorIndex = -1;
|
||||
|
||||
return commentList.map((item) => {
|
||||
let newIndex: number;
|
||||
|
||||
do {
|
||||
newIndex = Math.floor(Math.random() * COLOR_LIST.length);
|
||||
} while (newIndex === lastColorIndex && COLOR_LIST.length > 1);
|
||||
|
||||
lastColorIndex = newIndex;
|
||||
|
||||
return {
|
||||
...item,
|
||||
color: COLOR_LIST[newIndex],
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 打开评论详情抽屉
|
||||
*/
|
||||
const openDrawer = (item: CommentItem) => {
|
||||
showDrawer.value = true;
|
||||
clickItem.value = item;
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,136 @@
|
||||
<!-- 文章详情页面 -->
|
||||
<template>
|
||||
<div class="article-detail page-content">
|
||||
<div class="max-w-200 m-auto mt-15">
|
||||
<h1 class="text-3xl font-semibold">{{ articleTitle }}</h1>
|
||||
<div class="markdown-body mt-12.5" v-highlight v-html="articleHtml"></div>
|
||||
</div>
|
||||
<ArtBackToTop />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import "@/apps/demo/assets/styles/core/md.scss";
|
||||
import "@/apps/demo/assets/styles/custom/one-dark-pro.scss";
|
||||
import { useCommon } from "@/hooks/core/useCommon";
|
||||
import axios from "axios";
|
||||
|
||||
defineOptions({ name: "ArticleDetail" });
|
||||
|
||||
interface ArticleResponse {
|
||||
code: number;
|
||||
data: {
|
||||
title: string;
|
||||
html_content: string;
|
||||
};
|
||||
}
|
||||
|
||||
const route = useRoute();
|
||||
const articleId = computed(() => Number(route.params.id));
|
||||
const articleTitle = ref("");
|
||||
const articleHtml = shallowRef("");
|
||||
const loading = ref(false);
|
||||
const error = ref<string | null>(null);
|
||||
|
||||
const getArticleDetail = async () => {
|
||||
if (!articleId.value) return;
|
||||
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
|
||||
try {
|
||||
const { data } = await axios.get<ArticleResponse>(
|
||||
"https://www.qiniu.lingchen.kim/blog_detail.json"
|
||||
);
|
||||
|
||||
if (data.code === 200) {
|
||||
articleTitle.value = data.data.title;
|
||||
articleHtml.value = data.data.html_content;
|
||||
}
|
||||
} catch (err) {
|
||||
error.value = "文章加载失败";
|
||||
console.error("获取文章详情失败:", err);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const { scrollToTop } = useCommon();
|
||||
|
||||
onMounted(() => {
|
||||
scrollToTop();
|
||||
getArticleDetail();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.article-detail {
|
||||
:deep(.markdown-body) {
|
||||
margin-top: 60px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
border: 1px solid var(--art-gray-200);
|
||||
}
|
||||
|
||||
pre {
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
.copy-button {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50px;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
content: "";
|
||||
background: #0a0a0e;
|
||||
}
|
||||
}
|
||||
|
||||
.code-wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.line-number {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
box-sizing: border-box;
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
margin-right: 10px;
|
||||
font-size: 14px;
|
||||
color: #9e9e9e;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.copy-button {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
font-size: 20px;
|
||||
line-height: 40px;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
background-color: #000;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
opacity: 0;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,201 @@
|
||||
<!-- 文章列表页面 -->
|
||||
<template>
|
||||
<div class="page-content !mb-5">
|
||||
<ElRow justify="space-between" :gutter="10">
|
||||
<ElCol :lg="6" :md="6" :sm="14" :xs="16">
|
||||
<ElInput
|
||||
v-model="searchVal"
|
||||
:prefix-icon="Search"
|
||||
clearable
|
||||
placeholder="输入文章标题查询"
|
||||
@keyup.enter="searchArticle"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :lg="12" :md="12" :sm="0" :xs="0">
|
||||
<div class="custom-segmented">
|
||||
<ElSegmented
|
||||
v-model="yearVal"
|
||||
:options="YEAR_OPTIONS"
|
||||
@change="searchArticleByYear"
|
||||
/>
|
||||
</div>
|
||||
</ElCol>
|
||||
<ElCol :lg="6" :md="6" :sm="10" :xs="6" style="display: flex; justify-content: end">
|
||||
<ElButton @click="toAddArticle" v-auth="'add'">新增文章</ElButton>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<div class="mt-5">
|
||||
<div
|
||||
class="grid grid-cols-5 gap-5 max-2xl:grid-cols-4 max-xl:grid-cols-3 max-lg:grid-cols-2 max-sm:grid-cols-1"
|
||||
>
|
||||
<div
|
||||
class="group c-p overflow-hidden border border-g-300/60 rounded-custom-sm"
|
||||
v-for="item in articleList"
|
||||
:key="item.id"
|
||||
@click="toDetail(item)"
|
||||
>
|
||||
<div class="relative aspect-[16/9.5]">
|
||||
<ElImage
|
||||
class="flex align-center justify-center w-full h-full object-cover bg-gray-200"
|
||||
:src="item.home_img"
|
||||
lazy
|
||||
fit="cover"
|
||||
>
|
||||
</ElImage>
|
||||
|
||||
<span
|
||||
class="absolute top-1 right-1 bg-black/50 rounded text-xs px-1 py-0.5 text-white"
|
||||
>{{ item.type_name }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="px-2 py-1">
|
||||
<h2 class="text-base text-g-800 font-medium">{{ item.title }}</h2>
|
||||
<div class="flex-b w-full h-6 mt-1">
|
||||
<div class="flex-c text-g-500">
|
||||
<ArtSvgIcon icon="ri:time-line" class="mr-1 text-sm" />
|
||||
<span class="text-sm">{{
|
||||
useDateFormat(item.create_time, "YYYY-MM-DD")
|
||||
}}</span>
|
||||
<div class="w-px h-3 bg-g-400 mx-3.5"></div>
|
||||
<ArtSvgIcon icon="ri:eye-line" class="mr-1 text-sm" />
|
||||
<span class="text-sm">{{ item.count }}</span>
|
||||
</div>
|
||||
<ElButton
|
||||
class="opacity-0 group-hover:opacity-100"
|
||||
v-auth="'edit'"
|
||||
size="small"
|
||||
@click.stop="toEdit(item)"
|
||||
>编辑</ElButton
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 16vh" v-if="showEmpty">
|
||||
<ElEmpty :description="`未找到相关数据 ${EmojiText[0]}`" />
|
||||
</div>
|
||||
|
||||
<div style="display: flex; justify-content: center; margin-top: 20px">
|
||||
<ElPagination
|
||||
size="default"
|
||||
background
|
||||
v-model:current-page="currentPage"
|
||||
:page-size="pageSize"
|
||||
:pager-count="9"
|
||||
layout="prev, pager, next, total,jumper"
|
||||
:total="total"
|
||||
:hide-on-single-page="true"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Search } from "@element-plus/icons-vue";
|
||||
import { router } from "@/router";
|
||||
import { useDateFormat } from "@vueuse/core";
|
||||
import EmojiText from "@/utils/ui/emojo";
|
||||
import { ArticleList } from "@/apps/demo/mock/temp/articleList";
|
||||
import { useCommon } from "@/hooks/core/useCommon";
|
||||
|
||||
defineOptions({ name: "ArticleList" });
|
||||
|
||||
interface Article {
|
||||
id: number;
|
||||
home_img: string;
|
||||
type_name: string;
|
||||
title: string;
|
||||
create_time: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
interface GetArticleListOptions {
|
||||
backTop?: boolean;
|
||||
}
|
||||
|
||||
const YEAR_OPTIONS = ["All", "2024", "2023", "2022", "2021", "2020", "2019"];
|
||||
const PAGE_SIZE = 40;
|
||||
|
||||
const yearVal = ref("All");
|
||||
const searchVal = ref("");
|
||||
const articleList = ref<Article[]>([]);
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(PAGE_SIZE);
|
||||
const total = ref(0);
|
||||
const isLoading = ref(true);
|
||||
|
||||
const showEmpty = computed(() => articleList.value.length === 0 && !isLoading.value);
|
||||
|
||||
const getArticleList = async ({ backTop = false }: GetArticleListOptions = {}) => {
|
||||
isLoading.value = true;
|
||||
|
||||
try {
|
||||
if (searchVal.value) {
|
||||
yearVal.value = "All";
|
||||
}
|
||||
|
||||
// TODO: 替换为真实 API 调用
|
||||
// const params = {
|
||||
// page: currentPage.value,
|
||||
// size: pageSize.value,
|
||||
// searchVal: searchVal.value,
|
||||
// year: yearVal.value === 'All' ? '' : yearVal.value
|
||||
// }
|
||||
// const res = await ArticleService.getArticleList(params)
|
||||
|
||||
articleList.value = ArticleList as Article[];
|
||||
|
||||
if (backTop) {
|
||||
useCommon().scrollToTop();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("获取文章列表失败:", error);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const searchArticle = () => {
|
||||
currentPage.value = 1;
|
||||
getArticleList({ backTop: true });
|
||||
};
|
||||
|
||||
const searchArticleByYear = () => {
|
||||
currentPage.value = 1;
|
||||
getArticleList({ backTop: true });
|
||||
};
|
||||
|
||||
const handleCurrentChange = (val: number) => {
|
||||
currentPage.value = val;
|
||||
getArticleList({ backTop: true });
|
||||
};
|
||||
|
||||
const toDetail = (item: Article) => {
|
||||
router.push({ name: "ArticleDetail", params: { id: item.id } });
|
||||
};
|
||||
|
||||
const toEdit = (item: Article) => {
|
||||
router.push({ name: "ArticlePublish", query: { id: item.id } });
|
||||
};
|
||||
|
||||
const toAddArticle = () => {
|
||||
router.push({ name: "ArticlePublish" });
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getArticleList();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.custom-segmented .el-segmented {
|
||||
height: 40px;
|
||||
padding: 6px;
|
||||
|
||||
--el-border-radius-base: 8px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,339 @@
|
||||
<!-- 文章发布页面 -->
|
||||
<template>
|
||||
<div>
|
||||
<div>
|
||||
<div class="max-w-250 mx-auto my-5">
|
||||
<!-- 文章标题、类型 -->
|
||||
<ElRow :gutter="10">
|
||||
<ElCol :span="18">
|
||||
<ElInput
|
||||
v-model.trim="articleName"
|
||||
placeholder="请输入文章标题(最多100个字符)"
|
||||
maxlength="100"
|
||||
/>
|
||||
</ElCol>
|
||||
<ElCol :span="6">
|
||||
<ElSelect v-model="articleType" placeholder="请选择文章类型" filterable>
|
||||
<ElOption
|
||||
v-for="item in articleTypes"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElCol>
|
||||
</ElRow>
|
||||
|
||||
<!-- 富文本编辑器 -->
|
||||
<ArtWangEditor class="mt-2.5" v-model="editorHtml" />
|
||||
|
||||
<div class="p-5 mt-5 art-card-xs">
|
||||
<h2 class="mb-5 text-xl font-medium">发布设置</h2>
|
||||
<!-- 图片上传 -->
|
||||
<ElForm>
|
||||
<ElFormItem label="封面">
|
||||
<div class="mt-2.5">
|
||||
<ElUpload
|
||||
:action="uploadImageUrl"
|
||||
:headers="uploadHeaders"
|
||||
:show-file-list="false"
|
||||
:on-success="onSuccess"
|
||||
:on-error="onError"
|
||||
:before-upload="beforeUpload"
|
||||
>
|
||||
<div
|
||||
v-if="!cover"
|
||||
class="flex-cc flex-col w-65 h-40 border border-dashed border-[#d9d9d9] rounded-md"
|
||||
>
|
||||
<ElIcon class="!text-xl !text-g-600"><Plus /></ElIcon>
|
||||
<div class="mt-2 text-sm text-g-600">点击上传封面</div>
|
||||
</div>
|
||||
<img v-else :src="cover" class="block w-65 h-40 object-cover" />
|
||||
</ElUpload>
|
||||
<div class="mt-2 text-xs text-g-700">建议尺寸 16:9,jpg/png 格式</div>
|
||||
</div>
|
||||
</ElFormItem>
|
||||
<ElFormItem label="可见">
|
||||
<ElSwitch v-model="visible" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<ElButton type="primary" @click="submit" class="w-25">
|
||||
{{ pageMode === PageModeEnum.Edit ? '保存' : '发布' }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- <div class="box-border w-70 p-5 border border-[#e3e3e3] rounded-lg">
|
||||
<div v-for="(item, index) in outlineList" :key="index">
|
||||
<p :class="['h-7.5 text-xs leading-7.5 c-p', item.level === 3 && 'pl-2.5']">{{ item.text }}</p>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Plus } from '@element-plus/icons-vue'
|
||||
import { ApiStatus } from '@/utils/http/status'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import EmojiText from '@/utils/ui/emojo'
|
||||
import { PageModeEnum } from '@/enums/formEnum'
|
||||
import axios from 'axios'
|
||||
import { useCommon } from '@/hooks/core/useCommon'
|
||||
|
||||
defineOptions({ name: 'ArticlePublish' })
|
||||
|
||||
interface ArticleType {
|
||||
id: number
|
||||
name: string
|
||||
}
|
||||
|
||||
interface UploadResponse {
|
||||
data: {
|
||||
url: string
|
||||
}
|
||||
}
|
||||
|
||||
interface ArticleDetailResponse {
|
||||
code: number
|
||||
data: {
|
||||
title: string
|
||||
blog_class: string
|
||||
html_content: string
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_IMAGE_SIZE = 2 // MB
|
||||
const EMPTY_EDITOR_CONTENT = '<p><br></p>'
|
||||
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
const { accessToken } = userStore
|
||||
|
||||
const uploadImageUrl = `${import.meta.env.VITE_API_URL}/api/common/upload`
|
||||
const uploadHeaders = { Authorization: accessToken }
|
||||
|
||||
const pageMode = ref<PageModeEnum>(PageModeEnum.Add)
|
||||
const articleName = ref('')
|
||||
const articleType = ref<number>()
|
||||
const articleTypes = ref<ArticleType[]>([])
|
||||
const editorHtml = ref('')
|
||||
const createDate = ref('')
|
||||
const cover = ref('')
|
||||
const visible = ref(true)
|
||||
|
||||
/**
|
||||
* 初始化页面模式(新增或编辑)
|
||||
*/
|
||||
const initPageMode = () => {
|
||||
const { id } = route.query
|
||||
pageMode.value = id ? PageModeEnum.Edit : PageModeEnum.Add
|
||||
|
||||
if (pageMode.value === PageModeEnum.Edit) {
|
||||
getArticleDetail()
|
||||
} else {
|
||||
createDate.value = formatDate(useNow().value)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文章分类列表
|
||||
*/
|
||||
const getArticleTypes = async () => {
|
||||
try {
|
||||
const { data } = await axios.get('https://www.qiniu.lingchen.kim/classify.json')
|
||||
if (data.code === 200) {
|
||||
articleTypes.value = data.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取文章分类失败:', error)
|
||||
ElMessage.error('获取文章分类失败')
|
||||
}
|
||||
|
||||
// TODO: 替换为真实 API 调用
|
||||
// const res = await ArticleService.getArticleTypes({})
|
||||
// if (res.code === ApiStatus.success) {
|
||||
// articleTypes.value = res.data
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取文章详情(编辑模式)
|
||||
*/
|
||||
const getArticleDetail = async () => {
|
||||
try {
|
||||
const { data } = await axios.get<ArticleDetailResponse>(
|
||||
'https://www.qiniu.lingchen.kim/blog_list.json'
|
||||
)
|
||||
|
||||
if (data.code === ApiStatus.success) {
|
||||
const { title, blog_class, html_content } = data.data
|
||||
articleName.value = title
|
||||
articleType.value = Number(blog_class)
|
||||
editorHtml.value = html_content
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取文章详情失败:', error)
|
||||
ElMessage.error('获取文章详情失败')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期为 YYYY-MM-DD 格式
|
||||
*/
|
||||
const formatDate = (date: string | Date): string => {
|
||||
return useDateFormat(date, 'YYYY-MM-DD').value
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证文章表单数据
|
||||
*/
|
||||
const validateArticle = (): boolean => {
|
||||
if (!articleName.value.trim()) {
|
||||
ElMessage.error('请输入文章标题')
|
||||
return false
|
||||
}
|
||||
|
||||
if (!articleType.value) {
|
||||
ElMessage.error('请选择文章类型')
|
||||
return false
|
||||
}
|
||||
|
||||
if (!editorHtml.value || editorHtml.value === EMPTY_EDITOR_CONTENT) {
|
||||
ElMessage.error('请输入文章内容')
|
||||
return false
|
||||
}
|
||||
|
||||
if (!cover.value) {
|
||||
ElMessage.error('请上传封面图片')
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理代码块中的多余空格
|
||||
*/
|
||||
const cleanCodeContent = (content: string): string => {
|
||||
return content.replace(/(\s*)<\/code>/g, '</code>')
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增文章
|
||||
*/
|
||||
const addArticle = async () => {
|
||||
if (!validateArticle()) return
|
||||
|
||||
try {
|
||||
const cleanedContent = cleanCodeContent(editorHtml.value)
|
||||
|
||||
// TODO: 替换为真实 API 调用
|
||||
// const params = {
|
||||
// title: articleName.value,
|
||||
// type: articleType.value,
|
||||
// content: cleanedContent,
|
||||
// cover: cover.value,
|
||||
// visible: visible.value
|
||||
// }
|
||||
// const res = await ArticleService.addArticle(params)
|
||||
// if (res.code === ApiStatus.success) {
|
||||
// ElMessage.success('文章发布成功')
|
||||
// router.push({ name: 'ArticleList' })
|
||||
// }
|
||||
|
||||
console.log('新增文章:', { cleanedContent })
|
||||
} catch (error) {
|
||||
console.error('发布文章失败:', error)
|
||||
ElMessage.error('发布文章失败')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑文章
|
||||
*/
|
||||
const editArticle = async () => {
|
||||
if (!validateArticle()) return
|
||||
|
||||
try {
|
||||
const cleanedContent = cleanCodeContent(editorHtml.value)
|
||||
|
||||
// TODO: 替换为真实 API 调用
|
||||
// const params = {
|
||||
// id: route.query.id,
|
||||
// title: articleName.value,
|
||||
// type: articleType.value,
|
||||
// content: cleanedContent,
|
||||
// cover: cover.value,
|
||||
// visible: visible.value
|
||||
// }
|
||||
// const res = await ArticleService.editArticle(params)
|
||||
// if (res.code === ApiStatus.success) {
|
||||
// ElMessage.success('文章保存成功')
|
||||
// router.push({ name: 'ArticleList' })
|
||||
// }
|
||||
|
||||
console.log('编辑文章:', { cleanedContent })
|
||||
} catch (error) {
|
||||
console.error('保存文章失败:', error)
|
||||
ElMessage.error('保存文章失败')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交表单(新增或编辑)
|
||||
*/
|
||||
const submit = () => {
|
||||
if (pageMode.value === PageModeEnum.Edit) {
|
||||
editArticle()
|
||||
} else {
|
||||
addArticle()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片上传成功回调
|
||||
*/
|
||||
const onSuccess = (response: UploadResponse) => {
|
||||
cover.value = response.data.url
|
||||
ElMessage.success(`图片上传成功 ${EmojiText[200]}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片上传失败回调
|
||||
*/
|
||||
const onError = () => {
|
||||
ElMessage.error(`图片上传失败 ${EmojiText[500]}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传前的文件校验
|
||||
*/
|
||||
const beforeUpload = (file: File): boolean => {
|
||||
const isImage = file.type.startsWith('image/')
|
||||
const isLt2M = file.size / 1024 / 1024 < MAX_IMAGE_SIZE
|
||||
|
||||
if (!isImage) {
|
||||
ElMessage.error('只能上传图片文件')
|
||||
return false
|
||||
}
|
||||
|
||||
if (!isLt2M) {
|
||||
ElMessage.error(`图片大小不能超过 ${MAX_IMAGE_SIZE}MB`)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
const { scrollToTop } = useCommon()
|
||||
|
||||
onMounted(() => {
|
||||
scrollToTop()
|
||||
getArticleTypes()
|
||||
initPageMode()
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user