// D:\桌面\大学\大四上\vue3_vite_elementplus_admin\src\store\modules\info.js
import { defineStore } from "pinia";
import {
GetInfoList,
GetAllCategory,
ChangeInforStatus,
UploadFile,
CreateInfo,
UpdateInfo,
GetInfoDetail,
DeleteInfo,
BatchDeleteInfo,
} from "@/api/info";
export const useInfoStore = defineStore("info", () => {
function getInfoListAction(requestData) {
return new Promise((resolve, reject) => {
GetInfoList(requestData).then(resolve).catch(reject);
});
}
function getAllCategoryAction() {
return new Promise((resolve, reject) => {
GetAllCategory().then(resolve).catch(reject);
});
}
function changeInfoStatusAction(requestData) {
const { id, data } = requestData;
return new Promise((resolve, reject) => {
ChangeInforStatus(id, data)
.then((response) => {
resolve(response);
})
.catch((error) => {
reject(error);
});
});
}
function uploadFileAction(requestData = {}) {
return new Promise((resolve, reject) => {
UploadFile(requestData)
.then((response) => resolve(response))
.catch((error) => reject(error));
});
}
function createInfoAction(data = {}) {
return CreateInfo(data).then((res) => {
if (res.code !== 0)
return Promise.reject(new Error(res.msg || "创建失败"));
return res;
});
}
function updateInfoAction({ id, data }) {
return UpdateInfo(id, data).then((res) => {
if (res.code !== 0)
return Promise.reject(new Error(res.msg || "更新失败"));
return res;
});
}
function getInfoDetailAction(id) {
return new Promise((resolve, reject) => {
GetInfoDetail(id)
.then((response) => resolve(response))
.catch((error) => reject(error));
});
}
function deleteInfoAction(id) {
return new Promise((resolve, reject) => {
DeleteInfo(id)
.then((response) => resolve(response))
.catch((error) => reject(error));
});
}
function batchDeleteInfoAction(ids) {
return new Promise((resolve, reject) => {
BatchDeleteInfo(ids)
.then((response) => resolve(response))
.catch((error) => reject(error));
});
}
return {
getInfoListAction,
getAllCategoryAction,
changeInfoStatusAction,
uploadFileAction,
createInfoAction,
updateInfoAction,
getInfoDetailAction,
deleteInfoAction,
batchDeleteInfoAction,
};
});
// D:\桌面\大学\大四上\vue3_vite_elementplus_admin\src\utils\request.js
import axios from "axios";
import { ElMessage } from "element-plus";
import { getToken } from "@/utils/token";
import useStore from "@/store";
import router from "@/router";
// console.log("BASE_URL:", import.meta.env.VITE_APP_BASE_URL);
const instance = axios.create({
baseURL: "/devApi",
timeout: 20000,
});
// 请求拦截器
instance.interceptors.request.use(
function (config) {
const token = getToken();
if (token) {
config.headers["token"] = token;
}
return config;
},
function (error) {
return Promise.reject(error);
}
);
// 响应拦截器
instance.interceptors.response.use(
function (response) {
const date = response.data;
if (date.code == 0) {
return Promise.resolve(date);
} else {
// ElMessage.error(date.msg);
return Promise.reject(date);
}
},
function (error) {
console.log(" error ", error);
const response = error.request.response;
if (response) {
const errorData = JSON.parse(response);
if (errorData.message || errorData.msg) {
ElMessage.error(errorData.message || errorData.msg);
}
//系统强制退出(token失效,已经登出)
if (errorData.message === 999) {
const store = useStore();
store.app.updateToken("");
router.replace({ name: "Login" });
}
} else {
ElMessage.error("网络连接超时");
}
return Promise.reject(error);
}
);
export default instance;
<!-- D:\桌面\大学\大四上\vue3_vite_elementplus_admin\src\views\info\Detail.vue -->
<template>
<el-form label-width="90px" :model="form" :rules="rules" ref="formRef">
<el-form-item label="类别" prop="category_id">
<el-cascader v-model="category" :props="category_props" :options="categoryData.category_options" filterable
clearable style="width:100%" @change="handleCategoryChange" />
</el-form-item>
<el-form-item label="信息标题" prop="title">
<el-input v-model="form.title" />
</el-form-item>
<el-form-item label="缩略图" prop="image_url">
<el-upload ref="uploadRef" class="avatar-uploader" action="#" :limit="1" :multiple="false"
:auto-upload="false" :show-file-list="false" :on-change="handleChange">
<img v-if="form.image_url" :src="form.image_url" 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 style="border:1px solid #ccc; width: 180%;">
<Toolbar style="border-bottom: 1px solid #ccc" :editor="editorRef" :defaultConfig="toolbarConfig"
:mode="mode" />
<Editor style="height: 300px; overflow-y: hidden;" v-model="valueHtml" :defaultConfig="editorConfig"
:mode="mode" @onCreated="handleEditorCreated" @onChange="handleEditorChange" />
</div>
</el-form-item>
<el-form-item>
<el-button type="danger" :loading="submitLoading" @click="handleSubmit">确定</el-button>
<el-button @click="handleCancel">返回</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
import { reactive, toRefs, ref, shallowRef, onBeforeMount, onBeforeUnmount, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import '@wangeditor/editor/dist/css/style.css'
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
import { useCategoryHook } from '@/hooks/categoryHook'
import useStore from '@/store'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Plus } from '@element-plus/icons-vue' // 新增:导入Plus图标
const router = useRouter()
const route = useRoute()
const store = useStore()
const { categoryData, getAllCategory } = useCategoryHook()
const formRef = ref(null)
const uploadRef = ref(null)
const editorRef = shallowRef()
const valueHtml = ref('')
const mode = 'default'
const toolbarConfig = { excludeKeys: ['group-video', 'insertTable', 'fullScreen'] }
const editorConfig = { placeholder: '请输入内容...' }
const data = reactive({
category: [],
category_props: {
label: 'name',
value: 'id',
checkStrictly: false
},
file: null,
form: {
title: '',
image_url: '',
content: '',
category_id: null
},
rules: {
category_id: [{ required: true, message: '请选择一个类别', trigger: 'change' }],
title: [
{ required: true, message: '请输入标题', trigger: 'blur' },
{ min: 2, max: 50, message: '标题长度为2-50个字符', trigger: 'blur' }
],
image_url: [{ required: true, message: '请上传缩略图', trigger: 'change' }],
content: [{ required: true, message: '请输入内容', trigger: 'change' }]
},
submitLoading: false,
isEdit: false,
infoId: null
})
const { category, category_props, form, rules, submitLoading } = toRefs(data)
const resetForm = () => {
data.isEdit = false
data.infoId = null
data.file = null
data.category = []
form.value = {
title: '',
image_url: '',
content: '',
category_id: null
}
valueHtml.value = ''
if (formRef.value) {
formRef.value.resetFields()
}
}
const getInfoDetail = async (id) => {
try {
submitLoading.value = true
const res = await store.info.getInfoDetailAction(id)
const info = res.data
form.value.title = info.title || ''
form.value.content = info.content || ''
form.value.image_url = info.public_image_url || ''
if (info.category_id) {
const categoryPath = findPathInTree(categoryData.category_options, info.category_id)
data.category = categoryPath || [info.category_id]
form.value.category_id = info.category_id
}
valueHtml.value = info.content || ''
} catch (error) {
console.error('获取详情失败:', error)
ElMessage.error(error.msg || '获取信息详情失败')
} finally {
submitLoading.value = false
}
}
watch(() => route.query.id, async (newId) => {
if (newId) {
data.isEdit = true
data.infoId = newId
await getInfoDetail(newId)
} else {
resetForm()
}
}, { immediate: true })
function findPathInTree(tree, targetId, path = []) {
for (const node of tree) {
const currentPath = [...path, node.id]
if (node.id === targetId) {
return currentPath
}
if (node.children && node.children.length > 0) {
const found = findPathInTree(node.children, targetId, currentPath)
if (found) return found
}
}
return null
}
onBeforeMount(async () => {
await getAllCategory()
const id = route.query.id
if (id) {
await getInfoDetail(id)
}
})
onBeforeUnmount(() => {
if (editorRef.value) {
editorRef.value.destroy()
}
})
const handleEditorCreated = (editor) => {
editorRef.value = editor
}
const handleEditorChange = (editor) => {
form.value.content = editor.getHtml()
formRef.value?.validateField('content')
}
const handleCategoryChange = () => {
form.value.category_id = category.value?.length ? category.value[category.value.length - 1] : null
formRef.value?.validateField('category_id')
}
const checkUploadFile = (rawFile) => {
if (rawFile.size / 1024 / 1024 > 2) {
ElMessage.error('上传的文件大小不能超过2MB!')
return false
}
const fileType = rawFile.type.startsWith('image/')
if (!fileType) {
ElMessage.error('请上传图片格式的文件!')
return false
}
return true
}
const handleChange = (file) => {
if (!checkUploadFile(file.raw)) return
data.file = file.raw
form.value.image_url = URL.createObjectURL(file.raw)
uploadRef.value.clearFiles()
formRef.value?.validateField('image_url')
}
const handleSubmit = () => {
form.value.content = valueHtml.value
formRef.value.validate(async (valid) => {
if (!valid) {
ElMessage.error('请检查表单填写')
return
}
const fd = new FormData()
fd.append('category_id', form.value.category_id)
fd.append('title', form.value.title)
fd.append('content', form.value.content)
if (data.file) {
fd.append('file', data.file)
}
try {
submitLoading.value = true
if (data.isEdit) {
await store.info.updateInfoAction({
id: data.infoId,
data: fd
})
ElMessage.success('更新成功!')
} else {
await store.info.createInfoAction(fd)
ElMessage.success('创建成功!')
}
router.push({ name: 'InfoList' })
} catch (error) {
ElMessage.error(error.msg || '操作失败,请重试')
} finally {
submitLoading.value = false
}
})
}
const handleCancel = () => {
ElMessageBox.confirm(
'确定要离开吗?未保存的内容将会丢失。',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'info',
}
).then(() => {
router.back()
}).catch(() => {
})
}
</script>
<style scoped>
.avatar {
width: 178px;
height: 178px;
object-fit: cover;
display: block;
}
</style>
<style>
.avatar-uploader .el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
display: inline-block;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
}
.avatar-uploader .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;
display: flex;
align-items: center;
justify-content: center;
}
.w-e-text img {
max-width: 100%;
height: auto;
}
</style>
<!-- D:\桌面\大学\大四上\vue3_vite_elementplus_admin\src\views\info\index.vue -->
<template>
<div class="form-button-group margin-bottom-20">
<router-link :to="{ name: 'InfoDetail' }">
<el-button type="danger">新增</el-button>
</router-link>
</div>
<el-form :inline="true" ref="searchFormRef">
<el-form-item label="类别" class="type">
<el-cascader v-model="category" :props="category_props" :options="categoryData.category_options" filterable
clearable style="width:100%" />
</el-form-item>
<el-form-item label="关键词" prop="key">
<el-select v-model="keywords.key" placeholder="请选择">
<el-option v-for="item in keywords.options" :key="item.value" :value="item.value"
:label="item.label"></el-option>
</el-select>
</el-form-item>
<el-form-item prop="value">
<el-input placeholder="请输入关键词" v-model="keywords.value" clearable />
</el-form-item>
<el-form-item>
<el-button type="danger" @click="handleSearch">搜索</el-button>
<el-button type="default" @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
<el-table :data="infoList" border style="width: 100%;" @selection-change="handleSelectionChange"
v-loading="loading">
<el-table-column type="selection" width="55" />
<el-table-column label="标题" prop="title" width="200" />
<el-table-column label="类别" prop="category.name" width="100" />
<el-table-column label="创建时间" prop="created_time" width="200" />
<el-table-column label="发布状态" width="120">
<template #default="scope">
<el-switch v-model="scope.row.status" :active-value="1" :inactive-value="0" :loading="scope.row.loading"
@change="handleChangeStatus($event, scope.row)" />
</template>
</el-table-column>
<el-table-column label=" 发布时间" prop="release_time" width="200" />
<el-table-column label="操作">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.row.id)">
编辑
</el-button>
<el-button size="small" type="danger" @click="handleDelete(scope.row.id)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<el-row class="margin-top-20">
<el-col :span="6">
<el-button type="danger" :disabled="!row_data_id" @click="handleDelete(row_data_id)">批量删除</el-button>
</el-col>
<el-col :span="18">
<el-pagination class="pull-right" v-model:current-page="request_data.pageIndex"
v-model:page-size="request_data.pageSize" :page-sizes="[10, 20, 30, 50]" background small
layout="total, sizes, prev, pager, next, jumper" :total="total" @size-change="handleSizeChange"
@current-change="handleCurrentChange" />
</el-col>
</el-row>
</template>
<script setup>
import { ref, reactive, toRefs, onBeforeMount } from 'vue'
import { useRouter } from 'vue-router'
import useStore from '@/store'
import { ElMessage, ElMessageBox } from 'element-plus'
import { useCategoryHook } from '@/hooks/categoryHook'
const router = useRouter()
const store = useStore()
const { categoryData, getAllCategory } = useCategoryHook()
const data = reactive({
category: 0,
category_props: {
label: 'name',
value: 'id'
},
keywords: {
key: '',
options: [
{
label: '标题',
value: 'title'
},
{
label: '内容',
value: 'content'
}
],
value: ''
},
infoList: [],
total: 0,
request_data: {
pageIndex: 1,
pageSize: 10,
},
row_data_id: '',
loading: false
})
const { keywords, category, category_props, total, request_data, infoList, row_data_id, loading } = toRefs(data)
const getInfoList = async (searchParams = {}) => {
loading.value = true
try {
const params = {
pageIndex: request_data.value.pageIndex,
pageSize: request_data.value.pageSize,
...searchParams
}
const res = await store.info.getInfoListAction(params)
data.infoList = res.data.rows || []
data.total = res.data.count || 0
} catch (error) {
console.error('获取列表失败', error)
ElMessage.error('获取信息列表失败')
} finally {
loading.value = false
}
}
onBeforeMount(async () => {
await getAllCategory();
getInfoList();
});
const handleSelectionChange = (val) => {
console.log('handleSelectionChange val', val);
if (val && val.length > 0) {
const ids = val.map(item => item.id)
row_data_id.value = ids.join()
} else {
row_data_id.value = ""
}
}
const handleSizeChange = (val) => {
console.log(`${val} items per page`)
request_data.value.pageSize = val
request_data.value.pageIndex = 1
getInfoList()
}
const handleCurrentChange = (val) => {
console.log(`current page: ${val}`)
request_data.value.pageIndex = val
getInfoList()
}
const handleSearch = () => {
request_data.value.pageIndex = 1;
const searchParams = {};
if (category.value?.length) {
searchParams.category_id = category.value[category.value.length - 1];
}
if (keywords.value.key && keywords.value.value) {
searchParams.keywords_key = keywords.value.key;
searchParams.keywords_value = keywords.value.value;
}
getInfoList(searchParams);
}
const handleReset = () => {
category.value = [];
keywords.value.key = '';
keywords.value.value = '';
request_data.value.pageIndex = 1;
request_data.value.pageSize = 10;
getInfoList();
}
const handleChangeStatus = async (value, data) => {
data.loading = true
data.status = !data.status
try {
const res = await store.info.changeInfoStatusAction({ id: data.id, data: { status: value } })
ElMessage.success(res.msg)
await getInfoList()
data.status = value
} finally {
data.loading = false
}
}
const handleDelete = (id) => {
ElMessageBox.confirm(
'确定要删除当前信息吗?',
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
)
.then(async () => {
const res = await store.info.deleteInfoAction(id)
ElMessage.success(res.msg)
await getInfoList()
})
}
const handleEdit = (id) => {
console.log('handleEdit id', id);
router.push({
name: 'InfoDetail',
query: { id }
})
}
</script>
<style scoped>
.el-input {
--el-input-width: 220px;
}
.el-select {
--el-select-width: 220px;
}
</style>
// D:\桌面\大学\大四上\vue3_vite_elementplus_admin\src\api\info.js
import installer from "@/utils/request";
export function GetInfoList(params = {}) {
return installer.request({
url: "/infos",
method: "GET",
params,
});
}
export function ChangeInfoStatus(id, data = {}) {
return installer.request({
url: `/infos/${id}/status`,
method: "PUT",
data,
});
}
export function GetAllCategory(params) {
return installer.request({
url: "/categories",
method: "GET",
params,
});
}
export function ChangeInforStatus(id, data = {}) {
return installer.request({
url: `/infos/${id}/status`,
method: "PUT",
data,
});
}
export function UploadFile(data = {}) {
return installer.request({
url: "/upload",
method: "POST",
data,
});
}
export function CreateInfo(data = {}) {
return installer.request({
url: "/infos",
headers: { "Content-Type": "multipart/form-data" },
method: "POST",
data,
});
}
export function UpdateInfo(id, data = {}) {
return installer.request({
url: `/infos/${id}`,
method: "PUT",
data,
});
}
export function GetInfoDetail(id) {
return installer.request({
url: `/infos/${id}`,
method: "GET",
});
}
export function DeleteInfo(id) {
return installer.request({
url: `/infos/${id}`,
method: "DELETE",
});
}
export function BatchDeleteInfo(ids) {
const idsStr = Array.isArray(ids) ? ids.join(",") : ids;
return installer.request({
url: `/infos/${idsStr}`,
method: "DELETE",
});
}
为什么新增和编辑弹出成功提示后不跳转,并且还弹出错误提示
最新发布