【改进】后台管理系统剥离APP接口

This commit is contained in:
MichaelWin
2025-12-19 18:43:46 +08:00
parent 59307d612c
commit e96c54c328
83 changed files with 278 additions and 7401 deletions

View File

@@ -0,0 +1,46 @@
package com.corewing.app.common.base;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.util.Date;
@Getter
@Setter
public class CommonEntity implements Serializable {
/** 删除标志 */
@JsonIgnore
@TableLogic
@TableField(fill = FieldFill.INSERT)
private String deleteFlag;
/** 创建时间 */
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/** 创建人 */
@TableField(fill = FieldFill.INSERT)
private String createUser;
/** 创建人名称 */
@TableField(exist = false)
private String createUserName;
/** 更新时间 */
@TableField(fill = FieldFill.UPDATE)
private Date updateTime;
/** 更新人 */
@TableField(fill = FieldFill.UPDATE)
private String updateUser;
/** 更新人名称 */
@TableField(exist = false)
private String updateUserName;
}

View File

@@ -1,4 +1,4 @@
package com.corewing.app.dto.biz.appVersion; package com.corewing.app.dto;
import lombok.Data; import lombok.Data;

View File

@@ -1,4 +1,4 @@
package com.corewing.app.dto.api; package com.corewing.app.dto;
import lombok.Data; import lombok.Data;

View File

@@ -1,4 +1,4 @@
package com.corewing.app.dto.api; package com.corewing.app.dto;
import lombok.Data; import lombok.Data;

View File

@@ -1,4 +1,4 @@
package com.corewing.app.dto.api; package com.corewing.app.dto;
import lombok.Data; import lombok.Data;

View File

@@ -1,4 +1,4 @@
package com.corewing.app.dto.api; package com.corewing.app.dto;
import lombok.Data; import lombok.Data;

View File

@@ -1,4 +1,4 @@
package com.corewing.app.dto.api; package com.corewing.app.dto;
import lombok.Data; import lombok.Data;

View File

@@ -1,4 +1,4 @@
package com.corewing.app.dto.api; package com.corewing.app.dto;
import lombok.Data; import lombok.Data;

View File

@@ -0,0 +1,14 @@
package com.corewing.app.dto;
import lombok.Data;
@Data
public class ModelPageRequest {
private Long userId;
private String searchKey;
private String status;
}

View File

@@ -1,4 +1,4 @@
package com.corewing.app.dto.api; package com.corewing.app.dto;
import lombok.Data; import lombok.Data;

View File

@@ -1,4 +1,4 @@
package com.corewing.app.dto.api; package com.corewing.app.dto;
import lombok.Data; import lombok.Data;

View File

@@ -1,4 +1,4 @@
package com.corewing.app.dto.api; package com.corewing.app.dto;
import lombok.Data; import lombok.Data;

View File

@@ -1,4 +1,4 @@
package com.corewing.app.dto.api; package com.corewing.app.dto;
import lombok.Data; import lombok.Data;

View File

@@ -1,4 +1,4 @@
package com.corewing.app.dto.api; package com.corewing.app.dto;
import lombok.Data; import lombok.Data;

View File

@@ -1,4 +1,4 @@
package com.corewing.app.dto.api; package com.corewing.app.dto;
import lombok.Data; import lombok.Data;

View File

@@ -1,4 +1,4 @@
package com.corewing.app.dto.api; package com.corewing.app.dto;
import lombok.Data; import lombok.Data;

View File

@@ -1,11 +0,0 @@
package com.corewing.app.dto.biz.feedback;
import lombok.Data;
import java.util.List;
@Data
public class FeedbackBatchDeleteRequest {
private List<Long> ids;
}

View File

@@ -1,13 +0,0 @@
package com.corewing.app.dto.biz.feedback;
import lombok.Data;
import java.util.List;
@Data
public class FeedbackBatchStatusRequest {
private List<Long> ids;
private Integer status;
}

View File

@@ -1,10 +0,0 @@
package com.corewing.app.dto.biz.firmware;
import lombok.Data;
import java.util.List;
@Data
public class FirmwareBatchDeleteRequest {
private List<Long> ids;
}

View File

@@ -1,11 +0,0 @@
package com.corewing.app.dto.biz.privacyPolicy;
import lombok.Data;
import java.util.List;
@Data
public class PrivacyPolicyBatchDeleteRequest {
private List<Long> ids;
}

View File

@@ -1,10 +0,0 @@
package com.corewing.app.dto.biz.tutorial;
import lombok.Data;
import java.util.List;
@Data
public class BatchTutorialDeleteRequest {
private List<Long> ids;
}

View File

@@ -1,10 +0,0 @@
package com.corewing.app.dto.biz.tutorial;
import lombok.Data;
import java.util.List;
@Data
public class CategoryBatchDeleteRequest {
private List<Long> ids;
}

View File

@@ -1,12 +0,0 @@
package com.corewing.app.dto.biz.user;
import lombok.Data;
import java.util.List;
@Data
public class BizUserBatchDeleteRequest {
private List<Long> ids;
}

View File

@@ -1,8 +0,0 @@
package com.corewing.app.dto.biz.user;
import lombok.Data;
@Data
public class BizUserIdRequest {
private long id;
}

View File

@@ -1,11 +0,0 @@
package com.corewing.app.dto.biz.user;
import lombok.Data;
import java.util.List;
@Data
public class BizUserStatusRequest {
private List<Long> ids;
private Integer status;
}

View File

@@ -1,18 +0,0 @@
package com.corewing.app.dto.biz.user;
import lombok.Data;
@Data
public class ResetPasswordRequest {
/**
* 用户id
*/
private Integer userId;
/**
* 新密码
*/
private String password;
}

View File

@@ -1,9 +0,0 @@
package com.corewing.app.dto.sys;
import lombok.Data;
@Data
public class SysResetPasswordRequest {
private Long userId;
private String password;
}

View File

@@ -1,8 +0,0 @@
package com.corewing.app.dto.sys;
import lombok.Data;
@Data
public class SysUserIdRequest {
private Long id;
}

View File

@@ -1,4 +1,4 @@
package com.corewing.app.dto.api; package com.corewing.app.dto;
import lombok.Data; import lombok.Data;

View File

@@ -0,0 +1,46 @@
package com.corewing.app.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.corewing.app.common.base.CommonEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
@TableName("app_model")
public class AppModel extends CommonEntity {
@TableId
private String id;
private Long userId;
private String title;
private String description;
private String categoryId;
private String coverImage;
private String isPublic;
private String status;
private int downloadCount;
private int likeCount;
private int favoriteCount;
private String remark;
private String extJson;
@TableField(exist = false)
private AppModelFile modelFile;
}

View File

@@ -0,0 +1,27 @@
package com.corewing.app.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.corewing.app.common.base.CommonEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
@TableName("app_model_category")
public class AppModelCategory extends CommonEntity {
@TableId
private String id;
private String name;
private String parentId;
private int sortCode;
private String remark;
private String extJson;
}

View File

@@ -0,0 +1,28 @@
package com.corewing.app.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.corewing.app.common.base.CommonEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
@TableName("app_model_download_log")
public class AppModelDownloadLog extends CommonEntity {
@TableId
private String id;
private Long userId;
private String modelId;
private String ipAddress;
private String remark;
private String extJson;
}

View File

@@ -0,0 +1,25 @@
package com.corewing.app.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.corewing.app.common.base.CommonEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
@TableName("app_model_favorite")
public class AppModelFavorite extends CommonEntity {
@TableId
private Long id;
private String userId;
private String modelId;
private String remark;
private String extJson;
}

View File

@@ -0,0 +1,30 @@
package com.corewing.app.entity;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.corewing.app.common.base.CommonEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
@TableName("app_model_file")
public class AppModelFile extends CommonEntity {
@TableId
private String id;
private String modelId;
private String fileName;
private String fileType;
private Long fileSize;
private String fileUrl;
private String sortCode;
}

View File

@@ -4,7 +4,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.corewing.app.dto.api.TrajectoryData; import com.corewing.app.dto.TrajectoryData;
import lombok.Data; import lombok.Data;
import java.util.Date; import java.util.Date;

View File

@@ -1,101 +0,0 @@
package com.corewing.app.modules.admin.biz;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.corewing.app.common.Result;
import com.corewing.app.dto.biz.feedback.FeedbackBatchDeleteRequest;
import com.corewing.app.dto.biz.feedback.FeedbackBatchStatusRequest;
import com.corewing.app.entity.Feedback;
import com.corewing.app.service.FeedbackService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
/**
* 用户反馈
*/
@Controller
@RequestMapping("/biz/feedback")
public class BizFeedBackController {
@Resource
private FeedbackService feedbackService;
/**
* 反馈管理首页
* @return
*/
@GetMapping("/index")
public String index() {
return "admin/biz/feedback/index";
}
/**
* 查询反馈数据分页
* @param feedback
* @return
*/
@GetMapping("/page")
@ResponseBody
public Result<Page<Feedback>> page(Feedback feedback) {
return Result.success(feedbackService.page(feedback));
}
/**
* 新增反馈
* @param feedback
* @return
*/
@PostMapping("/save")
@ResponseBody
public Result<String> save(@RequestBody Feedback feedback) {
return Result.isBool(feedbackService.save(feedback));
}
/**
* 编辑反馈
* @param feedback
* @return
*/
@PostMapping("/update")
@ResponseBody
public Result<String> update(@RequestBody Feedback feedback) {
return Result.isBool(feedbackService.updateById(feedback));
}
/**
* 删除反馈
* @param id
* @return
*/
@DeleteMapping("/delete")
@ResponseBody
public Result<String> delete(Long id) {
return Result.isBool(feedbackService.removeById(id));
}
/**
* 批量删除反馈
* @param feedbackBatchDeleteRequest
* @return
*/
@PostMapping("/batchDelete")
@ResponseBody
public Result<String> batchDelete(@RequestBody FeedbackBatchDeleteRequest feedbackBatchDeleteRequest) {
return Result.isBool(feedbackService.removeBatchByIds(feedbackBatchDeleteRequest.getIds()));
}
/**
* 批量更改状态
* @param feedbackBatchStatusRequest
* @return
*/
@PostMapping("/batchStatus")
@ResponseBody
public Result<String> batchStatus(@RequestBody FeedbackBatchStatusRequest feedbackBatchStatusRequest) {
return Result.isBool(feedbackService.batchStatus(feedbackBatchStatusRequest));
}
}

View File

@@ -1,102 +0,0 @@
package com.corewing.app.modules.admin.biz;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.corewing.app.common.Result;
import com.corewing.app.dto.biz.firmware.FirmwareBatchDeleteRequest;
import com.corewing.app.entity.Firmware;
import com.corewing.app.service.FirmwareService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
/**
* 固件管理
*/
@Controller
@RequestMapping("/biz/firmware")
public class BizFirmwareController {
@Resource
private FirmwareService firmwareService;
/**
* 固件管理首页
* @return
*/
@GetMapping("/index")
public String index() {
return "admin/biz/firmware/index";
}
/**
* 分页查询
* @param firmware
* @return
*/
@GetMapping("/page")
@ResponseBody
public Result<Page<Firmware>> page(Firmware firmware) {
return Result.success(firmwareService.page(firmware));
}
/**
* 保存
* @param firmware
* @return
*/
@PostMapping("/save")
@ResponseBody
public Result<String> save(@RequestBody Firmware firmware) {
return Result.isBool(firmwareService.save(firmware));
}
/**
* 更新
* @param firmware
* @return
*/
@PostMapping("/update")
@ResponseBody
public Result<String> update(@RequestBody Firmware firmware) {
return Result.isBool(firmwareService.updateById(firmware));
}
/**
* 删除
* @param id
* @return
*/
@DeleteMapping("/delete")
@ResponseBody
public Result<Firmware> delete(Long id) {
return Result.isBool(firmwareService.removeData(id));
}
/**
* 批量删除
* @param firmwareBatchDeleteRequest
* @return
*/
@PostMapping("/batchDelete")
@ResponseBody
public Result<Firmware> batchDelete(@RequestBody FirmwareBatchDeleteRequest firmwareBatchDeleteRequest) {
return Result.isBool(firmwareService.removeBatchByIds(firmwareBatchDeleteRequest.getIds()));
}
/**
* 上传固件
* @param file
* @return
*/
@PostMapping("/uploadFile")
@ResponseBody
public Result<String> uploadFile(MultipartFile file, Long id) {
return Result.isBool(firmwareService.uploadFile(file, id));
}
}

View File

@@ -1,61 +0,0 @@
package com.corewing.app.modules.admin.biz;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.corewing.app.common.Result;
import com.corewing.app.dto.biz.privacyPolicy.PrivacyPolicyBatchDeleteRequest;
import com.corewing.app.entity.PrivacyPolicy;
import com.corewing.app.service.PrivacyPolicyService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
/**
* 隐私政策
*/
@Controller
@RequestMapping("/biz/privacy_policy")
public class BizPrivacyPolicyController {
@Resource
private PrivacyPolicyService privacyPolicyService;
@GetMapping("/index")
public String index() {
return "admin/biz/privacyPolicy/index";
}
@GetMapping("/page")
@ResponseBody
public Result<Page<PrivacyPolicy>> page(PrivacyPolicy privacyPolicy) {
return Result.success(privacyPolicyService.page(privacyPolicy));
}
@PostMapping("/save")
@ResponseBody
public Result<String> save(@RequestBody PrivacyPolicy privacyPolicy) {
return Result.isBool(privacyPolicyService.save(privacyPolicy));
}
@PostMapping("/update")
@ResponseBody
public Result<String> update(@RequestBody PrivacyPolicy privacyPolicy) {
return Result.isBool(privacyPolicyService.updateById(privacyPolicy));
}
@DeleteMapping("/delete")
@ResponseBody
public Result<String> delete(Long id) {
return Result.isBool(privacyPolicyService.removeById(id));
}
@PostMapping("/batchDelete")
@ResponseBody
public Result<String> batchDelete(@RequestBody PrivacyPolicyBatchDeleteRequest privacyPolicyBatchDeleteRequest) {
return Result.isBool(privacyPolicyService.removeBatchByIds(privacyPolicyBatchDeleteRequest.getIds()));
}
}

View File

@@ -1,143 +0,0 @@
package com.corewing.app.modules.admin.biz;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.corewing.app.common.Result;
import com.corewing.app.dto.biz.tutorial.CategoryBatchDeleteRequest;
import com.corewing.app.entity.Tutorial;
import com.corewing.app.entity.TutorialCategory;
import com.corewing.app.service.TutorialCategoryService;
import com.corewing.app.service.TutorialService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
/**
* 教程指南
*/
@Controller
@RequestMapping("/biz/tutorial")
public class BizTutorialController {
@Resource
private TutorialCategoryService tutorialCategoryService;
@Resource
private TutorialService tutorialService;
/**
* 教程分页
* @param tutorial
* @return
*/
@GetMapping("/page")
@ResponseBody
public Result<Page<Tutorial>> page(Tutorial tutorial) {
return Result.success(tutorialService.page(tutorial));
}
/**
* 新增教程
* @param tutorial
* @return
*/
@PostMapping("/save")
@ResponseBody
public Result<String> save(@RequestBody Tutorial tutorial) {
return Result.isBool(tutorialService.save(tutorial));
}
/**
* 更新教程
* @param tutorial
* @return
*/
@PostMapping("/update")
@ResponseBody
public Result<String> update(@RequestBody Tutorial tutorial) {
return Result.isBool(tutorialService.update(tutorial));
}
/**
* 删除教程
* @param id
* @return
*/
@DeleteMapping("/delete")
@ResponseBody
public Result<String> delete(Long id) {
return Result.isBool(tutorialService.remove(id));
}
/**
* 教程管理首页
* @return
*/
@GetMapping("/index")
public String index() {
return "admin/biz/tutorial/index";
}
/**
* 教程分类分页
* @param tutorialCategory
* @return
*/
@GetMapping("/category/page")
@ResponseBody
public Result<Page<TutorialCategory>> categoryPage(TutorialCategory tutorialCategory) {
return Result.success(tutorialCategoryService.page(tutorialCategory));
}
/**
* 新增教程分类
* @param tutorialCategory
* @return
*/
@PostMapping("/category/save")
@ResponseBody
public Result<String> saveCategory(@RequestBody TutorialCategory tutorialCategory) {
tutorialCategory.setType(TutorialCategory.typeCategory);
return Result.isBool(tutorialCategoryService.save(tutorialCategory));
}
/**
* 更新教程分类
* @param tutorialCategory
* @return
*/
@PostMapping("/category/update")
@ResponseBody
public Result<String> updateCategory(@RequestBody TutorialCategory tutorialCategory) {
return Result.isBool(tutorialCategoryService.updateById(tutorialCategory));
}
/**
* 删除教程分类
* @param id
* @return
*/
@DeleteMapping("/category/delete")
@ResponseBody
public Result<String> deleteCategory(Long id) {
return Result.isBool(tutorialCategoryService.removeById(id));
}
/**
* 批量删除教程分类
* @param categoryBatchDeleteRequest
* @return
*/
@PostMapping("/category/batchDelete")
@ResponseBody
public Result<String> batchDeleteCategory(@RequestBody CategoryBatchDeleteRequest categoryBatchDeleteRequest) {
return Result.isBool(tutorialCategoryService.removeBatchByIds(categoryBatchDeleteRequest.getIds()));
}
}

View File

@@ -1,106 +0,0 @@
package com.corewing.app.modules.admin.biz;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.corewing.app.common.Result;
import com.corewing.app.dto.biz.user.BizUserBatchDeleteRequest;
import com.corewing.app.dto.biz.user.BizUserIdRequest;
import com.corewing.app.dto.biz.user.BizUserStatusRequest;
import com.corewing.app.dto.biz.user.ResetPasswordRequest;
import com.corewing.app.entity.User;
import com.corewing.app.service.UserService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
@Controller
@RequestMapping("/biz/user")
public class BizUserController {
@Resource
private UserService userService;
/**
* 跳转到用户管理页面
* @return 页面名称
*/
@GetMapping("/index")
public String index() {
return "admin/biz/user/index";
}
/**
* 获取分页信息
* @param user 用户搜索对象
* @return 用户信息集合
*/
@GetMapping("/page")
@ResponseBody
public Result<Page<User>> page(User user) {
Page<User> sysUserPage = userService.page(user);
return Result.success(sysUserPage);
}
/**
* 修改密码
* @param resetPasswordRequest 修改密码DTO
* @return 成功 or 失败
*/
@PutMapping("/resetPassword")
@ResponseBody
public Result<String> resetPassword(@RequestBody ResetPasswordRequest resetPasswordRequest) {
boolean flag = userService.resetPassword(resetPasswordRequest);
return Result.isBoolAsMsg(flag, "修改密码成功");
}
/**
* 新增用户
* @param user
* @return
*/
@PostMapping("/save")
@ResponseBody
public Result<String> save(@RequestBody User user) {
return Result.isBool(userService.save(user));
}
/**
* 修改用户
* @param user
* @return
*/
@PostMapping("/update")
@ResponseBody
public Result<String> update(@RequestBody User user) {
return Result.isBool(userService.update(user));
}
/**
* 单删用户
* @param bizUserIdRequest
* @return
*/
@PostMapping("/delete")
@ResponseBody
public Result<String> delete(@RequestBody BizUserIdRequest bizUserIdRequest) {
return Result.isBool(userService.removeById(bizUserIdRequest.getId()));
}
/**
* 批量删除用户
* @return
*/
@PostMapping("/batchDelete")
@ResponseBody
public Result<String> batchDelete(@RequestBody BizUserBatchDeleteRequest bizUserBatchDeleteRequest) {
return Result.isBool(userService.removeByIds(bizUserBatchDeleteRequest.getIds()));
}
@PostMapping("/batchStatus")
@ResponseBody
public Result<String> batchStatus(@RequestBody BizUserStatusRequest bizUserStatusRequests) {
return Result.isBool(userService.batchStatus(bizUserStatusRequests));
}
}

View File

@@ -1,73 +0,0 @@
package com.corewing.app.modules.admin.sys;
import com.corewing.app.common.Result;
import com.corewing.app.service.FirmwareService;
import com.corewing.app.service.SysUserService;
import com.corewing.app.service.UserService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
@Controller
public class SysMainController {
@Resource
private UserService userService;
@Resource
private FirmwareService firmwareService;
/**
* 加载页
* @return
*/
@GetMapping({"/", "/index.html"})
public String loading() {
return "admin/loading";
}
/**
* 登录页
* @return
*/
@GetMapping("/admin/login.html")
public String login() {
return "admin/login";
}
/**
* 后台首页
* @return
*/
@GetMapping("/admin/index.html")
public String adminIndex() {
return "admin/main";
}
/**
* 仪表盘
* @return
*/
@GetMapping("/admin/dashboard")
public String dashboard() {
return "admin/dashboard";
}
/**
* 统计
* @return
*/
@GetMapping("/main/statistics")
@ResponseBody
public Result<Map<String, Object>> statistics() {
Map<String, Object> result = new HashMap<>();
result.put("userCount", userService.count ());
result.put("firmwareCount", firmwareService.count());
return Result.success(result);
}
}

View File

@@ -1,28 +0,0 @@
package com.corewing.app.modules.admin.sys;
import com.corewing.app.common.Result;
import com.corewing.app.entity.SysMenu;
import com.corewing.app.service.SysMenuService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.annotation.Resource;
import java.util.List;
@Controller
@RequestMapping("/sys/menu")
public class SysMenuController {
@Resource
private SysMenuService sysMenuService;
@GetMapping("/initSysMenu")
@ResponseBody
public Result<List<SysMenu>> initSysMenu() {
List<SysMenu> sysMenus = sysMenuService.initSysMenu();
return Result.success(sysMenus);
}
}

View File

@@ -1,19 +0,0 @@
package com.corewing.app.modules.admin.sys;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* 系统设置
*/
@Controller
@RequestMapping("/sys/setting")
public class SysSettingController {
@GetMapping
public String index() {
return "admin/sys/setting";
}
}

View File

