大事件管理系統(tǒng) -- 黑馬

一荸百、目錄結構

image.png

image.png

二挫望、各目錄代碼內容

api/artaicle.js :

import request from '@/utils/request'
// 分類:獲取文章分類
export const artGetChannelsService = () => request.get('/my/cate/list')
// 分類:添加文章分類
export const artAddChannelService = (data) => request.post('/my/cate/add', data)
// 分類:編輯文章分類
export const artEditChannelService = (data) =>
  request.put('/my/cate/info', data)
// 分類:刪除文章分類
export const artDelChannelService = (id) =>
  request.delete('/my/cate/del', {
    params: { id }
  })

// 文章:獲取文章列表
export const artGetListService = (params) =>
  request.get('/my/article/list', {
    params
  })

// 文章:添加文章
// 注意:data需要是一個formData格式的對象
export const artPublishService = (data) => request.post('/my/article/add', data)

// 文章:獲取文章詳情
export const artGetDetailService = (id) =>
  request.get('/my/article/info', {
    params: { id }
  })

// 文章:編輯文章接口
export const artEditService = (data) => request.put('/my/article/info', data)

// 文章:刪除文章接口
export const artDelService = (id) =>
  request.delete('/my/article/info', { params: { id } })

api/user.js

import request from '@/utils/request'

// 注冊接口
export const userRegisterService = ({ username, password, repassword }) =>
  request.post('/api/reg', { username, password, repassword })

// 登錄接口
export const userLoginService = ({ username, password }) =>
  request.post('/api/login', { username, password })

// 獲取用戶基本信息
export const userGetInfoService = () => request.get('/my/userinfo')

// 更新用戶基本信息
export const userUpdateInfoService = ({ id, nickname, email }) =>
  request.put('/my/userinfo', { id, nickname, email })

// 更新用戶頭像
export const userUpdateAvatarService = (avatar) =>
  request.patch('/my/update/avatar', { avatar })

// 更新用戶密碼
export const userUpdatePasswordService = ({ old_pwd, new_pwd, re_pwd }) =>
  request.patch('/my/updatepwd', { old_pwd, new_pwd, re_pwd })

assets/main.js

body {
  margin: 0;
  background-color: #f5f5f5;
}

/* fade-slide */
.fade-slide-leave-active,
.fade-slide-enter-active {
  transition: all 0.3s;
}

.fade-slide-enter-from {
  transform: translateX(-30px);
  opacity: 0;
}

.fade-slide-leave-to {
  transform: translateX(30px);
  opacity: 0;
}

components/PageContainer.vue

<script setup>
defineProps({
  title: {
    required: true,
    type: String
  }
})
</script>

<template>
  <el-card class="page-container">
    <template #header>
      <div class="header">
        <span>{{ title }}</span>
        <div class="extra">
          <slot name="extra"></slot>
        </div>
      </div>
    </template>
    <slot></slot>
  </el-card>
</template>

<style lang="scss" scoped>
.page-container {
  min-height: 100%;
  box-sizing: border-box;
  .header {
    display: flex;
    justify-content: space-between;
    align-items: center;
  }
}
</style>

components/TestDemo.vue

<template>
  <div>我是test測試的組件</div>
</template>

router/index.js

import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/stores'

// createRouter 創(chuàng)建路由實例
// 配置 history 模式
// 1. history模式:createWebHistory     地址欄不帶 #
// 2. hash模式:   createWebHashHistory 地址欄帶 #
// console.log(import.meta.env.DEV)

// vite 中的環(huán)境變量 import.meta.env.BASE_URL  就是 vite.config.js 中的 base 配置項
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    { path: '/login', component: () => import('@/views/login/LoginPage.vue') }, // 登錄頁
    {
      path: '/',
      component: () => import('@/views/layout/LayoutContainer.vue'),
      redirect: '/article/manage',
      children: [
        {
          path: '/article/manage',
          component: () => import('@/views/article/ArticleManage.vue')
        },
        {
          path: '/article/channel',
          component: () => import('@/views/article/ArticleChannel.vue')
        },
        {
          path: '/user/profile',
          component: () => import('@/views/user/UserProfile.vue')
        },
        {
          path: '/user/avatar',
          component: () => import('@/views/user/UserAvatar.vue')
        },
        {
          path: '/user/password',
          component: () => import('@/views/user/UserPassword.vue')
        }
      ]
    }
  ]
})

// 登錄訪問攔截 => 默認是直接放行的
// 根據返回值決定埋虹,是放行還是攔截
// 返回值:
// 1. undefined / true  直接放行
// 2. false 攔回from的地址頁面
// 3. 具體路徑 或 路徑對象  攔截到對應的地址
//    '/login'   { name: 'login' }
router.beforeEach((to) => {
  // 如果沒有token, 且訪問的是非登錄頁霜大,攔截到登錄姻成,其他情況正常放行
  const useStore = useUserStore()
  if (!useStore.token && to.path !== '/login') return '/login'
})

export default router

stores/modules/counter.js

import { defineStore } from 'pinia'
import { ref } from 'vue'

// 數(shù)字計數(shù)器模塊
export const useCountStore = defineStore('big-count', () => {
  const count = ref(100)
  const add = (n) => {
    count.value += n
  }
  return {
    count,
    add
  }
})

stores/modules/user.js

import { defineStore } from 'pinia'
import { ref } from 'vue'
import { userGetInfoService } from '../../api/user'

// 用戶模塊 token setToken removeToken
export const useUserStore = defineStore(
  'big-user',
  () => {
    const token = ref('')
    const setToken = (newToken) => {
      token.value = newToken
    }
    const removeToken = () => {
      token.value = ''
    }

    const user = ref({})
    const getUser = async () => {
      const res = await userGetInfoService() // 請求獲取數(shù)據
      user.value = res.data.data
    }
    const setUser = (obj) => {
      user.value = obj
    }

    return {
      token,
      setToken,
      removeToken,
      user,
      getUser,
      setUser
    }
  },
  {
    persist: true
  }
)

stores/index.js

import { createPinia } from 'pinia'
import persist from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(persist)

export default pinia
export * from './modules/user'
export * from './modules/counter'

