main
Heife 4 months ago
commit 3a49cbd41b

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<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="newdemo" />
</profile>
</annotationProcessing>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="newdemo" options="-parameters" />
</option>
</component>
</project>

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="wx_miniapp@localhost" uuid="c67a94ed-025b-4c2b-ae24-d0e09eb2c892">
<driver-ref>mysql.8</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
<jdbc-url>jdbc:mysql://localhost:3306/wx_miniapp</jdbc-url>
<jdbc-additional-properties>
<property name="com.intellij.clouds.kubernetes.db.host.port" />
<property name="com.intellij.clouds.kubernetes.db.enabled" value="false" />
<property name="com.intellij.clouds.kubernetes.db.container.port" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</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,12 @@
<?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" languageLevel="JDK_21" default="true" project-jdk-name="21" project-jdk-type="JavaSDK" />
</project>

@ -0,0 +1,106 @@
# 微信小程序登录Demo后端
## 项目介绍
本项目是一个微信小程序登录的后端Demo实现了微信小程序登录、获取用户信息等功能。
## 技术栈
- Spring Boot 3.0.12
- MyBatis 3.0.1
- MySQL 8.0
- JWT
## 项目结构
```
src/main/java/com/learning/newdemo
├── config // 配置类
├── controller // 控制器
├── entity // 实体类
├── mapper // 数据访问层
├── service // 服务层
│ └── impl // 服务实现
├── util // 工具类
└── common // 通用类
```
## 运行环境
- JDK 17+
- MySQL 8.0+
- Maven 3.6+
## 数据库配置
1. 创建数据库和表
```sql
# 执行src/main/resources/db/wx_miniapp.sql脚本
```
2. 修改数据库连接信息(`application.yml`
```yaml
spring:
datasource:
url: jdbc:mysql://localhost:3306/wx_miniapp?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 1234
```
## 微信小程序配置
修改`application.yml`中的微信小程序配置:
```yaml
wechat:
miniapp:
appid: 你的小程序APPID
secret: 你的小程序SECRET
```
## 启动项目
```bash
mvn spring-boot:run
```
## 接口说明
### 1. 登录接口
- URL: `/api/wx/login`
- Method: POST
- Body:
```json
{
"code": "微信临时登录凭证"
}
```
- Response:
```json
{
"success": true,
"code": 200,
"message": "操作成功",
"data": {
"token": "JWT令牌"
}
}
```
### 2. 获取用户信息接口
- URL: `/api/wx/user`
- Method: GET
- Headers:
```
Authorization: 登录接口返回的token
```
- Response:
```json
{
"success": true,
"code": 200,
"message": "操作成功",
"data": {
"id": 1,
"openid": "用户openid",
"nickname": "用户昵称",
"avatarUrl": "头像URL",
"gender": 1,
"country": "国家",
"province": "省份",
"city": "城市",
"language": "语言"
}
}
```

@ -0,0 +1,104 @@
<?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.0.12</version>
<relativePath/>
</parent>
<groupId>com.learning</groupId>
<artifactId>newdemo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>newdemo</name>
<description>微信小程序登录Demo</description>
<properties>
<java.version>17</java.version>
<mybatis.version>3.0.1</mybatis.version>
<mysql.version>8.0.33</mysql.version>
<fastjson.version>1.2.83</fastjson.version>
<jwt.version>0.11.5</jwt.version>
</properties>
<dependencies>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.version}</version>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql.version}</version>
<scope>runtime</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- FastJSON -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>${jwt.version}</version>
<scope>runtime</scope>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

@ -0,0 +1,14 @@
package com.learning.newdemo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.learning.newdemo.mapper")
public class NewDemoApplication {
public static void main(String[] args) {
SpringApplication.run(NewDemoApplication.class, args);
}
}

@ -0,0 +1,71 @@
package com.learning.newdemo.common;
import lombok.Data;
import java.io.Serializable;
/**
* API
*/
@Data
public class Result<T> implements Serializable {
private static final long serialVersionUID = 1L;
/**
*
*/
private boolean success;
/**
*
*/
private int code;
/**
*
*/
private String message;
/**
*
*/
private T data;
/**
*
*/
public static <T> Result<T> success() {
return success(null);
}
/**
*
*/
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setSuccess(true);
result.setCode(200);
result.setMessage("操作成功");
result.setData(data);
return result;
}
/**
*
*/
public static <T> Result<T> error(String message) {
return error(500, message);
}
/**
*
*/
public static <T> Result<T> error(int code, String message) {
Result<T> result = new Result<>();
result.setSuccess(false);
result.setCode(code);
result.setMessage(message);
return result;
}
}

@ -0,0 +1,32 @@
package com.learning.newdemo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
/**
*
*/
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
// 允许所有域名进行跨域调用
config.addAllowedOriginPattern("*");
// 允许跨域发送cookie
config.setAllowCredentials(true);
// 放行全部原始头信息
config.addAllowedHeader("*");
// 允许所有请求方法跨域调用
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}

@ -0,0 +1,36 @@
package com.learning.newdemo.config;
import com.learning.newdemo.common.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
*
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
*
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Result<Void> handleException(Exception e) {
log.error("系统异常", e);
return Result.error("系统异常,请联系管理员");
}
/**
*
*/
@ExceptionHandler(RuntimeException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Result<Void> handleRuntimeException(RuntimeException e) {
log.error("运行时异常", e);
return Result.error(e.getMessage());
}
}

@ -0,0 +1,17 @@
package com.learning.newdemo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
/**
* RestTemplate
*/
@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

