feat(back): 后台布局、书籍加载完成

This commit is contained in:
萌狼蓝天 2025-03-13 16:52:05 +08:00
parent fe388551db
commit 1a5453bb32
14 changed files with 591 additions and 18 deletions

1
package-lock.json generated
View File

@ -11,6 +11,7 @@
"@ant-design/icons-vue": "^7.0.1",
"ant-design-vue": "^4.2.6",
"axios": "^1.8.2",
"dayjs": "^1.11.13",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"vue3-slide-verify": "^1.1.6",

View File

@ -12,6 +12,7 @@
"@ant-design/icons-vue": "^7.0.1",
"ant-design-vue": "^4.2.6",
"axios": "^1.8.2",
"dayjs": "^1.11.13",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"vue3-slide-verify": "^1.1.6",

View File

@ -1,9 +1,19 @@
<script setup>
import zhCN from 'ant-design-vue/es/locale/zh_CN';
import dayjs from 'dayjs';
import 'dayjs/locale/zh-cn';
// dayjs
dayjs.locale('zh-cn');
//
const locale = zhCN;
</script>
<template style="width: 100%;height: 100%;margin: 0;padding: 0">
<a-config-provider :locale="locale">
<router-view style="width: 100%;height: 100%"></router-view>
</a-config-provider>
</template>
<style scoped>

24
src/apis/apis_book.js Normal file
View File

@ -0,0 +1,24 @@
// apis_book.js
// 获取所有图书
import {get, post,put, deleteRequest} from "./api.js";
// 分页查询查询参数通过URL传递
export const getBooks = async (data) => {
return get(`/book/books`,data);
};
// 新增POST
export const addBook = async (bookData) => {
return post("/book/books", bookData);
};
// 更新PUT注意路径拼接ID
export const updateBook = async (bookId, bookData) => {
return put(`/book/books/${bookId}`, bookData); // 关键修正拼接ID
};
// 删除DELETE注意路径拼接ID
export const deleteBook = async (bookId) => {
return deleteRequest(`/book/books/${bookId}`); // 关键修正拼接ID
};

View File

@ -0,0 +1,71 @@
<template>
<div style="width: 100%;height: 100%">
<a-menu
v-model:openKeys="openKeys"
v-model:selectedKeys="selectedKeys"
style="width: 100%;height: 100%"
mode="inline"
:theme="theme"
:items="items"
@click="handleMenuClick"
/>
</div>
</template>
<script setup>
import { h, ref } from 'vue';
import {
MailOutlined,
CalendarOutlined,
AppstoreOutlined,
SettingOutlined,
} from '@ant-design/icons-vue';
import {useRouter} from "vue-router";
const router = useRouter();
const theme = ref('light');
const selectedKeys = ref(['1']);
const openKeys = ref(['sub1']);
const items = ref([
{
key: '1',
icon: () => h(AppstoreOutlined),
label: '首页',
title: '首页',
path: '/back/index',
},
{
key: '2',
icon: () => h(CalendarOutlined),
label: '书籍管理',
title: '书籍管理',
path: '/back/admin/book',
},
{
key: 'sub1',
icon: () => h(SettingOutlined),
label: '系统管理',
title: '系统管理',
children: [
{
key: '3',
label: '用户管理',
title: '用户管理',
},
],
},
{
key: '3',
icon: () => h(MailOutlined),
label: '消息',
title: '消息',
},
]);
const changeTheme = checked => {
theme.value = checked ? 'dark' : 'light';
};
const handleMenuClick = ({ key }) => {
const item = items.value.find(item => item.key === key);
if (item && item.path) {
router.push(item.path);
}
};
</script>

View File

