Vue3+TypeScript+element-plus的文章管理项目(配后端接口)-退出及文章管理页面

首先,需要在pinia中封装退出的函数

TypeScript 复制代码
    async deleteAction() {
      this.token = ''
      this.userInfo = {}
      removeToken()
      router.push(`/login?redirectUrl=${router.currentRoute.value.path}`)
    },
html 复制代码
        <div class="logout">
          <el-popconfirm
            title="登出确认"
            confirm-button-text="确定"
            cancel-button-text="取消"
            hide-after="0"
            @confirm="onLogOut"
          >
            <template #reference>
              <el-icon :size="20" color="#999">
                <SwitchButton />
              </el-icon>
            </template>
          </el-popconfirm>
        </div>
function onLogOut() {
  userStore.deleteAction()
}

随后实现登出逻辑,删除本地和pinia中的token信息。

处理token过期

TypeScript 复制代码
const RESPONSE_CODE = {
  success: 10000,
  unauthorized: 401,
}

// 添加响应拦截器
instance.interceptors.response.use(
  function (response) {
    // 2xx 范围内的状态码都会触发该函数。
    const { data: resp } = response
    if (resp.code === RESPONSE_CODE.success) {
      return resp.data
    }
    return Promise.reject(new Error(resp.message))
  },
  function (error) {
    const userStore = useUserStore()
    // 超出 2xx 范围的状态码都会触发该函数。
    const {
      status,
      data: { message },
    } = error.response
    if (status === RESPONSE_CODE.unauthorized) {
      ElMessage.error(message)
      userStore.deleteAction()
    } else {
      return Promise.reject(error)
    }
  },
)

文章管理模块

html 复制代码
<template>
  <div class="article-box">
    <el-breadcrumb :separator-icon="ArrowRight">
      <el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
      <el-breadcrumb-item>文章管理</el-breadcrumb-item>
    </el-breadcrumb>
    <el-card>
      <template #header>
        <div class="card-header">
          <span>共 {{ total }} 条记录</span>
          <el-button round type="primary" @click="onOpen(ARTICLE_OPS_TYPE.add, '')">
            <el-icon><Plus /></el-icon>
            <span>新增文章</span>
          </el-button>
        </div>
      </template>
      <el-table :data="articleList" border striped>
        <el-table-column prop="stem" label="标题" width="300" />
        <el-table-column prop="creator" label="作者" width="180" />>
        <el-table-column prop="category" label="类别" wideh="140" />
        <el-table-column prop="likeCount" label="点赞" width="140" sortable />
        <el-table-column prop="views" label="浏览量" width="140" sortable />
        <el-table-column prop="createdAt" label="创建时间" width="240" />
        <el-table-column label="操作">
          <template #default="{ row }">
            <el-tooltip effect="dark" content="预览" placement="bottom">
              <el-icon size="18" @click="onOpen(ARTICLE_OPS_TYPE.preview, row.id)"
                ><View
              /></el-icon>
            </el-tooltip>

            <el-tooltip effect="dark" content="编辑" placement="bottom">
              <el-icon size="18" @click="onOpen(ARTICLE_OPS_TYPE.edit, row.id)"><Edit /></el-icon>
            </el-tooltip>

            <el-tooltip effect="dark" content="删除" placement="bottom">
              <el-icon size="18" @click="onDel(row.id)"><Delete /></el-icon>
            </el-tooltip>
          </template>
        </el-table-column>
      </el-table>
      <template #footer>
        <el-pagination
          :current-page="query.current"
          @current-change="onCurrentChange"
          background
          layout="prev, pager, next"
          :total="total"
        />
      </template>
    </el-card>
    <el-drawer v-model="drawer" size="45%" :title="activeTitle" direction="rtl">
      <ArticleOps
        :opsType="currentOpsType"
        :opsId="articleId"
        :key="articleId"
        @finish="onFinish"
        @delete="onDelete"
      />
    </el-drawer>
  </div>
</template>

<script setup lang="ts">
import { ArrowRight, Delete, Edit, Plus } from '@element-plus/icons-vue'
import { getArticleApi, deleteArticleApi } from '@/api/article'
import { type Query } from '@/types/allType'
import { reactive } from 'vue'
import { ref, computed } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import ArticleOps from './components/ArticleOps.vue'
import { ARTICLE_OPS_TYPE } from '@/constant/article.ts'
const drawer = ref(false)

const TITLE_MAP = {
  add: '新增',
  preview: '预览',
  edit: '编辑',
}
function onClose() {
  drawer.value = false
}
function onDelete() {
  onClose()
}
function onFinish() {
  onClose()
  articleApi(query)
}
const articleId = ref('')
const activeTitle = computed(() => {
  return TITLE_MAP[currentOpsType.value] + '文章'
})
const currentOpsType = ref()
function onOpen(opsType: string, id: string) {
  currentOpsType.value = opsType
  drawer.value = true
  articleId.value = id
}
async function onDel(id: number) {
  try {
    await ElMessageBox.confirm('确认删除吗?', '警告', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning',
    })
    await deleteArticleApi(id)
    ElMessage.success('删除成功')
    await articleApi(query)
  } catch (e) {}
}
function onCurrentChange(newvalue: any) {
  query.current = newvalue
  articleApi(query)
}
const query = reactive({
  current: 1,
  pageSize: 10,
} as Query)
const articleList = ref([])
const total = ref()
articleApi(query)