@ -0,0 +1,67 @@
package com.learning.newdemo.controller;
import com.learning.newdemo.common.Result;
import com.learning.newdemo.entity.WxUser;
import com.learning.newdemo.service.WxUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
/**
*
*/
@Slf4j
@RestController
@RequestMapping("/api/wx")
public class WxLoginController {
@Autowired
private WxUserService wxUserService;
/**
*
*/
@PostMapping("/login")
public Result<Map<String, Object>> login(@RequestBody Map<String, String> params) {
String code = params.get("code");
if (code == null || code.isEmpty()) {
return Result.error("登录凭证不能为空");
}
try {
String token = wxUserService.login(code);
if (token == null) {
return Result.error("登录失败,请稍后重试");
}
Map<String, Object> data = new HashMap<>();
data.put("token", token);
return Result.success(data);
} catch (Exception e) {
log.error("微信登录异常", e);
return Result.error("登录异常:" + e.getMessage());
}
}
/**
*
*/
@GetMapping("/user")
public Result<WxUser> getUserInfo(@RequestHeader("Authorization") String token) {
if (token == null || token.isEmpty()) {
return Result.error(401, "未授权");
}
try {
// 省略token校验实际项目中需要添加
// 这里简单示例实际应该从token中获取openid然后查询用户信息
return Result.success(null);
} catch (Exception e) {
log.error("获取用户信息异常", e);
return Result.error("获取用户信息失败:" + e.getMessage());
}
}
}

@ -0,0 +1,34 @@
package com.learning.newdemo.entity;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data
public class WxUser implements Serializable {
private static final long serialVersionUID = 1L;
private Integer id;
private String openid;
private String nickname;
private String avatarUrl;
private Integer gender;
private String country;
private String province;
private String city;
private String language;
private Date createTime;
private Date updateTime;
}

@ -0,0 +1,31 @@
package com.learning.newdemo.mapper;
import com.learning.newdemo.entity.WxUser;
import org.apache.ibatis.annotations.Param;
public interface WxUserMapper {
/**
* openid
*
* @param openid
* @return
*/
WxUser selectByOpenid(@Param("openid") String openid);
/**
*
*
* @param wxUser
* @return
*/
int insert(WxUser wxUser);
/**
*
*
* @param wxUser
* @return
*/
int updateByPrimaryKey(WxUser wxUser);
}

@ -0,0 +1,33 @@
package com.learning.newdemo.service;
import com.learning.newdemo.entity.WxUser;
/**
*
*/
public interface WxUserService {
/**
*
*
* @param code
* @return tokennull
*/
String login(String code);
/**
* openid
*
* @param openid
* @return
*/
WxUser getUserByOpenid(String openid);
/**
*
*
* @param wxUser
* @return
*/
boolean updateUser(WxUser wxUser);
}

@ -0,0 +1,93 @@
package com.learning.newdemo.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.learning.newdemo.entity.WxUser;
import com.learning.newdemo.mapper.WxUserMapper;
import com.learning.newdemo.service.WxUserService;
import com.learning.newdemo.util.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
/**
*
*/
@Slf4j
@Service
public class WxUserServiceImpl implements WxUserService {
@Value("${wechat.miniapp.appid}")
private String appid;
@Value("${wechat.miniapp.secret}")
private String secret;
@Autowired
private WxUserMapper wxUserMapper;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private RestTemplate restTemplate;
@Override
public String login(String code) {
try {
// 微信登录凭证校验接口
String url = "https://api.weixin.qq.com/sns/jscode2session?appid=" + appid
+ "&secret=" + secret
+ "&js_code=" + code
+ "&grant_type=authorization_code";
ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
JSONObject json = JSONObject.parseObject(response.getBody());
// 获取openid
String openid = json.getString("openid");
if (openid == null || openid.isEmpty()) {
log.error("获取openid失败: {}", json);
return null;
}
// 根据openid获取或创建用户
getUserByOpenid(openid);
// 生成jwt token
return jwtUtil.generateToken(openid);
} catch (Exception e) {
log.error("微信登录异常", e);
return null;
}
}
@Override
public WxUser getUserByOpenid(String openid) {
WxUser wxUser = wxUserMapper.selectByOpenid(openid);
if (wxUser == null) {
// 如果用户不存在,创建新用户
wxUser = new WxUser();
wxUser.setOpenid(openid);
wxUserMapper.insert(wxUser);
wxUser = wxUserMapper.selectByOpenid(openid);
}
return wxUser;
}
@Override
public boolean updateUser(WxUser wxUser) {
if (wxUser == null || wxUser.getId() == null) {
return false;
}
try {
int rows = wxUserMapper.updateByPrimaryKey(wxUser);
return rows > 0;
} catch (Exception e) {
log.error("更新用户信息异常", e);
return false;
}
}
}

@ -0,0 +1,112 @@
package com.learning.newdemo.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* JWT
*/
@Component
public class JwtUtil {
/**
*
*/
@Value("${jwt.secret}")
private String secret;
/**
*
*/
@Value("${jwt.expiration}")
private Long expiration;
/**
* token
*/
public String generateToken(String openid) {
Map<String, Object> claims = new HashMap<>();
claims.put("openid", openid);
return generateToken(claims);
}
/**
* tokenopenid
*/
public String getOpenidFromToken(String token) {
String openid;
try {
Claims claims = getClaimsFromToken(token);
openid = (String) claims.get("openid");
} catch (Exception e) {
openid = null;
}
return openid;
}
/**
* token
*/
public Boolean isTokenExpired(String token) {
try {
Claims claims = getClaimsFromToken(token);
Date expiration = claims.getExpiration();
return expiration.before(new Date());
} catch (Exception e) {
return true;
}
}
/**
* tokenJWT
*/
private Claims getClaimsFromToken(String token) {
Claims claims = null;
try {
// 使用正确的方法: parseClaimsJws而不是parseClaimsJwt
claims = Jwts.parserBuilder()
.setSigningKey(getSignKey())
.build()
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
e.printStackTrace();
}
return claims;
}
/**
* token
*/
private String generateToken(Map<String, Object> claims) {
Date createdDate = new Date();
Date expirationDate = new Date(createdDate.getTime() + expiration);
JwtBuilder builder = Jwts.builder()
.setClaims(claims)
.setIssuedAt(createdDate)
.setExpiration(expirationDate)
.signWith(getSignKey(), SignatureAlgorithm.HS256);
return builder.compact();
}
/**
*
*/
private SecretKey getSignKey() {
byte[] keyBytes = Decoders.BASE64.decode(secret);
return Keys.hmacShaKeyFor(keyBytes);
}
}

