Browse Source

feat(douyin): 添加抖音用户信息管理功能

- 新增抖音用户信息的CRUD接口实现
- 创建抖音用户信息管理页面,包含列表展示和卡片布局
- 集成瀑布流组件用于用户信息展示
- 实现用户头像、昵称、直播间等信息的显示功能
- 添加用户信息的新增、删除、导出等操作功能
- 创建相应的API接口和服务端点
- 移除原有的测试页面和相关代码
- 更新面包屑和汉堡菜单的颜色样式配置
- 调整布局样式以匹配新的主题色彩方案
JX.Li 3 weeks ago
parent
commit
b9c76ef8b5
27 changed files with 8538 additions and 862 deletions
  1. 7511 0
      config/geta_b.js
  2. 5 0
      nexo-common/nexo-common-core/pom.xml
  3. 2 0
      nexo-common/nexo-common-core/src/main/java/com/nexo/common/core/web/domain/BaseEntity.java
  4. 4 0
      nexo-example/nexo-model/pom.xml
  5. 102 0
      nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/controller/NexoDouyinUserInfoController.java
  6. 123 0
      nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/domain/NexoDouyinUserInfo.java
  7. 14 0
      nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/mapper/NexoDouyinUserInfoMapper.java
  8. 47 0
      nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/service/INexoDouyinUserInfoService.java
  9. 161 0
      nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/service/impl/NexoDouyinUserInfoServiceImpl.java
  10. 26 0
      nexo-example/nexo-model/src/main/resources/mapper/douyin/NexoDouyinUserInfoMapper.xml
  11. 1 1
      nexo-example/nexo-model/src/test/java/com/nexo/model/douyin/抖音测试工具类.java
  12. 3 3
      nexo-modules/nexo-gen/src/main/resources/vm/java/controller.java.vm
  13. 1 1
      nexo-modules/nexo-gen/src/main/resources/vm/java/service.java.vm
  14. 6 5
      nexo-modules/nexo-gen/src/main/resources/vm/java/serviceImpl.java.vm
  15. 1 0
      nexo-ui/package.json
  16. 0 54
      nexo-ui/src/api/demo/demo.js
  17. 0 44
      nexo-ui/src/api/demo/tree.js
  18. 44 0
      nexo-ui/src/api/douyin/douyinUserInfo.js
  19. 1 0
      nexo-ui/src/assets/icons/svg/delete.svg
  20. 13 0
      nexo-ui/src/assets/styles/nexo.scss
  21. 3 2
      nexo-ui/src/assets/styles/variables.scss
  22. 4 4
      nexo-ui/src/components/Breadcrumb/index.vue
  23. 9 1
      nexo-ui/src/components/Hamburger/index.vue
  24. 15 1
      nexo-ui/src/layout/index.vue
  25. 0 432
      nexo-ui/src/views/demo/demo/index.vue
  26. 0 314
      nexo-ui/src/views/demo/tree/index.vue
  27. 442 0
      nexo-ui/src/views/module/douyin/douyinUserInfo/index.vue

File diff suppressed because it is too large
+ 7511 - 0
config/geta_b.js


+ 5 - 0
nexo-common/nexo-common-core/pom.xml

@@ -112,6 +112,11 @@
             <artifactId>hutool-http</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>easyexcel</artifactId>
+        </dependency>
+
         <dependency>
             <groupId>cn.hutool</groupId>
             <artifactId>hutool-extra</artifactId>

+ 2 - 0
nexo-common/nexo-common-core/src/main/java/com/nexo/common/core/web/domain/BaseEntity.java

@@ -1,5 +1,6 @@
 package com.nexo.common.core.web.domain;
 
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
 import com.baomidou.mybatisplus.annotation.FieldFill;
 import com.baomidou.mybatisplus.annotation.TableField;
 import com.fasterxml.jackson.annotation.JsonIgnore;
@@ -18,6 +19,7 @@ import java.util.Map;
  */
 
 @Data