@ -0,0 +1,59 @@
<template>
<a-row>
<a-col flex="100px">
<span style="font-size: 24px; font-weight: bold">Libroro</span>
</a-col>
<a-col flex="auto" style="text-align: right">
{{ userName }}
<a-button style="margin-left: 1em;" @click="handleLogout" danger type="primary">退出登录</a-button>
</a-col>
</a-row>
</template>
<script setup>
import { useStore } from 'vuex';
import { useRouter } from 'vue-router';
import { computed, onMounted } from 'vue';
const store = useStore();
const router = useRouter();
//
const userInfo = computed(() => store.state.user.info);
//
onMounted(async () => {
try {
// Vuex action
await store.dispatch('user/initialize');
//
if (!userInfo.value) {
await router.push('/auth');
}
} catch (error) {
console.error('初始化失败:', error);
await router.push('/auth');
}
});
//
const userName = computed(() => userInfo.value?.name || '');
// 退
const handleLogout = async () => {
try {
await store.dispatch('user/logout');
await router.push('/auth');
} catch (error) {
console.error('退出登录失败:', error);
}
};
</script>
<style scoped>
/* 可以在这里添加一些样式 */
a-button {
margin-left: 10px;
}
</style>

View File

@ -4,6 +4,8 @@ import router from "./router/index.js";
import Antd from 'ant-design-vue';
import App from './App.vue'
import 'ant-design-vue/dist/reset.css';
import store from "@/store/index.js";
let app = createApp(App)
app.use(store)
app.use(router).use(Antd).mount('#app')

View File