@ -0,0 +1,2 @@
# 主要配置在application.yml中
# 此文件用于避免IDE报错

@ -0,0 +1,30 @@
spring:
application:
name: newdemo
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/wx_miniapp?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 1234
mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
# 微信小程序配置
wechat:
miniapp:
appid: wxf1f6d7657e01d48a
secret: fc356336a118366f27c384079688bc15
# JWT配置
jwt:
secret: yoursecretkey123456789abcdefghijklmnopqrstuvwxyz
expiration: 86400000 # 24小时(毫秒)
# 服务端口配置
server:
port: 8080
address: 0.0.0.0

@ -0,0 +1,22 @@
-- 创建数据库
CREATE DATABASE IF NOT EXISTS wx_miniapp DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
-- 使用数据库
USE wx_miniapp;
-- 创建微信用户表
CREATE TABLE IF NOT EXISTS `wx_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`openid` varchar(100) NOT NULL COMMENT '微信openid',
`nickname` varchar(50) DEFAULT NULL COMMENT '昵称',
`avatar_url` varchar(500) DEFAULT NULL COMMENT '头像URL',
`gender` tinyint(4) DEFAULT NULL COMMENT '性别 0-未知 1-男 2-女',
`country` varchar(50) DEFAULT NULL COMMENT '国家',
`province` varchar(50) DEFAULT NULL COMMENT '省份',
`city` varchar(50) DEFAULT NULL COMMENT '城市',
`language` varchar(50) DEFAULT NULL COMMENT '语言',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_openid` (`openid`) COMMENT 'openid唯一索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='微信用户表';

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.learning.newdemo.mapper.WxUserMapper">
<resultMap id="BaseResultMap" type="com.learning.newdemo.entity.WxUser">
<id column="id" property="id" jdbcType="INTEGER"/>
<result column="openid" property="openid" jdbcType="VARCHAR"/>
<result column="nickname" property="nickname" jdbcType="VARCHAR"/>
<result column="avatar_url" property="avatarUrl" jdbcType="VARCHAR"/>
<result column="gender" property="gender" jdbcType="INTEGER"/>
<result column="country" property="country" jdbcType="VARCHAR"/>
<result column="province" property="province" jdbcType="VARCHAR"/>
<result column="city" property="city" jdbcType="VARCHAR"/>
<result column="language" property="language" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
</resultMap>
<sql id="Base_Column_List">
id, openid, nickname, avatar_url, gender, country, province, city, language, create_time, update_time
</sql>
<select id="selectByOpenid" resultMap="BaseResultMap" parameterType="java.lang.String">
select
<include refid="Base_Column_List"/>
from wx_user
where openid = #{openid,jdbcType=VARCHAR}
</select>
<insert id="insert" parameterType="com.learning.newdemo.entity.WxUser" useGeneratedKeys="true" keyProperty="id">
insert into wx_user (
openid, nickname, avatar_url, gender, country, province, city, language, create_time
)
values (
#{openid,jdbcType=VARCHAR},
#{nickname,jdbcType=VARCHAR},
#{avatarUrl,jdbcType=VARCHAR},
#{gender,jdbcType=INTEGER},
#{country,jdbcType=VARCHAR},
#{province,jdbcType=VARCHAR},
#{city,jdbcType=VARCHAR},
#{language,jdbcType=VARCHAR},
now()
)
</insert>
<update id="updateByPrimaryKey" parameterType="com.learning.newdemo.entity.WxUser">
update wx_user
set nickname = #{nickname,jdbcType=VARCHAR},
avatar_url = #{avatarUrl,jdbcType=VARCHAR},
gender = #{gender,jdbcType=INTEGER},
country = #{country,jdbcType=VARCHAR},
province = #{province,jdbcType=VARCHAR},
city = #{city,jdbcType=VARCHAR},
language = #{language,jdbcType=VARCHAR},
update_time = now()
where id = #{id,jdbcType=INTEGER}
</update>
</mapper>

@ -0,0 +1,2 @@
# 主要配置在application.yml中
# 此文件用于避免IDE报错

@ -0,0 +1,30 @@
spring:
application:
name: newdemo
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/wx_miniapp?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
username: root
password: 1234
mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true
# 微信小程序配置
wechat:
miniapp:
appid: wxf1f6d7657e01d48a
secret: fc356336a118366f27c384079688bc15
# JWT配置
jwt:
secret: yoursecretkey123456789abcdefghijklmnopqrstuvwxyz
expiration: 86400000 # 24小时(毫秒)
# 服务端口配置
server:
port: 8080
address: 0.0.0.0