// import { useUserStore } from './modules/user'
// export { useUserStore }
// import { useCountStore } from './modules/counter'
// export { useCountStore }

utils/format.js

import { dayjs } from 'element-plus'

export const formatTime = (time) => dayjs(time).format('YYYY年MM月DD日')

utils/request.js

import axios from 'axios'
import { useUserStore } from '@/stores'
import { ElMessage } from 'element-plus'
import router from '@/router'
const baseURL = 'http://big-event-vue-api-t.itheima.net'

const instance = axios.create({
  // TODO 1. 基礎地址,超時時間
  baseURL,
  timeout: 10000
})

// 請求攔截器
instance.interceptors.request.use(
  (config) => {
    // TODO 2. 攜帶token
    const useStore = useUserStore()
    if (useStore.token) {
      config.headers.Authorization = useStore.token
    }
    return config
  },
  (err) => Promise.reject(err)
)

// 響應攔截器
instance.interceptors.response.use(
  (res) => {
    // TODO 4. 摘取核心響應數(shù)據
    if (res.data.code === 0) {
      return res
    }
    // TODO 3. 處理業(yè)務失敗
    // 處理業(yè)務失敗, 給錯誤提示等限,拋出錯誤
    ElMessage.error(res.data.message || '服務異常')
    return Promise.reject(res.data)
  },
  (err) => {
    // TODO 5. 處理401錯誤
    // 錯誤的特殊情況 => 401 權限不足 或 token 過期 => 攔截到登錄
    if (err.response?.status === 401) {
      router.push('/login')
    }

    // 錯誤的默認情況 => 只要給提示
    ElMessage.error(err.response.data.message || '服務異常')
    return Promise.reject(err)
  }
)

export default instance
export { baseURL }

views/article/components/ArticleEdit.vue

<script setup>
import { ref } from 'vue'
import ChannelSelect from './ChannelSelect.vue'
import { Plus } from '@element-plus/icons-vue'
import { QuillEditor } from '@vueup/vue-quill'
import '@vueup/vue-quill/dist/vue-quill.snow.css'
import {
  artPublishService,
  artGetDetailService,
  artEditService
} from '@/api/article'
import { baseURL } from '@/utils/request'
import axios from 'axios'
// 控制抽屜顯示隱藏
const visibleDrawer = ref(false)

// 默認數(shù)據
const defaultForm = {
  title: '', // 標題
  cate_id: '', // 分類id
  cover_img: '', // 封面圖片 file 對象
  content: '', // string 內容
  state: '' // 狀態(tài)
}

// 準備數(shù)據
const formModel = ref({ ...defaultForm })

// 圖片上傳相關邏輯
const imgUrl = ref('')
const onSelectFile = (uploadFile) => {
  imgUrl.value = URL.createObjectURL(uploadFile.raw) // 預覽圖片
  // 立刻將圖片對象屋彪,存入 formModel.value.cover_img 將來用于提交
  formModel.value.cover_img = uploadFile.raw
}

// 提交
const emit = defineEmits(['success'])
const onPublish = async (state) => {
  // 將已發(fā)布還是草稿狀態(tài),存入 formModel
  formModel.value.state = state

  // 注意:當前接口握玛,需要的是 formData 對象
  // 將普通對象 => 轉換成 => formData對象
  const fd = new FormData()
  for (let key in formModel.value) {
    fd.append(key, formModel.value[key])
  }

  // 發(fā)請求
  if (formModel.value.id) {
    // 編輯操作
    await artEditService(fd)
    ElMessage.success('修改成功')
    visibleDrawer.value = false
    emit('success', 'edit')
  } else {
    // 添加操作
    await artPublishService(fd)
    ElMessage.success('添加成功')
    visibleDrawer.value = false
    // 通知到父組件够傍,添加成功了
    emit('success', 'add')
  }
}

// 組件對外暴露一個方法 open,基于open傳來的參數(shù)挠铲,區(qū)分添加還是編輯
// open({})  => 表單無需渲染冕屯,說明是添加
// open({ id, ..., ... })  => 表單需要渲染,說明是編輯
// open調用后拂苹,可以打開抽屜
const editorRef = ref()
const open = async (row) => {
  visibleDrawer.value = true // 顯示抽屜

  if (row.id) {
    // 需要基于 row.id 發(fā)送請求安聘,獲取編輯對應的詳情數(shù)據,進行回顯
    const res = await artGetDetailService(row.id)
    formModel.value = res.data.data
    // 圖片需要單獨處理回顯
    imgUrl.value = baseURL + formModel.value.cover_img
    // 注意:提交給后臺,需要的數(shù)據格式浴韭,是file對象格式
    // 需要將網絡圖片地址 => 轉換成 file對象丘喻,存儲起來, 將來便于提交
    const file = await imageUrlToFileObject(
      imgUrl.value,
      formModel.value.cover_img
    )
    formModel.value.cover_img = file
  } else {
    formModel.value = { ...defaultForm } // 基于默認的數(shù)據,重置form數(shù)據
    // 這里重置了表單的數(shù)據念颈,但是圖片上傳img地址泉粉,富文本編輯器內容 => 需要手動重置
    imgUrl.value = ''
    editorRef.value.setHTML('')
  }
}

// 將網絡圖片地址轉換為 File 對象的函數(shù)
async function imageUrlToFileObject(imageUrl, filename) {
  try {
    // 使用 Axios 下載圖片數(shù)據
    const response = await axios.get(imageUrl, { responseType: 'arraybuffer' })

    // 將下載的數(shù)據轉換成 Blob 對象
    const blob = new Blob([response.data], {
      type: response.headers['content-type']
    })

    // 創(chuàng)建 File 對象
    const file = new File([blob], filename, {
      type: response.headers['content-type']
    })

    return file
  } catch (error) {
    console.error('Error converting image URL to File object:', error)
    return null
  }
}

defineExpose({
  open
})
</script>