@@ -1,118 +0,0 @@
package com.corewing.app.modules.admin.sys;
import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.corewing.app.common.Result;
import com.corewing.app.dto.api.SysLoginRequest;
import com.corewing.app.dto.sys.SysResetPasswordRequest;
import com.corewing.app.dto.sys.SysUserIdRequest;
import com.corewing.app.entity.SysUser;
import com.corewing.app.service.SysUserService;
import com.corewing.app.util.I18nUtil;
import com.corewing.app.util.IpUtil;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
/**
* 后台管理用户 Controller
*/
@Controller
@RequestMapping("/sys/user")
public class SysUserController {
private final SysUserService sysUserService;
public SysUserController(SysUserService sysUserService) {
this.sysUserService = sysUserService;
}
@GetMapping("/index")
public String index() {
return "admin/sys/user/index";
}
@GetMapping("/page")
@ResponseBody
public Result<Page<SysUser>> page(SysUser sysUser) {
Page<SysUser> sysUserPage = sysUserService.page(sysUser);
return Result.success(sysUserPage);
}
@PostMapping("/update")
@ResponseBody
public Result<String> update(@RequestBody SysUser sysUser) {
return Result.isBool(sysUserService.update(sysUser));
}
@PostMapping("/save")
@ResponseBody
public Result<String> save(@RequestBody SysUser sysUser) {
return Result.isBool(sysUserService.save(sysUser));
}
@PutMapping("/resetPassword")
@ResponseBody
public Result<String> resetPassword(@RequestBody SysResetPasswordRequest sysResetPasswordRequest) {
return Result.isBool(sysUserService.resetPassword(sysResetPasswordRequest));
}
@DeleteMapping("/delete")
@ResponseBody
public Result<String> delete(@RequestBody SysUserIdRequest sysUserIdRequest) {
return Result.isBool(sysUserService.removeById(sysUserIdRequest.getId()));
}
/**
* 后台管理登录
*/
@PostMapping("/login")
@ResponseBody
public Result<Map<String, Object>> login(@RequestBody SysLoginRequest request, HttpServletRequest httpRequest) {
try {
String loginIp = IpUtil.getClientIp(httpRequest);
// 执行登录
String token = sysUserService.login(request.getUsername(), request.getPassword(), loginIp);
// 查询用户信息
SysUser user = sysUserService.getByUsername(request.getUsername());
Map<String, Object> data = new HashMap<>();
data.put("token", token);
data.put("userId", user.getId());
data.put("username", user.getUsername());
data.put("realName", user.getRealName());
return Result.success(I18nUtil.getMessage("user.login.success"), data);
} catch (Exception e) {
return Result.error(e.getMessage());
}
}
/**
* 后台管理登出
*/
@PostMapping("/logout")
@ResponseBody
public Result<String> logout() {
StpUtil.logout();
return Result.success(I18nUtil.getMessage("user.logout.success"));
}
/**
* 获取当前登录用户信息
*/
@GetMapping("/info")
@ResponseBody
public Result<SysUser> getUserInfo() {
Long userId = StpUtil.getLoginIdAsLong();
SysUser user = sysUserService.getById(userId);
// 隐藏密码
user.setPassword(null);
return Result.success(user);
}
}

View File

@@ -4,7 +4,7 @@ import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.corewing.app.common.Result; import com.corewing.app.common.Result;
import com.corewing.app.dto.api.FeedbackRequest; import com.corewing.app.dto.FeedbackRequest;
import com.corewing.app.entity.Feedback; import com.corewing.app.entity.Feedback;
import com.corewing.app.service.FeedbackService; import com.corewing.app.service.FeedbackService;
import com.corewing.app.util.DingTalkUtil; import com.corewing.app.util.DingTalkUtil;

View File

@@ -0,0 +1,27 @@
package com.corewing.app.modules.app;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.corewing.app.common.Result;
import com.corewing.app.dto.ModelPageRequest;
import com.corewing.app.entity.AppModel;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 模型
*/
@RestController
@RequestMapping("/model")
public class AppModelController {
/**
* 获取模型集合
* @param modelPageRequest
* @return
*/
public Result<Page<AppModel>> page(ModelPageRequest modelPageRequest) {
return Result.success();
}
}

View File

@@ -4,8 +4,8 @@ import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.corewing.app.common.Result; import com.corewing.app.common.Result;
import com.corewing.app.dto.api.CreateParamRequest; import com.corewing.app.dto.CreateParamRequest;
import com.corewing.app.dto.api.UpdateParamRequest; import com.corewing.app.dto.UpdateParamRequest;
import com.corewing.app.entity.ParamsCenter; import com.corewing.app.entity.ParamsCenter;
import com.corewing.app.service.ParamsCenterService; import com.corewing.app.service.ParamsCenterService;
import com.corewing.app.util.I18nUtil; import com.corewing.app.util.I18nUtil;

View File

@@ -34,7 +34,7 @@ public class AppPrivacyPolicyController {
modelMap.put("list", list); modelMap.put("list", list);
// 获取最新更新时间 // 获取最新更新时间
modelMap.put("lastUpdateTime", privacyPolicyService.getLastUpdateTime()); modelMap.put("lastUpdateTime", privacyPolicyService.getLastUpdateTime());
return "app/privacyPolicy/index"; return "privacyPolicy/index";
} }
/** /**

View File

@@ -2,7 +2,7 @@ package com.corewing.app.modules.app;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;
import com.corewing.app.common.Result; import com.corewing.app.common.Result;
import com.corewing.app.dto.api.uploadReplaySessionRequest; import com.corewing.app.dto.uploadReplaySessionRequest;
import com.corewing.app.entity.ReplaySession; import com.corewing.app.entity.ReplaySession;
import com.corewing.app.service.ReplaySessionService; import com.corewing.app.service.ReplaySessionService;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;

View File

@@ -4,7 +4,6 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.corewing.app.common.Result; import com.corewing.app.common.Result;
import com.corewing.app.dto.api.TutorialListRequest;
import com.corewing.app.entity.Tutorial; import com.corewing.app.entity.Tutorial;
import com.corewing.app.entity.TutorialCategory; import com.corewing.app.entity.TutorialCategory;
import com.corewing.app.service.TutorialCategoryService; import com.corewing.app.service.TutorialCategoryService;
@@ -43,7 +42,7 @@ public class AppTutorialController {
public String viewDetail(@PathVariable Long tutorialId, ModelMap model) { public String viewDetail(@PathVariable Long tutorialId, ModelMap model) {
Tutorial tutorial = tutorialService.getById(tutorialId); Tutorial tutorial = tutorialService.getById(tutorialId);
model.put("tutorial", tutorial); model.put("tutorial", tutorial);
return "app/tutorial/viewDetail"; return "tutorial/viewDetail";
} }

View File

@@ -2,14 +2,12 @@ package com.corewing.app.modules.app;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;
import com.corewing.app.common.Result; import com.corewing.app.common.Result;
import com.corewing.app.dto.api.*; import com.corewing.app.dto.*;
import com.corewing.app.dto.biz.user.ResetPasswordRequest;
import com.corewing.app.entity.User; import com.corewing.app.entity.User;
import com.corewing.app.service.UserService; import com.corewing.app.service.UserService;
import com.corewing.app.service.VerifyCodeService; import com.corewing.app.service.VerifyCodeService;
import com.corewing.app.util.I18nUtil; import com.corewing.app.util.I18nUtil;
import com.corewing.app.util.IpUtil; import com.corewing.app.util.IpUtil;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;

View File

@@ -1,16 +1,14 @@
package com.corewing.app.modules.app; package com.corewing.app.modules.app;
import com.corewing.app.common.Result; import com.corewing.app.common.Result;
import com.corewing.app.dto.biz.appVersion.CheckVersionRequest; import com.corewing.app.dto.CheckVersionRequest;
import com.corewing.app.entity.AppVersion; import com.corewing.app.entity.AppVersion;
import com.corewing.app.service.AppVersionService; import com.corewing.app.service.AppVersionService;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.validation.Valid;
/** /**
* app升级校验接口 * app升级校验接口

View File

@@ -1,8 +1,7 @@
package com.corewing.app.service; package com.corewing.app.service;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.corewing.app.dto.biz.appVersion.CheckVersionRequest; import com.corewing.app.dto.CheckVersionRequest;
import com.corewing.app.entity.AppVersion; import com.corewing.app.entity.AppVersion;
import javax.validation.Valid; import javax.validation.Valid;

View File

@@ -3,7 +3,6 @@ package com.corewing.app.service;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.corewing.app.dto.biz.feedback.FeedbackBatchStatusRequest;
import com.corewing.app.entity.Feedback; import com.corewing.app.entity.Feedback;
import java.util.List; import java.util.List;
@@ -52,10 +51,4 @@ public interface FeedbackService extends IService<Feedback> {
*/ */
boolean updateStatus(Long id, Integer status); boolean updateStatus(Long id, Integer status);
/**
* 批量修改状态
* @param feedbackBatchStatusRequest
* @return
*/
boolean batchStatus(FeedbackBatchStatusRequest feedbackBatchStatusRequest);
} }

View File

@@ -1,7 +1,7 @@
package com.corewing.app.service; package com.corewing.app.service;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.corewing.app.dto.api.uploadReplaySessionRequest; import com.corewing.app.dto.uploadReplaySessionRequest;
import com.corewing.app.entity.ReplaySession; import com.corewing.app.entity.ReplaySession;
import java.util.List; import java.util.List;

View File

@@ -2,7 +2,6 @@ package com.corewing.app.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.corewing.app.dto.sys.SysResetPasswordRequest;
import com.corewing.app.entity.SysUser; import com.corewing.app.entity.SysUser;
/** /**
@@ -42,10 +41,4 @@ public interface SysUserService extends IService<SysUser> {
*/ */
void updateLoginInfo(Long userId, String loginIp); void updateLoginInfo(Long userId, String loginIp);
/**
* 修改密码
* @param sysResetPasswordRequest
* @return
*/
boolean resetPassword(SysResetPasswordRequest sysResetPasswordRequest);
} }

View File