@ -0,0 +1,22 @@
-- 创建数据库
CREATE DATABASE IF NOT EXISTS wx_miniapp DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
-- 使用数据库
USE wx_miniapp;
-- 创建微信用户表
CREATE TABLE IF NOT EXISTS `wx_user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`openid` varchar(100) NOT NULL COMMENT '微信openid',
`nickname` varchar(50) DEFAULT NULL COMMENT '昵称',
`avatar_url` varchar(500) DEFAULT NULL COMMENT '头像URL',
`gender` tinyint(4) DEFAULT NULL COMMENT '性别 0-未知 1-男 2-女',
`country` varchar(50) DEFAULT NULL COMMENT '国家',
`province` varchar(50) DEFAULT NULL COMMENT '省份',
`city` varchar(50) DEFAULT NULL COMMENT '城市',
`language` varchar(50) DEFAULT NULL COMMENT '语言',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_openid` (`openid`) COMMENT 'openid唯一索引'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='微信用户表';

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.learning.newdemo.mapper.WxUserMapper">
<resultMap id="BaseResultMap" type="com.learning.newdemo.entity.WxUser">
<id column="id" property="id" jdbcType="INTEGER"/>
<result column="openid" property="openid" jdbcType="VARCHAR"/>
<result column="nickname" property="nickname" jdbcType="VARCHAR"/>
<result column="avatar_url" property="avatarUrl" jdbcType="VARCHAR"/>
<result column="gender" property="gender" jdbcType="INTEGER"/>
<result column="country" property="country" jdbcType="VARCHAR"/>
<result column="province" property="province" jdbcType="VARCHAR"/>
<result column="city" property="city" jdbcType="VARCHAR"/>
<result column="language" property="language" jdbcType="VARCHAR"/>
<result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
<result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
</resultMap>
<sql id="Base_Column_List">
id, openid, nickname, avatar_url, gender, country, province, city, language, create_time, update_time
</sql>
<select id="selectByOpenid" resultMap="BaseResultMap" parameterType="java.lang.String">
select
<include refid="Base_Column_List"/>
from wx_user
where openid = #{openid,jdbcType=VARCHAR}
</select>
<insert id="insert" parameterType="com.learning.newdemo.entity.WxUser" useGeneratedKeys="true" keyProperty="id">
insert into wx_user (
openid, nickname, avatar_url, gender, country, province, city, language, create_time
)
values (
#{openid,jdbcType=VARCHAR},
#{nickname,jdbcType=VARCHAR},
#{avatarUrl,jdbcType=VARCHAR},
#{gender,jdbcType=INTEGER},
#{country,jdbcType=VARCHAR},
#{province,jdbcType=VARCHAR},
#{city,jdbcType=VARCHAR},
#{language,jdbcType=VARCHAR},
now()
)
</insert>
<update id="updateByPrimaryKey" parameterType="com.learning.newdemo.entity.WxUser">
update wx_user
set nickname = #{nickname,jdbcType=VARCHAR},
avatar_url = #{avatarUrl,jdbcType=VARCHAR},
gender = #{gender,jdbcType=INTEGER},
country = #{country,jdbcType=VARCHAR},
province = #{province,jdbcType=VARCHAR},
city = #{city,jdbcType=VARCHAR},
language = #{language,jdbcType=VARCHAR},
update_time = now()
where id = #{id,jdbcType=INTEGER}
</update>
</mapper>

@ -0,0 +1,21 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
*.local
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

@ -0,0 +1,40 @@
{
"name": "uni-preset-vue",
"version": "0.0.0",
"scripts": {
"dev:h5": "uni",
"dev:h5:ssr": "uni --ssr",
"dev:mp-weixin": "uni -p mp-weixin",
"build:h5": "uni build",
"build:h5:ssr": "uni build --ssr",
"build:mp-weixin": "uni build -p mp-weixin"
},
"dependencies": {
"@dcloudio/uni-app": "3.0.0-4040520250104002",
"@dcloudio/uni-app-harmony": "3.0.0-4040520250104002",
"@dcloudio/uni-app-plus": "3.0.0-4040520250104002",
"@dcloudio/uni-components": "3.0.0-4040520250104002",
"@dcloudio/uni-h5": "3.0.0-4040520250104002",
"@dcloudio/uni-mp-alipay": "3.0.0-4040520250104002",
"@dcloudio/uni-mp-baidu": "3.0.0-4040520250104002",
"@dcloudio/uni-mp-jd": "3.0.0-4040520250104002",
"@dcloudio/uni-mp-kuaishou": "3.0.0-4040520250104002",
"@dcloudio/uni-mp-lark": "3.0.0-4040520250104002",
"@dcloudio/uni-mp-qq": "3.0.0-4040520250104002",
"@dcloudio/uni-mp-toutiao": "3.0.0-4040520250104002",
"@dcloudio/uni-mp-weixin": "3.0.0-4040520250104002",
"@dcloudio/uni-mp-xhs": "3.0.0-4040520250104002",
"@dcloudio/uni-quickapp-webview": "3.0.0-4040520250104002",
"vue": "^3.4.21",
"vue-i18n": "^9.1.9"
},
"devDependencies": {
"@dcloudio/types": "^3.4.8",
"@dcloudio/uni-automator": "3.0.0-4040520250104002",
"@dcloudio/uni-cli-shared": "3.0.0-4040520250104002",
"@dcloudio/uni-stacktracey": "3.0.0-4040520250104002",
"@dcloudio/vite-plugin-uni": "3.0.0-4040520250104002",
"@vue/runtime-core": "^3.4.21",
"vite": "5.2.8"
}
}

@ -0,0 +1,4 @@
{
"miniprogramRoot": "",
"libVersion": "3.0.2"
}

@ -0,0 +1,10 @@
/// <reference types='@dcloudio/types' />
import 'vue'
declare module '@vue/runtime-core' {
type Hooks = App.AppInstance & Page.PageInstance;
interface ComponentCustomOptions extends Hooks {
}
}

@ -0,0 +1,17 @@
<script>
export default {
onLaunch: function () {
console.log('App Launch')
},
onShow: function () {
console.log('App Show')
},
onHide: function () {
console.log('App Hide')
},
}
</script>
<style>
/*每个页面公共css */
</style>