<template>
  <el-drawer
    v-model="visibleDrawer"
    :title="formModel.id ? '編輯文章' : '添加文章'"
    direction="rtl"
    size="50%"
  >
    <!-- 發(fā)表文章表單 -->
    <el-form :model="formModel" ref="formRef" label-width="100px">
      <el-form-item label="文章標題" prop="title">
        <el-input v-model="formModel.title" placeholder="請輸入標題"></el-input>
      </el-form-item>
      <el-form-item label="文章分類" prop="cate_id">
        <channel-select
          v-model="formModel.cate_id"
          width="100%"
        ></channel-select>
      </el-form-item>
      <el-form-item label="文章封面" prop="cover_img">
        <!-- 此處需要關閉 element-plus 的自動上傳,不需要配置 action 等參數(shù)
             只需要做前端的本地預覽圖片即可榴芳,無需在提交前上傳圖標
             語法:URL.createObjectURL(...) 創(chuàng)建本地預覽的地址嗡靡,來預覽
        -->
        <el-upload
          class="avatar-uploader"
          :show-file-list="false"
          :auto-upload="false"
          :on-change="onSelectFile"
        >
          <img v-if="imgUrl" :src="imgUrl" class="avatar" />
          <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
        </el-upload>
      </el-form-item>
      <el-form-item label="文章內容" prop="content">
        <div class="editor">
          <quill-editor
            ref="editorRef"
            v-model:content="formModel.content"
            content-type="html"
            theme="snow"
          ></quill-editor>
        </div>
      </el-form-item>
      <el-form-item>
        <el-button @click="onPublish('已發(fā)布')" type="primary">發(fā)布</el-button>
        <el-button @click="onPublish('草稿')" type="info">草稿</el-button>
      </el-form-item>
    </el-form>
  </el-drawer>
</template>

<style lang="scss" scoped>
.avatar-uploader {
  :deep() {
    .avatar {
      width: 178px;
      height: 178px;
      display: block;
    }
    .el-upload {
      border: 1px dashed var(--el-border-color);
      border-radius: 6px;
      cursor: pointer;
      position: relative;
      overflow: hidden;
      transition: var(--el-transition-duration-fast);
    }
    .el-upload:hover {
      border-color: var(--el-color-primary);
    }
    .el-icon.avatar-uploader-icon {
      font-size: 28px;
      color: #8c939d;
      width: 178px;
      height: 178px;
      text-align: center;
    }
  }
}

.editor {
  width: 100%;
  :deep(.ql-editor) {
    min-height: 200px;
  }
}
</style>

views/article/components/channelEdit.vue

<script setup>
import { ref } from 'vue'
import { artEditChannelService, artAddChannelService } from '@/api/article.js'
const dialogVisible = ref(false)
const formRef = ref()
const formModel = ref({
  cate_name: '',
  cate_alias: ''
})
const rules = {
  cate_name: [
    { required: true, message: '請輸入分類名稱', trigger: 'blur' },
    {
      pattern: /^\S{1,10}$/,
      message: '分類名必須是 1-10 位的非空字符',
      trigger: 'blur'
    }
  ],
  cate_alias: [
    { required: true, message: '請輸入分類別名', trigger: 'blur' },
    {
      pattern: /^[a-zA-Z0-9]{1,15}$/,
      message: '分類名必須是 1-15 位的字母或數(shù)字',
      trigger: 'blur'
    }
  ]
}

const emit = defineEmits(['success'])
const onSubmit = async () => {
  await formRef.value.validate()
  const isEdit = formModel.value.id
  if (isEdit) {
    await artEditChannelService(formModel.value)
    ElMessage.success('編輯成功')
  } else {
    await artAddChannelService(formModel.value)
    ElMessage.success('添加成功')
  }
  dialogVisible.value = false
  emit('success')
}

// 組件對外暴露一個方法 open,基于open傳來的參數(shù)窟感,區(qū)分添加還是編輯
// open({})  => 表單無需渲染叽躯,說明是添加
// open({ id, cate_name, ... })  => 表單需要渲染,說明是編輯
// open調用后肌括,可以打開彈窗
const open = (row) => {
  dialogVisible.value = true
  formModel.value = { ...row } // 添加 → 重置了表單內容点骑,編輯 → 存儲了需要回顯的數(shù)據
}

// 向外暴露方法
defineExpose({
  open
})
</script>

<template>
  <el-dialog
    v-model="dialogVisible"
    :title="formModel.id ? '編輯分類' : '添加分類'"
    width="30%"
  >
    <el-form
      ref="formRef"
      :model="formModel"
      :rules="rules"
      label-width="100px"
      style="padding-right: 30px"
    >
      <el-form-item label="分類名稱" prop="cate_name">
        <el-input
          v-model="formModel.cate_name"
          placeholder="請輸入分類名稱"
        ></el-input>
      </el-form-item>
      <el-form-item label="分類別名" prop="cate_alias">
        <el-input
          v-model="formModel.cate_alias"
          placeholder="請輸入分類別名"
        ></el-input>
      </el-form-item>
    </el-form>
    <template #footer>
      <span class="dialog-footer">
        <el-button @click="dialogVisible = false">取消</el-button>
        <el-button type="primary" @click="onSubmit"> 確認 </el-button>
      </span>
    </template>
  </el-dialog>
</template>

views/article/components/ChannelSelect.vue

<script setup>
import { artGetChannelsService } from '@/api/article.js'
import { ref } from 'vue'

defineProps({
  modelValue: {
    type: [Number, String]
  },
  width: {
    type: String
  }
})
const emit = defineEmits(['update:modelValue'])
const channelList = ref([])
const getChannelList = async () => {
  const res = await artGetChannelsService()
  channelList.value = res.data.data
}
getChannelList()
</script>

<template>
  <!-- label 展示給用戶看的,value 收集起來提交給后臺的 -->
  <el-select
    :modelValue="modelValue"
    @update:modelValue="emit('update:modelValue', $event)"
    :style="{ width }"
  >
    <el-option
      v-for="channel in channelList"
      :key="channel.id"
      :label="channel.cate_name"
      :value="channel.id"
    ></el-option>
  </el-select>
</template>

views/article/ArticleChannel.vue

<script setup>
import { ref } from 'vue'
import { Edit, Delete } from '@element-plus/icons-vue'
import { artGetChannelsService, artDelChannelService } from '../../api/article'
import ChannelEdit from './components/ChannelEdit.vue'
const channelList = ref([])
const loading = ref(false)
const dialog = ref()