@ -14,7 +14,13 @@ const isAdmin = computed(() => store.state.user.info?.isAdmin || false)
onMounted(async () => {
await store.dispatch('user/initialize')
})
//
const goToBackend = () => {
console.log("go to backend")
router.push({
path: '/back',
})
}
const logout = () => {
// Vuex action
store.dispatch('user/logout')
@ -98,7 +104,7 @@ const handleBorrow = (book) => {
<p style="text-align: center;">
<span class="inline-btn"><a-button type="primary">进入后台</a-button></span>
<span class="inline-btn"><a-button @click="goToBackend()" type="primary">进入后台</a-button></span>
<span class="inline-btn"><a-button type="primary" danger @click="logout()">退出登录</a-button></span>
</p>
</a-card>

View File

@ -0,0 +1,5 @@
<template>
</template>
<script setup>
</script>

View File

View File

@ -0,0 +1,333 @@
<template>
<div class="book-management">
<div style="width: 100%;height: 10%;text-align: left">
<a-button type="primary" @click="openAdd">
添加图书
</a-button>
</div>
<div style="height: 80%;">
<a-table
:data-source="dataSource"
:columns="columns"
:pagination="pagination"
:loading="loading"
:scroll="{ x: '100%' }"
@change="handleTableChange"
bordered
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-button type="link" @click="openEdit(record)">编辑</a-button>
<a-popconfirm
title="确定要删除这本图书吗?"
@confirm="handleDelete(record.id)"
>
<a-button type="link" danger>删除</a-button>
</a-popconfirm>
</template>
</template>
</a-table>
</div>
<a-modal
v-model:open="visible"
:title="isEdit ? '编辑图书' : '添加图书'"
@ok="handleSubmit"
:confirm-loading="confirmLoading"
>
<a-form
ref="formRef"
:model="formState"
:rules="rules"
layout="vertical"
>
<a-form-item label="图书名称" name="name">
<a-input v-model:value="formState.name"/>
</a-form-item>
<a-form-item label="作者" name="author">
<a-input v-model:value="formState.author"/>
</a-form-item>
<a-form-item label="出版日期" name="publishDate">
<a-date-picker
v-model:value="formState.publishDate"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="简介" name="description">
<a-textarea v-model:value="formState.description"/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup>
import {reactive, ref, onMounted} from 'vue';
import {message} from 'ant-design-vue';
import {
getBooks,
addBook,
updateBook,
deleteBook
} from '@/apis/apis_book.js';
//
const dataSource = ref([]);
const loading = ref(false);
const pagination = reactive({
current: 1,
pageSize: 10,
total: 0,
});
//
const visible = ref(false);
const isEdit = ref(false);
const confirmLoading = ref(false);
const formRef = ref();
const formState = reactive({
id: undefined,
name: '',
author: '',
publishDate: null,
description: '',
});
const columns = reactive([
{
title: '图书名称',
dataIndex: 'name',
key: 'name',
sorter: (a, b) => a.name.localeCompare(b.name), //
ellipsis: true, //
width: 200, //
},
{
title: '作者',
dataIndex: 'author',
key: 'author',
sorter: (a, b) => a.author.localeCompare(b.author), //
ellipsis: true,
width: 150,
},
{
title: '国际标准书号ISBN',
dataIndex: 'isbn',
key: 'isbn',
ellipsis: true,
width: 150,
},
{
title: '内部图书编码',
dataIndex: 'code',
key: 'code',
ellipsis: true,
width: 150,
},
{
title: '出版社',
dataIndex: 'publisher',
key: 'publisher',
sorter: (a, b) => a.publisher.localeCompare(b.publisher), //
ellipsis: true,
width: 200,
},
{
title: '出版日期',
dataIndex: 'publication_date',
key: 'publication_date',
sorter: (a, b) => new Date(a.publication_date) - new Date(b.publication_date), //
width: 150,
},
{
title: '版次',
dataIndex: 'edition',
key: 'edition',
sorter: (a, b) => a.edition - b.edition, //
width: 80,
},
{
title: '定价(元)',
dataIndex: 'price',
key: 'price',
sorter: (a, b) => a.price - b.price, //
width: 100,
},
{
title: '分类',
dataIndex: 'category',
key: 'category',
sorter: (a, b) => a.category.localeCompare(b.category), //
filters: [
//
{text: 'I247.5/悬疑小说', value: 'I247.5/悬疑小说'},
],
onFilter: (value, record) => record.category.includes(value),
ellipsis: true,
width: 150,
},
{
title: '总数量',
dataIndex: 'num',
key: 'num',
sorter: (a, b) => a.num - b.num, //
width: 100,
},
{
title: '库存量',
dataIndex: 'stock',
key: 'stock',
sorter: (a, b) => a.stock - b.stock, //
width: 100,
},
{
title: '是否绝版',
dataIndex: 'is_out_of_print',
key: 'is_out_of_print',
render: (text) => (text === 1 ? '是' : '否'),
sorter: (a, b) => a.is_out_of_print - b.is_out_of_print, //
width: 100,
},
{
title: '创建时间',
dataIndex: 'create_time',
key: 'create_time',
sorter: (a, b) => new Date(a.create_time) - new Date(b.create_time), //
width: 180,
},
{
title: '最后更新时间',
dataIndex: 'update_time',
key: 'update_time',
sorter: (a, b) => new Date(a.update_time) - new Date(b.update_time), //
width: 180,
},
{
title: '操作',
key: 'action',
fixed: 'right',
width: 150,
// render: (_, record) => (
// //
// <div>
// <a-button type="primary" size="small"></a-button>
// <a-button type="danger" size="small" style="margin-left: 8px"></a-button>
// </div>
// ),
},
]);
//
const rules = reactive({
name: [
{required: true, message: '请输入图书名称', trigger: 'blur'},
],
author: [
{required: true, message: '请输入作者姓名', trigger: 'blur'},
],
});
//
onMounted(() => {
fetchData();
});
//
const fetchData = async () => {
try {
loading.value = true;
const res = await getBooks({
page: pagination.current,
pageSize: pagination.pageSize,
});
console.log(res);
dataSource.value = res.data.list;
pagination.total = res.data.total;
} catch (error) {
message.error('获取数据失败');
} finally {
loading.value = false;
}
};
//
const handleTableChange = (pag) => {
pagination.current = pag.current;
pagination.pageSize = pag.pageSize;
fetchData();
};
//
const openAdd = () => {
isEdit.value = false;
resetForm();
visible.value = true;
};
//
const openEdit = (record) => {
isEdit.value = true;
Object.assign(formState, record);
visible.value = true;
};
//
const handleSubmit = async () => {
try {
await formRef.value.validate();
confirmLoading.value = true;
if (isEdit.value) {
await updateBook(formState.id, formState);
message.success('修改成功');
} else {
await addBook(formState);
message.success('添加成功');
}
visible.value = false;
fetchData();
} catch (error) {
console.error('提交失败:', error);
message.error('操作失败');
} finally {
confirmLoading.value = false;
}
};
//
const handleDelete = async (id) => {
try {
await deleteBook(id);
message.success('删除成功');
fetchData();
} catch (error) {
message.error('删除失败');
}
};
//
const resetForm = () => {
formRef.value?.resetFields();
Object.assign(formState, {
id: undefined,
name: '',
author: '',
publishDate: null,
description: '',
});
};
</script>
<style scoped>
.book-management {
padding: 1em;
box-sizing: border-box;
background: #fff;
height: 100%;
width: 100%;
overflow: auto;
}
.ant-form-item {
margin-bottom: 16px;
}
</style>

View File

@ -20,7 +20,17 @@ const routes = [
path: '/auth',
name: 'AuthView',
component: () => import('../views/AuthView.vue'),
}
},
{
name:'BackView',
path:'/back',
component:()=>import('../views/BackView.vue'),
redirect:'/back/index',
children:[
{name:'BackIndex',path:'/back/index',component:()=>import('../pages/back/BackIndex.vue')},
{name:'BackUser',path:'user',component:()=>import('../pages/back/BackUser.vue')},
{name:'BackBookAdmin',path:'/back/admin/book',component:()=>import('../pages/back/BookVueAdmin.vue')},
]},
];
// 创建路由实例