@ -0,0 +1,38 @@
<template>
<view class="argument-component">
<view class="content-card">
<view class="card-title">立论功能</view>
<view class="card-content">
在这里可以进行辩题立论和论点整理
</view>
</view>
</view>
</template>
<style scoped>
.argument-component {
width: 100%;
padding: 20rpx;
}
.content-card {
background-color: rgba(255, 255, 255, 0.1);
border-radius: 20rpx;
padding: 30rpx;
width: 90%;
margin: 0 auto;
backdrop-filter: blur(10px);
}
.card-title {
font-size: 36rpx;
color: #ffffff;
font-weight: bold;
margin-bottom: 20rpx;
}
.card-content {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.8);
}
</style>

@ -0,0 +1,387 @@
<template>
<view class="home-component">
<!-- 添加云的背景图像 -->
<image class="cloud-image" src="/static/cloud.png" mode="aspectFit"></image>
<!-- 欢迎标题保留 -->
<view class="welcome-section">
<view class="welcome-title">欢迎使用智辩云枢</view>
<view class="welcome-subtitle">AI驱动的辩论助手平台</view>
</view>
<!-- 三个功能卡片 - 错落布局 -->
<view class="feature-cards">
<!-- 第一张卡片 (奇数 - 靠左) -->
<view class="feature-card card-odd" @click="navigateToFeature(0)">
<view class="card-left">
<view class="card-icon">🧠</view>
</view>
<view class="card-right">
<view class="card-title">立论助手</view>
<view class="card-description">AI辅助构建辩论框架智能生成论点与论据</view>
</view>
</view>
<!-- 第二张卡片 (偶数 - 靠右) -->
<view class="feature-card card-even" @click="navigateToFeature(1)">
<view class="card-left">
<view class="card-title">复盘分析</view>
<view class="card-description">智能分析辩论过程提供专业化改进建议</view>
</view>
<view class="card-right">
<view class="card-icon">📊</view>
</view>
</view>
<!-- 第三张卡片 (奇数 - 靠左) -->
<view class="feature-card card-odd" @click="navigateToFeature(2)">
<view class="card-left">
<view class="card-icon">🤖</view>
</view>
<view class="card-right">
<view class="card-title">模拟辩论</view>
<view class="card-description">与AI进行实时辩论对练提升应变能力</view>
</view>
</view>
</view>
<!-- 底部名句展示 -->
<view class="quote-container">
<view class="quote-icon"></view>
<view class="quote-content">
<text class="quote-text">{{ currentQuote.text }}</text>
<text class="quote-author"> {{ currentQuote.author }}</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const emit = defineEmits(['change-tab', 'change-index']);
const tabList = [
{
pageName:'立论',
component:'PageOfArgument'
},
{
pageName:'复盘',
component:'PageOfReview'
},
{
pageName:'辩论',
component:'PageOfDebate'
}
];
const changeTab = (index) => {
emit('change-tab', tabList[index].component);
}
const changeIndex = (index) => {
emit('change-index', index);
}
//
const navigateToFeature = (index) => {
changeTab(index);
changeIndex(index + 1);
};
//
const debateQuotes = [
{
text: "辩论的艺术不在于赢得争论,而在于不失去朋友",
author: "爱因斯坦"
},
{
text: "真理越辩越明,正义自在人心",
author: "中国古语"
},
{
text: "不要用事实来干扰我的论点",
author: "辩论格言"
},
{
text: "辩论是发现真理的艺术,而不是为了胜利",
author: "苏格拉底"
},
{
text: "思辨是灵魂的对话",
author: "柏拉图"
},
{
text: "辩论不是为了证明谁对谁错,而是为了共同接近真理",
author: "亚里士多德"
},
{
text: "最好的论点不是说服对手,而是让对手思考",
author: "辩论艺术"
},
{
text: "辩论的目的不是为了赢,而是为了真理",
author: "苏格拉底"
}
];
//
const currentQuote = ref(debateQuotes[0]);
let quoteInterval = null;
//
const changeQuote = () => {
const randomIndex = Math.floor(Math.random() * debateQuotes.length);
currentQuote.value = debateQuotes[randomIndex];
};
onMounted(() => {
//
changeQuote();
// 15
quoteInterval = setInterval(changeQuote, 15000);
});
onUnmounted(() => {
//
if (quoteInterval) {
clearInterval(quoteInterval);
}
});
</script>
<style scoped>
.home-component {
width: 100%;
padding: 20rpx;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
z-index: 5;
border-radius: 20rpx;
box-sizing: border-box;
max-width: 100vw;
overflow-x: hidden;
}
/* 云图像样式 */
.cloud-image {
position: absolute;
top: 20rpx;
right: 40rpx;
width: 180rpx;
height: 120rpx;
opacity: 0.6;
z-index: 1;
animation: float 6s ease-in-out infinite;
}
@keyframes float {
0% {
transform: translateY(0px) translateX(0px);
}
50% {
transform: translateY(-15px) translateX(10px);
}
100% {
transform: translateY(0px) translateX(0px);
}
}
.welcome-section {
width: 90%;
padding: 40rpx 0;
text-align: center;
margin-bottom: 40rpx;
position: relative;
z-index: 2;
}
.welcome-title {
font-size: 42rpx;
color: #ffffff;
font-weight: bold;
margin-bottom: 10rpx;
text-shadow: 0 0 10rpx rgba(0, 214, 185, 0.3);
}
.welcome-subtitle {
font-size: 28rpx;
color: rgba(255, 255, 255, 0.7);
}
.feature-cards {
width: 100%;
display: flex;
flex-direction: column;
gap: 40rpx;
margin-bottom: 50rpx;
padding: 0 20rpx;
box-sizing: border-box;
position: relative;
z-index: 3;
}
.feature-card {
width: 90%;
display: flex;
background-color: rgba(255, 255, 255, 0.08);
border-radius: 20rpx;
padding: 30rpx 20rpx;
transition: all 0.3s ease;
box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.1);
border: 1px solid rgba(0, 214, 185, 0.2);
min-height: 160rpx;
box-sizing: border-box;
}
/* 悬停/触摸效果 */
.card-hover {
transform: translateY(-5rpx) scale(1.02);
background-color: rgba(0, 214, 185, 0.15);
box-shadow: 0 12rpx 25rpx rgba(0, 0, 0, 0.2), 0 0 20rpx rgba(0, 214, 185, 0.3);
border-color: rgba(0, 214, 185, 0.5);
}
/* 奇数卡片靠左 */
.card-odd {
align-self: flex-start;
margin-left: 0;
border-top-left-radius: 0;
border-left: 3px solid #00D6B9;
}
/* 偶数卡片靠右 */
.card-even {
align-self: flex-end;
margin-right: 0;
border-top-right-radius: 0;
border-right: 3px solid #00D6B9;
}
.feature-card:active {
transform: scale(0.98);
background-color: rgba(0, 214, 185, 0.2);
}
.card-left, .card-right {
display: flex;
flex-direction: column;
justify-content: center;
}
.card-left {
width: 30%;
align-items: center;
}
.card-right {
width: 70%;
padding-left: 20rpx;
}
/* 在偶数卡片中,反转左右区域的宽度 */
.card-even .card-left {
width: 70%;
align-items: flex-start;
padding-right: 20rpx;
}
.card-even .card-right {
width: 30%;
align-items: center;
padding-left: 0;
}
.card-icon {
font-size: 70rpx;
background: rgba(0, 214, 185, 0.1);
width: 100rpx;
height: 100rpx;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 10rpx;
box-shadow: 0 0 15rpx rgba(0, 214, 185, 0.3);
transition: all 0.3s ease;
}
.card-hover .card-icon {
background: rgba(0, 214, 185, 0.25);
box-shadow: 0 0 20rpx rgba(0, 214, 185, 0.5);
transform: scale(1.1);
}
.card-title {
font-size: 32rpx;
color: #ffffff;
font-weight: bold;
margin-bottom: 15rpx;
}
.card-description {
font-size: 26rpx;
color: rgba(255, 255, 255, 0.7);
line-height: 1.5;
}
/* 名句容器样式 */
.quote-container {
width: 90%;
display: flex;
padding: 30rpx;
background-color: rgba(0, 214, 185, 0.1);
border-radius: 20rpx;
margin-top: 20rpx;
position: relative;
z-index: 2;
box-shadow: 0 5rpx 15rpx rgba(0, 0, 0, 0.1);
border: 1px solid rgba(0, 214, 185, 0.2);
transition: all 0.5s ease;
overflow: hidden;
}
.quote-icon {
font-size: 60rpx;
color: rgba(0, 214, 185, 0.5);
margin-right: 15rpx;
align-self: flex-start;
line-height: 1;
}
.quote-content {
display: flex;
flex-direction: column;
flex: 1;
}
.quote-text {
font-size: 28rpx;
color: #ffffff;
line-height: 1.5;
font-style: italic;
margin-bottom: 10rpx;
animation: fadeIn 0.5s ease;
}
.quote-author {
font-size: 24rpx;
color: rgba(255, 255, 255, 0.6);
text-align: right;
animation: fadeIn 0.5s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

@ -0,0 +1,136 @@
<template>
<view class="tab-bar">
<!-- 添加移动的背景块 -->
<view class="active-bg" :style="{ transform: `translateX(${100 * props.currentComponentIndex}%)` }"></view>
<view
class="tab-item"
:class="{
'active': props.currentComponentIndex === index,
'is-first-tab': index === 0
}"
v-for="(item, index) in tabList"
:key="index"
@click="changeTab(index)">
<text :class="{'active': props.currentComponentIndex === index}">{{ item.pageName }}</text>
</view>
</view>
</template>
<script setup>
// emit
const emit = defineEmits(['change-tab', 'change-index']);
const props = defineProps(['currentComponentIndex']);
const tabList = [
{
pageName:'首页',
component:'PageOfHome'
},
{
pageName:'立论',
component:'PageOfArgument'
},
{
pageName:'复盘',
component:'PageOfReview'
},
{
pageName:'辩论',
component:'PageOfDebate'
}
];
const changeTab = (index) => {
if(props.currentComponentIndex === index) return;
//
emit('change-tab', tabList[index].component);
emit('change-index', index);
};
</script>
<style scoped>
.tab-bar {
display: flex;
width: 70%;
margin: 0 auto;
height: 70%;
background-color: #1A2C42;
border: none;
position: relative;
border-radius: 25rpx;
overflow: hidden;
box-shadow: 0 4rpx 15rpx rgba(0, 0, 0, 0.2);
transform: translateZ(0);
-webkit-transform: translateZ(0);
backface-visibility: hidden;
-webkit-backface-visibility: hidden;
}
/* 移动的背景块 */
.active-bg {
position: absolute;
width: 25%;
height: 100%;
background-color: #00D6B9;
border-radius: 25rpx;
transition: transform 0.3s ease;
z-index: 1;
box-shadow: 0 0 20rpx rgba(0, 214, 185, 0.5);
/* 添加硬件加速 */
will-change: transform;
}
.tab-item {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
height: 100%;
position: relative;
z-index: 2;
}
.tab-item::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 1px;
height: 60%;
z-index: 2;
background: linear-gradient(
to bottom,
transparent,
rgba(0, 214, 185, 0.1) 15%,
rgba(0, 214, 185, 0.5) 50%,
rgba(0, 214, 185, 0.1) 85%,
transparent
);
}
.tab-item.is-first-tab::before {
display: none;
}
.tab-item.active {
background-color: transparent;
}
.tab-item text {
font-size: 24rpx;
color: rgba(0, 214, 185, 0.7);
transition: all 0.3s ease;
text-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.2);
}
.tab-item text.active {
color: #ffffff;
font-weight: bold;
text-shadow: 0 1rpx 8rpx rgba(255, 255, 255, 0.4);
transform: scale(1.05);
}
</style>