+@ExcelIgnoreUnannotated
 public class BaseEntity implements Serializable {
 
     private static final long serialVersionUID = 1L;

+ 4 - 0
nexo-example/nexo-model/pom.xml

@@ -65,6 +65,10 @@
             <artifactId>nexo-common-dubbo</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>com.nexo</groupId>
+            <artifactId>nexo-common-dict</artifactId>
+        </dependency>
 
         <dependency>
             <groupId>com.nexo</groupId>

+ 102 - 0
nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/controller/NexoDouyinUserInfoController.java

@@ -0,0 +1,102 @@
+package com.nexo.model.douyin.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import com.nexo.common.core.domain.R;
+import com.nexo.common.core.validate.AddGroup;
+import com.nexo.common.core.validate.EditGroup;
+import com.nexo.common.core.web.controller.BaseController;
+import com.nexo.common.excel.utils.ExcelUtil;
+import com.nexo.common.log.annotation.Log;
+import com.nexo.common.log.enums.BusinessType;
+import com.nexo.common.mybatis.core.page.PageQuery;
+import com.nexo.common.mybatis.core.page.TableDataInfo;
+import com.nexo.model.douyin.domain.NexoDouyinUserInfo;
+import com.nexo.model.douyin.service.INexoDouyinUserInfoService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * 抖音用户信息控制器
+ * 前端访问路由地址为:/douyin/douyinUserInfo
+ *
+ * @author nexo
+ * @date 2026-05-21
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/douyin/userInfo")
+public class NexoDouyinUserInfoController extends BaseController {
+
+    private final INexoDouyinUserInfoService iNexoDouyinUserInfoService;
+
+    /**
+     * 查询抖音用户信息列表
+     */
+    @SaCheckPermission("douyin:douyinUserInfo:list")
+    @GetMapping("/list")
+    public TableDataInfo<NexoDouyinUserInfo> list(NexoDouyinUserInfo item, PageQuery pageQuery) {
+        return iNexoDouyinUserInfoService.queryPageList(item, pageQuery);
+    }
+
+    /**
+     * 导出抖音用户信息列表
+     */
+    @SaCheckPermission("douyin:douyinUserInfo:export")
+    @Log(title = "抖音用户信息", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    public void export(NexoDouyinUserInfo item, HttpServletResponse response) {
+        List<NexoDouyinUserInfo> list = iNexoDouyinUserInfoService.queryList(item);
+        ExcelUtil.exportExcel(list, "抖音用户信息", NexoDouyinUserInfo.class, response);
+    }
+
+    /**
+     * 获取抖音用户信息详细信息
+     *
+     * @param userId 主键
+     */
+    @SaCheckPermission("douyin:douyinUserInfo:query")
+    @GetMapping("/{userId}")
+    public R<NexoDouyinUserInfo> getInfo(@NotNull(message = "主键不能为空") @PathVariable String userId) {
+        return R.ok(iNexoDouyinUserInfoService.queryById(userId));
+    }
+
+    /**
+     * 新增抖音用户信息
+     */
+    @SaCheckPermission("douyin:douyinUserInfo:add")
+    @Log(title = "抖音用户信息", businessType = BusinessType.INSERT)
+    @PostMapping()
+    public R<Void> add(@Validated(AddGroup.class) @RequestBody NexoDouyinUserInfo item) {
+        return toAjax(iNexoDouyinUserInfoService.insertBy(item));
+    }
+
+    /**
+     * 修改抖音用户信息
+     */
+    @SaCheckPermission("douyin:douyinUserInfo:edit")
+    @Log(title = "抖音用户信息", businessType = BusinessType.UPDATE)
+    @PutMapping()
+    public R<Void> edit(@Validated(EditGroup.class) @RequestBody NexoDouyinUserInfo item) {
+        return toAjax(iNexoDouyinUserInfoService.updateBy(item));
+    }
+
+    /**
+     * 删除抖音用户信息
+     *
+     * @param userIds 主键串
+     */
+    @SaCheckPermission("douyin:douyinUserInfo:remove")
+    @Log(title = "抖音用户信息", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{userIds}")
+    public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable String[] userIds) {
+        return toAjax(iNexoDouyinUserInfoService.deleteWithValidByIds(Arrays.asList(userIds), true));
+    }
+}

+ 123 - 0
nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/domain/NexoDouyinUserInfo.java

@@ -0,0 +1,123 @@
+package com.nexo.model.douyin.domain;
+
+import com.alibaba.excel.annotation.ExcelProperty;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.nexo.common.core.web.domain.BaseEntity;
+import com.nexo.common.excel.annotation.ExcelDictFormat;
+import com.nexo.common.excel.convert.ExcelDictConvert;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+
+/**
+ * 抖音用户信息对象 nexo_douyin_user_info
+ *
+ * @author nexo
+ * @date 2026-05-21
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("nexo_douyin_user_info")
+public class NexoDouyinUserInfo extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+
+    /**
+     * 用户id
+     */
+    @TableId(value = "user_id")
+    @ExcelProperty(value = "用户id")
+    private String userId;
+
+    /**
+     *
+     */
+    @ExcelProperty(value = "")
+    private String secUid;
+
+    /**
+     * 抖音号
+     */
+    @ExcelProperty(value = "抖音号")
+    private String uniqueId;
+
+    /**
+     * 昵称
+     */
+    @ExcelProperty(value = "昵称")
+    private String nickname;
+
+    /**
+     * 签名
+     */
+    @ExcelProperty(value = "签名")
+    private String signature;
+
+    /**
+     * 归属地
+     */
+    @ExcelProperty(value = "归属地")
+    private String ipLocation;
+
+    /**
+     * 头像
+     */
+    @ExcelProperty(value = "头像")
+    private String avatar;
+
+    /**
+     * 直播状态
+     */
+    @ExcelProperty(value = "直播状态", converter = ExcelDictConvert.class)
+    @ExcelDictFormat(readConverterExp = "0=未直播,1=直播中")
+    private Long liveStatus;
+
+    /**
+     * 年龄
+     */
+    @ExcelProperty(value = "年龄")
+    private Long userAge;
+
+    /**
+     * 获赞
+     */
+    @ExcelProperty(value = "获赞")
+    private Long totalFavorited;
+
+    /**
+     * 粉丝
+     */
+    @ExcelProperty(value = "粉丝")
+    private Long followerCount;
+
+    /**
+     * 关注
+     */
+    @ExcelProperty(value = "关注")
+    private Long followingCount;
+
+    /**
+     * 作品数量
+     */
+    @ExcelProperty(value = "作品数量")
+    private Long awemeCount;
+
+    /**
+     * 资料背景
+     */
+    @ExcelProperty(value = "资料背景")
+    private String coverUrl;
+
+    /**
+     * 直播间ID
+     */
+    @ExcelProperty(value = "直播间ID")
+    private String roomId;
+
+    @TableField(exist = false)
+    private String userUrl;
+
+}

+ 14 - 0
nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/mapper/NexoDouyinUserInfoMapper.java

@@ -0,0 +1,14 @@
+package com.nexo.model.douyin.mapper;
+
+import com.nexo.common.mybatis.core.mapper.BaseMapperPlus;
+import com.nexo.model.douyin.domain.NexoDouyinUserInfo;
+
+/**
+ * 抖音用户信息Mapper接口
+ *
+ * @author nexo
+ * @date 2026-05-21
+ */
+public interface NexoDouyinUserInfoMapper extends BaseMapperPlus<NexoDouyinUserInfoMapper, NexoDouyinUserInfo, NexoDouyinUserInfo> {
+
+}

+ 47 - 0
nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/service/INexoDouyinUserInfoService.java

@@ -0,0 +1,47 @@
+package com.nexo.model.douyin.service;
+
+import com.nexo.common.mybatis.core.page.PageQuery;
+import com.nexo.common.mybatis.core.page.TableDataInfo;
+import com.nexo.model.douyin.domain.NexoDouyinUserInfo;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 抖音用户信息Service接口
+ *
+ * @author nexo
+ * @date 2026-05-21
+ */
+public interface INexoDouyinUserInfoService {
+
+    /**
+     * 查询抖音用户信息
+     */
+    NexoDouyinUserInfo queryById(String userId);
+
+    /**
+     * 查询抖音用户信息列表
+     */
+    TableDataInfo<NexoDouyinUserInfo> queryPageList(NexoDouyinUserInfo item, PageQuery pageQuery);
+
+    /**
+     * 查询抖音用户信息列表
+     */
+    List<NexoDouyinUserInfo> queryList(NexoDouyinUserInfo item);
+
+    /**
+     * 修改抖音用户信息
+     */
+    Boolean insertBy(NexoDouyinUserInfo item);
+
+    /**
+     * 修改抖音用户信息
+     */
+    Boolean updateBy(NexoDouyinUserInfo item);
+
+    /**
+     * 校验并批量删除抖音用户信息信息
+     */
+    Boolean deleteWithValidByIds(Collection<String> ids, Boolean isValid);
+}

+ 161 - 0
nexo-example/nexo-model/src/main/java/com/nexo/model/douyin/service/impl/NexoDouyinUserInfoServiceImpl.java

@@ -0,0 +1,161 @@
+package com.nexo.model.douyin.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.nexo.common.core.utils.OkHttpClientUtils;
+import com.nexo.common.core.utils.StringUtils;
+import com.nexo.common.mybatis.core.page.PageQuery;
+import com.nexo.common.mybatis.core.page.TableDataInfo;
+import com.nexo.model.douyin.domain.NexoDouyinUserInfo;
+import com.nexo.model.douyin.mapper.NexoDouyinUserInfoMapper;
+import com.nexo.model.douyin.service.INexoDouyinUserInfoService;
+import com.nexo.model.douyin.utils.DouYinUtils;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 抖音用户信息Service业务层处理
+ *
+ * @author nexo
+ * @date 2026-05-21
+ */
+@RequiredArgsConstructor
+@Service
+@Slf4j
+public class NexoDouyinUserInfoServiceImpl implements INexoDouyinUserInfoService {
+
+    private final NexoDouyinUserInfoMapper baseMapper;
+
+    /**
+     * 查询抖音用户信息
+     */
+    @Override
+    public NexoDouyinUserInfo queryById(String userId) {
+        return baseMapper.selectById(userId);
+    }
+
+    /**
+     * 查询抖音用户信息列表
+     */
+    @Override
+    public TableDataInfo<NexoDouyinUserInfo> queryPageList(NexoDouyinUserInfo item, PageQuery pageQuery) {
+        LambdaQueryWrapper<NexoDouyinUserInfo> lqw = buildQueryWrapper(item);
+        Page<NexoDouyinUserInfo> result = baseMapper.selectPage(pageQuery.build(), lqw);
+        return TableDataInfo.build(result);
+    }
+
+    /**
+     * 查询抖音用户信息列表
+     */
+    @Override
+    public List<NexoDouyinUserInfo> queryList(NexoDouyinUserInfo item) {
+        LambdaQueryWrapper<NexoDouyinUserInfo> lqw = buildQueryWrapper(item);
+        return baseMapper.selectList(lqw);
+    }
+
+    private LambdaQueryWrapper<NexoDouyinUserInfo> buildQueryWrapper(NexoDouyinUserInfo item) {
+        Map<String, Object> params = item.getParams();
+        LambdaQueryWrapper<NexoDouyinUserInfo> lqw = Wrappers.lambdaQuery();
+        lqw.eq(StringUtils.isNotBlank(item.getSecUid()), NexoDouyinUserInfo::getSecUid, item.getSecUid());
+        lqw.eq(StringUtils.isNotBlank(item.getUniqueId()), NexoDouyinUserInfo::getUniqueId, item.getUniqueId());
+        lqw.like(StringUtils.isNotBlank(item.getNickname()), NexoDouyinUserInfo::getNickname, item.getNickname());
+        lqw.eq(StringUtils.isNotBlank(item.getSignature()), NexoDouyinUserInfo::getSignature, item.getSignature());
+        lqw.eq(StringUtils.isNotBlank(item.getIpLocation()), NexoDouyinUserInfo::getIpLocation, item.getIpLocation());
+        lqw.eq(StringUtils.isNotBlank(item.getAvatar()), NexoDouyinUserInfo::getAvatar, item.getAvatar());
+        lqw.eq(item.getLiveStatus() != null, NexoDouyinUserInfo::getLiveStatus, item.getLiveStatus());
+        lqw.eq(item.getUserAge() != null, NexoDouyinUserInfo::getUserAge, item.getUserAge());
+        lqw.eq(item.getTotalFavorited() != null, NexoDouyinUserInfo::getTotalFavorited, item.getTotalFavorited());
+        lqw.eq(item.getFollowerCount() != null, NexoDouyinUserInfo::getFollowerCount, item.getFollowerCount());
+        lqw.eq(item.getFollowingCount() != null, NexoDouyinUserInfo::getFollowingCount, item.getFollowingCount());
+        lqw.eq(item.getAwemeCount() != null, NexoDouyinUserInfo::getAwemeCount, item.getAwemeCount());
+        lqw.eq(StringUtils.isNotBlank(item.getCoverUrl()), NexoDouyinUserInfo::getCoverUrl, item.getCoverUrl());
+        lqw.eq(StringUtils.isNotBlank(item.getRoomId()), NexoDouyinUserInfo::getRoomId, item.getRoomId());
+        return lqw;
+    }
+
+    /**
+     * 新增抖音用户信息
+     */
+    @Override
+    public Boolean insertBy(NexoDouyinUserInfo item) {
+        String string = DouYinUtils.getRegexString(item.getUserUrl(), "[a-zA-z]+://[^\\s]*", 0);
+        String done = OkHttpClientUtils.doGetFollowRedirects(string, null);
+        String mid = item.getUserUrl();
+        if (string.contains("v.douyin.com")) {
+            try {
+                mid = URLDecoder.decode(StringUtils.extractMiddleText(done, "\"", "\""), "UTF-8");
+            } catch (UnsupportedEncodingException e) {
+                throw new RuntimeException(e);
+            }
+        }
+        String sec_user_id = DouYinUtils.getRegexString(mid, "user/(.*?)\\?", 0);
+        sec_user_id = StringUtils.extractMiddleText(sec_user_id, "user/", "?");
+        String params = "?device_platform=webapp&aid=6383&channel=channel_pc_web&publish_video_strategy_type=2&source=channel_pc_web&sec_user_id=" + sec_user_id + "&personal_center_strategy=1&profile_other_record_enable=1&land_to=1&update_version_code=170400&pc_client_type=1&pc_libra_divert=Windows&support_h265=0&support_dash=1&cpu_core_num=24&version_code=170400&version_name=17.4.0&cookie_enabled=true&screen_width=1920&screen_height=1080&browser_language=zh-CN&browser_platform=Win32&browser_name=Edge&browser_version=148.0.0.0&browser_online=true&engine_name=Blink&engine_version=148.0.0.0&os_name=Windows&os_version=10&device_memory=32&platform=PC&downlink=10&effective_type=4g&round_trip_time=50&webid=7639313678160397875";
+        String aBogus = DouYinUtils.getABogus(params);
+        String url = "https://www.douyin.com/aweme/v1/web/user/profile/other/" + params + "&a_bogus=" + aBogus + "&verifyFp=verify_mp4xmi43_R2ZADT9W_IKha_4C4e_9b3a_ctEUtT2EsPOe&fp=verify_mp4xmi43_R2ZADT9W_IKha_4C4e_9b3a_ctEUtT2EsPOe";
+        HashMap<String, String> header = DouYinUtils.getHeader();
+        String reqStr = OkHttpClientUtils.doGet(url, null, header);
+        // 解析json
+        try {
+            JSONObject reqJson = JSONObject.parseObject(reqStr);
+            item.setUserId(reqJson.getJSONObject("user").getString("uid"));
+            item.setSecUid(reqJson.getJSONObject("user").getString("sec_uid"));
+            item.setUniqueId(reqJson.getJSONObject("user").getString("unique_id"));
+            item.setNickname(reqJson.getJSONObject("user").getString("nickname"));
+            item.setSignature(reqJson.getJSONObject("user").getString("signature"));
+            item.setIpLocation(reqJson.getJSONObject("user").getString("ip_location"));
+            item.setAvatar(reqJson.getJSONObject("user").getJSONObject("avatar_medium").getJSONArray("url_list").getString(0));
+            item.setLiveStatus(reqJson.getJSONObject("user").getLong("live_status"));
+            item.setUserAge(reqJson.getJSONObject("user").getLong("user_age"));
+            item.setTotalFavorited(reqJson.getJSONObject("user").getLong("total_favorited"));
+            item.setFollowerCount(reqJson.getJSONObject("user").getLong("follower_count"));
+            item.setFollowingCount(reqJson.getJSONObject("user").getLong("following_count"));
+            item.setAwemeCount(reqJson.getJSONObject("user").getLong("aweme_count"));
+            item.setCoverUrl(reqJson.getJSONObject("user").getJSONArray("cover_url").getJSONObject(0).getJSONArray("url_list").getString(0));
+            item.setRoomId(reqJson.getJSONObject("user").getString("room_id"));
+        } catch (Exception e) {
+            throw new RuntimeException("查询用户信息时解析失败");
+        }
+        Boolean flag = baseMapper.insert(item) > 0;
+        if (flag) {
+            item.setUserId(item.getUserId());
+        }
+        return flag;
+    }
+
+    /**
+     * 修改抖音用户信息
+     */
+    @Override
+    public Boolean updateBy(NexoDouyinUserInfo item) {
+        return baseMapper.updateById(item) > 0;
+    }
+
+    /**
+     * 保存前的数据校验
+     */
+    private void validEntityBeforeSave(NexoDouyinUserInfo entity) {
+        //TODO 做一些数据校验,如唯一约束
+    }
+
+    /**
+     * 批量删除抖音用户信息
+     */
+    @Override
+    public Boolean deleteWithValidByIds(Collection<String> ids, Boolean isValid) {
+        if (isValid) {
+            //TODO 做一些业务上的校验,判断是否需要校验
+        }
+        return baseMapper.deleteBatchIds(ids) > 0;
+    }
+}

+ 26 - 0
nexo-example/nexo-model/src/main/resources/mapper/douyin/NexoDouyinUserInfoMapper.xml

@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.nexo.model.douyin.mapper.NexoDouyinUserInfoMapper">
+
+    <resultMap type="com.nexo.model.douyin.domain.NexoDouyinUserInfo" id="NexoDouyinUserInfoResult">
+        <result property="userId" column="user_id"/>
+        <result property="secUid" column="sec_uid"/>
+        <result property="uniqueId" column="unique_id"/>
+        <result property="nickname" column="nickname"/>
+        <result property="signature" column="signature"/>
+        <result property="ipLocation" column="ip_location"/>
+        <result property="avatar" column="avatar"/>
+        <result property="liveStatus" column="live_status"/>
+        <result property="userAge" column="user_age"/>
+        <result property="totalFavorited" column="total_favorited"/>
+        <result property="followerCount" column="follower_count"/>
+        <result property="followingCount" column="following_count"/>
+        <result property="awemeCount" column="aweme_count"/>
+        <result property="coverUrl" column="cover_url"/>
+        <result property="roomId" column="room_id"/>
+    </resultMap>
+
+
+</mapper>

+ 1 - 1
nexo-example/nexo-model/src/test/java/com/nexo/model/douyin/抖音测试工具类.java

@@ -108,7 +108,7 @@ public class 抖音测试工具类 {
 
     @Test
     public void 获取用户信息() {
-        String 分享连接 = "0- 长按复制此条消息,打开抖音搜索,查看TA的更多作品。 https://v.douyin.com/iz6uY8RfrJ8/ 7@5.com :2pm";
+        String 分享连接 = "长按复制此条消息,打开抖音搜索,查看TA的更多作品。 https://v.douyin.com/MQHBwAkCYew/";
         String string = DouYinUtils.getRegexString(分享连接, "[a-zA-z]+://[^\\s]*", 0);
         String done = OkHttpClientUtils.doGetFollowRedirects(string, null);
         String mid = null;

+ 3 - 3
nexo-modules/nexo-gen/src/main/resources/vm/java/controller.java.vm

@@ -83,7 +83,7 @@ public class ${ClassName}Controller extends BaseController {
     @SaCheckPermission("${permissionPrefix}:add")
     @Log(title = "${functionName}", businessType = BusinessType.INSERT)
     @PostMapping()
-    public R<id> add(@Validated(AddGroup.class) @Requestdy ${ClassName} item) {
+    public R<Void> add(@Validated(AddGroup.class) @RequestBody ${ClassName} item) {
         return toAjax(i${ClassName}Service.insertBy (item));
     }
 
@@ -93,7 +93,7 @@ public class ${ClassName}Controller extends BaseController {
     @SaCheckPermission("${permissionPrefix}:edit")
     @Log(title = "${functionName}", businessType = BusinessType.UPDATE)
     @PutMapping()
-    public R<id> edit(@Validated(EditGroup.class) @Requestdy ${ClassName} item) {
+    public R<Void> edit(@Validated(EditGroup.class) @RequestBody ${ClassName} item) {
         return toAjax(i${ClassName}Service.updateBy (item));
     }
 
@@ -105,7 +105,7 @@ public class ${ClassName}Controller extends BaseController {
     @SaCheckPermission("${permissionPrefix}:remove")
     @Log(title = "${functionName}", businessType = BusinessType.DELETE)
     @DeleteMapping("/{${pkColumn.javaField}s}")
-    public R<id> remove(@NotEmpty(message = "主键不能为空") @PathVariable ${pkColumn.javaType}[] ${pkColumn.javaField}s) {
+    public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable ${pkColumn.javaType}[] ${pkColumn.javaField}s) {
         return toAjax(i${ClassName}Service.deleteWithValidByIds(Arrays.asList(${pkColumn.javaField}s), true));
     }
 }

+ 1 - 1
nexo-modules/nexo-gen/src/main/resources/vm/java/service.java.vm

@@ -47,5 +47,5 @@ public interface I${ClassName}Service {
     /**
      * 校验并批量删除${functionName}信息
      */
-    Boolean deleteWithValidByIds(Collection<${pkColumn.javaType}> ids, olean isValid);
+    Boolean deleteWithValidByIds(Collection<${pkColumn.javaType}> ids, Boolean isValid);
 }

+ 6 - 5
nexo-modules/nexo-gen/src/main/resources/vm/java/serviceImpl.java.vm

@@ -2,6 +2,7 @@ package ${packageName}.service.impl;
 
 import cn.hutool.core.bean.BeanUtil;
     #if($table.crud || $table.sub)
+import com.nexo.common.core.utils.StringUtils;
 import com.nexo.common.mybatis.core.page.PageQuery;
 import com.nexo.common.mybatis.core.page.TableDataInfo;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
@@ -74,9 +75,9 @@ public class ${ClassName}ServiceImpl implements I${ClassName}Service {
 #set($mpMethod=$column.queryType.toLowerCase())
 #if($queryType != 'BETWEEN')
 #if($javaType == 'String')
-#set($condition='StringUtils.isNotBlank(bo.get'+$AttrName+'())')
+#set($condition='StringUtils.isNotBlank(item.get'+$AttrName+'())')
 #else
-#set($condition='bo.get'+$AttrName+'() != null')
+#set($condition='item.get'+$AttrName+'() != null')
 #end
         lqw.$mpMethod($condition, ${ClassName}::get$AttrName, item.get$AttrName());
 #else
@@ -92,8 +93,8 @@ public class ${ClassName}ServiceImpl implements I${ClassName}Service {
      * 新增${functionName}
      */
     @Override
-    public olean insertBy(${ClassName} item) {
-        itemolean flag = baseMapper.insert(item) > 0;
+    public Boolean insertBy(${ClassName} item) {
+        Boolean flag = baseMapper.insert(item) > 0;
 #set($pk=$pkColumn.javaField.substring(0,1).toUpperCase() + ${pkColumn.javaField.substring(1)})
         if (flag) {
             item.set$pk(item.get$pk());
@@ -120,7 +121,7 @@ public class ${ClassName}ServiceImpl implements I${ClassName}Service {
      * 批量删除${functionName}
      */
     @Override
-    public Boolean deleteWithValidByIds(Collection<${pkColumn.javaType}> ids, olean isValid) {
+    public Boolean deleteWithValidByIds(Collection<${pkColumn.javaType}> ids, Boolean isValid) {
         if(isValid){
             //TODO 做一些业务上的校验,判断是否需要校验
         }

+ 1 - 0
nexo-ui/package.json

@@ -56,6 +56,7 @@
     "vue-cropper": "0.5.5",
     "vue-meta": "2.4.0",
     "vue-router": "3.4.9",
+    "vue-waterfall-plugin": "^3.3.2",
     "vuedraggable": "2.24.3",
     "vuex": "3.6.0"
   },

+ 0 - 54
nexo-ui/src/api/demo/demo.js

@@ -1,54 +0,0 @@
-import request from '@/utils/request'
-
-// 查询测试单表列表
-export function listDemo(query) {
-  return request({
-    url: '/demo/demo/demo/list',
-    method: 'get',
-    params: query
-  })
-}
-
-// 自定义分页接口
-export function pageDemo(query) {
-  return request({
-    url: '/demo/demo/demo/page',
-    method: 'get',
-    params: query
-  })
-}
-
-// 查询测试单表详细
-export function getDemo(id) {
-  return request({
-    url: '/demo/demo/demo/' + id,
-    method: 'get'
-  })
-}
-
-// 新增测试单表
-export function addDemo(data) {
-  return request({
-    url: '/demo/demo/demo',
-    method: 'post',
-    data: data
-  })
-}
-
-// 修改测试单表
-export function updateDemo(data) {
-  return request({
-    url: '/demo/demo/demo',
-    method: 'put',
-    data: data
-  })
-}
-
-// 删除测试单表
-export function delDemo(id) {
-  return request({
-    url: '/demo/demo/demo/' + id,
-    method: 'delete'
-  })
-}
-

+ 0 - 44
nexo-ui/src/api/demo/tree.js

@@ -1,44 +0,0 @@
-import request from '@/utils/request'
-
-// 查询测试树表列表
-export function listTree(query) {
-  return request({
-    url: '/demo/demo/tree/list',
-    method: 'get',
-    params: query
-  })
-}
-
-// 查询测试树表详细
-export function getTree(id) {
-  return request({
-    url: '/demo/demo/tree/' + id,
-    method: 'get'
-  })
-}
-
-// 新增测试树表
-export function addTree(data) {
-  return request({
-    url: '/demo/demo/tree',
-    method: 'post',
-    data: data
-  })
-}
-
-// 修改测试树表
-export function updateTree(data) {
-  return request({
-    url: '/demo/demo/tree',
-    method: 'put',
-    data: data
-  })
-}
-
-// 删除测试树表
-export function delTree(id) {
-  return request({
-    url: '/demo/demo/tree/' + id,
-    method: 'delete'
-  })
-}

+ 44 - 0
nexo-ui/src/api/douyin/douyinUserInfo.js

@@ -0,0 +1,44 @@
+import request from '@/utils/request'
+
+// 查询抖音用户信息列表
+export function listDouyinUserInfo(query) {
+  return request({
+    url: '/model/douyin/userInfo/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询抖音用户信息详细
+export function getDouyinUserInfo(userId) {
+  return request({
+    url: '/model/douyin/userInfo/' + userId,
+    method: 'get'
+  })
+}
+
+// 新增抖音用户信息
+export function addDouyinUserInfo(data) {
+  return request({
+    url: '/model/douyin/userInfo',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改抖音用户信息
+export function updateDouyinUserInfo(data) {
+  return request({
+    url: '/model/douyin/userInfo',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除抖音用户信息
+export function delDouyinUserInfo(userId) {
+  return request({
+    url: '/model/douyin/userInfo/' + userId,
+    method: 'delete'
+  })
+}

+ 1 - 0
nexo-ui/src/assets/icons/svg/delete.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1779420816373" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4772" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M920 242.82H768v-75.45C768 128 736.66 96 698 96H326c-38.66 0-70 32-70 71.37v75.45H104c-22.09 0-40 18.26-40 40.79 0 22.52 17.91 40.78 40 40.78h88v532.24c0 39.42 31.34 71.37 70 71.37h500c38.66 0 70-32 70-71.37V324.39h88c22.09 0 40-18.26 40-40.78 0-22.53-17.91-40.79-40-40.79z m-584-24.47c0-22.52 17.91-40.78 40-40.78h272c22.09 0 40 18.26 40 40.78v24.47H336z m416 587.3c0 22.52-17.91 40.78-40 40.78H312c-22.09 0-40-18.26-40-40.78V324.39h480z" fill="" p-id="4773"></path><path d="M360 424a40 40 0 0 0-40 40v240a40 40 0 0 0 80 0V464a40 40 0 0 0-40-40zM512 424a40 40 0 0 0-40 40v240a40 40 0 0 0 80 0V464a40 40 0 0 0-40-40zM624 464v240a40 40 0 0 0 80 0V464a40 40 0 0 0-80 0z" fill="" p-id="4774"></path></svg>

+ 13 - 0
nexo-ui/src/assets/styles/nexo.scss

@@ -4,6 +4,19 @@
 */
 
 /** 基础通用 **/
+.fs12 {
+  font-size: 12px;
+}
+.fs14 {
+  font-size: 14px;
+}
+.fs16 {
+  font-size: 16px;
+}
+.fs18 {
+  font-size: 18px;
+}
+
 .pt5 {
   padding-top: 5px;
 }

+ 3 - 2
nexo-ui/src/assets/styles/variables.scss

@@ -12,7 +12,8 @@ $panGreen: #30B08F;
 $base-menu-color: #ffffff80;
 $base-menu-color-active: #0138c6;
 $base-menu-background: #002786;
-$base-logo-title-color: #ffffff;
+
+$base-logo-title-color: #b7cff7;
 
 $base-menu-light-color:rgba(0,0,0,.70);
 $base-menu-light-background:#ffffff;
@@ -51,5 +52,5 @@ $base-sidebar-width: 200px;
   subMenuHover: $base-sub-menu-hover;
   sideBarWidth: $base-sidebar-width;
   logoTitleColor: $base-logo-title-color;
-  logoLightTitleColor: $base-logo-light-title-color
+  logoLightTitleColor: $base-logo-light-title-color // 废弃
 }

+ 4 - 4
nexo-ui/src/components/Breadcrumb/index.vue

@@ -68,11 +68,11 @@ export default {
   margin-left: 8px;
 
   a, .no-redirect {
-    color: #888c93 !important;
-    cursor: text;
+    color: #b7cff7 !important;
   }
 }
-::v-deep .el-breadcrumb__separator{
-  color: #888c93 !important;
+
+::v-deep .el-breadcrumb__separator {
+  color: #b7cff7 !important;
 }
 </style>

+ 9 - 1
nexo-ui/src/components/Hamburger/index.vue

@@ -3,7 +3,7 @@
     <svg
       :class="{'is-active':isActive}"
       class="hamburger"
-      fill="#888c93"
+      :fill="variables.logoTitleColor"
       viewBox="0 0 1024 1024"
       xmlns="http://www.w3.org/2000/svg"
       width="64"
@@ -15,6 +15,8 @@
 </template>
 
 <script>
+import variables from '@/assets/styles/variables.scss'
+
 export default {
   name: 'Hamburger',
   props: {
@@ -23,6 +25,12 @@ export default {
       default: false
     }
   },
+
+  computed: {
+    variables() {
+      return variables
+    },
+  },
   methods: {
     toggleClick() {
       this.$emit('toggleClick')

+ 15 - 1
nexo-ui/src/layout/index.vue

@@ -102,7 +102,7 @@ export default {
       position: absolute;
       left: -10px;
       top: 40px;
-      z-index: 100;
+      z-index: 1;
       width: 20px;
       height: 20px;
       background-color: #002786;
@@ -111,6 +111,20 @@ export default {
       transform: scaleX(1) translateZ(0);
     }
 
+    &::before {
+      content: "";
+      position: absolute;
+      right: -10px;
+      top: 40px;
+      z-index: 1;
+      width: 20px;
+      height: 20px;
+      background-color: $base-logo-title-color;
+      mask: radial-gradient(circle at 100% 100%, transparent 10px, #f1f1ff 11px);
+      backface-visibility: hidden;
+      transform: scaleX(-1) translateZ(0);
+    }
+
 
   }
 

+ 0 - 432
nexo-ui/src/views/demo/demo/index.vue

@@ -1,432 +0,0 @@
-<template>
-  <div class="app-container">
-    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
-      <el-form-item label="key键" prop="testKey">
-        <el-input
-          v-model="queryParams.testKey"
-          placeholder="请输入key键"
-          clearable
-          size="small"
-          @keyup.enter.native="handleQuery"
-        />
-      </el-form-item>
-      <el-form-item label="值" prop="value">
-        <el-input
-          v-model="queryParams.value"
-          placeholder="请输入值"
-          clearable
-          size="small"
-          @keyup.enter.native="handleQuery"
-        />
-      </el-form-item>
-      <el-form-item label="创建时间">
-        <el-date-picker
-          v-model="daterangeCreateTime"
-          size="small"
-          style="width: 240px"
-          value-format="yyyy-MM-dd HH:mm:ss"
-          type="daterange"
-          range-separator="-"
-          start-placeholder="开始日期"
-          end-placeholder="结束日期"
-          :default-time="['00:00:00', '23:59:59']"
-        ></el-date-picker>
-      </el-form-item>
-      <el-form-item>
-        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
-        <el-button type="primary" icon="el-icon-search" size="mini" @click="handlePage">搜索(自定义分页接口)</el-button>
-        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
-      </el-form-item>
-    </el-form>
-
-    <el-row :gutter="10" class="mb8">
-      <el-col :span="1.5">
-        <el-button
-          type="primary"
-          plain
-          icon="el-icon-plus"
-          size="mini"
-          @click="handleAdd"
-          v-hasPermi="['demo:demo:add']"
-        >新增</el-button>
-      </el-col>
-      <el-col :span="1.5">
-        <el-button
-          type="success"
-          plain
-          icon="el-icon-edit"
-          size="mini"
-          :disabled="single"
-          @click="handleUpdate"
-          v-hasPermi="['demo:demo:edit']"
-        >修改</el-button>
-      </el-col>
-      <el-col :span="1.5">
-        <el-button
-          type="danger"
-          plain
-          icon="el-icon-delete"
-          size="mini"
-          :disabled="multiple"
-          @click="handleDelete"
-          v-hasPermi="['demo:demo:remove']"
-        >删除</el-button>
-      </el-col>
-      <el-col :span="1.5">
-        <el-button
-          type="info"
-          plain
-          icon="el-icon-upload2"
-          size="mini"
-          @click="handleImport"
-          v-hasPermi="['demo:demo:import']"
-        >导入(校验)</el-button>
-      </el-col>
-      <el-col :span="1.5">
-        <el-button
-          type="warning"
-          plain
-          icon="el-icon-download"
-          size="mini"
-          @click="handleExport"
-          v-hasPermi="['demo:demo:export']"
-        >导出</el-button>
-      </el-col>
-      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
-    </el-row>
-
-    <el-table v-loading="loading" :data="demoList" @selection-change="handleSelectionChange">
-      <el-table-column type="selection" width="55" align="center" />
-      <el-table-column label="主键" align="center" prop="id" v-if="false"/>
-      <el-table-column label="部门id" align="center" prop="deptId" />
-      <el-table-column label="用户id" align="center" prop="userId" />
-      <el-table-column label="排序号" align="center" prop="orderNum" />
-      <el-table-column label="key键" align="center" prop="testKey" />
-      <el-table-column label="值" align="center" prop="value" />
-      <el-table-column label="创建时间" align="center" prop="createTime" width="180">
-        <template slot-scope="scope">
-          <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="创建人" align="center" prop="createBy" />
-      <el-table-column label="更新时间" align="center" prop="updateTime" width="180">
-        <template slot-scope="scope">
-          <span>{{ parseTime(scope.row.updateTime, '{y}-{m}-{d}') }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="更新人" align="center" prop="updateBy" />
-      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
-        <template slot-scope="scope">
-          <el-button
-            size="mini"
-            type="text"
-            icon="el-icon-edit"
-            @click="handleUpdate(scope.row)"
-            v-hasPermi="['demo:demo:edit']"
-          >修改</el-button>
-          <el-button
-            size="mini"
-            type="text"
-            icon="el-icon-delete"
-            @click="handleDelete(scope.row)"
-            v-hasPermi="['demo:demo:remove']"
-          >删除</el-button>
-        </template>
-      </el-table-column>
-    </el-table>
-
-    <pagination
-      v-show="total>0"
-      :total="total"
-      :page.sync="queryParams.pageNum"
-      :limit.sync="queryParams.pageSize"
-      @pagination="getList"
-    />
-
-    <!-- 添加或修改测试单表对话框 -->
-    <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
-      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
-        <el-form-item label="部门id" prop="deptId">
-          <el-input v-model="form.deptId" placeholder="请输入部门id" />
-        </el-form-item>
-        <el-form-item label="用户id" prop="userId">
-          <el-input v-model="form.userId" placeholder="请输入用户id" />
-        </el-form-item>
-        <el-form-item label="排序号" prop="orderNum">
-          <el-input v-model="form.orderNum" placeholder="请输入排序号" />
-        </el-form-item>
-        <el-form-item label="key键" prop="testKey">
-          <el-input v-model="form.testKey" placeholder="请输入key键" />
-        </el-form-item>
-        <el-form-item label="值" prop="value">
-          <el-input v-model="form.value" placeholder="请输入值" />
-        </el-form-item>
-        <el-form-item label="创建时间" prop="createTime">
-          <el-date-picker clearable size="small"
-                          v-model="form.createTime"
-                          type="datetime"
-                          value-format="yyyy-MM-dd HH:mm:ss"
-                          placeholder="选择创建时间">
-          </el-date-picker>
-        </el-form-item>
-      </el-form>
-      <div slot="footer" class="dialog-footer">
-        <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
-        <el-button @click="cancel">取 消</el-button>
-      </div>
-    </el-dialog>
-    <!-- 用户导入对话框 -->
-    <el-dialog :title="upload.title" :visible.sync="upload.open" width="400px" append-to-body>
-      <el-upload
-        ref="upload"
-        :limit="1"
-        accept=".xlsx, .xls"
-        :headers="upload.headers"
-        :action="upload.url + '?updateSupport=' + upload.updateSupport"
-        :disabled="upload.isUploading"
-        :on-progress="handleFileUploadProgress"
-        :on-success="handleFileSuccess"
-        :auto-upload="false"
-        drag
-      >
-        <i class="el-icon-upload"></i>
-        <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
-      </el-upload>
-      <div slot="footer" class="dialog-footer">
-        <el-button type="primary" @click="submitFileForm">确 定</el-button>
-        <el-button @click="upload.open = false">取 消</el-button>
-      </div>
-    </el-dialog>
-  </div>
-</template>
-
-<script>
-import { listDemo, pageDemo, getDemo, delDemo, addDemo, updateDemo } from "@/api/demo/demo";
-import {getToken} from "@/utils/auth";
-
-export default {
-  name: "Demo",
-  components: {
-  },
-  data() {
-    return {
-      //按钮loading
-      buttonLoading: false,
-      // 遮罩层
-      loading: true,
-      // 选中数组
-      ids: [],
-      // 非单个禁用
-      single: true,
-      // 非多个禁用
-      multiple: true,
-      // 显示搜索条件
-      showSearch: true,
-      // 总条数
-      total: 0,
-      // 测试单表表格数据
-      demoList: [],
-      // 弹出层标题
-      title: "",
-      // 是否显示弹出层
-      open: false,
-      // 创建时间时间范围
-      daterangeCreateTime: [],
-      // 用户导入参数
-      upload: {
-        // 是否显示弹出层(用户导入)
-        open: false,
-        // 弹出层标题(用户导入)
-        title: "",
-        // 是否禁用上传
-        isUploading: false,
-        // 设置上传的请求头部
-        headers: { Authorization: "Bearer " + getToken() },
-        // 上传的地址
-        url: process.env.VUE_APP_BASE_API + "/demo/demo/importData"
-      },
-      // 查询参数
-      queryParams: {
-        pageNum: 1,
-        pageSize: 10,
-        testKey: undefined,
-        value: undefined,
-        createTime: undefined,
-      },
-      // 表单参数
-      form: {},
-      // 表单校验
-      rules: {
-        testKey: [
-          { required: true, message: "key键不能为空", trigger: "blur" }
-        ],
-        value: [
-          { required: true, message: "值不能为空", trigger: "blur" }
-        ],
-      }
-    };
-  },
-  created() {
-    this.getList();
-  },
-  methods: {
-    /** 查询测试单表列表 */
-    getList() {
-      this.loading = true;
-      this.queryParams.params = {};
-      if (null != this.daterangeCreateTime && '' != this.daterangeCreateTime) {
-        this.queryParams.params["beginCreateTime"] = this.daterangeCreateTime[0];
-        this.queryParams.params["endCreateTime"] = this.daterangeCreateTime[1];
-      }
-      listDemo(this.queryParams).then(response => {
-        this.demoList = response.rows;
-        this.total = response.total;
-        this.loading = false;
-      });
-    },
-    /** 自定义分页查询 */
-    getPage() {
-      this.loading = true;
-      this.queryParams.params = {};
-      if (null != this.daterangeCreateTime && '' != this.daterangeCreateTime) {
-        this.queryParams.params["beginCreateTime"] = this.daterangeCreateTime[0];
-        this.queryParams.params["endCreateTime"] = this.daterangeCreateTime[1];
-      }
-      pageDemo(this.queryParams).then(response => {
-        this.demoList = response.rows;
-        this.total = response.total;
-        this.loading = false;
-      });
-    },
-    // 取消按钮
-    cancel() {
-      this.open = false;
-      this.reset();
-    },
-    // 表单重置
-    reset() {
-      this.form = {
-        id: undefined,
-        deptId: undefined,
-        userId: undefined,
-        orderNum: undefined,
-        testKey: undefined,
-        value: undefined,
-        version: undefined,
-        createTime: undefined,
-        createBy: undefined,
-        updateTime: undefined,
-        updateBy: undefined,
-        delFlag: undefined
-      };
-      this.resetForm("form");
-    },
-    /** 搜索按钮操作 */
-    handleQuery() {
-      this.queryParams.pageNum = 1;
-      this.getList();
-    },
-    /** 搜索按钮操作 */
-    handlePage() {
-      this.queryParams.pageNum = 1;
-      this.getPage();
-    },
-    /** 重置按钮操作 */
-    resetQuery() {
-      this.daterangeCreateTime = [];
-      this.resetForm("queryForm");
-      this.handleQuery();
-    },
-    // 多选框选中数据
-    handleSelectionChange(selection) {
-      this.ids = selection.map(item => item.id)
-      this.single = selection.length!==1
-      this.multiple = !selection.length
-    },
-    /** 新增按钮操作 */
-    handleAdd() {
-      this.reset();
-      this.open = true;
-      this.title = "添加测试单表";
-    },
-    /** 修改按钮操作 */
-    handleUpdate(row) {
-      this.loading = true;
-      this.reset();
-      const id = row.id || this.ids
-      getDemo(id).then(response => {
-        this.loading = false;
-        this.form = response.data;
-        this.open = true;
-        this.title = "修改测试单表";
-      });
-    },
-    /** 提交按钮 */
-    submitForm() {
-      this.$refs["form"].validate(valid => {
-        if (valid) {
-          this.buttonLoading = true;
-          if (this.form.id != null) {
-            updateDemo(this.form).then(response => {
-              this.$modal.msgSuccess("修改成功");
-              this.open = false;
-              this.getList();
-            }).finally(() => {
-              this.buttonLoading = false;
-            });
-          } else {
-            addDemo(this.form).then(response => {
-              this.$modal.msgSuccess("新增成功");
-              this.open = false;
-              this.getList();
-            }).finally(() => {
-              this.buttonLoading = false;
-            });
-          }
-        }
-      });
-    },
-    /** 删除按钮操作 */
-    handleDelete(row) {
-      const ids = row.id || this.ids;
-      this.$modal.confirm('是否确认删除测试单表编号为"' + ids + '"的数据项?').then(() => {
-        this.loading = true;
-        return delDemo(ids);
-      }).then(() => {
-        this.loading = false;
-        this.getList();
-        this.$modal.msgSuccess("删除成功");
-      }).finally(() => {
-        this.loading = false;
-      });
-    },
-    /** 导入按钮操作 */
-    handleImport() {
-      this.upload.title = "用户导入";
-      this.upload.open = true;
-    },
-    /** 导出按钮操作 */
-    handleExport() {
-      this.download('demo/demo/demo/export', {
-        ...this.queryParams
-      }, `demo_${new Date().getTime()}.xlsx`)
-    },
-    // 文件上传中处理
-    handleFileUploadProgress(event, file, fileList) {
-      this.upload.isUploading = true;
-    },
-    // 文件上传成功处理
-    handleFileSuccess(response, file, fileList) {
-      this.upload.open = false;
-      this.upload.isUploading = false;
-      this.$refs.upload.clearFiles();
-      this.$alert(response.msg, "导入结果", { dangerouslyUseHTMLString: true });
-      this.getList();
-    },
-    // 提交上传文件
-    submitFileForm() {
-      this.$refs.upload.submit();
-    }
-  }
-};
-</script>

+ 0 - 314
nexo-ui/src/views/demo/tree/index.vue

@@ -1,314 +0,0 @@
-<template>
-  <div class="app-container">
-    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
-      <el-form-item label="树节点名" prop="treeName">
-        <el-input
-          v-model="queryParams.treeName"
-          placeholder="请输入树节点名"
-          clearable
-          size="small"
-          @keyup.enter.native="handleQuery"
-        />
-      </el-form-item>
-      <el-form-item label="创建时间">
-        <el-date-picker
-          v-model="daterangeCreateTime"
-          size="small"
-          style="width: 240px"
-          value-format="yyyy-MM-dd HH:mm:ss"
-          type="daterange"
-          range-separator="-"
-          start-placeholder="开始日期"
-          end-placeholder="结束日期"
-          :default-time="['00:00:00', '23:59:59']"
-        ></el-date-picker>
-      </el-form-item>
-      <el-form-item>
-        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
-        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
-      </el-form-item>
-    </el-form>
-
-    <el-row :gutter="10" class="mb8">
-      <el-col :span="1.5">
-        <el-button
-          type="primary"
-          plain
-          icon="el-icon-plus"
-          size="mini"
-          @click="handleAdd"
-          v-hasPermi="['demo:tree:add']"
-        >新增</el-button>
-      </el-col>
-      <el-col :span="1.5">
-        <el-button
-          type="info"
-          plain
-          icon="el-icon-sort"
-          size="mini"
-          @click="toggleExpandAll"
-        >展开/折叠</el-button>
-      </el-col>
-      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
-    </el-row>
-
-    <el-table
-      v-if="refreshTable"
-      v-loading="loading"
-      :data="treeList"
-      row-key="id"
-      :default-expand-all="isExpandAll"
-      :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
-    >
-      <el-table-column label="父id" prop="parentId" />
-      <el-table-column label="部门id" align="center" prop="deptId" />
-      <el-table-column label="用户id" align="center" prop="userId" />
-      <el-table-column label="树节点名" align="center" prop="treeName" />
-      <el-table-column label="创建时间" align="center" prop="createTime" width="180">
-        <template #default="scope">
-          <span>{{ parseTime(scope.row.createTime, '{y}-{m}-{d}') }}</span>
-        </template>
-      </el-table-column>
-      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
-        <template #default="scope">
-          <el-button
-            size="mini"
-            type="text"
-            icon="el-icon-edit"
-            @click="handleUpdate(scope.row)"
-            v-hasPermi="['demo:tree:edit']"
-          >修改</el-button>
-          <el-button
-            size="mini"
-            type="text"
-            icon="el-icon-plus"
-            @click="handleAdd(scope.row)"
-            v-hasPermi="['demo:tree:add']"
-          >新增</el-button>
-          <el-button
-            size="mini"
-            type="text"
-            icon="el-icon-delete"
-            @click="handleDelete(scope.row)"
-            v-hasPermi="['demo:tree:remove']"
-          >删除</el-button>
-        </template>
-      </el-table-column>
-    </el-table>
-
-    <!-- 添加或修改测试树表对话框 -->
-    <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
-      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
-        <el-form-item label="父id" prop="parentId">
-          <treeselect v-model="form.parentId" :options="treeOptions" :normalizer="normalizer" placeholder="请选择父id" />
-        </el-form-item>
-        <el-form-item label="部门id" prop="deptId">
-          <el-input v-model="form.deptId" placeholder="请输入部门id" />
-        </el-form-item>
-        <el-form-item label="用户id" prop="userId">
-          <el-input v-model="form.userId" placeholder="请输入用户id" />
-        </el-form-item>
-        <el-form-item label="树节点名" prop="treeName">
-          <el-input v-model="form.treeName" placeholder="请输入树节点名" />
-        </el-form-item>
-      </el-form>
-      <div slot="footer" class="dialog-footer">
-        <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
-        <el-button @click="cancel">取 消</el-button>
-      </div>
-    </el-dialog>
-  </div>
-</template>
-
-<script>
-import { listTree, getTree, delTree, addTree, updateTree } from "@/api/demo/tree";
-import Treeselect from "@riophae/vue-treeselect";
-import "@riophae/vue-treeselect/dist/vue-treeselect.css";
-
-export default {
-  name: "Tree",
-  components: {
-    Treeselect
-  },
-  data() {
-    return {
-      //按钮loading
-      buttonLoading: false,
-      // 遮罩层
-      loading: true,
-      // 显示搜索条件
-      showSearch: true,
-      // 测试树表表格数据
-      treeList: [],
-      // 测试树表树选项
-      treeOptions: [],
-      // 弹出层标题
-      title: "",
-      // 是否显示弹出层
-      open: false,
-      // 是否展开,默认全部展开
-      isExpandAll: true,
-      // 重新渲染表格状态
-      refreshTable: true,
-      // 创建时间时间范围
-      daterangeCreateTime: [],
-      // 查询参数
-      queryParams: {
-        treeName: null,
-        createTime: null,
-      },
-      // 表单参数
-      form: {},
-      // 表单校验
-      rules: {
-        treeName: [
-          { required: true, message: "树节点名不能为空", trigger: "blur" }
-        ],
-      }
-    };
-  },
-  created() {
-    this.getList();
-  },
-  methods: {
-    /** 查询测试树表列表 */
-    getList() {
-      this.loading = true;
-      this.queryParams.params = {};
-      if (null != this.daterangeCreateTime && '' != this.daterangeCreateTime) {
-        this.queryParams.params["beginCreateTime"] = this.daterangeCreateTime[0];
-        this.queryParams.params["endCreateTime"] = this.daterangeCreateTime[1];
-      }
-      listTree(this.queryParams).then(response => {
-        this.treeList = this.handleTree(response.data, "id", "parentId");
-        this.loading = false;
-      });
-    },
-    /** 转换测试树表数据结构 */
-    normalizer(node) {
-      if (node.children && !node.children.length) {
-        delete node.children;
-      }
-      return {
-        id: node.id,
-        label: node.treeName,
-        children: node.children
-      };
-    },
-    /** 查询测试树表下拉树结构 */
-    getTreeselect() {
-      listTree().then(response => {
-        this.treeOptions = [];
-        const data = { id: 0, treeName: '顶级节点', children: [] };
-        data.children = this.handleTree(response.data, "id", "parentId");
-        this.treeOptions.push(data);
-      });
-    },
-    // 取消按钮
-    cancel() {
-      this.open = false;
-      this.reset();
-    },
-    // 表单重置
-    reset() {
-      this.form = {
-        id: null,
-        parentId: null,
-        deptId: null,
-        userId: null,
-        treeName: null,
-        version: null,
-        createTime: null,
-        createBy: null,
-        updateTime: null,
-        updateBy: null,
-        delFlag: null
-      };
-      this.resetForm("form");
-    },
-    /** 搜索按钮操作 */
-    handleQuery() {
-      this.getList();
-    },
-    /** 重置按钮操作 */
-    resetQuery() {
-      this.daterangeCreateTime = [];
-      this.resetForm("queryForm");
-      this.handleQuery();
-    },
-    /** 新增按钮操作 */
-    handleAdd(row) {
-      this.reset();
-      this.getTreeselect();
-      if (row != null && row.id) {
-        this.form.parentId = row.id;
-      } else {
-        this.form.parentId = 0;
-      }
-      this.open = true;
-      this.title = "添加测试树表";
-    },
-    /** 展开/折叠操作 */
-    toggleExpandAll() {
-      this.refreshTable = false;
-      this.isExpandAll = !this.isExpandAll;
-      this.$nextTick(() => {
-        this.refreshTable = true;
-      });
-    },
-    /** 修改按钮操作 */
-    handleUpdate(row) {
-      this.loading = true;
-      this.reset();
-      this.getTreeselect();
-      if (row != null) {
-        this.form.parentId = row.id;
-      }
-      getTree(row.id).then(response => {
-        this.loading = false;
-        this.form = response.data;
-        this.open = true;
-        this.title = "修改测试树表";
-      });
-    },
-    /** 提交按钮 */
-    submitForm() {
-      this.$refs["form"].validate(valid => {
-        if (valid) {
-          this.buttonLoading = true;
-          if (this.form.id != null) {
-            updateTree(this.form).then(response => {
-              this.$modal.msgSuccess("修改成功");
-              this.open = false;
-              this.getList();
-            }).finally(() => {
-              this.buttonLoading = false;
-            });
-          } else {
-            addTree(this.form).then(response => {
-              this.$modal.msgSuccess("新增成功");
-              this.open = false;
-              this.getList();
-            }).finally(() => {
-              this.buttonLoading = false;
-            });
-          }
-        }
-      });
-    },
-    /** 删除按钮操作 */
-    handleDelete(row) {
-      this.$modal.confirm('是否确认删除测试树表编号为"' + row.id + '"的数据项?').then(() => {
-        this.loading = true;
-        return delTree(row.id);
-      }).then(() => {
-        this.loading = false;
-        this.getList();
-        this.$modal.msgSuccess("删除成功");
-      }).finally(() => {
-        this.loading = false;
-      });
-    }
-  }
-};
-</script>

+ 442 - 0
nexo-ui/src/views/module/douyin/douyinUserInfo/index.vue

@@ -0,0 +1,442 @@
+<template>
+  <div class="app-container">
+    <el-card shadow="never">
+      <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
+        <el-form-item label="SecUid" prop="secUid">
+          <el-input
+            v-model="queryParams.secUid"
+            placeholder="请输入SecUid"
+            clearable
+            @keyup.enter.native="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item label="抖音号" prop="uniqueId">
+          <el-input
+            v-model="queryParams.uniqueId"
+            placeholder="请输入抖音号"
+            clearable
+            @keyup.enter.native="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item label="昵称" prop="nickname">
+          <el-input
+            v-model="queryParams.nickname"
+            placeholder="请输入昵称"
+            clearable
+            @keyup.enter.native="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item label="直播间ID" prop="roomId">
+          <el-input
+            v-model="queryParams.roomId"
+            placeholder="请输入直播间ID"
+            clearable
+            @keyup.enter.native="handleQuery"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+          <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+    <el-card shadow="never" class="mt10">
+      <el-row :gutter="10" class="mb8">
+        <el-col :span="1.5">
+          <el-button
+            type="primary"
+            plain
+            icon="el-icon-plus"
+            size="mini"
+            @click="handleAdd"
+            v-hasPermi="['douyin:douyinUserInfo:add']"
+          >新增
+          </el-button>
+        </el-col>
+        <el-col :span="1.5">
+          <el-button
+            type="warning"
+            plain
+            icon="el-icon-download"
+            size="mini"
+            @click="handleExport"
+            v-hasPermi="['douyin:douyinUserInfo:export']"
+          >导出
+          </el-button>
+        </el-col>
+        <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+      </el-row>
+      <Waterfall :list="douyinUserInfoList" :width="310" :gutter="12">
+        <template #item="{ item }">
+          <el-card shadow="never" :style="{ backgroundImage: `url(${item.coverUrl})` }" class="card-back">
+            <div class="cover">
+              <div class="delete-but" @click="handleDelete(item)">
+                <svg-icon icon-class="delete"/>
+              </div>
+              <div class="no_one">
+                <div>
+                  <el-image :src="item.avatar" :preview-src-list="[item.avatar]"
+                            style="width: 50px;border-radius: 50%" :class="item.liveStatus === 0 ?'':'live'"
+                  >
+                  </el-image>
+                  <span v-if="item.liveStatus === 1" class="live-text">直播中</span>
+
+                </div>
+                <div class="user-info">
+                  <span class="fs16" style="color: #333333">{{ item.nickname }}</span>
+                  <span class="fs12">抖音号:{{ item.uniqueId }}</span>
+                  <span class="fs12 disabled">{{ item.ipLocation === null ? 'IP属地:未知' : item.ipLocation }}</span>
+                </div>
+              </div>
+              <div class="mt10">
+                 <span
+                   class="fs12 disabled signature-text"
+                   :class="{ 'expanded': item.signatureExpanded }"
+                   v-html="item.signature === null ? '这个人很懒什么都没留下' : item.signature"
+                   @click="toggleSignature(item)"
+                 ></span>
+                <span
+                  v-if="(item.signature && item.signature.length > 50) || (item.signature === null && '这个人很懒什么都没留下'.length > 50)"
+                  class="expand-btn fs12"
+                  @click="toggleSignature(item)"
+                >
+                  {{ item.signatureExpanded ? '收起' : '展开' }}
+                </span>
+              </div>
+              <div class="mt10">
+                <div style="display: flex; gap: 10px;justify-content: space-around;">
+                  <span class="fs12 disabled user-info-count">作品<span>{{ item.awemeCount }}</span></span>
+                  <span class="fs12 disabled user-info-count">获赞<span>{{ item.totalFavorited }}</span></span>
+                  <span class="fs12 disabled user-info-count">关注<span>{{ item.followingCount }}</span></span>
+                  <span class="fs12 disabled user-info-count">粉丝<span>{{ item.followerCount }}</span></span>
+                </div>
+              </div>
+            </div>
+          </el-card>
+        </template>
+      </Waterfall>
+      <pagination
+        v-show="total>0"
+        :total="total"
+        :pageSizes="[50, 100, 150]"
+        :page.sync="queryParams.pageNum"
+        :limit.sync="queryParams.pageSize"
+        @pagination="getList"
+      />
+    </el-card>
+    <!-- 添加或修改抖音用户信息对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="用户链接" prop="userUrl">
+          <el-input type="textarea" v-model="form.userUrl" placeholder="请输入用户链接"/>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button :loading="buttonLoading" type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {
+  listDouyinUserInfo,
+  getDouyinUserInfo,
+  delDouyinUserInfo,
+  addDouyinUserInfo,
+  updateDouyinUserInfo
+} from '@/api/douyin/douyinUserInfo'
+
+import { LazyImg, Waterfall } from 'vue-waterfall-plugin'
+import 'vue-waterfall-plugin/dist/style.css'
+
+export default {
+  name: 'DouyinUserInfo',
+  components: { LazyImg, Waterfall }, // 组件列表
+  data() {
+    return {
+      // 按钮loading
+      buttonLoading: false,
+      // 遮罩层
+      loading: true,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 抖音用户信息表格数据
+      douyinUserInfoList: [],
+      // 弹出层标题
+      title: '',
+      // 是否显示弹出层
+      open: false,
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 50,
+        secUid: undefined,
+        uniqueId: undefined,
+        nickname: undefined,
+        signature: undefined,
+        ipLocation: undefined,
+        avatar: undefined,
+        liveStatus: undefined,
+        userAge: undefined,
+        totalFavorited: undefined,
+        followerCount: undefined,
+        followingCount: undefined,
+        awemeCount: undefined,
+        coverUrl: undefined,
+        roomId: undefined
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+        userUrl: [
+          { required: true, message: '用户链接不能为空', trigger: 'blur' }
+        ]
+      }
+    }
+  },
+  created() {
+    this.getList()
+  },
+  methods: {
+    /** 查询抖音用户信息列表 */
+    getList() {
+      this.loading = true
+      listDouyinUserInfo(this.queryParams).then(response => {
+        this.douyinUserInfoList = response.rows
+        this.total = response.total
+        this.loading = false
+      })
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false
+      this.reset()
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        userUrl: '长按复制此条消息,打开抖音搜索,查看TA的更多作品。 https://v.douyin.com/MQHBwAkCYew/'
+      }
+      this.resetForm('form')
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm('queryForm')
+      this.handleQuery()
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.userId)
+      this.single = selection.length !== 1
+      this.multiple = !selection.length
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset()
+      this.open = true
+      this.title = '添加抖音用户信息'
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.loading = true
+      this.reset()
+      const userId = row.userId || this.ids
+      getDouyinUserInfo(userId).then(response => {
+        this.loading = false
+        this.form = response.data
+        this.open = true
+        this.title = '修改抖音用户信息'
+      })
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs['form'].validate(valid => {
+        if (valid) {
+          this.buttonLoading = true
+          if (this.form.userId != null) {
+            updateDouyinUserInfo(this.form).then(response => {
+              this.$modal.msgSuccess('修改成功')
+              this.open = false
+              this.getList()
+            }).finally(() => {
+              this.buttonLoading = false
+            })
+          } else {
+            addDouyinUserInfo(this.form).then(response => {
+              this.$modal.msgSuccess('新增成功')
+              this.open = false
+              this.getList()
+            }).finally(() => {
+              this.buttonLoading = false
+            })
+          }
+        }
+      })
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const userIds = row.userId || this.ids
+      this.$modal.confirm('是否确认删除抖音用户信息为"' + row.nickname + '"的数据项?').then(() => {
+        this.loading = true
+        return delDouyinUserInfo(userIds)
+      }).then(() => {
+        this.loading = false
+        this.getList()
+        this.$modal.msgSuccess('删除成功')
+      }).catch(() => {
+      }).finally(() => {
+        this.loading = false
+      })
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      this.download('/model/douyin/userInfo/export', {
+        ...this.queryParams
+      }, `douyinUserInfo_${new Date().getTime()}.xlsx`)
+    },
+    /** 切换签名展开/收缩 */
+    toggleSignature(item) {
+      // 只有当签名内容较长时才允许展开/收缩
+      const signatureText = item.signature === null ? '这个人很懒什么都没留下' : item.signature
+      if (signatureText && signatureText.length > 50) {
+        this.$set(item, 'signatureExpanded', !item.signatureExpanded)
+      }
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+.card-back {
+  background: #304661;
+  background-size: contain;
+  background-repeat: no-repeat;
+  position: relative;
+  overflow: hidden;
+  border: #e5e5e5 solid 1px;
+
+  ::v-deep .el-card__body {
+    padding: 0 !important;
+  }
+
+  &::before {
+    content: '';
+    position: absolute;
+    top: 0;
+    right: 0;
+    width: 100%;
+    height: 100%;
+    background: #ffffff50;
+    z-index: 0;
+  }
+
+}
+
+.disabled {
+  user-select: none;
+}
+
+.cover {
+  border-radius: 2px;
+  padding: 15px 20px;
+  z-index: 2;
+  backdrop-filter: blur(5px);
+  color: #33333390;
+}
+
+.no_one {
+  display: flex;
+  gap: 10px;
+
+  .user-info {
+    display: flex;
+    flex-direction: column;
+  }
+}
+
+.user-info-count {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+}
+
+.live {
+  border: 2px solid #fe346e;
+  animation: live-border-glow 2s ease-in-out infinite;
+}
+
+.live-text {
+  background: #fe346e;
+  color: #fff;
+  font-size: 10px;
+  padding: 1px 3px;
+  border-radius: 3px;
+  position: absolute;
+  top: 55px;
+  left: 27px;
+}
+
+@keyframes live-border-glow {
+  0% {
+    border-color: rgba(254, 52, 110, 1);
+    box-shadow: 0 0 0 0 rgba(254, 52, 110, 0.3);
+  }
+  70% {
+    border-color: rgba(254, 52, 110, 0.8);
+    box-shadow: 0 0 0 10px rgba(254, 52, 110, 0);
+  }
+  100% {
+    border-color: rgba(254, 52, 110, 1);
+    box-shadow: 0 0 0 0 rgba(254, 52, 110, 0);
+  }
+}
+
+.signature-text {
+  display: -webkit-box;
+  -webkit-line-clamp: 2;
+  -webkit-box-orient: vertical;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  cursor: pointer;
+
+  &.expanded {
+    -webkit-line-clamp: unset;
+    display: block;
+  }
+}
+
+.expand-btn {
+  color: #0138c6;
+  cursor: pointer;
+  margin-left: 5px;
+  user-select: none;
+
+  &:hover {
+    text-decoration: underline;
+  }
+}
+
+.delete-but {
+  position: absolute;
+  right: 10px;
+  top: 10px;
+  z-index: 1;
+  cursor: pointer;
+  color: #fe346e;
+}
+</style>

Some files were not shown because too many files changed in this diff