async function articleApi(query: Query) {
  const api = (await getArticleApi(query)) as any
  articleList.value = api.rows
  total.value = api.total
}
</script>

<style scoped lang="scss">
.article-box {
  .el-card {
    margin-top: 25px;
    .card-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      height: 40px;
    }
    .el-pagination {
      display: flex;
      justify-content: center;
    }
    :deep(.el-card__footer) {
      border-top: none;
      margin-top: -15px;
    }
    .el-table {
      .el-icon:nth-child(2) {
        margin: 0 15px;
      }
    }
  }
}
</style>
html 复制代码
<script setup lang="ts">
import { ARTICLE_OPS_TYPE } from '@/constant/article.ts'
import { getArticleDetail } from '@/api/article'
import { reactive } from 'vue'
import { postArticleApi, putArticleApi } from '@/api/article'
import { ElMessage } from 'element-plus'

const emit = defineEmits(['finish', 'delete'])
const props = defineProps({
  opsType: {
    type: String,
    default: '',
  },
  opsId: {
    type: String,
    default: '',
  },
})
const articleForm = reactive({
  id: '',
  stem: '',
  content: '',
})
getDetail()
async function getDetail() {
  if (props.opsId) {
    const req = (await getArticleDetail(props.opsId)) as any
    articleForm.id = req.id
    articleForm.stem = req.stem
    articleForm.content = req.content
  } else {
    return
  }
}
function deleteSubmit() {
  emit('delete')
}
async function submit() {
  if (props.opsId) {
    await putArticleApi(articleForm)
  } else {
    await postArticleApi(articleForm)
    articleForm.stem = ''
    articleForm.content = ''
  }

  ElMessage.success(props.opsId ? '编辑成功' : '新增成功')
  emit('finish')
}
</script>

<template>
  <div>
    <el-form lable-width="55px" v-if="props.opsType !== ARTICLE_OPS_TYPE.preview">
      <el-form-item label="标题">
        <el-input placeholder="请输入文章标题" v-model="articleForm.stem" />
      </el-form-item>
      <el-form-item label="内容">
        <QuillEditor
          theme="snow"
          toolbar="full"
          placeholder="请输入文章内容"
          v-model:content="articleForm.content"
          contentType="html"
        />
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="submit">确认</el-button>
        <el-button @click="deleteSubmit">取消</el-button>
      </el-form-item>
    </el-form>
    <!-- 预览 -->
    <div class="preview" v-else>
      <h4>{{ articleForm.stem }}</h4>
      <div v-html="articleForm.content"></div>
    </div>
  </div>
</template>

<style scoped lang="scss">
.el-form {
  .el-input {
    height: 36px;
  }
  :deep(.ql-container) {
    width: 100%;
    min-height: 200px;
  }
  .el-form-item:last-child {
    margin-top: 100px;
  }
}
</style>

同时在api中定义

TypeScript 复制代码
function getArticleDetail(id: string) {
  return instance({
    method: 'get',
    url: `/article/${id}`,
  })
}
function postArticleApi(articleForm: articleForm) {
  return instance({
    method: 'post',
    url: '/article',
    data: articleForm,
  })
}
function putArticleApi(articleForm: articleForm) {
  return instance({
    method: 'put',
    url: `/article/${articleForm.id}`,
    data: articleForm,
  })

这里使用到了富文本需要安装,并注册

TypeScript 复制代码
npm install @vueup/vue-quill@latest
TypeScript 复制代码
import { QuillEditor } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.snow.css'
app.component('QuillEditor', QuillEditor)

最后,需要接口文档的也可以找我

相关推荐
万少1 天前
DeepSeek 昨晚刚开源了 Harness:附万少的2 万字保姆级教程
前端·后端·架构
程序员黑豆1 天前
Java字符串详解
java·前端·ai编程
mCell1 天前
DeepSeek Harness 速览:“一切皆插件”意味着什么
typescript·agent·deepseek
无我Code1 天前
开发者-2026年中总结
前端·面试·程序员
To_OC1 天前
LC 560 和为 K 的子数组:前缀和配哈希表,这对组合我是真的服了
javascript·算法·程序员
浮生望1 天前
React 受控与非受控组件:从表单状态管理到实时校验的完整实践
前端
明朝百晓生1 天前
Deep RL learning[2026/8]
开发语言·javascript·人工智能
糖墨夕1 天前
第二章:AI Agent 到底是个啥?—— 用生活场景把概念讲明白
前端
Csvn1 天前
⚡ content-visibility: auto —— 长页面渲染提速的隐藏神器,白捡的性能优化
前端
用户938515635071 天前
ESLint 代码规范完全指南——从 AST 原理到 flat config 逐行解析
javascript·后端·代码规范