@ -0,0 +1,10 @@
import {
createSSRApp
} from "vue";
import App from "./App.vue";
export function createApp() {
const app = createSSRApp(App);
return {
app,
};
}

@ -0,0 +1,64 @@
{
"name" : "智辨云枢",
"appid" : "wxf1f6d7657e01d48a",
"description" : "智辨云枢",
"versionName" : "1.0.0",
"versionCode" : "100",
"transformPx" : false,
"app-plus" : {
"usingComponents" : true,
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3,
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
},
"modules" : {},
"distribute" : {
"android" : {
"permissions" : [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
"ios" : {},
"sdkConfigs" : {}
}
},
"quickapp" : {},
"mp-weixin" : {
"appid" : "wxf1f6d7657e01d48a",
"setting" : {
"urlCheck" : false
},
"usingComponents" : true
},
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"uniStatistics": {
"enable": false
},
"vueVersion" : "3"
}

@ -0,0 +1,22 @@
{
"pages": [
{
"path": "pages/login/index",
"style": {
"navigationBarTitleText": "登录"
}
},
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页"
}
}
],
"globalStyle": {
"navigationBarTextStyle": "white",
"navigationBarTitleText": "智辩云枢",
"navigationBarBackgroundColor": "#1a2a6c",
"backgroundColor": "#000000"
}
}