const getChannelList = async () => {
  loading.value = true
  const res = await artGetChannelsService()
  channelList.value = res.data.data
  loading.value = false
}
getChannelList()

const onDelChannel = async (row) => {
  await ElMessageBox.confirm('你確認要刪除該分類么', '溫馨提示', {
    type: 'warning',
    confirmButtonText: '確認',
    cancelButtonText: '取消'
  })
  await artDelChannelService(row.id)
  ElMessage.success('刪除成功')
  getChannelList()
}
const onEditChannel = (row) => {
  dialog.value.open(row)
}
const onAddChannel = () => {
  dialog.value.open({})
}
const onSuccess = () => {
  getChannelList()
}
</script>

<template>
  <page-container title="文章分類">
    <template #extra>
      <el-button @click="onAddChannel">添加分類</el-button>
    </template>

    <el-table v-loading="loading" :data="channelList" style="width: 100%">
      <el-table-column type="index" label="序號" width="100"></el-table-column>
      <el-table-column prop="cate_name" label="分類名稱"></el-table-column>
      <el-table-column prop="cate_alias" label="分類別名"></el-table-column>
      <el-table-column label="操作" width="150">
        <!-- row 就是 channelList 的一項谍夭, $index 下標 -->
        <template #default="{ row, $index }">
          <el-button
            :icon="Edit"
            circle
            plain
            type="primary"
            @click="onEditChannel(row, $index)"
          ></el-button>
          <el-button
            :icon="Delete"
            circle
            plain
            type="danger"
            @click="onDelChannel(row, $index)"
          ></el-button>
        </template>
      </el-table-column>

      <template #empty>
        <el-empty description="沒有數(shù)據"></el-empty>
      </template>
    </el-table>

    <channel-edit ref="dialog" @success="onSuccess"></channel-edit>
  </page-container>
</template>

<style lang="scss" scoped></style>

views/article/ArticleManage.vue

<script setup>
import { ref } from 'vue'
import { Delete, Edit } from '@element-plus/icons-vue'
import ChannelSelect from './components/ChannelSelect.vue'
import ArticleEdit from './components/ArticleEdit.vue'
import { artGetListService, artDelService } from '@/api/article.js'
import { formatTime } from '@/utils/format.js'
const articleList = ref([]) // 文章列表
const total = ref(0) // 總條數(shù)
const loading = ref(false) // loading狀態(tài)

// 定義請求參數(shù)對象
const params = ref({
  pagenum: 1, // 當前頁
  pagesize: 5, // 當前生效的每頁條數(shù)
  cate_id: '',
  state: ''
})

// 基于params參數(shù)黑滴,獲取文章列表
const getArticleList = async () => {
  loading.value = true

  const res = await artGetListService(params.value)
  articleList.value = res.data.data
  total.value = res.data.total

  loading.value = false
}
getArticleList()

// 處理分頁邏輯
const onSizeChange = (size) => {
  // console.log('當前每頁條數(shù)', size)
  // 只要是每頁條數(shù)變化了,那么原本正在訪問的當前頁意義不大了紧索,數(shù)據大概率已經不在原來那一頁了
  // 重新從第一頁渲染即可
  params.value.pagenum = 1
  params.value.pagesize = size
  // 基于最新的當前頁 和 每頁條數(shù)袁辈,渲染數(shù)據
  getArticleList()
}
const onCurrentChange = (page) => {
  // 更新當前頁
  params.value.pagenum = page
  // 基于最新的當前頁,渲染數(shù)據
  getArticleList()
}

// 搜索邏輯 => 按照最新的條件珠漂,重新檢索晚缩,從第一頁開始展示
const onSearch = () => {
  params.value.pagenum = 1 // 重置頁面
  getArticleList()
}

// 重置邏輯 => 將篩選條件清空,重新檢索媳危,從第一頁開始展示
const onReset = () => {
  params.value.pagenum = 1 // 重置頁面
  params.value.cate_id = ''
  params.value.state = ''
  getArticleList()
}

const articleEditRef = ref()
// 添加邏輯
const onAddArticle = () => {
  articleEditRef.value.open({})
}
// 編輯邏輯
const onEditArticle = (row) => {
  articleEditRef.value.open(row)
}

// 刪除邏輯
const onDeleteArticle = async (row) => {
  // 提示用戶是否要刪除
  await ElMessageBox.confirm('此操作將永久刪除該文件, 是否繼續(xù)?', '提示', {
    confirmButtonText: '確定',
    cancelButtonText: '取消',
    type: 'warning'
  })
  await artDelService(row.id)
  ElMessage.success('刪除成功')
  // 重新渲染列表
  getArticleList()
}

// 添加或者編輯 成功的回調
const onSuccess = (type) => {
  if (type === 'add') {
    // 如果是添加荞彼,最好渲染最后一頁
    const lastPage = Math.ceil((total.value + 1) / params.value.pagesize)
    // 更新成最大頁碼數(shù),再渲染
    params.value.pagenum = lastPage
  }

  getArticleList()
}
</script>