View File

@ -1,6 +1,17 @@
import service from '@/apis/axios';
import axios from 'axios';
import {auth_logout} from "@/apis/apis_auth.js";
// 清除会话的辅助函数
const clearSession = (state, commit) => {
state.info = null;
state.lastLogin = null;
localStorage.removeItem('userInfo');
if (state.refreshTimer) {
clearTimeout(state.refreshTimer);
state.refreshTimer = null;
}
commit('CLEAR_SESSION');
};
export default {
namespaced: true,
@ -18,13 +29,14 @@ export default {
state.lastLogin = new Date().toISOString();
},
CLEAR_SESSION(state) {
state.info = null;
state.lastLogin = null;
localStorage.removeItem('userInfo');
if (state.refreshTimer) {
clearTimeout(state.refreshTimer);
state.refreshTimer = null;
}
// state.info = null;
// state.lastLogin = null;
// localStorage.removeItem('userInfo');
// if (state.refreshTimer) {
// clearTimeout(state.refreshTimer);
// state.refreshTimer = null;
// }
clearSession(state, () => {});
},
},
actions: {
@ -44,8 +56,9 @@ export default {
}
},
CLEAR_SESSION({ commit }) {
commit('CLEAR_SESSION');
localStorage.removeItem('userInfo');
// commit('CLEAR_SESSION');
// localStorage.removeItem('userInfo');
clearSession(this.state, commit);
},
async logout({commit}) {
//向后端发起退出登录的请求

View File

@ -1,11 +1,49 @@
<script setup>
</script>
<template>
<a-layout>
<a-layout-header class="layout-header"><header-vue/></a-layout-header>
<a-layout>
<a-layout-sider class="layout-sider"><aside-vue/></a-layout-sider>
<a-layout-content class="layout-content"><router-view></router-view></a-layout-content>
</a-layout>
<a-layout-footer class="layout-footer"></a-layout-footer>
</a-layout>
</template>
<style scoped>
<script setup>
import HeaderVue from "@/components/back/layout/HeaderVue.vue";
import AsideVue from "@/components/back/layout/AsideVue.vue";
</script>
<style scoped>
.layout-header {
text-align: left;
color: #fff;
height: 64px;
padding-inline: 50px;
line-height: 64px;
background-color: #87b9ff;
}
.layout-content {
text-align: center;
min-height: 120px;
line-height: 120px;
color: #fff;
background-color: rgb(243, 255, 249);
}
.layout-sider {
text-align: center;
width: 256px;
line-height: 120px;
color: #fff;
background-color: rgba(95, 183, 255, 0.54);
}
.layout-footer {
text-align: center;
color: #fff;
background-color: #87b9ff;
}
</style>