@@ -1,14 +1,8 @@
package com.corewing.app.service; package com.corewing.app.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.corewing.app.dto.api.TutorialListRequest;
import com.corewing.app.entity.Tutorial; import com.corewing.app.entity.Tutorial;
import com.corewing.app.entity.TutorialCategory;
import org.springframework.web.bind.annotation.RequestBody;
import java.util.List;
public interface TutorialService extends IService<Tutorial> { public interface TutorialService extends IService<Tutorial> {
Page<Tutorial> pageList(Page<Tutorial> page, Long categoryId, String tutorialTitle, String lang); Page<Tutorial> pageList(Page<Tutorial> page, Long categoryId, String tutorialTitle, String lang);

View File

@@ -3,11 +3,9 @@ package com.corewing.app.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService; import com.baomidou.mybatisplus.extension.service.IService;
import com.corewing.app.common.Result; import com.corewing.app.common.Result;
import com.corewing.app.dto.api.CodeLoginRequest; import com.corewing.app.dto.CodeLoginRequest;
import com.corewing.app.dto.api.ForgetPasswordRequest; import com.corewing.app.dto.ForgetPasswordRequest;
import com.corewing.app.dto.api.LogoffRequest; import com.corewing.app.dto.LogoffRequest;
import com.corewing.app.dto.biz.user.BizUserStatusRequest;
import com.corewing.app.dto.biz.user.ResetPasswordRequest;
import com.corewing.app.entity.User; import com.corewing.app.entity.User;
import java.util.Map; import java.util.Map;
@@ -106,19 +104,6 @@ public interface UserService extends IService<User> {
*/ */
void updateLoginInfo(Long userId, String loginIp); void updateLoginInfo(Long userId, String loginIp);
/**
* 修改密码
* @param resetPasswordRequest
* @return
*/
boolean resetPassword(ResetPasswordRequest resetPasswordRequest);
/**
* 批量修改状态
* @param bizUserStatusRequest
* @return
*/
boolean batchStatus(BizUserStatusRequest bizUserStatusRequest);
/** /**
* 忘记密码 * 忘记密码

View File

@@ -2,7 +2,7 @@ package com.corewing.app.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.corewing.app.dto.biz.appVersion.CheckVersionRequest; import com.corewing.app.dto.CheckVersionRequest;
import com.corewing.app.entity.AppVersion; import com.corewing.app.entity.AppVersion;
import com.corewing.app.mapper.AppVersionMapper; import com.corewing.app.mapper.AppVersionMapper;
import com.corewing.app.service.AppVersionService; import com.corewing.app.service.AppVersionService;

View File

@@ -6,7 +6,6 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.corewing.app.common.page.PageContext; import com.corewing.app.common.page.PageContext;
import com.corewing.app.dto.biz.feedback.FeedbackBatchStatusRequest;
import com.corewing.app.entity.Feedback; import com.corewing.app.entity.Feedback;
import com.corewing.app.mapper.FeedbackMapper; import com.corewing.app.mapper.FeedbackMapper;
import com.corewing.app.service.FeedbackService; import com.corewing.app.service.FeedbackService;
@@ -96,15 +95,4 @@ public class FeedbackServiceImpl extends ServiceImpl<FeedbackMapper, Feedback> i
return this.updateById(feedback); return this.updateById(feedback);
} }
@Transactional(rollbackFor = Exception.class)
@Override
public boolean batchStatus(FeedbackBatchStatusRequest feedbackBatchStatusRequest) {
feedbackBatchStatusRequest.getIds().forEach(id -> {
LambdaUpdateWrapper<Feedback> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(Feedback::getId, id);
wrapper.set(Feedback::getStatus, feedbackBatchStatusRequest.getStatus());
update(wrapper);
});
return true;
}
} }

View File

@@ -3,14 +3,12 @@ package com.corewing.app.service.impl;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.corewing.app.dto.api.TrajectoryData; import com.corewing.app.dto.TrajectoryData;
import com.corewing.app.dto.api.uploadReplaySessionRequest; import com.corewing.app.dto.uploadReplaySessionRequest;
import com.corewing.app.entity.ReplaySession; import com.corewing.app.entity.ReplaySession;
import com.corewing.app.mapper.ReplaySessionMapper; import com.corewing.app.mapper.ReplaySessionMapper;
import com.corewing.app.service.ReplaySessionService; import com.corewing.app.service.ReplaySessionService;
import com.corewing.app.util.OSSUploadUtil; import com.corewing.app.util.OSSUploadUtil;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken; import com.google.gson.reflect.TypeToken;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;

View File

@@ -5,7 +5,6 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.corewing.app.common.page.PageContext; import com.corewing.app.common.page.PageContext;
import com.corewing.app.dto.sys.SysResetPasswordRequest;
import com.corewing.app.entity.SysUser; import com.corewing.app.entity.SysUser;
import com.corewing.app.mapper.SysUserMapper; import com.corewing.app.mapper.SysUserMapper;
import com.corewing.app.service.SysUserService; import com.corewing.app.service.SysUserService;
@@ -88,15 +87,4 @@ public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> impl
this.updateById(user); this.updateById(user);
} }
@Override
public boolean resetPassword(SysResetPasswordRequest sysResetPasswordRequest) {
SysUser sysUser = getById(sysResetPasswordRequest.getUserId());
if(sysUser == null) {
throw new RuntimeException(I18nUtil.getMessage("error.user.not.found"));
}
String encryptPassword = DigestUtils.md5DigestAsHex(sysResetPasswordRequest.getPassword().getBytes(StandardCharsets.UTF_8));
sysUser.setPassword(encryptPassword);
return updateById(sysUser);
}
} }

View File

@@ -1,13 +1,10 @@
package com.corewing.app.service.impl; package com.corewing.app.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.corewing.app.common.page.PageContext; import com.corewing.app.common.page.PageContext;
import com.corewing.app.dto.api.TutorialListRequest;
import com.corewing.app.entity.Tutorial; import com.corewing.app.entity.Tutorial;
import com.corewing.app.entity.TutorialCategory;
import com.corewing.app.entity.TutorialCategoryRelation; import com.corewing.app.entity.TutorialCategoryRelation;
import com.corewing.app.mapper.TutorialMapper; import com.corewing.app.mapper.TutorialMapper;
import com.corewing.app.service.TutorialCategoryRelationService; import com.corewing.app.service.TutorialCategoryRelationService;
@@ -15,10 +12,8 @@ import com.corewing.app.service.TutorialCategoryService;
import com.corewing.app.service.TutorialService; import com.corewing.app.service.TutorialService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.List;
@Service @Service
public class TutorialServiceImpl extends ServiceImpl<TutorialMapper, Tutorial> implements TutorialService { public class TutorialServiceImpl extends ServiceImpl<TutorialMapper, Tutorial> implements TutorialService {

View File

@@ -7,18 +7,15 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.corewing.app.common.Result; import com.corewing.app.common.Result;
import com.corewing.app.common.page.PageContext; import com.corewing.app.common.page.PageContext;
import com.corewing.app.dto.api.CodeLoginRequest; import com.corewing.app.dto.CodeLoginRequest;
import com.corewing.app.dto.api.ForgetPasswordRequest; import com.corewing.app.dto.ForgetPasswordRequest;
import com.corewing.app.dto.api.LogoffRequest; import com.corewing.app.dto.LogoffRequest;
import com.corewing.app.dto.biz.user.BizUserStatusRequest;
import com.corewing.app.dto.biz.user.ResetPasswordRequest;
import com.corewing.app.entity.User; import com.corewing.app.entity.User;
import com.corewing.app.mapper.UserMapper; import com.corewing.app.mapper.UserMapper;
import com.corewing.app.service.UserService; import com.corewing.app.service.UserService;
import com.corewing.app.service.VerifyCodeService; import com.corewing.app.service.VerifyCodeService;
import com.corewing.app.util.*; import com.corewing.app.util.*;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.codehaus.jettison.json.JSONObject;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.DigestUtils; import org.springframework.util.DigestUtils;
@@ -246,29 +243,7 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements Us
this.updateById(user); this.updateById(user);
} }
@Override
public boolean resetPassword(ResetPasswordRequest resetPasswordRequest) {
User user = getById(resetPasswordRequest.getUserId());
if(user == null) {
throw new RuntimeException(I18nUtil.getMessage("error.user.not.found"));
}
// 更新新密码
String newPasswordMd5 = DigestUtils.md5DigestAsHex(resetPasswordRequest.getPassword().getBytes(StandardCharsets.UTF_8));
user.setPassword(newPasswordMd5);
return updateById(user);
}
@Transactional(rollbackFor = Exception.class)
@Override
public boolean batchStatus(BizUserStatusRequest bizUserStatusRequest) {
bizUserStatusRequest.getIds().forEach(id -> {
LambdaUpdateWrapper<User> wrapper = new LambdaUpdateWrapper<>();
wrapper.eq(User::getId, id);
wrapper.set(User::getStatus, bizUserStatusRequest.getStatus());
this.update(wrapper);
});
return true;
}
@Override @Override
public boolean forgetPassword(ForgetPasswordRequest request) { public boolean forgetPassword(ForgetPasswordRequest request) {

View File

@@ -1,112 +0,0 @@
:root {
--primary-color: #5B5FDE;
--secondary-color: #0EA5E9;
--accent-color: #FF6B6B;
--error-color: #F43F5E;
--success-color: #10B981;
--warning-color: #F59E0B;
--info-color: #3B82F6;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: #F3F4F6;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
.main-container {
padding: 14px;
}
/* 欢迎卡片 */
.welcome-card {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
border-radius: 16px;
padding: 35px;
margin-bottom: 25px;
box-shadow: 0 10px 30px rgba(91, 95, 222, 0.2);
position: relative;
overflow: hidden;
}
.welcome-card::before {
content: '';
position: absolute;
width: 250px;
height: 250px;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
top: -80px;
right: -80px;
}
.welcome-card h2 {
font-size: 26px;
font-weight: 700;
margin-bottom: 10px;
position: relative;
z-index: 1;
}
.welcome-card p {
font-size: 15px;
opacity: 0.95;
position: relative;
z-index: 1;
}
/* 统计卡片 */
.stat-card {
background: white;
border-radius: 12px;
padding: 25px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
transition: all 0.3s;
border: 1px solid #E5E7EB;
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
.stat-card i {
font-size: 40px;
opacity: 0.9;
margin-bottom: 12px;
}
.stat-card h3 {
font-size: 30px;
font-weight: 700;
margin: 12px 0 6px;
color: #1F2937;
}
.stat-card p {
color: #6B7280;
font-size: 13px;
margin: 0;
}
.stat-card.primary i {
color: var(--primary-color);
}
.stat-card.success i {
color: var(--success-color);
}
.stat-card.warning i {
color: var(--warning-color);
}
.stat-card.info i {
color: var(--info-color);
}

View File

@@ -1,217 +0,0 @@
:root {
--primary-color: #5B5FDE;
--secondary-color: #0EA5E9;
--accent-color: #FF6B6B;
--error-color: #F43F5E;
--success-color: #10B981;
--warning-color: #F59E0B;
--info-color: #3B82F6;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background-color: #F3F4F6;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
/* 侧边栏样式 */
.sidebar {
min-height: 100vh;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
color: white;
padding: 0;
position: fixed;
left: 0;
top: 0;
width: 180px;
box-shadow: 2px 0 10px rgba(0, 0, 0, 0.1);
}
.sidebar-header {
padding: 18px 12px;
text-align: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.sidebar-header h4 {
font-size: 16px;
font-weight: 700;
margin: 0;
}
.sidebar-header i {
margin-right: 5px;
font-size: 16px;
}
.sidebar-menu {
padding: 12px 8px;
}
.sidebar .nav-link {
color: rgba(255, 255, 255, 0.85);
padding: 9px 10px;
border-radius: 8px;
margin: 3px 0;
transition: all 0.3s;
font-weight: 500;
font-size: 13px;
display: flex;
align-items: center;
}
.sidebar .nav-link i {
width: 18px;
font-size: 15px;
}
.sidebar .nav-link:hover {
background-color: rgba(255, 255, 255, 0.15);
color: white;
transform: translateX(5px);
}
.sidebar .nav-link.active {
background-color: rgba(255, 255, 255, 0.25);
color: white;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.sidebar-footer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 12px 8px;
}
.btn-logout {
width: 100%;
padding: 9px;
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
border-radius: 8px;
font-weight: 600;
font-size: 13px;
transition: all 0.3s;
}
.btn-logout:hover {
background: rgba(255, 255, 255, 0.25);
transform: translateY(-2px);
}
/* 顶部导航栏 */
.navbar {
background-color: white;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
padding: 12px 25px;
}
.navbar-brand {
font-size: 18px;
font-weight: 700;
color: #1F2937;
}
.user-info {
display: flex;
align-items: center;
gap: 8px;
color: #4B5563;
font-weight: 500;
font-size: 14px;
}
.user-info i {
font-size: 20px;
color: var(--primary-color);
}
/* 主内容区 */
.main-content {
margin-left: 180px;
}
/* 内容区域 */
.content-wrapper {
padding: 0;
}
/* 页面标题 */
.page-title {
font-size: 24px;
font-weight: 700;
color: #1F2937;
margin-bottom: 20px;
}
/* 卡片容器 */
.card {
background: white;
border-radius: 12px;
border: 1px solid #E5E7EB;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.card-body {
padding: 25px;
}
/* iframe样式 */
.page-iframe {
width: 100%;
height: calc(100vh - 72px); /* 减去导航栏高度 */
border: none;
transition: opacity 0.3s ease;
border-radius: 8px;
}
/* 加载状态 */
.loading-state {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
/* 响应式 */
@media (max-width: 768px) {
.sidebar {
width: 100%;
position: relative;
min-height: auto;
}
.main-content {
margin-left: 0;
}
.sidebar-footer {
position: static;
margin-top: 15px;
}
.welcome-card {
padding: 25px;
}
.welcome-card h2 {
font-size: 20px;
}
.content-wrapper {
padding: 0;
min-height: calc(100vh - 72px);
}
}

View File

@@ -1,124 +0,0 @@
/* 基础样式优化 */
body {
background-color: #f8f9fa;
padding: 20px;
}
.table-container {
background: #fff;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
border-radius: 8px;
padding: 18px;
margin-bottom: 2rem;
}
.search-bar {
margin-bottom: 2rem;
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: center;
}
/* 批量操作样式(表格下方左侧) */
.batch-actions {
display: flex;
align-items: center;
}
.page-info {
display: flex;
align-items: center;
color: #6c757d;
margin-right: 1rem;
}
/* 表格样式优化 */
.table {
margin-bottom: 0;
}
.table-hover tbody tr:hover {
background-color: rgba(14, 165, 233, 0.05);
cursor: pointer;
}
.table tbody td, table thead th{
text-align: center;
}
/*.table tbody tr.selected {*/
/* background-color: rgba(14, 165, 233, 0.1);*/
/* border-color: #0ea5e9;*/
/*}*/
/*.table th {*/
/* font-weight: 600;*/
/* color: #212529;*/
/* border-bottom-width: 2px;*/
/*}*/
/*.table td, .table th {*/
/* vertical-align: middle;*/
/* padding: 12px 16px;*/
/*}*/
/* 搜索框样式 */
.search-item {
flex-grow: 1;
flex-shrink: 0;
min-width: 200px;
max-width: 280px;
}
.input-group-text {
background-color: #f1f3f5;
border-color: #dee2e6;
}
/* 按钮样式优化 */
.btn {
padding: 6px 16px;
border-radius: 6px;
}
.btn-sm {
padding: 4px 12px;
}
/* 徽章样式 */
.badge {
padding: 4px 8px;
font-size: 13px;
font-weight: 500;
border-radius: 4px;
}
/* 加载动画 */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
}
/* 无数据提示 */
.no-data {
color: #6c757d;
text-align: center;
padding: 40px 0;
}
.no-data i {
font-size: 48px;
margin-bottom: 16px;
color: #adb5bd;
}
.ellipsis-single {
white-space: nowrap; /* 禁止换行 */
overflow: hidden; /* 隐藏超出部分 */
text-overflow: ellipsis; /* 显示省略号 */
max-width: 200px; /* 自定义最大宽度(根据需求调整,如 150px/300px */
}

View File

@@ -1,83 +0,0 @@
// 引入 axios@1.4.0(确保已引入 axios.min.js
if (!window.axios) throw new Error("请先引入 axios@1.4.0 脚本!");
const service = axios.create({
baseURL: "",
timeout: 5000,
headers: {
"Content-Type": "application/json;charset=utf-8"
}
});
// -------------------------- 请求拦截器:自动携带 Token --------------------------
service.interceptors.request.use(
(config) => {
const token = localStorage.getItem("token");
if (token) {
config.headers["Authorization"] = token;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// -------------------------- 响应拦截器:统一处理结果 --------------------------
service.interceptors.response.use(
(response) => {
return response.data;
},
(error) => {
let errorMsg = "请求失败,请稍后重试";
if (error.response) {
switch (error.response.status) {
case 401:
errorMsg = "Token 已过期,请重新登录";
localStorage.removeItem("token");
top.location.href = "/admin/login.html";
break;
case 403:
errorMsg = "暂无权限访问该接口";
break;
case 500:
errorMsg = "服务器内部错误";
break;
default:
errorMsg = error.response.data?.msg || errorMsg;
}
} else if (error.request) {
errorMsg = "网络异常,请检查网络连接";
}
console.error("接口请求错误:", errorMsg);
return Promise.reject(error);
}
);
// -------------------------- 封装常用请求方法get/post/put/delete --------------------------
const request = {
// GET
get(url, params = {}) {
return service.get(url, { params });
},
// POST
post(url, data = {}) {
return service.post(url, data);
},
// PUT
put(url, data = {}) {
return service.put(url, data);
},
// DELETE
delete(url, params = {}) {
return service.delete(url, { params });
}
};
window.request = request;

View File

@@ -1,55 +0,0 @@
// assets/js/confirmModal.js
(function(window) {
// 全局确认弹窗函数
window.showConfirmModal = function(options = {}) {
// 默认配置
const {
title = '系统提示',
content = '确定执行此操作吗?',
onConfirm = () => {}, // 确认回调
onCancel = () => {} // 取消回调(可选)
} = options;
// 生成唯一ID避免重复
const modalId = `confirm-modal-${Date.now()}`;
// 动态创建模态框DOM
const modalHtml = `
<div class="modal fade" id="${modalId}" tabindex="-1" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5">${title}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">${content}</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" id="${modalId}-confirm">确认</button>
</div>
</div>
</div>
</div>
`;
// 插入到页面
document.body.insertAdjacentHTML('beforeend', modalHtml);
// 初始化并显示弹窗
const modal = new bootstrap.Modal(document.getElementById(modalId));
modal.show();
// 绑定确认按钮事件
document.getElementById(`${modalId}-confirm`).addEventListener('click', () => {
onConfirm(); // 执行确认逻辑
modal.hide();
});
// 弹窗关闭后清理DOM
const modalElement = document.getElementById(modalId);
modalElement.addEventListener('hidden.bs.modal', () => {
onCancel(); // 执行取消逻辑(可选)
modalElement.remove(); // 移除DOM避免冗余
});
};
})(window);

View File

@@ -1,145 +0,0 @@
if (!window.Vue) {
const vueScript = document.createElement('script');
vueScript.src = 'https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.prod.js';
document.head.appendChild(vueScript);
}
if (!document.querySelector('link[href*="bootstrap-icons"]')) {
const iconLink = document.createElement('link');
iconLink.rel = 'stylesheet';
iconLink.href = 'https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css';
document.head.appendChild(iconLink);
}
const style = document.createElement('style');
style.textContent = `
.h5-message-container {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 9999;
pointer-events: none;
}
.h5-message {
display: flex;
align-items: center;
padding: 14px 24px;
border-radius: 8px;
color: #fff;
font-size: 16px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);
animation: h5MsgShow 0.4s ease-out forwards;
}
.h5-message.success { background-color: rgba(40, 167, 69, 0.95); }
.h5-message.error { background-color: rgba(220, 53, 69, 0.95); }
.h5-message.warning { background-color: rgba(255, 193, 7, 0.95); color: #333; }
.h5-message.info { background-color: rgba(13, 110, 253, 0.95); }
.h5-message-icon {
margin-right: 10px;
font-size: 20px;
}
@keyframes h5MsgShow {
from { opacity: 0; transform: translate(-50%, -50%) scale(0.9); }
to { opacity: 1; transform: translate(-50%, -50%) scale(1); }
}
.h5-message.hide { animation: h5MsgHide 0.4s ease-in forwards; }
@keyframes h5MsgHide {
from { opacity: 1; transform: translate(-50%, -50%) scale(1); }
to { opacity: 0; transform: translate(-50%, -50%) scale(0.9); }
}
`;
document.head.appendChild(style);
const messageTemplate = `
<div class="h5-message" :class="[type, { hide: !visible }]">
<i class="h5-message-icon" :class="iconClass"></i>
<span>{{ message }}</span>
</div>
`;
const messageInstances = [];
function showMessage(options) {
if (typeof options === 'string') {
options = { message: options };
}
const config = {
message: '',
type: 'info',
duration: 3000,
...options,
duration: Math.max(1000, options.duration || 3000)
};
const checkVueLoaded = setInterval(() => {
if (window.Vue) {
clearInterval(checkVueLoaded);
const container = document.createElement('div');
container.className = 'h5-message-container';
document.body.appendChild(container);
const app = Vue.createApp({
template: messageTemplate,
data() {
return {
message: config.message,
type: config.type,
visible: true
};
},
computed: {
iconClass() {
const icons = {
success: 'bi bi-check-circle',
error: 'bi bi-exclamation-circle',
warning: 'bi bi-exclamation-triangle',
info: 'bi bi-info-circle'
};
return icons[this.type] || icons.info;
}
},
mounted() {
const showTimer = setTimeout(() => {
this.visible = false;
const hideTimer = setTimeout(() => {
const index = messageInstances.indexOf(app);
if (index > -1) {
messageInstances.splice(index, 1);
}
app.unmount(container);
document.body.removeChild(container);
clearTimeout(hideTimer);
}, 400);
clearTimeout(showTimer);
}, config.duration);
}
});
const instance = app.mount(container);
messageInstances.push(instance);
}
}, 50);
setTimeout(() => {
clearInterval(checkVueLoaded);
if (!window.Vue) {
alert(config.message);
}
}, 1000);
}
window.$message = showMessage;
['success', 'error', 'warning', 'info'].forEach(type => {
window.$message[type] = (message, duration) => {
window.$message({
message,
type,
// 确保时长有效用户未传则用默认3秒
duration: duration ? Math.max(1000, duration) : 500
});
};
});

View File

@@ -1,593 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>反馈管理</title>
<!-- 外部引入库文件 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<!-- 外部引入自定义样式 -->
<link rel="stylesheet" href="/assets/css/table.css">
</head>
<body id="app">
<div class="main-container">
<div class="table-container">
<h4 class="mb-3">反馈管理</h4>
<div class="search-bar">
<div class="search-item">
<div class="input-group">
<span class="input-group-text">
名称
</span>
<input
v-model="searchParams.title"
type="text"
class="form-control"
placeholder="请输入名称"
@keyup.enter="fetchData()"
>
</div>
</div>
<div class="search-item">
<div class="input-group">
<span class="input-group-text">
流程
</span>
<select
v-model="searchParams.status"
class="form-select"
@change="fetchData()"
>
<option value="">全部</option>
<option value="0">待处理</option>
<option value="1">处理中</option>
<option value="2">已完成</option>
<option value="3">已关闭</option>
</select>
</div>
</div>
<div class="d-flex gap-2">
<button class="btn btn-primary" @click="fetchData()">
<i class="bi bi-search me-1"></i> 搜索
</button>
<button class="btn btn-success" @click="resetSearch()">
<i class="bi bi-arrow-counterclockwise me-1"></i> 重置
</button>
</div>
</div>
<!-- <div class="mb-2">-->
<!-- <button class="btn btn-info" @click="openAddModal()">-->
<!-- <i class="bi bi-plus-circle me-1"></i> 新增-->
<!-- </button>-->
<!-- </div>-->
<div class="table-responsive">
<table class="table table-sm table-striped table-hover table-bordered ">
<thead class="table-light">
<tr>
<!-- 全选复选框 -->
<th style="width: 50px;">
<input
type="checkbox"
class="form-check-input"
v-model="selectAll"
@change="toggleSelectAll()"
>
</th>
<th>反馈问题</th>
<th>反馈详情</th>
<th>反馈用户</th>
<th>联系方式</th>
<th>流程</th>
<th>创建时间</th>
<th style="width: 120px;">操作</th>
</tr>
</thead>
<tbody>
<!-- 加载中状态 -->
<tr v-if="loading">
<td colspan="8" class="p-0">
<div class="loading-container">
<div class="spinner-border text-primary" role="status" style="width: 2rem; height: 2rem;">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2 text-muted">加载中,请稍候...</p>
</div>
</td>
</tr>
<!-- 无数据状态 -->
<tr v-else-if="tableData.length === 0">
<td colspan="8" class="p-0">
<div class="no-data">
<i class="bi bi-folder-x"></i>
<h5>暂无匹配数据</h5>
<p class="text-muted">请尝试调整搜索条件或重置查询</p>
</div>
</td>
</tr>
<!-- 数据列表 -->
<tr v-else v-for="(item, index) in tableData" :key="item.id"
:class="{ selected: selectedIds.includes(item.id) }">
<td>
<input
type="checkbox"
class="form-check-input"
v-model="selectedIds"
:value="item.id"
@change="toggleSelectItem(item.id)"
>
</td>
<td>{{ item.title }}</td>
<td class="ellipsis-single">{{ item.content }}</td>
<td>{{ item.nickName }}</td>
<td>{{ item.contact }}</td>
<td>
<span class="badge" :class="item.status === 0 ? 'bg-warning' : (item.status === 2 ? 'bg-success': (item.status === 3 ? 'bg-dark':'bg-info'))">
{{ item.statusName }}
</span>
</td>
<td>{{ formatTime(item.createTime) }}</td>
<td>
<div class="d-flex gap-1">
<button class="btn btn-sm btn-primary" @click="openEditModal(item)">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-danger" @click="handleDelete(item.id)">
<i class="bi bi-trash"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="batch-actions mt-3">
<div class="d-flex align-items-center gap-2">
<span class="text-muted">已选中 {{ selectedIds.length }} 条数据</span>
<select
class="form-select"
v-model="batchAction"
:disabled="selectedIds.length === 0"
@change="handleBatchOperation"
style="width: auto; min-width: 160px;"
>
<option value="">-- 批量操作 --</option>
<option value="delete">批量删除</option>
</select>
<button
class="btn btn-secondary"
@click="clearSelected()"
:disabled="selectedIds.length === 0"
>
<i class="bi bi-x-circle me-1"></i> 取消选择
</button>
</div>
</div>
<div class="d-flex justify-content-between align-items-center mt-3">
<div class="page-info">
共 {{ total }} 条数据,当前第 {{ pageNum }}/{{ totalPages }} 页
</div>
<nav>
<ul class="pagination pagination-sm mb-0">
<li class="page-item" :class="{ disabled: pageNum === 1 }">
<a class="page-link" href="#" @click.prevent="changePage(1)">首页</a>
</li>
<li class="page-item" :class="{ disabled: pageNum === 1 }">
<a class="page-link" href="#" @click.prevent="changePage(pageNum - 1)">上一页</a>
</li>
<li class="page-item" :class="page === pageNum ? 'active' : ''" v-for="page in pageList"
:key="page">
<a class="page-link" href="#" @click.prevent="changePage(page)">{{ page }}</a>
</li>
<li class="page-item" :class="{ disabled: pageNum === totalPages }">
<a class="page-link" href="#" @click.prevent="changePage(pageNum + 1)">下一页</a>
</li>
<li class="page-item" :class="{ disabled: pageNum === totalPages }">
<a class="page-link" href="#" @click.prevent="changePage(totalPages)">末页</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<!-- 新增 or 编辑 -->
<div class="modal fade" id="addOrEditModel" tabindex="-1" aria-labelledby="addOrEditModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="addOrEditModalLabel">{{ addOrEditTitle }}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form>
<div class="mb-3 row">
<label class="col-sm-3 col-form-label">反馈标题</label>
<div class="col-sm-9">
<input type="text" class="form-control" v-model="addOrEditDto.title"
placeholder="请输入名称">
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-3 col-form-label">联系方式</label>
<div class="col-sm-9">
<input type="text" class="form-control" v-model="addOrEditDto.contact"
placeholder="请输入联系方式">
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-3 col-form-label">反馈内容</label>
<div class="col-sm-9">
<!-- &lt;!&ndash; 富文本编辑器容器 &ndash;&gt;-->
<!-- <div id="contentEditor" style="border: 1px solid #dee2e6; border-radius: 0.375rem;"></div>-->
<!-- &lt;!&ndash; 错误提示 &ndash;&gt;-->
<!-- <div class="invalid-feedback" id="contentError" style="display: none; margin-top: 0.25rem;"></div>-->
<textarea class="form-control" v-model="addOrEditDto.content"></textarea>
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-3 col-form-label">流程</label>
<div class="col-sm-9">
<select class="form-control" v-model="addOrEditDto.status">
<option value="0">待处理</option>
<option value="1">处理中</option>
<option value="2">已完成</option>
<option value="3">已关闭</option>
</select>
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-3 col-form-label">反馈用户</label>
<div class="col-sm-9">
<input type="text" class="form-control" readonly v-model="addOrEditDto.nickName">
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
<button type="button" class="btn btn-primary" @click.prevent="saveEntity">保存</button>
</div>
</div>
</div>
</div>
<!-- 外部引入库文件 -->
<script src="https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios@1.4.0/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://unpkg.com/wangeditor@4.7.15/dist/wangEditor.min.js"></script>
<!-- 外部引入模拟请求文件 -->
<script src="/assets/js/axiosRequest.js"></script>
<script src="/assets/js/message.js"></script>
<script src="/assets/js/confirmModal.js"></script>
<!-- 页面核心逻辑 -->
<script>
const {createApp} = Vue;
createApp({
data() {
return {
tableData: [], // 表格数据源
loading: false, // 加载状态
searchParams: {
title: '',
status: '',
},
// 分页参数
pageNum: 1, // 当前页码
pageSize: 10, // 每页条数
total: 0, // 总数据量
totalPages: 0, // 总页数
// 批量操作
selectedIds: [], // 选中的ID集合
selectAll: false, // 全选状态
batchAction: '', // 批量操作选择
pageRange: 5,
modalInstances: {},
addOrEditTitle: '',
addOrEditDto: {
id: null,
title: null,
content: null,
contact: null,
status: 0,
nickName: 0,
},
editor: null, // WangEditor 实例
}
},
computed: {
// 计算当前显示的页码列表
pageList() {
const list = [];
if (this.totalPages === 0) return list;
// 总页数小于等于显示范围,直接显示所有页码
if (this.totalPages <= this.pageRange) {
for (let i = 1; i <= this.totalPages; i++) {
list.push(i);
}
} else {
// 总页数大于显示范围显示当前页前后2个页码
let start = Math.max(1, this.pageNum - 2);
let end = Math.min(this.totalPages, this.pageNum + 2);
// 确保显示5个页码
if (end - start < this.pageRange - 1) {
if (start === 1) {
end = this.pageRange;
} else if (end === this.totalPages) {
start = this.totalPages - this.pageRange + 1;
}
}
for (let i = start; i <= end; i++) {
list.push(i);
}
}
return list;
}
},
methods: {
formatTime(time) {
if (!time) return '-';
const date = new Date(time);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
},
async fetchData() {
this.loading = true;
try {
const response = await request.get('/biz/feedback/page', {
...this.searchParams,
current: this.pageNum,
size: this.pageSize
});
console.log(response)
if (response.code === 200) {
this.tableData = response.data.records; // 列表数据
this.total = response.data.total; // 总条数
this.totalPages = response.data.pages; // 总页数
}
} catch (error) {
console.error('获取数据失败:', error);
this.tableData = [];
this.total = 0;
this.totalPages = 0;
} finally {
this.loading = false;
this.clearSelected();
}
},
// 切换页码
changePage(page) {
if (page < 1 || page > this.totalPages || page === this.pageNum) return;
this.pageNum = page;
this.fetchData();
},
// 重置搜索
resetSearch() {
this.searchParams = {
title: '',
status: '',
};
this.pageNum = 1;
this.fetchData();
},
// 全选/取消全选
toggleSelectAll() {
if (this.selectAll) {
this.selectedIds = this.tableData.map(item => item.id);
} else {
this.selectedIds = [];
}
},
// 单个选中/取消选中
toggleSelectItem(id) {
this.selectAll = this.selectedIds.length === this.tableData.length && this.tableData.length > 0;
},
// 清空选中状态
clearSelected() {
this.selectedIds = [];
this.selectAll = false;
this.batchAction = '';
},
// 处理批量操作
handleBatchOperation() {
if (!this.batchAction || this.selectedIds.length === 0) return;
switch (this.batchAction) {
case 'delete':
this.handleBatchDelete();
break;
}
this.batchAction = '';
},
// 批量删除
async handleBatchDelete() {
showConfirmModal({
title: '删除确认',
content: `确定要删除选中的 ${this.selectedIds.length} 条数据吗?`,
onConfirm: async () => {
try {
const response = await request.post('/biz/feedback/batchDelete', {ids: this.selectedIds});
if (response.code === 200) {
$message.success("删除成功");
this.fetchData();
}
} catch (error) {
console.error('删除失败:', error);
$message.error('删除失败,请重试!');
}
}
});
},
// 单个删除
async handleDelete(id) {
showConfirmModal({
title: '删除确认',
content: '确定要删除这条数据吗?',
onConfirm: async () => {
try {
const response = await request.delete('/biz/feedback/delete', {id});
if (response.code === 200) {
$message.success("删除成功");
this.fetchData(); // 重新加载数据
}
} catch (error) {
console.error('删除失败:', error);
$message.error('删除失败,请重试!');
}
}
});
},
// 打开新增模态框
openAddModal() {
this.addOrEditTitle = '新增';
this.clearForm();
this.modalInstances['addOrEditModel'].show();
},
// 打开编辑模态框
openEditModal(item) {
this.addOrEditTitle = '编辑';
this.addOrEditDto = {...item};
// 延迟回显富文本内容
setTimeout(() => {
if (this.editor) this.editor.txt.html(item.content || '');
}, 300);
this.modalInstances['addOrEditModel'].show();
},
saveEntity() {
let url = (this.addOrEditDto === null || this.addOrEditDto.id === null) ? '/biz/feedback/save' : '/biz/feedback/update';
request.post(url, this.addOrEditDto)
.then((res) => {
if (res.code === 200) {
$message.success('保存成功!', 500);
this.fetchData();
this.clearForm();
this.modalInstances['addOrEditModel'].hide();
} else {
$message.error('保存失败!', 500);
}
}).catch(() => {
})
},
// 清空表单数据
clearForm() {
this.addOrEditDto = {
id: null,
title: null,
content: null,
contact: null,
status: 0,
nickName: 0,
};
},
// 初始化 WangEditor
initWangEditor() {
const _this = this;
// 获取编辑器容器
const editorDom = document.getElementById('contentEditor');
// 创建编辑器实例
this.editor = new wangEditor(editorDom);
// 编辑器配置
this.editor.config.height = 200; // 高度
this.editor.config.zIndex = 1050; // 层级(确保在模态框上方)
this.editor.config.uploadImgShowBase64 = true; // 图片以 Base64 格式保存(无需后端接口)
this.editor.config.menus = [
'head', // 标题
'bold', // 粗体
'italic', // 斜体
'underline', // 下划线
'strikeThrough', // 删除线
'foreColor', // 文字颜色
'backColor', // 背景颜色
'link', // 插入链接
'list', // 列表
'justify', // 对齐方式
'image', // 插入图片
'table', // 表格
'code', // 插入代码
'undo', // 撤销
'redo' // 重做
];
// 内容变化时同步到 Vue 数据
this.editor.config.onchange = function (html) {
_this.addOrEditDto.content = html;
};
// 创建编辑器
this.editor.create();
},
},
mounted() {
this.fetchData();
const modalIds = ['uploadFileModal', 'addOrEditModel'];
modalIds.forEach(id => {
const modalElement = document.getElementById(id);
if (modalElement) {
this.modalInstances[id] = new bootstrap.Modal(modalElement, {
backdrop: 'static',
keyboard: true
});
}
});
// // 初始化 WangEditor延迟初始化确保DOM加载完成
// setTimeout(() => {
// this.initWangEditor();
// }, 500);
//
// // 监听模态框关闭事件,避免内存泄漏
// const modal = document.getElementById('addOrEditModel');
// modal.addEventListener('hidden.bs.modal', () => {
// if (this.editor) {
// this.editor.txt.clear(); // 清空内容
// document.getElementById('contentEditor').style.borderColor = '#dee2e6';
// document.getElementById('contentError').style.display = 'none';
// }
// });
}
}).mount('#app');
</script>
</body>
</html>

View File

@@ -1,616 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>固件管理</title>
<!-- 外部引入库文件 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<!-- 外部引入自定义样式 -->
<link rel="stylesheet" href="/assets/css/table.css">
</head>
<body id="app">
<div class="main-container">
<div class="table-container">
<h4 class="mb-3">固件管理</h4>
<div class="search-bar">
<div class="search-item">
<div class="input-group">
<span class="input-group-text">
固件名
</span>
<input
v-model="searchParams.firmwareName"
type="text"
class="form-control"
placeholder="请输入固件名称"
@keyup.enter="fetchData()"
>
</div>
</div>
<div class="search-item">
<div class="input-group">
<span class="input-group-text">
类型
</span>
<select
v-model="searchParams.firmwareType"
class="form-select"
@change="fetchData()"
>
<option value="">全部状态</option>
<option value="1">调参固件</option>
<option value="2">AP固件</option>
<option value="3">INAV固件</option>
</select>
</div>
</div>
<div class="d-flex gap-2">
<button class="btn btn-primary" @click="fetchData()">
<i class="bi bi-search me-1"></i> 搜索
</button>
<button class="btn btn-success" @click="resetSearch()">
<i class="bi bi-arrow-counterclockwise me-1"></i> 重置
</button>
</div>
</div>
<div class="mb-2">
<button class="btn btn-info" @click="openAddModal()">
<i class="bi bi-plus-circle me-1"></i> 新增
</button>
</div>
<div class="table-responsive">
<table class="table table-sm table-striped table-hover table-bordered ">
<thead class="table-light">
<tr>
<!-- 全选复选框 -->
<th style="width: 50px;">
<input
type="checkbox"
class="form-check-input"
v-model="selectAll"
@change="toggleSelectAll()"
>
</th>
<th>固件名</th>
<th>大小(字节)</th>
<th>描述</th>
<th>类型</th>
<th>创建时间</th>
<th style="width: 120px;">操作</th>
</tr>
</thead>
<tbody>
<!-- 加载中状态 -->
<tr v-if="loading">
<td colspan="8" class="p-0">
<div class="loading-container">
<div class="spinner-border text-primary" role="status" style="width: 2rem; height: 2rem;">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2 text-muted">加载中,请稍候...</p>
</div>
</td>
</tr>
<!-- 无数据状态 -->
<tr v-else-if="tableData.length === 0">
<td colspan="8" class="p-0">
<div class="no-data">
<i class="bi bi-folder-x"></i>
<h5>暂无匹配数据</h5>
<p class="text-muted">请尝试调整搜索条件或重置查询</p>
</div>
</td>
</tr>
<!-- 数据列表 -->
<tr v-else v-for="(item, index) in tableData" :key="item.id"
:class="{ selected: selectedIds.includes(item.id) }">
<td>
<input
type="checkbox"
class="form-check-input"
v-model="selectedIds"
:value="item.id"
@change="toggleSelectItem(item.id)"
>
</td>
<td>{{ item.firmwareName }}</td>
<td>{{ item.firmwareSize }}</td>
<td>{{ item.firmwareDescription }}</td>
<td>{{ item.firmwareTypeName }}</td>
<td>{{ formatTime(item.createTime) }}</td>
<td>
<div class="d-flex gap-1">
<button class="btn btn-sm btn-primary" @click="openUploadModal(item)" title="上传固件">
<i class="bi bi-cloud-upload-fill"></i>
</button>
<button class="btn btn-sm btn-primary" @click="openEditModal(item)">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-danger" @click="handleDelete(item.id)">
<i class="bi bi-trash"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="batch-actions mt-3">
<div class="d-flex align-items-center gap-2">
<span class="text-muted">已选中 {{ selectedIds.length }} 条数据</span>
<select
class="form-select"
v-model="batchAction"
:disabled="selectedIds.length === 0"
@change="handleBatchOperation"
style="width: auto; min-width: 160px;"
>
<option value="">-- 批量操作 --</option>
<option value="delete">批量删除</option>
</select>
<button
class="btn btn-secondary"
@click="clearSelected()"
:disabled="selectedIds.length === 0"
>
<i class="bi bi-x-circle me-1"></i> 取消选择
</button>
</div>
</div>
<div class="d-flex justify-content-between align-items-center mt-3">
<div class="page-info">
共 {{ total }} 条数据,当前第 {{ pageNum }}/{{ totalPages }} 页
</div>
<nav>
<ul class="pagination pagination-sm mb-0">
<li class="page-item" :class="{ disabled: pageNum === 1 }">
<a class="page-link" href="#" @click.prevent="changePage(1)">首页</a>
</li>
<li class="page-item" :class="{ disabled: pageNum === 1 }">
<a class="page-link" href="#" @click.prevent="changePage(pageNum - 1)">上一页</a>
</li>
<li class="page-item" :class="page === pageNum ? 'active' : ''" v-for="page in pageList"
:key="page">
<a class="page-link" href="#" @click.prevent="changePage(page)">{{ page }}</a>
</li>
<li class="page-item" :class="{ disabled: pageNum === totalPages }">
<a class="page-link" href="#" @click.prevent="changePage(pageNum + 1)">下一页</a>
</li>
<li class="page-item" :class="{ disabled: pageNum === totalPages }">
<a class="page-link" href="#" @click.prevent="changePage(totalPages)">末页</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<!-- 上传文件 -->
<div class="modal fade" id="uploadFileModal" tabindex="-1" aria-labelledby="uploadFileModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="uploadFileModalLabel">上传固件</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form>
<div class="mb-3 row">
<label class="col-sm-3 col-form-label">选择固件</label>
<div class="col-sm-9">
<input
type="file"
class="form-control"
id="firmwareFile"
name="file"
accept=".bin"
ref="firmwareFileRef"
>
<div class="form-text text-muted mt-1">支持 .bin 格式,单个文件不超过 100MB</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
<button type="button" class="btn btn-primary" @click.prevent="uploadFile">上传</button>
</div>
</div>
</div>
</div>
<!-- 新增 or 编辑 -->
<div class="modal fade" id="addOrEditModel" tabindex="-1" aria-labelledby="addOrEditModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="addOrEditModalLabel">{{ addOrEditTitle }}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form>
<div class="mb-3 row">
<label class="col-sm-3 col-form-label">固件名称</label>
<div class="col-sm-9">
<input type="text" class="form-control" v-model="addOrEditDto.firmwareName"
placeholder="请输入固件名称">
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-3 col-form-label">固件描述</label>
<div class="col-sm-9">
<input type="text" class="form-control" v-model="addOrEditDto.firmwareDescription"
placeholder="请输入固件描述">
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-3 col-form-label">固件类型</label>
<div class="col-sm-9">
<select class="form-control" v-model="addOrEditDto.firmwareType">
<option value=1>调参固件</option>
<option value=2>AP固件</option>
<option value=3>INAV固件</option>
</select>
</div>
</div>
<div class="mb-3 row" v-if="addOrEditDto.id != null">
<label class="col-sm-3 col-form-label">固件地址</label>
<div class="col-sm-9">
<input type="text" class="form-control" v-model="addOrEditDto.downloadUrl"
placeholder="请输入固件地址">
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
<button type="button" class="btn btn-primary" @click.prevent="saveEntity">保存</button>
</div>
</div>
</div>
</div>
<!-- 外部引入库文件 -->
<script src="https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios@1.4.0/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- 外部引入模拟请求文件 -->
<script src="/assets/js/axiosRequest.js"></script>
<script src="/assets/js/message.js"></script>
<script src="/assets/js/confirmModal.js"></script>
<!-- 页面核心逻辑 -->
<script>
const {createApp} = Vue;
createApp({
data() {
return {
tableData: [], // 表格数据源
loading: false, // 加载状态
searchParams: {
firmwareName: '',
firmwareType: '',
},
// 分页参数
pageNum: 1, // 当前页码
pageSize: 10, // 每页条数
total: 0, // 总数据量
totalPages: 0, // 总页数
// 批量操作
selectedIds: [], // 选中的ID集合
selectAll: false, // 全选状态
batchAction: '', // 批量操作选择
pageRange: 5,
modalInstances: {},
resetPasswordDto: {
userId: null,
nickName: null,
username: null,
password: null,
},
addOrEditTitle: '',
addOrEditDto: {
id: null,
firmwareName: null,
firmwareDescription: null,
firmwareType: 1,
downloadUrl: ''
}
}
},
computed: {
// 计算当前显示的页码列表
pageList() {
const list = [];
if (this.totalPages === 0) return list;
// 总页数小于等于显示范围,直接显示所有页码
if (this.totalPages <= this.pageRange) {
for (let i = 1; i <= this.totalPages; i++) {
list.push(i);
}
} else {
// 总页数大于显示范围显示当前页前后2个页码
let start = Math.max(1, this.pageNum - 2);
let end = Math.min(this.totalPages, this.pageNum + 2);
// 确保显示5个页码
if (end - start < this.pageRange - 1) {
if (start === 1) {
end = this.pageRange;
} else if (end === this.totalPages) {
start = this.totalPages - this.pageRange + 1;
}
}
for (let i = start; i <= end; i++) {
list.push(i);
}
}
return list;
}
},
methods: {
formatTime(time) {
if (!time) return '-';
const date = new Date(time);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
},
async fetchData() {
this.loading = true;
try {
const response = await request.get('/biz/firmware/page', {
...this.searchParams,
current: this.pageNum,
size: this.pageSize
});
console.log(response)
if (response.code === 200) {
this.tableData = response.data.records; // 列表数据
this.total = response.data.total; // 总条数
this.totalPages = response.data.pages; // 总页数
}
} catch (error) {
console.error('获取数据失败:', error);
this.tableData = [];
this.total = 0;
this.totalPages = 0;
} finally {
this.loading = false;
this.clearSelected();
}
},
// 切换页码
changePage(page) {
if (page < 1 || page > this.totalPages || page === this.pageNum) return;
this.pageNum = page;
this.fetchData();
},
// 重置搜索
resetSearch() {
this.searchParams = {
firmwareName: '',
firmwareType: '',
};
this.pageNum = 1;
this.fetchData();
},
// 全选/取消全选
toggleSelectAll() {
if (this.selectAll) {
this.selectedIds = this.tableData.map(item => item.id);
} else {
this.selectedIds = [];
}
},
// 单个选中/取消选中
toggleSelectItem(id) {
this.selectAll = this.selectedIds.length === this.tableData.length && this.tableData.length > 0;
},
// 清空选中状态
clearSelected() {
this.selectedIds = [];
this.selectAll = false;
this.batchAction = '';
},
// 处理批量操作
handleBatchOperation() {
if (!this.batchAction || this.selectedIds.length === 0) return;
switch (this.batchAction) {
case 'delete':
this.handleBatchDelete();
break;
}
this.batchAction = '';
},
// 批量删除
async handleBatchDelete() {
showConfirmModal({
title: '删除确认',
content: `确定要删除选中的 ${this.selectedIds.length} 条数据吗?`,
onConfirm: async () => {
try {
const response = await request.post('/biz/firmware/batchDelete', {ids: this.selectedIds});
if (response.code === 200) {
$message.success("删除成功");
this.fetchData();
}
} catch (error) {
console.error('删除失败:', error);
$message.error('删除失败,请重试!');
}
}
});
},
// 单个删除
async handleDelete(id) {
showConfirmModal({
title: '删除确认',
content: '确定要删除这条数据吗?',
onConfirm: async () => {
try {
const response = await request.delete('/biz/firmware/delete', {id});
if (response.code === 200) {
$message.success("删除成功");
this.fetchData(); // 重新加载数据
}
} catch (error) {
console.error('删除失败:', error);
$message.error('删除失败,请重试!');
}
}
});
},
// 打开文件上传模态框
openUploadModal(item) {
this.addOrEditDto = item;
this.modalInstances['uploadFileModal'].show();
},
async uploadFile() {
// 1. 获取选中的文件
const fileInput = this.$refs.firmwareFileRef;
const file = fileInput.files[0];
// 2. 校验文件(格式、大小)
if (!file) {
alert("请选择 .bin 格式的固件文件");
return;
}
if (file.type !== "application/octet-stream" && !file.name.endsWith(".bin")) {
alert("仅支持 .bin 格式文件");
return;
}
if (file.size > 100 * 1024 * 1024) { // 100MB
alert("文件大小不能超过 100MB");
return;
}
const formData = new FormData();
formData.append("file", file);
formData.append("id", this.addOrEditDto.id);
// 4. 发送上传请求Axios
try {
const response = await axios({
url: "/biz/firmware/uploadFile",
method: "POST",
data: formData,
headers: {
"Content-Type": "multipart/form-data"
},
onUploadProgress: (progressEvent) => {
const progress = (progressEvent.loaded / progressEvent.total) * 100;
console.log(`上传进度:${progress.toFixed(2)}%`);
}
});
if (response.data.success) {
$message.success('固件上传成功');
this.$refs.firmwareFileRef.value = "";
this.modalInstances['uploadFileModal'].hide();
this.fetchData();
} else {
$message.error("上传失败:" + response.data.message);
}
} catch (error) {
console.error("上传出错:", error);
$message.error("网络异常,上传失败");
}
},
// 打开新增模态框
openAddModal() {
this.clearForm();
this.addOrEditTitle = '新增';
this.modalInstances['addOrEditModel'].show();
},
// 打开编辑模态框
openEditModal(item) {
this.clearForm();
this.addOrEditTitle = '编辑';
this.addOrEditDto = item;
this.modalInstances['addOrEditModel'].show();
},
saveEntity() {
let url = (this.addOrEditDto === null || this.addOrEditDto.id === null) ? '/biz/firmware/save' : '/biz/firmware/update';
request.post(url, this.addOrEditDto)
.then((res) => {
if (res.code === 200) {
$message.success('保存成功!', 500);
this.fetchData();
this.modalInstances['addOrEditModel'].hide();
} else {
$message.error('保存失败!', 500);
}
this.clearForm();
}).catch(() => {
})
},
// 清空表单数据
clearForm() {
this.addOrEditDto = {
id: null,
firmwareName: null,
firmwareDescription: null,
firmwareType: 1,
downloadUrl: ''
};
}
},
mounted() {
this.fetchData();
const modalIds = ['uploadFileModal', 'addOrEditModel'];
modalIds.forEach(id => {
const modalElement = document.getElementById(id);
if (modalElement) {
this.modalInstances[id] = new bootstrap.Modal(modalElement, {
backdrop: 'static',
keyboard: true
});
}
});
}
}).mount('#app');
</script>
</body>
</html>

View File

@@ -1,581 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>隐私政策</title>
<!-- 外部引入库文件 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<!-- 外部引入自定义样式 -->
<link rel="stylesheet" href="/assets/css/table.css">
</head>
<body id="app">
<div class="main-container">
<div class="table-container">
<h4 class="mb-3">隐私政策</h4>
<div class="search-bar">
<div class="search-item">
<div class="input-group">
<span class="input-group-text">
名称
</span>
<input
v-model="searchParams.title"
type="text"
class="form-control"
placeholder="请输入名称"
@keyup.enter="fetchData()"
>
</div>
</div>
<div class="search-item">
<div class="input-group">
<span class="input-group-text">
是否显示
</span>
<select
v-model="searchParams.visible"
class="form-select"
@change="fetchData()"
>
<option value="">全部状态</option>
<option value="1">显示</option>
<option value="2">隐藏</option>
</select>
</div>
</div>
<div class="d-flex gap-2">
<button class="btn btn-primary" @click="fetchData()">
<i class="bi bi-search me-1"></i> 搜索
</button>
<button class="btn btn-success" @click="resetSearch()">
<i class="bi bi-arrow-counterclockwise me-1"></i> 重置
</button>
</div>
</div>
<div class="mb-2">
<button class="btn btn-info" @click="openAddModal()">
<i class="bi bi-plus-circle me-1"></i> 新增
</button>
</div>
<div class="table-responsive">
<table class="table table-sm table-striped table-hover table-bordered ">
<thead class="table-light">
<tr>
<!-- 全选复选框 -->
<th style="width: 50px;">
<input
type="checkbox"
class="form-check-input"
v-model="selectAll"
@change="toggleSelectAll()"
>
</th>
<th>名称</th>
<th>是否显示</th>
<th>排序</th>
<th>创建时间</th>
<th style="width: 120px;">操作</th>
</tr>
</thead>
<tbody>
<!-- 加载中状态 -->
<tr v-if="loading">
<td colspan="8" class="p-0">
<div class="loading-container">
<div class="spinner-border text-primary" role="status" style="width: 2rem; height: 2rem;">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2 text-muted">加载中,请稍候...</p>
</div>
</td>
</tr>
<!-- 无数据状态 -->
<tr v-else-if="tableData.length === 0">
<td colspan="8" class="p-0">
<div class="no-data">
<i class="bi bi-folder-x"></i>
<h5>暂无匹配数据</h5>
<p class="text-muted">请尝试调整搜索条件或重置查询</p>
</div>
</td>
</tr>
<!-- 数据列表 -->
<tr v-else v-for="(item, index) in tableData" :key="item.id"
:class="{ selected: selectedIds.includes(item.id) }">
<td>
<input
type="checkbox"
class="form-check-input"
v-model="selectedIds"
:value="item.id"
@change="toggleSelectItem(item.id)"
>
</td>
<td>{{ item.title }}</td>
<td>
<span class="badge" :class="item.visible === 1 ? 'bg-success' : 'bg-danger'">
{{ item.visible === 1 ? '启用' : '禁用' }}
</span>
</td>
<td>{{ item.sort }}</td>
<td>{{ formatTime(item.createTime) }}</td>
<td>
<div class="d-flex gap-1">
<button class="btn btn-sm btn-primary" @click="openEditModal(item)">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-danger" @click="handleDelete(item.id)">
<i class="bi bi-trash"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="batch-actions mt-3">
<div class="d-flex align-items-center gap-2">
<span class="text-muted">已选中 {{ selectedIds.length }} 条数据</span>
<select
class="form-select"
v-model="batchAction"
:disabled="selectedIds.length === 0"
@change="handleBatchOperation"
style="width: auto; min-width: 160px;"
>
<option value="">-- 批量操作 --</option>
<option value="delete">批量删除</option>
</select>
<button
class="btn btn-secondary"
@click="clearSelected()"
:disabled="selectedIds.length === 0"
>
<i class="bi bi-x-circle me-1"></i> 取消选择
</button>
</div>
</div>
<div class="d-flex justify-content-between align-items-center mt-3">
<div class="page-info">
共 {{ total }} 条数据,当前第 {{ pageNum }}/{{ totalPages }} 页
</div>
<nav>
<ul class="pagination pagination-sm mb-0">
<li class="page-item" :class="{ disabled: pageNum === 1 }">
<a class="page-link" href="#" @click.prevent="changePage(1)">首页</a>
</li>
<li class="page-item" :class="{ disabled: pageNum === 1 }">
<a class="page-link" href="#" @click.prevent="changePage(pageNum - 1)">上一页</a>
</li>
<li class="page-item" :class="page === pageNum ? 'active' : ''" v-for="page in pageList"
:key="page">
<a class="page-link" href="#" @click.prevent="changePage(page)">{{ page }}</a>
</li>
<li class="page-item" :class="{ disabled: pageNum === totalPages }">
<a class="page-link" href="#" @click.prevent="changePage(pageNum + 1)">下一页</a>
</li>
<li class="page-item" :class="{ disabled: pageNum === totalPages }">
<a class="page-link" href="#" @click.prevent="changePage(totalPages)">末页</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<!-- 新增 or 编辑 -->
<div class="modal fade" id="addOrEditModel" tabindex="-1" aria-labelledby="addOrEditModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="addOrEditModalLabel">{{ addOrEditTitle }}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form>
<div class="mb-3 row">
<label class="col-sm-3 col-form-label">名称</label>
<div class="col-sm-9">
<input type="text" class="form-control" v-model="addOrEditDto.title"
placeholder="请输入名称">
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-3 col-form-label">内容</label>
<div class="col-sm-9">
<!-- 富文本编辑器容器 -->
<div id="contentEditor" style="border: 1px solid #dee2e6; border-radius: 0.375rem;"></div>
<!-- 错误提示 -->
<div class="invalid-feedback" id="contentError" style="display: none; margin-top: 0.25rem;"></div>
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-3 col-form-label">是否显示</label>
<div class="col-sm-9">
<select class="form-control" v-model="addOrEditDto.visible">
<option value=1>显示</option>
<option value=2>隐藏</option>
</select>
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-3 col-form-label">序号</label>
<div class="col-sm-9">
<input type="number" class="form-control" v-model="addOrEditDto.sort"
placeholder="请输入序号">
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
<button type="button" class="btn btn-primary" @click.prevent="saveEntity">保存</button>
</div>
</div>
</div>
</div>
<!-- 外部引入库文件 -->
<script src="https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios@1.4.0/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://unpkg.com/wangeditor@4.7.15/dist/wangEditor.min.js"></script>
<!-- 外部引入模拟请求文件 -->
<script src="/assets/js/axiosRequest.js"></script>
<script src="/assets/js/message.js"></script>
<script src="/assets/js/confirmModal.js"></script>
<!-- 页面核心逻辑 -->
<script>
const {createApp} = Vue;
createApp({
data() {
return {
tableData: [], // 表格数据源
loading: false, // 加载状态
searchParams: {
title: '',
visible: '',
},
// 分页参数
pageNum: 1, // 当前页码
pageSize: 10, // 每页条数
total: 0, // 总数据量
totalPages: 0, // 总页数
// 批量操作
selectedIds: [], // 选中的ID集合
selectAll: false, // 全选状态
batchAction: '', // 批量操作选择
pageRange: 5,
modalInstances: {},
resetPasswordDto: {
userId: null,
nickName: null,
username: null,
password: null,
},
addOrEditTitle: '',
addOrEditDto: {
id: null,
title: null,
content: null,
visible: 1,
sort: 99
},
editor: null, // WangEditor 实例
}
},
computed: {
// 计算当前显示的页码列表
pageList() {
const list = [];
if (this.totalPages === 0) return list;
// 总页数小于等于显示范围,直接显示所有页码
if (this.totalPages <= this.pageRange) {
for (let i = 1; i <= this.totalPages; i++) {
list.push(i);
}
} else {
// 总页数大于显示范围显示当前页前后2个页码
let start = Math.max(1, this.pageNum - 2);
let end = Math.min(this.totalPages, this.pageNum + 2);
// 确保显示5个页码
if (end - start < this.pageRange - 1) {
if (start === 1) {
end = this.pageRange;
} else if (end === this.totalPages) {
start = this.totalPages - this.pageRange + 1;
}
}
for (let i = start; i <= end; i++) {
list.push(i);
}
}
return list;
}
},
methods: {
formatTime(time) {
if (!time) return '-';
const date = new Date(time);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
},
async fetchData() {
this.loading = true;
try {
const response = await request.get('/biz/privacy_policy/page', {
...this.searchParams,
current: this.pageNum,
size: this.pageSize
});
console.log(response)
if (response.code === 200) {
this.tableData = response.data.records; // 列表数据
this.total = response.data.total; // 总条数
this.totalPages = response.data.pages; // 总页数
}
} catch (error) {
console.error('获取数据失败:', error);
this.tableData = [];
this.total = 0;
this.totalPages = 0;
} finally {
this.loading = false;
this.clearSelected();
}
},
// 切换页码
changePage(page) {
if (page < 1 || page > this.totalPages || page === this.pageNum) return;
this.pageNum = page;
this.fetchData();
},
// 重置搜索
resetSearch() {
this.searchParams = {
title: '',
visible: '',
};
this.pageNum = 1;
this.fetchData();
},
// 全选/取消全选
toggleSelectAll() {
if (this.selectAll) {
this.selectedIds = this.tableData.map(item => item.id);
} else {
this.selectedIds = [];
}
},
// 单个选中/取消选中
toggleSelectItem(id) {
this.selectAll = this.selectedIds.length === this.tableData.length && this.tableData.length > 0;
},
// 清空选中状态
clearSelected() {
this.selectedIds = [];
this.selectAll = false;
this.batchAction = '';
},
// 处理批量操作
handleBatchOperation() {
if (!this.batchAction || this.selectedIds.length === 0) return;
switch (this.batchAction) {
case 'delete':
this.handleBatchDelete();
break;
}
this.batchAction = '';
},
// 批量删除
async handleBatchDelete() {
showConfirmModal({
title: '删除确认',
content: `确定要删除选中的 ${this.selectedIds.length} 条数据吗?`,
onConfirm: async () => {
try {
const response = await request.post('/biz/privacy_policy/batchDelete', {ids: this.selectedIds});
if (response.code === 200) {
$message.success("删除成功");
this.fetchData();
}
} catch (error) {
console.error('删除失败:', error);
$message.error('删除失败,请重试!');
}
}
});
},
// 单个删除
async handleDelete(id) {
showConfirmModal({
title: '删除确认',
content: '确定要删除这条数据吗?',
onConfirm: async () => {
try {
const response = await request.delete('/biz/privacy_policy/delete', {id});
if (response.code === 200) {
$message.success("删除成功");
this.fetchData(); // 重新加载数据
}
} catch (error) {
console.error('删除失败:', error);
$message.error('删除失败,请重试!');
}
}
});
},
// 打开新增模态框
openAddModal() {
this.addOrEditTitle = '新增';
this.clearForm();
this.modalInstances['addOrEditModel'].show();
},
// 打开编辑模态框
openEditModal(item) {
this.addOrEditTitle = '编辑';
this.addOrEditDto = {...item};
// 延迟回显富文本内容
setTimeout(() => {
if (this.editor) this.editor.txt.html(item.content || '');
}, 300);
this.modalInstances['addOrEditModel'].show();
},
saveEntity() {
let url = (this.addOrEditDto === null || this.addOrEditDto.id === null) ? '/biz/privacy_policy/save' : '/biz/privacy_policy/update';
request.post(url, this.addOrEditDto)
.then((res) => {
if (res.code === 200) {
$message.success('保存成功!', 500);
this.fetchData();
this.modalInstances['addOrEditModel'].hide();
} else {
$message.error('保存失败!', 500);
}
this.clearForm();
}).catch(() => {
})
},
// 清空表单数据
clearForm() {
this.addOrEditDto = {
id: null,
title: null,
content: null,
visible: 1,
sort: 99
};
},
// 初始化 WangEditor
initWangEditor() {
const _this = this;
// 获取编辑器容器
const editorDom = document.getElementById('contentEditor');
// 创建编辑器实例
this.editor = new wangEditor(editorDom);
// 编辑器配置
this.editor.config.height = 200; // 高度
this.editor.config.zIndex = 1050; // 层级(确保在模态框上方)
this.editor.config.uploadImgShowBase64 = true; // 图片以 Base64 格式保存(无需后端接口)
this.editor.config.menus = [
'head', // 标题
'bold', // 粗体
'italic', // 斜体
'underline', // 下划线
'strikeThrough', // 删除线
'foreColor', // 文字颜色
'backColor', // 背景颜色
'link', // 插入链接
'list', // 列表
'justify', // 对齐方式
'image', // 插入图片
'table', // 表格
'code', // 插入代码
'undo', // 撤销
'redo' // 重做
];
// 内容变化时同步到 Vue 数据
this.editor.config.onchange = function (html) {
_this.addOrEditDto.content = html;
};
// 创建编辑器
this.editor.create();
},
},
mounted() {
this.fetchData();
const modalIds = ['uploadFileModal', 'addOrEditModel'];
modalIds.forEach(id => {
const modalElement = document.getElementById(id);
if (modalElement) {
this.modalInstances[id] = new bootstrap.Modal(modalElement, {
backdrop: 'static',
keyboard: true
});
}
});
// 初始化 WangEditor延迟初始化确保DOM加载完成
setTimeout(() => {
this.initWangEditor();
}, 500);
// 监听模态框关闭事件,避免内存泄漏
const modal = document.getElementById('addOrEditModel');
modal.addEventListener('hidden.bs.modal', () => {
if (this.editor) {
this.editor.txt.clear(); // 清空内容
document.getElementById('contentEditor').style.borderColor = '#dee2e6';
document.getElementById('contentError').style.display = 'none';
}
});
}
}).mount('#app');
</script>
</body>
</html>

View File

@@ -1,931 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>教程管理</title>
<!-- 外部引入库文件 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<!-- 谷歌 Material Icons 库 -->
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<!-- 引入Pickr颜色选择库样式 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@simonwep/pickr/dist/themes/nano.min.css">
<!-- 外部引入自定义样式 -->
<link rel="stylesheet" href="/assets/css/table.css">
<!-- 颜色选择器适配样式 -->
<style>
.pickr-container {
width: 100%;
}
.pickr .pcr-app {
margin-top: 5px;
z-index: 1060;
}
.pickr .pcr-button {
width: 100%;
}
.pickr .pcr-interaction input.pcr-input {
display: none;
}
.ellipsis-single {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 200px;
}
</style>
</head>
<body id="app">
<div class="main-container row">
<div class="col-sm-4">
<div class="table-container">
<h4 class="mb-3">分类管理</h4>
<div class="search-bar">
<div class="search-item">
<div class="input-group">
<span class="input-group-text">
名称
</span>
<input
v-model="categorySearchParams.categoryTitle"
type="text"
class="form-control"
placeholder="请输入名称"
@keyup.enter="fetchCategoryData()"
>
</div>
</div>
<div class="d-flex gap-2">
<button class="btn btn-primary" @click="fetchCategoryData()">
<i class="bi bi-search me-1"></i> 搜索
</button>
<button class="btn btn-success" @click="categoryResetSearch()">
<i class="bi bi-arrow-counterclockwise me-1"></i> 重置
</button>
</div>
</div>
<div class="mb-2">
<button class="btn btn-info" @click="categoryOpenAddModal()">
<i class="bi bi-plus-circle me-1"></i> 新增
</button>
</div>
<div class="table-responsive">
<table class="table table-sm table-striped table-hover table-bordered ">
<thead class="table-light">
<tr>
<th>名称</th>
<th>颜色</th>
<th>置首</th>
<th style="width: 120px;">操作</th>
</tr>
</thead>
<tbody>
<!-- 加载中状态 -->
<tr v-if="categoryLoading">
<td colspan="8" class="p-0">
<div class="loading-container">
<div class="spinner-border text-primary" role="status"
style="width: 2rem; height: 2rem;">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2 text-muted">加载中,请稍候...</p>
</div>
</td>
</tr>
<!-- 无数据状态 -->
<tr v-else-if="categoryTableData.length === 0">
<td colspan="8" class="p-0">
<div class="no-data">
<i class="bi bi-folder-x"></i>
<h5>暂无匹配数据</h5>
<p class="text-muted">请尝试调整搜索条件或重置查询</p>
</div>
</td>
</tr>
<!-- 数据列表 -->
<tr v-else v-for="(item, index) in categoryTableData" :key="item.id">
<td>{{ item.categoryTitle }}</td>
<td>
<div
style="width: 40px; height: 30px; border: 1px solid #ddd; border-radius: 4px;"
:style="{ backgroundColor: item.color || '#ffffff' }"
></div>
</td>
<td>
<span class="badge" :class="item.firstStatus === 1 ? 'bg-success' : 'bg-info'">
{{ item.firstStatus === 1 ? '置首' : '不置首' }}
</span>
</td>
<td>
<div class="d-flex gap-1">
<button class="btn btn-sm btn-primary" @click="categoryQueryData(item)">
<i class="bi bi-box-arrow-right"></i>
</button>
<button class="btn btn-sm btn-primary" @click="categoryOpenEditModal(item)">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-danger" @click="categoryHandleDelete(item.id)">
<i class="bi bi-trash"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="d-flex justify-content-between align-items-center mt-3">
<div class="page-info">
</div>
<nav>
<ul class="pagination pagination-sm mb-0">
<li class="page-item" :class="{ disabled: categoryPageNum === 1 }">
<a class="page-link" href="#" @click.prevent="changePage(categoryPageNum - 1)">上一页</a>
</li>
<li class="page-item" :class="page === categoryPageNum ? 'active' : ''"
v-for="page in getPageList(categoryPageNum, categoryTotalPages, categoryPageRange)"
:key="page">
<a class="page-link" href="#" @click.prevent="categoryChangePage(page)">{{ page }}</a>
</li>
<li class="page-item" :class="{ disabled: categoryPageNum === totalPages }">
<a class="page-link" href="#" @click.prevent="changePage(categoryPageNum + 1)">下一页</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<div class="col-sm-8">
<div class="table-container">
<h4 class="mb-3">教程管理</h4>
<div class="search-bar">
<div class="search-item">
<div class="input-group">
<span class="input-group-text">
名称
</span>
<input
v-model="searchParams.tutorialTitle"
type="text"
class="form-control"
placeholder="请输入名称"
@keyup.enter="fetchData()"
>
</div>
</div>
<div class="search-item">
<div class="input-group">
<span class="input-group-text">
类型
</span>
<select
v-model="searchParams.status"
class="form-select"
@change="fetchData()"
>
<option value="">全部状态</option>
<option value="1">启用</option>
<option value="2">关闭</option>
</select>
</div>
</div>
<div class="d-flex gap-2">
<button class="btn btn-primary" @click="fetchData()">
<i class="bi bi-search me-1"></i> 搜索
</button>
<button class="btn btn-success" @click="resetSearch()">
<i class="bi bi-arrow-counterclockwise me-1"></i> 重置
</button>
</div>
</div>
<div class="mb-2">
<button class="btn btn-info" @click="openAddModal()">
<i class="bi bi-plus-circle me-1"></i> 新增
</button>
</div>
<div class="table-responsive">
<table class="table table-sm table-striped table-hover table-bordered ">
<thead class="table-light">
<tr>
<th>名称</th>
<th>描述</th>
<th>推荐</th>
<th>状态</th>
<th>查看次数</th>
<th style="width: 120px;">操作</th>
</tr>
</thead>
<tbody>
<!-- 加载中状态 -->
<tr v-if="loading">
<td colspan="8" class="p-0">
<div class="loading-container">
<div class="spinner-border text-primary" role="status"
style="width: 2rem; height: 2rem;">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2 text-muted">加载中,请稍候...</p>
</div>
</td>
</tr>
<!-- 无数据状态 -->
<tr v-else-if="tableData.length === 0">
<td colspan="8" class="p-0">
<div class="no-data">
<i class="bi bi-folder-x"></i>
<h5>暂无匹配数据</h5>
<p class="text-muted">请尝试调整搜索条件或重置查询</p>
</div>
</td>
</tr>
<!-- 数据列表 -->
<tr v-else v-for="(item, index) in tableData" :key="item.id">
<td>{{ item.tutorialTitle }}</td>
<td class="ellipsis-single">{{ item.description }}</td>
<td>
<span class="badge" :class="item.recommendStatus === 1 ? 'bg-success' : 'bg-info'">
{{ item.recommendStatus === 1 ? '推荐' : '不推荐' }}
</span>
</td>
<td>
<span class="badge" :class="item.status === 1 ? 'bg-success' : 'bg-danger'">
{{ item.status === 1 ? '启用' : '禁用' }}
</span>
</td>
<td>{{ item.viewCount }}</td>
<td>
<div class="d-flex gap-1">
<button class="btn btn-sm btn-primary" @click="openEditModal(item)">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-danger" @click="handleDelete(item.id)">
<i class="bi bi-trash"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="d-flex justify-content-between align-items-center mt-3">
<div class="page-info">
共 {{ total }} 条数据,当前第 {{ pageNum }}/{{ totalPages }} 页
</div>
<nav>
<ul class="pagination pagination-sm mb-0">
<li class="page-item" :class="{ disabled: pageNum === 1 }">
<a class="page-link" href="#" @click.prevent="changePage(1)">首页</a>
</li>
<li class="page-item" :class="{ disabled: pageNum === 1 }">
<a class="page-link" href="#" @click.prevent="changePage(pageNum - 1)">上一页</a>
</li>
<li class="page-item" :class="page === pageNum ? 'active' : ''"
v-for="page in getPageList(pageNum, totalPages, pageRange)"
:key="page">
<a class="page-link" href="#" @click.prevent="changePage(page)">{{ page }}</a>
</li>
<li class="page-item" :class="{ disabled: pageNum === totalPages }">
<a class="page-link" href="#" @click.prevent="changePage(pageNum + 1)">下一页</a>
</li>
<li class="page-item" :class="{ disabled: pageNum === totalPages }">
<a class="page-link" href="#" @click.prevent="changePage(totalPages)">末页</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
</div>
<!-- 分类 新增 or 编辑 -->
<div class="modal fade" id="addOrEditCategoryModel" tabindex="-1" aria-labelledby="addOrEditCategoryModalLabel"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="addOrEditCategoryModalLabel">{{ categoryAddOrEditTitle }}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form>
<div class="mb-3 row">
<label class="col-sm-3 col-form-label">图标</label>
<div class="col-sm-9">
<input type="text" class="form-control" v-model="categoryAddOrEditDto.icon"
placeholder="请输入图标">
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-3 col-form-label">颜色</label>
<div class="col-sm-9">
<div id="colorPickerContainer" style="border: 1px #dee2e6 solid"></div>
<input
type="hidden"
v-model="categoryAddOrEditDto.color"
id="categoryColorValue"
>
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-3 col-form-label">分类名称</label>
<div class="col-sm-9">
<input type="text" class="form-control" v-model="categoryAddOrEditDto.categoryTitle"
placeholder="请输入分类名称">
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-3 col-form-label">描述</label>
<div class="col-sm-9">
<textarea class="form-control" v-model="categoryAddOrEditDto.description"
placeholder="请输入描述"></textarea>
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-3 col-form-label">是否置首</label>
<div class="col-sm-9">
<select class="form-control" v-model="categoryAddOrEditDto.firstStatus">
<option value=0>不置首</option>
<option value=1>置首</option>
</select>
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-3 col-form-label">状态</label>
<div class="col-sm-9">
<select class="form-control" v-model="categoryAddOrEditDto.status">
<option value=1>启用</option>
<option value=2>关闭</option>
</select>
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-3 col-form-label">语言</label>
<div class="col-sm-9">
<select class="form-control" v-model="categoryAddOrEditDto.lang">
<option value='zh'>中文</option>
<option value='en'>英文</option>
</select>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
<button type="button" class="btn btn-primary" @click.prevent="categorySaveEntity">保存</button>
</div>
</div>
</div>
</div>
<!-- 新增 or 编辑 -->
<div class="modal fade" id="addOrEditModel" tabindex="-1" aria-labelledby="addOrEditModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="addOrEditModalLabel">{{ addOrEditTitle }}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form>
<div class="mb-3 row">
<label class="col-sm-2 col-form-label">教程分类</label>
<div class="col-sm-10">
<select class="form-control" v-model="addOrEditDto.categoryId">
<option v-for="(item, index) in categoryTableData" :value="item.id">{{item.categoryTitle}}</option>
</select>
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-2 col-form-label">教程名称</label>
<div class="col-sm-10">
<input type="text" class="form-control" v-model="addOrEditDto.tutorialTitle"
placeholder="请输入教程名称">
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-2 col-form-label">教程描述</label>
<div class="col-sm-10">
<input type="text" class="form-control" v-model="addOrEditDto.description"
placeholder="请输入教程描述">
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-2 col-form-label">教程详情</label>
<div class="col-sm-10">
<!-- 富文本编辑器容器 -->
<div id="tutorialContentEditor" style="border: 1px solid #dee2e6; border-radius: 0.375rem;"></div>
<!-- 错误提示 -->
<div class="invalid-feedback" id="contentError" style="display: none; margin-top: 0.25rem;"></div>
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-2 col-form-label">知识库</label>
<div class="col-sm-10">
<input type="text" class="form-control" v-model="addOrEditDto.knowledgeUrl"
placeholder="请输入知识库地址(优先级最高)">
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-2 col-form-label">是否推荐</label>
<div class="col-sm-10">
<select class="form-control" v-model="addOrEditDto.recommendStatus">
<option value=0>不推荐</option>
<option value=1>推荐</option>
</select>
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-2 col-form-label">状态</label>
<div class="col-sm-10">
<select class="form-control" v-model="addOrEditDto.status">
<option value=1>启用</option>
<option value=2>关闭</option>
</select>
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-2 col-form-label">语言</label>
<div class="col-sm-10">
<select class="form-control" v-model="addOrEditDto.lang">
<option value='zh'>中文</option>
<option value='en'>英文</option>
</select>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
<button type="button" class="btn btn-primary" @click.prevent="saveEntity">保存</button>
</div>
</div>
</div>
</div>
<!-- 外部引入库文件 -->
<script src="https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios@1.4.0/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- 引入Pickr颜色选择库核心JS -->
<script src="https://cdn.jsdelivr.net/npm/@simonwep/pickr/dist/pickr.min.js"></script>
<script src="https://unpkg.com/wangeditor@4.7.15/dist/wangEditor.min.js"></script>
<!-- 外部引入模拟请求文件 -->
<script src="/assets/js/axiosRequest.js"></script>
<script src="/assets/js/message.js"></script>
<script src="/assets/js/confirmModal.js"></script>
<!-- 页面核心逻辑 -->
<script>
const {createApp} = Vue;
createApp({
data() {
return {
tableData: [], // 表格数据源
loading: false, // 加载状态
searchParams: {
tutorialTitle: '',
status: '',
},
// 分页参数
pageNum: 1, // 当前页码
pageSize: 10, // 每页条数
total: 0, // 总数据量
totalPages: 0, // 总页数
pageRange: 5,
modalInstances: {},
editor: null, // WangEditor 实例
addOrEditTitle: '',
addOrEditDto: {
id: null,
tutorialTitle: null,
description: null,
content: null,
status: 1,
recommendStatus: 1,
lang: 'zh',
categoryId: 0,
knowledgeUrl: null,
},
// 教程分类
categoryTableData: [], // 表格数据源
categoryLoading: false, // 加载状态
categorySearchParams: {
categoryTitle: '',
status: '',
},
// 分页参数
categoryPageNum: 1, // 当前页码
categoryPageSize: 10, // 每页条数
categoryTotal: 0, // 总数据量
categoryTotalPages: 0, // 总页数
categoryPageRange: 5,
categoryAddOrEditTitle: '',
categoryAddOrEditDto: {
id: null,
icon: null,
color: '#ffffff', // 默认白色
categoryTitle: null,
description: null,
firstStatus: 0,
status: 1,
lang: 'zh',
},
}
},
methods: {
getPageList(currentPage, totalPages, pageRange) {
const list = [];
if (totalPages === 0) return list;
if (totalPages <= pageRange) {
for (let i = 1; i <= totalPages; i++) {
list.push(i);
}
} else {
let start = Math.max(1, currentPage - 2);
let end = Math.min(totalPages, currentPage + 2);
if (end - start < pageRange - 1) {
if (start === 1) {
end = pageRange;
} else if (end === totalPages) {
start = totalPages - pageRange + 1;
}
}
for (let i = start; i <= end; i++) {
list.push(i);
}
}
return list;
},
// 教程分类
async fetchCategoryData() {
this.categoryLoading = true;
try {
const response = await request.get('/biz/tutorial/category/page', {
...this.categorySearchParams,
current: this.categoryPageNum,
size: this.categoryPageSize
});
if (response.code === 200) {
this.categoryTableData = response.data.records;
this.categoryTotal = response.data.total;
this.categoryTotalPages = response.data.pages;
}
} catch (error) {
this.categoryTableData = [];
this.categoryTotal = 0;
this.categoryTotalPages = 0;
} finally {
this.categoryLoading = false;
}
},
// 切换页码
categoryChangePage(page) {
if (page < 1 || page > this.categoryTotalPages || page === this.categoryPageNum) return;
this.categoryPageNum = page;
this.fetchCategoryData();
},
// 重置搜索
categoryResetSearch() {
this.categorySearchParams = {
categoryTitle: '',
};
this.categoryPageNum = 1;
this.fetchCategoryData();
},
// 单个删除
async categoryHandleDelete(id) {
showConfirmModal({
title: '删除确认',
content: '确定要删除这条数据吗?',
onConfirm: async () => {
try {
const response = await request.delete('/biz/tutorial/category/delete', {id});
if (response.code === 200) {
$message.success("删除成功");
this.fetchCategoryData();
}
} catch (error) {
$message.error('删除失败,请重试!');
}
}
});
},
categoryQueryData(item) {
this.searchParams.categoryId = item.id;
this.fetchData();
},
// 打开新增模态框
categoryOpenAddModal() {
this.categoryAddOrEditTitle = '新增分类';
this.categoryClearForm(); // 重置表单默认值
this.modalInstances['addOrEditCategoryModel'].show();
},
// 打开编辑模态框
categoryOpenEditModal(item) {
this.categoryAddOrEditTitle = '编辑分类';
this.categoryAddOrEditDto = {...item}; // 深拷贝避免引用问题
this.modalInstances['addOrEditCategoryModel'].show();
},
categorySaveEntity() {
let url = (this.categoryAddOrEditDto.id === null) ? '/biz/tutorial/category/save' : '/biz/tutorial/category/update';
request.post(url, this.categoryAddOrEditDto)
.then((res) => {
if (res.code === 200) {
$message.success('保存成功!', 500);
this.fetchCategoryData();
this.modalInstances['addOrEditCategoryModel'].hide();
} else {
$message.error('保存失败!', 500);
}
}).catch(() => {
$message.error('网络错误,请重试!');
})
},
// 清空表单数据
categoryClearForm() {
this.categoryAddOrEditDto = {
id: null,
icon: null,
color: '#ffffff',
categoryTitle: null,
description: '',
type: 'category',
firstStatus: 0,
status: 1,
lang: 'zh',
};
},
// =========================== 教程 =========================
async fetchData() {
this.loading = true;
try {
const response = await request.get('/biz/tutorial/page', {
...this.searchParams,
current: this.pageNum,
size: this.pageSize
});
if (response.code === 200) {
this.tableData = response.data.records;
this.total = response.data.total;
this.totalPages = response.data.pages;
}
} catch (error) {
console.error('获取数据失败:', error);
this.tableData = [];
this.total = 0;
this.totalPages = 0;
} finally {
this.loading = false;
}
},
// 切换页码
changePage(page) {
if (page < 1 || page > this.totalPages || page === this.pageNum) return;
this.pageNum = page;
this.fetchData();
},
// 重置搜索
resetSearch() {
this.searchParams = {
tutorialTitle: '',
status: '',
};
this.pageNum = 1;
this.fetchData();
},
// 单个删除
async handleDelete(id) {
showConfirmModal({
title: '删除确认',
content: '确定要删除这条数据吗?',
onConfirm: async () => {
try {
const response = await request.delete('/biz/tutorial/delete', {id});
if (response.code === 200) {
$message.success("删除成功");
this.fetchData();
}
} catch (error) {
console.error('删除失败:', error);
$message.error('删除失败,请重试!');
}
}
});
},
// 打开新增模态框
openAddModal() {
this.addOrEditTitle = '新增';
this.clearForm();
this.addOrEditDto.categoryId = this.categoryTableData[0]?.id;
this.modalInstances['addOrEditModel'].show();
},
// 打开编辑模态框
openEditModal(item) {
this.addOrEditTitle = '编辑';
this.addOrEditDto = {...item};
// 延迟回显富文本内容
setTimeout(() => {
if (this.editor) this.editor.txt.html(item.content || '');
}, 300);
this.modalInstances['addOrEditModel'].show();
},
saveEntity() {
let url = (this.addOrEditDto.id === null) ? '/biz/tutorial/save' : '/biz/tutorial/update';
request.post(url, this.addOrEditDto)
.then((res) => {
if (res.code === 200) {
$message.success('保存成功!', 500);
this.fetchData();
this.modalInstances['addOrEditModel'].hide();
} else {
$message.error('保存失败!', 500);
}
this.clearForm();
}).catch(() => {
$message.error('网络错误,请重试!');
})
},
// 清空表单数据
clearForm() {
this.addOrEditDto = {
id: null,
tutorialTitle: null,
description: null,
content: '',
status: 1,
recommendStatus: 1,
lang: 'zh',
};
},
createColorPicker() {
const container = document.getElementById('colorPickerContainer');
container.innerHTML = '<div id="categoryColorPicker" class="pickr-container"></div>';
// 创建新实例
return Pickr.create({
el: '#categoryColorPicker',
theme: 'nano',
default: this.categoryAddOrEditDto.color || '#dee2e6',
swatches: [
'#000000', '#ffffff', '#ff0000', '#00ff00', '#0000ff',
'#ffff00', '#ff7f50', '#98fb98', '#87ceeb', '#dda0dd'
],
components: {
preview: true,
opacity: false,
hue: true,
interaction: {
hex: true,
rgba: false,
hsla: false,
hsva: false,
cmyk: false,
input: true,
clear: true,
save: true
}
}
});
},
// 初始化 WangEditor
initWangEditor() {
const _this = this;
// 获取编辑器容器
const editorDom = document.getElementById('tutorialContentEditor');
// 创建编辑器实例
this.editor = new wangEditor(editorDom);
// 编辑器配置
this.editor.config.height = 200; // 高度
this.editor.config.zIndex = 1050; // 层级(确保在模态框上方)
this.editor.config.uploadImgShowBase64 = true; // 图片以 Base64 格式保存(无需后端接口)
this.editor.config.menus = [
'head', // 标题
'bold', // 粗体
'italic', // 斜体
'underline', // 下划线
'strikeThrough', // 删除线
'foreColor', // 文字颜色
'backColor', // 背景颜色
'link', // 插入链接
'list', // 列表
'justify', // 对齐方式
'image', // 插入图片
'table', // 表格
'code', // 插入代码
'undo', // 撤销
'redo' // 重做
];
// 内容变化时同步到 Vue 数据
this.editor.config.onchange = function (html) {
_this.addOrEditDto.content = html;
};
// 创建编辑器
this.editor.create();
},
},
mounted() {
this.fetchData();
this.fetchCategoryData();
// 初始化模态框
const modalIds = ['addOrEditModel', 'addOrEditCategoryModel'];
modalIds.forEach(id => {
const modalElement = document.getElementById(id);
if (modalElement) {
this.modalInstances[id] = new bootstrap.Modal(modalElement, {
backdrop: 'static',
keyboard: true
});
}
});
// 修复颜色选择器多次触发问题
const categoryModal = document.getElementById('addOrEditCategoryModel');
let pickrInstance = null; // 用局部变量存储实例,避免全局污染
categoryModal.addEventListener('shown.bs.modal', () => {
if (pickrInstance) {
pickrInstance.destroy();
}
pickrInstance = this.createColorPicker();
pickrInstance.on('change', (color) => {
this.categoryAddOrEditDto.color = color.toHEXA().toString();
});
pickrInstance.on('save', (color) => {
this.categoryAddOrEditDto.color = color.toHEXA().toString();
pickrInstance.hide();
});
pickrInstance.on('clear', () => {
this.categoryAddOrEditDto.color = '#dee2e6';
});
});
// 模态框关闭时彻底清理实例和DOM
categoryModal.addEventListener('hidden.bs.modal', () => {
if (pickrInstance) {
pickrInstance.destroy();
pickrInstance = null; // 重置实例
}
// 清空容器确保下次打开是全新DOM
document.getElementById('colorPickerContainer').innerHTML = '';
});
// 初始化 WangEditor延迟初始化确保DOM加载完成
setTimeout(() => {
this.initWangEditor();
}, 500);
// 监听模态框关闭事件,避免内存泄漏
const modal = document.getElementById('addOrEditModel');
modal.addEventListener('hidden.bs.modal', () => {
if (this.editor) {
this.editor.txt.clear(); // 清空内容
document.getElementById('tutorialContentEditor').style.borderColor = '#dee2e6';
document.getElementById('contentError').style.display = 'none';
}
});
}
}).mount('#app');
</script>
</body>
</html>

View File

@@ -1,659 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户管理</title>
<!-- 外部引入库文件 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<!-- 外部引入自定义样式 -->
<link rel="stylesheet" href="/assets/css/table.css">
</head>
<body id="app">
<div class="main-container">
<div class="table-container">
<h4 class="mb-3">用户管理</h4>
<div class="search-bar">
<div class="search-item">
<div class="input-group">
<span class="input-group-text">
昵称
</span>
<input
v-model="searchParams.nickName"
type="text"
class="form-control"
placeholder="请输入昵称"
@keyup.enter="fetchData()"
>
</div>
</div>
<div class="search-item">
<div class="input-group">
<span class="input-group-text">
用户名
</span>
<input
v-model="searchParams.username"
type="text"
class="form-control"
placeholder="请输入用户名"
@keyup.enter="fetchData()"
>
</div>
</div>
<div class="search-item">
<div class="input-group">
<span class="input-group-text">
状态
</span>
<select
v-model="searchParams.status"
class="form-select"
@change="fetchData()"
>
<option value="">全部状态</option>
<option value="1">启用</option>
<option value="0">禁用</option>
</select>
</div>
</div>
<div class="d-flex gap-2">
<button class="btn btn-primary" @click="fetchData()">
<i class="bi bi-search me-1"></i> 搜索
</button>
<button class="btn btn-success" @click="resetSearch()">
<i class="bi bi-arrow-counterclockwise me-1"></i> 重置
</button>
</div>
</div>
<div class="mb-2">
<button class="btn btn-info" @click="openAddModal()">
<i class="bi bi-plus-circle me-1"></i> 新增
</button>
</div>
<div class="table-responsive">
<table class="table table-sm table-striped table-hover table-bordered ">
<thead class="table-light">
<tr>
<th style="width: 50px;">
<input
type="checkbox"
class="form-check-input"
v-model="selectAll"
@change="toggleSelectAll()"
>
</th>
<th>昵称</th>
<th>用户名</th>
<th>号码</th>
<th>邮箱</th>
<th>状态</th>
<th>创建时间</th>
<th style="width: 120px;">操作</th>
</tr>
</thead>
<tbody>
<!-- 加载中状态 -->
<tr v-if="loading">
<td colspan="8" class="p-0">
<div class="loading-container">
<div class="spinner-border text-primary" role="status" style="width: 2rem; height: 2rem;">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2 text-muted">加载中,请稍候...</p>
</div>
</td>
</tr>
<!-- 无数据状态 -->
<tr v-else-if="tableData.length === 0">
<td colspan="8" class="p-0">
<div class="no-data">
<i class="bi bi-folder-x"></i>
<h5>暂无匹配数据</h5>
<p class="text-muted">请尝试调整搜索条件或重置查询</p>
</div>
</td>
</tr>
<!-- 数据列表 -->
<tr v-else v-for="(item, index) in tableData" :key="item.id"
:class="{ selected: selectedIds.includes(item.id) }">
<td>
<input
type="checkbox"
class="form-check-input"
v-model="selectedIds"
:value="item.id"
@change="toggleSelectItem(item.id)"
>
</td>
<td>{{ item.nickName }}</td>
<td>{{ item.username }}</td>
<td>{{ item.telephone }}</td>
<td>{{ item.email }}</td>
<td>
<span class="badge" :class="item.status === 1 ? 'bg-success' : 'bg-danger'">
{{ item.status === 1 ? '启用' : '禁用' }}
</span>
</td>
<td>{{ formatTime(item.createTime) }}</td>
<td>
<div class="d-flex gap-1">
<button class="btn btn-sm btn-warning" @click="openPwdModal(item)">
<i class="bi bi-lock"></i>
</button>
<button class="btn btn-sm btn-primary" @click="openEditModal(item)">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-danger" @click="handleDelete(item.id)">
<i class="bi bi-trash"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="batch-actions mt-3">
<div class="d-flex align-items-center gap-2">
<span class="text-muted">已选中 {{ selectedIds.length }} 条数据</span>
<select
class="form-select"
v-model="batchAction"
:disabled="selectedIds.length === 0"
@change="handleBatchOperation"
style="width: auto; min-width: 160px;"
>
<option value="">-- 批量操作 --</option>
<option value="delete">批量删除</option>
<option value="enable">批量启用</option>
<option value="disable">批量禁用</option>
</select>
<button
class="btn btn-secondary"
@click="clearSelected()"
:disabled="selectedIds.length === 0"
>
<i class="bi bi-x-circle me-1"></i> 取消选择
</button>
</div>
</div>
<div class="d-flex justify-content-between align-items-center mt-3">
<div class="page-info">
共 {{ total }} 条数据,当前第 {{ pageNum }}/{{ totalPages }} 页
</div>
<nav>
<ul class="pagination pagination-sm mb-0">
<li class="page-item" :class="{ disabled: pageNum === 1 }">
<a class="page-link" href="#" @click.prevent="changePage(1)">首页</a>
</li>
<li class="page-item" :class="{ disabled: pageNum === 1 }">
<a class="page-link" href="#" @click.prevent="changePage(pageNum - 1)">上一页</a>
</li>
<li class="page-item" :class="page === pageNum ? 'active' : ''" v-for="page in pageList"
:key="page">
<a class="page-link" href="#" @click.prevent="changePage(page)">{{ page }}</a>
</li>
<li class="page-item" :class="{ disabled: pageNum === totalPages }">
<a class="page-link" href="#" @click.prevent="changePage(pageNum + 1)">下一页</a>
</li>
<li class="page-item" :class="{ disabled: pageNum === totalPages }">
<a class="page-link" href="#" @click.prevent="changePage(totalPages)">末页</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<!-- 修改密码 -->
<div class="modal fade" id="resetPwdModal" tabindex="-1" aria-labelledby="resetPwdModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="resetPwdModalLabel">修改密码</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form>
<div class="mb-3 row">
<label class="col-sm-2 col-form-label">昵称</label>
<div class="col-sm-10">
<input type="text" class="form-control" v-model="resetPasswordDto.nickName" disabled>
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-2 col-form-label">用户名</label>
<div class="col-sm-10">
<input type="text" class="form-control" v-model="resetPasswordDto.username" disabled>
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-2 col-form-label">密码</label>
<div class="col-sm-10 position-relative">
<!-- 密码输入框 -->
<input :type="isPasswordVisible ? 'text' : 'password'" class="form-control pe-10"
id="togglePassword"
placeholder="请输入密码" v-model="resetPasswordDto.password">
<!-- 切换按钮(绝对定位在输入框右侧) -->
<button type="button"
@click="togglePasswordVisibility"
class="btn btn-transparent position-absolute end-0 top-0 h-100 px-3 border-0 bg-transparent"
id="toggleBtn">
<i :class="isPasswordVisible ? 'bi bi-eye-slash text-secondary' : 'bi bi-eye text-secondary'"
id="toggleIcon"></i>
</button>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
<button type="button" class="btn btn-primary" @click.prevent="resetPwd">修改</button>
</div>
</div>
</div>
</div>
<!-- 新增 or 编辑 -->
<div class="modal fade" id="addOrEditModel" tabindex="-1" aria-labelledby="addOrEditModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="addOrEditModalLabel">{{ addOrEditTitle }}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form>
<div class="mb-3 row">
<label class="col-sm-2 col-form-label">昵称</label>
<div class="col-sm-10">
<input type="text" class="form-control" v-model="addOrEditDto.nickName"
placeholder="请输入昵称">
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-2 col-form-label">用户名</label>
<div class="col-sm-10">
<input type="text" class="form-control" v-model="addOrEditDto.username"
placeholder="请输入用户名">
</div>
</div>
<div class="mb-3 row" v-if="addOrEditDto.id == null">
<label class="col-sm-2 col-form-label">密码</label>
<div class="col-sm-10">
<input type="password" class="form-control" v-model="addOrEditDto.password"
placeholder="请输入密码">
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-2 col-form-label">号码</label>
<div class="col-sm-10">
<input type="text" class="form-control" v-model="addOrEditDto.telephone"
placeholder="请输入号码">
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-2 col-form-label">邮箱</label>
<div class="col-sm-10">
<input type="text" class="form-control" v-model="addOrEditDto.email"
placeholder="请输入邮箱">
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
<button type="button" class="btn btn-primary" @click.prevent="saveUser">保存</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios@1.4.0/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script src="/assets/js/axiosRequest.js"></script>
<script src="/assets/js/message.js"></script>
<script src="/assets/js/confirmModal.js"></script>
<script>
const {createApp} = Vue;
createApp({
data() {
return {
tableData: [],
loading: false,
searchParams: {
nickName: '',
username: '',
status: '',
},
pageNum: 1,
pageSize: 10,
total: 0,
totalPages: 0,
selectedIds: [],
selectAll: false,
batchAction: '',
pageRange: 5,
modalInstances: {},
resetPasswordDto: {
userId: null,
nickName: null,
username: null,
password: null,
},
isPasswordVisible: false,
addOrEditTitle: '',
addOrEditDto: {
id: null,
username: null,
nickName: '',
password: null,
status: 1,
email: '',
telephone: ''
}
}
},
computed: {
// 计算当前显示的页码列表
pageList() {
const list = [];
if (this.totalPages === 0) return list;
if (this.totalPages <= this.pageRange) {
for (let i = 1; i <= this.totalPages; i++) {
list.push(i);
}
} else {
let start = Math.max(1, this.pageNum - 2);
let end = Math.min(this.totalPages, this.pageNum + 2);
if (end - start < this.pageRange - 1) {
if (start === 1) {
end = this.pageRange;
} else if (end === this.totalPages) {
start = this.totalPages - this.pageRange + 1;
}
}
for (let i = start; i <= end; i++) {
list.push(i);
}
}
return list;
}
},
methods: {
formatTime(time) {
if (!time) return '-';
const date = new Date(time);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
},
async fetchData() {
this.loading = true;
try {
const response = await request.get('/biz/user/page', {
...this.searchParams,
current: this.pageNum,
size: this.pageSize
});
console.log(response)
if (response.code === 200) {
this.tableData = response.data.records;
this.total = response.data.total;
this.totalPages = response.data.pages;
}
} catch (error) {
console.error('获取数据失败:', error);
this.tableData = [];
this.total = 0;
this.totalPages = 0;
} finally {
this.loading = false;
this.clearSelected();
}
},
// 切换页码
changePage(page) {
// 边界判断
if (page < 1 || page > this.totalPages || page === this.pageNum) return;
this.pageNum = page;
this.fetchData();
},
// 重置搜索
resetSearch() {
this.searchParams = {
nickName: '',
username: '',
status: '',
};
this.pageNum = 1;
this.fetchData();
},
// 全选/取消全选
toggleSelectAll() {
if (this.selectAll) {
this.selectedIds = this.tableData.map(item => item.id);
} else {
this.selectedIds = [];
}
},
// 单个选中/取消选中
toggleSelectItem(id) {
// 当选中状态变化时,更新全选状态
this.selectAll = this.selectedIds.length === this.tableData.length && this.tableData.length > 0;
},
// 清空选中状态
clearSelected() {
this.selectedIds = [];
this.selectAll = false;
this.batchAction = ''; // 重置批量操作选择
},
// 处理批量操作
handleBatchOperation() {
if (!this.batchAction || this.selectedIds.length === 0) return;
switch (this.batchAction) {
case 'delete':
this.handleBatchDelete();
break;
case 'enable':
this.handleBatchStatus(1);
break;
case 'disable':
this.handleBatchStatus(0);
break;
}
// 执行操作后重置选择
this.batchAction = '';
},
// 批量删除
async handleBatchDelete() {
showConfirmModal({
title: '删除确认',
content: `确定要删除选中的 ${this.selectedIds.length} 条数据吗?`,
onConfirm: async () => {
try {
const response = await request.post('/biz/user/batchDelete', {ids: this.selectedIds});
if (response.code === 200) {
$message.success("删除成功");
this.fetchData(); // 重新加载数据
}
} catch (error) {
console.error('删除失败:', error);
$message.error('删除失败,请重试!');
}
}
});
},
// 批量修改状态(启用/禁用)
async handleBatchStatus(status) {
showConfirmModal({
title: '提示',
content: '确认更新这些数据吗?',
onConfirm: async () => {
try {
const response = await request.post('/biz/user/batchStatus', {
ids: this.selectedIds,
status: status
});
if (response.code === 200) {
$message.success(`${status === 1 ? '启用' : '禁用'}选中数据!`);
this.fetchData();
}
} catch (error) {
console.error('批量更新状态失败:', error);
$message.error('操作失败,请重试!');
}
}
});
},
// 单个删除
async handleDelete(id) {
showConfirmModal({
title: '删除确认',
content: '确定要删除这条数据吗?',
onConfirm: async () => {
try {
const response = await request.post('/biz/user/delete', {id});
if (response.code === 200) {
$message.success("删除成功");
this.fetchData(); // 重新加载数据
}
} catch (error) {
console.error('删除失败:', error);
$message.error('删除失败,请重试!');
}
}
});
},
// 打开重置密码墨台框
openPwdModal(item) {
console.log(item)
this.resetPasswordDto.userId = item.id;
this.resetPasswordDto.username = item.username;
this.resetPasswordDto.nickName = item.nickName;
this.resetPasswordDto.password = '';
this.modalInstances['resetPwdModal'].show();
},
resetPwd() {
request.put("/biz/user/resetPassword", this.resetPasswordDto)
.then((res) => {
if (res.code === 200) {
$message.success('修改成功!', 500);
this.modalInstances['resetPwdModal'].hide();
} else {
$message.error('修改失败,请重试!');
}
this.resetPasswordDto = {};
}).catch((error) => {
$message.error('修改失败,请重试!');
})
},
// 打开新增模态框
openAddModal() {
this.addOrEditTitle = '新增';
this.clearForm();
this.modalInstances['addOrEditModel'].show();
},
// 打开编辑模态框
openEditModal(item) {
this.clearForm();
this.addOrEditTitle = '编辑';
this.addOrEditDto = item;
this.modalInstances['addOrEditModel'].show();
},
saveUser() {
let url = (this.addOrEditDto === null || this.addOrEditDto.id === null) ? '/biz/user/save' : '/biz/user/update';
request.post(url, this.addOrEditDto)
.then((res) => {
if (res.code === 200) {
$message.success('保存成功!', 500);
this.fetchData();
this.modalInstances['addOrEditModel'].hide();
} else {
$message.error('保存失败!', 500);
}
this.clearForm();
}).catch(() => {
})
},
togglePasswordVisibility() {
this.isPasswordVisible = !this.isPasswordVisible;
},
// 清空表单数据
clearForm() {
this.addOrEditDto = {
id: null,
username: null,
nickName: '',
password: null,
status: 1,
email: '',
telephone: ''
};
}
},
mounted() {
this.fetchData();
const modalIds = ['resetPwdModal', 'addOrEditModel'];
modalIds.forEach(id => {
console.log(id)
const modalElement = document.getElementById(id);
if (modalElement) {
this.modalInstances[id] = new bootstrap.Modal(modalElement, {
backdrop: 'static',
keyboard: true
});
}
});
}
}).mount('#app');
</script>
</body>
</html>

View File

@@ -1,118 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<link href="/assets/css/dashboard.css" rel="stylesheet">
</head>
<body id="app">
<div class="main-container">
<div class="welcome-card">
<h2>
<i class="bi bi-emoji-smile"></i>
你好,{{ userInfo.realName || userInfo.username }}
</h2>
<p class="mb-0 mt-2">欢迎使用 CoreWing 后台管理系统</p>
</div>
<div class="row">
<div class="col-md-4 mb-4">
<div class="stat-card primary">
<i class="bi bi-people-fill"></i>
<h3>{{statistics.userCount}}</h3>
<p>用户数</p>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="stat-card success">
<i class="bi bi-file-earmark-text-fill"></i>
<h3>567</h3>
<p>设备数</p>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="stat-card warning">
<i class="bi bi-graph-up"></i>
<h3>{{statistics.firmwareCount}}</h3>
<p>固件数</p>
</div>
</div>
</div>
</div>
<!-- Vue 3 -->
<script src="https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.prod.js"></script>
<!-- Axios -->
<script src="https://cdn.jsdelivr.net/npm/axios@1.4.0/dist/axios.min.js"></script>
<!-- Bootstrap 5 JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- 引入封装的 axiosRequest.js -->
<script src="/assets/js/axiosRequest.js"></script>
<script>
const {createApp} = Vue;
createApp({
data() {
return {
userInfo: {
username: '',
realName: '',
userId: ''
},
statistics: {
userCount: 0,
firmwareCount: 0,
}
}
},
methods: {
async loadUserInfo() {
// 从 localStorage 获取用户信息
this.userInfo.username = localStorage.getItem('username') || '';
this.userInfo.realName = localStorage.getItem('realName') || '';
this.userInfo.userId = localStorage.getItem('userId') || '';
const token = localStorage.getItem("token");
if (token) {
try {
const response = await request.get('/sys/user/info');
if (response.code === 200) {
const user = response.data;
this.userInfo.username = user.username;
this.userInfo.realName = user.realName || user.username;
this.userInfo.userId = user.id;
}
} catch (error) {
console.error('获取用户信息失败:', error);
}
}
},
async loadStatistics() {
try {
const response = await request.get('/main/statistics');
if (response.code === 200) {
this.statistics = response.data;
}
} catch (error) {
console.error('获取用户信息失败:', error);
}
}
},
mounted() {
// 加载用户信息
this.loadUserInfo();
// 加载统计信息
this.loadStatistics();
}
}).mount('#app');
</script>
</body>
</html>

View File

@@ -1,93 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CoreWing - 后台管理系统</title>
<style>
:root {
--primary-color: #5B5FDE;
--secondary-color: #0EA5E9;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.container {
text-align: center;
}
.logo {
font-size: 72px;
margin-bottom: 20px;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
h1 {
font-size: 48px;
font-weight: 700;
margin-bottom: 15px;
}
p {
font-size: 18px;
opacity: 0.9;
margin-bottom: 30px;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
font-size: 16px;
}
.spinner {
width: 20px;
height: 20px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-top-color: white;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
</style>
<script>
// 自动跳转到后台登录页面
window.location.href = '/admin/login.html';
</script>
</head>
<body>
<div class="container">
<div class="logo">🚀</div>
<h1>CoreWing</h1>
<p>后台管理系统</p>
<div class="loading">
<div class="spinner"></div>
<span>正在跳转...</span>
</div>
</div>
</body>
</html>

View File

@@ -1,497 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>后台管理系统 - 登录</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<style>
:root {
--primary-color: #5B5FDE;
--secondary-color: #0EA5E9;
--accent-color: #FF6B6B;
--error-color: #F43F5E;
--success-color: #10B981;
--warning-color: #F59E0B;
--info-color: #3B82F6;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: linear-gradient(135deg, #5B5FDE 0%, #0EA5E9 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
padding: 20px;
position: relative;
overflow: hidden;
}
/* 背景装饰圆圈 */
body::before {
content: '';
position: absolute;
width: 500px;
height: 500px;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
top: -200px;
right: -200px;
animation: float 6s ease-in-out infinite;
}
body::after {
content: '';
position: absolute;
width: 300px;
height: 300px;
background: rgba(255, 255, 255, 0.08);
border-radius: 50%;
bottom: -100px;
left: -100px;
animation: float 8s ease-in-out infinite;
}
@keyframes float {
0%, 100% { transform: translateY(0px) rotate(0deg); }
50% { transform: translateY(-20px) rotate(5deg); }
}
.login-container {
position: relative;
z-index: 1;
width: 100%;
max-width: 1800px;
display: flex;
background: white;
border-radius: 24px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
overflow: hidden;
}
/* 左侧装饰区域 */
.login-decoration {
flex: 0.9;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
padding: 60px 50px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
color: white;
position: relative;
overflow: hidden;
}
.login-decoration::before {
content: '';
position: absolute;
width: 300px;
height: 300px;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
top: -80px;
left: -80px;
}
.login-decoration::after {
content: '';
position: absolute;
width: 220px;
height: 220px;
background: rgba(255, 255, 255, 0.08);
border-radius: 50%;
bottom: -60px;
right: -60px;
}
.decoration-content {
position: relative;
z-index: 1;
text-align: center;
}
.decoration-icon {
font-size: 140px;
margin-bottom: 40px;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.05); }
}
.decoration-content h2 {
font-size: 42px;
font-weight: 700;
margin-bottom: 20px;
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}
.decoration-content p {
font-size: 18px;
opacity: 0.95;
line-height: 1.6;
}
/* 右侧表单区域 */
.login-form-section {
flex: 1.3;
padding: 60px 120px;
display: flex;
flex-direction: column;
justify-content: center;
}
.form-header {
margin-bottom: 40px;
}
.form-header h3 {
font-size: 32px;
font-weight: 700;
color: #1F2937;
margin-bottom: 12px;
}
.form-header p {
color: #6B7280;
font-size: 16px;
}
.form-group {
margin-bottom: 25px;
}
.form-label {
display: block;
font-size: 15px;
font-weight: 600;
color: #374151;
margin-bottom: 8px;
}
.input-wrapper {
position: relative;
}
.input-icon {
position: absolute;
left: 18px;
top: 50%;
transform: translateY(-50%);
color: var(--primary-color);
font-size: 20px;
z-index: 1;
}
.form-control {
width: 100%;
padding: 16px 18px 16px 52px;
border: 2px solid #E5E7EB;
border-radius: 12px;
font-size: 16px;
transition: all 0.3s;
background-color: #F9FAFB;
}
.form-control:focus {
outline: none;
border-color: var(--primary-color);
background-color: white;
box-shadow: 0 0 0 4px rgba(91, 95, 222, 0.1);
}
.form-control::placeholder {
color: #9CA3AF;
}
.btn-login {
width: 100%;
padding: 16px;
background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
border: none;
border-radius: 12px;
color: white;
font-size: 17px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
box-shadow: 0 4px 15px rgba(91, 95, 222, 0.3);
}
.btn-login:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(91, 95, 222, 0.4);
}
.btn-login:active:not(:disabled) {
transform: translateY(0);
}
.btn-login:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.alert {
padding: 14px 18px;
border-radius: 12px;
margin-bottom: 20px;
font-size: 15px;
display: flex;
align-items: center;
gap: 10px;
animation: slideDown 0.3s ease;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.alert-success {
background-color: #D1FAE5;
color: var(--success-color);
border: 1px solid var(--success-color);
}
.alert-danger {
background-color: #FEE2E2;
color: var(--error-color);
border: 1px solid var(--error-color);
}
.alert i {
font-size: 18px;
}
.login-footer {
text-align: center;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #E5E7EB;
}
.login-footer small {
color: #6B7280;
font-size: 14px;
}
.spinner-border {
width: 1.2rem;
height: 1.2rem;
border-width: 2px;
}
/* 响应式设计 */
@media (max-width: 768px) {
.login-decoration {
display: none;
}
.login-container {
max-width: 450px;
}
.login-form-section {
padding: 40px 30px;
}
}
</style>
</head>
<body>
<div id="app">
<div class="login-container">
<!-- 左侧装饰区域 -->
<div class="login-decoration">
<div class="decoration-content">
<div class="decoration-icon">
<i class="bi bi-shield-lock-fill"></i>
</div>
<h2>CoreWing</h2>
<p>后台管理系统</p>
<p style="margin-top: 20px; opacity: 0.85;">
安全 · 高效 · 智能
</p>
</div>
</div>
<!-- 右侧表单区域 -->
<div class="login-form-section">
<div class="form-header">
<h3>欢迎回来</h3>
<p>请登录您的账号以继续</p>
</div>
<!-- 提示消息 -->
<div v-if="message.text" :class="'alert alert-' + message.type" role="alert">
<i :class="'bi bi-' + (message.type === 'success' ? 'check-circle-fill' : 'exclamation-circle-fill')"></i>
<span>{{ message.text }}</span>
</div>
<!-- 登录表单 -->
<form @submit.prevent="handleLogin">
<div class="form-group">
<label class="form-label">用户名</label>
<div class="input-wrapper">
<i class="bi bi-person-fill input-icon"></i>
<input
type="text"
class="form-control"
v-model="loginForm.username"
placeholder="请输入用户名"
required
autocomplete="username">
</div>
</div>
<div class="form-group">
<label class="form-label">密码</label>
<div class="input-wrapper">
<i class="bi bi-lock-fill input-icon"></i>
<input
type="password"
class="form-control"
v-model="loginForm.password"
placeholder="请输入密码"
required
autocomplete="current-password">
</div>
</div>
<button
type="submit"
class="btn-login"
:disabled="loading">
<span v-if="loading">
<span class="spinner-border spinner-border-sm" role="status" style="margin-right: 8px;"></span>
登录中...
</span>
<span v-else>
<i class="bi bi-box-arrow-in-right" style="margin-right: 8px;"></i>
登录
</span>
</button>
</form>
<div class="login-footer">
<small>默认管理员账号admin / admin123</small>
</div>
</div>
</div>
</div>
<!-- Vue 3 -->
<script src="https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.prod.js"></script>
<!-- Axios -->
<script src="https://cdn.jsdelivr.net/npm/axios@1.4.0/dist/axios.min.js"></script>
<!-- Bootstrap 5 JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
const { createApp } = Vue;
createApp({
data() {
return {
loginForm: {
username: '',
password: ''
},
loading: false,
message: {
text: '',
type: 'success' // success, danger
}
}
},
methods: {
async handleLogin() {
// 表单验证
if (!this.loginForm.username || !this.loginForm.password) {
this.showMessage('请填写完整的登录信息', 'danger');
return;
}
this.loading = true;
this.message.text = '';
try {
const response = await axios.post('/sys/user/login', this.loginForm);
if (response.data.code === 200) {
// 登录成功
this.showMessage('登录成功,正在跳转...', 'success');
// 保存 token 和用户信息
localStorage.setItem('token', response.data.data.token);
localStorage.setItem('userId', response.data.data.userId);
localStorage.setItem('username', response.data.data.username);
localStorage.setItem('realName', response.data.data.realName || '');
// 2秒后跳转到主页
setTimeout(() => {
window.location.href = '/admin/index.html';
}, 2000);
} else {
// 登录失败
this.showMessage(response.data.message || '登录失败', 'danger');
}
} catch (error) {
console.error('登录错误:', error);
this.showMessage(
error.response?.data?.message || '登录失败,请稍后重试',
'danger'
);
} finally {
this.loading = false;
}
},
showMessage(text, type) {
this.message.text = text;
this.message.type = type;
// 3秒后自动清除消息错误消息
if (type === 'danger') {
setTimeout(() => {
this.message.text = '';
}, 3000);
}
}
},
mounted() {
// 检查是否已登录
const token = localStorage.getItem('token');
if (token) {
// 如果已有 token尝试跳转到主页
// window.location.href = '/admin/loading.html';
}
}
}).mount('#app');
</script>
</body>
</html>

View File

@@ -1,222 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>后台管理系统</title>
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<link rel="stylesheet" href="/assets/css/main.css">
</head>
<body>
<div id="app">
<!-- 侧边栏 -->
<nav class="sidebar">
<div class="sidebar-header">
<h4>
<i class="bi bi-shield-lock-fill"></i>
CoreWing
</h4>
</div>
<div class="sidebar-menu">
<ul class="nav flex-column">
<li class="nav-item">
<a :class="['nav-link', currentUrl === '/admin/dashboard' ? 'active' : '']"
href="#"
@click.prevent="changeMenu('/admin/dashboard')">
<i class="bi bi-speedometer2"></i>
仪表盘
</a>
</li>
<li class="nav-item" v-for="(item, index) in menus" :key="index">
<a :class="['nav-link', currentUrl === item.menuUrl ? 'active' : '']"
href="#"
@click.prevent="changeMenu(item.menuUrl)">
<i :class="item.menuIcon"></i>
{{ item.menuName }}
</a>
</li>
</ul>
</div>
<div class="sidebar-footer">
<button class="btn-logout" data-bs-toggle="modal" data-bs-target="#exitModal">
<i class="bi bi-box-arrow-right me-2"></i>
退出登录
</button>
</div>
</nav>
<!-- 主内容区 -->
<main class="main-content">
<!-- 顶部导航栏 -->
<nav class="navbar navbar-expand-lg navbar-light">
<div class="container-fluid">
<span class="navbar-brand">欢迎回来</span>
<div class="user-info" style="cursor: pointer">
<i class="bi bi-person-circle"></i>
<span>{{ userInfo.realName || userInfo.username }}</span>
</div>
</div>
</nav>
<!-- 内容区域 -->
<div class="content-wrapper">
<!-- iframe容器 - 用于加载菜单对应的URL页面 -->
<iframe
v-if="currentUrl"
class="page-iframe"
:src="currentUrl"
@load="handleIframeLoad"
title="页面内容"
></iframe>
</div>
</main>
<div class="modal fade" id="exitModal" tabindex="-1" aria-labelledby="exitModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="exampleModalLabel">系统提示</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
您确认退出系统吗?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
<button type="button" class="btn btn-primary" @click="handleLogout">退出</button>
</div>
</div>
</div>
</div>
</div>
<!-- Vue 3 -->
<script src="https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.prod.js"></script>
<!-- Axios -->
<script src="https://cdn.jsdelivr.net/npm/axios@1.4.0/dist/axios.min.js"></script>
<!-- Bootstrap 5 JS -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- 引入封装的 axiosRequest.js -->
<script src="/assets/js/axiosRequest.js"></script>
<script>
const {createApp} = Vue;
createApp({
data() {
return {
userInfo: {
username: '',
realName: '',
userId: ''
},
menus: [],
currentUrl: '/admin/dashboard', // 当前加载的URL
isLoading: false // 加载状态
}
},
methods: {
async loadUserInfo() {
// 从 localStorage 获取用户信息
this.userInfo.username = localStorage.getItem('username') || '';
this.userInfo.realName = localStorage.getItem('realName') || '';
this.userInfo.userId = localStorage.getItem('userId') || '';
const token = localStorage.getItem("token");
if (token) {
try {
const response = await request.get('/sys/user/info');
if (response.code === 200) {
const user = response.data;
this.userInfo.username = user.username;
this.userInfo.realName = user.realName || user.username;
this.userInfo.userId = user.id;
}
} catch (error) {
console.error('获取用户信息失败:', error);
}
}
},
async handleLogout() {
try {
await request.get('/sys/user/logout');
} catch (error) {
console.error('退出登录失败:', error);
} finally {
// 清除本地存储
localStorage.clear();
// 跳转到登录页
window.location.href = '/admin/login.html';
}
},
checkLogin() {
const token = localStorage.getItem('token');
if (!token) {
// 未登录,跳转到登录页
window.location.href = '/admin/login.html';
}
},
async initMenu() {
const response = await request.get('/sys/menu/initSysMenu');
if (response.code === 200) {
this.menus = response.data;
}
this.initCurrentUrlFromHash();
},
// 从浏览器hash初始化当前URL
initCurrentUrlFromHash() {
const hash = window.location.hash.slice(1); // 获取 # 后的内容(如 #/admin/user 取 /admin/user
// 如果hash存在且是有效菜单URL用hash否则用默认的仪表盘
const validMenuUrls = this.menus.map(item => item.menuUrl);
if (hash && (validMenuUrls.includes(hash) || hash === '/admin/dashboard')) {
this.currentUrl = hash;
} else {
this.currentUrl = '/admin/dashboard';
// 同步默认值到地址栏
window.location.hash = this.currentUrl;
}
},
// 切换菜单时更新URL和状态
changeMenu(menuUrl) {
this.currentUrl = menuUrl;
this.isLoading = true;
window.location.hash = menuUrl;
},
handleIframeLoad() {
console.log('加载完成');
this.isLoading = false;
}
},
mounted() {
// 检查登录状态
this.checkLogin();
// 加载系统菜单
this.initMenu();
// 加载用户信息
this.loadUserInfo();
this.isLoading = true;
// 监听浏览器hash变化刷新/前进后退时触发)
window.addEventListener('hashchange', () => {
this.initCurrentUrlFromHash();
});
}
}).mount('#app');
</script>
</body>
</html>

View File

@@ -1,669 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>系统设置</title>
<!-- 外部引入库文件 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<!-- 外部引入自定义样式 -->
<style>
body {
background-color: #f8f9fa;
color: #333;
font-size: 14px;
line-height: 1.5;
}
.main-container {
margin: 0 auto;
padding: 14px;
}
.settings-tabs {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.05);
overflow: hidden;
}
.nav-tabs {
border-bottom: 1px solid #e9ecef;
background-color: #f8f9fa;
}
.nav-tabs .nav-item .nav-link {
color: #6c757d;
border: none;
padding: 12px 20px;
font-weight: 500;
border-radius: 0;
}
.nav-tabs .nav-item .nav-link.active {
color: #0d6efd;
background-color: #fff;
border-bottom: 3px solid #0d6efd;
}
.nav-tabs .nav-item .nav-link:hover {
color: #0d6efd;
background-color: rgba(13, 110, 253, 0.05);
}
.tab-content {
padding: 25px;
}
.settings-section {
margin-bottom: 30px;
}
.settings-section h3 {
font-size: 18px;
margin-bottom: 15px;
color: #212529;
padding-bottom: 8px;
border-bottom: 1px solid #e9ecef;
}
.form-group {
margin-bottom: 20px;
}
.form-label {
font-weight: 500;
margin-bottom: 8px;
color: #495057;
}
.form-text {
font-size: 13px;
color: #6c757d;
}
.settings-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
padding-top: 20px;
margin-top: 20px;
border-top: 1px solid #e9ecef;
}
.btn {
padding: 8px 20px;
font-size: 14px;
border-radius: 6px;
}
.switch-label {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
}
.switch-label .form-text {
margin-bottom: 0;
}
.profile-picture {
display: flex;
align-items: center;
gap: 20px;
margin-bottom: 20px;
}
.profile-picture img {
width: 100px;
height: 100px;
border-radius: 50%;
object-fit: cover;
border: 2px solid #e9ecef;
}
.upload-btn {
display: inline-flex;
align-items: center;
gap: 5px;
}
.language-selector, .theme-selector {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-top: 10px;
}
.language-option, .theme-option {
display: flex;
align-items: center;
gap: 5px;
}
.alert-message {
position: fixed;
top: 20px;
right: 20px;
z-index: 1050;
animation: slideIn 0.3s ease-out;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@media (max-width: 768px) {
.nav-tabs {
flex-wrap: wrap;
}
.profile-picture {
flex-direction: column;
align-items: flex-start;
}
.settings-actions {
flex-direction: column;
}
.settings-actions .btn {
width: 100%;
}
}
</style>
</head>
<body id="app">
<div class="main-container">
<!-- 设置选项卡 -->
<div class="settings-tabs">
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" data-bs-toggle="tab" data-bs-target="#basic-settings" type="button" role="tab">
<i class="bi bi-person me-1"></i>基本设置
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#security-settings" type="button" role="tab">
<i class="bi bi-shield me-1"></i>安全设置
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#notification-settings" type="button" role="tab">
<i class="bi bi-bell me-1"></i>通知设置
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#system-preferences" type="button" role="tab">
<i class="bi bi-sliders me-1"></i>系统偏好
</button>
</li>
</ul>
<!-- 选项卡内容 -->
<div class="tab-content">
<!-- 基本设置 -->
<div class="tab-pane fade show active" id="basic-settings" role="tabpanel">
<div class="settings-section">
<h3><i class="bi bi-person-circle me-2"></i>个人资料</h3>
<div class="profile-picture">
<img src="https://picsum.photos/id/1005/200/200" alt="用户头像">
<div>
<button class="btn btn-outline-primary upload-btn">
<i class="bi bi-upload"></i> 更换头像
</button>
<p class="form-text mt-2">支持 JPG、PNG 格式,建议尺寸 200x200px</p>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label class="form-label" for="fullname">姓名</label>
<input type="text" class="form-control" id="fullname" v-model="profile.fullname" placeholder="请输入您的姓名">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label class="form-label" for="username">用户名</label>
<input type="text" class="form-control" id="username" v-model="profile.username" placeholder="请输入用户名">
<div class="form-text">用户名用于登录,不可修改</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label class="form-label" for="email">电子邮箱</label>
<input type="email" class="form-control" id="email" v-model="profile.email" placeholder="请输入电子邮箱">
<div class="form-text">用于接收系统通知和密码重置</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label class="form-label" for="phone">手机号码</label>
<input type="tel" class="form-control" id="phone" v-model="profile.phone" placeholder="请输入手机号码">
</div>
</div>
</div>
<div class="form-group">
<label class="form-label" for="bio">个人简介</label>
<textarea class="form-control" id="bio" v-model="profile.bio" rows="3" placeholder="请输入个人简介"></textarea>
</div>
</div>
<div class="settings-actions">
<button class="btn btn-secondary" @click="resetForm('basic')">取消</button>
<button class="btn btn-primary" @click="saveSettings('basic')">保存设置</button>
</div>
</div>
<!-- 安全设置 -->
<div class="tab-pane fade" id="security-settings" role="tabpanel">
<div class="settings-section">
<h3><i class="bi bi-lock me-2"></i>密码管理</h3>
<div class="form-group">
<label class="form-label" for="current-password">当前密码</label>
<input type="password" class="form-control" id="current-password" v-model="security.currentPassword" placeholder="请输入当前密码">
</div>
<div class="form-group">
<label class="form-label" for="new-password">新密码</label>
<input type="password" class="form-control" id="new-password" v-model="security.newPassword" placeholder="请输入新密码">
<div class="form-text">密码长度至少8位包含字母和数字</div>
</div>
<div class="form-group">
<label class="form-label" for="confirm-password">确认新密码</label>
<input type="password" class="form-control" id="confirm-password" v-model="security.confirmPassword" placeholder="请再次输入新密码">
</div>
</div>
<div class="settings-section">
<h3><i class="bi bi-shield-lock me-2"></i>登录安全</h3>
<div class="form-group">
<div class="form-check form-switch">
<label class="switch-label">
<input class="form-check-input" type="checkbox" v-model="security.twoFactorAuth">
启用两步验证
<span class="form-text">启用后,登录时需要额外验证,提高账号安全性</span>
</label>
</div>
</div>
<div class="form-group">
<div class="form-check form-switch">
<label class="switch-label">
<input class="form-check-input" type="checkbox" v-model="security.loginAlert">
登录提醒
<span class="form-text">异地登录或新设备登录时发送提醒</span>
</label>
</div>
</div>
<div class="form-group">
<label class="form-label">最近登录记录</label>
<table class="table table-sm">
<thead>
<tr>
<th>时间</th>
<th>IP地址</th>
<th>设备</th>
<th>状态</th>
</tr>
</thead>
<tbody>
<tr v-for="log in loginLogs" :key="log.id">
<td>{{ log.time }}</td>
<td>{{ log.ip }}</td>
<td>{{ log.device }}</td>
<td><span class="badge bg-success">正常</span></td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="settings-actions">
<button class="btn btn-secondary" @click="resetForm('security')">取消</button>
<button class="btn btn-primary" @click="saveSettings('security')">保存设置</button>
</div>
</div>
<!-- 通知设置 -->
<div class="tab-pane fade" id="notification-settings" role="tabpanel">
<div class="settings-section">
<h3><i class="bi bi-envelope me-2"></i>通知方式</h3>
<div class="form-group">
<div class="form-check form-switch">
<label class="switch-label">
<input class="form-check-input" type="checkbox" v-model="notifications.email">
邮件通知
<span class="form-text">通过电子邮件接收通知</span>
</label>
</div>
</div>
<div class="form-group">
<div class="form-check form-switch">
<label class="switch-label">
<input class="form-check-input" type="checkbox" v-model="notifications.sms">
短信通知
<span class="form-text">通过手机短信接收通知</span>
</label>
</div>
</div>
<div class="form-group">
<div class="form-check form-switch">
<label class="switch-label">
<input class="form-check-input" type="checkbox" v-model="notifications.inApp">
应用内通知
<span class="form-text">在系统内接收通知提醒</span>
</label>
</div>
</div>
</div>
<div class="settings-section">
<h3><i class="bi bi-bell-ring me-2"></i>通知类型</h3>
<div class="form-group">
<div class="form-check form-switch">
<label class="switch-label">
<input class="form-check-input" type="checkbox" v-model="notifications.types.system">
系统通知
<span class="form-text">接收系统更新、维护等重要通知</span>
</label>
</div>
</div>
<div class="form-group">
<div class="form-check form-switch">
<label class="switch-label">
<input class="form-check-input" type="checkbox" v-model="notifications.types.task">
任务通知
<span class="form-text">接收任务分配、截止提醒等通知</span>
</label>
</div>
</div>
<div class="form-group">
<div class="form-check form-switch">
<label class="switch-label">
<input class="form-check-input" type="checkbox" v-model="notifications.types.message">
消息通知
<span class="form-text">接收其他用户发送的消息通知</span>
</label>
</div>
</div>
<div class="form-group">
<div class="form-check form-switch">
<label class="switch-label">
<input class="form-check-input" type="checkbox" v-model="notifications.types.marketing">
营销通知
<span class="form-text">接收产品推广、活动等营销信息</span>
</label>
</div>
</div>
</div>
<div class="settings-actions">
<button class="btn btn-secondary" @click="resetForm('notifications')">取消</button>
<button class="btn btn-primary" @click="saveSettings('notifications')">保存设置</button>
</div>
</div>
<!-- 系统偏好 -->
<div class="tab-pane fade" id="system-preferences" role="tabpanel">
<div class="settings-section">
<h3><i class="bi bi-globe me-2"></i>语言设置</h3>
<div class="form-group">
<label class="form-label">界面语言</label>
<div class="language-selector">
<div class="language-option">
<input class="form-check-input" type="radio" name="language" id="zh-CN" v-model="preferences.language" value="zh-CN">
<label class="form-check-label" for="zh-CN">简体中文</label>
</div>
<div class="language-option">
<input class="form-check-input" type="radio" name="language" id="en-US" v-model="preferences.language" value="en-US">
<label class="form-check-label" for="en-US">English (US)</label>
</div>
<div class="language-option">
<input class="form-check-input" type="radio" name="language" id="ja-JP" v-model="preferences.language" value="ja-JP">
<label class="form-check-label" for="ja-JP">日本語</label>
</div>
</div>
</div>
<div class="form-group">
<div class="form-check form-switch">
<label class="switch-label">
<input class="form-check-input" type="checkbox" v-model="preferences.autoTranslate">
自动翻译
<span class="form-text">自动翻译系统内容为所选语言</span>
</label>
</div>
</div>
</div>
<div class="settings-section">
<h3><i class="bi bi-paint me-2"></i>主题设置</h3>
<div class="form-group">
<label class="form-label">界面主题</label>
<div class="theme-selector">
<div class="theme-option">
<input class="form-check-input" type="radio" name="theme" id="light-theme" v-model="preferences.theme" value="light">
<label class="form-check-label" for="light-theme">浅色主题</label>
</div>
<div class="theme-option">
<input class="form-check-input" type="radio" name="theme" id="dark-theme" v-model="preferences.theme" value="dark">
<label class="form-check-label" for="dark-theme">深色主题</label>
</div>
<div class="theme-option">
<input class="form-check-input" type="radio" name="theme" id="auto-theme" v-model="preferences.theme" value="auto">
<label class="form-check-label" for="auto-theme">跟随系统</label>
</div>
</div>
</div>
</div>
<div class="settings-section">
<h3><i class="bi bi-clock me-2"></i>时间与日期</h3>
<div class="form-group">
<label class="form-label" for="timezone">时区</label>
<select class="form-select" id="timezone" v-model="preferences.timezone">
<option value="Asia/Shanghai">Asia/Shanghai (GMT+8)</option>
<option value="UTC">UTC (GMT+0)</option>
<option value="America/New_York">America/New_York (GMT-5)</option>
<option value="Europe/London">Europe/London (GMT+0)</option>
</select>
</div>
<div class="form-group">
<label class="form-label" for="date-format">日期格式</label>
<select class="form-select" id="date-format" v-model="preferences.dateFormat">
<option value="yyyy-MM-dd">2023-12-31</option>
<option value="dd/MM/yyyy">31/12/2023</option>
<option value="MM/dd/yyyy">12/31/2023</option>
</select>
</div>
</div>
<div class="settings-actions">
<button class="btn btn-secondary" @click="resetForm('preferences')">取消</button>
<button class="btn btn-primary" @click="saveSettings('preferences')">保存设置</button>
</div>
</div>
</div>
</div>
<!-- 成功提示框 -->
<div v-if="showSuccessAlert" class="alert-message">
<div class="alert alert-success alert-dismissible fade show" role="alert">
<strong>设置已保存!</strong> 您的更改已成功应用。
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close" @click="showSuccessAlert = false"></button>
</div>
</div>
</div>
<!-- 外部引入库文件 -->
<script src="https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios@1.4.0/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- 外部引入模拟请求文件 -->
<script src="js/axiosRequest.js"></script>
<!-- 页面核心逻辑 -->
<script>
const {createApp} = Vue;
createApp({
data() {
return {
// 个人资料设置
profile: {
fullname: '张三',
username: 'zhangsan',
email: 'zhangsan@example.com',
phone: '13800138000',
bio: '系统管理员,负责系统日常维护和管理工作'
},
// 安全设置
security: {
currentPassword: '',
newPassword: '',
confirmPassword: '',
twoFactorAuth: false,
loginAlert: true
},
// 通知设置
notifications: {
email: true,
sms: false,
inApp: true,
types: {
system: true,
task: true,
message: true,
marketing: false
}
},
// 系统偏好设置
preferences: {
language: 'zh-CN',
autoTranslate: false,
theme: 'light',
timezone: 'Asia/Shanghai',
dateFormat: 'yyyy-MM-dd'
},
// 登录日志
loginLogs: [
{ id: 1, time: '2023-11-01 09:30:25', ip: '192.168.1.1', device: 'Chrome / Windows 10' },
{ id: 2, time: '2023-10-29 15:45:12', ip: '192.168.1.1', device: 'Safari / macOS' },
{ id: 3, time: '2023-10-28 20:12:36', ip: '123.123.123.123', device: 'Mobile Safari / iOS' }
],
// 保存原始数据用于重置
originalData: {},
// 成功提示框显示状态
showSuccessAlert: false
}
},
computed: {
},
methods: {
// 保存设置
async saveSettings(type) {
// 模拟表单验证
if (type === 'security' && this.security.newPassword) {
if (this.security.newPassword !== this.security.confirmPassword) {
alert('新密码和确认密码不一致');
return;
}
if (this.security.newPassword.length < 8) {
alert('密码长度至少8位');
return;
}
}
// 模拟API请求保存设置
try {
const response = await request.post(`/system/settings/${type}`, this[type]);
if (response.code === 200) {
// 显示成功提示
this.showSuccessAlert = true;
// 3秒后自动关闭提示
setTimeout(() => {
this.showSuccessAlert = false;
}, 3000);
// 更新原始数据
this.originalData[type] = JSON.parse(JSON.stringify(this[type]));
}
} catch (error) {
console.error('保存设置失败:', error);
alert('保存设置失败,请重试');
}
},
// 重置表单
resetForm(type) {
if (this.originalData[type]) {
this[type] = JSON.parse(JSON.stringify(this.originalData[type]));
}
},
// 获取设置数据
async fetchData() {
try {
// 模拟获取设置数据
const response = await request.get('/system/settings');
if (response.code === 200) {
const data = response.data;
// 初始化设置数据
if (data.profile) this.profile = data.profile;
if (data.security) this.security = data.security;
if (data.notifications) this.notifications = data.notifications;
if (data.preferences) this.preferences = data.preferences;
// 保存原始数据用于重置
this.originalData = {
profile: JSON.parse(JSON.stringify(this.profile)),
security: JSON.parse(JSON.stringify(this.security)),
notifications: JSON.parse(JSON.stringify(this.notifications)),
preferences: JSON.parse(JSON.stringify(this.preferences))
};
}
} catch (error) {
console.error('获取设置数据失败:', error);
}
}
},
mounted() {
// 页面加载完成后初始化数据
this.fetchData();
}
}).mount('#app');
</script>
</body>
</html>

View File

@@ -1,672 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户管理系统</title>
<!-- 外部引入库文件 -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css" rel="stylesheet">
<!-- 外部引入自定义样式 -->
<link rel="stylesheet" href="/assets/css/table.css">
</head>
<body id="app">
<div class="main-container">
<div class="table-container">
<!-- 页面标题 -->
<h3 class="mb-4">系统用户</h3>
<!-- 多参数搜索栏 -->
<div class="search-bar">
<!-- 搜索参数1关键词 -->
<div class="search-item">
<div class="input-group">
<span class="input-group-text">
用户名
</span>
<input
v-model="searchParams.username"
type="text"
class="form-control"
placeholder="关键词搜索"
@keyup.enter="fetchData()"
>
</div>
</div>
<!-- 搜索参数2状态筛选 -->
<div class="search-item">
<div class="input-group">
<span class="input-group-text">
状态
</span>
<select
v-model="searchParams.status"
class="form-select"
@change="fetchData()"
>
<option value="">全部状态</option>
<option value="1">启用</option>
<option value="0">禁用</option>
</select>
</div>
</div>
<!-- 搜索和重置按钮 -->
<div class="d-flex gap-2">
<button class="btn btn-primary" @click="fetchData()">
<i class="bi bi-search me-1"></i> 搜索
</button>
<button class="btn btn-success" @click="resetSearch()">
<i class="bi bi-arrow-counterclockwise me-1"></i> 重置
</button>
</div>
</div>
<!-- 新增按钮 -->
<div class="mb-3">
<button class="btn btn-info" @click="openAddModal()">
<i class="bi bi-plus-circle me-1"></i> 新增
</button>
</div>
<!-- 表格 -->
<div class="table-responsive">
<table class="table table-striped table-hover table-bordered">
<thead class="table-light">
<tr>
<!-- 全选复选框 -->
<th style="width: 50px;">
<input
type="checkbox"
class="form-check-input"
v-model="selectAll"
@change="toggleSelectAll()"
>
</th>
<th>ID</th>
<th>名称</th>
<th>状态</th>
<th>用户类型</th>
<th>创建时间</th>
<th style="width: 120px;">操作</th>
</tr>
</thead>
<tbody>
<!-- 加载中状态 -->
<tr v-if="loading">
<td colspan="8" class="p-0">
<div class="loading-container">
<div class="spinner-border text-primary" role="status" style="width: 2rem; height: 2rem;">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2 text-muted">加载中,请稍候...</p>
</div>
</td>
</tr>
<!-- 无数据状态 -->
<tr v-else-if="tableData.length === 0">
<td colspan="8" class="p-0">
<div class="no-data">
<i class="bi bi-folder-x"></i>
<h5>暂无匹配数据</h5>
<p class="text-muted">请尝试调整搜索条件或重置查询</p>
</div>
</td>
</tr>
<!-- 数据列表 -->
<tr v-else v-for="(item, index) in tableData" :key="item.id"
:class="{ selected: selectedIds.includes(item.id) }">
<td>
<input
type="checkbox"
class="form-check-input"
v-model="selectedIds"
:value="item.id"
@change="toggleSelectItem(item.id)"
>
</td>
<td>{{ item.id }}</td>
<td>{{ item.username }}</td>
<td>
<span class="badge" :class="item.status === 1 ? 'bg-success' : 'bg-danger'">
{{ item.status === 1 ? '启用' : '禁用' }}
</span>
</td>
<td>
<span class="badge bg-primary">
{{ item.userType === 'admin' ? '管理员' : item.userType === 'editor' ? '编辑' : '查看者' }}
</span>
</td>
<td>{{ item.createTime }}</td>
<td>
<div class="d-flex gap-1">
<button class="btn btn-sm btn-warning" @click="openPwdModal(item)">
<i class="bi bi-lock"></i>
</button>
<button class="btn btn-sm btn-primary" @click="openEditModal(item)">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-danger" @click="handleDelete(item.id)">
<i class="bi bi-trash"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- <div class="batch-actions mt-3">-->
<!-- <div class="d-flex align-items-center gap-2">-->
<!-- <span class="text-muted">已选中 {{ selectedIds.length }} 条数据</span>-->
<!-- <select-->
<!-- class="form-select"-->
<!-- v-model="batchAction"-->
<!-- :disabled="selectedIds.length === 0"-->
<!-- @change="handleBatchOperation"-->
<!-- style="width: auto; min-width: 160px;"-->
<!-- >-->
<!-- <option value="">&#45;&#45; 批量操作 &#45;&#45;</option>-->
<!-- <option value="delete">批量删除</option>-->
<!-- <option value="enable">批量启用</option>-->
<!-- <option value="disable">批量禁用</option>-->
<!-- </select>-->
<!-- <button-->
<!-- class="btn btn-secondary"-->
<!-- @click="clearSelected()"-->
<!-- :disabled="selectedIds.length === 0"-->
<!-- >-->
<!-- <i class="bi bi-x-circle me-1"></i> 取消选择-->
<!-- </button>-->
<!-- </div>-->
<!-- </div>-->
<!-- 分页控件 -->
<div class="d-flex justify-content-between align-items-center mt-3">
<div class="page-info">
共 {{ total }} 条数据,当前第 {{ pageNum }}/{{ totalPages }} 页
</div>
<nav>
<ul class="pagination pagination-sm mb-0">
<li class="page-item" :class="{ disabled: pageNum === 1 }">
<a class="page-link" href="#" @click.prevent="changePage(1)">首页</a>
</li>
<li class="page-item" :class="{ disabled: pageNum === 1 }">
<a class="page-link" href="#" @click.prevent="changePage(pageNum - 1)">上一页</a>
</li>
<li class="page-item active" v-for="page in pageList" :key="page">
<a class="page-link" href="#" @click.prevent="changePage(page)">{{ page }}</a>
</li>
<li class="page-item" :class="{ disabled: pageNum === totalPages }">
<a class="page-link" href="#" @click.prevent="changePage(pageNum + 1)">下一页</a>
</li>
<li class="page-item" :class="{ disabled: pageNum === totalPages }">
<a class="page-link" href="#" @click.prevent="changePage(totalPages)">末页</a>
</li>
</ul>
</nav>
</div>
</div>
</div>
<!-- 修改密码 -->
<div class="modal fade" id="resetPwdModal" tabindex="-1" aria-labelledby="resetPwdModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="resetPwdModalLabel">修改密码</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form>
<div class="mb-3 row">
<label class="col-sm-2 col-form-label">用户名</label>
<div class="col-sm-10">
<input type="text" class="form-control" v-model="resetPasswordDto.username" disabled>
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-2 col-form-label">密码</label>
<div class="col-sm-10 position-relative">
<!-- 密码输入框 -->
<input :type="isPasswordVisible ? 'text' : 'password'" class="form-control pe-10"
id="togglePassword"
placeholder="请输入密码" v-model="resetPasswordDto.password">
<!-- 切换按钮(绝对定位在输入框右侧) -->
<button type="button"
@click="togglePasswordVisibility"
class="btn btn-transparent position-absolute end-0 top-0 h-100 px-3 border-0 bg-transparent"
id="toggleBtn">
<i :class="isPasswordVisible ? 'bi bi-eye-slash text-secondary' : 'bi bi-eye text-secondary'"
id="toggleIcon"></i>
</button>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
<button type="button" class="btn btn-primary" @click.prevent="resetPwd">修改</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="addOrEditModel" tabindex="-1" aria-labelledby="addOrEditModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="addOrEditModalLabel">{{ addOrEditTitle }}</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form>
<div class="mb-3 row">
<label class="col-sm-2 col-form-label">姓名</label>
<div class="col-sm-10">
<input type="text" class="form-control" v-model="addOrEditDto.realName"
placeholder="请输入真实姓名">
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-2 col-form-label">用户名</label>
<div class="col-sm-10">
<input type="text" class="form-control" v-model="addOrEditDto.username"
placeholder="请输入用户名">
</div>
</div>
<div class="mb-3 row" v-if="addOrEditDto.id == null">
<label class="col-sm-2 col-form-label">密码</label>
<div class="col-sm-10">
<input type="password" class="form-control" v-model="addOrEditDto.password"
placeholder="请输入密码">
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-2 col-form-label">号码</label>
<div class="col-sm-10">
<input type="text" class="form-control" v-model="addOrEditDto.telephone"
placeholder="请输入号码">
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-2 col-form-label">邮箱</label>
<div class="col-sm-10">
<input type="text" class="form-control" v-model="addOrEditDto.email"
placeholder="请输入邮箱">
</div>
</div>
<div class="mb-3 row">
<label class="col-sm-2 col-form-label">状态</label>
<div class="col-sm-10">
<div class="form-check form-check-inline" >
<input class="form-check-input" type="radio" name="status" id="enableRadio" value="1" v-model="addOrEditDto.status">
<label class="form-check-label" for="enableRadio">启用</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="status" id="disableRadio" value="0" v-model="addOrEditDto.status">
<label class="form-check-label" for="disableRadio">禁用</label>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">关闭</button>
<button type="button" class="btn btn-primary" @click.prevent="saveUser">保存</button>
</div>
</div>
</div>
</div>
<!-- 外部引入库文件 -->
<script src="https://cdn.jsdelivr.net/npm/vue@3.3.4/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/axios@1.4.0/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<!-- 外部引入模拟请求文件 -->
<script src="/assets/js/axiosRequest.js"></script>
<script src="/assets/js/message.js"></script>
<script src="/assets/js/confirmModal.js"></script>
<!-- 页面核心逻辑 -->
<script>
const {createApp} = Vue;
createApp({
data() {
return {
tableData: [], // 表格数据源
loading: false, // 加载状态
// 多搜索参数绑定(与搜索栏对应)
searchParams: {
username: '',
status: '',
},
// 分页参数
pageNum: 1, // 当前页码
pageSize: 10, // 每页条数
total: 0, // 总数据量
totalPages: 0, // 总页数
// 批量操作
selectedIds: [], // 选中的ID集合
selectAll: false, // 全选状态
batchAction: '', // 批量操作选择
// 分页显示范围最多显示5个页码
pageRange: 5,
modalInstances: {},
resetPasswordDto: {
userId: null,
nickName: null,
username: null,
password: null,
},
isPasswordVisible: false,
addOrEditTitle: '',
addOrEditDto: {
id: null,
username: null,
realName: '',
password: null,
status: 1,
email: '',
telephone: ''
}
}
},
computed: {
// 计算当前显示的页码列表
pageList() {
const list = [];
if (this.totalPages === 0) return list;
// 总页数小于等于显示范围,直接显示所有页码
if (this.totalPages <= this.pageRange) {
for (let i = 1; i <= this.totalPages; i++) {
list.push(i);
}
} else {
// 总页数大于显示范围显示当前页前后2个页码
let start = Math.max(1, this.pageNum - 2);
let end = Math.min(this.totalPages, this.pageNum + 2);
// 确保显示5个页码
if (end - start < this.pageRange - 1) {
if (start === 1) {
end = this.pageRange;
} else if (end === this.totalPages) {
start = this.totalPages - this.pageRange + 1;
}
}
for (let i = start; i <= end; i++) {
list.push(i);
}
}
return list;
}
},
methods: {
// 格式化时间(毫秒转字符串)
formatTime(time) {
if (!time) return '-';
const date = new Date(time);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
},
// 获取表格数据(核心方法)
async fetchData() {
this.loading = true;
try {
// 调用模拟接口获取数据
const response = await request.get('/sys/user/page', {
...this.searchParams, // 传递所有搜索参数
current: this.pageNum,
size: this.pageSize
});
if (response.code === 200) {
this.tableData = response.data.records;
this.total = response.data.total;
this.totalPages = response.data.pages;
}
} catch (error) {
console.error('获取数据失败:', error);
this.tableData = [];
this.total = 0;
this.totalPages = 0;
} finally {
this.loading = false;
this.clearSelected(); // 每次加载数据清空选中状态
}
},
// 切换页码
changePage(page) {
// 边界判断
if (page < 1 || page > this.totalPages || page === this.pageNum) return;
this.pageNum = page;
this.fetchData(); // 切换页码后重新加载数据
},
// 重置搜索
resetSearch() {
// 重置所有搜索参数
this.searchParams = {
keyword: '',
status: '',
createTimeStart: '',
createTimeEnd: '',
userType: '',
deptId: ''
};
this.pageNum = 1; // 重置到第一页
this.fetchData();
},
// 全选/取消全选
toggleSelectAll() {
if (this.selectAll) {
// 全选收集所有数据的ID
this.selectedIds = this.tableData.map(item => item.id);
} else {
// 取消全选清空选中ID
this.selectedIds = [];
}
},
// 单个选中/取消选中
toggleSelectItem(id) {
// 当选中状态变化时,更新全选状态
this.selectAll = this.selectedIds.length === this.tableData.length && this.tableData.length > 0;
},
// 清空选中状态
clearSelected() {
this.selectedIds = [];
this.selectAll = false;
this.batchAction = ''; // 重置批量操作选择
},
// 处理批量操作
handleBatchOperation() {
if (!this.batchAction || this.selectedIds.length === 0) return;
switch (this.batchAction) {
case 'delete':
this.handleBatchDelete();
break;
case 'enable':
this.handleBatchStatus(1);
break;
case 'disable':
this.handleBatchStatus(0);
break;
}
// 执行操作后重置选择
this.batchAction = '';
},
// 批量删除
async handleBatchDelete() {
if (!confirm(`确定要删除选中的 ${this.selectedIds.length} 条数据吗?`)) return;
try {
const response = await request.post('/sys/data/batchDelete', {
ids: this.selectedIds
});
if (response.code === 200) {
alert('删除成功!');
this.fetchData(); // 重新加载数据
}
} catch (error) {
console.error('批量删除失败:', error);
alert('删除失败,请重试!');
}
},
// 批量修改状态(启用/禁用)
async handleBatchStatus(status) {
try {
const response = await request.post('/sys/data/batchUpdateStatus', {
ids: this.selectedIds,
status: status
});
if (response.code === 200) {
alert(`${status === 1 ? '启用' : '禁用'}选中数据!`);
this.fetchData(); // 重新加载数据
}
} catch (error) {
console.error('批量更新状态失败:', error);
alert('操作失败,请重试!');
}
},
// 单个删除
async handleDelete(id) {
showConfirmModal({
title: '删除确认',
content: '确定要删除这条数据吗?',
onConfirm: async () => {
try {
const response = await request.post('/sys/user/delete', {id});
if (response.code === 200) {
$message.success("删除成功");
this.fetchData(); // 重新加载数据
}
} catch (error) {
console.error('删除失败:', error);
$message.error('删除失败,请重试!');
}
}
});
},
// 打开重置密码墨台框
openPwdModal(item) {
console.log(item)
this.resetPasswordDto.userId = item.id;
this.resetPasswordDto.username = item.username;
this.resetPasswordDto.password = '';
this.modalInstances['resetPwdModal'].show();
},
resetPwd() {
request.put("/sys/user/resetPassword", this.resetPasswordDto)
.then((res) => {
if (res.code === 200) {
$message.success('修改成功!', 500);
this.modalInstances['resetPwdModal'].hide();
} else {
$message.error('修改失败,请重试!');
}
this.resetPasswordDto = {};
}).catch((error) => {
$message.error('修改失败,请重试!');
})
},
// 打开新增模态框
openAddModal() {
this.addOrEditTitle = '新增';
this.clearForm();
this.modalInstances['addOrEditModel'].show();
},
// 打开编辑模态框
openEditModal(item) {
this.clearForm();
this.addOrEditTitle = '编辑';
this.addOrEditDto = item;
this.modalInstances['addOrEditModel'].show();
},
saveUser() {
let url = (this.addOrEditDto === null || this.addOrEditDto.id === null) ? '/sys/user/save' : '/sys/user/update';
request.post(url, this.addOrEditDto)
.then((res) => {
if (res.code === 200) {
$message.success('保存成功!', 500);
this.fetchData();
this.modalInstances['addOrEditModel'].hide();
} else {
$message.error('保存失败!', 500);
}
this.clearForm();
}).catch(() => {
})
},
togglePasswordVisibility() {
this.isPasswordVisible = !this.isPasswordVisible;
},
// 清空表单数据
clearForm() {
this.addOrEditDto = {
id: null,
username: '',
realName: '',
password: null,
status: 1,
email: '',
telephone: ''
};
}
},
mounted() {
// 页面加载完成后初始化数据
this.fetchData();
const modalIds = ['resetPwdModal', 'addOrEditModel'];
modalIds.forEach(id => {
console.log(id)
const modalElement = document.getElementById(id);
if (modalElement) {
this.modalInstances[id] = new bootstrap.Modal(modalElement, {
backdrop: 'static',
keyboard: true
});
}
});
}
}).mount('#app');
</script>
</body>
</html>