<template>
  <page-container title="文章管理">
    <template #extra>
      <el-button type="primary" @click="onAddArticle">添加文章</el-button>
    </template>

    <!-- 表單區(qū)域 -->
    <el-form inline>
      <el-form-item label="文章分類:">
        <!-- Vue2 => v-model :value 和 @input 的簡寫 -->
        <!-- Vue3 => v-model :modelValue 和 @update:modelValue 的簡寫 -->
        <channel-select v-model="params.cate_id"></channel-select>

        <!-- Vue3 => v-model:cid  :cid 和 @update:cid 的簡寫 -->
        <!-- <channel-select v-model:cid="params.cate_id"></channel-select> -->
      </el-form-item>
      <el-form-item label="發(fā)布狀態(tài):">
        <!-- 這里后臺標記發(fā)布狀態(tài)待笑,就是通過中文標記的鸣皂,已發(fā)布 / 草稿 -->
        <el-select v-model="params.state">
          <el-option label="已發(fā)布" value="已發(fā)布"></el-option>
          <el-option label="草稿" value="草稿"></el-option>
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button @click="onSearch" type="primary">搜索</el-button>
        <el-button @click="onReset">重置</el-button>
      </el-form-item>
    </el-form>

    <!-- 表格區(qū)域 -->
    <el-table :data="articleList" v-loading="loading">
      <el-table-column label="文章標題" prop="title">
        <template #default="{ row }">
          <el-link type="primary" :underline="false">{{ row.title }}</el-link>
        </template>
      </el-table-column>
      <el-table-column label="分類" prop="cate_name"></el-table-column>
      <el-table-column label="發(fā)表時間" prop="pub_date">
        <template #default="{ row }">
          {{ formatTime(row.pub_date) }}
        </template>
      </el-table-column>
      <el-table-column label="狀態(tài)" prop="state"></el-table-column>
      <!-- 利用作用域插槽 row 可以獲取當前行的數(shù)據 => v-for 遍歷 item -->
      <el-table-column label="操作">
        <template #default="{ row }">
          <el-button
            circle
            plain
            type="primary"
            :icon="Edit"
            @click="onEditArticle(row)"
          ></el-button>
          <el-button
            circle
            plain
            type="danger"
            :icon="Delete"
            @click="onDeleteArticle(row)"
          ></el-button>
        </template>
      </el-table-column>
    </el-table>

    <!-- 分頁區(qū)域 -->
    <el-pagination
      v-model:current-page="params.pagenum"
      v-model:page-size="params.pagesize"
      :page-sizes="[2, 3, 5, 10]"
      :background="true"
      layout="jumper, total, sizes, prev, pager, next"
      :total="total"
      @size-change="onSizeChange"
      @current-change="onCurrentChange"
      style="margin-top: 20px; justify-content: flex-end"
    />

    <!-- 添加編輯的抽屜 -->
    <article-edit ref="articleEditRef" @success="onSuccess"></article-edit>
  </page-container>
</template>

<style lang="scss" scoped></style>

views/layout/LayoutContainer.vue

<script setup>
import {
  Management,
  Promotion,
  UserFilled,
  User,
  Crop,
  EditPen,
  SwitchButton,
  CaretBottom
} from '@element-plus/icons-vue'
import avatar from '@/assets/default.png'
import { useUserStore } from '@/stores'
import { onMounted } from 'vue'
import { useRouter } from 'vue-router'
const userStore = useUserStore()
const router = useRouter()

onMounted(() => {
  userStore.getUser()
})

const handleCommand = async (key) => {
  if (key === 'logout') {
    // 退出操作
    await ElMessageBox.confirm('你確認要進行退出么', '溫馨提示', {
      type: 'warning',
      confirmButtonText: '確認',
      cancelButtonText: '取消'
    })

    // 清除本地的數(shù)據 (token + user信息)
    userStore.removeToken()
    userStore.setUser({})
    router.push('/login')
  } else {
    // 跳轉操作
    router.push(`/user/${key}`)
  }
}
</script>

<template>
  <!-- 
    el-menu 整個菜單組件
      :default-active="$route.path"  配置默認高亮的菜單項
      router  router選項開啟,el-menu-item 的 index 就是點擊跳轉的路徑

    el-menu-item 菜單項
      index="/article/channel" 配置的是訪問的跳轉路徑暮蹂,配合default-active的值寞缝,實現(xiàn)高亮
  -->
  <el-container class="layout-container">
    <el-aside width="200px">
      <div class="el-aside__logo"></div>
      <el-menu
        active-text-color="#ffd04b"
        background-color="#232323"
        :default-active="$route.path"
        text-color="#fff"
        router
      >
        <el-menu-item index="/article/channel">
          <el-icon><Management /></el-icon>
          <span>文章分類</span>
        </el-menu-item>
        <el-menu-item index="/article/manage">
          <el-icon><Promotion /></el-icon>
          <span>文章管理</span>
        </el-menu-item>

        <el-sub-menu index="/user">
          <!-- 多級菜單的標題 - 具名插槽 title -->
          <template #title>
            <el-icon><UserFilled /></el-icon>
            <span>個人中心</span>
          </template>

          <!-- 展開的內容 - 默認插槽 -->
          <el-menu-item index="/user/profile">
            <el-icon><User /></el-icon>
            <span>基本資料</span>
          </el-menu-item>
          <el-menu-item index="/user/avatar">
            <el-icon><Crop /></el-icon>
            <span>更換頭像</span>
          </el-menu-item>
          <el-menu-item index="/user/password">
            <el-icon><EditPen /></el-icon>
            <span>重置密碼</span>
          </el-menu-item>
        </el-sub-menu>
      </el-menu>
    </el-aside>
    <el-container>
      <el-header>
        <div>
          黑馬程序員:<strong>{{
            userStore.user.nickname || userStore.user.username
          }}</strong>
        </div>
        <el-dropdown placement="bottom-end" @command="handleCommand">
          <!-- 展示給用戶,默認看到的 -->
          <span class="el-dropdown__box">
            <el-avatar :src="userStore.user.user_pic || avatar" />
            <el-icon><CaretBottom /></el-icon>
          </span>

          <!-- 折疊的下拉部分 -->
          <template #dropdown>
            <el-dropdown-menu>
              <el-dropdown-item command="profile" :icon="User"
                >基本資料</el-dropdown-item
              >
              <el-dropdown-item command="avatar" :icon="Crop"
                >更換頭像</el-dropdown-item
              >
              <el-dropdown-item command="password" :icon="EditPen"
                >重置密碼</el-dropdown-item
              >
              <el-dropdown-item command="logout" :icon="SwitchButton"
                >退出登錄</el-dropdown-item
              >
            </el-dropdown-menu>
          </template>
        </el-dropdown>
      </el-header>
      <el-main>
        <router-view></router-view>
      </el-main>
      <el-footer>大事件 ?2023 Created by 黑馬程序員</el-footer>
    </el-container>
  </el-container>
</template>

<style lang="scss" scoped>
.layout-container {
  height: 100vh;
  .el-aside {
    background-color: #232323;
    &__logo {
      height: 120px;
      background: url('@/assets/logo.png') no-repeat center / 120px auto;
    }
    .el-menu {
      border-right: none;
    }
  }
  .el-header {
    background-color: #fff;
    display: flex;
    align-items: center;
    justify-content: space-between;
    .el-dropdown__box {
      display: flex;
      align-items: center;
      .el-icon {
        color: #999;
        margin-left: 10px;
      }

      &:active,
      &:focus {
        outline: none;
      }
    }
  }
  .el-footer {
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 14px;
    color: #666;
  }
}
</style>

