parent
94d755e510
commit
7ce222aa1d
@ -0,0 +1,8 @@
|
||||
# 默认忽略的文件
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# 基于编辑器的 HTTP 客户端请求
|
||||
/httpRequests/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
@ -0,0 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<annotationProcessing>
|
||||
<profile default="true" name="Default" enabled="true" />
|
||||
<profile name="Maven default annotation processors profile" enabled="true">
|
||||
<sourceOutputDir name="target/generated-sources/annotations" />
|
||||
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
|
||||
<outputRelativeToContentRoot value="true" />
|
||||
<module name="demo" />
|
||||
</profile>
|
||||
</annotationProcessing>
|
||||
</component>
|
||||
<component name="JavacSettings">
|
||||
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
|
||||
<module name="demo" options="-parameters" />
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding">
|
||||
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
||||
</component>
|
||||
</project>
|
@ -0,0 +1,20 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RemoteRepositoriesConfiguration">
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Central Repository" />
|
||||
<option name="url" value="https://repo.maven.apache.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="central" />
|
||||
<option name="name" value="Maven Central repository" />
|
||||
<option name="url" value="https://repo1.maven.org/maven2" />
|
||||
</remote-repository>
|
||||
<remote-repository>
|
||||
<option name="id" value="jboss.community" />
|
||||
<option name="name" value="JBoss Community repository" />
|
||||
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
|
||||
</remote-repository>
|
||||
</component>
|
||||
</project>
|
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="MavenProjectsManager">
|
||||
<option name="originalFiles">
|
||||
<list>
|
||||
<option value="$PROJECT_DIR$/pom.xml" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="17" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
@ -0,0 +1,124 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Palette2">
|
||||
<group name="Swing">
|
||||
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
|
||||
</item>
|
||||
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
|
||||
</item>
|
||||
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
|
||||
</item>
|
||||
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.svg" removable="false" auto-create-binding="false" can-attach-label="true">
|
||||
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
|
||||
</item>
|
||||
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
|
||||
<initial-values>
|
||||
<property name="text" value="Button" />
|
||||
</initial-values>
|
||||
</item>
|
||||
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
|
||||
<initial-values>
|
||||
<property name="text" value="RadioButton" />
|
||||
</initial-values>
|
||||
</item>
|
||||
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
|
||||
<initial-values>
|
||||
<property name="text" value="CheckBox" />
|
||||
</initial-values>
|
||||
</item>
|
||||
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
|
||||
<initial-values>
|
||||
<property name="text" value="Label" />
|
||||
</initial-values>
|
||||
</item>
|
||||
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
|
||||
<preferred-size width="150" height="-1" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
|
||||
<preferred-size width="150" height="-1" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
|
||||
<preferred-size width="150" height="-1" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
|
||||
</item>
|
||||
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
|
||||
<preferred-size width="200" height="200" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
|
||||
<preferred-size width="200" height="200" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
|
||||
</item>
|
||||
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
|
||||
</item>
|
||||
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
|
||||
</item>
|
||||
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
|
||||
</item>
|
||||
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
|
||||
<preferred-size width="-1" height="20" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
|
||||
</item>
|
||||
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
|
||||
</item>
|
||||
</group>
|
||||
</component>
|
||||
</project>
|
@ -0,0 +1,19 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
wrapperVersion=3.3.2
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
|
@ -0,0 +1,109 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.4.2</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>com.example</groupId>
|
||||
<artifactId>demo</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<packaging>jar</packaging>
|
||||
<name>demo</name>
|
||||
<description>demo</description>
|
||||
<url/>
|
||||
<licenses>
|
||||
<license/>
|
||||
</licenses>
|
||||
<developers>
|
||||
<developer/>
|
||||
</developers>
|
||||
<scm>
|
||||
<connection/>
|
||||
<developerConnection/>
|
||||
<tag/>
|
||||
<url/>
|
||||
</scm>
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<!-- Apache Commons FileUpload -->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>commons-fileupload</groupId>-->
|
||||
<!-- <artifactId>commons-fileupload</artifactId>-->
|
||||
<!-- <version>1.4</version>-->
|
||||
<!-- </dependency>-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web-services</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mybatis.spring.boot</groupId>
|
||||
<artifactId>mybatis-spring-boot-starter</artifactId>
|
||||
<version>3.0.4</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.mysql</groupId>
|
||||
<artifactId>mysql-connector-j</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-tomcat</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mybatis.spring.boot</groupId>
|
||||
<artifactId>mybatis-spring-boot-starter-test</artifactId>
|
||||
<version>3.0.4</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.auth0</groupId>
|
||||
<artifactId>java-jwt</artifactId>
|
||||
<version>4.4.0</version>
|
||||
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.tomcat</groupId>
|
||||
<artifactId>tomcat-jdbc</artifactId>
|
||||
</dependency>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
@ -0,0 +1,13 @@
|
||||
package com.example.demo;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class Demo4Application {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Demo4Application.class, args);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
package com.example.demo.config;
|
||||
|
||||
import com.example.demo.pojo.Result;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.annotation.WebFilter;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Component
|
||||
//重点
|
||||
@WebFilter({"/user/*","/api","/upload","/borrow","/api/rank"})
|
||||
public class MyFilter implements Filter{
|
||||
|
||||
List<String> exclude;
|
||||
|
||||
@Override
|
||||
public void init(FilterConfig filterConfig) throws ServletException {
|
||||
System.out.println("初始化MyFilter==========================");
|
||||
exclude = new ArrayList<>();
|
||||
exclude.add("/user/login");
|
||||
exclude.add("/user/register");
|
||||
}
|
||||
@Override
|
||||
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
|
||||
HttpServletResponse response = (HttpServletResponse) servletResponse;
|
||||
HttpServletRequest request = (HttpServletRequest) servletRequest;
|
||||
String requestURI = request.getRequestURI();
|
||||
if (exclude.contains(requestURI) ||
|
||||
requestURI.endsWith(".html") ||
|
||||
requestURI.endsWith(".js") ||
|
||||
requestURI.endsWith(".css") ||
|
||||
requestURI.endsWith(".png") ||
|
||||
requestURI.endsWith(".jpg") ||
|
||||
requestURI.endsWith(".jpeg")
|
||||
) {
|
||||
// 在排除列表中,继续过滤链
|
||||
filterChain.doFilter(servletRequest,servletResponse);
|
||||
return;
|
||||
}
|
||||
|
||||
boolean loginflag = request.getSession().getAttribute("username") != null;
|
||||
if(loginflag){
|
||||
//登陆成功
|
||||
//这就代码就是放行
|
||||
filterChain.doFilter(servletRequest, servletResponse);
|
||||
}else{
|
||||
//登陆失败,打回请求
|
||||
ObjectMapper mapper = new ObjectMapper();
|
||||
response.getWriter().write(mapper.writeValueAsString(Result.error("未登录")));
|
||||
}
|
||||
|
||||
System.out.println("filter==========================");
|
||||
}
|
||||
@Override
|
||||
public void destroy() {
|
||||
System.out.println("销毁filter==========================");
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import com.example.demo.mapper.UserMapper;
|
||||
import com.example.demo.pojo.Article;
|
||||
import com.example.demo.pojo.Result;
|
||||
import com.example.demo.service.ArticleService;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@CrossOrigin
|
||||
@RequestMapping("/api")
|
||||
public class ArticleController {
|
||||
|
||||
|
||||
@Autowired
|
||||
ArticleService articleService;
|
||||
|
||||
@Autowired
|
||||
UserMapper userMapper;
|
||||
|
||||
@PostMapping("/add")
|
||||
public Result<Article> addarticle(@RequestBody Article article, HttpSession session) throws JsonProcessingException {
|
||||
System.out.println("Received article: " + article); // 添加此行以调试
|
||||
int admin = userMapper.findByUserName(String.valueOf(session.getAttribute("username"))).getAdmin();
|
||||
if (admin == 1) {
|
||||
articleService.addarticle(article);
|
||||
return Result.success(article);
|
||||
}
|
||||
else{
|
||||
return Result.error("权限不够。。。");
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/select")
|
||||
public Result selectarticle(HttpServletRequest request) {
|
||||
List<Article> articles = articleService.selectarticle();
|
||||
System.out.println(request.getRequestURL());
|
||||
return Result.success(articles);
|
||||
}
|
||||
|
||||
|
||||
|
||||
//根据书籍名字搜索单个书籍信息
|
||||
@GetMapping("/selectone")
|
||||
public Result<Article> selectone(String title){
|
||||
Article article= articleService.selectonearticle(title);
|
||||
return Result.success(article);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import com.example.demo.mapper.BorrowMapper;
|
||||
import com.example.demo.pojo.Article;
|
||||
import com.example.demo.pojo.Borrow;
|
||||
import com.example.demo.pojo.Result;
|
||||
import com.example.demo.service.ArticleService;
|
||||
import com.example.demo.service.BorrowService;
|
||||
|
||||
import com.example.demo.service.UserService;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@RestController
|
||||
@CrossOrigin
|
||||
@RequestMapping("/borrow")
|
||||
public class BorrowController {
|
||||
@Autowired
|
||||
private BorrowService borrowService;
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Autowired
|
||||
private ArticleService articleService;
|
||||
@Autowired
|
||||
private BorrowMapper borrowMapper;
|
||||
|
||||
|
||||
//租借书
|
||||
@PostMapping("/borrowbook")
|
||||
public Result borrowbook(String title, HttpSession session) {
|
||||
Borrow borrow = new Borrow();
|
||||
borrow.setTitle(title);
|
||||
borrow.setBorrower((String) session.getAttribute("username"));
|
||||
borrow.setBorrow_time(LocalDateTime.now());
|
||||
Article article = articleService.selectonearticle(title);
|
||||
float money = article.getMoney();
|
||||
float balance = userService.findmoney(session.getAttribute("username"));
|
||||
if (balance >= money) {
|
||||
borrowService.borrow(borrow);
|
||||
userService.deduct(money,session.getAttribute("username"));
|
||||
return Result.success(borrow);
|
||||
}
|
||||
else{
|
||||
return Result.error("余额不足!");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//还书
|
||||
@PostMapping("/returnbook")
|
||||
public Result returnbook(String title, HttpSession session){
|
||||
Borrow borrow=new Borrow();
|
||||
borrow.setTitle(title);
|
||||
borrow.setBorrower((String) session.getAttribute("username"));
|
||||
borrow.setReturn_time(LocalDateTime.now());
|
||||
borrowService.returnbook(borrow);
|
||||
borrowMapper.fine();
|
||||
return Result.success(borrow);
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
package com.example.demo.controller;
|
||||
|
||||
import com.example.demo.pojo.ArticleRentRankDTO;
|
||||
import com.example.demo.service.BorrowRankService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/rank")
|
||||
public class BorrowRankController {
|
||||
|
||||
@Autowired
|
||||
private BorrowRankService borrowRankService;
|
||||
|
||||
/**
|
||||
* 本周热租榜单接口
|
||||
*/
|
||||
@GetMapping("/weekly")
|
||||
public List<ArticleRentRankDTO> weeklyRank() {
|
||||
return borrowRankService.getWeeklyRank();
|
||||
}
|
||||
|
||||
/**
|
||||
* 本月热租榜单接口
|
||||
*/
|
||||
@GetMapping("/monthly")
|
||||
public List<ArticleRentRankDTO> monthlyRank() {
|
||||
return borrowRankService.getMonthlyRank();
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package com.example.demo.mapper;
|
||||
|
||||
|
||||
import com.example.demo.pojo.Article;
|
||||
import org.apache.ibatis.annotations.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface ArticleMapper {
|
||||
|
||||
@Insert("INSERT INTO article (title, content, url, state)\n" +
|
||||
"VALUES \n" +
|
||||
"(#{title}, #{content}, #{url},#{state})")
|
||||
@Options(useGeneratedKeys = true, keyColumn = "id", keyProperty = "id")
|
||||
Long addarticle(Article article);
|
||||
|
||||
|
||||
//查询所有书籍所有信息
|
||||
@Select("SELECT * FROM article")
|
||||
List<Article> selectarticle();
|
||||
|
||||
//查询
|
||||
|
||||
//管理员删除书籍
|
||||
@Delete("DELETE FROM article WHERE id = #{id}")
|
||||
Integer deleteArticle(Long id);
|
||||
|
||||
//查询单本书
|
||||
@Select("SELECT * from article where title=#{title}")
|
||||
Article selectonearticle(String title);
|
||||
|
||||
|
||||
@Select("SELECT a.* " +
|
||||
"FROM article a " +
|
||||
"INNER JOIN borrow b ON a.title = b.title " +
|
||||
"INNER JOIN user u ON b.borrower = u.username " +
|
||||
"WHERE u.username = #{username} " +
|
||||
"AND b.borrow_time IS NOT NULL " +
|
||||
"AND b.return_time IS NULL")
|
||||
List<Article> findBorrowedBooksByUsername(String username);
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
package com.example.demo.mapper;
|
||||
|
||||
|
||||
import com.example.demo.pojo.User;
|
||||
import com.example.demo.pojo.info;
|
||||
import org.apache.ibatis.annotations.*;
|
||||
|
||||
@Mapper
|
||||
public interface UserMapper {
|
||||
@Select("select * from user where username=#{username}")
|
||||
User findByUserName(String username);
|
||||
|
||||
@Insert("insert into user(username,password,vip,create_time,update_time,admin,balance)" +
|
||||
" values(#{username},#{password},#{vip},now(),now(),#{admin},#{balance})")
|
||||
void add(String username,String password,int vip,int admin,float balance);
|
||||
|
||||
@Select("SELECT password FROM user WHERE username=#{username}")
|
||||
String login(String username);
|
||||
|
||||
|
||||
@Select("select username,pic from user where username=#{username}")
|
||||
info getinfo( String username);
|
||||
|
||||
|
||||
//充钱
|
||||
@Update("UPDATE user\n" +
|
||||
"SET balance=balance+#{money1}\n" +
|
||||
"WHERE username=#{username};")
|
||||
void recharge(float money1,String username);
|
||||
|
||||
|
||||
//查询余额
|
||||
@Select("select balance from user where username=#{username}")
|
||||
float findbalance(Object username);
|
||||
|
||||
//更新VIP余额
|
||||
@Update("UPDATE `user`\n" +
|
||||
"SET `vip` = CASE\n" +
|
||||
" WHEN `balance` >= 10 AND `balance` < 30 THEN '1'\n" +
|
||||
" WHEN `balance` >= 30 AND `balance` < 100 THEN '2'\n" +
|
||||
" WHEN `balance` >= 100 AND `balance` < 300 THEN '3'\n" +
|
||||
" WHEN `balance` >= 300 AND `balance` < 500 THEN '4'\n" +
|
||||
" WHEN `balance` >= 500 THEN '5'\n" +
|
||||
" ELSE `vip` -- 不满足条件的记录保持原有vip值\n" +
|
||||
"END\n" +
|
||||
"WHERE `username` = #{username};")
|
||||
void updateVIP( float balance,String username);
|
||||
|
||||
@Select("select vip from user where username=#{username}")
|
||||
int findVIP(String username);
|
||||
|
||||
//扣钱
|
||||
@Update("UPDATE user\n" +
|
||||
"SET balance=balance-#{money1}\n" +
|
||||
"WHERE username=#{username};")
|
||||
void deduct(float money1,String username);
|
||||
|
||||
//管理员删除书
|
||||
@Delete("DELETE FROM article WHERE title=#{title}")
|
||||
void deletebook(String title);
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package com.example.demo.pojo;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class Article {
|
||||
private int id;
|
||||
private String title;
|
||||
private String content;
|
||||
private String url;
|
||||
private String state;
|
||||
private float money;
|
||||
private int number;
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
package com.example.demo.pojo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ArticleRentRankDTO {
|
||||
private String title; // 物品名称
|
||||
private String url; // 封面图片
|
||||
private Float money; // 租借价格
|
||||
private Integer number; // 租借次数
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
package com.example.demo.pojo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
public class Borrow {
|
||||
private String title;
|
||||
private String borrower;
|
||||
private LocalDateTime borrow_time;
|
||||
private LocalDateTime return_time;
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package com.example.demo.pojo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
@Data
|
||||
public class User {
|
||||
private Integer id;
|
||||
private String username;
|
||||
private String password;
|
||||
private String vip;
|
||||
private String userPic;
|
||||
private LocalDateTime createTime;
|
||||
private LocalDateTime updateTime;
|
||||
private Integer admin;
|
||||
private float balance;
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.example.demo.pojo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class info {
|
||||
private String username;
|
||||
private String pic;
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import com.example.demo.pojo.Article;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ArticleService {
|
||||
Long addarticle(Article article);
|
||||
|
||||
List<Article> selectarticle();
|
||||
|
||||
List<Article> getUserBorrowedBooks(String username);
|
||||
|
||||
boolean deletearticle(Long id);
|
||||
|
||||
Article selectonearticle(String title);
|
||||
}
|
||||
|
@ -0,0 +1,28 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import com.example.demo.mapper.BorrowRankMapper;
|
||||
import com.example.demo.pojo.ArticleRentRankDTO;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class BorrowRankService {
|
||||
|
||||
@Autowired // 改用Spring的@Autowired注解
|
||||
private BorrowRankMapper borrowRankMapper;
|
||||
|
||||
/**
|
||||
* 获取本周热租榜单
|
||||
*/
|
||||
public List<ArticleRentRankDTO> getWeeklyRank() {
|
||||
return borrowRankMapper.listWeeklyRank();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本月热租榜单
|
||||
*/
|
||||
public List<ArticleRentRankDTO> getMonthlyRank() {
|
||||
return borrowRankMapper.listMonthlyRank();
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import com.example.demo.mapper.BorrowMapper;
|
||||
import com.example.demo.pojo.Borrow;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
|
||||
public interface BorrowService {
|
||||
|
||||
|
||||
void borrow(Borrow borrow);
|
||||
|
||||
void returnbook(Borrow borrow);
|
||||
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
package com.example.demo.service;
|
||||
|
||||
import com.example.demo.pojo.User;
|
||||
import com.example.demo.pojo.info;
|
||||
|
||||
public interface UserService {
|
||||
User findByUserName(String username);
|
||||
|
||||
void register(String username, String password);
|
||||
|
||||
void login(String username, String password);
|
||||
|
||||
info getinfo(String username);
|
||||
|
||||
void recharge(float money, String username);
|
||||
|
||||
float findmoney(Object username);
|
||||
|
||||
void deduct(float money, Object username);
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package com.example.demo.service.impl;
|
||||
|
||||
import com.example.demo.mapper.ArticleMapper;
|
||||
import com.example.demo.pojo.Article;
|
||||
import com.example.demo.service.ArticleService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class ArticleServiceImpl implements ArticleService {
|
||||
|
||||
|
||||
@Autowired
|
||||
ArticleMapper articleMapper;
|
||||
@Override
|
||||
public Long addarticle(Article article) {
|
||||
|
||||
return articleMapper.addarticle(article);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Article> selectarticle() {
|
||||
return articleMapper.selectarticle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean deletearticle(Long id) {
|
||||
final Integer integer = articleMapper.deleteArticle((long) Math.toIntExact(id));
|
||||
return integer == 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Article selectonearticle(String title) {
|
||||
return articleMapper.selectonearticle(title);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Article> getUserBorrowedBooks(String username) {
|
||||
// 可添加参数校验(如用户名非空)
|
||||
if (username == null || username.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("用户名不能为空");
|
||||
}
|
||||
return articleMapper.findBorrowedBooksByUsername(username);
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
package com.example.demo.service.impl;
|
||||
|
||||
|
||||
import com.example.demo.mapper.UserMapper;
|
||||
import com.example.demo.pojo.User;
|
||||
import com.example.demo.pojo.info;
|
||||
import com.example.demo.service.UserService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class UserServiceImpl implements UserService {
|
||||
|
||||
@Autowired
|
||||
private UserMapper userMapper;
|
||||
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public User findByUserName(String username) {
|
||||
User u=userMapper.findByUserName(username);
|
||||
return u;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register(String username, String password) {
|
||||
userMapper.add(username,password,0,0,0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void login(String username, String password) {
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
public info getinfo(String username) {
|
||||
return userMapper.getinfo(username);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void recharge(float money, String username) {
|
||||
userMapper.recharge(money,username);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float findmoney(Object username) {
|
||||
return userMapper.findbalance(username);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deduct(float money,Object name) {
|
||||
userMapper.deduct(money, (String) name);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package com.example.demo.utils;
|
||||
|
||||
import com.auth0.jwt.JWT;
|
||||
import com.auth0.jwt.algorithms.Algorithm;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
public class JwtUtil {
|
||||
|
||||
private static final String KEY = "itheima";
|
||||
|
||||
//接收业务数据,生成token并返回
|
||||
public static String genToken(Map<String, Object> claims) {
|
||||
return JWT.create()
|
||||
.withClaim("claims", claims)
|
||||
.withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 12))
|
||||
.sign(Algorithm.HMAC256(KEY));
|
||||
}
|
||||
|
||||
//接收token,验证token,并返回业务数据
|
||||
public static Map<String, Object> parseToken(String token) {
|
||||
return JWT.require(Algorithm.HMAC256(KEY))
|
||||
.build()
|
||||
.verify(token)
|
||||
.getClaim("claims")
|
||||
.asMap();
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
|
||||
|
||||
file.upload-dir=D:\\uploads\\files
|
||||
# MySQL ?????
|
||||
|
||||
#spring.datasource.url= jdbc:mysql://124.220.45.50:3306/big_event
|
||||
#spring.datasource.username= big_event
|
||||
#spring.datasource.password= 3cjBiCj3ZBafn3kP
|
||||
|
||||
#spring.datasource.url= jdbc:mysql://127.0.0.1:3306/big_event
|
||||
#spring.datasource.username=root
|
||||
#spring.datasource.password=hsp
|
||||
|
||||
|
||||
spring.datasource.url= jdbc:mysql://127.0.0.1:3306/big_event1
|
||||
spring.datasource.username= root
|
||||
spring.datasource.password= hsp
|
||||
|
||||
|
||||
|
||||
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
|
||||
server.port=8877
|
||||
spring.servlet.multipart.max-file-size=5MB
|
||||
spring.servlet.multipart.max-request-size=5MB
|
||||
|
@ -0,0 +1,13 @@
|
||||
package com.example.demo;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class Demo4ApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
|
||||
|
||||
file.upload-dir=D:\\uploads\\files
|
||||
# MySQL ?????
|
||||
|
||||
#spring.datasource.url= jdbc:mysql://124.220.45.50:3306/big_event
|
||||
#spring.datasource.username= big_event
|
||||
#spring.datasource.password= 3cjBiCj3ZBafn3kP
|
||||
|
||||
#spring.datasource.url= jdbc:mysql://127.0.0.1:3306/big_event
|
||||
#spring.datasource.username=root
|
||||
#spring.datasource.password=hsp
|
||||
|
||||
|
||||
spring.datasource.url= jdbc:mysql://127.0.0.1:3306/big_event1
|
||||
spring.datasource.username= root
|
||||
spring.datasource.password= hsp
|
||||
|
||||
|
||||
|
||||
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
|
||||
server.port=8877
|
||||
spring.servlet.multipart.max-file-size=5MB
|
||||
spring.servlet.multipart.max-request-size=5MB
|
||||
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,24 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
@ -0,0 +1,3 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
# Vue 3 + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
|
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Library_system</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "library_system",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.11.0",
|
||||
"element-plus": "^2.10.4",
|
||||
"pinia": "^3.0.3",
|
||||
"vue": "^3.5.17",
|
||||
"vue-router": "^4.5.1",
|
||||
"vuex": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.1.0",
|
||||
"@vitejs/plugin-vue": "^6.0.0",
|
||||
"unplugin-auto-import": "^19.3.0",
|
||||
"unplugin-vue-components": "^28.8.0",
|
||||
"vite": "^7.0.4"
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 1.5 KiB |
@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<div v-if="sessionInitialized">
|
||||
<el-container class="app-container">
|
||||
<el-header>
|
||||
<HeaderBar />
|
||||
</el-header>
|
||||
<el-main>
|
||||
<router-view />
|
||||
</el-main>
|
||||
<el-footer>
|
||||
<div class="footer-content">
|
||||
<p>图书馆管理系统 © {{ new Date().getFullYear() }}</p>
|
||||
</div>
|
||||
</el-footer>
|
||||
</el-container>
|
||||
</div>
|
||||
<div v-else class="loading-container">
|
||||
<el-loading-spinner />
|
||||
<p>正在初始化...</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import HeaderBar from './components/HeaderBar.vue'
|
||||
|
||||
const store = useStore()
|
||||
const sessionInitialized = computed(() => store.state.sessionInitialized)
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.app-container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.el-main {
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.el-footer {
|
||||
background-color: #545c64;
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 15px 0;
|
||||
}
|
||||
|
||||
.footer-content {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
|
||||
.loading-container p {
|
||||
margin-top: 20px;
|
||||
color: #606266;
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,62 @@
|
||||
/* Element Plus 主题定制 */
|
||||
:root {
|
||||
--el-color-primary: #165dff;
|
||||
--el-color-primary-light-3: #3c8dff;
|
||||
--el-color-primary-light-5: #6baaff;
|
||||
--el-color-primary-light-7: #a3cfff;
|
||||
--el-color-primary-light-8: #d6eaff;
|
||||
--el-color-primary-light-9: #f4faff;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', Arial, sans-serif;
|
||||
background: #f6f8fa;
|
||||
color: #222;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.el-header, .el-footer {
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px 0 rgba(0,0,0,0.03);
|
||||
}
|
||||
|
||||
.el-menu {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.el-card {
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 12px 0 rgba(22,93,255,0.04);
|
||||
}
|
||||
|
||||
.el-main {
|
||||
padding: 32px 24px 24px 24px;
|
||||
min-height: 80vh;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--el-color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* 登录/注册页面居中 */
|
||||
.page-center {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(135deg, #e3f0ff 0%, #f6f8fa 100%);
|
||||
}
|
||||
|
||||
/* 头像样式 */
|
||||
.avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
box-shadow: 0 2px 8px 0 rgba(22,93,255,0.08);
|
||||
}
|
After Width: | Height: | Size: 496 B |
@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<el-card class="book-card" shadow="hover" @click="$emit('click')">
|
||||
<div class="book-cover">
|
||||
<el-image :src="book.url" fit="cover" class="cover-image" :alt="book.title" />
|
||||
</div>
|
||||
<div class="book-info">
|
||||
<h3 class="book-title">{{ book.title }}</h3>
|
||||
<div class="book-meta" v-if="showMeta">
|
||||
<el-tag type="info" size="small">{{ book.content }}</el-tag>
|
||||
<el-tag type="success" size="small">¥{{ book.money }}/天</el-tag>
|
||||
</div>
|
||||
<div class="book-status"v-if="showMeta">
|
||||
<el-tag :type="book.state" size="small">
|
||||
{{ book.state }}
|
||||
</el-tag>
|
||||
<span>阅读量: {{ book.number }}次</span>
|
||||
<el-button @click="$emit('borrow')">借阅</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
book: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
showMeta: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.book-card {
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.book-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.book-cover {
|
||||
height: 180px;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.cover-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.book-info {
|
||||
padding: 0 10px 10px;
|
||||
}
|
||||
|
||||
.book-title {
|
||||
margin: 0 0 10px;
|
||||
font-size: 16px;
|
||||
height: 24px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.book-meta {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
margin-bottom: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.book-status {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,249 @@
|
||||
<!-- src/components/HeaderBar.vue -->
|
||||
<template>
|
||||
<el-header class="header">
|
||||
<div class="logo">
|
||||
<el-icon><Reading /></el-icon>
|
||||
<span>图书馆管理系统</span>
|
||||
</div>
|
||||
<div class="nav">
|
||||
<el-menu
|
||||
:default-active="activeIndex"
|
||||
mode="horizontal"
|
||||
@select="handleSelect"
|
||||
background-color="#545c64"
|
||||
text-color="#fff"
|
||||
active-text-color="#ffd04b">
|
||||
<el-menu-item index="home">首页</el-menu-item>
|
||||
<el-menu-item index="books">图书查询</el-menu-item>
|
||||
<el-menu-item index="borrow">借阅图书</el-menu-item>
|
||||
<el-menu-item index="return">归还图书</el-menu-item>
|
||||
<el-sub-menu index="ranking">
|
||||
<template #title>排行榜</template>
|
||||
<el-menu-item index="weekly">本周热租榜</el-menu-item>
|
||||
<el-menu-item index="monthly">本月热租榜</el-menu-item>
|
||||
</el-sub-menu>
|
||||
<el-menu-item index="bookManagement" v-if="user && user.admin">
|
||||
图书管理
|
||||
</el-menu-item>
|
||||
<el-menu-item index="allBorrowRecords" v-if="user && user.admin">
|
||||
用户借阅记录
|
||||
</el-menu-item>
|
||||
</el-menu>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<el-dropdown v-if="user">
|
||||
<span class="el-dropdown-link">
|
||||
<el-avatar :src="user.pic" size="small" />
|
||||
<span class="username">{{ user.username }}</span>
|
||||
<el-icon><ArrowDown /></el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<!-- 用户信息展示区域 -->
|
||||
<div class="user-dropdown-info">
|
||||
<div class="user-name">{{ user.username }}</div>
|
||||
<div class="user-vip">VIP{{ vip }}级</div>
|
||||
<div class="user-balance">
|
||||
<el-icon><Wallet /></el-icon>
|
||||
余额: ¥{{ balance }}
|
||||
</div>
|
||||
</div>
|
||||
<el-divider /> <!-- 分割线 -->
|
||||
|
||||
<!-- 保留的功能选项 -->
|
||||
<el-dropdown-item @click="goToRecharge">账户充值</el-dropdown-item>
|
||||
<el-dropdown-item @click="goToRecords">借阅记录</el-dropdown-item>
|
||||
<el-dropdown-item divided @click="logout">退出登录</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<div v-else>
|
||||
<el-button type="text" @click="goToLogin">登录</el-button>
|
||||
<el-button type="text" @click="goToRegister">注册</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-header>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useStore } from 'vuex'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Reading, ArrowDown } from '@element-plus/icons-vue'
|
||||
import { Wallet } from '@element-plus/icons-vue'
|
||||
import { computed, onMounted, watch } from 'vue'
|
||||
|
||||
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(async () => {
|
||||
// 等待会话初始化完成
|
||||
if (store.state.sessionInitialized && store.getters.isAuthenticated) {
|
||||
await fetchUserInfo()
|
||||
}
|
||||
})
|
||||
|
||||
// 监听会话状态变化
|
||||
watch(() => store.state.sessionInitialized, async (newVal) => {
|
||||
if (newVal && store.getters.isAuthenticated) {
|
||||
await fetchUserInfo()
|
||||
}
|
||||
})
|
||||
|
||||
// 添加获取用户信息的方法
|
||||
async function fetchUserInfo() {
|
||||
try {
|
||||
// 获取余额和VIP信息
|
||||
await store.dispatch('fetchBalanceAndVip')
|
||||
} catch (error) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
// 避免在未登录状态下显示错误信息
|
||||
if (store.getters.isAuthenticated) {
|
||||
ElMessage.error('获取用户信息失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const vip = computed(() => store.state.vipLevel)
|
||||
const balance = computed(() => store.state.balance)
|
||||
const user = computed(() => store.state.user)
|
||||
console.log("user.pic:"+user.pic)
|
||||
|
||||
|
||||
|
||||
const activeIndex = computed(() => {
|
||||
const routeName = router.currentRoute.value.name
|
||||
if (routeName === 'Home') return 'home'
|
||||
if (routeName === 'Books' || routeName === 'BookDetail' || routeName === 'AddBook' || routeName === 'EditBook') return 'books'
|
||||
if (routeName === 'BorrowBook') return 'borrow'
|
||||
if (routeName === 'ReturnBook') return 'return'
|
||||
if (routeName === 'WeeklyRank') return 'weekly'
|
||||
if (routeName === 'MonthlyRank') return 'monthly'
|
||||
if (routeName === 'BookManagement') return 'bookManagement'
|
||||
if (routeName === 'AllBorrowRecords') return 'allBorrowRecords'
|
||||
return ''
|
||||
})
|
||||
|
||||
|
||||
|
||||
const handleSelect = (index) => {
|
||||
switch(index) {
|
||||
case 'home':
|
||||
router.push('/')
|
||||
break
|
||||
case 'books':
|
||||
router.push('/books')
|
||||
break
|
||||
case 'borrow':
|
||||
router.push('/borrow')
|
||||
break
|
||||
case 'return':
|
||||
router.push('/return')
|
||||
break
|
||||
case 'weekly':
|
||||
router.push('/ranking/weekly')
|
||||
break
|
||||
case 'monthly':
|
||||
router.push('/ranking/monthly')
|
||||
break
|
||||
case 'bookManagement':
|
||||
router.push('/admin/books')
|
||||
break
|
||||
case 'allBorrowRecords':
|
||||
router.push('/admin/borrow-records')
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const goToLogin = () => {
|
||||
router.push('/login')
|
||||
}
|
||||
|
||||
const goToRegister = () => {
|
||||
router.push('/register')
|
||||
}
|
||||
|
||||
const goToRecharge = () => {
|
||||
router.push('/recharge')
|
||||
}
|
||||
|
||||
const goToRecords = () => {
|
||||
router.push('/borrow-records')
|
||||
}
|
||||
|
||||
|
||||
const logout = () => {
|
||||
store.dispatch('logout')
|
||||
router.push('/login')
|
||||
ElMessage.success('退出登录成功')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
height: 60px;
|
||||
background-color: #545c64;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
flex: 1;
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.username {
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.el-dropdown-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.user-dropdown-info {
|
||||
padding: 5px 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-weight: bold;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.user-vip {
|
||||
color: #ff9a2e;
|
||||
font-size: 14px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.user-balance {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,25 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
import ElementPlus from 'element-plus'
|
||||
import 'element-plus/dist/index.css'
|
||||
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
// 注册所有图标
|
||||
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
app.use(ElementPlus)
|
||||
app.use(store)
|
||||
app.use(router)
|
||||
|
||||
// 初始化会话
|
||||
store.dispatch('initSession')
|
||||
|
||||
app.mount('#app')
|
||||
|
||||
window.store = store
|
@ -0,0 +1,131 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import store from '../store'
|
||||
|
||||
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: () => import('../views/Home.vue'),
|
||||
meta: { requiresAuth: false }
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('../views/Auth/Login.vue')
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
name: 'Register',
|
||||
component: () => import('../views/Auth/Register.vue')
|
||||
},
|
||||
|
||||
{
|
||||
path: '/books',
|
||||
name: 'Books',
|
||||
component: () => import('../views/Books/BookList.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/books/:id',
|
||||
name: 'BookDetail',
|
||||
component: () => import('../views/Books/BookDetail.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/books/add',
|
||||
name: 'AddBook',
|
||||
component: () => import('../views/Books/AddBook.vue'),
|
||||
meta: { requiresAuth: true, requiresAdmin: true }
|
||||
},
|
||||
// {
|
||||
// path: '/books/edit/:id',
|
||||
// name: 'EditBook',
|
||||
// component: () => import('../views/Books/EditBook.vue'),
|
||||
// meta: { requiresAuth: true, requiresAdmin: true }
|
||||
// },
|
||||
{
|
||||
path: '/borrow',
|
||||
name: 'BorrowBook',
|
||||
component: () => import('../views/Borrow/BorrowBook.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/return',
|
||||
name: 'ReturnBook',
|
||||
component: () => import('../views/Borrow/ReturnBook.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/borrow-records',
|
||||
name: 'BorrowRecords',
|
||||
component: () => import('../views/User/BorrowRecords.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/recharge',
|
||||
name: 'Recharge',
|
||||
component: () => import('../views/User/Recharge.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/ranking/weekly',
|
||||
name: 'WeeklyRank',
|
||||
component: () => import('../views/Ranking/WeeklyRank.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/ranking/monthly',
|
||||
name: 'MonthlyRank',
|
||||
component: () => import('../views/Ranking/MonthlyRank.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
},
|
||||
{
|
||||
path: '/admin/borrow-records',
|
||||
name: 'AllBorrowRecords',
|
||||
component: () => import('../views/Admin/AllBorrowRecords.vue'),
|
||||
meta: { requiresAdmin: true }
|
||||
},
|
||||
{
|
||||
path: '/admin/books',
|
||||
name: 'BookManagement',
|
||||
component: () => import('../views/Admin/BookManagement.vue'),
|
||||
meta: { requiresAdmin: true }
|
||||
}
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes
|
||||
})
|
||||
|
||||
// 路由守卫
|
||||
router.beforeEach((to, from, next) => {
|
||||
// 等待会话初始化完成
|
||||
if (!store.state.sessionInitialized) {
|
||||
return next()
|
||||
}
|
||||
|
||||
const isAuthenticated = store.getters.isAuthenticated
|
||||
const isAdmin = store.getters.isAdmin
|
||||
|
||||
// 如果路由需要认证但用户未登录,跳转到登录页
|
||||
if (to.meta.requiresAuth && !isAuthenticated) {
|
||||
return next('/login')
|
||||
}
|
||||
|
||||
// 如果路由需要管理员权限但用户不是管理员
|
||||
if (to.meta.requiresAdmin && !isAdmin) {
|
||||
return next({ name: 'Home' })
|
||||
}
|
||||
|
||||
// 如果用户已登录但访问登录/注册页,跳转到首页
|
||||
if ((to.name === 'Login' || to.name === 'Register') && isAuthenticated) {
|
||||
return next({ name: 'Home' })
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
export default router
|
@ -0,0 +1,16 @@
|
||||
export function formatDate(dateString) {
|
||||
if (!dateString) return '未知时间'
|
||||
|
||||
const date = new Date(dateString)
|
||||
|
||||
// 处理无效日期
|
||||
if (isNaN(date.getTime())) return dateString
|
||||
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}`
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
import axios from 'axios'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import store from '../store/index'
|
||||
import router from '../router/index'
|
||||
|
||||
// 创建axios实例
|
||||
const service = axios.create({
|
||||
baseURL: 'http://localhost:8877',
|
||||
timeout: 10000,
|
||||
withCredentials: true // 允许携带cookie
|
||||
})
|
||||
|
||||
// 请求拦截器
|
||||
service.interceptors.request.use(
|
||||
config => {
|
||||
|
||||
return config
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// 响应拦截器
|
||||
service.interceptors.response.use(
|
||||
response => {
|
||||
// 处理成功响应
|
||||
const res = response.data
|
||||
|
||||
// 处理业务错误 (code !== 200)
|
||||
if (res && typeof res === 'object' && res.code !== undefined && res.code !== 200) {
|
||||
// 检查是否为静默请求
|
||||
if (!response.config.silent) {
|
||||
ElMessage.error(res.message || '请求失败')
|
||||
}
|
||||
return Promise.reject(new Error(res.message || 'Error'))
|
||||
}
|
||||
|
||||
// 返回整个响应对象,确保组件可以访问响应头等信息
|
||||
return response
|
||||
},
|
||||
error => {
|
||||
// 处理HTTP错误
|
||||
if (error.response) {
|
||||
switch (error.response.status) {
|
||||
case 401:
|
||||
// 只有在非静默请求时才显示错误信息
|
||||
if (!error.config?.silent) {
|
||||
store.dispatch('logout')
|
||||
router.push('/login')
|
||||
ElMessage.error('请先登录')
|
||||
}
|
||||
break
|
||||
case 403:
|
||||
if (!error.config?.silent) {
|
||||
ElMessage.error('没有操作权限')
|
||||
}
|
||||
break
|
||||
default:
|
||||
if (!error.config?.silent) {
|
||||
ElMessage.error(error.response.data?.message || '请求失败')
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!error.config?.silent) {
|
||||
ElMessage.error('网络错误,请检查连接')
|
||||
}
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default service
|
@ -0,0 +1,43 @@
|
||||
// 安全地解析JSON字符串
|
||||
export function safeParseJSON(str, defaultValue = null) {
|
||||
if (!str) return defaultValue
|
||||
try {
|
||||
return JSON.parse(str)
|
||||
} catch (error) {
|
||||
console.error('JSON解析失败:', error)
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
// 安全地存储数据到sessionStorage
|
||||
export function safeSetItem(key, value) {
|
||||
try {
|
||||
sessionStorage.setItem(key, JSON.stringify(value))
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('存储数据失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 安全地从sessionStorage获取数据
|
||||
export function safeGetItem(key, defaultValue = null) {
|
||||
try {
|
||||
const item = sessionStorage.getItem(key)
|
||||
return item ? JSON.parse(item) : defaultValue
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error)
|
||||
return defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
// 安全地从sessionStorage删除数据
|
||||
export function safeRemoveItem(key) {
|
||||
try {
|
||||
sessionStorage.removeItem(key)
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('删除数据失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<div class="add-book-container">
|
||||
<el-card class="add-book-card">
|
||||
<h2 class="add-book-title">添加新书</h2>
|
||||
<el-form
|
||||
ref="addBookForm"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="100px">
|
||||
<el-form-item label="书名" prop="title">
|
||||
<el-input v-model="form.title" placeholder="请输入书名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="简介" prop="content">
|
||||
<el-input
|
||||
v-model="form.content"
|
||||
type="textarea"
|
||||
:rows="4"
|
||||
placeholder="请输入书籍简介" />
|
||||
</el-form-item>
|
||||
<el-form-item label="封面URL" prop="url">
|
||||
<el-input v-model="form.url" placeholder="请输入封面图片URL" />
|
||||
<div class="cover-preview" v-if="form.url">
|
||||
<el-image :src="form.url" fit="cover" class="preview-image" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="state">
|
||||
<el-select v-model="form.state" placeholder="请选择状态">
|
||||
<el-option label="可借" value="可借" />
|
||||
<el-option label="已借完" value="已借完" />
|
||||
<el-option label="维护中" value="维护中" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="价格(元/天)" prop="money">
|
||||
<el-input-number
|
||||
v-model="form.money"
|
||||
:min="0.1"
|
||||
:step="0.1"
|
||||
:precision="2" />
|
||||
</el-form-item>
|
||||
<el-form-item label="阅读量" prop="number">
|
||||
<el-input-number v-model="form.number" :min="1" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="submitForm" :loading="loading">提交</el-button>
|
||||
<el-button @click="resetForm">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useStore } from 'vuex'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
|
||||
const form = ref({
|
||||
title: '',
|
||||
content: '',
|
||||
url: '',
|
||||
state: '可借',
|
||||
money: 5.0,
|
||||
number: 1
|
||||
})
|
||||
|
||||
const rules = ref({
|
||||
title: [
|
||||
{ required: true, message: '请输入书名', trigger: 'blur' }
|
||||
],
|
||||
content: [
|
||||
{ required: true, message: '请输入书籍简介', trigger: 'blur' }
|
||||
],
|
||||
url: [
|
||||
{ required: true, message: '请输入封面URL', trigger: 'blur' }
|
||||
],
|
||||
state: [
|
||||
{ required: true, message: '请选择状态', trigger: 'change' }
|
||||
],
|
||||
money: [
|
||||
{ required: true, message: '请输入价格', trigger: 'blur' }
|
||||
],
|
||||
number: [
|
||||
{ required: true, message: '请输入阅读量', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
const addBookForm = ref(null)
|
||||
const loading = ref(false)
|
||||
|
||||
const submitForm = async () => {
|
||||
try {
|
||||
await addBookForm.value.validate()
|
||||
loading.value = true
|
||||
|
||||
await store.dispatch('addBook', form.value)
|
||||
ElMessage.success('添加书籍成功')
|
||||
router.push('/books')
|
||||
} catch (error) {
|
||||
console.error('添加书籍失败:', error)
|
||||
ElMessage.error(error.message || '添加书籍失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
addBookForm.value.resetFields()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.add-book-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.add-book-card {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.add-book-title {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.cover-preview {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.preview-image {
|
||||
width: 200px;
|
||||
height: 250px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<div class="book-list-container">
|
||||
<div class="search-bar">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索书名"
|
||||
clearable
|
||||
@clear="fetchBooks"
|
||||
@keyup.enter="fetchBooks"
|
||||
class="search-input">
|
||||
<template #append>
|
||||
<el-button :icon="Search" @click="fetchBooks" />
|
||||
</template>
|
||||
</el-input>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="goToAddBook"
|
||||
v-if="user && user.admin"
|
||||
class="add-button">
|
||||
<el-icon><Plus /></el-icon>
|
||||
添加书籍
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<div class="books-grid">
|
||||
<el-card
|
||||
v-for="book in books"
|
||||
:key="book.id"
|
||||
class="book-card"
|
||||
shadow="hover"
|
||||
@click="goToBookDetail(book.id)">
|
||||
<div class="book-cover">
|
||||
<el-image
|
||||
:src="book.url"
|
||||
fit="cover"
|
||||
class="cover-image"
|
||||
:alt="book.title" />
|
||||
</div>
|
||||
<div class="book-info">
|
||||
<h3 class="book-title">{{ book.title }}</h3>
|
||||
<div class="book-meta">
|
||||
<el-tag type="info" size="small">{{ book.content }}</el-tag>
|
||||
<el-tag type="success" size="small">¥{{ book.money }}/天</el-tag>
|
||||
</div>
|
||||
<div class="book-status">
|
||||
<el-tag
|
||||
:type="book.state === '正常' ? 'success' : 'danger'"
|
||||
size="small">
|
||||
{{ book.state }}
|
||||
</el-tag>
|
||||
<span>阅读量: {{ book.number }}次</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
layout="prev, pager, next, jumper"
|
||||
@current-change="fetchBooks"
|
||||
background />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useStore } from 'vuex'
|
||||
import { Search, Plus } from '@element-plus/icons-vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
const user = computed(() => store.state.user)
|
||||
const isAdmin = computed(() => user.value?.admin || false)
|
||||
const books = ref([])
|
||||
const searchKeyword = ref('')
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(12)
|
||||
const total = ref(0)
|
||||
|
||||
onMounted(() => {
|
||||
fetchBooks()
|
||||
})
|
||||
|
||||
const fetchBooks = async () => {
|
||||
try {
|
||||
const params = {
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
keyword: searchKeyword.value
|
||||
}
|
||||
|
||||
const response = await store.dispatch('fetchBooks', params)
|
||||
books.value = response.data.list
|
||||
total.value = response.data.total
|
||||
} catch (error) {
|
||||
console.error('获取书籍列表失败:', error)
|
||||
ElMessage.error('获取书籍列表失败')
|
||||
books.value = [
|
||||
{ id: 1, title: '三体', url: 'https://picsum.photos/id/24/200/300', money: 5, number: 12, state: '正常', content: '科幻' },
|
||||
{ id: 2, title: '人类简史', url: 'https://picsum.photos/id/25/200/300', money: 4, number: 10, state: '正常', content: '历史' },
|
||||
{ id: 3, title: '百年孤独', url: 'https://picsum.photos/id/26/200/300', money: 6, number: 0, state: '维护中', content: '文学' },
|
||||
]
|
||||
|
||||
total.value = books.value.length
|
||||
}
|
||||
}
|
||||
|
||||
const goToBookDetail = (id) => {
|
||||
router.push({ name: 'BookDetail', params: { id } })
|
||||
}
|
||||
|
||||
const goToAddBook = () => {
|
||||
router.push({ name: 'AddBook' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.book-list-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 300px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.books-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.book-card {
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.book-card:hover {
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
|
||||
.book-cover {
|
||||
height: 180px;
|
||||
overflow: hidden;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.cover-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.book-info {
|
||||
padding: 0 10px 10px;
|
||||
}
|
||||
|
||||
.book-title {
|
||||
margin: 0 0 10px;
|
||||
font-size: 16px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.book-meta {
|
||||
display: flex;
|
||||
gap: 5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.book-status {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,3 @@
|
||||
<template>
|
||||
<div>编辑图书页面</div>
|
||||
</template>
|
@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div class="borrow-container">
|
||||
<el-card class="borrow-card">
|
||||
<h2 class="borrow-title">借阅图书</h2>
|
||||
|
||||
<div class="search-bar">
|
||||
<el-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="搜索书名"
|
||||
clearable
|
||||
@clear="searchBooks"
|
||||
@keyup.enter="searchBooks"
|
||||
class="search-input">
|
||||
<template #append>
|
||||
<el-button :icon="Search" @click="searchBooks" />
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<div class="books-grid">
|
||||
<BookCard
|
||||
v-for="book in books"
|
||||
:key="book.id"
|
||||
:book="book"
|
||||
@borrow="handleBorrow(book)" />
|
||||
</div>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="currentPage"
|
||||
v-model:page-size="pageSize"
|
||||
:total="total"
|
||||
layout="prev, pager, next, jumper"
|
||||
@current-change="fetchBooks"
|
||||
background />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import BookCard from '@/components/BookCard.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const store = useStore()
|
||||
|
||||
const books = ref([])
|
||||
const searchKeyword = ref('')
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(8)
|
||||
const total = ref(0)
|
||||
|
||||
onMounted(() => {
|
||||
fetchBooks()
|
||||
})
|
||||
|
||||
const fetchBooks = async () => {
|
||||
try {
|
||||
const params = {
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
keyword: searchKeyword.value
|
||||
}
|
||||
|
||||
const response = await store.dispatch('fetchBooks', params)
|
||||
books.value = response.data.list
|
||||
total.value = response.data.total
|
||||
} catch (error) {
|
||||
console.error('获取书籍列表失败:', error)
|
||||
ElMessage.error('获取书籍列表失败')
|
||||
// 预设数据
|
||||
books.value = [
|
||||
{ id: 1, title: '三体', url: 'https://picsum.photos/id/24/200/300', money: 5, number: 12, state: '正常', content: '科幻' },
|
||||
{ id: 2, title: '人类简史', url: 'https://picsum.photos/id/25/200/300', money: 4, number: 10, state: '正常', content: '历史' },
|
||||
{ id: 3, title: '百年孤独', url: 'https://picsum.photos/id/26/200/300', money: 6, number: 0, state: '维护中', content: '文学' },
|
||||
]
|
||||
total.value = books.value.length
|
||||
}
|
||||
}
|
||||
|
||||
const searchBooks = () => {
|
||||
currentPage.value = 1
|
||||
fetchBooks()
|
||||
}
|
||||
|
||||
const handleBorrow = async (book) => {
|
||||
try {
|
||||
if (book.state !=='正常') {
|
||||
ElMessage.warning('该书籍维护中,无法借阅')
|
||||
return
|
||||
}
|
||||
|
||||
await store.dispatch('borrowBook', { title: book.title })
|
||||
ElMessage.success(`成功借阅《${book.title}》`)
|
||||
// 刷新列表
|
||||
await fetchBooks()
|
||||
} catch (error) {
|
||||
console.error('借阅失败:', error)
|
||||
ElMessage.error(error.message || '借阅失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.borrow-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.borrow-card {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.borrow-title {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.books-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<div class="return-container">
|
||||
<el-card class="return-card">
|
||||
<h2 class="return-title">归还图书</h2>
|
||||
|
||||
<div class="books-grid">
|
||||
<el-card
|
||||
v-for="book in borrowedBooks"
|
||||
:key="book.id || book.title"
|
||||
class="borrowed-book-card"
|
||||
shadow="hover">
|
||||
<div class="book-info">
|
||||
<el-image :src="book.url" class="book-cover" fit="cover" />
|
||||
<div class="book-details">
|
||||
<h3 class="book-title">{{ book.title }}</h3>
|
||||
<p class="borrow-time">借阅量: {{ book.borrow_time }}</p>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="handleReturn(book)"
|
||||
class="return-button">
|
||||
立即归还
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<div class="no-books" v-if="borrowedBooks.length === 0">
|
||||
<el-empty description="暂无借阅中的图书" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { formatDate } from '@/utils/date'
|
||||
|
||||
const store = useStore()
|
||||
const borrowedBooks = computed(() => store.state.borrowedBooks)
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchBorrowedBooks()
|
||||
})
|
||||
|
||||
const fetchBorrowedBooks = async () => {
|
||||
try {
|
||||
await store.dispatch('fetchBorrowedBooks')
|
||||
} catch (error) {
|
||||
console.error('获取已借书籍失败:', error)
|
||||
ElMessage.error('获取已借书籍失败')
|
||||
// 预设数据
|
||||
store.commit('setBorrowedBooks', [
|
||||
{
|
||||
id: 1,
|
||||
title: '三体',
|
||||
url: 'https://picsum.photos/id/24/200/300',
|
||||
borrow_time: '2023-10-01T10:30:00'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '活着',
|
||||
url: 'https://picsum.photos/id/27/200/300',
|
||||
borrow_time: '2023-10-05T14:20:00'
|
||||
}
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
const handleReturn = async (book) => {
|
||||
try {
|
||||
const response = await store.dispatch('returnBook', { title: book.title })
|
||||
|
||||
if (response.code === 200) {
|
||||
ElMessage.success(`《${book.title}》归还成功`)
|
||||
|
||||
// 从本地状态中移除已归还的书籍
|
||||
store.commit('removeBorrowedBook', book.title)
|
||||
|
||||
// 重新获取借阅书籍列表以确保数据同步
|
||||
await fetchBorrowedBooks()
|
||||
} else {
|
||||
ElMessage.error(response.message || '归还失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('归还失败:', error)
|
||||
ElMessage.error(error.message || '归还失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.return-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.return-card {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.return-title {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.books-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.borrowed-book-card {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.book-info {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.book-cover {
|
||||
width: 120px;
|
||||
height: 180px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.book-details {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.book-title {
|
||||
margin-top: 0;
|
||||
margin-bottom: 15px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.borrow-time {
|
||||
color: #666;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.return-button {
|
||||
margin-top: auto;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.no-books {
|
||||
margin: 50px 0;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div class="ranking-container">
|
||||
<el-card class="ranking-card">
|
||||
<h2 class="ranking-title">本月热租榜</h2>
|
||||
|
||||
<el-table :data="rankList" style="width: 100%">
|
||||
<el-table-column type="index" label="排名" width="80" />
|
||||
<el-table-column label="封面" width="120">
|
||||
<template #default="scope">
|
||||
<el-image :src="scope.row.url" class="cover-small" fit="cover" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="title" label="书名" width="200" />
|
||||
<el-table-column prop="number" label="阅读量" width="120" />
|
||||
<el-table-column prop="money" label="价格(元/天)" width="120">
|
||||
<template #default="scope">
|
||||
¥{{ scope.row.money }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="操作" width="120">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="text"
|
||||
@click="handleBorrow(scope.row)"
|
||||
:disabled="scope.row.number <= 0">
|
||||
立即借阅
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="no-data" v-if="rankList.length === 0">
|
||||
<el-empty description="暂无排行数据" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const store = useStore()
|
||||
const rankList = ref([])
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchMonthlyRank()
|
||||
})
|
||||
|
||||
const fetchMonthlyRank = async () => {
|
||||
try {
|
||||
const response = await store.dispatch('fetchMonthlyRank')
|
||||
rankList.value = response.data
|
||||
} catch (error) {
|
||||
console.error('获取本月热租榜失败:', error)
|
||||
ElMessage.error('获取本月热租榜失败')
|
||||
// 预设数据
|
||||
books.value = [
|
||||
{ title: '三体', url: 'https://picsum.photos/id/24/200/300', money: 5, number: 12},
|
||||
{ title: '人类简史', url: 'https://picsum.photos/id/25/200/300', money: 4, number: 10 },
|
||||
{ title: '百年孤独', url: 'https://picsum.photos/id/26/200/300', money: 6, number: 8},
|
||||
{ title: '活着', url: 'https://picsum.photos/id/27/200/300', money: 3, number: 15 },
|
||||
{ title: '追风筝的人', url: 'https://picsum.photos/id/28/200/300', money: 4, number: 9 },
|
||||
{ title: '解忧杂货店', url: 'https://picsum.photos/id/29/200/300', money: 5, number: 7 },
|
||||
{ title: '小王子', url: 'https://picsum.photos/id/30/200/300', money: 2, number: 20 },
|
||||
{ title: '围城', url: 'https://picsum.photos/id/31/200/300', money: 4, number: 18 }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const handleBorrow = async (book) => {
|
||||
try {
|
||||
if (book.number <= 0) {
|
||||
ElMessage.warning('该书籍已无库存')
|
||||
return
|
||||
}
|
||||
|
||||
await store.dispatch('borrowBook', { title: book.title })
|
||||
ElMessage.success(`成功借阅《${book.title}》`)
|
||||
await fetchMonthlyRank()
|
||||
} catch (error) {
|
||||
console.error('借阅失败:', error)
|
||||
ElMessage.error(error.message || '借阅失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ranking-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.ranking-card {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.ranking-title {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.cover-small {
|
||||
width: 80px;
|
||||
height: 120px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
margin: 40px 0;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<div class="ranking-container">
|
||||
<el-card class="ranking-card">
|
||||
<h2 class="ranking-title">本周热租榜</h2>
|
||||
|
||||
<el-table :data="rankList" style="width: 100%">
|
||||
<el-table-column type="index" label="排名" width="80" />
|
||||
<el-table-column label="封面" width="120">
|
||||
<template #default="scope">
|
||||
<el-image :src="scope.row.url" class="cover-small" fit="cover" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="title" label="书名" width="200" />
|
||||
<el-table-column prop="number" label="租借次数" width="120" />
|
||||
<el-table-column prop="money" label="价格(元/天)" width="120">
|
||||
<template #default="scope">
|
||||
¥{{ scope.row.money }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
type="text"
|
||||
@click="handleBorrow(scope.row)"
|
||||
:disabled="scope.row.number <= 0">
|
||||
立即借阅
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="no-data" v-if="rankList.length === 0">
|
||||
<el-empty description="暂无排行数据" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const store = useStore()
|
||||
const rankList = ref([])
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchWeeklyRank()
|
||||
})
|
||||
|
||||
const fetchWeeklyRank = async () => {
|
||||
try {
|
||||
const response = await store.dispatch('fetchWeeklyRank')
|
||||
rankList.value = response.data
|
||||
} catch (error) {
|
||||
console.error('获取本周热租榜失败:', error)
|
||||
ElMessage.error('获取本周热租榜失败')
|
||||
// 预设数据
|
||||
books.value = [
|
||||
{ title: '三体', url: 'https://picsum.photos/id/24/200/300', money: 5, number: 12},
|
||||
{ title: '人类简史', url: 'https://picsum.photos/id/25/200/300', money: 4, number: 10 },
|
||||
{ title: '百年孤独', url: 'https://picsum.photos/id/26/200/300', money: 6, number: 8},
|
||||
{ title: '活着', url: 'https://picsum.photos/id/27/200/300', money: 3, number: 15 },
|
||||
{ title: '追风筝的人', url: 'https://picsum.photos/id/28/200/300', money: 4, number: 9 },
|
||||
{ title: '解忧杂货店', url: 'https://picsum.photos/id/29/200/300', money: 5, number: 7 },
|
||||
{ title: '小王子', url: 'https://picsum.photos/id/30/200/300', money: 2, number: 20 },
|
||||
{ title: '围城', url: 'https://picsum.photos/id/31/200/300', money: 4, number: 18 }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
const handleBorrow = async (book) => {
|
||||
try {
|
||||
if (book.number <= 0) {
|
||||
ElMessage.warning('该书籍已无库存')
|
||||
return
|
||||
}
|
||||
|
||||
await store.dispatch('borrowBook', { title: book.title })
|
||||
ElMessage.success(`成功借阅《${book.title}》`)
|
||||
await fetchWeeklyRank()
|
||||
} catch (error) {
|
||||
console.error('借阅失败:', error)
|
||||
ElMessage.error(error.message || '借阅失败')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ranking-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.ranking-card {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.ranking-title {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.cover-small {
|
||||
width: 80px;
|
||||
height: 120px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
margin: 40px 0;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<div class="records-container">
|
||||
<el-card class="records-card">
|
||||
<h2 class="records-title">借阅记录</h2>
|
||||
|
||||
<el-table :data="records" style="width: 100%">
|
||||
<el-table-column prop="title" label="书名" width="180" />
|
||||
<el-table-column prop="borrow_time" label="借阅时间" width="200">
|
||||
<template #default="scope">
|
||||
{{ formatDate(scope.row.borrow_time) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="return_time" label="归还时间" width="200">
|
||||
<template #default="scope">
|
||||
{{ scope.row.return_time ? formatDate(scope.row.return_time) : '尚未归还' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="scope">
|
||||
<el-tag :type="scope.row.return_time ? 'success' : 'warning'">
|
||||
{{ scope.row.return_time ? '已归还' : '借阅中' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="no-data" v-if="records.length === 0">
|
||||
<el-empty description="暂无借阅记录" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { formatDate } from '@/utils/date'
|
||||
|
||||
const store = useStore()
|
||||
const records = ref([])
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchRecords()
|
||||
})
|
||||
|
||||
const fetchRecords = async () => {
|
||||
try {
|
||||
const response = await store.dispatch('fetchBorrowRecords')
|
||||
records.value = response.data
|
||||
} catch (error) {
|
||||
console.error('获取借阅记录失败:', error)
|
||||
ElMessage.error('获取借阅记录失败')
|
||||
records.value = [
|
||||
{
|
||||
title: '三体',
|
||||
borrow_time: '2023-10-01 10:30:00',
|
||||
return_time: null,
|
||||
|
||||
},
|
||||
{
|
||||
title: '人类简史',
|
||||
borrow_time: '2023-09-15 09:15:00',
|
||||
return_time: '2023-09-30 16:40:00',
|
||||
|
||||
},
|
||||
{
|
||||
title: '百年孤独',
|
||||
borrow_time: '2023-08-20 15:20:00',
|
||||
return_time: '2023-09-05 11:30:00',
|
||||
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.records-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.records-card {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.records-title {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.no-data {
|
||||
margin: 40px 0;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<div class="profile-container">
|
||||
<el-card class="profile-card">
|
||||
<div class="profile-header">
|
||||
<el-avatar :src="user.pic" :size="100" class="avatar" />
|
||||
<div class="user-info">
|
||||
<h2>{{ user.username }}</h2>
|
||||
<div class="vip-level">
|
||||
<el-tag type="warning" size="large">VIP{{ vip }}级</el-tag>
|
||||
</div>
|
||||
<div class="balance">
|
||||
<el-text type="primary" size="large">
|
||||
<el-icon><Wallet /></el-icon>
|
||||
余额: ¥{{ balance }}
|
||||
</el-text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-divider />
|
||||
|
||||
<div class="profile-details">
|
||||
<el-descriptions title="个人信息" :column="2" border>
|
||||
<el-descriptions-item label="用户名">{{ user.username }}</el-descriptions-item>
|
||||
<el-descriptions-item label="账户类型">
|
||||
<el-tag :type="isAdmin ? 'danger' : 'success'">
|
||||
{{ isAdmin ? '管理员' : '普通用户' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="注册时间">{{ formatDate(user.create_time) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="最后登录时间">{{ formatDate(user.update_time) }}</el-descriptions-item>
|
||||
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<div class="profile-actions">
|
||||
<el-button type="primary" @click="goToRecharge">账户充值</el-button>
|
||||
<el-button @click="goToRecords">查看借阅记录</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useStore } from 'vuex'
|
||||
import { Wallet } from '@element-plus/icons-vue'
|
||||
import { formatDate } from '@/utils/date'
|
||||
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
|
||||
const user = computed(() => store.state.user || {})
|
||||
const balance = computed(() => store.state.balance)
|
||||
const vip = computed(() => store.state.vip)
|
||||
const isAdmin = computed(() => store.getters.isAdmin)
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
store.dispatch('fetchUser')
|
||||
}catch (error) {
|
||||
console.error('获取用户信息失败:', error)
|
||||
ElMessage.error('获取用户信息失败,展示示例数据')
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
const goToRecharge = () => {
|
||||
router.push('/recharge')
|
||||
}
|
||||
|
||||
const goToRecords = () => {
|
||||
router.push('/borrow-records')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.profile-container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.profile-card {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.profile-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin-right: 30px;
|
||||
}
|
||||
|
||||
.user-info h2 {
|
||||
margin: 0 0 10px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.vip-level {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.balance {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.profile-details {
|
||||
margin: 30px 0;
|
||||
}
|
||||
|
||||
.profile-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<div class="recharge-container">
|
||||
<el-card class="recharge-card">
|
||||
<h2 class="recharge-title">账户充值</h2>
|
||||
|
||||
<div class="balance-display">
|
||||
<el-text type="primary" size="large">
|
||||
当前余额: <span class="balance-amount">¥{{ balance || 0 }}</span>
|
||||
</el-text>
|
||||
</div>
|
||||
|
||||
<el-form
|
||||
ref="rechargeForm"
|
||||
:model="form"
|
||||
:rules="rules"
|
||||
label-width="100px">
|
||||
<el-form-item label="充值金额" prop="money">
|
||||
<el-input-number
|
||||
v-model="form.money"
|
||||
:min="10"
|
||||
:step="10"
|
||||
:precision="2"
|
||||
controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="submitRecharge">立即充值</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="recharge-options">
|
||||
<h3>快捷充值</h3>
|
||||
<div class="options-grid">
|
||||
<el-button
|
||||
v-for="amount in [10, 50, 100,150,200,300,400,500]"
|
||||
:key="amount"
|
||||
@click="quickRecharge(amount)">
|
||||
¥{{ amount }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useStore } from 'vuex'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import service from '../../utils/request'
|
||||
|
||||
const store = useStore()
|
||||
const user = computed(() => store.state.user || {})
|
||||
const balance = ref(0)
|
||||
console.log(store.state.user)
|
||||
onMounted(async ()=>{
|
||||
const response = await service.post("/user/findmoney")
|
||||
balance.value = response.data.data.balance
|
||||
})
|
||||
const form = ref({
|
||||
money: 10
|
||||
})
|
||||
|
||||
const rules = ref({
|
||||
money: [
|
||||
{ required: true, message: '请输入充值金额', trigger: 'blur' },
|
||||
{ type: 'number', min: 0.01, message: '充值金额必须大于0', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
const rechargeForm = ref(null)
|
||||
|
||||
const submitRecharge = async () => {
|
||||
try {
|
||||
await rechargeForm.value.validate()
|
||||
|
||||
await store.dispatch('recharge', { money: form.value.money })
|
||||
ElMessage.success(`成功充值¥${form.value.money}元`)
|
||||
|
||||
// 更新用户信息
|
||||
await store.dispatch('fetchUser')
|
||||
} catch (error) {
|
||||
console.error('充值失败:', error)
|
||||
ElMessage.error(error.message || '充值失败')
|
||||
}
|
||||
}
|
||||
|
||||
const quickRecharge = (amount) => {
|
||||
form.value.money = amount
|
||||
submitRecharge()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.recharge-container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.recharge-card {
|
||||
padding: 30px;
|
||||
}
|
||||
|
||||
.recharge-title {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.balance-display {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.balance-amount {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.recharge-options {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
.recharge-options h3 {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.options-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,13 @@
|
||||
// vite.config.js
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import path from 'path' // 确保引入 path 模块
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src') // 关键配置:@ 指向 src 目录
|
||||
}
|
||||
}
|
||||
})
|
Loading…
Reference in new issue