@ -0,0 +1,154 @@
<template>
<view class="home-page">
<view class="home-header">
<text class="home-header-title"></text>
<text class="home-header-title"></text>
<text class="home-header-title"></text>
<text class="home-header-title"></text>
</view>
<view class="home-content">
<!-- 使用条件渲染替代动态组件 -->
<PageOfHome v-if="currentComponent === 'PageOfHome'" @change-tab="handleTabChange" @change-index="handleIndexChange" />
<PageOfArgument v-else-if="currentComponent === 'PageOfArgument'" />
<PageOfReview v-else-if="currentComponent === 'PageOfReview'" />
<PageOfDebate v-else-if="currentComponent === 'PageOfDebate'" />
</view>
<view class="home-footer">
<TabBar @change-tab="handleTabChange" @change-index="handleIndexChange" :currentComponentIndex="currentComponentIndex"/>
</view>
</view>
</template>
<script setup>
import TabBar from '../../components/TabBar.vue';
import { ref } from 'vue';
//
import PageOfHome from '../../components/HomeCom.vue';
import PageOfArgument from '../../components/ArgumentCom.vue';
// import PageOfReview from '../../components/ReviewCom.vue';
// import PageOfDebate from '../../components/DebateCom.vue';
//
const currentComponent = ref('PageOfHome');
// index
const currentComponentIndex = ref(0);
//
const handleTabChange = (componentName) => {
currentComponent.value = componentName;
};
const handleIndexChange = (index) => {
currentComponentIndex.value = index;
}
</script>
<style scoped>
.home-page {
position: relative;
width: 100vw;
height: 100vh;
background: linear-gradient(135deg, #1a2a6c, #000000);
display: flex;
flex-direction: column;
align-items: center;
}
.home-header {
width: 100%;
height: min(100rpx, fit-content);
/* background-color: #000000; */
margin-top: 20rpx;
margin-left: 25rpx;
margin-bottom: 20rpx;
display: flex;
}
.home-header::after{
content: '';
width: 100%;
height: 1px;
background: linear-gradient(to right, transparent, rgba(0, 214, 185, 0.1) 15%, rgba(0, 214, 185, 0.5) 50%, rgba(0, 214, 185, 0.1) 85%, transparent);
position: absolute;
top: 100rpx; /* 将分割线定位在header的底部 */
left: 0;
z-index: 1;
transform: translateY(20rpx);
}
.home-header-title {
font-size: 40rpx;
color: #ffffff;
text-align: left;
margin-left: 20rpx;
font-weight: bold;
margin-top: 20rpx;
}
.home-header-title:nth-child(odd) {
animation: float-up-to-down 3s infinite;
}
.home-header-title:nth-child(even) {
animation: float-down-to-up 3s infinite;
}
@keyframes float-up-to-down {
0%{
transform: translateY(-10rpx);
}
20%{
transform: translateY(-7rpx);
}
50%{
transform: translateY(0rpx);
}
70%{
transform: translateY(7rpx);
}
100%{
transform: translateY(-10rpx);
}
}
@keyframes float-down-to-up {
0%{
transform: translateY(10rpx);
}
20%{
transform: translateY(7rpx);
}
50%{
transform: translateY(0rpx);
}
70%{
transform: translateY(-7rpx);
}
100%{
transform: translateY(10rpx);
}
}
.home-content {
flex: 1;
width: 100%;
padding: 30rpx 0;
box-sizing: border-box;
overflow-y: auto;
min-height: 400rpx;
position: relative;
z-index: 2;
margin-bottom: 140rpx;
}
.home-footer {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 120rpx;
z-index: 100;
}
</style>

@ -0,0 +1,229 @@
<!-- login.vue -->
<template>
<view class="login-container">
<image class="bg-image" src="/static/login-bg.jpg" mode="aspectFill"></image>
<view class="content-wrapper">
<!-- 所有内容放在气泡下方 -->
<view class="bottom-content">
<!-- 标题移到气泡下方 -->
<view class="login-title">
<text class="title-text">智辨云枢</text>
<text class="subtitle-text">Intelligent Debate Cloud</text>
</view>
<!-- 按钮在标题下方 -->
<view class="login-button">
<button @click="getCode">
<text>{{ displayText }}</text>
<text v-if="isTyping">|</text>
</button>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const fullText = ref('微信一键登录');
const displayText = ref('');
const isTyping = ref(true);
let currentIndex = 0;
let typingTimer = null;
let blinkingTimer = null;
// 2
const TypeText = () => {
//
if (currentIndex === fullText.value.length) {
// 2
clearTimeout(typingTimer);
typingTimer = setTimeout(() => {
displayText.value = "";
currentIndex = 0;
TypeText(); //
}, 2000);
return;
}
//
displayText.value += fullText.value[currentIndex];
currentIndex++;
typingTimer = setTimeout(TypeText, 300);
}
const blinkText = () => {
blinkingTimer = setInterval(() => {
isTyping.value = !isTyping.value;
}, 700);
}
const getCode = () => {
wx.login({
success(res){
if (res.code) {
console.log("登录code", res.code);
requestLogin(res.code);
}else {
console.error("登录失败", res.errMsg);
}
}
})
};
const requestLogin = (code) =>{
wx.request({
url: 'http://127.0.0.1:8080/api/wx/login',
method: 'POST',
data:{
code: code
},
success(res) {
if(res.data.code === 200){
wx.setStorageSync('token', res.data.data.token);
console.log("登录成功");
uni.redirectTo({
url: '/pages/index/index'
});
}else{
console.error(res.data.message);
}
},
fail(err){
console.error("login failed", err);
}
});
}
onMounted(() => {
TypeText();
blinkText();
});
onUnmounted(() => {
clearTimeout(typingTimer);
clearInterval(blinkingTimer);
});
</script>
<style scoped>
.login-container {
width: 100vw;
height: 100vh;
position: relative;
background-color: #152238;
overflow: hidden;
}
.bg-image {
position: absolute;
width: 100%;
height: 100%;
object-fit: cover;
top: 0;
left: 0;
z-index: 1;
}
.content-wrapper {
position: relative;
width: 100%;
height: 100%;
z-index: 2;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
box-sizing: border-box;
}
/* 底部内容容器 - 包含标题和按钮 */
.bottom-content {
position: absolute;
bottom: 20%;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
/* 标题样式 - 现在在气泡下方 */
.login-title {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 50rpx;
}
.title-text {
font-size: min(48rpx, 8vw);
font-weight: bold;
color: #ffffff; /* 白色文字,在深色背景上 */
letter-spacing: 8rpx;
}
.subtitle-text {
font-size: min(24rpx, 4vw);
color: rgba(255, 255, 255, 0.7);
margin-top: 10rpx;
letter-spacing: 2rpx;
}
.login-button {
width: 100%;
display: flex;
justify-content: center;
margin-top: 50rpx;
}
.login-button button {
background-color: #00D6B9;
color: white;
border: none;
border-radius: 50rpx;
font-weight: 600;
font-size: min(34rpx, 5vw);
padding: 10rpx 0;
width: 60%;
height: 100rpx;
box-shadow: 0 8rpx 20rpx rgba(0, 214, 185, 0.3);
}
/* 适配不同屏幕高度 */
@media screen and (max-height: 700px) {
.bottom-content {
bottom: 15%;
}
.login-title {
margin-bottom: 30rpx;
}
.login-button {
margin-top: 30rpx;
}
}
@media screen and (max-height: 600px) {
.bottom-content {
bottom: 10%;
}
.login-title {
margin-bottom: 20rpx;
}
.login-button {
margin-top: 20rpx;
}
}
@media screen and (min-height: 800px) {
.bottom-content {
bottom: 25%;
}
}
</style>

@ -0,0 +1,6 @@
export {};
declare module "vue" {
type Hooks = App.AppInstance & Page.PageInstance;
interface ComponentCustomOptions extends Hooks {}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 KiB

@ -0,0 +1,76 @@
/**
* uni-app
*
* uni-app https://ext.dcloud.net.cn使
* 使scss使 import 便App
*
*/
/**
* App使
*
* 使scss scss 使 import
*/
/* 颜色变量 */
/* 行为相关颜色 */
$uni-color-primary: #007aff;
$uni-color-success: #4cd964;
$uni-color-warning: #f0ad4e;
$uni-color-error: #dd524d;
/* 文字基本颜色 */
$uni-text-color: #333; //
$uni-text-color-inverse: #fff; //
$uni-text-color-grey: #999; //
$uni-text-color-placeholder: #808080;
$uni-text-color-disable: #c0c0c0;
/* 背景颜色 */
$uni-bg-color: #fff;
$uni-bg-color-grey: #f8f8f8;
$uni-bg-color-hover: #f1f1f1; //
$uni-bg-color-mask: rgba(0, 0, 0, 0.4); //
/* 边框颜色 */
$uni-border-color: #c8c7cc;
/* 尺寸变量 */
/* 文字尺寸 */
$uni-font-size-sm: 12px;
$uni-font-size-base: 14px;
$uni-font-size-lg: 16;
/* 图片尺寸 */
$uni-img-size-sm: 20px;
$uni-img-size-base: 26px;
$uni-img-size-lg: 40px;
/* Border Radius */
$uni-border-radius-sm: 2px;
$uni-border-radius-base: 3px;
$uni-border-radius-lg: 6px;
$uni-border-radius-circle: 50%;
/* 水平间距 */
$uni-spacing-row-sm: 5px;
$uni-spacing-row-base: 10px;
$uni-spacing-row-lg: 15px;
/* 垂直间距 */
$uni-spacing-col-sm: 4px;
$uni-spacing-col-base: 8px;
$uni-spacing-col-lg: 12px;
/* 透明度 */
$uni-opacity-disabled: 0.3; //
/* 文章场景相关 */
$uni-color-title: #2c405a; //
$uni-font-size-title: 20px;
$uni-color-subtitle: #555; //
$uni-font-size-subtitle: 18px;
$uni-color-paragraph: #3f536e; //
$uni-font-size-paragraph: 15px;

@ -0,0 +1,8 @@
import { defineConfig } from 'vite'
import uni from '@dcloudio/vite-plugin-uni'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
uni(),
],
})
Loading…
Cancel
Save