views/logon/LoginPage.vue

<script setup>
import { userRegisterService, userLoginService } from '@/api/user.js'
import { User, Lock } from '@element-plus/icons-vue'
import { ref, watch } from 'vue'
import { useUserStore } from '@/stores'
import { useRouter } from 'vue-router'
const isRegister = ref(false)
const form = ref()

// 整個的用于提交的form數(shù)據對象
const formModel = ref({
  username: '',
  password: '',
  repassword: ''
})
// 整個表單的校驗規(guī)則
// 1. 非空校驗 required: true      message消息提示仰泻,  trigger觸發(fā)校驗的時機 blur change
// 2. 長度校驗 min:xx, max: xx
// 3. 正則校驗 pattern: 正則規(guī)則    \S 非空字符
// 4. 自定義校驗 => 自己寫邏輯校驗 (校驗函數(shù))
//    validator: (rule, value, callback)
//    (1) rule  當前校驗規(guī)則相關的信息
//    (2) value 所校驗的表單元素目前的表單值
//    (3) callback 無論成功還是失敗荆陆,都需要 callback 回調
//        - callback() 校驗成功
//        - callback(new Error(錯誤信息)) 校驗失敗
const rules = {
  username: [
    { required: true, message: '請輸入用戶名', trigger: 'blur' },
    { min: 5, max: 10, message: '用戶名必須是 5-10位 的字符', trigger: 'blur' }
  ],
  password: [
    { required: true, message: '請輸入密碼', trigger: 'blur' },
    {
      pattern: /^\S{6,15}$/,
      message: '密碼必須是 6-15位 的非空字符',
      trigger: 'blur'
    }
  ],
  repassword: [
    { required: true, message: '請輸入密碼', trigger: 'blur' },
    {
      pattern: /^\S{6,15}$/,
      message: '密碼必須是 6-15位 的非空字符',
      trigger: 'blur'
    },
    {
      validator: (rule, value, callback) => {
        // 判斷 value 和 當前 form 中收集的 password 是否一致
        if (value !== formModel.value.password) {
          callback(new Error('兩次輸入密碼不一致'))
        } else {
          callback() // 就算校驗成功,也需要callback
        }
      },
      trigger: 'blur'
    }
  ]
}

const register = async () => {
  // 注冊成功之前集侯,先進行校驗被啼,校驗成功 → 請求帜消,校驗失敗 → 自動提示
  await form.value.validate()
  await userRegisterService(formModel.value)
  ElMessage.success('注冊成功')
  isRegister.value = false
}

const userStore = useUserStore()
const router = useRouter()
const login = async () => {
  await form.value.validate()
  const res = await userLoginService(formModel.value)
  userStore.setToken(res.data.token)
  ElMessage.success('登錄成功')
  router.push('/')
}

// 切換的時候,重置表單內容
watch(isRegister, () => {
  formModel.value = {
    username: '',
    password: '',
    repassword: ''
  }
})
</script>

<template>
  <!-- 
    1. 結構相關
      el-row表示一行趟据,一行分成24份 
       el-col表示列  
       (1) :span="12"  代表在一行中券犁,占12份 (50%)
       (2) :span="6"   表示在一行中术健,占6份  (25%)
       (3) :offset="3" 代表在一行中汹碱,左側margin份數(shù)

       el-form 整個表單組件
       el-form-item 表單的一行 (一個表單域)
       el-input 表單元素(輸入框)
    2. 校驗相關
       (1) el-form => :model="ruleForm"      綁定的整個form的數(shù)據對象 { xxx, xxx, xxx }
       (2) el-form => :rules="rules"         綁定的整個rules規(guī)則對象  { xxx, xxx, xxx }
       (3) 表單元素 => v-model="ruleForm.xxx" 給表單元素,綁定form的子屬性
       (4) el-form-item => prop配置生效的是哪個校驗規(guī)則 (和rules中的字段要對應)
  -->
  <el-row class="login-page">
    <el-col :span="12" class="bg"></el-col>
    <el-col :span="6" :offset="3" class="form">
      <!-- 注冊相關表單 -->
      <el-form
        :model="formModel"
        :rules="rules"
        ref="form"
        size="large"
        autocomplete="off"
        v-if="isRegister"
      >
        <el-form-item>
          <h1>注冊</h1>
        </el-form-item>
        <el-form-item prop="username">
          <el-input
            v-model="formModel.username"
            :prefix-icon="User"
            placeholder="請輸入用戶名"
          ></el-input>
        </el-form-item>
        <el-form-item prop="password">
          <el-input
            v-model="formModel.password"
            :prefix-icon="Lock"
            type="password"
            placeholder="請輸入密碼"
          ></el-input>
        </el-form-item>
        <el-form-item prop="repassword">
          <el-input
            v-model="formModel.repassword"
            :prefix-icon="Lock"
            type="password"
            placeholder="請輸入再次密碼"
          ></el-input>
        </el-form-item>
        <el-form-item>
          <el-button
            @click="register"
            class="button"
            type="primary"
            auto-insert-space
          >
            注冊
          </el-button>
        </el-form-item>
        <el-form-item class="flex">
          <el-link type="info" :underline="false" @click="isRegister = false">
            ← 返回
          </el-link>
        </el-form-item>
      </el-form>

      <!-- 登錄相關表單 -->
      <el-form
        :model="formModel"
        :rules="rules"
        ref="form"
        size="large"
        autocomplete="off"
        v-else
      >
        <el-form-item>
          <h1>登錄</h1>
        </el-form-item>
        <el-form-item prop="username">
          <el-input
            v-model="formModel.username"
            :prefix-icon="User"
            placeholder="請輸入用戶名"
          ></el-input>
        </el-form-item>
        <el-form-item prop="password">
          <el-input
            v-model="formModel.password"
            name="password"
            :prefix-icon="Lock"
            type="password"
            placeholder="請輸入密碼"
          ></el-input>
        </el-form-item>
        <el-form-item class="flex">
          <div class="flex">
            <el-checkbox>記住我</el-checkbox>
            <el-link type="primary" :underline="false">忘記密碼荞估?</el-link>
          </div>
        </el-form-item>
        <el-form-item>
          <el-button
            @click="login"
            class="button"
            type="primary"
            auto-insert-space
            >登錄</el-button
          >
        </el-form-item>
        <el-form-item class="flex">
          <el-link type="info" :underline="false" @click="isRegister = true">
            注冊 →
          </el-link>
        </el-form-item>
      </el-form>
    </el-col>
  </el-row>
</template>

<style lang="scss" scoped>
.login-page {
  height: 100vh;
  background-color: #fff;
  .bg {
    background: url('@/assets/logo2.png') no-repeat 60% center / 240px auto,
      url('@/assets/login_bg.jpg') no-repeat center / cover;
    border-radius: 0 20px 20px 0;
  }
  .form {
    display: flex;
    flex-direction: column;
    justify-content: center;
    user-select: none;
    .title {
      margin: 0 auto;
    }
    .button {
      width: 100%;
    }
    .flex {
      width: 100%;
      display: flex;
      justify-content: space-between;
    }
  }
}
</style>

views/user/UserAvatar.vue

<script setup>
import { ref } from 'vue'
import { Plus, Upload } from '@element-plus/icons-vue'
import { useUserStore } from '@/stores'
import { userUpdateAvatarService } from '@/api/user'
const userStore = useUserStore()
const imgUrl = ref(userStore.user.user_pic)
const uploadRef = ref()
const onSelectFile = (uploadFile) => {
  // 基于 FileReader 讀取圖片做預覽
  const reader = new FileReader()
  reader.readAsDataURL(uploadFile.raw)
  reader.onload = () => {
    imgUrl.value = reader.result
  }
}

const onUpdateAvatar = async () => {
  // 發(fā)送請求更新頭像
  await userUpdateAvatarService(imgUrl.value)
  // userStore 重新渲染
  await userStore.getUser()
  // 提示用戶
  ElMessage.success('頭像更新成功')
}
</script>

<template>
  <page-container title="更換頭像">
    <el-upload
      ref="uploadRef"
      :auto-upload="false"
      class="avatar-uploader"
      :show-file-list="false"
      :on-change="onSelectFile"
    >
      <img v-if="imgUrl" :src="imgUrl" class="avatar" />
      <el-icon v-else class="avatar-uploader-icon"><Plus /></el-icon>
    </el-upload>

    <br />

    <el-button
      @click="uploadRef.$el.querySelector('input').click()"
      type="primary"
      :icon="Plus"
      size="large"
      >選擇圖片</el-button
    >
    <el-button
      @click="onUpdateAvatar"
      type="success"
      :icon="Upload"
      size="large"
      >上傳頭像</el-button
    >
  </page-container>
</template>

<style lang="scss" scoped>
.avatar-uploader {
  :deep() {
    .avatar {
      width: 278px;
      height: 278px;
      display: block;
    }
    .el-upload {
      border: 1px dashed var(--el-border-color);
      border-radius: 6px;
      cursor: pointer;
      position: relative;
      overflow: hidden;
      transition: var(--el-transition-duration-fast);
    }
    .el-upload:hover {
      border-color: var(--el-color-primary);
    }
    .el-icon.avatar-uploader-icon {
      font-size: 28px;
      color: #8c939d;
      width: 278px;
      height: 278px;
      text-align: center;
    }
  }
}
</style>

views/user/UserPassword.vue

<script setup>
import { ref } from 'vue'
import { userUpdatePasswordService } from '@/api/user'
import { useUserStore } from '@/stores'
import { useRouter } from 'vue-router'

const formRef = ref()
const pwdForm = ref({
  old_pwd: '',
  new_pwd: '',
  re_pwd: ''
})

const checkDifferent = (rule, value, callback) => {
  // 校驗新密碼和原密碼不能一樣
  if (value === pwdForm.value.old_pwd) {
    callback(new Error('新密碼不能與原密碼一樣'))
  } else {
    callback()
  }
}
const checkSameAsNewPwd = (rule, value, callback) => {
  // 校驗確認密碼必須和新密碼一樣
  if (value !== pwdForm.value.new_pwd) {
    callback(new Error('確認密碼必須和新密碼一樣'))
  } else {
    callback()
  }
}
const rules = ref({
  old_pwd: [
    { required: true, message: '請輸入原密碼', trigger: 'blur' },
    { min: 6, max: 15, message: '原密碼長度在6-15位之間', trigger: 'blur' }
  ],
  new_pwd: [
    { required: true, message: '請輸入新密碼', trigger: 'blur' },
    { min: 6, max: 15, message: '新密碼長度在6-15位之間', trigger: 'blur' },
    { validator: checkDifferent, trigger: 'blur' }
  ],
  re_pwd: [
    { required: true, message: '請再次輸入新密碼', trigger: 'blur' },
    { min: 6, max: 15, message: '確認密碼長度在6-15位之間', trigger: 'blur' },
    { validator: checkSameAsNewPwd, trigger: 'blur' }
  ]
})

const userStore = useUserStore()
const router = useRouter()

const submitForm = async () => {
  await formRef.value.validate()
  await userUpdatePasswordService(pwdForm.value)
  ElMessage.success('密碼修改成功')

  // 密碼修改成功后咳促,退出重新登錄
  // 清空本地存儲的 token 和 個人信息
  userStore.setToken('')
  userStore.setUser({})

  // 攔截登錄
  router.push('/login')
}

const resetForm = () => {
  formRef.value.resetFields()
}
</script>

<template>
  <page-container title="修改密碼">
    <el-row>
      <el-col :span="12">
        <el-form
          ref="formRef"
          :model="pwdForm"
          :rules="rules"
          label-width="100px"
        >
          <el-form-item label="原密碼" prop="old_pwd">
            <el-input v-model="pwdForm.old_pwd" show-password></el-input>
          </el-form-item>
          <el-form-item label="新密碼" prop="new_pwd">
            <el-input v-model="pwdForm.new_pwd" show-password></el-input>
          </el-form-item>
          <el-form-item label="確認密碼" prop="re_pwd">
            <el-input v-model="pwdForm.re_pwd" show-password></el-input>
          </el-form-item>
          <el-form-item>
            <el-button type="primary" @click="submitForm">修改密碼</el-button>
            <el-button @click="resetForm">重置</el-button>
          </el-form-item>
        </el-form></el-col
      >
    </el-row>
  </page-container>
</template>

views/user/UserProfile.vue

<script setup>
import PageContainer from '@/components/PageContainer.vue'
import { ref } from 'vue'
import { useUserStore } from '@/stores'
import { userUpdateInfoService } from '@/api/user'

const formRef = ref()

// 是在使用倉庫中數(shù)據的初始值 (無需響應式) 解構無問題
const {
  user: { email, id, nickname, username },
  getUser
} = useUserStore()

const form = ref({
  id,
  username,
  nickname,
  email
})

const rules = ref({
  nickname: [
    { required: true, message: '請輸入用戶昵稱', trigger: 'blur' },
    {
      pattern: /^\S{2,10}/,
      message: '昵稱長度在2-10個非空字符',
      trigger: 'blur'
    }
  ],
  email: [
    { required: true, message: '請輸入用戶郵箱', trigger: 'blur' },
    {
      type: 'email',
      message: '請輸入正確的郵箱格式',
      trigger: ['blur', 'change']
    }
  ]
})

const submitForm = async () => {
  // 等待校驗結果
  await formRef.value.validate()
  // 提交修改
  await userUpdateInfoService(form.value)
  // 通知 user 模塊,進行數(shù)據的更新
  getUser()
  // 提示用戶
  ElMessage.success('修改成功')
}
</script>
<template>
  <page-container title="基本資料">
    <!-- 表單部分 -->
    <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
      <el-form-item label="登錄名稱">
        <el-input v-model="form.username" disabled></el-input>
      </el-form-item>
      <el-form-item label="用戶昵稱" prop="nickname">
        <el-input v-model="form.nickname"></el-input>
      </el-form-item>
      <el-form-item label="用戶郵箱" prop="email">
        <el-input v-model="form.email"></el-input>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" @click="submitForm">提交修改</el-button>
      </el-form-item>
    </el-form>
  </page-container>
</template>

App.vue

<script setup>
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
</script>

<template>
  <div>
    <!-- App.vue只需要留一個路由出口 router-view即可 -->
    <el-config-provider :locale="zhCn">
      <router-view></router-view>
    </el-config-provider>
  </div>
</template>

<style scoped></style>

main.js

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import pinia from '@/stores/index'
import '@/assets/main.scss'

const app = createApp(App)
app.use(pinia)
app.use(router)
app.mount('#app')

.eslintrc.cjs

/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')

module.exports = {
  root: true,
  extends: [
    'plugin:vue/vue3-essential',
    'eslint:recommended',
    '@vue/eslint-config-prettier/skip-formatting'
  ],
  parserOptions: {
    ecmaVersion: 'latest'
  },
  rules: {
    // prettier專注于代碼的美觀度 (格式化工具)
    // 前置:
    // 1. 禁用格式化插件 prettier  format on save 關閉
    // 2. 安裝Eslint插件, 并配置保存時自動修復
    'prettier/prettier': [
      'warn',
      {
        singleQuote: true, // 單引號
        semi: false, // 無分號
        printWidth: 80, // 每行寬度至多80字符
        trailingComma: 'none', // 不加對象|數(shù)組最后逗號
        endOfLine: 'auto' // 換行符號不限制(win mac 不一致)
      }
    ],
    // ESLint關注于規(guī)范, 如果不符合規(guī)范勘伺,報錯
    'vue/multi-word-component-names': [
      'warn',
      {
        ignores: ['index'] // vue組件名稱多單詞組成(忽略index.vue)
      }
    ],
    'vue/no-setup-props-destructure': ['off'], // 關閉 props 解構的校驗 (props解構丟失響應式)
    // 添加未定義變量錯誤提示跪腹,create-vue@3.6.3 關閉,這里加上是為了支持下一個章節(jié)演示飞醉。
    'no-undef': 'error'
  },
  globals: {
    ElMessage: 'readonly',
    ElMessageBox: 'readonly',
    ElLoading: 'readonly'
  }
}

vite.config.js

import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    vue(),
    AutoImport({
      resolvers: [ElementPlusResolver()]
    }),
    Components({
      resolvers: [ElementPlusResolver()]
    })
  ],
  base: '/',
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
})
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末冲茸,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子缅帘,更是在濱河造成了極大的恐慌轴术,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件钦无,死亡現(xiàn)場離奇詭異逗栽,居然都是意外死亡,警方通過查閱死者的電腦和手機失暂,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進店門彼宠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人弟塞,你說我怎么就攤上這事凭峡。” “怎么了决记?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵想罕,是天一觀的道長。 經常有香客問我霉涨,道長按价,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任笙瑟,我火速辦了婚禮楼镐,結果婚禮上,老公的妹妹穿的比我還像新娘往枷。我一直安慰自己框产,他們只是感情好凄杯,可當我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著秉宿,像睡著了一般戒突。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上描睦,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天膊存,我揣著相機與錄音,去河邊找鬼忱叭。 笑死隔崎,一個胖子當著我的面吹牛,可吹牛的內容都是我干的韵丑。 我是一名探鬼主播爵卒,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼撵彻!你這毒婦竟也來了钓株?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤陌僵,失蹤者是張志新(化名)和其女友劉穎轴合,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體拾弃,經...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡值桩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了豪椿。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片奔坟。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖搭盾,靈堂內的尸體忽然破棺而出咳秉,到底是詐尸還是另有隱情,我是刑警寧澤鸯隅,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布澜建,位于F島的核電站,受9級特大地震影響蝌以,放射性物質發(fā)生泄漏炕舵。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一跟畅、第九天 我趴在偏房一處隱蔽的房頂上張望咽筋。 院中可真熱鬧,春花似錦徊件、人聲如沸奸攻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽睹耐。三九已至辐赞,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間硝训,已是汗流浹背响委。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留捎迫,地道東北人晃酒。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓表牢,卻偏偏與公主長得像窄绒,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子崔兴,可洞房花燭夜當晚...
    茶點故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內容