Prechádzať zdrojové kódy

Merge branch '特权藏品' into 特权合并

# Conflicts:
#	src/main/java/com/izouma/nineth/domain/Collection.java
licailing 3 rokov pred
rodič
commit
6bcbc2503f
53 zmenil súbory, kde vykonal 5599 pridanie a 851 odobranie
  1. 3 0
      src/main/java/com/izouma/nineth/domain/CollectionPrivilege.java
  2. 88 0
      src/main/java/com/izouma/nineth/domain/CompanyCollection.java
  3. 2 0
      src/main/java/com/izouma/nineth/domain/Setting.java
  4. 2 0
      src/main/java/com/izouma/nineth/domain/ShowCollection.java
  5. 17 4
      src/main/java/com/izouma/nineth/domain/Showroom.java
  6. 3 0
      src/main/java/com/izouma/nineth/domain/User.java
  7. 2 0
      src/main/java/com/izouma/nineth/dto/UserRegister.java
  8. 17 0
      src/main/java/com/izouma/nineth/enums/CollectionStatus.java
  9. 3 1
      src/main/java/com/izouma/nineth/repo/CollectionPrivilegeRepo.java
  10. 16 0
      src/main/java/com/izouma/nineth/repo/CompanyCollectionRepo.java
  11. 2 0
      src/main/java/com/izouma/nineth/repo/ShowCollectionRepo.java
  12. 2 1
      src/main/java/com/izouma/nineth/repo/ShowroomRepo.java
  13. 14 14
      src/main/java/com/izouma/nineth/service/AirDropService.java
  14. 8 8
      src/main/java/com/izouma/nineth/service/AssetService.java
  15. 88 0
      src/main/java/com/izouma/nineth/service/CompanyCollectionService.java
  16. 15 14
      src/main/java/com/izouma/nineth/service/OrderService.java
  17. 48 3
      src/main/java/com/izouma/nineth/service/ShowroomService.java
  18. 5 4
      src/main/java/com/izouma/nineth/service/UserService.java
  19. 84 0
      src/main/java/com/izouma/nineth/web/CompanyCollectionController.java
  20. 24 3
      src/main/java/com/izouma/nineth/web/ShowroomController.java
  21. 0 0
      src/main/resources/genjson/CompanyCollection.json
  22. 0 1
      src/main/resources/genjson/Showroom.json
  23. 9 2
      src/main/vue/src/components/ModelUpload.vue
  24. 1 1
      src/main/vue/src/components/RichText.vue
  25. 6 1
      src/main/vue/src/components/SingleUpload.vue
  26. 135 7
      src/main/vue/src/router.js
  27. 1 1
      src/main/vue/src/views/BannerList.vue
  28. 2 2
      src/main/vue/src/views/CollectionEdit.vue
  29. 796 0
      src/main/vue/src/views/CollectionRoomEdit.vue
  30. 257 0
      src/main/vue/src/views/CollectionRoomList.vue
  31. 269 0
      src/main/vue/src/views/CompanyCollFailList.vue
  32. 308 0
      src/main/vue/src/views/CompanyCollectionList.vue
  33. 470 0
      src/main/vue/src/views/CompanyCollectionShow.vue
  34. 242 0
      src/main/vue/src/views/CompanyEdit.vue
  35. 195 0
      src/main/vue/src/views/CompanyList.vue
  36. 1 1
      src/main/vue/src/views/Dashboard.vue
  37. 7 1
      src/main/vue/src/views/MenuAuthority.vue
  38. 1 1
      src/main/vue/src/views/Menus.vue
  39. 4 4
      src/main/vue/src/views/MinterList.vue
  40. 9 0
      src/main/vue/src/views/NewsList.vue
  41. 10 2
      src/main/vue/src/views/Settings.vue
  42. 96 691
      src/main/vue/src/views/ShowroomEdit.vue
  43. 144 81
      src/main/vue/src/views/ShowroomList.vue
  44. 244 0
      src/main/vue/src/views/company/CollectionFailList.vue
  45. 268 0
      src/main/vue/src/views/company/CollectionPassList.vue
  46. 244 0
      src/main/vue/src/views/company/CollectionPendingList.vue
  47. 463 0
      src/main/vue/src/views/company/CompanyCollectionEdit.vue
  48. 481 0
      src/main/vue/src/views/company/CompanyCollectionShelf.vue
  49. 293 0
      src/main/vue/src/views/company/CompanyRoomEdit.vue
  50. 189 0
      src/main/vue/src/views/company/CompanyRoomList.vue
  51. 1 1
      src/main/vue/src/widgets/PriceWidget.vue
  52. 1 1
      src/main/vue/src/widgets/TopWidget.vue
  53. 9 1
      src/test/java/com/izouma/nineth/service/AssetServiceTest.java

+ 3 - 0
src/main/java/com/izouma/nineth/domain/CollectionPrivilege.java

@@ -39,6 +39,9 @@ public class CollectionPrivilege extends BaseEntity {
     @ApiModelProperty("vip特权")
     private boolean vip;
 
+    @ApiModelProperty("企业藏品")
+    private Long companyCollectionId;
+
     public CollectionPrivilege(CollectionInfoDTO info, Long collectionId) {
         BeanUtils.copyProperties(info, this);
         this.collectionId = collectionId;

+ 88 - 0
src/main/java/com/izouma/nineth/domain/CompanyCollection.java

@@ -0,0 +1,88 @@
+package com.izouma.nineth.domain;
+
+import com.izouma.nineth.annotations.Searchable;
+import com.izouma.nineth.converter.FileObjectConverter;
+import com.izouma.nineth.converter.FileObjectListConverter;
+import com.izouma.nineth.converter.PrivilegeListConverter;
+import com.izouma.nineth.converter.PropertyListConverter;
+import com.izouma.nineth.enums.CollectionStatus;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.*;
+import java.math.BigDecimal;
+import java.util.List;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+@Entity
+@ApiModel("企业申请的藏品")
+public class CompanyCollection extends BaseEntity {
+    private Long userId;
+
+    @ApiModelProperty("铸造者")
+    private String minter;
+
+    @ApiModelProperty("名称")
+    @Searchable
+    private String name;
+
+    @ApiModelProperty("图片")
+    @Column(columnDefinition = "TEXT")
+    @Convert(converter = FileObjectListConverter.class)
+    private List<FileObject> pic;
+
+    @Column(columnDefinition = "TEXT")
+    @Convert(converter = FileObjectConverter.class)
+    private FileObject model3d;
+
+    @ApiModelProperty("详情")
+    @Column(columnDefinition = "TEXT")
+    private String detail;
+
+    @ApiModelProperty("分类")
+    private String category;
+
+    @ApiModelProperty("发行数量")
+    private int total;
+
+    @ApiModelProperty("价格")
+    @Column(precision = 10, scale = 2)
+    private BigDecimal price;
+
+    @ApiModelProperty("原价")
+    @Column(precision = 10, scale = 2)
+    private BigDecimal originalPrice;
+
+    @ApiModelProperty("限购数量")
+    private int maxCount;
+
+    @ApiModelProperty("限购识别码")
+    private String countId;
+
+    @Column(columnDefinition = "TEXT")
+    @Convert(converter = PropertyListConverter.class)
+    @ApiModelProperty("特性")
+    private List<CollectionProperty> properties;
+
+    @Column(columnDefinition = "TEXT")
+    @Convert(converter = PrivilegeListConverter.class)
+    @ApiModelProperty("特权")
+    private List<Privilege> privileges;
+
+    @ApiModelProperty("是否可转售")
+    private boolean canResale;
+
+    @ApiModelProperty("状态")
+    @Enumerated(EnumType.STRING)
+    private CollectionStatus status;
+
+    @ApiModelProperty("拒绝理由")
+    private String reason;
+}

+ 2 - 0
src/main/java/com/izouma/nineth/domain/Setting.java

@@ -26,6 +26,8 @@ public class Setting extends BaseEntity {
 
     private String description;
 
+    private String type;
+
     private Long parent;
 
     @Transient

+ 2 - 0
src/main/java/com/izouma/nineth/domain/ShowCollection.java

@@ -32,5 +32,7 @@ public class ShowCollection extends BaseEntity {
 
     private String pic;
 
+    private String name;
+
     private int sort;
 }

+ 17 - 4
src/main/java/com/izouma/nineth/domain/Showroom.java

@@ -1,5 +1,7 @@
 package com.izouma.nineth.domain;
 
+import com.izouma.nineth.enums.AuthStatus;
+import com.izouma.nineth.enums.CollectionStatus;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.AllArgsConstructor;
@@ -8,10 +10,7 @@ import lombok.Data;
 import lombok.NoArgsConstructor;
 import org.hibernate.annotations.Where;
 
-import javax.persistence.Entity;
-import javax.persistence.Index;
-import javax.persistence.Table;
-import javax.persistence.Transient;
+import javax.persistence.*;
 import java.util.List;
 
 @Data
@@ -54,9 +53,23 @@ public class Showroom extends BaseEntity {
     @ApiModelProperty("展厅背景")
     private String showroomBg;
 
+    private String type;
+
+    @Enumerated(EnumType.STRING)
+    private AuthStatus status;
+
+    @ApiModelProperty("拒绝理由")
+    private String reason;
+
+    @Column(columnDefinition = "tinyint unsigned")
+    private int sort;
+
+    private Long settingId;
+
     @Transient
     private List<ShowCollection> collections;
 
     @Transient
     private boolean liked;
+
 }

+ 3 - 0
src/main/java/com/izouma/nineth/domain/User.java

@@ -167,4 +167,7 @@ public class User extends BaseEntityNoID implements Serializable {
 
     @Column(columnDefinition = "tinyint unsigned default 0")
     private boolean canSale = false;
+
+    @Column(columnDefinition = "tinyint unsigned default 0")
+    private boolean company = false;
 }

+ 2 - 0
src/main/java/com/izouma/nineth/dto/UserRegister.java

@@ -53,4 +53,6 @@ public class UserRegister {
     private String intro;
 
     private boolean minter;
+
+    private boolean company;
 }

+ 17 - 0
src/main/java/com/izouma/nineth/enums/CollectionStatus.java

@@ -0,0 +1,17 @@
+package com.izouma.nineth.enums;
+
+public enum CollectionStatus {
+    PENDING("审核中"),
+    SUCCESS("通过"),
+    FAIL("失败");
+
+    private final String description;
+
+    CollectionStatus(String description) {
+        this.description = description;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+}

+ 3 - 1
src/main/java/com/izouma/nineth/repo/CollectionPrivilegeRepo.java

@@ -23,10 +23,12 @@ public interface CollectionPrivilegeRepo extends JpaRepository<CollectionPrivile
     @Query(value = "update collection_privilege c set c.collection_id = ?2, c.head_bg = ?3, c.max_collection = ?4, " +
             "c.showroom_bg = ?5, c.vip = ?6 where c.id = ?1", nativeQuery = true)
     @CacheEvict(value = {"collectionInfo"}, allEntries = true)
-    void update(@Nonnull Long id, Long collectionId, String headBg, Integer maxCollection, String showroomBg, Boolean vip);
+    void update(@Nonnull Long id, Long collectionId, String headBg, Integer maxCollection, String showroomBg, boolean vip);
 
     @Transactional
     @Modifying
     @CacheEvict(value = {"collectionInfo"}, allEntries = true)
     CollectionPrivilege save(CollectionPrivilege record);
+
+    CollectionPrivilege findByCompanyCollectionId(Long collectionId);
 }

+ 16 - 0
src/main/java/com/izouma/nineth/repo/CompanyCollectionRepo.java

@@ -0,0 +1,16 @@
+package com.izouma.nineth.repo;
+
+import com.izouma.nineth.domain.CompanyCollection;
+import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+
+import javax.transaction.Transactional;
+
+public interface CompanyCollectionRepo extends JpaRepository<CompanyCollection, Long>, JpaSpecificationExecutor<CompanyCollection> {
+    @Query("update CompanyCollection t set t.del = true where t.id = ?1")
+    @Modifying
+    @Transactional
+    void softDelete(Long id);
+}

+ 2 - 0
src/main/java/com/izouma/nineth/repo/ShowCollectionRepo.java

@@ -28,6 +28,8 @@ public interface ShowCollectionRepo extends JpaRepository<ShowCollection, Long>,
 
     List<ShowCollection> findAllByShowroomId(Long showroomId);
 
+    List<ShowCollection> findAllByShowroomIdOrderBySort(Long showroomId);
+
     List<ShowCollection> findAllByShowroomIdIn(Collection<Long> showroomId);
 
     @Query("update ShowCollection t set t.del = true where t.showroomId = ?1")

+ 2 - 1
src/main/java/com/izouma/nineth/repo/ShowroomRepo.java

@@ -22,7 +22,7 @@ public interface ShowroomRepo extends JpaRepository<Showroom, Long>, JpaSpecific
     @Transactional
     void addLike(Long id, int num);
 
-    Optional<Showroom> findByUserIdAndAssetId(Long userId, Long assetId);
+    Optional<Showroom> findByUserIdAndType(Long userId, String type);
 
     Optional<Showroom> findByAssetId(Long assetId);
 
@@ -33,4 +33,5 @@ public interface ShowroomRepo extends JpaRepository<Showroom, Long>, JpaSpecific
 
     @Cacheable(value = "showroom", key = "#id")
     Optional<Showroom> findById(@Nonnull Long id);
+
 }

+ 14 - 14
src/main/java/com/izouma/nineth/service/AirDropService.java

@@ -70,13 +70,13 @@ public class AirDropService {
                                     collection.getHoldDays());
                         } else {
                             //查看有无vip权限
-//                            CollectionPrivilege collectionPrivilege = collectionPrivilegeRepo.findByCollectionId(record.getCollectionId());
-//                            if (ObjectUtils.isNotEmpty(collectionPrivilege)) {
-//                                if (collectionPrivilege.isVip()) {
-//                                    //更新vip信息
-//                                    userRepo.updateVipPurchase(user.getId(), 1);
-//                                }
-//                            }
+                            CollectionPrivilege collectionPrivilege = collectionPrivilegeRepo.findByCollectionId(record.getCollectionId());
+                            if (ObjectUtils.isNotEmpty(collectionPrivilege)) {
+                                if (collectionPrivilege.isVip()) {
+                                    //更新vip信息
+                                    userRepo.updateVipPurchase(user.getId(), 1);
+                                }
+                            }
                             assetService.createAsset(winItem, user, null, null, "空投",
                                     collectionService.getNextNumber(winItem.getCollectionId()), collection.getHoldDays());
                         }
@@ -86,13 +86,13 @@ public class AirDropService {
                                     collection.getTotal() > 1 ? collectionService.getNextNumber(collection.getId()) : null);
                         } else {
                             //查看有无vip权限
-//                            CollectionPrivilege collectionPrivilege = collectionPrivilegeRepo.findByCollectionId(record.getCollectionId());
-//                            if (ObjectUtils.isNotEmpty(collectionPrivilege)) {
-//                                if (collectionPrivilege.isVip()) {
-//                                    //更新vip信息
-//                                    userRepo.updateVipPurchase(user.getId(), 1);
-//                                }
-//                            }
+                            CollectionPrivilege collectionPrivilege = collectionPrivilegeRepo.findByCollectionId(record.getCollectionId());
+                            if (ObjectUtils.isNotEmpty(collectionPrivilege)) {
+                                if (collectionPrivilege.isVip()) {
+                                    //更新vip信息
+                                    userRepo.updateVipPurchase(user.getId(), 1);
+                                }
+                            }
                             Asset asset = assetService.createAsset(collection, user, null, null, "空投", collectionService.getNextNumber(collection.getId()));
                             //创建展厅
                             if (collection.getType() == CollectionType.SHOWROOM) {

+ 8 - 8
src/main/java/com/izouma/nineth/service/AssetService.java

@@ -346,14 +346,14 @@ public class AssetService {
         }
 
         //vip权限转让
-//        CollectionPrivilege collectionPrivilege = collectionPrivilegeRepo.findByCollectionId(asset.getCollectionId());
-//        if (ObjectUtils.isNotEmpty(collectionPrivilege)) {
-//            if (collectionPrivilege.isVip()) {
-//                //更新vip信息
-//                userRepo.updateVipPurchase(toUser.getId(), 1);
-//                userRepo.updateVipPurchase(asset.getUserId(), 0);
-//            }
-//        }
+        CollectionPrivilege collectionPrivilege = collectionPrivilegeRepo.findByCollectionId(asset.getCollectionId());
+        if (ObjectUtils.isNotEmpty(collectionPrivilege)) {
+            if (collectionPrivilege.isVip()) {
+                //更新vip信息
+                userRepo.updateVipPurchase(toUser.getId(), 1);
+                userRepo.updateVipPurchase(asset.getUserId(), 0);
+            }
+        }
     }
 
     public List<TokenHistory> tokenHistory(String tokenId, Long assetId) {

+ 88 - 0
src/main/java/com/izouma/nineth/service/CompanyCollectionService.java

@@ -0,0 +1,88 @@
+package com.izouma.nineth.service;
+
+import com.izouma.nineth.domain.*;
+import com.izouma.nineth.dto.PageQuery;
+import com.izouma.nineth.enums.CollectionSource;
+import com.izouma.nineth.enums.CollectionStatus;
+import com.izouma.nineth.enums.CollectionType;
+import com.izouma.nineth.exception.BusinessException;
+import com.izouma.nineth.repo.*;
+import com.izouma.nineth.utils.JpaUtils;
+import lombok.AllArgsConstructor;
+import org.apache.commons.lang3.ObjectUtils;
+import org.springframework.beans.BeanUtils;
+import org.springframework.data.domain.Page;
+import org.springframework.stereotype.Service;
+
+@Service
+@AllArgsConstructor
+public class CompanyCollectionService {
+
+    private CompanyCollectionRepo   companyCollectionRepo;
+    private CollectionRepo          collectionRepo;
+    private UserRepo                userRepo;
+    private CollectionPrivilegeRepo collectionPrivilegeRepo;
+    private SysConfigService        sysConfigService;
+
+    public Page<CompanyCollection> all(PageQuery pageQuery) {
+        return companyCollectionRepo.findAll(JpaUtils.toSpecification(pageQuery, CompanyCollection.class), JpaUtils.toPageRequest(pageQuery));
+    }
+
+    public void audit(Long id, CollectionStatus status, String reason) {
+        CompanyCollection companyCollection = companyCollectionRepo.findById(id)
+                .orElseThrow(new BusinessException("无申请"));
+
+
+        if (!CollectionStatus.PENDING.equals(companyCollection.getStatus())) {
+            throw new BusinessException("已审核");
+        }
+
+        companyCollection.setStatus(status);
+        companyCollection.setReason(reason);
+
+        if (CollectionStatus.FAIL.equals(status)) {
+            //存状态
+            companyCollectionRepo.save(companyCollection);
+            return;
+        }
+
+        CollectionPrivilege privilege = collectionPrivilegeRepo.findByCompanyCollectionId(id);
+        if (ObjectUtils.isNotEmpty(privilege)) {
+            return;
+        }
+
+        Collection collection = new Collection();
+        BeanUtils.copyProperties(companyCollection, collection);
+        collection.setSource(CollectionSource.COMPANY);
+        collection.setType(CollectionType.DEFAULT);
+        collection.setStock(companyCollection.getTotal());
+
+        //铸造者/持有者
+        Long userId = companyCollection.getUserId();
+        User user = userRepo.findById(userId).orElseThrow(new BusinessException("无企业"));
+        collection.setMinter(user.getNickname());
+        collection.setMinterId(userId);
+        collection.setMinterAvatar(user.getAvatar());
+        collection.setOwner(user.getNickname());
+        collection.setOwnerId(userId);
+        collection.setOwnerAvatar(user.getAvatar());
+
+//        Showroom showroom = showroomRepo.findFirstByUserId(userId);
+        int royalties = sysConfigService.getInt("royalties");
+        int serviceCharge = sysConfigService.getInt("service_charge");
+        collection.setRoyalties(royalties);
+        collection.setServiceCharge(serviceCharge);
+        collection.setId(null);
+        collection = collectionRepo.save(collection);
+
+        //企业属性
+        collectionPrivilegeRepo.save(CollectionPrivilege.builder()
+                .collectionId(collection.getId())
+                .companyCollectionId(id)
+                .build());
+
+        //存状态
+        companyCollectionRepo.save(companyCollection);
+    }
+
+}

+ 15 - 14
src/main/java/com/izouma/nineth/service/OrderService.java

@@ -630,13 +630,14 @@ public class OrderService {
                     orderRepo.save(order);
 
                     //藏品其他信息/是否vip
-//                    CollectionPrivilege collectionPrivilege = collectionPrivilegeRepo.findByCollectionId(order.getCollectionId());
-//                    if (ObjectUtils.isNotEmpty(collectionPrivilege)) {
-//                        if (collectionPrivilege.isVip()) {
-//                            //更新vip信息
-//                            userRepo.updateVipPurchase(order.getUserId(), 1);
-//                        }
-//                    }
+                    CollectionPrivilege collectionPrivilege = collectionPrivilegeRepo.findByCollectionId(order.getCollectionId());
+                    if (ObjectUtils.isNotEmpty(collectionPrivilege)) {
+                        if (collectionPrivilege.isVip()) {
+                            //更新vip信息
+                            userRepo.updateVipPurchase(order.getUserId(), 1);
+                        }
+                    }
+
                     assetService.createAsset(winItem, user, order.getId(), order.getPrice(), "出售",
                             winItem.getTotal() > 1 ? collectionService.getNextNumber(winItem.getCollectionId()) : null,
                             collection.getHoldDays());
@@ -659,13 +660,13 @@ public class OrderService {
                     } else {
                         orderRepo.save(order);
                         //藏品其他信息/是否vip
-//                        CollectionPrivilege collectionPrivilege = collectionPrivilegeRepo.findByCollectionId(order.getCollectionId());
-//                        if (ObjectUtils.isNotEmpty(collectionPrivilege)) {
-//                            if (collectionPrivilege.isVip()) {
-//                                //更新vip信息
-//                                userRepo.updateVipPurchase(order.getUserId(), 1);
-//                            }
-//                        }
+                        CollectionPrivilege collectionPrivilege = collectionPrivilegeRepo.findByCollectionId(order.getCollectionId());
+                        if (ObjectUtils.isNotEmpty(collectionPrivilege)) {
+                            if (collectionPrivilege.isVip()) {
+                                //更新vip信息
+                                userRepo.updateVipPurchase(order.getUserId(), 1);
+                            }
+                        }
                         Asset asset = assetService.createAsset(collection, user, order.getId(), order.getPrice(), "出售",
                                 collection.getTotal() > 1 ? collectionService.getNextNumber(order.getCollectionId()) : null);
 

+ 48 - 3
src/main/java/com/izouma/nineth/service/ShowroomService.java

@@ -5,19 +5,17 @@ import com.izouma.nineth.domain.*;
 import com.izouma.nineth.domain.Collection;
 import com.izouma.nineth.dto.PageQuery;
 import com.izouma.nineth.enums.AssetStatus;
+import com.izouma.nineth.enums.AuthStatus;
 import com.izouma.nineth.enums.CollectionType;
 import com.izouma.nineth.exception.BusinessException;
 import com.izouma.nineth.repo.*;
 import com.izouma.nineth.utils.JpaUtils;
 import com.izouma.nineth.utils.ObjUtils;
 import lombok.AllArgsConstructor;
-import org.apache.commons.beanutils.BeanUtils;
 import org.apache.commons.lang3.ObjectUtils;
-import org.springframework.cache.annotation.CacheEvict;
 import org.springframework.data.domain.Page;
 import org.springframework.stereotype.Service;
 
-import javax.persistence.Id;
 import java.util.*;
 import java.util.stream.Collectors;
 
@@ -31,6 +29,8 @@ public class ShowroomService {
     private ShowCollectionRepo      showCollectionRepo;
     private CollectionPrivilegeRepo collectionPrivilegeRepo;
     private CacheService            cacheService;
+    private UserRepo                userRepo;
+    private SysConfigService        sysConfigService;
 
     public Page<Showroom> all(PageQuery pageQuery) {
         return showroomRepo.findAll(JpaUtils.toSpecification(pageQuery, Showroom.class), JpaUtils.toPageRequest(pageQuery));
@@ -124,6 +124,7 @@ public class ShowroomService {
                 .userId(asset.getUserId())
                 .assetId(asset.getId())
                 .nickname(asset.getOwner())
+                .type("USER")
                 .build();
         showroom = showroomRepo.save(showroom);
 
@@ -131,6 +132,34 @@ public class ShowroomService {
         return showroom;
     }
 
+    public Showroom save(Long userId) {
+        User user = userRepo.findByIdAndDelFalse(userId).orElseThrow(new BusinessException("无用户"));
+        if (!user.isCompany()) {
+            throw new BusinessException("无用户权限");
+        }
+
+        if (showroomRepo.findByUserIdAndType(user.getId(), "COMPANY").isPresent()) {
+            throw new BusinessException("已创建过展厅");
+        }
+        int maxCollection = sysConfigService.getInt("max_collection");
+
+        Showroom showroom = Showroom.builder()
+                .headBg("")
+                .showroomBg("")
+                .maxCollection(maxCollection)
+                .publish(false)
+                .userId(user.getId())
+                .nickname(user.getNickname())
+                .type("COMPANY")
+                .status(AuthStatus.NOT_AUTH)
+                .build();
+        showroom = showroomRepo.save(showroom);
+
+        cacheService.clearShowroom();
+        return showroom;
+    }
+
+
     public Showroom update(Long userId, Showroom showroom) {
         Showroom recordRoom = showroomRepo.findById(showroom.getId()).orElseThrow(new BusinessException("无展厅"));
         showroom.setMaxCollection(recordRoom.getMaxCollection());
@@ -190,7 +219,9 @@ public class ShowroomService {
                                 coll.setPic(pic.getThumb());
                             }
                             coll.setShowroomId(recordRoom.getId());
+                            // 可能没有
                             coll.setAssetId(collection.getAssetId());
+
                             showCollectionRepo.save(coll);
                         }
                     }
@@ -212,4 +243,18 @@ public class ShowroomService {
         return showroom;
     }
 
+    public void audit(Long id, AuthStatus status, String reason) {
+        Showroom showroom = showroomRepo.findById(id).orElseThrow(new BusinessException("无展厅"));
+        if (!"COMPANY".equals(showroom.getType())) {
+            // 非企业类型不需要审核
+            showroom.setStatus(null);
+            showroomRepo.save(showroom);
+            return;
+        }
+        showroom.setStatus(status);
+        showroom.setReason(reason);
+        showroomRepo.save(showroom);
+        cacheService.clearShowroom();
+    }
+
 }

+ 5 - 4
src/main/java/com/izouma/nineth/service/UserService.java

@@ -152,7 +152,8 @@ public class UserService {
                 if (roleName.equals("ROLE_MINTER")) {
                     and.add(criteriaBuilder.equal(root.get("minter"), true));
                 } else {
-                    and.add(criteriaBuilder.isMember(Authority.get(AuthorityName.valueOf(roleName)), root.get("authorities")));
+                    and.add(criteriaBuilder
+                            .isMember(Authority.get(AuthorityName.valueOf(roleName)), root.get("authorities")));
                 }
             }
 
@@ -516,7 +517,7 @@ public class UserService {
             throw new BusinessException("用户不存在或未认证");
         }
         String realName = identityAuthRepo.findFirstByUserIdAndStatusAndDelFalseOrderByCreatedAtDesc(
-                        user.getId(), AuthStatus.SUCCESS)
+                user.getId(), AuthStatus.SUCCESS)
                 .map(IdentityAuth::getRealName).orElse("").replaceAll(".*(?=.)", "**");
         Map<String, Object> map = new HashMap<>();
         map.put("id", user.getId());
@@ -529,8 +530,8 @@ public class UserService {
 
     public Map<String, Object> searchByPhoneAdmin(String phoneStr) {
         List<String> phone = Arrays.stream(phoneStr.replaceAll("\n", " ")
-                        .replaceAll("\r\n", " ")
-                        .split(" "))
+                .replaceAll("\r\n", " ")
+                .split(" "))
                 .map(String::trim)
                 .filter(s -> !StringUtils.isEmpty(s))
                 .collect(Collectors.toList());

+ 84 - 0
src/main/java/com/izouma/nineth/web/CompanyCollectionController.java

@@ -0,0 +1,84 @@
+package com.izouma.nineth.web;
+
+import com.izouma.nineth.domain.CompanyCollection;
+import com.izouma.nineth.enums.CollectionStatus;
+import com.izouma.nineth.service.CompanyCollectionService;
+import com.izouma.nineth.dto.PageQuery;
+import com.izouma.nineth.exception.BusinessException;
+import com.izouma.nineth.repo.CompanyCollectionRepo;
+import com.izouma.nineth.utils.ObjUtils;
+import com.izouma.nineth.utils.SecurityUtils;
+import com.izouma.nineth.utils.excel.ExcelUtils;
+import lombok.AllArgsConstructor;
+import org.springframework.data.domain.Page;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.List;
+
+@RestController
+@RequestMapping("/companyCollection")
+@AllArgsConstructor
+public class CompanyCollectionController extends BaseController {
+    private CompanyCollectionService companyCollectionService;
+    private CompanyCollectionRepo    companyCollectionRepo;
+
+    @PreAuthorize("hasAnyRole('ADMIN','COMPANY')")
+    @PostMapping("/save")
+    public CompanyCollection save(@RequestBody CompanyCollection record) {
+        if (record.getId() != null) {
+            CompanyCollection orig = companyCollectionRepo.findById(record.getId())
+                    .orElseThrow(new BusinessException("无记录"));
+            if (CollectionStatus.SUCCESS.equals(orig.getStatus())) {
+                throw new BusinessException("已通过申请,不可编辑");
+            }
+
+            ObjUtils.merge(orig, record);
+            return companyCollectionRepo.save(orig);
+        }
+        record.setUserId(SecurityUtils.getAuthenticatedUser().getId());
+        return companyCollectionRepo.save(record);
+    }
+
+
+    //@PreAuthorize("hasRole('ADMIN')")
+    @PostMapping("/all")
+    public Page<CompanyCollection> all(@RequestBody PageQuery pageQuery) {
+        return companyCollectionService.all(pageQuery);
+    }
+
+    @GetMapping("/get/{id}")
+    public CompanyCollection get(@PathVariable Long id) {
+        return companyCollectionRepo.findById(id).orElseThrow(new BusinessException("无记录"));
+    }
+
+    @PostMapping("/del/{id}")
+    public void del(@PathVariable Long id) {
+        companyCollectionRepo.softDelete(id);
+    }
+
+    @GetMapping("/excel")
+    @ResponseBody
+    public void excel(HttpServletResponse response, PageQuery pageQuery) throws IOException {
+        List<CompanyCollection> data = all(pageQuery).getContent();
+        ExcelUtils.export(response, data);
+    }
+
+    @PostMapping("/pass")
+    public void pass(@RequestParam Long id) {
+        companyCollectionService.audit(id, CollectionStatus.SUCCESS, null);
+    }
+
+    @PostMapping("/deny")
+    public void deny(@RequestParam Long id, String reason) {
+        companyCollectionService.audit(id, CollectionStatus.FAIL, reason);
+    }
+
+//    @PostMapping("/createDistrict")
+//    public void deny(@RequestParam Long id, String reason) {
+//        companyCollectionService.audit(id, CollectionStatus.FAIL, reason);
+//    }
+}
+

+ 24 - 3
src/main/java/com/izouma/nineth/web/ShowroomController.java

@@ -6,6 +6,7 @@ import com.izouma.nineth.domain.ShowCollection;
 import com.izouma.nineth.domain.Showroom;
 import com.izouma.nineth.domain.User;
 import com.izouma.nineth.dto.PageQuery;
+import com.izouma.nineth.enums.AuthStatus;
 import com.izouma.nineth.exception.BusinessException;
 import com.izouma.nineth.repo.NewsLikeRepo;
 import com.izouma.nineth.repo.ShowCollectionRepo;
@@ -15,13 +16,13 @@ import com.izouma.nineth.utils.SecurityUtils;
 import com.izouma.nineth.utils.excel.ExcelUtils;
 import lombok.AllArgsConstructor;
 import org.apache.commons.lang3.ObjectUtils;
-import org.springframework.cache.annotation.Cacheable;
 import org.springframework.data.domain.Page;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
 import javax.servlet.http.HttpServletResponse;
 import java.io.IOException;
+import java.util.Comparator;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -46,6 +47,11 @@ public class ShowroomController extends BaseController {
         return showroomService.update(SecurityUtils.getAuthenticatedUser().getId(), record);
     }
 
+    @PostMapping("/create")
+    public Showroom create(@RequestParam Long userId) {
+        return showroomService.save(userId);
+    }
+
     @PostMapping("/all")
     public Page<Showroom> all(@RequestBody PageQuery pageQuery) {
         Page<Showroom> all = showroomService.all(pageQuery);
@@ -67,7 +73,12 @@ public class ShowroomController extends BaseController {
 
         Map<Long, Long> finalLikesMap = likesMap;
         return all.map(showroom -> {
-            showroom.setCollections(collect.get(showroom.getId()));
+            List<ShowCollection> collections = collect.get(showroom.getId());
+            if (CollUtil.isNotEmpty(collections)) {
+                collections.sort(Comparator.comparingInt(ShowCollection::getSort));
+                showroom.setCollections(collections);
+            }
+
             showroom.setLiked(ObjectUtils.isNotEmpty(finalLikesMap.get(showroom.getId())));
             return showroom;
         });
@@ -76,7 +87,7 @@ public class ShowroomController extends BaseController {
     @GetMapping("/get/{id}")
     public Showroom get(@PathVariable Long id) {
         Showroom showroom = showroomRepo.findById(id).orElseThrow(new BusinessException("无记录"));
-        showroom.setCollections(showCollectionRepo.findAllByShowroomId(id));
+        showroom.setCollections(showCollectionRepo.findAllByShowroomIdOrderBySort(id));
         User user = SecurityUtils.getAuthenticatedUser();
 
         if (user != null && !user.isAdmin()) {
@@ -105,5 +116,15 @@ public class ShowroomController extends BaseController {
     public void addShare(@PathVariable Long id) {
         showroomRepo.addShare(id, 1);
     }
+
+    @PostMapping("/pass")
+    public void pass(@RequestParam Long id) {
+        showroomService.audit(id, AuthStatus.SUCCESS, null);
+    }
+
+    @PostMapping("/deny")
+    public void deny(@RequestParam Long id, String reason) {
+        showroomService.audit(id, AuthStatus.FAIL, reason);
+    }
 }
 

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
src/main/resources/genjson/CompanyCollection.json


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 1
src/main/resources/genjson/Showroom.json


+ 9 - 2
src/main/vue/src/components/ModelUpload.vue

@@ -10,8 +10,9 @@
         :on-preview="onPreview"
         :accept="accept || '*/*'"
         ref="upload"
+        :disabled="isDisabled"
     >
-        <el-button type="primary" size="mini" slot="trigger"> 点击上传 </el-button>
+        <el-button type="primary" size="mini" slot="trigger" :disabled="isDisabled"> 点击上传 </el-button>
         <div class="file-list-item" slot="file" slot-scope="{ file }">
             <div class="file-name">
                 <i class="status-icon el-icon-warning-outline danger" v-if="file.status === 'fail'"></i>
@@ -65,7 +66,13 @@ export default {
             default: 'string'
         },
         customUrl: {},
-        accept: {}
+        accept: {},
+        isDisabled: {
+            type: Boolean,
+            default() {
+                return false;
+            }
+        }
     },
     data() {
         return {

+ 1 - 1
src/main/vue/src/components/RichText.vue

@@ -1,5 +1,5 @@
 <template>
-    <tinymce-editor :init="init" :value="value" @input="onInput"> </tinymce-editor>
+    <tinymce-editor :init="init" :value="value" @input="onInput" :disabled="disabled"> </tinymce-editor>
 </template>
 <script>
 import Editor from '@tinymce/tinymce-vue';

+ 6 - 1
src/main/vue/src/components/SingleUpload.vue

@@ -6,6 +6,7 @@
         :show-file-list="false"
         :on-success="onSuccess"
         :before-upload="beforeUpload"
+        :disabled="disabled"
     >
         <div></div>
         <div class="wrapper">
@@ -36,7 +37,11 @@ export default {
         url: {
             type: String
         },
-        compress: { type: Boolean, default: false }
+        compress: { type: Boolean, default: false },
+        disabled: {
+            type: Boolean,
+            default: false
+        }
     },
     data() {
         return {

+ 135 - 7
src/main/vue/src/router.js

@@ -524,17 +524,17 @@ const router = new Router({
                     }
                 },
                 {
-                    path: '/showroomEdit',
-                    name: 'ShowroomEdit',
-                    component: () => import(/* webpackChunkName: "showroomEdit" */ '@/views/ShowroomEdit.vue'),
+                    path: '/collectionRoomEdit',
+                    name: 'CollectionRoomEdit',
+                    component: () => import(/* webpackChunkName: "showroomEdit" */ '@/views/CollectionRoomEdit.vue'),
                     meta: {
                         title: '展厅编辑'
                     }
                 },
                 {
-                    path: '/showroomList',
-                    name: 'ShowroomList',
-                    component: () => import(/* webpackChunkName: "showroomList" */ '@/views/ShowroomList.vue'),
+                    path: '/collectionRoomList',
+                    name: 'CollectionRoomList',
+                    component: () => import(/* webpackChunkName: "showroomList" */ '@/views/CollectionRoomList.vue'),
                     meta: {
                         title: '展厅'
                     }
@@ -554,6 +554,134 @@ const router = new Router({
                     meta: {
                         title: '结算查询'
                     }
+                },
+                {
+                    path: '/companyCollectionShow',
+                    name: 'CompanyCollectionShow',
+                    component: () =>
+                        import(/* webpackChunkName: "companyCollectionShow" */ '@/views/CompanyCollectionShow.vue'),
+                    meta: {
+                        title: '企业藏品详情'
+                    }
+                },
+                {
+                    path: '/companyCollectionList',
+                    name: 'CompanyCollectionList',
+                    component: () =>
+                        import(/* webpackChunkName: "companyCollectionList" */ '@/views/CompanyCollectionList.vue'),
+                    meta: {
+                        title: '企业藏品'
+                    }
+                },
+                {
+                    path: '/companyCollFailList',
+                    name: 'CompanyCollFailList',
+                    component: () =>
+                        import(/* webpackChunkName: "companyCollectionList" */ '@/views/CompanyCollFailList.vue'),
+                    meta: {
+                        title: '企业藏品'
+                    }
+                },
+                {
+                    path: '/companyEdit',
+                    name: 'CompanyEdit',
+                    component: () => import(/* webpackChunkName: "companyEdit" */ '@/views/CompanyEdit.vue'),
+                    meta: {
+                        title: '企业编辑'
+                    }
+                },
+                {
+                    path: '/companyList',
+                    name: 'CompanyList',
+                    component: () => import(/* webpackChunkName: "companyList" */ '@/views/CompanyList.vue'),
+                    meta: {
+                        title: '企业'
+                    }
+                },
+                /**企业 */
+                {
+                    path: '/collectionPendingList',
+                    name: 'CollectionPendingList',
+                    component: () =>
+                        import(
+                            /* webpackChunkName: "collectionPendingList" */ '@/views/company/CollectionPendingList.vue'
+                        ),
+                    meta: {
+                        title: '申请中藏品'
+                    }
+                },
+                {
+                    path: '/collectionPassList',
+                    name: 'CollectionPassList',
+                    component: () =>
+                        import(/* webpackChunkName: "collectionPassList" */ '@/views/company/CollectionPassList.vue'),
+                    meta: {
+                        title: '已通过藏品'
+                    }
+                },
+                {
+                    path: '/collectionFailList',
+                    name: 'CollectionFailList',
+                    component: () =>
+                        import(/* webpackChunkName: "collectionFailList" */ '@/views/company/CollectionFailList.vue'),
+                    meta: {
+                        title: '未通过藏品'
+                    }
+                },
+                {
+                    path: '/companyCollectionEdit',
+                    name: 'CompanyCollectionEdit',
+                    component: () =>
+                        import(
+                            /* webpackChunkName: "companyCollectionEdit" */ '@/views/company/CompanyCollectionEdit.vue'
+                        ),
+                    meta: {
+                        title: '企业藏品编辑'
+                    }
+                },
+                {
+                    path: '/companyCollectionShelf',
+                    name: 'CompanyCollectionShelf',
+                    component: () =>
+                        import(
+                            /* webpackChunkName: "companyCollectionShelf" */ '@/views/company/CompanyCollectionShelf.vue'
+                        ),
+                    meta: {
+                        title: '企业藏品查看'
+                    }
+                },
+                {
+                    path: '/showroomEdit',
+                    name: 'ShowroomEdit',
+                    component: () => import(/* webpackChunkName: "showroomEdit" */ '@/views/ShowroomEdit.vue'),
+                    meta: {
+                        title: '展厅编辑'
+                    }
+                },
+                {
+                    path: '/showroomList',
+                    name: 'ShowroomList',
+                    component: () => import(/* webpackChunkName: "showroomList" */ '@/views/ShowroomList.vue'),
+                    meta: {
+                        title: '展厅'
+                    }
+                },
+                {
+                    path: '/companyRoomEdit',
+                    name: 'CompanyRoomEdit',
+                    component: () =>
+                        import(/* webpackChunkName: "companyRoomEdit" */ '@/views/company/CompanyRoomEdit.vue'),
+                    meta: {
+                        title: '展厅编辑'
+                    }
+                },
+                {
+                    path: '/companyRoomList',
+                    name: 'CompanyRoomList',
+                    component: () => import(/* webpackChunkName: "companyRoomList" */ '@/views/company/CompanyRoomList.vue'),
+                    meta: {
+                        title: '展厅'
+                    }
                 }
                 /**INSERT_LOCATION**/
             ]
@@ -614,4 +742,4 @@ router.beforeEach((to, from, next) => {
     }
 });
 
-export default router;
+export default router;

+ 1 - 1
src/main/vue/src/views/BannerList.vue

@@ -12,7 +12,7 @@
             </el-button>
         </page-title>
         <div class="filters-container">
-            <el-select v-model="type" clearable placeholder="筛选类型" @change="getData">
+            <el-select v-model="type" clearable placeholder="筛选展示位置" @change="getData">
                 <el-option
                     v-for="item in typeOptions"
                     :key="item.value"

+ 2 - 2
src/main/vue/src/views/CollectionEdit.vue

@@ -255,12 +255,12 @@
                         </el-radio-group>
                     </el-form-item>
 
-                    <!-- <el-form-item prop="vip" label="享有特权">
+                    <el-form-item prop="vip" label="享有特权">
                         <el-radio-group v-model="formData.vip">
                             <el-radio :label="true">白名单权利</el-radio>
                             <el-radio :label="false">无特权</el-radio>
                         </el-radio-group>
-                    </el-form-item> -->
+                    </el-form-item>
 
                     <div class="inline-wrapper">
                         <el-form-item prop="assignment" label="拉新任务指标">

+ 796 - 0
src/main/vue/src/views/CollectionRoomEdit.vue

@@ -0,0 +1,796 @@
+<template>
+    <div class="edit-view">
+        <page-title>
+            <el-button @click="$router.go(-1)" :disabled="saving">取消</el-button>
+            <!-- <el-button @click="onDelete" :disabled="saving" type="danger" v-if="formData.id"> 删除 </el-button> -->
+            <el-button @click="onSave" :loading="saving" type="primary">保存</el-button>
+        </page-title>
+        <div class="edit-view__content-wrapper">
+            <div class="edit-view__content-section">
+                <el-form
+                    :model="formData"
+                    :rules="rules"
+                    ref="form"
+                    label-width="120px"
+                    label-position="right"
+                    size="small"
+                >
+                    <el-form-item prop="name" label="名称">
+                        <el-input v-model="formData.name" :disabled="!canEdit" style="width: 500px"></el-input>
+                    </el-form-item>
+                    <el-form-item prop="pic" label="图片">
+                        <object-upload
+                            v-model="formData.pic[0]"
+                            :disabled="!canEdit"
+                            compress
+                            width="3000"
+                            height="3000"
+                        ></object-upload>
+                        <div class="tip">支持JPG、PNG、GIF、MP4,推荐长宽比1:1</div>
+                    </el-form-item>
+                    <!-- <el-form-item prop="model3d" label="3D模型">
+                        <model-upload
+                            :limit="1"
+                            v-model="formData.model3d"
+                            :customUrl="customUrl"
+                            accept="application/zip"
+                            format="json"
+                            single
+                        ></model-upload>
+                        <div class="tip">请将FBX文件与贴图打包成zip压缩包上传</div>
+                    </el-form-item> -->
+                    <el-form-item label="相机距离" v-if="formData.model3d">
+                        <el-input-number v-model="scale" :min="0.1" :step="0.1"></el-input-number>
+                    </el-form-item>
+                    <el-form-item label="Y轴偏移" v-if="formData.model3d">
+                        <el-input-number v-model="yOffset"></el-input-number>
+                    </el-form-item>
+                    <div class="inline-wrapper">
+                        <el-form-item prop="minterId" label="铸造者">
+                            <minter-select
+                                v-model="formData.minterId"
+                                @detail="onMinterDetail"
+                                :disabled="!canEdit"
+                            ></minter-select>
+                        </el-form-item>
+                        <el-form-item prop="category" label="分类">
+                            <el-select v-model="formData.category" :disabled="!canEdit">
+                                <el-option
+                                    v-for="item in cateogories"
+                                    :label="item"
+                                    :value="item"
+                                    :key="item"
+                                ></el-option>
+                            </el-select>
+                        </el-form-item>
+                    </div>
+                    <el-form-item prop="detail" label="详情" style="width: calc(100vw - 450px)">
+                        <rich-text v-model="formData.detail"></rich-text>
+                    </el-form-item>
+                    <el-form-item prop="properties" label="特性" style="width: calc(100vw - 450px)" size="mini">
+                        <el-table :data="formData.properties">
+                            <el-table-column prop="name" label="名称">
+                                <template v-slot="{ row }">
+                                    <el-input v-model="row.name" placeholder="20字以内" maxlength="20"></el-input>
+                                </template>
+                            </el-table-column>
+                            <el-table-column prop="value" label="内容">
+                                <template v-slot="{ row }">
+                                    <el-input v-model="row.value" placeholder="20字以内" maxlength="20"></el-input>
+                                </template>
+                            </el-table-column>
+                            <el-table-column width="80" align="center">
+                                <template v-slot="{ $index }">
+                                    <el-button type="danger" plain size="mini" @click="delProperty($index)">
+                                        删除
+                                    </el-button>
+                                </template>
+                            </el-table-column>
+                        </el-table>
+                    </el-form-item>
+                    <el-form-item>
+                        <el-button size="mini" @click="addProperty"> 添加特性 </el-button>
+                    </el-form-item>
+                    <el-form-item label="特权" prop="privileges" style="width: calc(100vw - 450px)">
+                        <el-table :data="privilegeOptions">
+                            <el-table-column prop="name" label="可选特权" width="150"></el-table-column>
+                            <el-table-column prop="description"></el-table-column>
+                            <el-table-column width="155" align="right">
+                                <template v-slot="{ row, $index }">
+                                    <el-button size="mini" v-if="!row.added" @click="addPrivilege(row, $index)">
+                                        添加
+                                    </el-button>
+                                    <el-button size="mini" v-if="!!row.added" plain @click="editPrivilege(row, $index)">
+                                        编辑
+                                    </el-button>
+                                    <el-button
+                                        size="mini"
+                                        v-if="!!row.added"
+                                        type="danger"
+                                        plain
+                                        @click="delPrivilege(row, $index)"
+                                    >
+                                        删除
+                                    </el-button>
+                                </template>
+                            </el-table-column>
+                        </el-table>
+                    </el-form-item>
+                    <div class="inline-wrapper">
+                        <el-form-item prop="price" label="价格">
+                            <el-input-number
+                                type="number"
+                                v-model="formData.price"
+                                :disabled="!canEdit"
+                            ></el-input-number>
+                        </el-form-item>
+                        <el-form-item prop="price" label="原价">
+                            <el-input-number
+                                type="number"
+                                v-model="formData.originalPrice"
+                                :disabled="!canEdit"
+                            ></el-input-number>
+                        </el-form-item>
+                    </div>
+                    <div class="inline-wrapper">
+                        <el-form-item prop="royalties" label="版税(%)">
+                            <el-input-number v-model="formData.royalties" :min="0" :max="99" :disabled="!canEdit">
+                            </el-input-number>
+                        </el-form-item>
+                        <el-form-item prop="serviceCharge" label="手续费(%)">
+                            <el-input-number v-model="formData.serviceCharge" :min="0" :max="99" :disabled="!canEdit">
+                            </el-input-number>
+                        </el-form-item>
+                    </div>
+                    <el-form-item prop="total" label="发行数量">
+                        <el-input-number v-model="formData.total" :disabled="!canEdit"></el-input-number>
+                    </el-form-item>
+
+                    <el-form-item prop="scanCode" label="仅扫码可见">
+                        <el-radio v-model="formData.scanCode" :label="true">是</el-radio>
+                        <el-radio v-model="formData.scanCode" :label="false">否</el-radio>
+                    </el-form-item>
+                    <el-form-item prop="onShelf" label="上架" v-if="!formData.scanCode">
+                        <el-radio v-model="formData.onShelf" :label="true">是</el-radio>
+                        <el-radio v-model="formData.onShelf" :label="false">否</el-radio>
+                    </el-form-item>
+
+                    <div class="inline-wrapper">
+                        <el-form-item prop="startTime" label="定时发布">
+                            <el-radio v-model="formData.scheduleSale" :label="true">是</el-radio>
+                            <el-radio v-model="formData.scheduleSale" :label="false">否</el-radio>
+                        </el-form-item>
+                        <el-form-item
+                            prop="startTime"
+                            label="发布时间"
+                            v-if="formData.scheduleSale"
+                            style="margin-left: 22px"
+                        >
+                            <el-date-picker
+                                v-model="formData.startTime"
+                                type="datetime"
+                                value-format="yyyy-MM-dd HH:mm:ss"
+                                placeholder="发布时间"
+                            ></el-date-picker>
+                        </el-form-item>
+                    </div>
+
+                    <el-form-item
+                        prop="salable"
+                        label="可售"
+                        v-if="
+                            formData.onShelf === true || (formData.scanCode === true && formData.scheduleSale === false)
+                        "
+                    >
+                        <el-radio v-model="formData.salable" :label="true">是</el-radio>
+                        <el-radio v-model="formData.salable" :label="false">仅展示</el-radio>
+                    </el-form-item>
+
+                    <el-form-item prop="sort" label="排序">
+                        <el-input-number v-model="formData.sort" :min="0"></el-input-number>
+                        <div class="tip">数字越大排序越靠前,相同数值按创建时间倒序排列</div>
+                    </el-form-item>
+
+                    <div class="inline-wrapper">
+                        <el-form-item prop="maxCount" label="限购">
+                            <el-input-number v-model="formData.maxCount" :min="0" :step="1"></el-input-number>
+                            <div class="tip">0表示不限购</div>
+                        </el-form-item>
+                        <el-form-item prop="countId" label="限购识别码" v-if="formData.maxCount > 0">
+                            <el-input v-model="formData.countId" style="width: 300px"></el-input>
+                            <div class="tip">相同识别码的藏品共享限购数量</div>
+                        </el-form-item>
+                    </div>
+
+                    <el-form-item prop="holdDays" label="持有天数">
+                        <el-input-number
+                            type="number"
+                            :min="0"
+                            :step="1"
+                            :max="2147483647"
+                            v-model="formData.holdDays"
+                            style="width: 180px"
+                        ></el-input-number>
+                        <div class="tip">持有多少天可转赠/转让。为空时,按系统设置天数。</div>
+                    </el-form-item>
+                    <el-form-item prop="noSoldOut" label="售罄">
+                        <el-radio v-model="formData.noSoldOut" :label="false">是</el-radio>
+                        <el-radio v-model="formData.noSoldOut" :label="true">否</el-radio>
+                    </el-form-item>
+                    <el-form-item prop="couponPayment" label="支付方式">
+                        <el-radio-group v-model="formData.couponPayment">
+                            <el-radio :label="true">兑换券</el-radio>
+                            <el-radio :label="false">支付宝/微信</el-radio>
+                        </el-radio-group>
+                    </el-form-item>
+
+                    <el-form-item prop="vip" label="享有特权">
+                        <el-radio-group v-model="formData.vip">
+                            <el-radio :label="true">白名单权利</el-radio>
+                            <el-radio :label="false">无特权</el-radio>
+                        </el-radio-group>
+                    </el-form-item>
+
+                    <el-form-item prop="headBg" label="展厅头部背景">
+                        <single-upload v-model="formData.headBg"></single-upload>
+                    </el-form-item>
+                    <el-form-item prop="showroomBg" label="展厅背景">
+                        <single-upload v-model="formData.showroomBg"></single-upload>
+                    </el-form-item>
+                    <el-form-item prop="maxCollection" label="最多可放数量">
+                        <el-input-number type="number" v-model="formData.maxCollection" :min="1"></el-input-number>
+                    </el-form-item>
+
+                    <div class="inline-wrapper">
+                        <el-form-item prop="assignment" label="拉新任务指标">
+                            <el-input-number
+                                type="number"
+                                :min="0"
+                                :step="1"
+                                :max="5"
+                                v-model="formData.assignment"
+                            ></el-input-number>
+                            <div class="tip">0表示无拉新任务限制</div>
+                        </el-form-item>
+                        <el-form-item prop="totalQuota" label="白名单额度" v-if="formData.assignment > 0">
+                            <el-input-number
+                                type="number"
+                                :min="0"
+                                :step="1"
+                                v-model="formData.totalQuota"
+                                :disabled="!editQuota"
+                            ></el-input-number>
+                            <div class="tip">多少人拉新可获得积分</div>
+                        </el-form-item>
+                    </div>
+                    <el-form-item prop="openQuota" label="白名单分享" v-if="formData.assignment > 0">
+                        <el-radio v-model="formData.openQuota" :label="true">开启</el-radio>
+                        <el-radio v-model="formData.openQuota" :label="false">关闭</el-radio>
+                    </el-form-item>
+                    <div class="inline-wrapper">
+                        <el-form-item prop="timeDelay" label="延迟销售" v-if="formData.assignment > 0">
+                            <el-radio v-model="formData.timeDelay" :label="true">是</el-radio>
+                            <el-radio v-model="formData.timeDelay" :label="false">否</el-radio>
+                        </el-form-item>
+                        <el-form-item
+                            prop="saleTime"
+                            label="销售时间"
+                            v-if="formData.assignment > 0 && formData.timeDelay"
+                            style="margin-left: 22px"
+                        >
+                            <el-date-picker
+                                v-model="formData.saleTime"
+                                type="datetime"
+                                value-format="yyyy-MM-dd HH:mm:ss"
+                                placeholder="销售时间"
+                            ></el-date-picker>
+                        </el-form-item>
+                    </div>
+
+                    <el-form-item label="分享海报" v-if="formData.assignment > 0">
+                        <single-upload v-model="formData.shareBg"></single-upload>
+                    </el-form-item>
+                    <el-form-item label="注册海报" v-if="formData.assignment > 0">
+                        <single-upload v-model="formData.registerBg"></single-upload>
+                    </el-form-item>
+
+                    <el-form-item class="form-submit">
+                        <el-button @click="onSave" :loading="saving" type="primary"> 保存 </el-button>
+                        <!-- <el-button @click="onDelete" :disabled="saving" type="danger" v-if="formData.id">
+                            删除
+                        </el-button> -->
+                        <el-button @click="$router.go(-1)" :disabled="saving">取消</el-button>
+                    </el-form-item>
+                </el-form>
+            </div>
+        </div>
+
+        <el-dialog :visible.sync="showPrivilegeEditDialog" width="600px" :title="privilegeForm.name">
+            <el-form
+                ref="privilegeForm"
+                :model="privilegeForm"
+                label-position="right"
+                label-width="80px"
+                :rules="privelegeRules"
+            >
+                <el-form-item
+                    prop="detail"
+                    label="详细内容"
+                    v-if="privilegeForm.type === 'text' || privilegeForm.type === 'exchange'"
+                >
+                    <el-input type="textarea" :autosize="{ minRows: 3 }" v-model="privilegeForm.detail"></el-input>
+                </el-form-item>
+                <el-form-item prop="detail" label="二维码" v-if="privilegeForm.type === 'qrcode'">
+                    <single-upload v-model="privilegeForm.detail"></single-upload>
+                </el-form-item>
+                <el-form-item
+                    prop="remark"
+                    label="说明"
+                    v-if="privilegeForm.type === 'qrcode' || privilegeForm.type === 'code'"
+                >
+                    <el-input type="textarea" :autosize="{ minRows: 3 }" v-model="privilegeForm.remark"></el-input>
+                </el-form-item>
+            </el-form>
+            <div slot="footer">
+                <el-button @click="showPrivilegeEditDialog = false">取消</el-button>
+                <el-button type="primary" @click="savePrivilege">保存</el-button>
+            </div>
+        </el-dialog>
+    </div>
+</template>
+<script>
+import resolveUrl from 'resolve-url';
+import ModelUpload from '../components/ModelUpload.vue';
+import { format, parse, isBefore } from 'date-fns';
+import SingleUpload from '../components/SingleUpload.vue';
+export default {
+    name: 'CollectionEdit',
+    components: { ModelUpload, SingleUpload },
+    created() {
+        Promise.all([
+            new Promise((resolve, reject) => {
+                if (this.$route.query.id) {
+                    return this.$http
+                        .get('collection/getInfo/' + this.$route.query.id)
+                        .then(res => {
+                            if (res.model3d) {
+                                let url = new URL(res.model3d.url);
+                                this.scale = Number(url.searchParams.get('scale')) || 1;
+                                this.yOffset = Number(url.searchParams.get('yOffset')) || 0;
+                                res.model3d.url = url.origin + url.pathname;
+                            }
+                            res.properties = res.properties || [];
+                            res.privileges = res.privileges || [];
+                            this.formData = res;
+                            if (res.totalQuota !== res.vipQuota) {
+                                this.editQuota = false;
+                            }
+                            resolve();
+                        })
+                        .catch(e => {
+                            console.log(e);
+                            this.$message.error(e.error);
+                            resolve();
+                        });
+                }
+                return resolve();
+            }),
+            this.$http
+                .post('/privilegeOption/all', { size: 10000, query: { del: false } }, { body: 'json' })
+                .then(res => {
+                    this.privilegeOptions = res.content;
+                    return Promise.resolve();
+                })
+        ]).then(() => {
+            console.log(this.formData, this.privilegeOptions);
+            this.formData.privileges.forEach(p => {
+                let idx = this.privilegeOptions.findIndex(i => i.name === p.name);
+                if (idx > -1) {
+                    this.$set(this.privilegeOptions[idx], 'added', true);
+                }
+            });
+        });
+    },
+    computed: {
+        canEdit() {
+            return !!!this.$route.query.id;
+        }
+    },
+    data() {
+        return {
+            saving: false,
+            formData: {
+                onShelf: false,
+                salable: true,
+                properties: [],
+                type: 'SHOWROOM',
+                source: 'OFFICIAL',
+                pic: [],
+                scheduleSale: true,
+                sort: 0,
+                privileges: [],
+                maxCount: 0,
+                countId: null,
+                canResale: true,
+                scanCode: false,
+                noSoldOut: true,
+                assignment: 0,
+                couponPayment: false,
+                maxCollection: 20
+            },
+            rules: {
+                name: [
+                    {
+                        required: true,
+                        message: '请输入名称',
+                        trigger: 'blur'
+                    }
+                ],
+                pic: [
+                    {
+                        validator: (rule, value, callback) => {
+                            if (value) {
+                                if (!(value instanceof Array)) {
+                                    callback(new Error('请上传内容'));
+                                    return;
+                                } else {
+                                    for (let f of value) {
+                                        if (!f.url) {
+                                            callback(new Error('请上传内容'));
+                                            return;
+                                        }
+                                    }
+                                }
+                                callback();
+                            } else {
+                                callback(new Error('请上传内容'));
+                            }
+                        },
+                        trigger: 'blur'
+                    }
+                ],
+                minter: [
+                    {
+                        required: true,
+                        message: '请输入铸造者',
+                        trigger: 'blur'
+                    }
+                ],
+                minterId: [
+                    {
+                        required: true,
+                        message: '请输入铸造者ID',
+                        trigger: 'blur'
+                    }
+                ],
+                minterAvatar: [
+                    {
+                        required: true,
+                        message: '请输入铸造者头像',
+                        trigger: 'blur'
+                    }
+                ],
+                detail: [
+                    {
+                        required: true,
+                        message: '请输入详情',
+                        trigger: 'blur'
+                    }
+                ],
+                type: [
+                    {
+                        required: true,
+                        message: '请输入类型',
+                        trigger: 'blur'
+                    }
+                ],
+                source: [
+                    {
+                        required: true,
+                        message: '请输入来源',
+                        trigger: 'blur'
+                    }
+                ],
+                total: [
+                    {
+                        required: true,
+                        message: '请输入发行数量',
+                        trigger: 'blur'
+                    }
+                ],
+                onShelf: [
+                    {
+                        required: true,
+                        message: '请输入上架',
+                        trigger: 'blur'
+                    }
+                ],
+                price: [
+                    {
+                        required: true,
+                        message: '请输入价格',
+                        trigger: 'blur'
+                    }
+                ],
+                properties: [
+                    {
+                        validator: (rule, value, callback) => {
+                            if (value) {
+                                if (!(value instanceof Array)) {
+                                    callback(new Error('properties must be array!'));
+                                    return;
+                                } else {
+                                    for (let i = 0; i < value.length; i++) {
+                                        if (value[i].name === '' || value[i].name === undefined) {
+                                            callback(new Error('请填写名称'));
+                                            return;
+                                        }
+                                        if (value[i].value === '' || value[i].value === undefined) {
+                                            callback(new Error('请填写内容'));
+                                            return;
+                                        }
+                                    }
+                                }
+                            }
+                            callback();
+                        },
+                        trigger: 'blur'
+                    }
+                ],
+                category: [{ required: true, message: '请填写分类' }],
+                royalties: [{ required: true, message: '请填写版税' }],
+                serviceCharge: [{ required: true, message: '请填手续费' }],
+                startTime: [
+                    {
+                        validator: (rule, value, callback) => {
+                            if (this.formData.scheduleSale) {
+                                if (!value) {
+                                    callback(new Error('请填写发布时间'));
+                                } else if (isBefore(parse(value, 'yyyy-MM-dd HH:mm:ss', new Date()), new Date())) {
+                                    callback(new Error('发布时间不能小于当前时间'));
+                                } else {
+                                    callback();
+                                }
+                            } else {
+                                callback();
+                            }
+                        },
+                        trigger: 'blur'
+                    }
+                ],
+                saleTime: [
+                    {
+                        validator: (rule, value, callback) => {
+                            if (this.formData.timeDelay) {
+                                if (!value) {
+                                    callback(new Error('请填写销售时间'));
+                                } else if (isBefore(parse(value, 'yyyy-MM-dd HH:mm:ss', new Date()), new Date())) {
+                                    callback(new Error('销售时间不能小于当前时间'));
+                                } else if (this.formData.scheduleSale) {
+                                    if (
+                                        isBefore(
+                                            parse(value, 'yyyy-MM-dd HH:mm:ss', new Date()),
+                                            parse(this.formData.startTime, 'yyyy-MM-dd HH:mm:ss', new Date())
+                                        )
+                                    ) {
+                                        callback(new Error('销售时间不能小于发布时间'));
+                                    }
+                                }
+                            }
+                            callback();
+                        },
+                        trigger: 'blur'
+                    }
+                ],
+                timeDelay: [
+                    {
+                        validator: (rule, value, callback) => {
+                            if (this.formData.assignment > 0) {
+                                if (value === '' || value === undefined) {
+                                    callback(new Error('请选择是否延迟销售'));
+                                    return;
+                                }
+                            }
+                            callback();
+                        },
+                        trigger: 'blur'
+                    }
+                ],
+                totalQuota: [
+                    {
+                        validator: (rule, value, callback) => {
+                            if (this.formData.assignment > 0) {
+                                if (value === '' || value === undefined) {
+                                    callback(new Error('请输入白名单额度'));
+                                    return;
+                                }
+                            }
+
+                            callback();
+                        },
+                        trigger: 'blur'
+                    }
+                ],
+                openQuota: [
+                    {
+                        validator: (rule, value, callback) => {
+                            if (this.formData.assignment > 0) {
+                                if (value === '' || value === undefined) {
+                                    callback(new Error('请选择开启/关闭白名单分享'));
+                                    return;
+                                }
+                            }
+                            callback();
+                        },
+                        trigger: 'blur'
+                    }
+                ]
+            },
+            sourceOptions: [
+                { label: '官方', value: 'OFFICIAL' },
+                { label: '用户铸造', value: 'USER' },
+                { label: '转让', value: 'TRANSFER' }
+            ],
+            cateogories: ['勋章', '收藏品', '数字艺术', '门票', '游戏', '音乐', '使用', '其他'],
+            privilegeOptions: [],
+            showPrivilegeEditDialog: false,
+            privilegeForm: {},
+            privelegeRules: {
+                detail: [{ required: true, message: '请填写内容' }],
+                remark: [{ required: true, message: '请填写说明' }]
+            },
+            customUrl: resolveUrl(this.$baseUrl, 'upload/3dModel'),
+            scale: 1,
+            yOffset: 0,
+            editQuota: true
+        };
+    },
+    methods: {
+        onSave() {
+            this.$refs.form.validate(valid => {
+                if (valid) {
+                    this.submit();
+                } else {
+                    return false;
+                }
+            });
+        },
+        submit() {
+            let data = { ...this.formData };
+            if (data.model3d) {
+                data.model3d.url = data.model3d.url + '?scale=' + this.scale + '&yOffset=' + this.yOffset;
+            }
+            if (this.editQuota && data.totalQuota) {
+                data.vipQuota = data.totalQuota;
+            }
+
+            this.saving = true;
+            this.$http
+                .post(this.formData.id ? '/collection/save' : '/collection/create', data, { body: 'json' })
+                .then(res => {
+                    this.saving = false;
+                    this.$message.success('成功');
+                    this.$router.go(-1);
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.saving = false;
+                    this.$message.error(e.error);
+                });
+        },
+        onDelete() {
+            this.$confirm('删除将无法恢复,确认要删除么?', '警告', { type: 'error' })
+                .then(() => {
+                    return this.$http.post(`/collection/del/${this.formData.id}`);
+                })
+                .then(() => {
+                    this.$message.success('删除成功');
+                    this.$router.go(-1);
+                })
+                .catch(e => {
+                    if (e !== 'cancel') {
+                        console.log(e);
+                        this.$message.error((e || {}).error || '删除失败');
+                    }
+                });
+        },
+        onMinterDetail(e) {
+            console.log(e);
+            this.$set(this.formData, 'minter', e.nickname);
+            this.$set(this.formData, 'minterAvatar', e.avatar);
+        },
+        addProperty() {
+            this.formData.properties.push({
+                name: '',
+                value: ''
+            });
+        },
+        delProperty(i) {
+            this.formData.properties.splice(i, 1);
+        },
+        addPrivilege(row, i) {
+            this.privilegeForm = { ...row };
+            this.showPrivilegeEditDialog = true;
+            if (this.$refs.privilegeForm) {
+                this.$nextTick(() => {
+                    this.$refs.privilegeForm.clearValidate();
+                });
+            }
+        },
+        editPrivilege(row, i) {
+            this.privilegeForm = { ...(this.formData.privileges.find(e => e.name === row.name) || {}) };
+            this.showPrivilegeEditDialog = true;
+            if (this.$refs.privilegeForm) {
+                this.$nextTick(() => {
+                    this.$refs.privilegeForm.clearValidate();
+                });
+            }
+        },
+        delPrivilege(row, i) {
+            let idx = this.formData.privileges.findIndex(e => e.name === row.name);
+            if (idx > -1) {
+                this.formData.privileges.splice(idx, 1);
+            }
+            this.$set(this.privilegeOptions[i], 'added', false);
+        },
+        savePrivilege() {
+            this.$refs.privilegeForm
+                .validate()
+                .then(() => {
+                    let i = this.formData.privileges.findIndex(e => e.name === this.privilegeForm.name);
+                    if (i > -1) {
+                        this.$set(this.formData.privileges, i, { ...this.privilegeForm });
+                    } else {
+                        this.formData.privileges.push({ ...this.privilegeForm });
+                    }
+                    let ii = this.privilegeOptions.findIndex(i => i.name === this.privilegeForm.name);
+                    console.log(ii);
+                    this.$set(this.privilegeOptions[ii], 'added', true);
+                    this.showPrivilegeEditDialog = false;
+                })
+                .catch(e => {
+                    console.log(e);
+                });
+        }
+    },
+    watch: {
+        'formData.scanCode'(val) {
+            if (val === true) {
+                this.$set(this.formData, 'onShelf', false);
+            }
+        }
+    }
+};
+</script>
+<style lang="less" scoped>
+/deep/ .el-switch__label--left {
+    width: 50px;
+    text-align: right;
+}
+.number-percent {
+    display: flex;
+    align-items: center;
+    .percent {
+        border: 1px solid #dcdfe6;
+        border-radius: 4px;
+        width: 30px;
+        text-align: center;
+        line-height: 30px;
+        color: @text2;
+        font-size: 13px;
+    }
+}
+.tip {
+    font-size: 12px;
+    color: @text3;
+    margin-top: 5px;
+}
+.inline-wrapper {
+    .el-form-item {
+        display: inline-block;
+    }
+}
+.right-margin {
+    margin-left: 50px;
+}
+</style>

+ 257 - 0
src/main/vue/src/views/CollectionRoomList.vue

@@ -0,0 +1,257 @@
+<template>
+    <div class="list-view">
+        <page-title>
+            <el-button
+                @click="addRow"
+                type="primary"
+                icon="el-icon-plus"
+                :disabled="fetchingData || downloading"
+                class="filter-item"
+            >
+                新增
+            </el-button>
+        </page-title>
+        <div class="filters-container">
+            <created-at-picker v-model="createdAt" @input="getData" class="filter-item"></created-at-picker>
+            <minter-filter v-model="minterId" @input="getData" class="filter-item"></minter-filter>
+            <el-input
+                placeholder="搜索..."
+                v-model="search"
+                clearable
+                class="filter-item search"
+                @keyup.enter.native="getData"
+            >
+                <el-button @click="getData" slot="append" icon="el-icon-search"> </el-button>
+            </el-input>
+        </div>
+        <el-table
+            :data="tableData"
+            row-key="id"
+            ref="table"
+            header-row-class-name="table-header-row"
+            header-cell-class-name="table-header-cell"
+            row-class-name="table-row"
+            cell-class-name="table-cell"
+            :height="tableHeight"
+            v-loading="fetchingData"
+        >
+            <el-table-column v-if="multipleMode" align="center" type="selection" width="50"> </el-table-column>
+            <el-table-column prop="id" label="ID" width="100"> </el-table-column>
+            <el-table-column prop="name" label="名称" show-overflow-tooltip> </el-table-column>
+            <el-table-column prop="createdAt" label="创建时间" width="150">
+                <template slot="header" slot-scope="{ column }">
+                    <sortable-header :column="column" :current-sort="sort" @changeSort="changeSort"> </sortable-header>
+                </template>
+            </el-table-column>
+            <el-table-column prop="pic" label="作品内容" width="90" align="center">
+                <template slot-scope="{ row }">
+                    <el-image
+                        style="width: 30px; height: 30px"
+                        :src="row.pic[0].thumb || row.pic[0].url"
+                        fit="cover"
+                        :preview-src-list="row.pic.map(i => i.thumb || i.url)"
+                    ></el-image>
+                </template>
+            </el-table-column>
+            <el-table-column prop="minter" label="铸造者"> </el-table-column>
+            <el-table-column prop="minterId" label="铸造者ID"> </el-table-column>
+            <el-table-column prop="type" label="类型" :formatter="typeFormatter"> </el-table-column>
+            <el-table-column prop="source" label="来源" :formatter="sourceFormatter"> </el-table-column>
+            <el-table-column prop="total" label="发行数量" width="105" align="center">
+                <template slot="header" slot-scope="{ column }">
+                    <sortable-header :column="column" :current-sort="sort" @changeSort="changeSort"> </sortable-header>
+                </template>
+            </el-table-column>
+            <el-table-column prop="stock" label="剩余库存" width="105" align="center">
+                <template slot="header" slot-scope="{ column }">
+                    <sortable-header :column="column" :current-sort="sort" @changeSort="changeSort"> </sortable-header>
+                </template>
+            </el-table-column>
+            <el-table-column prop="onShelf" label="上架" width="90" align="center">
+                <template v-slot="{ row }">
+                    <el-tag type="success" v-if="row.onShelf">是</el-tag>
+                    <el-tag type="info" v-else>否</el-tag>
+                </template>
+            </el-table-column>
+            <el-table-column prop="salable" label="仅展示" width="90" align="center">
+                <template v-slot="{ row }">
+                    <el-tag type="success" v-if="!row.salable">是</el-tag>
+                    <el-tag type="info" v-else>否</el-tag>
+                </template>
+            </el-table-column>
+            <el-table-column prop="price" label="价格" width="90"> </el-table-column>
+            <el-table-column prop="noSoldOut" label="售罄">
+                <template v-slot="{ row }">
+                    <el-tag type="success" v-if="!row.noSoldOut">是</el-tag>
+                    <el-tag type="info" v-else>否</el-tag>
+                </template>
+            </el-table-column>
+            <el-table-column prop="sort" label="排序" width="90" align="center">
+                <template slot="header" slot-scope="{ column }">
+                    <sortable-header :column="column" :current-sort="sort" @changeSort="changeSort"> </sortable-header>
+                </template>
+            </el-table-column>
+            <el-table-column label="操作" align="center" fixed="right" width="150">
+                <template slot-scope="{ row }">
+                    <el-button @click="editRow(row)" type="primary" size="mini" plain>查看</el-button>
+                    <!-- <el-button @click="deleteRow(row)" type="danger" size="mini" plain>删除</el-button> -->
+                </template>
+            </el-table-column>
+        </el-table>
+        <div class="pagination-wrapper">
+            <!-- <div class="multiple-mode-wrapper">
+                <el-button v-if="!multipleMode" @click="toggleMultipleMode(true)">批量编辑</el-button>
+                <el-button-group v-else>
+                    <el-button @click="operation1">批量操作1</el-button>
+                    <el-button @click="operation2">批量操作2</el-button>
+                    <el-button @click="toggleMultipleMode(false)">取消</el-button>
+                </el-button-group>
+            </div> -->
+            <el-pagination
+                background
+                @size-change="onSizeChange"
+                @current-change="onCurrentChange"
+                :current-page="page"
+                :page-sizes="[10, 20, 30, 40, 50]"
+                :page-size="pageSize"
+                layout="total, sizes, prev, pager, next, jumper"
+                :total="totalElements"
+            >
+            </el-pagination>
+        </div>
+    </div>
+</template>
+<script>
+import { mapState } from 'vuex';
+import pageableTable from '@/mixins/pageableTable';
+import SortableHeader from '../components/SortableHeader.vue';
+import MinterSelect from '../components/MinterSelect.vue';
+
+export default {
+    components: { SortableHeader, MinterSelect },
+    name: 'ShowroomList',
+    mixins: [pageableTable],
+    data() {
+        return {
+            multipleMode: false,
+            search: '',
+            url: '/collection/all',
+            downloading: false,
+            typeOptions: [
+                { label: '默认', value: 'DEFAULT' },
+                { label: '盲盒', value: 'BLIND_BOX' },
+                { label: '拍卖', value: 'AUCTION' },
+                { label: '展厅', value: 'SHOWROOM' }
+            ],
+            sourceOptions: [
+                { label: '官方', value: 'OFFICIAL' },
+                { label: '用户铸造', value: 'USER' },
+                { label: '转让', value: 'TRANSFER' }
+            ],
+            sort: { sort: 'desc' },
+            sortStr: 'sort,desc',
+            createdAt: '',
+            minterId: ''
+        };
+    },
+    computed: {
+        selection() {
+            return this.$refs.table.selection.map(i => i.id);
+        }
+    },
+    methods: {
+        typeFormatter(row, column, cellValue, index) {
+            let selectedOption = this.typeOptions.find(i => i.value === cellValue);
+            if (selectedOption) {
+                return selectedOption.label;
+            }
+            return '';
+        },
+        sourceFormatter(row, column, cellValue, index) {
+            let selectedOption = this.sourceOptions.find(i => i.value === cellValue);
+            if (selectedOption) {
+                return selectedOption.label;
+            }
+            return '';
+        },
+        beforeGetData() {
+            return {
+                search: this.search,
+                query: { del: false, source: 'OFFICIAL', type: 'SHOWROOM', createdAt: this.createdAt, minterId: this.minterId }
+            };
+        },
+        toggleMultipleMode(multipleMode) {
+            this.multipleMode = multipleMode;
+            if (!multipleMode) {
+                this.$refs.table.clearSelection();
+            }
+        },
+        addRow() {
+            this.$router.push({
+                path: '/collectionRoomEdit',
+                query: {
+                    ...this.$route.query
+                }
+            });
+        },
+        editRow(row) {
+            this.$router.push({
+                path: 'collectionRoomEdit',
+                query: {
+                    id: row.id
+                }
+            });
+        },
+        download() {
+            this.downloading = true;
+            this.$axios
+                .get('/collection/excel', {
+                    responseType: 'blob',
+                    params: { size: 10000 }
+                })
+                .then(res => {
+                    console.log(res);
+                    this.downloading = false;
+                    const downloadUrl = window.URL.createObjectURL(new Blob([res.data]));
+                    const link = document.createElement('a');
+                    link.href = downloadUrl;
+                    link.setAttribute('download', res.headers['content-disposition'].split('filename=')[1]);
+                    document.body.appendChild(link);
+                    link.click();
+                    link.remove();
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.downloading = false;
+                    this.$message.error(e.error);
+                });
+        },
+        operation1() {
+            this.$notify({
+                title: '提示',
+                message: this.selection
+            });
+        },
+        operation2() {
+            this.$message('操作2');
+        },
+        deleteRow(row) {
+            this.$alert('删除将无法恢复,确认要删除么?', '警告', { type: 'error' })
+                .then(() => {
+                    return this.$http.post(`/collection/del/${row.id}`);
+                })
+                .then(() => {
+                    this.$message.success('删除成功');
+                    this.getData();
+                })
+                .catch(e => {
+                    if (e !== 'cancel') {
+                        this.$message.error(e.error);
+                    }
+                });
+        }
+    }
+};
+</script>
+<style lang="less" scoped>
+</style>

+ 269 - 0
src/main/vue/src/views/CompanyCollFailList.vue

@@ -0,0 +1,269 @@
+<template>
+    <div class="list-view">
+        <page-title>
+            <!-- <el-button
+                @click="addRow"
+                type="primary"
+                icon="el-icon-plus"
+                :disabled="fetchingData || downloading"
+                class="filter-item"
+            >
+                新增
+            </el-button>
+            <el-button
+                @click="download"
+                icon="el-icon-upload2"
+                :loading="downloading"
+                :disabled="fetchingData"
+                class="filter-item"
+            >
+                导出
+            </el-button> -->
+        </page-title>
+        <div class="filters-container">
+            <el-input
+                placeholder="搜索..."
+                v-model="search"
+                clearable
+                class="filter-item search"
+                @keyup.enter.native="getData"
+            >
+                <el-button @click="getData" slot="append" icon="el-icon-search"> </el-button>
+            </el-input>
+            <el-input class="filter-item" placeholder="搜索企业ID" v-model="userId" @keyup.enter.native="getData"></el-input>
+        </div>
+        <el-table
+            :data="tableData"
+            row-key="id"
+            ref="table"
+            header-row-class-name="table-header-row"
+            header-cell-class-name="table-header-cell"
+            row-class-name="table-row"
+            cell-class-name="table-cell"
+            :height="tableHeight"
+            v-loading="fetchingData"
+        >
+            <el-table-column v-if="multipleMode" align="center" type="selection" width="50"> </el-table-column>
+            <el-table-column prop="id" label="ID" width="100"> </el-table-column>
+            <el-table-column prop="userId" label="企业管理者ID" width="100"></el-table-column>
+            <el-table-column prop="minter" label="企业名称" width="120"></el-table-column>
+            <el-table-column prop="name" label="名称"> </el-table-column>
+            <el-table-column prop="createdAt" label="创建时间" width="150">
+                <template slot="header" slot-scope="{ column }">
+                    <sortable-header :column="column" :current-sort="sort" @changeSort="changeSort"> </sortable-header>
+                </template>
+            </el-table-column>
+            <el-table-column prop="pic" label="作品内容" width="90" align="center">
+                <template slot-scope="{ row }">
+                    <el-image
+                        style="width: 30px; height: 30px"
+                        :src="row.pic[0].thumb || row.pic[0].url"
+                        fit="cover"
+                        :preview-src-list="row.pic.map(i => i.thumb || i.url)"
+                    ></el-image>
+                </template>
+            </el-table-column>
+            <el-table-column prop="total" label="发行数量" width="105" align="center">
+                <template slot="header" slot-scope="{ column }">
+                    <sortable-header :column="column" :current-sort="sort" @changeSort="changeSort"> </sortable-header>
+                </template>
+            </el-table-column>
+            <el-table-column prop="price" label="价格"> </el-table-column>
+            <el-table-column prop="status" label="审核状态" :formatter="statusFormatter" width="120" align="center">
+            </el-table-column>
+            <el-table-column label="操作" align="center" fixed="right" width="120">
+                <template slot-scope="{ row }">
+                    <el-button @click="editRow(row)" type="primary" size="mini" plain>查看</el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+        <div class="pagination-wrapper">
+            <!-- <div class="multiple-mode-wrapper">
+                <el-button v-if="!multipleMode" @click="toggleMultipleMode(true)">批量编辑</el-button>
+                <el-button-group v-else>
+                    <el-button @click="operation1">批量操作1</el-button>
+                    <el-button @click="operation2">批量操作2</el-button>
+                    <el-button @click="toggleMultipleMode(false)">取消</el-button>
+                </el-button-group>
+            </div> -->
+            <el-pagination
+                background
+                @size-change="onSizeChange"
+                @current-change="onCurrentChange"
+                :current-page="page"
+                :page-sizes="[10, 20, 30, 40, 50]"
+                :page-size="pageSize"
+                layout="total, sizes, prev, pager, next, jumper"
+                :total="totalElements"
+            >
+            </el-pagination>
+        </div>
+
+        <el-dialog :visible.sync="showReasonDialog" width="600px" title="拒绝理由">
+            <el-form ref="denyForm" :model="denyForm" label-position="right" label-width="60px" :rules="denyRules">
+                <el-form-item prop="reason" label="理由">
+                    <el-input type="textarea" :autosize="{ minRows: 3 }" v-model="denyForm.reason"></el-input>
+                </el-form-item>
+            </el-form>
+            <div slot="footer">
+                <el-button @click="showReasonDialog = false">取消</el-button>
+                <el-button type="primary" @click="saveReason">保存</el-button>
+            </div>
+        </el-dialog>
+    </div>
+</template>
+<script>
+import { mapState } from 'vuex';
+import pageableTable from '@/mixins/pageableTable';
+
+export default {
+    mixins: [pageableTable],
+    data() {
+        return {
+            multipleMode: false,
+            search: '',
+            url: '/companyCollection/all',
+            downloading: false,
+            statusOptions: [
+                { label: '审核中', value: 'PENDING' },
+                { label: '通过', value: 'SUCCESS' },
+                { label: '失败', value: 'FAIL' }
+            ],
+            typeOptions: [
+                { label: '默认', value: 'DEFAULT' },
+                { label: '盲盒', value: 'BLIND_BOX' },
+                { label: '拍卖', value: 'AUCTION' }
+            ],
+            denyForm: {},
+            denyRules: {
+                reason: [{ required: true, message: '请填写理由' }]
+            },
+            showReasonDialog: false,
+            userId: ''
+        };
+    },
+    computed: {
+        selection() {
+            return this.$refs.table.selection.map(i => i.id);
+        }
+    },
+    methods: {
+        typeFormatter(row, column, cellValue, index) {
+            let selectedOption = this.typeOptions.find(i => i.value === cellValue);
+            if (selectedOption) {
+                return selectedOption.label;
+            }
+            return '';
+        },
+        statusFormatter(row, column, cellValue, index) {
+            let selectedOption = this.statusOptions.find(i => i.value === cellValue);
+            if (selectedOption) {
+                return selectedOption.label;
+            }
+            return '';
+        },
+        beforeGetData() {
+            return { search: this.search, query: { del: false, status: 'FAIL', userId: this.userId } };
+        },
+        toggleMultipleMode(multipleMode) {
+            this.multipleMode = multipleMode;
+            if (!multipleMode) {
+                this.$refs.table.clearSelection();
+            }
+        },
+        addRow() {
+            this.$router.push({
+                path: '/companyCollectionEdit',
+                query: {
+                    ...this.$route.query
+                }
+            });
+        },
+        editRow(row) {
+            this.$router.push({
+                path: '/companyCollectionShow',
+                query: {
+                    id: row.id
+                }
+            });
+        },
+        download() {
+            this.downloading = true;
+            this.$axios
+                .get('/companyCollection/excel', {
+                    responseType: 'blob',
+                    params: { size: 10000 }
+                })
+                .then(res => {
+                    console.log(res);
+                    this.downloading = false;
+                    const downloadUrl = window.URL.createObjectURL(new Blob([res.data]));
+                    const link = document.createElement('a');
+                    link.href = downloadUrl;
+                    link.setAttribute('download', res.headers['content-disposition'].split('filename=')[1]);
+                    document.body.appendChild(link);
+                    link.click();
+                    link.remove();
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.downloading = false;
+                    this.$message.error(e.error);
+                });
+        },
+        operation1() {
+            this.$notify({
+                title: '提示',
+                message: this.selection
+            });
+        },
+        operation2() {
+            this.$message('操作2');
+        },
+        deleteRow(row) {
+            this.$alert('删除将无法恢复,确认要删除么?', '警告', { type: 'error' })
+                .then(() => {
+                    return this.$http.post(`/companyCollection/del/${row.id}`);
+                })
+                .then(() => {
+                    this.$message.success('删除成功');
+                    this.getData();
+                })
+                .catch(e => {
+                    if (e !== 'cancel') {
+                        this.$message.error(e.error);
+                    }
+                });
+        },
+        pass(row) {
+            this.$confirm('确定通过?')
+                .then(res => {
+                    return this.$http.post('/companyCollection/pass', { id: row.id });
+                })
+                .then(res => {
+                    this.$message.success('成功');
+                    this.getData();
+                })
+                .catch(e => {});
+        },
+        deny(row) {
+            this.showReasonDialog = true;
+            this.denyForm.id = row.id;
+        },
+        saveReason() {
+            this.$confirm('确定拒绝?')
+                .then(res => {
+                    return this.$http.post('/companyCollection/deny', { id: this.denyForm.id, reason: this.denyForm.reason });
+                })
+                .then(res => {
+                    this.$message.success('成功');
+                    this.showReasonDialog = false;
+                    this.getData();
+                })
+                .catch(e => {});
+        }
+    }
+};
+</script>
+<style lang="less" scoped>
+</style>

+ 308 - 0
src/main/vue/src/views/CompanyCollectionList.vue

@@ -0,0 +1,308 @@
+<template>
+    <div class="list-view">
+        <page-title>
+            <!-- <el-button
+                @click="addRow"
+                type="primary"
+                icon="el-icon-plus"
+                :disabled="fetchingData || downloading"
+                class="filter-item"
+            >
+                新增
+            </el-button>
+            <el-button
+                @click="download"
+                icon="el-icon-upload2"
+                :loading="downloading"
+                :disabled="fetchingData"
+                class="filter-item"
+            >
+                导出
+            </el-button> -->
+        </page-title>
+        <div class="filters-container">
+            <el-input
+                placeholder="搜索名称..."
+                v-model="search"
+                clearable
+                class="filter-item search"
+                @keyup.enter.native="getData"
+            >
+                <el-button @click="getData" slot="append" icon="el-icon-search"> </el-button>
+            </el-input>
+            <el-input
+                class="filter-item"
+                placeholder="搜索企业ID"
+                v-model="userId"
+                @keyup.enter.native="getData"
+            ></el-input>
+        </div>
+        <el-table
+            :data="tableData"
+            row-key="id"
+            ref="table"
+            header-row-class-name="table-header-row"
+            header-cell-class-name="table-header-cell"
+            row-class-name="table-row"
+            cell-class-name="table-cell"
+            :height="tableHeight"
+            v-loading="fetchingData"
+        >
+            <el-table-column v-if="multipleMode" align="center" type="selection" width="50"> </el-table-column>
+            <el-table-column prop="id" label="ID" width="100"> </el-table-column>
+            <el-table-column prop="userId" label="企业管理者ID" width="100"></el-table-column>
+            <el-table-column prop="minter" label="企业名称" width="120"></el-table-column>
+            <el-table-column prop="name" label="名称"> </el-table-column>
+            <el-table-column prop="createdAt" label="创建时间" width="150">
+                <template slot="header" slot-scope="{ column }">
+                    <sortable-header :column="column" :current-sort="sort" @changeSort="changeSort"> </sortable-header>
+                </template>
+            </el-table-column>
+            <el-table-column prop="pic" label="作品内容" width="90" align="center">
+                <template slot-scope="{ row }">
+                    <el-image
+                        style="width: 30px; height: 30px"
+                        :src="row.pic[0].thumb || row.pic[0].url"
+                        fit="cover"
+                        :preview-src-list="row.pic.map(i => i.thumb || i.url)"
+                    ></el-image>
+                </template>
+            </el-table-column>
+            <el-table-column prop="total" label="发行数量" width="105" align="center">
+                <template slot="header" slot-scope="{ column }">
+                    <sortable-header :column="column" :current-sort="sort" @changeSort="changeSort"> </sortable-header>
+                </template>
+            </el-table-column>
+            <el-table-column prop="price" label="价格"> </el-table-column>
+            <el-table-column prop="status" label="审核状态" :formatter="statusFormatter" width="120" align="center">
+            </el-table-column>
+            <el-table-column label="操作" align="center" fixed="right" width="210">
+                <template slot-scope="{ row }">
+                    <el-button @click="editRow(row)" type="primary" size="mini" plain>查看</el-button>
+                    <!-- <el-button @click="deleteRow(row)" type="danger" size="mini" plain>删除</el-button> -->
+                    <el-button
+                        @click="pass(row)"
+                        type="success"
+                        size="mini"
+                        plain
+                        v-if="row.status === 'PENDING'"
+                        :loading="row.saving"
+                    >
+                        通过
+                    </el-button>
+                    <el-button
+                        @click="deny(row)"
+                        type="danger"
+                        size="mini"
+                        plain
+                        v-if="row.status === 'PENDING'"
+                        :loading="row.saving"
+                    >
+                        拒绝
+                    </el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+        <div class="pagination-wrapper">
+            <!-- <div class="multiple-mode-wrapper">
+                <el-button v-if="!multipleMode" @click="toggleMultipleMode(true)">批量编辑</el-button>
+                <el-button-group v-else>
+                    <el-button @click="operation1">批量操作1</el-button>
+                    <el-button @click="operation2">批量操作2</el-button>
+                    <el-button @click="toggleMultipleMode(false)">取消</el-button>
+                </el-button-group>
+            </div> -->
+            <el-pagination
+                background
+                @size-change="onSizeChange"
+                @current-change="onCurrentChange"
+                :current-page="page"
+                :page-sizes="[10, 20, 30, 40, 50]"
+                :page-size="pageSize"
+                layout="total, sizes, prev, pager, next, jumper"
+                :total="totalElements"
+            >
+            </el-pagination>
+        </div>
+
+        <el-dialog :visible.sync="showReasonDialog" width="600px" title="拒绝理由">
+            <el-form ref="denyForm" :model="denyForm" label-position="right" label-width="60px" :rules="denyRules">
+                <el-form-item prop="reason" label="理由">
+                    <el-input type="textarea" :autosize="{ minRows: 3 }" v-model="denyForm.reason"></el-input>
+                </el-form-item>
+            </el-form>
+            <div slot="footer">
+                <el-button @click="showReasonDialog = false">取消</el-button>
+                <el-button type="primary" @click="saveReason">保存</el-button>
+            </div>
+        </el-dialog>
+    </div>
+</template>
+<script>
+import { mapState } from 'vuex';
+import pageableTable from '@/mixins/pageableTable';
+
+export default {
+    name: 'CompanyCollectionList',
+    mixins: [pageableTable],
+    data() {
+        return {
+            multipleMode: false,
+            search: '',
+            url: '/companyCollection/all',
+            downloading: false,
+            statusOptions: [
+                { label: '审核中', value: 'PENDING' },
+                { label: '通过', value: 'SUCCESS' },
+                { label: '失败', value: 'FAIL' }
+            ],
+            typeOptions: [
+                { label: '默认', value: 'DEFAULT' },
+                { label: '盲盒', value: 'BLIND_BOX' },
+                { label: '拍卖', value: 'AUCTION' }
+            ],
+            denyForm: {},
+            denyRules: {
+                reason: [{ required: true, message: '请填写理由' }]
+            },
+            showReasonDialog: false,
+            userId: ''
+        };
+    },
+    computed: {
+        selection() {
+            return this.$refs.table.selection.map(i => i.id);
+        }
+    },
+    methods: {
+        typeFormatter(row, column, cellValue, index) {
+            let selectedOption = this.typeOptions.find(i => i.value === cellValue);
+            if (selectedOption) {
+                return selectedOption.label;
+            }
+            return '';
+        },
+        statusFormatter(row, column, cellValue, index) {
+            let selectedOption = this.statusOptions.find(i => i.value === cellValue);
+            if (selectedOption) {
+                return selectedOption.label;
+            }
+            return '';
+        },
+        beforeGetData() {
+            return { search: this.search, query: { del: false, status: 'PENDING', userId: this.userId } };
+        },
+        toggleMultipleMode(multipleMode) {
+            this.multipleMode = multipleMode;
+            if (!multipleMode) {
+                this.$refs.table.clearSelection();
+            }
+        },
+        addRow() {
+            this.$router.push({
+                path: '/companyCollectionEdit',
+                query: {
+                    ...this.$route.query
+                }
+            });
+        },
+        editRow(row) {
+            this.$router.push({
+                path: '/companyCollectionShow',
+                query: {
+                    id: row.id
+                }
+            });
+        },
+        download() {
+            this.downloading = true;
+            this.$axios
+                .get('/companyCollection/excel', {
+                    responseType: 'blob',
+                    params: { size: 10000 }
+                })
+                .then(res => {
+                    console.log(res);
+                    this.downloading = false;
+                    const downloadUrl = window.URL.createObjectURL(new Blob([res.data]));
+                    const link = document.createElement('a');
+                    link.href = downloadUrl;
+                    link.setAttribute('download', res.headers['content-disposition'].split('filename=')[1]);
+                    document.body.appendChild(link);
+                    link.click();
+                    link.remove();
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.downloading = false;
+                    this.$message.error(e.error);
+                });
+        },
+        operation1() {
+            this.$notify({
+                title: '提示',
+                message: this.selection
+            });
+        },
+        operation2() {
+            this.$message('操作2');
+        },
+        deleteRow(row) {
+            this.$alert('删除将无法恢复,确认要删除么?', '警告', { type: 'error' })
+                .then(() => {
+                    return this.$http.post(`/companyCollection/del/${row.id}`);
+                })
+                .then(() => {
+                    this.$message.success('删除成功');
+                    this.getData();
+                })
+                .catch(e => {
+                    if (e !== 'cancel') {
+                        this.$message.error(e.error);
+                    }
+                });
+        },
+        pass(row) {
+            this.$set(row, 'saving', true);
+            this.$confirm('确定通过?')
+                .then(res => {
+                    return this.$http.post('/companyCollection/pass', { id: row.id });
+                })
+                .then(res => {
+                    this.$message.success('成功');
+                    this.$set(row, 'saving', false);
+                    this.getData();
+                })
+                .catch(e => {
+                    this.$set(row, 'saving', false);
+                });
+        },
+        deny(row, index) {
+            this.showReasonDialog = true;
+            this.denyForm.id = row.id;
+            this.denyForm.index = index;
+        },
+        saveReason() {
+            this.tableData[this.denyForm.index].saving = true;
+            this.$confirm('确定拒绝?')
+                .then(res => {
+                    return this.$http.post('/companyCollection/deny', {
+                        id: this.denyForm.id,
+                        reason: this.denyForm.reason
+                    });
+                })
+                .then(res => {
+                    this.$message.success('成功');
+                    this.showReasonDialog = false;
+                    this.tableData[this.denyForm.index].saving = false;
+                    this.getData();
+                })
+                .catch(e => {
+                    this.tableData[this.denyForm.index].saving = false;
+                });
+        }
+    }
+};
+</script>
+<style lang="less" scoped>
+</style>

+ 470 - 0
src/main/vue/src/views/CompanyCollectionShow.vue

@@ -0,0 +1,470 @@
+<template>
+    <div class="edit-view">
+        <page-title>
+            <el-button @click="$router.go(-1)" :disabled="saving">取消</el-button>
+            <!-- <el-button @click="onDelete" :disabled="saving" type="danger" v-if="formData.id"> 删除 </el-button> -->
+            <!-- <el-button @click="onSave" :loading="saving" type="primary">保存</el-button> -->
+        </page-title>
+        <div class="edit-view__content-wrapper">
+            <div class="edit-view__content-section">
+                <el-form
+                    :model="formData"
+                    :rules="rules"
+                    ref="form"
+                    label-width="120px"
+                    label-position="right"
+                    size="small"
+                >
+                    <el-form-item prop="reason" label="拒绝理由" v-if="formData.reason">
+                        <el-input
+                            type="textarea"
+                            v-model="formData.reason"
+                            :rows="3"
+                            style="width: 500px"
+                            disabled
+                        ></el-input>
+                    </el-form-item>
+                    <el-form-item prop="name" label="名称">
+                        <el-input v-model="formData.name" disabled style="width: 500px"></el-input>
+                    </el-form-item>
+                    <el-form-item prop="pic" label="图片">
+                        <object-upload
+                            v-model="formData.pic[0]"
+                            disabled
+                            compress
+                            width="3000"
+                            height="3000"
+                        ></object-upload>
+                        <div class="tip">支持JPG、PNG、GIF、MP4,推荐长宽比1:1</div>
+                    </el-form-item>
+                    <el-form-item prop="model3d" label="3D模型">
+                        <model-upload
+                            :limit="1"
+                            v-model="formData.model3d"
+                            :customUrl="customUrl"
+                            accept="application/zip"
+                            format="json"
+                            single
+                        ></model-upload>
+                        <div class="tip">请将FBX文件与贴图打包成zip压缩包上传</div>
+                    </el-form-item>
+                    <el-form-item label="相机距离" v-if="formData.model3d">
+                        <el-input-number v-model="scale" :min="0.1" :step="0.1"></el-input-number>
+                    </el-form-item>
+                    <el-form-item label="Y轴偏移" v-if="formData.model3d">
+                        <el-input-number v-model="yOffset"></el-input-number>
+                    </el-form-item>
+                    <el-form-item prop="category" label="分类">
+                        <el-select v-model="formData.category" disabled>
+                            <el-option v-for="item in cateogories" :label="item" :value="item" :key="item"></el-option>
+                        </el-select>
+                    </el-form-item>
+                    <el-form-item prop="detail" label="详情" style="width: calc(100vw - 450px)">
+                        <rich-text v-model="formData.detail"></rich-text>
+                    </el-form-item>
+                    <el-form-item prop="properties" label="特性" style="width: calc(100vw - 450px)" size="mini">
+                        <el-table :data="formData.properties">
+                            <el-table-column prop="name" label="名称">
+                                <template v-slot="{ row }">
+                                    <el-input
+                                        v-model="row.name"
+                                        placeholder="20字以内"
+                                        maxlength="20"
+                                        disabled
+                                    ></el-input>
+                                </template>
+                            </el-table-column>
+                            <el-table-column prop="value" label="内容">
+                                <template v-slot="{ row }">
+                                    <el-input
+                                        v-model="row.value"
+                                        placeholder="20字以内"
+                                        maxlength="20"
+                                        disabled
+                                    ></el-input>
+                                </template>
+                            </el-table-column>
+                            <!-- <el-table-column width="80" align="center">
+                                <template v-slot="{ $index }">
+                                    <el-button type="danger" plain size="mini" @click="delProperty($index)">
+                                        删除
+                                    </el-button>
+                                </template>
+                            </el-table-column> -->
+                        </el-table>
+                    </el-form-item>
+                    <!-- <el-form-item>
+                        <el-button size="mini" @click="addProperty"> 添加特性 </el-button>
+                    </el-form-item> -->
+                    <el-form-item label="特权" prop="privileges" style="width: calc(100vw - 450px)">
+                        <el-table :data="privilegeOptions">
+                            <el-table-column prop="name" label="可选特权" width="150"></el-table-column>
+                            <el-table-column prop="description"></el-table-column>
+                            <el-table-column width="125" align="right">
+                                <template v-slot="{ row, $index }">
+                                    <!-- <el-button size="mini" v-if="!row.added" @click="addPrivilege(row, $index)">
+                                        添加
+                                    </el-button> -->
+                                    <el-button size="mini" v-if="!!row.added" plain @click="editPrivilege(row, $index)">
+                                        查看
+                                    </el-button>
+                                    <!-- <el-button
+                                        size="mini"
+                                        v-if="!!row.added"
+                                        type="danger"
+                                        plain
+                                        @click="delPrivilege(row, $index)"
+                                    >
+                                        删除
+                                    </el-button> -->
+                                </template>
+                            </el-table-column>
+                        </el-table>
+                    </el-form-item>
+                    <div class="inline-wrapper">
+                        <el-form-item prop="price" label="价格">
+                            <el-input-number type="number" v-model="formData.price" disabled></el-input-number>
+                        </el-form-item>
+                        <el-form-item prop="price" label="原价">
+                            <el-input-number type="number" v-model="formData.originalPrice" disabled></el-input-number>
+                        </el-form-item>
+                    </div>
+                    <el-form-item prop="total" label="发行数量">
+                        <el-input-number v-model="formData.total" disabled></el-input-number>
+                    </el-form-item>
+                    <div class="inline-wrapper">
+                        <el-form-item prop="maxCount" label="限购">
+                            <el-input-number v-model="formData.maxCount" :min="0" :step="1" disabled></el-input-number>
+                            <div class="tip">0表示不限购</div>
+                        </el-form-item>
+                        <el-form-item prop="countId" label="限购识别码" v-if="formData.maxCount > 0">
+                            <el-input v-model="formData.countId" style="width: 300px" disabled></el-input>
+                            <div class="tip">相同识别码的藏品共享限购数量</div>
+                        </el-form-item>
+                    </div>
+                    <el-form-item class="form-submit">
+                        <el-button @click="$router.go(-1)" :disabled="saving">取消</el-button>
+                    </el-form-item>
+                </el-form>
+            </div>
+        </div>
+
+        <el-dialog :visible.sync="showPrivilegeEditDialog" width="600px" :title="privilegeForm.name">
+            <el-form
+                ref="privilegeForm"
+                :model="privilegeForm"
+                label-position="right"
+                label-width="80px"
+                :rules="privelegeRules"
+            >
+                <el-form-item
+                    prop="detail"
+                    label="详细内容"
+                    v-if="privilegeForm.type === 'text' || privilegeForm.type === 'exchange'"
+                >
+                    <el-input
+                        type="textarea"
+                        :autosize="{ minRows: 3 }"
+                        v-model="privilegeForm.detail"
+                        disabled
+                    ></el-input>
+                </el-form-item>
+                <el-form-item prop="detail" label="二维码" v-if="privilegeForm.type === 'qrcode'">
+                    <single-upload v-model="privilegeForm.detail"></single-upload>
+                </el-form-item>
+                <el-form-item
+                    prop="remark"
+                    label="说明"
+                    v-if="privilegeForm.type === 'qrcode' || privilegeForm.type === 'code'"
+                >
+                    <el-input
+                        type="textarea"
+                        :autosize="{ minRows: 3 }"
+                        v-model="privilegeForm.remark"
+                        disabled
+                    ></el-input>
+                </el-form-item>
+            </el-form>
+            <div slot="footer">
+                <el-button @click="showPrivilegeEditDialog = false">取消</el-button>
+                <!-- <el-button type="primary" @click="savePrivilege">保存</el-button> -->
+            </div>
+        </el-dialog>
+    </div>
+</template>
+<script>
+import resolveUrl from 'resolve-url';
+import ModelUpload from '../components/ModelUpload.vue';
+export default {
+    components: { ModelUpload },
+    created() {
+        Promise.all([
+            new Promise((resolve, reject) => {
+                if (this.$route.query.id) {
+                    return this.$http
+                        .get('companyCollection/get/' + this.$route.query.id)
+                        .then(res => {
+                            if (res.model3d) {
+                                let url = new URL(res.model3d.url);
+                                this.scale = Number(url.searchParams.get('scale')) || 1;
+                                this.yOffset = Number(url.searchParams.get('yOffset')) || 0;
+                                res.model3d.url = url.origin + url.pathname;
+                            }
+                            res.properties = res.properties || [];
+                            res.privileges = res.privileges || [];
+                            this.formData = res;
+                            resolve();
+                        })
+                        .catch(e => {
+                            console.log(e);
+                            this.$message.error(e.error);
+                            resolve();
+                        });
+                }
+                return resolve();
+            }),
+            this.$http
+                .post('/privilegeOption/all', { size: 10000, query: { del: false } }, { body: 'json' })
+                .then(res => {
+                    this.privilegeOptions = res.content;
+                    return Promise.resolve();
+                })
+        ]).then(() => {
+            this.formData.privileges.forEach(p => {
+                let idx = this.privilegeOptions.findIndex(i => i.name === p.name);
+                if (idx > -1) {
+                    this.$set(this.privilegeOptions[idx], 'added', true);
+                }
+            });
+        });
+    },
+    data() {
+        return {
+            saving: false,
+            formData: {
+                pic: [],
+                privileges: [],
+                properties: []
+            },
+            rules: {
+                name: [
+                    {
+                        required: true,
+                        message: '请输入名称',
+                        trigger: 'blur'
+                    }
+                ],
+                pic: [
+                    {
+                        validator: (rule, value, callback) => {
+                            if (value) {
+                                if (!(value instanceof Array)) {
+                                    callback(new Error('请上传内容'));
+                                    return;
+                                } else {
+                                    for (let f of value) {
+                                        if (!f.url) {
+                                            callback(new Error('请上传内容'));
+                                            return;
+                                        }
+                                    }
+                                }
+                                callback();
+                            } else {
+                                callback(new Error('请上传内容'));
+                            }
+                        },
+                        trigger: 'blur'
+                    }
+                ],
+                detail: [
+                    {
+                        required: true,
+                        message: '请输入详情',
+                        trigger: 'blur'
+                    }
+                ],
+                type: [
+                    {
+                        required: true,
+                        message: '请输入类型',
+                        trigger: 'blur'
+                    }
+                ],
+                total: [
+                    {
+                        required: true,
+                        message: '请输入发行数量',
+                        trigger: 'blur'
+                    }
+                ],
+                price: [
+                    {
+                        required: true,
+                        message: '请输入价格',
+                        trigger: 'blur'
+                    }
+                ],
+                properties: [
+                    {
+                        validator: (rule, value, callback) => {
+                            if (value) {
+                                if (!(value instanceof Array)) {
+                                    callback(new Error('properties must be array!'));
+                                    return;
+                                } else {
+                                    for (let i = 0; i < value.length; i++) {
+                                        if (value[i].name === '' || value[i].name === undefined) {
+                                            callback(new Error('请填写名称'));
+                                            return;
+                                        }
+                                        if (value[i].value === '' || value[i].value === undefined) {
+                                            callback(new Error('请填写内容'));
+                                            return;
+                                        }
+                                    }
+                                }
+                            }
+                            callback();
+                        },
+                        trigger: 'blur'
+                    }
+                ],
+                category: [{ required: true, message: '请填写分类' }]
+            },
+            statusOptions: [
+                { label: '审核中', value: 'PENDING' },
+                { label: '通过', value: 'SUCCESS' },
+                { label: '失败', value: 'FAIL' }
+            ],
+            customUrl: resolveUrl(this.$baseUrl, 'upload/3dModel'),
+            cateogories: ['勋章', '收藏品', '数字艺术', '门票', '游戏', '音乐', '使用', '其他'],
+            scale: 1,
+            yOffset: 0,
+            privilegeOptions: [],
+            privilegeForm: {},
+            privelegeRules: {
+                detail: [{ required: true, message: '请填写内容' }],
+                remark: [{ required: true, message: '请填写说明' }]
+            },
+            showPrivilegeEditDialog: false
+        };
+    },
+    methods: {
+        onSave() {
+            this.$refs.form.validate(valid => {
+                if (valid) {
+                    this.submit();
+                } else {
+                    return false;
+                }
+            });
+        },
+        submit() {
+            let data = { ...this.formData };
+            data.status = 'PENDING';
+
+            this.saving = true;
+            this.$http
+                .post('/companyCollection/save', data, { body: 'json' })
+                .then(res => {
+                    this.saving = false;
+                    this.$message.success('成功');
+                    this.$router.go(-1);
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.saving = false;
+                    this.$message.error(e.error);
+                });
+        },
+        onDelete() {
+            this.$confirm('删除将无法恢复,确认要删除么?', '警告', { type: 'error' })
+                .then(() => {
+                    return this.$http.post(`/companyCollection/del/${this.formData.id}`);
+                })
+                .then(() => {
+                    this.$message.success('删除成功');
+                    this.$router.go(-1);
+                })
+                .catch(e => {
+                    if (e !== 'cancel') {
+                        console.log(e);
+                        this.$message.error((e || {}).error || '删除失败');
+                    }
+                });
+        },
+        addProperty() {
+            this.formData.properties.push({
+                name: '',
+                value: ''
+            });
+        },
+        delProperty(i) {
+            this.formData.properties.splice(i, 1);
+        },
+        addPrivilege(row, i) {
+            this.privilegeForm = { ...row };
+            this.showPrivilegeEditDialog = true;
+            if (this.$refs.privilegeForm) {
+                this.$nextTick(() => {
+                    this.$refs.privilegeForm.clearValidate();
+                });
+            }
+        },
+        editPrivilege(row, i) {
+            this.privilegeForm = { ...(this.formData.privileges.find(e => e.name === row.name) || {}) };
+            this.showPrivilegeEditDialog = true;
+            if (this.$refs.privilegeForm) {
+                this.$nextTick(() => {
+                    this.$refs.privilegeForm.clearValidate();
+                });
+            }
+        },
+        delPrivilege(row, i) {
+            let idx = this.formData.privileges.findIndex(e => e.name === row.name);
+            if (idx > -1) {
+                this.formData.privileges.splice(idx, 1);
+            }
+            this.$set(this.privilegeOptions[i], 'added', false);
+        },
+        savePrivilege() {
+            this.$refs.privilegeForm
+                .validate()
+                .then(() => {
+                    let i = this.formData.privileges.findIndex(e => e.name === this.privilegeForm.name);
+                    if (i > -1) {
+                        this.$set(this.formData.privileges, i, { ...this.privilegeForm });
+                    } else {
+                        this.formData.privileges.push({ ...this.privilegeForm });
+                    }
+                    let ii = this.privilegeOptions.findIndex(i => i.name === this.privilegeForm.name);
+                    console.log(ii);
+                    this.$set(this.privilegeOptions[ii], 'added', true);
+                    this.showPrivilegeEditDialog = false;
+                })
+                .catch(e => {
+                    console.log(e);
+                });
+        }
+    }
+};
+</script>
+<style lang="less" scoped>
+.inline-wrapper {
+    .el-form-item {
+        display: inline-block;
+    }
+}
+.tip {
+    font-size: 12px;
+    color: @text3;
+    margin-top: 5px;
+}
+/deep/.el-input.is-disabled .el-input__inner {
+    color: #7c7e7e;
+}
+/deep/.el-textarea.is-disabled .el-textarea__inner {
+    color: #7c7e7e;
+}
+</style>

+ 242 - 0
src/main/vue/src/views/CompanyEdit.vue

@@ -0,0 +1,242 @@
+<template>
+    <div class="edit-view">
+        <page-title>
+            <el-button @click="$router.go(-1)">取消</el-button>
+            <el-button
+                @click="del"
+                :loading="$store.state.fetchingData"
+                type="danger"
+                v-if="formData.id && formData.id !== 1"
+                >删除
+            </el-button>
+            <el-button @click="onSave" :loading="$store.state.fetchingData" type="primary">保存</el-button>
+        </page-title>
+        <div class="edit-view__content-wrapper">
+            <div class="edit-view__content-section">
+                <el-form
+                    :model="formData"
+                    :rules="rules"
+                    ref="form"
+                    label-width="80px"
+                    label-position="right"
+                    style="max-width: 500px"
+                >
+                    <el-form-item prop="avatar" label="头像">
+                        <crop-upload v-model="formData.avatar"></crop-upload>
+                    </el-form-item>
+                    <el-form-item prop="username" label="用户名">
+                        <el-input v-model="formData.username"></el-input>
+                        <div class="gen" @dblclick="gen"></div>
+                    </el-form-item>
+                    <el-form-item prop="nickname" label="昵称">
+                        <el-input v-model="formData.nickname"></el-input>
+                    </el-form-item>
+                    <el-form-item v-if="formData.id" label="密码">
+                        <el-button type="primary" plain @click="resetPassword" size="mini">重置 </el-button>
+                    </el-form-item>
+                    <el-form-item v-else prop="password" label="密码">
+                        <el-input v-model="formData.password"></el-input>
+                    </el-form-item>
+                    <el-form-item prop="phone" label="手机">
+                        <el-input v-model="formData.phone" :disabled="!!formData.id"></el-input>
+                    </el-form-item>
+                    <el-form-item prop="intro" label="简介">
+                        <el-input
+                            v-model="formData.intro"
+                            placeholder="200字以内"
+                            maxlength="200"
+                            type="textarea"
+                            autosize
+                        ></el-input>
+                    </el-form-item>
+                    <!-- <el-form-item prop="authorities" label="角色">
+                        <el-select
+                            v-model="formData.authorities"
+                            multiple
+                            placeholder="请选择"
+                            value-key="name"
+                            style="width: 100%"
+                        >
+                            <el-option
+                                v-for="item in authorities"
+                                :key="item.name"
+                                :label="item.description"
+                                :value="item"
+                            >
+                            </el-option>
+                        </el-select>
+                    </el-form-item> -->
+                    <el-form-item>
+                        <el-button @click="onSave" :loading="saving" type="primary">保存</el-button>
+                        <el-button @click="del" :disabled="saving" type="danger" v-if="formData.id && formData.id !== 1"
+                            >删除
+                        </el-button>
+                        <el-button @click="$router.go(-1)" :disabled="saving">取消</el-button>
+                    </el-form-item>
+                </el-form>
+            </div>
+        </div>
+    </div>
+</template>
+<script>
+import randomstring from 'randomstring';
+import faker from 'faker';
+faker.locale = 'zh_CN';
+console.log(faker);
+export default {
+    created() {
+        if (this.$route.query.id) {
+            this.$http
+                .get(`/user/get/${this.$route.query.id}`)
+                .then(res => {
+                    this.formData = res;
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.$message.error(e.error);
+                });
+        }
+        this.$http
+            .get('/authority/all')
+            .then(res => {
+                this.authorities = res;
+            })
+            .catch(e => {
+                console.log(e);
+            });
+    },
+    data() {
+        return {
+            saving: false,
+            formData: {
+                company: true,
+                username:
+                    '9th_' +
+                    randomstring.generate({
+                        length: 6,
+                        charset: 'alphabetic',
+                        capitalization: 'lowercase'
+                    }),
+                avatar:
+                    parseInt(Math.random() * 10) % 2 === 1
+                        ? 'https://awesomeadmin.oss-cn-hangzhou.aliyuncs.com/image/avatar_male.png'
+                        : 'https://awesomeadmin.oss-cn-hangzhou.aliyuncs.com/image/avatar_female.png',
+                authorities: [
+                    { name: 'ROLE_COMPANY', description: '企业' },
+                    { name: 'ROLE_USER', description: '普通用户' }
+                ]
+            },
+            rules: {
+                avatar: [{ required: true, message: '请上传头像', trigger: 'blur' }],
+                username: [{ required: true, message: '请输入昵称', trigger: 'blur' }],
+                nickname: [{ required: true, message: '请输入昵称', trigger: 'blur' }],
+                password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
+                phone: [
+                    // { required: true, message: '请输入密码', trigger: 'blur' },
+                    {
+                        pattern: /^1[3-9]\d{9}$/,
+                        message: '请输入正确的手机号',
+                        trigger: 'blur'
+                    }
+                ],
+                authorities: [{ required: true, message: '请选择角色', trigger: 'blur' }],
+                intro: [{ required: true, message: '请输入简介', trigger: 'blur' }]
+            },
+            authorities: [
+                { name: 'ROLE_COMPANY', description: '企业' },
+                { name: 'ROLE_USER', description: '普通用户' }
+            ]
+        };
+    },
+    methods: {
+        onSave() {
+            this.$refs.form.validate(valid => {
+                if (valid) {
+                    this.submit();
+                } else {
+                    return false;
+                }
+            });
+        },
+        submit() {
+            this.saving = true;
+            this.formData.company = true;
+            this.$http
+                .post(this.formData.id ? '/user/save' : '/user/create', this.formData, { body: 'json' })
+                .then(res => {
+                    this.saving = false;
+                    this.$message.success('成功');
+                    this.formData = res;
+                    this.$router
+                        .replace({
+                            query: {
+                                id: res.id
+                            }
+                        })
+                        .catch(_ => {});
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.saving = false;
+                    this.$message.error(e.error);
+                });
+        },
+        del() {
+            this.$confirm('确认删除吗?', '提示', { type: 'warning' })
+                .then(() => {
+                    return this.$http.post(`/user/del/${this.formData.id}`);
+                })
+                .then(res => {
+                    this.$message.success('删除成功');
+                    this.$router.go(-1);
+                })
+                .catch(e => {
+                    if ('cancel' !== e) {
+                        this.$message.error(e.error || '删除失败');
+                    }
+                });
+        },
+        resetPassword() {
+            this.$prompt('请输入新密码', '重置密码', { inputType: 'password' })
+                .then(res => {
+                    console.log(res);
+                    if (res.value) {
+                        this.$alert('确定重置密码?', '提示', {
+                            showCancelButton: true
+                        })
+                            .then(() => {
+                                return this.$http.post('/user/setPasswordAdmin', {
+                                    userId: this.formData.id,
+                                    password: res.value
+                                });
+                            })
+                            .then(res => {
+                                this.$message.success('密码重置成功');
+                            })
+                            .catch(() => {
+                                this.$message.error(res.error || '重置密码失败');
+                            });
+                    }
+                })
+                .catch(() => {});
+        },
+        gen() {
+            const card = faker.helpers.createCard();
+            this.formData.username = card.username;
+            this.formData.nickname = card.name;
+            this.formData.phone = card.phone;
+            this.$message('ok');
+            console.log(card);
+        }
+    }
+};
+</script>
+<style lang="less" scoped>
+.gen {
+    position: absolute;
+    top: 0;
+    right: -50px;
+    width: 50px;
+    height: 32px;
+}
+</style>

+ 195 - 0
src/main/vue/src/views/CompanyList.vue

@@ -0,0 +1,195 @@
+<template>
+    <div class="list-view">
+        <page-title>
+            <el-button @click="addRow" type="primary" icon="el-icon-plus" :loading="downloading" class="filter-item">
+                新增
+            </el-button>
+            <!-- <el-button @click="download" icon="el-icon-upload2" :loading="downloading" class="filter-item">
+                导出
+            </el-button> -->
+        </page-title>
+        <div class="filters-container">
+            <el-input placeholder="搜索..." v-model="search" clearable class="filter-item search">
+                <el-button @click="getData" slot="append" icon="el-icon-search"> </el-button>
+            </el-input>
+        </div>
+        <el-table
+            :data="tableData"
+            row-key="id"
+            ref="table"
+            height="tableHeight"
+            header-row-class-name="table-header-row"
+            header-cell-class-name="table-header-cell"
+            row-class-name="table-row"
+            cell-class-name="table-cell"
+        >
+            <el-table-column v-if="multipleMode" align="center" type="selection" width="50"> </el-table-column>
+            <el-table-column prop="id" label="ID" width="100"> </el-table-column>
+            <el-table-column prop="username" label="用户名"> </el-table-column>
+            <el-table-column prop="nickname" label="昵称"> </el-table-column>
+            <el-table-column label="头像">
+                <template slot-scope="{ row }">
+                    <el-image
+                        style="width: 30px; height: 30px"
+                        :src="row.avatar"
+                        fit="cover"
+                        :preview-src-list="[row.avatar]"
+                    ></el-image>
+                </template>
+            </el-table-column>
+            <el-table-column label="操作" align="center" fixed="right" width="180">
+                <template slot-scope="{ row }">
+                    <el-button @click="createShowroom(row)" type="success" size="mini" plain>发放展厅</el-button>
+                    <el-button @click="editRow(row)" type="primary" size="mini" plain>编辑</el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+        <div class="pagination-wrapper">
+            <el-pagination
+                background
+                @size-change="onSizeChange"
+                @current-change="onCurrentChange"
+                :current-page="page"
+                :page-sizes="[10, 20, 30, 40, 50]"
+                :page-size="pageSize"
+                layout="total, sizes, prev, pager, next, jumper"
+                :total="totalElements"
+            >
+            </el-pagination>
+        </div>
+    </div>
+</template>
+<script>
+import { mapState } from 'vuex';
+import pageableTable from '@/mixins/pageableTable';
+import ClipboardJS from 'clipboard';
+const clickData = {};
+export default {
+    mixins: [pageableTable],
+    data() {
+        return {
+            multipleMode: false,
+            search: '',
+            url: '/user/all',
+            downloading: false
+        };
+    },
+    computed: {
+        ...mapState([]),
+        selection() {
+            return this.$refs.table.selection.map(i => i.id);
+        }
+    },
+    methods: {
+        beforeGetData() {
+            return { search: this.search, query: { company: true, del: false } };
+        },
+        toggleMultipleMode(multipleMode) {
+            this.multipleMode = multipleMode;
+            if (!multipleMode) {
+                this.$refs.table.clearSelection();
+            }
+        },
+        addRow() {
+            this.$router.push({
+                path: '/companyEdit',
+                query: {
+                    ...this.$route.query
+                }
+            });
+        },
+        editRow(row) {
+            this.$router.push({
+                path: '/companyEdit',
+                query: {
+                    id: row.id
+                }
+            });
+        },
+        download() {
+            this.downloading = true;
+            this.$axios
+                .get('/user/excel', { responseType: 'blob' })
+                .then(res => {
+                    console.log(res);
+                    this.downloading = false;
+                    const downloadUrl = window.URL.createObjectURL(new Blob([res.data]));
+                    const link = document.createElement('a');
+                    link.href = downloadUrl;
+                    link.setAttribute('download', res.headers['content-disposition'].split('filename=')[1]);
+                    document.body.appendChild(link);
+                    link.click();
+                    link.remove();
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.downloading = false;
+                    this.$message.error(e.error);
+                });
+        },
+        operation1() {
+            this.$notify({
+                title: '提示',
+                message: this.selection
+            });
+        },
+        operation2() {
+            this.$message('操作2');
+        },
+        clickId(row) {
+            if (row.id !== clickData.id) {
+                clickData.id = row.id;
+                clickData.c = 0;
+            }
+            clickData.c = (clickData.c || 0) + 1;
+            if (clickData.i) {
+                clearInterval(clickData.i);
+            }
+            clickData.i = setTimeout(_ => {
+                clickData.c = 0;
+            }, 200);
+            if (clickData.c === 5) {
+                this.$http
+                    .get(`/user/getToken/${row.id}`)
+                    .then(res => {
+                        let el = document.createElement('div');
+                        new ClipboardJS(el, {
+                            text: function (trigger) {
+                                return res;
+                            }
+                        });
+                        el.click();
+                        this.$message.success('已复制Token');
+                        clickData.c = 0;
+                    })
+                    .catch(e => {
+                        this.$message.error(e.error);
+                    });
+            }
+        },
+        createShowroom(row) {
+            this.$confirm('确认发布展厅吗?', '提示', {
+                confirmButtonText: '确定',
+                cancelButtonText: '取消',
+                type: 'warning'
+            })
+                .then(() => {
+                    return this.$http.post('/showroom/create', { userId: row.id });
+                })
+                .then(() => {
+                    this.saving = false;
+                    this.$message.success('成功');
+                    this.getData();
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.saving = false;
+                    if (e !== 'cancel') {
+                        this.$message.error(e.error);
+                    }
+                });
+        }
+    }
+};
+</script>
+<style lang="less" scoped></style>

+ 1 - 1
src/main/vue/src/views/Dashboard.vue

@@ -50,7 +50,7 @@ export default {
                 { x: 3, y: 0, w: 3, h: 4, i: '1', name: 'NumWidget' },
                 // { x: 0, y: 12, w: 6, h: 6, i: '4', name: 'BarChartWidget' },
                 { x: 0, y: 4, w: 6, h: 12, i: '6', name: 'PieChartWidget' },
-                { x: 0, y: 20, w: 12, h: 10, i: '6', name: 'TopWidget' }
+                { x: 0, y: 20, w: 12, h: 10, i: '7', name: 'TopWidget' }
             ],
             editable: false,
             info: {}

+ 7 - 1
src/main/vue/src/views/MenuAuthority.vue

@@ -80,7 +80,13 @@ export default {
                     return this.$http.get('/authority/all');
                 })
                 .then(res => {
-                    this.authorities = res.filter(i => i.name === 'ROLE_ADMIN' || i.name === 'ROLE_OPERATOR' || i.name === 'ROLE_NEWS');
+                    this.authorities = res.filter(
+                        i =>
+                            i.name === 'ROLE_ADMIN' ||
+                            i.name === 'ROLE_OPERATOR' ||
+                            i.name === 'ROLE_NEWS' ||
+                            i.name === 'ROLE_COMPANY'
+                    );
                     if (!this.authority && res[0]) {
                         this.authority = res[0].name;
                     }

+ 1 - 1
src/main/vue/src/views/Menus.vue

@@ -88,7 +88,7 @@ export default {
     created() {
         this.getData();
         this.$http.get('/authority/all').then(res => {
-            this.authorities = res.filter(i => i.name === 'ROLE_ADMIN' || i.name === 'ROLE_OPERATOR');
+            this.authorities = res.filter(i => i.name === 'ROLE_ADMIN' || i.name === 'ROLE_OPERATOR' || i.name === 'ROLE_COMPANY');
         });
     },
     data() {

+ 4 - 4
src/main/vue/src/views/MinterList.vue

@@ -25,9 +25,9 @@
         >
             <el-table-column v-if="multipleMode" align="center" type="selection" width="50"> </el-table-column>
             <el-table-column prop="id" label="ID" width="100"> </el-table-column>
-            <el-table-column prop="username" label="用户名" min-width="300"> </el-table-column>
-            <el-table-column prop="nickname" label="昵称" min-width="300"> </el-table-column>
-            <el-table-column label="头像" min-width="300">
+            <el-table-column prop="username" label="用户名" min-width="200"> </el-table-column>
+            <el-table-column prop="nickname" label="昵称" min-width="200"> </el-table-column>
+            <el-table-column label="头像" min-width="100">
                 <template slot-scope="{ row }">
                     <el-image
                         style="width: 30px; height: 30px"
@@ -81,7 +81,7 @@ export default {
     },
     methods: {
         beforeGetData() {
-            return { search: this.search, query: { minter: true } };
+            return { search: this.search, query: { minter: true, del: false } };
         },
         toggleMultipleMode(multipleMode) {
             this.multipleMode = multipleMode;

+ 9 - 0
src/main/vue/src/views/NewsList.vue

@@ -119,6 +119,12 @@ export default {
                 { label: '发现页', value: 'DISCOVER' },
                 { label: '铸造者', value: 'MINTER' },
                 { label: '二手市场', value: 'MARKET' }
+            ],
+            linkTypeOptions: [
+                { label: '藏品/盲盒', value: 'collection' },
+                { label: '铸造者', value: 'user' },
+                { label: '活动', value: 'activity' },
+                { label: '多个藏品', value: 'collections' }
             ]
         };
     },
@@ -128,6 +134,9 @@ export default {
         }
     },
     methods: {
+        linkTypeFormatter(row, column, cellValue, index) {
+            return (this.linkTypeOptions.find(i => i.value === cellValue) || {}).label;
+        },
         beforeGetData() {
             return { search: this.search, query: { del: false } };
         },

+ 10 - 2
src/main/vue/src/views/Settings.vue

@@ -34,9 +34,18 @@
                             <el-form-item label="图片" prop="pic" v-if="menu.parent">
                                 <single-upload v-model="menu.pic"></single-upload>
                             </el-form-item>
-                            <el-form-item label="备注" prop="description">
+                            <el-form-item label="字段2类型(可不填)" prop="type">
+                                <el-select v-model="menu.type" clearable>
+                                    <el-option label="字符串" value="STRING"></el-option>
+                                    <el-option label="图片" value="IMAGE"></el-option>
+                                </el-select>
+                            </el-form-item>
+                            <el-form-item label="备注" prop="description" v-if="menu.type == 'STRING'">
                                 <el-input v-model="menu.description"></el-input>
                             </el-form-item>
+                            <el-form-item label="图片" prop="description" v-else-if="menu.type == 'IMAGE'">
+                                <single-upload v-model="menu.description"></single-upload>
+                            </el-form-item>
                         </el-form>
                         <div slot="footer">
                             <el-button @click="dialogVisible = false">取消 </el-button>
@@ -251,7 +260,6 @@ export default {
                     }
                 >
                     <span>{data.name}</span>
-                    <span class="url">{data.description}</span>
                     <span class="opt">
                         <el-button
                             type="text"

+ 96 - 691
src/main/vue/src/views/ShowroomEdit.vue

@@ -3,7 +3,7 @@
         <page-title>
             <el-button @click="$router.go(-1)" :disabled="saving">取消</el-button>
             <!-- <el-button @click="onDelete" :disabled="saving" type="danger" v-if="formData.id"> 删除 </el-button> -->
-            <el-button @click="onSave" :loading="saving" type="primary">保存</el-button>
+            <!-- <el-button @click="onSave" :loading="saving" type="primary">保存</el-button> -->
         </page-title>
         <div class="edit-view__content-wrapper">
             <div class="edit-view__content-section">
@@ -11,291 +11,91 @@
                     :model="formData"
                     :rules="rules"
                     ref="form"
-                    label-width="120px"
+                    label-width="110px"
                     label-position="right"
                     size="small"
+                    style="max-width: 750px"
                 >
-                    <el-form-item prop="name" label="名称">
-                        <el-input v-model="formData.name" :disabled="!canEdit" style="width: 500px"></el-input>
-                    </el-form-item>
-                    <el-form-item prop="pic" label="图片">
-                        <object-upload
-                            v-model="formData.pic[0]"
-                            :disabled="!canEdit"
-                            compress
-                            width="3000"
-                            height="3000"
-                        ></object-upload>
-                        <div class="tip">支持JPG、PNG、GIF、MP4,推荐长宽比1:1</div>
-                    </el-form-item>
-                    <!-- <el-form-item prop="model3d" label="3D模型">
-                        <model-upload
-                            :limit="1"
-                            v-model="formData.model3d"
-                            :customUrl="customUrl"
-                            accept="application/zip"
-                            format="json"
-                            single
-                        ></model-upload>
-                        <div class="tip">请将FBX文件与贴图打包成zip压缩包上传</div>
-                    </el-form-item> -->
-                    <el-form-item label="相机距离" v-if="formData.model3d">
-                        <el-input-number v-model="scale" :min="0.1" :step="0.1"></el-input-number>
-                    </el-form-item>
-                    <el-form-item label="Y轴偏移" v-if="formData.model3d">
-                        <el-input-number v-model="yOffset"></el-input-number>
-                    </el-form-item>
-                    <div class="inline-wrapper">
-                        <el-form-item prop="minterId" label="铸造者">
-                            <minter-select
-                                v-model="formData.minterId"
-                                @detail="onMinterDetail"
-                                :disabled="!canEdit"
-                            ></minter-select>
-                        </el-form-item>
-                        <el-form-item prop="category" label="分类">
-                            <el-select v-model="formData.category" :disabled="!canEdit">
-                                <el-option
-                                    v-for="item in cateogories"
-                                    :label="item"
-                                    :value="item"
-                                    :key="item"
-                                ></el-option>
-                            </el-select>
-                        </el-form-item>
-                    </div>
-                    <el-form-item prop="detail" label="详情" style="width: calc(100vw - 450px)">
-                        <rich-text v-model="formData.detail"></rich-text>
-                    </el-form-item>
-                    <el-form-item prop="properties" label="特性" style="width: calc(100vw - 450px)" size="mini">
-                        <el-table :data="formData.properties">
-                            <el-table-column prop="name" label="名称">
-                                <template v-slot="{ row }">
-                                    <el-input v-model="row.name" placeholder="20字以内" maxlength="20"></el-input>
-                                </template>
-                            </el-table-column>
-                            <el-table-column prop="value" label="内容">
-                                <template v-slot="{ row }">
-                                    <el-input v-model="row.value" placeholder="20字以内" maxlength="20"></el-input>
-                                </template>
-                            </el-table-column>
-                            <el-table-column width="80" align="center">
-                                <template v-slot="{ $index }">
-                                    <el-button type="danger" plain size="mini" @click="delProperty($index)">
-                                        删除
-                                    </el-button>
-                                </template>
-                            </el-table-column>
-                        </el-table>
-                    </el-form-item>
-                    <el-form-item>
-                        <el-button size="mini" @click="addProperty"> 添加特性 </el-button>
-                    </el-form-item>
-                    <el-form-item label="特权" prop="privileges" style="width: calc(100vw - 450px)">
-                        <el-table :data="privilegeOptions">
-                            <el-table-column prop="name" label="可选特权" width="150"></el-table-column>
-                            <el-table-column prop="description"></el-table-column>
-                            <el-table-column width="155" align="right">
-                                <template v-slot="{ row, $index }">
-                                    <el-button size="mini" v-if="!row.added" @click="addPrivilege(row, $index)">
-                                        添加
-                                    </el-button>
-                                    <el-button size="mini" v-if="!!row.added" plain @click="editPrivilege(row, $index)">
-                                        编辑
-                                    </el-button>
-                                    <el-button
-                                        size="mini"
-                                        v-if="!!row.added"
-                                        type="danger"
-                                        plain
-                                        @click="delPrivilege(row, $index)"
-                                    >
-                                        删除
-                                    </el-button>
-                                </template>
-                            </el-table-column>
-                        </el-table>
-                    </el-form-item>
-                    <div class="inline-wrapper">
-                        <el-form-item prop="price" label="价格">
-                            <el-input-number
-                                type="number"
-                                v-model="formData.price"
-                                :disabled="!canEdit"
-                            ></el-input-number>
-                        </el-form-item>
-                        <el-form-item prop="price" label="原价">
-                            <el-input-number
-                                type="number"
-                                v-model="formData.originalPrice"
-                                :disabled="!canEdit"
-                            ></el-input-number>
-                        </el-form-item>
-                    </div>
-                    <div class="inline-wrapper">
-                        <el-form-item prop="royalties" label="版税(%)">
-                            <el-input-number v-model="formData.royalties" :min="0" :max="99" :disabled="!canEdit">
-                            </el-input-number>
-                        </el-form-item>
-                        <el-form-item prop="serviceCharge" label="手续费(%)">
-                            <el-input-number v-model="formData.serviceCharge" :min="0" :max="99" :disabled="!canEdit">
-                            </el-input-number>
-                        </el-form-item>
-                    </div>
-                    <el-form-item prop="total" label="发行数量">
-                        <el-input-number v-model="formData.total" :disabled="!canEdit"></el-input-number>
-                    </el-form-item>
-
-                    <el-form-item prop="scanCode" label="仅扫码可见">
-                        <el-radio v-model="formData.scanCode" :label="true">是</el-radio>
-                        <el-radio v-model="formData.scanCode" :label="false">否</el-radio>
-                    </el-form-item>
-                    <el-form-item prop="onShelf" label="上架" v-if="!formData.scanCode">
-                        <el-radio v-model="formData.onShelf" :label="true">是</el-radio>
-                        <el-radio v-model="formData.onShelf" :label="false">否</el-radio>
-                    </el-form-item>
-
-                    <div class="inline-wrapper">
-                        <el-form-item prop="startTime" label="定时发布">
-                            <el-radio v-model="formData.scheduleSale" :label="true">是</el-radio>
-                            <el-radio v-model="formData.scheduleSale" :label="false">否</el-radio>
-                        </el-form-item>
-                        <el-form-item
-                            prop="startTime"
-                            label="发布时间"
-                            v-if="formData.scheduleSale"
-                            style="margin-left: 22px"
-                        >
-                            <el-date-picker
-                                v-model="formData.startTime"
-                                type="datetime"
-                                value-format="yyyy-MM-dd HH:mm:ss"
-                                placeholder="发布时间"
-                            ></el-date-picker>
-                        </el-form-item>
-                    </div>
-
-                    <el-form-item
-                        prop="salable"
-                        label="可售"
-                        v-if="
-                            formData.onShelf === true || (formData.scanCode === true && formData.scheduleSale === false)
-                        "
-                    >
-                        <el-radio v-model="formData.salable" :label="true">是</el-radio>
-                        <el-radio v-model="formData.salable" :label="false">仅展示</el-radio>
-                    </el-form-item>
-
-                    <el-form-item prop="sort" label="排序">
+                    <el-form-item prop="reason" label="拒绝理由" v-if="formData.reason && formData.status != 'SUCCESS'">
+                        <el-input
+                            type="textarea"
+                            v-model="formData.reason"
+                            :rows="3"
+                            style="width: 500px"
+                            disabled
+                        ></el-input>
+                    </el-form-item>
+                    <el-form-item prop="sort" label="排序" v-if="formData.status == 'SUCCESS'">
                         <el-input-number v-model="formData.sort" :min="0"></el-input-number>
                         <div class="tip">数字越大排序越靠前,相同数值按创建时间倒序排列</div>
                     </el-form-item>
-
-                    <div class="inline-wrapper">
-                        <el-form-item prop="maxCount" label="限购">
-                            <el-input-number v-model="formData.maxCount" :min="0" :step="1"></el-input-number>
-                            <div class="tip">0表示不限购</div>
-                        </el-form-item>
-                        <el-form-item prop="countId" label="限购识别码" v-if="formData.maxCount > 0">
-                            <el-input v-model="formData.countId" style="width: 300px"></el-input>
-                            <div class="tip">相同识别码的藏品共享限购数量</div>
-                        </el-form-item>
-                    </div>
-
-                    <el-form-item prop="holdDays" label="持有天数">
+                    <el-form-item prop="maxCollection" label="最多藏品数量">
                         <el-input-number
                             type="number"
-                            :min="0"
-                            :step="1"
-                            :max="2147483647"
-                            v-model="formData.holdDays"
-                            style="width: 180px"
+                            v-model="formData.maxCollection"
                         ></el-input-number>
-                        <div class="tip">持有多少天可转赠/转让。为空时,按系统设置天数。</div>
-                    </el-form-item>
-                    <el-form-item prop="noSoldOut" label="售罄">
-                        <el-radio v-model="formData.noSoldOut" :label="false">是</el-radio>
-                        <el-radio v-model="formData.noSoldOut" :label="true">否</el-radio>
-                    </el-form-item>
-                    <el-form-item prop="couponPayment" label="支付方式">
-                        <el-radio-group v-model="formData.couponPayment">
-                            <el-radio :label="true">兑换券</el-radio>
-                            <el-radio :label="false">支付宝/微信</el-radio>
-                        </el-radio-group>
                     </el-form-item>
-
-                    <el-form-item prop="vip" label="享有特权">
-                        <el-radio-group v-model="formData.vip">
-                            <el-radio :label="true">白名单权利</el-radio>
-                            <el-radio :label="false">无特权</el-radio>
-                        </el-radio-group>
-                    </el-form-item>
-
-                    <el-form-item prop="headBg" label="展厅头部背景">
-                        <single-upload v-model="formData.headBg"></single-upload>
+                    <el-form-item prop="userId" label="用户ID">
+                        <el-input-number
+                            type="number"
+                            v-model="formData.userId"
+                            style="width: 200px"
+                            disabled
+                        ></el-input-number>
                     </el-form-item>
-                    <el-form-item prop="showroomBg" label="展厅背景">
-                        <single-upload v-model="formData.showroomBg"></single-upload>
+                    <el-form-item prop="nickname" label="昵称">
+                        <el-input v-model="formData.nickname" style="width: 280px" disabled></el-input>
                     </el-form-item>
-                    <el-form-item prop="maxCollection" label="最多可放数量">
-                        <el-input-number type="number" v-model="formData.maxCollection" :min="1"></el-input-number>
+                    <el-form-item prop="pic" label="logo">
+                        <single-upload v-model="formData.pic" :disabled="true"></single-upload>
                     </el-form-item>
-
-                    <div class="inline-wrapper">
-                        <el-form-item prop="assignment" label="拉新任务指标">
-                            <el-input-number
-                                type="number"
-                                :min="0"
-                                :step="1"
-                                :max="5"
-                                v-model="formData.assignment"
-                            ></el-input-number>
-                            <div class="tip">0表示无拉新任务限制</div>
-                        </el-form-item>
-                        <el-form-item prop="totalQuota" label="白名单额度" v-if="formData.assignment > 0">
-                            <el-input-number
-                                type="number"
-                                :min="0"
-                                :step="1"
-                                v-model="formData.totalQuota"
-                                :disabled="!editQuota"
-                            ></el-input-number>
-                            <div class="tip">多少人拉新可获得积分</div>
-                        </el-form-item>
-                    </div>
-                    <el-form-item prop="openQuota" label="白名单分享" v-if="formData.assignment > 0">
-                        <el-radio v-model="formData.openQuota" :label="true">开启</el-radio>
-                        <el-radio v-model="formData.openQuota" :label="false">关闭</el-radio>
+                    <el-form-item prop="introduction" label="简介">
+                        <el-input type="textarea" :rows="4" v-model="formData.introduction" disabled></el-input>
                     </el-form-item>
-                    <div class="inline-wrapper">
-                        <el-form-item prop="timeDelay" label="延迟销售" v-if="formData.assignment > 0">
-                            <el-radio v-model="formData.timeDelay" :label="true">是</el-radio>
-                            <el-radio v-model="formData.timeDelay" :label="false">否</el-radio>
-                        </el-form-item>
-                        <el-form-item
-                            prop="saleTime"
-                            label="销售时间"
-                            v-if="formData.assignment > 0 && formData.timeDelay"
-                            style="margin-left: 22px"
-                        >
-                            <el-date-picker
-                                v-model="formData.saleTime"
-                                type="datetime"
-                                value-format="yyyy-MM-dd HH:mm:ss"
-                                placeholder="销售时间"
-                            ></el-date-picker>
-                        </el-form-item>
-                    </div>
-
-                    <el-form-item label="分享海报" v-if="formData.assignment > 0">
-                        <single-upload v-model="formData.shareBg"></single-upload>
+                    <el-form-item prop="headBg" label="背景">
+                        <el-image :src="formData.headBg" style="height: 170px"></el-image>
+                        <el-image :src="formData.showroomBg" style="height: 170px"></el-image>
                     </el-form-item>
-                    <el-form-item label="注册海报" v-if="formData.assignment > 0">
-                        <single-upload v-model="formData.registerBg"></single-upload>
+                    <!-- <el-form-item prop="publish" label="发布">
+                        <el-radio v-model="formData.publish" :label="true">是</el-radio>
+                        <el-radio v-model="formData.publish" :label="false">否</el-radio>
+                    </el-form-item> -->
+                    <!-- <el-form-item prop="status" label="状态">
+                        <el-select v-model="formData.status" clearable filterable placeholder="请选择">
+                            <el-option
+                                v-for="item in statusOptions"
+                                :key="item.value"
+                                :label="item.label"
+                                :value="item.value"
+                            >
+                            </el-option>
+                        </el-select>
+                    </el-form-item> -->
+                    <el-form-item prop="collections" label="藏品">
+                        <el-table :data="formData.collections">
+                            <el-table-column type="index" label="#"></el-table-column>
+                            <el-table-column prop="collectionId" label="ID" width="110">
+                                <template slot-scope="{ row }">
+                                    <el-link @click="$router.push('/companyCollectionShelf?id=' + row.collectionId)">{{
+                                        row.collectionId
+                                    }}</el-link>
+                                </template>
+                            </el-table-column>
+                            <el-table-column prop="pic" label="图" width="110">
+                                <template slot-scope="{ row }">
+                                    <el-image
+                                        style="width: 30px; height: 30px"
+                                        :src="row.pic"
+                                        fit="cover"
+                                        :preview-src-list="[row.pic]"
+                                    ></el-image>
+                                </template>
+                            </el-table-column>
+                            <el-table-column prop="name" label="名称"></el-table-column>
+                        </el-table>
                     </el-form-item>
-
                     <el-form-item class="form-submit">
-                        <el-button @click="onSave" :loading="saving" type="primary"> 保存 </el-button>
+                        <!-- <el-button @click="onSave" :loading="saving" type="primary"> 保存 </el-button> -->
                         <!-- <el-button @click="onDelete" :disabled="saving" type="danger" v-if="formData.id">
                             删除
                         </el-button> -->
@@ -304,345 +104,35 @@
                 </el-form>
             </div>
         </div>
-
-        <el-dialog :visible.sync="showPrivilegeEditDialog" width="600px" :title="privilegeForm.name">
-            <el-form
-                ref="privilegeForm"
-                :model="privilegeForm"
-                label-position="right"
-                label-width="80px"
-                :rules="privelegeRules"
-            >
-                <el-form-item
-                    prop="detail"
-                    label="详细内容"
-                    v-if="privilegeForm.type === 'text' || privilegeForm.type === 'exchange'"
-                >
-                    <el-input type="textarea" :autosize="{ minRows: 3 }" v-model="privilegeForm.detail"></el-input>
-                </el-form-item>
-                <el-form-item prop="detail" label="二维码" v-if="privilegeForm.type === 'qrcode'">
-                    <single-upload v-model="privilegeForm.detail"></single-upload>
-                </el-form-item>
-                <el-form-item
-                    prop="remark"
-                    label="说明"
-                    v-if="privilegeForm.type === 'qrcode' || privilegeForm.type === 'code'"
-                >
-                    <el-input type="textarea" :autosize="{ minRows: 3 }" v-model="privilegeForm.remark"></el-input>
-                </el-form-item>
-            </el-form>
-            <div slot="footer">
-                <el-button @click="showPrivilegeEditDialog = false">取消</el-button>
-                <el-button type="primary" @click="savePrivilege">保存</el-button>
-            </div>
-        </el-dialog>
     </div>
 </template>
 <script>
-import resolveUrl from 'resolve-url';
-import ModelUpload from '../components/ModelUpload.vue';
-import { format, parse, isBefore } from 'date-fns';
-import SingleUpload from '../components/SingleUpload.vue';
 export default {
-    name: 'CollectionEdit',
-    components: { ModelUpload, SingleUpload },
+    name: 'ShowroomEdit',
     created() {
-        Promise.all([
-            new Promise((resolve, reject) => {
-                if (this.$route.query.id) {
-                    return this.$http
-                        .get('collection/getInfo/' + this.$route.query.id)
-                        .then(res => {
-                            if (res.model3d) {
-                                let url = new URL(res.model3d.url);
-                                this.scale = Number(url.searchParams.get('scale')) || 1;
-                                this.yOffset = Number(url.searchParams.get('yOffset')) || 0;
-                                res.model3d.url = url.origin + url.pathname;
-                            }
-                            res.properties = res.properties || [];
-                            res.privileges = res.privileges || [];
-                            this.formData = res;
-                            if (res.totalQuota !== res.vipQuota) {
-                                this.editQuota = false;
-                            }
-                            resolve();
-                        })
-                        .catch(e => {
-                            console.log(e);
-                            this.$message.error(e.error);
-                            resolve();
-                        });
-                }
-                return resolve();
-            }),
+        if (this.$route.query.id) {
             this.$http
-                .post('/privilegeOption/all', { size: 10000, query: { del: false } }, { body: 'json' })
+                .get('showroom/get/' + this.$route.query.id)
                 .then(res => {
-                    this.privilegeOptions = res.content;
-                    return Promise.resolve();
+                    this.formData = res;
                 })
-        ]).then(() => {
-            console.log(this.formData, this.privilegeOptions);
-            this.formData.privileges.forEach(p => {
-                let idx = this.privilegeOptions.findIndex(i => i.name === p.name);
-                if (idx > -1) {
-                    this.$set(this.privilegeOptions[idx], 'added', true);
-                }
-            });
-        });
-    },
-    computed: {
-        canEdit() {
-            return !!!this.$route.query.id;
+                .catch(e => {
+                    console.log(e);
+                    this.$message.error(e.error);
+                });
         }
     },
     data() {
         return {
             saving: false,
-            formData: {
-                onShelf: false,
-                salable: true,
-                properties: [],
-                type: 'SHOWROOM',
-                source: 'OFFICIAL',
-                pic: [],
-                scheduleSale: true,
-                sort: 0,
-                privileges: [],
-                maxCount: 0,
-                countId: null,
-                canResale: true,
-                scanCode: false,
-                noSoldOut: true,
-                assignment: 0,
-                couponPayment: false,
-                maxCollection: 20
-            },
-            rules: {
-                name: [
-                    {
-                        required: true,
-                        message: '请输入名称',
-                        trigger: 'blur'
-                    }
-                ],
-                pic: [
-                    {
-                        validator: (rule, value, callback) => {
-                            if (value) {
-                                if (!(value instanceof Array)) {
-                                    callback(new Error('请上传内容'));
-                                    return;
-                                } else {
-                                    for (let f of value) {
-                                        if (!f.url) {
-                                            callback(new Error('请上传内容'));
-                                            return;
-                                        }
-                                    }
-                                }
-                                callback();
-                            } else {
-                                callback(new Error('请上传内容'));
-                            }
-                        },
-                        trigger: 'blur'
-                    }
-                ],
-                minter: [
-                    {
-                        required: true,
-                        message: '请输入铸造者',
-                        trigger: 'blur'
-                    }
-                ],
-                minterId: [
-                    {
-                        required: true,
-                        message: '请输入铸造者ID',
-                        trigger: 'blur'
-                    }
-                ],
-                minterAvatar: [
-                    {
-                        required: true,
-                        message: '请输入铸造者头像',
-                        trigger: 'blur'
-                    }
-                ],
-                detail: [
-                    {
-                        required: true,
-                        message: '请输入详情',
-                        trigger: 'blur'
-                    }
-                ],
-                type: [
-                    {
-                        required: true,
-                        message: '请输入类型',
-                        trigger: 'blur'
-                    }
-                ],
-                source: [
-                    {
-                        required: true,
-                        message: '请输入来源',
-                        trigger: 'blur'
-                    }
-                ],
-                total: [
-                    {
-                        required: true,
-                        message: '请输入发行数量',
-                        trigger: 'blur'
-                    }
-                ],
-                onShelf: [
-                    {
-                        required: true,
-                        message: '请输入上架',
-                        trigger: 'blur'
-                    }
-                ],
-                price: [
-                    {
-                        required: true,
-                        message: '请输入价格',
-                        trigger: 'blur'
-                    }
-                ],
-                properties: [
-                    {
-                        validator: (rule, value, callback) => {
-                            if (value) {
-                                if (!(value instanceof Array)) {
-                                    callback(new Error('properties must be array!'));
-                                    return;
-                                } else {
-                                    for (let i = 0; i < value.length; i++) {
-                                        if (value[i].name === '' || value[i].name === undefined) {
-                                            callback(new Error('请填写名称'));
-                                            return;
-                                        }
-                                        if (value[i].value === '' || value[i].value === undefined) {
-                                            callback(new Error('请填写内容'));
-                                            return;
-                                        }
-                                    }
-                                }
-                            }
-                            callback();
-                        },
-                        trigger: 'blur'
-                    }
-                ],
-                category: [{ required: true, message: '请填写分类' }],
-                royalties: [{ required: true, message: '请填写版税' }],
-                serviceCharge: [{ required: true, message: '请填手续费' }],
-                startTime: [
-                    {
-                        validator: (rule, value, callback) => {
-                            if (this.formData.scheduleSale) {
-                                if (!value) {
-                                    callback(new Error('请填写发布时间'));
-                                } else if (isBefore(parse(value, 'yyyy-MM-dd HH:mm:ss', new Date()), new Date())) {
-                                    callback(new Error('发布时间不能小于当前时间'));
-                                } else {
-                                    callback();
-                                }
-                            } else {
-                                callback();
-                            }
-                        },
-                        trigger: 'blur'
-                    }
-                ],
-                saleTime: [
-                    {
-                        validator: (rule, value, callback) => {
-                            if (this.formData.timeDelay) {
-                                if (!value) {
-                                    callback(new Error('请填写销售时间'));
-                                } else if (isBefore(parse(value, 'yyyy-MM-dd HH:mm:ss', new Date()), new Date())) {
-                                    callback(new Error('销售时间不能小于当前时间'));
-                                } else if (this.formData.scheduleSale) {
-                                    if (
-                                        isBefore(
-                                            parse(value, 'yyyy-MM-dd HH:mm:ss', new Date()),
-                                            parse(this.formData.startTime, 'yyyy-MM-dd HH:mm:ss', new Date())
-                                        )
-                                    ) {
-                                        callback(new Error('销售时间不能小于发布时间'));
-                                    }
-                                }
-                            }
-                            callback();
-                        },
-                        trigger: 'blur'
-                    }
-                ],
-                timeDelay: [
-                    {
-                        validator: (rule, value, callback) => {
-                            if (this.formData.assignment > 0) {
-                                if (value === '' || value === undefined) {
-                                    callback(new Error('请选择是否延迟销售'));
-                                    return;
-                                }
-                            }
-                            callback();
-                        },
-                        trigger: 'blur'
-                    }
-                ],
-                totalQuota: [
-                    {
-                        validator: (rule, value, callback) => {
-                            if (this.formData.assignment > 0) {
-                                if (value === '' || value === undefined) {
-                                    callback(new Error('请输入白名单额度'));
-                                    return;
-                                }
-                            }
-
-                            callback();
-                        },
-                        trigger: 'blur'
-                    }
-                ],
-                openQuota: [
-                    {
-                        validator: (rule, value, callback) => {
-                            if (this.formData.assignment > 0) {
-                                if (value === '' || value === undefined) {
-                                    callback(new Error('请选择开启/关闭白名单分享'));
-                                    return;
-                                }
-                            }
-                            callback();
-                        },
-                        trigger: 'blur'
-                    }
-                ]
-            },
-            sourceOptions: [
-                { label: '官方', value: 'OFFICIAL' },
-                { label: '用户铸造', value: 'USER' },
-                { label: '转让', value: 'TRANSFER' }
-            ],
-            cateogories: ['勋章', '收藏品', '数字艺术', '门票', '游戏', '音乐', '使用', '其他'],
-            privilegeOptions: [],
-            showPrivilegeEditDialog: false,
-            privilegeForm: {},
-            privelegeRules: {
-                detail: [{ required: true, message: '请填写内容' }],
-                remark: [{ required: true, message: '请填写说明' }]
-            },
-            customUrl: resolveUrl(this.$baseUrl, 'upload/3dModel'),
-            scale: 1,
-            yOffset: 0,
-            editQuota: true
+            formData: {},
+            rules: {},
+            statusOptions: [
+                { label: '未审核', value: 'NOT_AUTH' },
+                { label: '认证中', value: 'PENDING' },
+                { label: '已认证', value: 'SUCCESS' },
+                { label: '失败', value: 'FAIL' }
+            ]
         };
     },
     methods: {
@@ -657,16 +147,10 @@ export default {
         },
         submit() {
             let data = { ...this.formData };
-            if (data.model3d) {
-                data.model3d.url = data.model3d.url + '?scale=' + this.scale + '&yOffset=' + this.yOffset;
-            }
-            if (this.editQuota && data.totalQuota) {
-                data.vipQuota = data.totalQuota;
-            }
 
             this.saving = true;
             this.$http
-                .post(this.formData.id ? '/collection/save' : '/collection/create', data, { body: 'json' })
+                .post('/showroom/save', data, { body: 'json' })
                 .then(res => {
                     this.saving = false;
                     this.$message.success('成功');
@@ -681,7 +165,7 @@ export default {
         onDelete() {
             this.$confirm('删除将无法恢复,确认要删除么?', '警告', { type: 'error' })
                 .then(() => {
-                    return this.$http.post(`/collection/del/${this.formData.id}`);
+                    return this.$http.post(`/showroom/del/${this.formData.id}`);
                 })
                 .then(() => {
                     this.$message.success('删除成功');
@@ -693,104 +177,25 @@ export default {
                         this.$message.error((e || {}).error || '删除失败');
                     }
                 });
-        },
-        onMinterDetail(e) {
-            console.log(e);
-            this.$set(this.formData, 'minter', e.nickname);
-            this.$set(this.formData, 'minterAvatar', e.avatar);
-        },
-        addProperty() {
-            this.formData.properties.push({
-                name: '',
-                value: ''
-            });
-        },
-        delProperty(i) {
-            this.formData.properties.splice(i, 1);
-        },
-        addPrivilege(row, i) {
-            this.privilegeForm = { ...row };
-            this.showPrivilegeEditDialog = true;
-            if (this.$refs.privilegeForm) {
-                this.$nextTick(() => {
-                    this.$refs.privilegeForm.clearValidate();
-                });
-            }
-        },
-        editPrivilege(row, i) {
-            this.privilegeForm = { ...(this.formData.privileges.find(e => e.name === row.name) || {}) };
-            this.showPrivilegeEditDialog = true;
-            if (this.$refs.privilegeForm) {
-                this.$nextTick(() => {
-                    this.$refs.privilegeForm.clearValidate();
-                });
-            }
-        },
-        delPrivilege(row, i) {
-            let idx = this.formData.privileges.findIndex(e => e.name === row.name);
-            if (idx > -1) {
-                this.formData.privileges.splice(idx, 1);
-            }
-            this.$set(this.privilegeOptions[i], 'added', false);
-        },
-        savePrivilege() {
-            this.$refs.privilegeForm
-                .validate()
-                .then(() => {
-                    let i = this.formData.privileges.findIndex(e => e.name === this.privilegeForm.name);
-                    if (i > -1) {
-                        this.$set(this.formData.privileges, i, { ...this.privilegeForm });
-                    } else {
-                        this.formData.privileges.push({ ...this.privilegeForm });
-                    }
-                    let ii = this.privilegeOptions.findIndex(i => i.name === this.privilegeForm.name);
-                    console.log(ii);
-                    this.$set(this.privilegeOptions[ii], 'added', true);
-                    this.showPrivilegeEditDialog = false;
-                })
-                .catch(e => {
-                    console.log(e);
-                });
-        }
-    },
-    watch: {
-        'formData.scanCode'(val) {
-            if (val === true) {
-                this.$set(this.formData, 'onShelf', false);
-            }
         }
     }
 };
 </script>
 <style lang="less" scoped>
-/deep/ .el-switch__label--left {
-    width: 50px;
-    text-align: right;
+/deep/.el-input.is-disabled .el-input__inner {
+    color: #7c7e7e;
 }
-.number-percent {
-    display: flex;
-    align-items: center;
-    .percent {
-        border: 1px solid #dcdfe6;
-        border-radius: 4px;
-        width: 30px;
-        text-align: center;
-        line-height: 30px;
-        color: @text2;
-        font-size: 13px;
-    }
-}
-.tip {
-    font-size: 12px;
-    color: @text3;
-    margin-top: 5px;
+/deep/.el-textarea.is-disabled .el-textarea__inner {
+    color: #7c7e7e;
 }
 .inline-wrapper {
     .el-form-item {
         display: inline-block;
     }
 }
-.right-margin {
-    margin-left: 50px;
+.tip {
+    font-size: 12px;
+    color: @text3;
+    margin-top: 5px;
 }
-</style>
+</style>

+ 144 - 81
src/main/vue/src/views/ShowroomList.vue

@@ -1,7 +1,7 @@
 <template>
     <div class="list-view">
         <page-title>
-            <el-button
+            <!-- <el-button
                 @click="addRow"
                 type="primary"
                 icon="el-icon-plus"
@@ -10,10 +10,17 @@
             >
                 新增
             </el-button>
+            <el-button
+                @click="download"
+                icon="el-icon-upload2"
+                :loading="downloading"
+                :disabled="fetchingData"
+                class="filter-item"
+            >
+                导出
+            </el-button> -->
         </page-title>
         <div class="filters-container">
-            <created-at-picker v-model="createdAt" @input="getData" class="filter-item"></created-at-picker>
-            <minter-filter v-model="minterId" @input="getData" class="filter-item"></minter-filter>
             <el-input
                 placeholder="搜索..."
                 v-model="search"
@@ -23,6 +30,11 @@
             >
                 <el-button @click="getData" slot="append" icon="el-icon-search"> </el-button>
             </el-input>
+            <el-radio-group v-model="status" @change="getData">
+                <el-radio-button v-for="item in statusOptions" :label="item.value" :key="item.value">{{
+                    item.label
+                }}</el-radio-button>
+            </el-radio-group>
         </div>
         <el-table
             :data="tableData"
@@ -37,64 +49,71 @@
         >
             <el-table-column v-if="multipleMode" align="center" type="selection" width="50"> </el-table-column>
             <el-table-column prop="id" label="ID" width="100"> </el-table-column>
-            <el-table-column prop="name" label="名称" show-overflow-tooltip> </el-table-column>
-            <el-table-column prop="createdAt" label="创建时间" width="150">
-                <template slot="header" slot-scope="{ column }">
-                    <sortable-header :column="column" :current-sort="sort" @changeSort="changeSort"> </sortable-header>
-                </template>
-            </el-table-column>
-            <el-table-column prop="pic" label="作品内容" width="90" align="center">
+            <el-table-column prop="userId" label="用户ID"> </el-table-column>
+            <el-table-column prop="nickname" label="昵称"> </el-table-column>
+            <el-table-column prop="pic" label="logo">
                 <template slot-scope="{ row }">
                     <el-image
                         style="width: 30px; height: 30px"
-                        :src="row.pic[0].thumb || row.pic[0].url"
+                        :src="row.pic"
                         fit="cover"
-                        :preview-src-list="row.pic.map(i => i.thumb || i.url)"
+                        :preview-src-list="[row.pic]"
                     ></el-image>
                 </template>
             </el-table-column>
-            <el-table-column prop="minter" label="铸造者"> </el-table-column>
-            <el-table-column prop="minterId" label="铸造者ID"> </el-table-column>
-            <el-table-column prop="type" label="类型" :formatter="typeFormatter"> </el-table-column>
-            <el-table-column prop="source" label="来源" :formatter="sourceFormatter"> </el-table-column>
-            <el-table-column prop="total" label="发行数量" width="105" align="center">
-                <template slot="header" slot-scope="{ column }">
-                    <sortable-header :column="column" :current-sort="sort" @changeSort="changeSort"> </sortable-header>
-                </template>
-            </el-table-column>
-            <el-table-column prop="stock" label="剩余库存" width="105" align="center">
-                <template slot="header" slot-scope="{ column }">
-                    <sortable-header :column="column" :current-sort="sort" @changeSort="changeSort"> </sortable-header>
-                </template>
-            </el-table-column>
-            <el-table-column prop="onShelf" label="上架" width="90" align="center">
-                <template v-slot="{ row }">
-                    <el-tag type="success" v-if="row.onShelf">是</el-tag>
-                    <el-tag type="info" v-else>否</el-tag>
-                </template>
-            </el-table-column>
-            <el-table-column prop="salable" label="仅展示" width="90" align="center">
-                <template v-slot="{ row }">
-                    <el-tag type="success" v-if="!row.salable">是</el-tag>
-                    <el-tag type="info" v-else>否</el-tag>
-                </template>
-            </el-table-column>
-            <el-table-column prop="price" label="价格" width="90"> </el-table-column>
-            <el-table-column prop="noSoldOut" label="售罄">
-                <template v-slot="{ row }">
-                    <el-tag type="success" v-if="!row.noSoldOut">是</el-tag>
-                    <el-tag type="info" v-else>否</el-tag>
+            <el-table-column prop="introduction" label="简介"> </el-table-column>
+            <!-- <el-table-column prop="publish" label="发布">
+                <template slot-scope="{ row }">
+                    <el-tag :type="row.publish ? 'success' : 'info'">{{ row.publish ? '是' : '否' }}</el-tag>
                 </template>
-            </el-table-column>
-            <el-table-column prop="sort" label="排序" width="90" align="center">
-                <template slot="header" slot-scope="{ column }">
-                    <sortable-header :column="column" :current-sort="sort" @changeSort="changeSort"> </sortable-header>
+            </el-table-column> -->
+            <el-table-column prop="likes" label="点赞"> </el-table-column>
+            <!-- <el-table-column prop="maxCollection" label="最多可放藏品数量"> </el-table-column> -->
+            <!-- <el-table-column prop="headBg" label="头部背景">
+                <template slot-scope="{ row }">
+                    <el-image
+                        style="width: 30px; height: 30px"
+                        :src="row.headBg"
+                        fit="cover"
+                        :preview-src-list="[row.headBg]"
+                    ></el-image>
                 </template>
             </el-table-column>
-            <el-table-column label="操作" align="center" fixed="right" width="150">
+            <el-table-column prop="showroomBg" label="展厅背景">
                 <template slot-scope="{ row }">
+                    <el-image
+                        style="width: 30px; height: 30px"
+                        :src="row.showroomBg"
+                        fit="cover"
+                        :preview-src-list="[row.showroomBg]"
+                    ></el-image>
+                </template>
+            </el-table-column> -->
+            <el-table-column prop="status" label="状态" :formatter="statusFormatter"> </el-table-column>
+            <el-table-column label="操作" align="center" fixed="right" width="210">
+                <template slot-scope="{ row, $index }">
                     <el-button @click="editRow(row)" type="primary" size="mini" plain>查看</el-button>
                     <!-- <el-button @click="deleteRow(row)" type="danger" size="mini" plain>删除</el-button> -->
+                    <el-button
+                        @click="pass(row, $index)"
+                        type="success"
+                        size="mini"
+                        plain
+                        v-if="row.status === 'PENDING'"
+                        :loading="row.saving"
+                    >
+                        通过
+                    </el-button>
+                    <el-button
+                        @click="deny(row, $index)"
+                        type="danger"
+                        size="mini"
+                        plain
+                        v-if="row.status === 'PENDING'"
+                        :loading="row.saving"
+                    >
+                        拒绝
+                    </el-button>
                 </template>
             </el-table-column>
         </el-table>
@@ -119,39 +138,46 @@
             >
             </el-pagination>
         </div>
+
+        <el-dialog :visible.sync="showReasonDialog" width="600px" title="拒绝理由">
+            <el-form ref="denyForm" :model="denyForm" label-position="right" label-width="60px" :rules="denyRules">
+                <el-form-item prop="reason" label="理由">
+                    <el-input type="textarea" :autosize="{ minRows: 3 }" v-model="denyForm.reason"></el-input>
+                </el-form-item>
+            </el-form>
+            <div slot="footer">
+                <el-button @click="showReasonDialog = false">取消</el-button>
+                <el-button type="primary" @click="saveReason">保存</el-button>
+            </div>
+        </el-dialog>
     </div>
 </template>
 <script>
 import { mapState } from 'vuex';
 import pageableTable from '@/mixins/pageableTable';
-import SortableHeader from '../components/SortableHeader.vue';
-import MinterSelect from '../components/MinterSelect.vue';
 
 export default {
-    components: { SortableHeader, MinterSelect },
     name: 'ShowroomList',
     mixins: [pageableTable],
     data() {
         return {
             multipleMode: false,
             search: '',
-            url: '/collection/all',
+            url: '/showroom/all',
             downloading: false,
-            typeOptions: [
-                { label: '默认', value: 'DEFAULT' },
-                { label: '盲盒', value: 'BLIND_BOX' },
-                { label: '拍卖', value: 'AUCTION' },
-                { label: '展厅', value: 'SHOWROOM' }
-            ],
-            sourceOptions: [
-                { label: '官方', value: 'OFFICIAL' },
-                { label: '用户铸造', value: 'USER' },
-                { label: '转让', value: 'TRANSFER' }
+            statusOptions: [
+                { label: '审核中', value: 'PENDING' },
+                { label: '失败', value: 'FAIL' },
+                { label: '已成功', value: 'SUCCESS' },
+                { label: '未审核', value: 'NOT_AUTH' }
             ],
-            sort: { sort: 'desc' },
-            sortStr: 'sort,desc',
-            createdAt: '',
-            minterId: ''
+            showReasonDialog: false,
+            denyForm: {},
+            denyRules: {
+                reason: [{ required: true, message: '请填写理由' }]
+            },
+            status: 'PENDING',
+            saving: false
         };
     },
     computed: {
@@ -160,25 +186,23 @@ export default {
         }
     },
     methods: {
-        typeFormatter(row, column, cellValue, index) {
-            let selectedOption = this.typeOptions.find(i => i.value === cellValue);
-            if (selectedOption) {
-                return selectedOption.label;
-            }
-            return '';
+        afterGetData(res) {
+            this.tableData = res.content.map(item => {
+                return {
+                    ...item,
+                    saving: false
+                };
+            });
         },
-        sourceFormatter(row, column, cellValue, index) {
-            let selectedOption = this.sourceOptions.find(i => i.value === cellValue);
+        statusFormatter(row, column, cellValue, index) {
+            let selectedOption = this.statusOptions.find(i => i.value === cellValue);
             if (selectedOption) {
                 return selectedOption.label;
             }
             return '';
         },
         beforeGetData() {
-            return {
-                search: this.search,
-                query: { del: false, source: 'OFFICIAL', type: 'SHOWROOM', createdAt: this.createdAt, minterId: this.minterId }
-            };
+            return { search: this.search, query: { del: false, type: 'COMPANY', status: this.status } };
         },
         toggleMultipleMode(multipleMode) {
             this.multipleMode = multipleMode;
@@ -196,7 +220,7 @@ export default {
         },
         editRow(row) {
             this.$router.push({
-                path: 'showroomEdit',
+                path: '/showroomEdit',
                 query: {
                     id: row.id
                 }
@@ -205,7 +229,7 @@ export default {
         download() {
             this.downloading = true;
             this.$axios
-                .get('/collection/excel', {
+                .get('/showroom/excel', {
                     responseType: 'blob',
                     params: { size: 10000 }
                 })
@@ -238,7 +262,7 @@ export default {
         deleteRow(row) {
             this.$alert('删除将无法恢复,确认要删除么?', '警告', { type: 'error' })
                 .then(() => {
-                    return this.$http.post(`/collection/del/${row.id}`);
+                    return this.$http.post(`/showroom/del/${row.id}`);
                 })
                 .then(() => {
                     this.$message.success('删除成功');
@@ -249,6 +273,45 @@ export default {
                         this.$message.error(e.error);
                     }
                 });
+        },
+        pass(row, index) {
+            this.tableData[index].saving = true;
+            this.$confirm('确定通过?')
+                .then(res => {
+                    return this.$http.post('/showroom/pass', { id: row.id });
+                })
+                .then(res => {
+                    this.$message.success('成功');
+                    this.tableData[index].saving = false;
+                    this.getData();
+                })
+                .catch(e => {
+                    this.tableData[index].saving = false;
+                });
+        },
+        deny(row, index) {
+            this.showReasonDialog = true;
+            this.denyForm.id = row.id;
+            this.denyForm.index = index;
+        },
+        saveReason() {
+            this.tableData[this.denyForm.index].saving = true;
+            this.$confirm('确定拒绝?')
+                .then(res => {
+                    return this.$http.post('/showroom/deny', {
+                        id: this.denyForm.id,
+                        reason: this.denyForm.reason
+                    });
+                })
+                .then(res => {
+                    this.$message.success('成功');
+                    this.showReasonDialog = false;
+                    this.tableData[this.denyForm.index].saving = false;
+                    this.getData();
+                })
+                .catch(e => {
+                    this.tableData[this.denyForm.index].saving = false;
+                });
         }
     }
 };

+ 244 - 0
src/main/vue/src/views/company/CollectionFailList.vue

@@ -0,0 +1,244 @@
+<template>
+    <div class="list-view">
+        <page-title>
+            <!-- <el-button
+                @click="addRow"
+                type="primary"
+                icon="el-icon-plus"
+                :disabled="fetchingData || downloading"
+                class="filter-item"
+            >
+                新增
+            </el-button>
+            <el-button
+                @click="download"
+                icon="el-icon-upload2"
+                :loading="downloading"
+                :disabled="fetchingData"
+                class="filter-item"
+            >
+                导出
+            </el-button> -->
+        </page-title>
+        <div class="filters-container">
+            <el-input
+                placeholder="搜索..."
+                v-model="search"
+                clearable
+                class="filter-item search"
+                @keyup.enter.native="getData"
+            >
+                <el-button @click="getData" slot="append" icon="el-icon-search"> </el-button>
+            </el-input>
+        </div>
+        <el-table
+            :data="tableData"
+            row-key="id"
+            ref="table"
+            header-row-class-name="table-header-row"
+            header-cell-class-name="table-header-cell"
+            row-class-name="table-row"
+            cell-class-name="table-cell"
+            :height="tableHeight"
+            v-loading="fetchingData"
+        >
+            <el-table-column v-if="multipleMode" align="center" type="selection" width="50"> </el-table-column>
+            <el-table-column prop="id" label="ID" width="100"> </el-table-column>
+            <el-table-column prop="name" label="名称"> </el-table-column>
+            <el-table-column prop="createdAt" label="创建时间" width="150">
+                <template slot="header" slot-scope="{ column }">
+                    <sortable-header :column="column" :current-sort="sort" @changeSort="changeSort"> </sortable-header>
+                </template>
+            </el-table-column>
+            <el-table-column prop="pic" label="作品内容" width="90" align="center">
+                <template slot-scope="{ row }">
+                    <el-image
+                        style="width: 30px; height: 30px"
+                        :src="row.pic[0].thumb || row.pic[0].url"
+                        fit="cover"
+                        :preview-src-list="row.pic.map(i => i.thumb || i.url)"
+                    ></el-image>
+                </template>
+            </el-table-column>
+            <el-table-column prop="total" label="发行数量" width="105" align="center">
+                <template slot="header" slot-scope="{ column }">
+                    <sortable-header :column="column" :current-sort="sort" @changeSort="changeSort"> </sortable-header>
+                </template>
+            </el-table-column>
+            <el-table-column prop="price" label="价格"> </el-table-column>
+            <el-table-column prop="status" label="审核状态" :formatter="statusFormatter" width="120" align="center">
+            </el-table-column>
+            <el-table-column label="操作" align="center" fixed="right" width="120">
+                <template slot-scope="{ row }">
+                    <el-button @click="editRow(row)" type="primary" size="mini" plain>编辑</el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+        <div class="pagination-wrapper">
+            <!-- <div class="multiple-mode-wrapper">
+                <el-button v-if="!multipleMode" @click="toggleMultipleMode(true)">批量编辑</el-button>
+                <el-button-group v-else>
+                    <el-button @click="operation1">批量操作1</el-button>
+                    <el-button @click="operation2">批量操作2</el-button>
+                    <el-button @click="toggleMultipleMode(false)">取消</el-button>
+                </el-button-group>
+            </div> -->
+            <el-pagination
+                background
+                @size-change="onSizeChange"
+                @current-change="onCurrentChange"
+                :current-page="page"
+                :page-sizes="[10, 20, 30, 40, 50]"
+                :page-size="pageSize"
+                layout="total, sizes, prev, pager, next, jumper"
+                :total="totalElements"
+            >
+            </el-pagination>
+        </div>
+    </div>
+</template>
+<script>
+import { mapState } from 'vuex';
+import pageableTable from '@/mixins/pageableTable';
+
+export default {
+    mixins: [pageableTable],
+    data() {
+        return {
+            multipleMode: false,
+            search: '',
+            url: '/companyCollection/all',
+            downloading: false,
+            statusOptions: [
+                { label: '审核中', value: 'PENDING' },
+                { label: '通过', value: 'SUCCESS' },
+                { label: '失败', value: 'FAIL' }
+            ],
+            typeOptions: [
+                { label: '默认', value: 'DEFAULT' },
+                { label: '盲盒', value: 'BLIND_BOX' },
+                { label: '拍卖', value: 'AUCTION' }
+            ]
+        };
+    },
+    computed: {
+        selection() {
+            return this.$refs.table.selection.map(i => i.id);
+        },
+        ...mapState(['userInfo'])
+    },
+    methods: {
+        typeFormatter(row, column, cellValue, index) {
+            let selectedOption = this.typeOptions.find(i => i.value === cellValue);
+            if (selectedOption) {
+                return selectedOption.label;
+            }
+            return '';
+        },
+        statusFormatter(row, column, cellValue, index) {
+            let selectedOption = this.statusOptions.find(i => i.value === cellValue);
+            if (selectedOption) {
+                return selectedOption.label;
+            }
+            return '';
+        },
+        beforeGetData() {
+            return { search: this.search, query: { del: false, userId: this.userInfo.id , status: 'FAIL' } };
+        },
+        toggleMultipleMode(multipleMode) {
+            this.multipleMode = multipleMode;
+            if (!multipleMode) {
+                this.$refs.table.clearSelection();
+            }
+        },
+        addRow() {
+            this.$router.push({
+                path: '/companyCollectionEdit',
+                query: {
+                    ...this.$route.query
+                }
+            });
+        },
+        editRow(row) {
+            this.$router.push({
+                path: '/companyCollectionEdit',
+                query: {
+                    id: row.id
+                }
+            });
+        },
+        download() {
+            this.downloading = true;
+            this.$axios
+                .get('/companyCollection/excel', {
+                    responseType: 'blob',
+                    params: { size: 10000 }
+                })
+                .then(res => {
+                    console.log(res);
+                    this.downloading = false;
+                    const downloadUrl = window.URL.createObjectURL(new Blob([res.data]));
+                    const link = document.createElement('a');
+                    link.href = downloadUrl;
+                    link.setAttribute('download', res.headers['content-disposition'].split('filename=')[1]);
+                    document.body.appendChild(link);
+                    link.click();
+                    link.remove();
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.downloading = false;
+                    this.$message.error(e.error);
+                });
+        },
+        operation1() {
+            this.$notify({
+                title: '提示',
+                message: this.selection
+            });
+        },
+        operation2() {
+            this.$message('操作2');
+        },
+        deleteRow(row) {
+            this.$alert('删除将无法恢复,确认要删除么?', '警告', { type: 'error' })
+                .then(() => {
+                    return this.$http.post(`/companyCollection/del/${row.id}`);
+                })
+                .then(() => {
+                    this.$message.success('删除成功');
+                    this.getData();
+                })
+                .catch(e => {
+                    if (e !== 'cancel') {
+                        this.$message.error(e.error);
+                    }
+                });
+        },
+        pass(row) {
+            this.$confirm('确定通过?')
+                .then(res => {
+                    return this.$http.post('/companyCollection/pass', { id: row.id });
+                })
+                .then(res => {
+                    this.$message.success('成功');
+                    this.getData();
+                })
+                .catch(e => {});
+        },
+        deny(row) {
+            this.$confirm('确定拒绝?')
+                .then(res => {
+                    return this.$http.post('/companyCollection/deny', { id: row.id });
+                })
+                .then(res => {
+                    this.$message.success('成功');
+                    this.getData();
+                })
+                .catch(e => {});
+        }
+    }
+};
+</script>
+<style lang="less" scoped>
+</style>

+ 268 - 0
src/main/vue/src/views/company/CollectionPassList.vue

@@ -0,0 +1,268 @@
+<template>
+    <div class="list-view">
+        <page-title>
+            <!-- <el-button
+                @click="addRow"
+                type="primary"
+                icon="el-icon-plus"
+                :disabled="fetchingData || downloading"
+                class="filter-item"
+            >
+                新增
+            </el-button>
+            <el-button
+                @click="download"
+                icon="el-icon-upload2"
+                :loading="downloading"
+                :disabled="fetchingData"
+                class="filter-item"
+            >
+                导出
+            </el-button> -->
+        </page-title>
+        <div class="filters-container">
+            <el-input
+                placeholder="搜索..."
+                v-model="search"
+                clearable
+                class="filter-item search"
+                @keyup.enter.native="getData"
+            >
+                <el-button @click="getData" slot="append" icon="el-icon-search"> </el-button>
+            </el-input>
+        </div>
+        <el-table
+            :data="tableData"
+            row-key="id"
+            ref="table"
+            header-row-class-name="table-header-row"
+            header-cell-class-name="table-header-cell"
+            row-class-name="table-row"
+            cell-class-name="table-cell"
+            :height="tableHeight"
+            v-loading="fetchingData"
+        >
+            <el-table-column v-if="multipleMode" align="center" type="selection" width="50"> </el-table-column>
+            <el-table-column prop="id" label="ID" width="100"> </el-table-column>
+            <el-table-column prop="name" label="名称"> </el-table-column>
+            <el-table-column prop="createdAt" label="创建时间" width="150">
+                <template slot="header" slot-scope="{ column }">
+                    <sortable-header :column="column" :current-sort="sort" @changeSort="changeSort"> </sortable-header>
+                </template>
+            </el-table-column>
+            <el-table-column prop="pic" label="作品内容" width="90" align="center">
+                <template slot-scope="{ row }">
+                    <el-image
+                        style="width: 30px; height: 30px"
+                        :src="row.pic[0].thumb || row.pic[0].url"
+                        fit="cover"
+                        :preview-src-list="row.pic.map(i => i.thumb || i.url)"
+                    ></el-image>
+                </template>
+            </el-table-column>
+            <el-table-column prop="minter" label="铸造者"> </el-table-column>
+            <el-table-column prop="minterId" label="铸造者ID"> </el-table-column>
+            <el-table-column prop="total" label="发行数量" width="105" align="center">
+                <template slot="header" slot-scope="{ column }">
+                    <sortable-header :column="column" :current-sort="sort" @changeSort="changeSort"> </sortable-header>
+                </template>
+            </el-table-column>
+            <el-table-column prop="stock" label="剩余库存" width="105" align="center">
+                <template slot="header" slot-scope="{ column }">
+                    <sortable-header :column="column" :current-sort="sort" @changeSort="changeSort"> </sortable-header>
+                </template>
+            </el-table-column>
+            <el-table-column prop="onShelf" label="上架" width="90" align="center">
+                <template v-slot="{ row }">
+                    <el-tag type="success" v-if="row.onShelf">是</el-tag>
+                    <el-tag type="info" v-else>否</el-tag>
+                </template>
+            </el-table-column>
+            <el-table-column prop="salable" label="仅展示" width="90" align="center">
+                <template v-slot="{ row }">
+                    <el-tag type="success" v-if="!row.salable">是</el-tag>
+                    <el-tag type="info" v-else>否</el-tag>
+                </template>
+            </el-table-column>
+            <el-table-column prop="price" label="价格"> </el-table-column>
+            <el-table-column label="操作" align="center" fixed="right" width="120">
+                <template slot-scope="{ row }">
+                    <el-button @click="editRow(row)" type="primary" size="mini" plain>查看</el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+        <div class="pagination-wrapper">
+            <!-- <div class="multiple-mode-wrapper">
+                <el-button v-if="!multipleMode" @click="toggleMultipleMode(true)">批量编辑</el-button>
+                <el-button-group v-else>
+                    <el-button @click="operation1">批量操作1</el-button>
+                    <el-button @click="operation2">批量操作2</el-button>
+                    <el-button @click="toggleMultipleMode(false)">取消</el-button>
+                </el-button-group>
+            </div> -->
+            <el-pagination
+                background
+                @size-change="onSizeChange"
+                @current-change="onCurrentChange"
+                :current-page="page"
+                :page-sizes="[10, 20, 30, 40, 50]"
+                :page-size="pageSize"
+                layout="total, sizes, prev, pager, next, jumper"
+                :total="totalElements"
+            >
+            </el-pagination>
+        </div>
+    </div>
+</template>
+<script>
+import { mapState } from 'vuex';
+import pageableTable from '@/mixins/pageableTable';
+
+export default {
+    name: 'CompanyCollectionList',
+    mixins: [pageableTable],
+    data() {
+        return {
+            multipleMode: false,
+            search: '',
+            url: '/collection/all',
+            downloading: false,
+            statusOptions: [
+                { label: '审核中', value: 'PENDING' },
+                { label: '通过', value: 'SUCCESS' },
+                { label: '失败', value: 'FAIL' }
+            ],
+            typeOptions: [
+                { label: '默认', value: 'DEFAULT' },
+                { label: '盲盒', value: 'BLIND_BOX' },
+                { label: '拍卖', value: 'AUCTION' }
+            ]
+        };
+    },
+    computed: {
+        selection() {
+            return this.$refs.table.selection.map(i => i.id);
+        },
+        ...mapState(['userInfo']),
+        isAdmin() {
+            return this.userInfo.admin;
+        }
+    },
+    methods: {
+        typeFormatter(row, column, cellValue, index) {
+            let selectedOption = this.typeOptions.find(i => i.value === cellValue);
+            if (selectedOption) {
+                return selectedOption.label;
+            }
+            return '';
+        },
+        statusFormatter(row, column, cellValue, index) {
+            let selectedOption = this.statusOptions.find(i => i.value === cellValue);
+            if (selectedOption) {
+                return selectedOption.label;
+            }
+            return '';
+        },
+        beforeGetData() {
+            return {
+                search: this.search,
+                query: { del: false, minterId: this.isAdmin ? '' : this.userInfo.id, source: 'COMPANY' }
+            };
+        },
+        toggleMultipleMode(multipleMode) {
+            this.multipleMode = multipleMode;
+            if (!multipleMode) {
+                this.$refs.table.clearSelection();
+            }
+        },
+        addRow() {
+            this.$router.push({
+                path: '/companyCollectionEdit',
+                query: {
+                    ...this.$route.query
+                }
+            });
+        },
+        editRow(row) {
+            this.$router.push({
+                path: '/companyCollectionShelf',
+                query: {
+                    id: row.id
+                }
+            });
+        },
+        download() {
+            this.downloading = true;
+            this.$axios
+                .get('/companyCollection/excel', {
+                    responseType: 'blob',
+                    params: { size: 10000 }
+                })
+                .then(res => {
+                    console.log(res);
+                    this.downloading = false;
+                    const downloadUrl = window.URL.createObjectURL(new Blob([res.data]));
+                    const link = document.createElement('a');
+                    link.href = downloadUrl;
+                    link.setAttribute('download', res.headers['content-disposition'].split('filename=')[1]);
+                    document.body.appendChild(link);
+                    link.click();
+                    link.remove();
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.downloading = false;
+                    this.$message.error(e.error);
+                });
+        },
+        operation1() {
+            this.$notify({
+                title: '提示',
+                message: this.selection
+            });
+        },
+        operation2() {
+            this.$message('操作2');
+        },
+        deleteRow(row) {
+            this.$alert('删除将无法恢复,确认要删除么?', '警告', { type: 'error' })
+                .then(() => {
+                    return this.$http.post(`/companyCollection/del/${row.id}`);
+                })
+                .then(() => {
+                    this.$message.success('删除成功');
+                    this.getData();
+                })
+                .catch(e => {
+                    if (e !== 'cancel') {
+                        this.$message.error(e.error);
+                    }
+                });
+        },
+        pass(row) {
+            this.$confirm('确定通过?')
+                .then(res => {
+                    return this.$http.post('/companyCollection/pass', { id: row.id });
+                })
+                .then(res => {
+                    this.$message.success('成功');
+                    this.getData();
+                })
+                .catch(e => {});
+        },
+        deny(row) {
+            this.$confirm('确定拒绝?')
+                .then(res => {
+                    return this.$http.post('/companyCollection/deny', { id: row.id });
+                })
+                .then(res => {
+                    this.$message.success('成功');
+                    this.getData();
+                })
+                .catch(e => {});
+        }
+    }
+};
+</script>
+<style lang="less" scoped>
+</style>

+ 244 - 0
src/main/vue/src/views/company/CollectionPendingList.vue

@@ -0,0 +1,244 @@
+<template>
+    <div class="list-view">
+        <page-title>
+            <el-button
+                @click="addRow"
+                type="primary"
+                icon="el-icon-plus"
+                :disabled="fetchingData || downloading"
+                class="filter-item"
+            >
+                新增
+            </el-button>
+            <!-- <el-button
+                @click="download"
+                icon="el-icon-upload2"
+                :loading="downloading"
+                :disabled="fetchingData"
+                class="filter-item"
+            >
+                导出
+            </el-button> -->
+        </page-title>
+        <div class="filters-container">
+            <el-input
+                placeholder="搜索..."
+                v-model="search"
+                clearable
+                class="filter-item search"
+                @keyup.enter.native="getData"
+            >
+                <el-button @click="getData" slot="append" icon="el-icon-search"> </el-button>
+            </el-input>
+        </div>
+        <el-table
+            :data="tableData"
+            row-key="id"
+            ref="table"
+            header-row-class-name="table-header-row"
+            header-cell-class-name="table-header-cell"
+            row-class-name="table-row"
+            cell-class-name="table-cell"
+            :height="tableHeight"
+            v-loading="fetchingData"
+        >
+            <el-table-column v-if="multipleMode" align="center" type="selection" width="50"> </el-table-column>
+            <el-table-column prop="id" label="ID" width="100"> </el-table-column>
+            <el-table-column prop="name" label="名称"> </el-table-column>
+            <el-table-column prop="createdAt" label="创建时间" width="150">
+                <template slot="header" slot-scope="{ column }">
+                    <sortable-header :column="column" :current-sort="sort" @changeSort="changeSort"> </sortable-header>
+                </template>
+            </el-table-column>
+            <el-table-column prop="pic" label="作品内容" width="90" align="center">
+                <template slot-scope="{ row }">
+                    <el-image
+                        style="width: 30px; height: 30px"
+                        :src="row.pic[0].thumb || row.pic[0].url"
+                        fit="cover"
+                        :preview-src-list="row.pic.map(i => i.thumb || i.url)"
+                    ></el-image>
+                </template>
+            </el-table-column>
+            <el-table-column prop="total" label="发行数量" width="105" align="center">
+                <template slot="header" slot-scope="{ column }">
+                    <sortable-header :column="column" :current-sort="sort" @changeSort="changeSort"> </sortable-header>
+                </template>
+            </el-table-column>
+            <el-table-column prop="price" label="价格"> </el-table-column>
+            <el-table-column prop="status" label="审核状态" :formatter="statusFormatter" width="120" align="center">
+            </el-table-column>
+            <el-table-column label="操作" align="center" fixed="right" width="120">
+                <template slot-scope="{ row }">
+                    <el-button @click="editRow(row)" type="primary" size="mini" plain>编辑</el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+        <div class="pagination-wrapper">
+            <!-- <div class="multiple-mode-wrapper">
+                <el-button v-if="!multipleMode" @click="toggleMultipleMode(true)">批量编辑</el-button>
+                <el-button-group v-else>
+                    <el-button @click="operation1">批量操作1</el-button>
+                    <el-button @click="operation2">批量操作2</el-button>
+                    <el-button @click="toggleMultipleMode(false)">取消</el-button>
+                </el-button-group>
+            </div> -->
+            <el-pagination
+                background
+                @size-change="onSizeChange"
+                @current-change="onCurrentChange"
+                :current-page="page"
+                :page-sizes="[10, 20, 30, 40, 50]"
+                :page-size="pageSize"
+                layout="total, sizes, prev, pager, next, jumper"
+                :total="totalElements"
+            >
+            </el-pagination>
+        </div>
+    </div>
+</template>
+<script>
+import { mapState } from 'vuex';
+import pageableTable from '@/mixins/pageableTable';
+
+export default {
+    mixins: [pageableTable],
+    data() {
+        return {
+            multipleMode: false,
+            search: '',
+            url: '/companyCollection/all',
+            downloading: false,
+            statusOptions: [
+                { label: '审核中', value: 'PENDING' },
+                { label: '通过', value: 'SUCCESS' },
+                { label: '失败', value: 'FAIL' }
+            ],
+            typeOptions: [
+                { label: '默认', value: 'DEFAULT' },
+                { label: '盲盒', value: 'BLIND_BOX' },
+                { label: '拍卖', value: 'AUCTION' }
+            ]
+        };
+    },
+    computed: {
+        selection() {
+            return this.$refs.table.selection.map(i => i.id);
+        },
+        ...mapState(['userInfo'])
+    },
+    methods: {
+        typeFormatter(row, column, cellValue, index) {
+            let selectedOption = this.typeOptions.find(i => i.value === cellValue);
+            if (selectedOption) {
+                return selectedOption.label;
+            }
+            return '';
+        },
+        statusFormatter(row, column, cellValue, index) {
+            let selectedOption = this.statusOptions.find(i => i.value === cellValue);
+            if (selectedOption) {
+                return selectedOption.label;
+            }
+            return '';
+        },
+        beforeGetData() {
+            return { search: this.search, query: { del: false, userId: this.userInfo.id, status: 'PENDING' } };
+        },
+        toggleMultipleMode(multipleMode) {
+            this.multipleMode = multipleMode;
+            if (!multipleMode) {
+                this.$refs.table.clearSelection();
+            }
+        },
+        addRow() {
+            this.$router.push({
+                path: '/companyCollectionEdit',
+                query: {
+                    ...this.$route.query
+                }
+            });
+        },
+        editRow(row) {
+            this.$router.push({
+                path: '/companyCollectionEdit',
+                query: {
+                    id: row.id
+                }
+            });
+        },
+        download() {
+            this.downloading = true;
+            this.$axios
+                .get('/companyCollection/excel', {
+                    responseType: 'blob',
+                    params: { size: 10000 }
+                })
+                .then(res => {
+                    console.log(res);
+                    this.downloading = false;
+                    const downloadUrl = window.URL.createObjectURL(new Blob([res.data]));
+                    const link = document.createElement('a');
+                    link.href = downloadUrl;
+                    link.setAttribute('download', res.headers['content-disposition'].split('filename=')[1]);
+                    document.body.appendChild(link);
+                    link.click();
+                    link.remove();
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.downloading = false;
+                    this.$message.error(e.error);
+                });
+        },
+        operation1() {
+            this.$notify({
+                title: '提示',
+                message: this.selection
+            });
+        },
+        operation2() {
+            this.$message('操作2');
+        },
+        deleteRow(row) {
+            this.$alert('删除将无法恢复,确认要删除么?', '警告', { type: 'error' })
+                .then(() => {
+                    return this.$http.post(`/companyCollection/del/${row.id}`);
+                })
+                .then(() => {
+                    this.$message.success('删除成功');
+                    this.getData();
+                })
+                .catch(e => {
+                    if (e !== 'cancel') {
+                        this.$message.error(e.error);
+                    }
+                });
+        },
+        pass(row) {
+            this.$confirm('确定通过?')
+                .then(res => {
+                    return this.$http.post('/companyCollection/pass', { id: row.id });
+                })
+                .then(res => {
+                    this.$message.success('成功');
+                    this.getData();
+                })
+                .catch(e => {});
+        },
+        deny(row) {
+            this.$confirm('确定拒绝?')
+                .then(res => {
+                    return this.$http.post('/companyCollection/deny', { id: row.id });
+                })
+                .then(res => {
+                    this.$message.success('成功');
+                    this.getData();
+                })
+                .catch(e => {});
+        }
+    }
+};
+</script>
+<style lang="less" scoped>
+</style>

+ 463 - 0
src/main/vue/src/views/company/CompanyCollectionEdit.vue

@@ -0,0 +1,463 @@
+<template>
+    <div class="edit-view">
+        <page-title>
+            <el-button @click="$router.go(-1)" :disabled="saving">取消</el-button>
+            <el-button @click="onDelete" :disabled="saving" type="danger" v-if="formData.id"> 删除 </el-button>
+            <el-button @click="onSave" :loading="saving" type="primary">保存</el-button>
+        </page-title>
+        <div class="edit-view__content-wrapper">
+            <div class="edit-view__content-section">
+                <el-form
+                    :model="formData"
+                    :rules="rules"
+                    ref="form"
+                    label-width="120px"
+                    label-position="right"
+                    size="small"
+                >
+                    <el-form-item prop="reason" label="拒绝理由" v-if="formData.reason">
+                        <el-input
+                            type="textarea"
+                            v-model="formData.reason"
+                            :rows="3"
+                            style="width: 500px"
+                            disabled
+                        ></el-input>
+                    </el-form-item>
+                    <el-form-item prop="name" label="名称">
+                        <el-input v-model="formData.name" style="width: 500px"></el-input>
+                    </el-form-item>
+                    <el-form-item prop="pic" label="图片">
+                        <object-upload v-model="formData.pic[0]" compress width="3000" height="3000"></object-upload>
+                        <div class="tip">支持JPG、PNG、GIF、MP4,推荐长宽比1:1</div>
+                    </el-form-item>
+                    <el-form-item prop="model3d" label="3D模型">
+                        <model-upload
+                            :limit="1"
+                            v-model="formData.model3d"
+                            :customUrl="customUrl"
+                            accept="application/zip"
+                            format="json"
+                            single
+                        ></model-upload>
+                        <div class="tip">请将FBX文件与贴图打包成zip压缩包上传</div>
+                    </el-form-item>
+                    <el-form-item label="相机距离" v-if="formData.model3d">
+                        <el-input-number v-model="scale" :min="0.1" :step="0.1"></el-input-number>
+                    </el-form-item>
+                    <el-form-item label="Y轴偏移" v-if="formData.model3d">
+                        <el-input-number v-model="yOffset"></el-input-number>
+                    </el-form-item>
+                    <el-form-item prop="category" label="分类">
+                        <el-select v-model="formData.category">
+                            <el-option v-for="item in cateogories" :label="item" :value="item" :key="item"></el-option>
+                        </el-select>
+                    </el-form-item>
+                    <el-form-item prop="detail" label="详情" style="width: calc(100vw - 450px)">
+                        <rich-text v-model="formData.detail"></rich-text>
+                    </el-form-item>
+                    <el-form-item prop="properties" label="特性" style="width: calc(100vw - 450px)" size="mini">
+                        <el-table :data="formData.properties">
+                            <el-table-column prop="name" label="名称">
+                                <template v-slot="{ row }">
+                                    <el-input v-model="row.name" placeholder="20字以内" maxlength="20"></el-input>
+                                </template>
+                            </el-table-column>
+                            <el-table-column prop="value" label="内容">
+                                <template v-slot="{ row }">
+                                    <el-input v-model="row.value" placeholder="20字以内" maxlength="20"></el-input>
+                                </template>
+                            </el-table-column>
+                            <el-table-column width="80" align="center">
+                                <template v-slot="{ $index }">
+                                    <el-button type="danger" plain size="mini" @click="delProperty($index)">
+                                        删除
+                                    </el-button>
+                                </template>
+                            </el-table-column>
+                        </el-table>
+                    </el-form-item>
+                    <el-form-item>
+                        <el-button size="mini" @click="addProperty"> 添加特性 </el-button>
+                    </el-form-item>
+                    <el-form-item label="特权" prop="privileges" style="width: calc(100vw - 450px)">
+                        <el-table :data="privilegeOptions">
+                            <el-table-column prop="name" label="可选特权" width="150"></el-table-column>
+                            <el-table-column prop="description"></el-table-column>
+                            <el-table-column width="155" align="right">
+                                <template v-slot="{ row, $index }">
+                                    <el-button size="mini" v-if="!row.added" @click="addPrivilege(row, $index)">
+                                        添加
+                                    </el-button>
+                                    <el-button size="mini" v-if="!!row.added" plain @click="editPrivilege(row, $index)">
+                                        编辑
+                                    </el-button>
+                                    <el-button
+                                        size="mini"
+                                        v-if="!!row.added"
+                                        type="danger"
+                                        plain
+                                        @click="delPrivilege(row, $index)"
+                                    >
+                                        删除
+                                    </el-button>
+                                </template>
+                            </el-table-column>
+                        </el-table>
+                    </el-form-item>
+                    <div class="inline-wrapper">
+                        <el-form-item prop="price" label="价格">
+                            <el-input-number type="number" v-model="formData.price"></el-input-number>
+                        </el-form-item>
+                        <el-form-item prop="price" label="原价">
+                            <el-input-number type="number" v-model="formData.originalPrice"></el-input-number>
+                        </el-form-item>
+                    </div>
+                    <el-form-item prop="total" label="发行数量">
+                        <el-input-number v-model="formData.total"></el-input-number>
+                    </el-form-item>
+                    <div class="inline-wrapper">
+                        <el-form-item prop="maxCount" label="限购">
+                            <el-input-number v-model="formData.maxCount" :min="0" :step="1"></el-input-number>
+                            <div class="tip">0表示不限购</div>
+                        </el-form-item>
+                        <el-form-item prop="countId" label="限购识别码" v-if="formData.maxCount > 0">
+                            <el-input v-model="formData.countId" style="width: 300px"></el-input>
+                            <div class="tip">相同识别码的藏品共享限购数量</div>
+                        </el-form-item>
+                    </div>
+                    <!-- <el-form-item prop="status" label="状态">
+                        <el-select v-model="formData.status" clearable filterable placeholder="请选择">
+                            <el-option
+                                v-for="item in statusOptions"
+                                :key="item.value"
+                                :label="item.label"
+                                :value="item.value"
+                            >
+                            </el-option>
+                        </el-select>
+                    </el-form-item> -->
+                    <el-form-item class="form-submit">
+                        <el-button @click="onSave" :loading="saving" type="primary"> 保存 </el-button>
+                        <el-button @click="onDelete" :disabled="saving" type="danger" v-if="formData.id">
+                            删除
+                        </el-button>
+                        <el-button @click="$router.go(-1)" :disabled="saving">取消</el-button>
+                    </el-form-item>
+                </el-form>
+            </div>
+        </div>
+
+        <el-dialog :visible.sync="showPrivilegeEditDialog" width="600px" :title="privilegeForm.name">
+            <el-form
+                ref="privilegeForm"
+                :model="privilegeForm"
+                label-position="right"
+                label-width="80px"
+                :rules="privelegeRules"
+            >
+                <el-form-item
+                    prop="detail"
+                    label="详细内容"
+                    v-if="privilegeForm.type === 'text' || privilegeForm.type === 'exchange'"
+                >
+                    <el-input type="textarea" :autosize="{ minRows: 3 }" v-model="privilegeForm.detail"></el-input>
+                </el-form-item>
+                <el-form-item prop="detail" label="二维码" v-if="privilegeForm.type === 'qrcode'">
+                    <single-upload v-model="privilegeForm.detail"></single-upload>
+                </el-form-item>
+                <el-form-item
+                    prop="remark"
+                    label="说明"
+                    v-if="privilegeForm.type === 'qrcode' || privilegeForm.type === 'code'"
+                >
+                    <el-input type="textarea" :autosize="{ minRows: 3 }" v-model="privilegeForm.remark"></el-input>
+                </el-form-item>
+            </el-form>
+            <div slot="footer">
+                <el-button @click="showPrivilegeEditDialog = false">取消</el-button>
+                <el-button type="primary" @click="savePrivilege">保存</el-button>
+            </div>
+        </el-dialog>
+    </div>
+</template>
+<script>
+import { mapState } from 'vuex';
+import resolveUrl from 'resolve-url';
+import ModelUpload from '../../components/ModelUpload.vue';
+export default {
+    name: 'CompanyCollectionEdit',
+    components: { ModelUpload },
+    created() {
+        Promise.all([
+            new Promise((resolve, reject) => {
+                if (this.$route.query.id) {
+                    return this.$http
+                        .get('companyCollection/get/' + this.$route.query.id)
+                        .then(res => {
+                            if (res.model3d) {
+                                let url = new URL(res.model3d.url);
+                                this.scale = Number(url.searchParams.get('scale')) || 1;
+                                this.yOffset = Number(url.searchParams.get('yOffset')) || 0;
+                                res.model3d.url = url.origin + url.pathname;
+                            }
+                            res.properties = res.properties || [];
+                            res.privileges = res.privileges || [];
+                            this.formData = res;
+                            resolve();
+                        })
+                        .catch(e => {
+                            console.log(e);
+                            this.$message.error(e.error);
+                            resolve();
+                        });
+                }
+                return resolve();
+            }),
+            this.$http
+                .post('/privilegeOption/all', { size: 10000, query: { del: false } }, { body: 'json' })
+                .then(res => {
+                    this.privilegeOptions = res.content;
+                    return Promise.resolve();
+                })
+        ]).then(() => {
+            this.formData.privileges.forEach(p => {
+                let idx = this.privilegeOptions.findIndex(i => i.name === p.name);
+                if (idx > -1) {
+                    this.$set(this.privilegeOptions[idx], 'added', true);
+                }
+            });
+        });
+    },
+    data() {
+        return {
+            saving: false,
+            formData: {
+                pic: [],
+                privileges: [],
+                properties: [],
+                status: 'PENDING'
+            },
+            rules: {
+                name: [
+                    {
+                        required: true,
+                        message: '请输入名称',
+                        trigger: 'blur'
+                    }
+                ],
+                pic: [
+                    {
+                        validator: (rule, value, callback) => {
+                            if (value) {
+                                if (!(value instanceof Array)) {
+                                    callback(new Error('请上传内容'));
+                                    return;
+                                } else {
+                                    for (let f of value) {
+                                        if (!f.url) {
+                                            callback(new Error('请上传内容'));
+                                            return;
+                                        }
+                                    }
+                                }
+                                callback();
+                            } else {
+                                callback(new Error('请上传内容'));
+                            }
+                        },
+                        trigger: 'blur'
+                    }
+                ],
+                detail: [
+                    {
+                        required: true,
+                        message: '请输入详情',
+                        trigger: 'blur'
+                    }
+                ],
+                type: [
+                    {
+                        required: true,
+                        message: '请输入类型',
+                        trigger: 'blur'
+                    }
+                ],
+                total: [
+                    {
+                        required: true,
+                        message: '请输入发行数量',
+                        trigger: 'blur'
+                    }
+                ],
+                price: [
+                    {
+                        required: true,
+                        message: '请输入价格',
+                        trigger: 'blur'
+                    }
+                ],
+                properties: [
+                    {
+                        validator: (rule, value, callback) => {
+                            if (value) {
+                                if (!(value instanceof Array)) {
+                                    callback(new Error('properties must be array!'));
+                                    return;
+                                } else {
+                                    for (let i = 0; i < value.length; i++) {
+                                        if (value[i].name === '' || value[i].name === undefined) {
+                                            callback(new Error('请填写名称'));
+                                            return;
+                                        }
+                                        if (value[i].value === '' || value[i].value === undefined) {
+                                            callback(new Error('请填写内容'));
+                                            return;
+                                        }
+                                    }
+                                }
+                            }
+                            callback();
+                        },
+                        trigger: 'blur'
+                    }
+                ],
+                category: [{ required: true, message: '请填写分类' }]
+            },
+            statusOptions: [
+                { label: '审核中', value: 'PENDING' },
+                { label: '通过', value: 'SUCCESS' },
+                { label: '失败', value: 'FAIL' }
+            ],
+            customUrl: resolveUrl(this.$baseUrl, 'upload/3dModel'),
+            cateogories: ['勋章', '收藏品', '数字艺术', '门票', '游戏', '音乐', '使用', '其他'],
+            scale: 1,
+            yOffset: 0,
+            privilegeOptions: [],
+            privilegeForm: {},
+            privelegeRules: {
+                detail: [{ required: true, message: '请填写内容' }],
+                remark: [{ required: true, message: '请填写说明' }]
+            },
+            showPrivilegeEditDialog: false
+        };
+    },
+    computed: {
+        ...mapState(['userInfo'])
+    },
+    methods: {
+        onSave() {
+            this.$refs.form.validate(valid => {
+                if (valid) {
+                    this.submit();
+                } else {
+                    return false;
+                }
+            });
+        },
+        submit() {
+            let data = { ...this.formData };
+            data.status = 'PENDING';
+            data.minter = this.userInfo.nickname;
+
+            this.saving = true;
+            this.$http
+                .post('/companyCollection/save', data, { body: 'json' })
+                .then(res => {
+                    this.saving = false;
+                    this.$message.success('成功');
+                    this.$router.go(-1);
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.saving = false;
+                    this.$message.error(e.error);
+                });
+        },
+        onDelete() {
+            this.$confirm('删除将无法恢复,确认要删除么?', '警告', { type: 'error' })
+                .then(() => {
+                    return this.$http.post(`/companyCollection/del/${this.formData.id}`);
+                })
+                .then(() => {
+                    this.$message.success('删除成功');
+                    this.$router.go(-1);
+                })
+                .catch(e => {
+                    if (e !== 'cancel') {
+                        console.log(e);
+                        this.$message.error((e || {}).error || '删除失败');
+                    }
+                });
+        },
+        addProperty() {
+            this.formData.properties.push({
+                name: '',
+                value: ''
+            });
+        },
+        delProperty(i) {
+            this.formData.properties.splice(i, 1);
+        },
+        addPrivilege(row, i) {
+            this.privilegeForm = { ...row };
+            this.showPrivilegeEditDialog = true;
+            if (this.$refs.privilegeForm) {
+                this.$nextTick(() => {
+                    this.$refs.privilegeForm.clearValidate();
+                });
+            }
+        },
+        editPrivilege(row, i) {
+            this.privilegeForm = { ...(this.formData.privileges.find(e => e.name === row.name) || {}) };
+            this.showPrivilegeEditDialog = true;
+            if (this.$refs.privilegeForm) {
+                this.$nextTick(() => {
+                    this.$refs.privilegeForm.clearValidate();
+                });
+            }
+        },
+        delPrivilege(row, i) {
+            let idx = this.formData.privileges.findIndex(e => e.name === row.name);
+            if (idx > -1) {
+                this.formData.privileges.splice(idx, 1);
+            }
+            this.$set(this.privilegeOptions[i], 'added', false);
+        },
+        savePrivilege() {
+            this.$refs.privilegeForm
+                .validate()
+                .then(() => {
+                    let i = this.formData.privileges.findIndex(e => e.name === this.privilegeForm.name);
+                    if (i > -1) {
+                        this.$set(this.formData.privileges, i, { ...this.privilegeForm });
+                    } else {
+                        this.formData.privileges.push({ ...this.privilegeForm });
+                    }
+                    let ii = this.privilegeOptions.findIndex(i => i.name === this.privilegeForm.name);
+                    console.log(ii);
+                    this.$set(this.privilegeOptions[ii], 'added', true);
+                    this.showPrivilegeEditDialog = false;
+                })
+                .catch(e => {
+                    console.log(e);
+                });
+        }
+    }
+};
+</script>
+<style lang="less" scoped>
+.inline-wrapper {
+    .el-form-item {
+        display: inline-block;
+    }
+}
+.tip {
+    font-size: 12px;
+    color: @text3;
+    margin-top: 5px;
+}
+/deep/.el-textarea.is-disabled .el-textarea__inner {
+    color: #7c7e7e;
+}
+</style>

+ 481 - 0
src/main/vue/src/views/company/CompanyCollectionShelf.vue

@@ -0,0 +1,481 @@
+<template>
+    <div class="edit-view">
+        <page-title>
+            <el-button @click="$router.go(-1)" :disabled="saving">取消</el-button>
+            <!-- <el-button @click="onDelete" :disabled="saving" type="danger" v-if="formData.id">删除</el-button> -->
+            <el-button @click="onSave" :loading="saving" type="primary">保存</el-button>
+        </page-title>
+        <div class="edit-view__content-wrapper">
+            <div class="edit-view__content-section">
+                <el-form
+                    :model="formData"
+                    :rules="rules"
+                    ref="form"
+                    label-width="120px"
+                    label-position="right"
+                    size="small"
+                >
+                    <el-form-item prop="reason" label="拒绝理由" v-if="formData.reason">
+                        <el-input
+                            type="textarea"
+                            v-model="formData.reason"
+                            :rows="3"
+                            style="width: 500px"
+                            disabled
+                        ></el-input>
+                    </el-form-item>
+                    <el-form-item prop="name" label="名称">
+                        <el-input v-model="formData.name" disabled style="width: 500px"></el-input>
+                    </el-form-item>
+                    <el-form-item prop="pic" label="图片">
+                        <object-upload
+                            v-model="formData.pic[0]"
+                            disabled
+                            compress
+                            width="3000"
+                            height="3000"
+                        ></object-upload>
+                        <div class="tip">支持JPG、PNG、GIF、MP4,推荐长宽比1:1</div>
+                    </el-form-item>
+                    <el-form-item prop="model3d" label="3D模型">
+                        <model-upload
+                            :limit="1"
+                            v-model="formData.model3d"
+                            :customUrl="customUrl"
+                            accept="application/zip"
+                            format="json"
+                            single
+                            :isDisabled="true"
+                        ></model-upload>
+                        <div class="tip">请将FBX文件与贴图打包成zip压缩包上传</div>
+                    </el-form-item>
+                    <el-form-item label="相机距离" v-if="formData.model3d">
+                        <el-input-number v-model="scale" :min="0.1" :step="0.1"></el-input-number>
+                    </el-form-item>
+                    <el-form-item label="Y轴偏移" v-if="formData.model3d">
+                        <el-input-number v-model="yOffset"></el-input-number>
+                    </el-form-item>
+                    <el-form-item prop="category" label="分类">
+                        <el-select v-model="formData.category" disabled>
+                            <el-option v-for="item in cateogories" :label="item" :value="item" :key="item"></el-option>
+                        </el-select>
+                    </el-form-item>
+                    <el-form-item prop="detail" label="详情" style="width: calc(100vw - 450px)">
+                        <rich-text v-model="formData.detail" disabled></rich-text>
+                    </el-form-item>
+                    <el-form-item prop="properties" label="特性" style="width: calc(100vw - 450px)" size="mini">
+                        <el-table :data="formData.properties">
+                            <el-table-column prop="name" label="名称">
+                                <template v-slot="{ row }">
+                                    <el-input
+                                        v-model="row.name"
+                                        placeholder="20字以内"
+                                        maxlength="20"
+                                        disabled
+                                    ></el-input>
+                                </template>
+                            </el-table-column>
+                            <el-table-column prop="value" label="内容">
+                                <template v-slot="{ row }">
+                                    <el-input
+                                        v-model="row.value"
+                                        placeholder="20字以内"
+                                        maxlength="20"
+                                        disabled
+                                    ></el-input>
+                                </template>
+                            </el-table-column>
+                            <!-- <el-table-column width="80" align="center">
+                                <template v-slot="{ $index }">
+                                    <el-button type="danger" plain size="mini" @click="delProperty($index)">
+                                        删除
+                                    </el-button>
+                                </template>
+                            </el-table-column> -->
+                        </el-table>
+                    </el-form-item>
+                    <!-- <el-form-item>
+                        <el-button size="mini" @click="addProperty"> 添加特性 </el-button>
+                    </el-form-item> -->
+                    <el-form-item label="特权" prop="privileges" style="width: calc(100vw - 450px)">
+                        <el-table :data="privilegeOptions">
+                            <el-table-column prop="name" label="可选特权" width="150"></el-table-column>
+                            <el-table-column prop="description"></el-table-column>
+                            <el-table-column width="125" align="right">
+                                <template v-slot="{ row, $index }">
+                                    <!-- <el-button size="mini" v-if="!row.added" @click="addPrivilege(row, $index)">
+                                        添加
+                                    </el-button> -->
+                                    <el-button size="mini" v-if="!!row.added" plain @click="editPrivilege(row, $index)">
+                                        查看
+                                    </el-button>
+                                    <!-- <el-button
+                                        size="mini"
+                                        v-if="!!row.added"
+                                        type="danger"
+                                        plain
+                                        @click="delPrivilege(row, $index)"
+                                    >
+                                        删除
+                                    </el-button> -->
+                                </template>
+                            </el-table-column>
+                        </el-table>
+                    </el-form-item>
+                    <div class="inline-wrapper">
+                        <el-form-item prop="price" label="价格">
+                            <el-input-number type="number" v-model="formData.price" disabled></el-input-number>
+                        </el-form-item>
+                        <el-form-item prop="price" label="原价">
+                            <el-input-number type="number" v-model="formData.originalPrice" disabled></el-input-number>
+                        </el-form-item>
+                    </div>
+                    <el-form-item prop="total" label="发行数量">
+                        <el-input-number v-model="formData.total" disabled></el-input-number>
+                    </el-form-item>
+                    <div class="inline-wrapper">
+                        <el-form-item prop="maxCount" label="限购">
+                            <el-input-number v-model="formData.maxCount" :min="0" :step="1" disabled></el-input-number>
+                            <div class="tip">0表示不限购</div>
+                        </el-form-item>
+                        <el-form-item prop="countId" label="限购识别码" v-if="formData.maxCount > 0">
+                            <el-input v-model="formData.countId" style="width: 300px" disabled></el-input>
+                            <div class="tip">相同识别码的藏品共享限购数量</div>
+                        </el-form-item>
+                    </div>
+                    <el-form-item prop="onShelf" label="上架">
+                        <el-radio v-model="formData.onShelf" :label="true">是</el-radio>
+                        <el-radio v-model="formData.onShelf" :label="false">否</el-radio>
+                    </el-form-item>
+                    <el-form-item prop="salable" label="可售" v-if="formData.onShelf === true">
+                        <el-radio v-model="formData.salable" :label="true">是</el-radio>
+                        <el-radio v-model="formData.salable" :label="false">仅展示</el-radio>
+                    </el-form-item>
+                    <el-form-item class="form-submit">
+                        <el-button @click="onSave" :loading="saving" type="primary">保存</el-button>
+                        <!-- <el-button @click="onDelete" :disabled="saving" type="danger" v-if="formData.id">
+                            删除
+                        </el-button> -->
+                        <el-button @click="$router.go(-1)" :disabled="saving">取消</el-button>
+                    </el-form-item>
+                </el-form>
+            </div>
+        </div>
+
+        <el-dialog :visible.sync="showPrivilegeEditDialog" width="600px" :title="privilegeForm.name">
+            <el-form
+                ref="privilegeForm"
+                :model="privilegeForm"
+                label-position="right"
+                label-width="80px"
+                :rules="privelegeRules"
+            >
+                <el-form-item
+                    prop="detail"
+                    label="详细内容"
+                    v-if="privilegeForm.type === 'text' || privilegeForm.type === 'exchange'"
+                >
+                    <el-input
+                        type="textarea"
+                        :autosize="{ minRows: 3 }"
+                        v-model="privilegeForm.detail"
+                        disabled
+                    ></el-input>
+                </el-form-item>
+                <el-form-item prop="detail" label="二维码" v-if="privilegeForm.type === 'qrcode'">
+                    <single-upload v-model="privilegeForm.detail"></single-upload>
+                </el-form-item>
+                <el-form-item
+                    prop="remark"
+                    label="说明"
+                    v-if="privilegeForm.type === 'qrcode' || privilegeForm.type === 'code'"
+                >
+                    <el-input
+                        type="textarea"
+                        :autosize="{ minRows: 3 }"
+                        v-model="privilegeForm.remark"
+                        disabled
+                    ></el-input>
+                </el-form-item>
+            </el-form>
+            <div slot="footer">
+                <el-button @click="showPrivilegeEditDialog = false">取消</el-button>
+            </div>
+        </el-dialog>
+    </div>
+</template>
+<script>
+import resolveUrl from 'resolve-url';
+import ModelUpload from '@/components/ModelUpload.vue';
+export default {
+    components: { ModelUpload },
+    created() {
+        Promise.all([
+            new Promise((resolve, reject) => {
+                if (this.$route.query.id) {
+                    return this.$http
+                        .get('collection/get/' + this.$route.query.id)
+                        .then(res => {
+                            if (res.model3d) {
+                                let url = new URL(res.model3d.url);
+                                this.scale = Number(url.searchParams.get('scale')) || 1;
+                                this.yOffset = Number(url.searchParams.get('yOffset')) || 0;
+                                res.model3d.url = url.origin + url.pathname;
+                            }
+                            res.properties = res.properties || [];
+                            res.privileges = res.privileges || [];
+                            this.formData = res;
+                            resolve();
+                        })
+                        .catch(e => {
+                            console.log(e);
+                            this.$message.error(e.error);
+                            resolve();
+                        });
+                }
+                return resolve();
+            }),
+            this.$http
+                .post('/privilegeOption/all', { size: 10000, query: { del: false } }, { body: 'json' })
+                .then(res => {
+                    this.privilegeOptions = res.content;
+                    return Promise.resolve();
+                })
+        ]).then(() => {
+            this.formData.privileges.forEach(p => {
+                let idx = this.privilegeOptions.findIndex(i => i.name === p.name);
+                if (idx > -1) {
+                    this.$set(this.privilegeOptions[idx], 'added', true);
+                }
+            });
+        });
+    },
+    data() {
+        return {
+            saving: false,
+            formData: {
+                pic: [],
+                privileges: [],
+                properties: []
+            },
+            rules: {
+                name: [
+                    {
+                        required: true,
+                        message: '请输入名称',
+                        trigger: 'blur'
+                    }
+                ],
+                pic: [
+                    {
+                        validator: (rule, value, callback) => {
+                            if (value) {
+                                if (!(value instanceof Array)) {
+                                    callback(new Error('请上传内容'));
+                                    return;
+                                } else {
+                                    for (let f of value) {
+                                        if (!f.url) {
+                                            callback(new Error('请上传内容'));
+                                            return;
+                                        }
+                                    }
+                                }
+                                callback();
+                            } else {
+                                callback(new Error('请上传内容'));
+                            }
+                        },
+                        trigger: 'blur'
+                    }
+                ],
+                detail: [
+                    {
+                        required: true,
+                        message: '请输入详情',
+                        trigger: 'blur'
+                    }
+                ],
+                type: [
+                    {
+                        required: true,
+                        message: '请输入类型',
+                        trigger: 'blur'
+                    }
+                ],
+                total: [
+                    {
+                        required: true,
+                        message: '请输入发行数量',
+                        trigger: 'blur'
+                    }
+                ],
+                price: [
+                    {
+                        required: true,
+                        message: '请输入价格',
+                        trigger: 'blur'
+                    }
+                ],
+                properties: [
+                    {
+                        validator: (rule, value, callback) => {
+                            if (value) {
+                                if (!(value instanceof Array)) {
+                                    callback(new Error('properties must be array!'));
+                                    return;
+                                } else {
+                                    for (let i = 0; i < value.length; i++) {
+                                        if (value[i].name === '' || value[i].name === undefined) {
+                                            callback(new Error('请填写名称'));
+                                            return;
+                                        }
+                                        if (value[i].value === '' || value[i].value === undefined) {
+                                            callback(new Error('请填写内容'));
+                                            return;
+                                        }
+                                    }
+                                }
+                            }
+                            callback();
+                        },
+                        trigger: 'blur'
+                    }
+                ],
+                category: [{ required: true, message: '请填写分类' }]
+            },
+            statusOptions: [
+                { label: '审核中', value: 'PENDING' },
+                { label: '通过', value: 'SUCCESS' },
+                { label: '失败', value: 'FAIL' }
+            ],
+            customUrl: resolveUrl(this.$baseUrl, 'upload/3dModel'),
+            cateogories: ['勋章', '收藏品', '数字艺术', '门票', '游戏', '音乐', '使用', '其他'],
+            scale: 1,
+            yOffset: 0,
+            privilegeOptions: [],
+            privilegeForm: {},
+            privelegeRules: {
+                detail: [{ required: true, message: '请填写内容' }],
+                remark: [{ required: true, message: '请填写说明' }]
+            },
+            showPrivilegeEditDialog: false
+        };
+    },
+    methods: {
+        onSave() {
+            this.$refs.form.validate(valid => {
+                if (valid) {
+                    this.submit();
+                } else {
+                    return false;
+                }
+            });
+        },
+        submit() {
+            let data = { ...this.formData };
+
+            this.saving = true;
+            this.$http
+                .post('/collection/save', data, { body: 'json' })
+                .then(res => {
+                    this.saving = false;
+                    this.$message.success('成功');
+                    this.$router.go(-1);
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.saving = false;
+                    this.$message.error(e.error);
+                });
+        },
+        onDelete() {
+            this.$confirm('删除将无法恢复,确认要删除么?', '警告', { type: 'error' })
+                .then(() => {
+                    return this.$http.post(`/collection/del/${this.formData.id}`);
+                })
+                .then(() => {
+                    this.$message.success('删除成功');
+                    this.$router.go(-1);
+                })
+                .catch(e => {
+                    if (e !== 'cancel') {
+                        console.log(e);
+                        this.$message.error((e || {}).error || '删除失败');
+                    }
+                });
+        },
+        addProperty() {
+            this.formData.properties.push({
+                name: '',
+                value: ''
+            });
+        },
+        delProperty(i) {
+            this.formData.properties.splice(i, 1);
+        },
+        addPrivilege(row, i) {
+            this.privilegeForm = { ...row };
+            this.showPrivilegeEditDialog = true;
+            if (this.$refs.privilegeForm) {
+                this.$nextTick(() => {
+                    this.$refs.privilegeForm.clearValidate();
+                });
+            }
+        },
+        editPrivilege(row, i) {
+            this.privilegeForm = { ...(this.formData.privileges.find(e => e.name === row.name) || {}) };
+            this.showPrivilegeEditDialog = true;
+            if (this.$refs.privilegeForm) {
+                this.$nextTick(() => {
+                    this.$refs.privilegeForm.clearValidate();
+                });
+            }
+        },
+        delPrivilege(row, i) {
+            let idx = this.formData.privileges.findIndex(e => e.name === row.name);
+            if (idx > -1) {
+                this.formData.privileges.splice(idx, 1);
+            }
+            this.$set(this.privilegeOptions[i], 'added', false);
+        },
+        savePrivilege() {
+            this.$refs.privilegeForm
+                .validate()
+                .then(() => {
+                    let i = this.formData.privileges.findIndex(e => e.name === this.privilegeForm.name);
+                    if (i > -1) {
+                        this.$set(this.formData.privileges, i, { ...this.privilegeForm });
+                    } else {
+                        this.formData.privileges.push({ ...this.privilegeForm });
+                    }
+                    let ii = this.privilegeOptions.findIndex(i => i.name === this.privilegeForm.name);
+                    console.log(ii);
+                    this.$set(this.privilegeOptions[ii], 'added', true);
+                    this.showPrivilegeEditDialog = false;
+                })
+                .catch(e => {
+                    console.log(e);
+                });
+        }
+    }
+};
+</script>
+<style lang="less" scoped>
+.inline-wrapper {
+    .el-form-item {
+        display: inline-block;
+    }
+}
+.tip {
+    font-size: 12px;
+    color: @text3;
+    margin-top: 5px;
+}
+/deep/.el-input.is-disabled .el-input__inner {
+    color: #7c7e7e;
+}
+/deep/.el-textarea.is-disabled .el-textarea__inner {
+    color: #7c7e7e;
+}
+</style>

+ 293 - 0
src/main/vue/src/views/company/CompanyRoomEdit.vue

@@ -0,0 +1,293 @@
+<template>
+    <div class="edit-view">
+        <page-title>
+            <el-button @click="$router.go(-1)" :disabled="saving">取消</el-button>
+            <!-- <el-button @click="onDelete" :disabled="saving" type="danger" v-if="formData.id"> 删除 </el-button> -->
+            <el-button @click="onSave" :loading="saving" type="primary">保存</el-button>
+        </page-title>
+        <div class="edit-view__content-wrapper">
+            <div class="edit-view__content-section">
+                <el-form
+                    :model="formData"
+                    :rules="rules"
+                    ref="form"
+                    label-width="125px"
+                    label-position="right"
+                    size="small"
+                    style="max-width: 750px"
+                >
+                    <el-form-item prop="reason" label="拒绝理由" v-if="formData.reason && formData.status!='SUCCESS'">
+                        <el-input
+                            type="textarea"
+                            v-model="formData.reason"
+                            :rows="3"
+                            style="width: 500px"
+                            disabled
+                        ></el-input>
+                    </el-form-item>
+                    <el-form-item prop="userId" label="用户ID">
+                        <el-input-number
+                            type="number"
+                            v-model="formData.userId"
+                            style="width: 200px"
+                            disabled
+                        ></el-input-number>
+                    </el-form-item>
+                    <el-form-item prop="nickname" label="昵称">
+                        <el-input v-model="formData.nickname" style="width: 280px" disabled></el-input>
+                    </el-form-item>
+                    <el-form-item prop="maxCollection" label="最多可放藏品数量">
+                        <el-input-number
+                            type="number"
+                            v-model="formData.maxCollection"
+                            style="width: 280px"
+                            disabled
+                        ></el-input-number>
+                    </el-form-item>
+                    <el-form-item prop="pic" label="logo">
+                        <single-upload v-model="formData.pic"></single-upload>
+                    </el-form-item>
+                    <el-form-item prop="introduction" label="简介">
+                        <el-input type="textarea" :rows="4" v-model="formData.introduction"></el-input>
+                    </el-form-item>
+
+                    <el-form-item prop="settingId" label="模版">
+                        <el-select v-model="formData.settingId" style="width: 280px" @change="changeBg">
+                            <el-option
+                                v-for="item in settings"
+                                :key="item.id"
+                                :label="item.name"
+                                :value="item.id"
+                            ></el-option>
+                        </el-select>
+                    </el-form-item>
+                    <el-form-item prop="headBg" label="背景" v-if="formData.settingId">
+                        <el-image :src="formData.headBg" style="height: 170px"></el-image>
+                        <el-image :src="formData.showroomBg" style="height: 170px"></el-image>
+                    </el-form-item>
+                    <el-form-item prop="collections" label="藏品">
+                        <el-table :data="formData.collections">
+                            <el-table-column type="index" label="#"></el-table-column>
+                            <el-table-column prop="collectionId" label="ID" width="110"></el-table-column>
+                            <el-table-column prop="pic" label="图" width="110">
+                                <template slot-scope="{ row }">
+                                    <el-image style="width: 30px; height: 30px" :src="row.pic" fit="cover"></el-image>
+                                </template>
+                            </el-table-column>
+                            <el-table-column prop="name" label="名称"></el-table-column>
+                            <el-table-column width="80" align="center">
+                                <template v-slot="{ $index }">
+                                    <el-link type="danger" plain @click="delCollection($index)" size="mini">
+                                        删除
+                                    </el-link>
+                                </template>
+                            </el-table-column>
+                        </el-table>
+                        <el-button @click="addCollection" size="mini">添加</el-button>
+                    </el-form-item>
+                    <!-- <el-form-item prop="publish" label="发布" v-if="formData.status == 'SUCCESS'">
+                        <el-radio v-model="formData.publish" :label="true">是</el-radio>
+                        <el-radio v-model="formData.publish" :label="false">否</el-radio>
+                    </el-form-item> -->
+
+                    <el-form-item class="form-submit">
+                        <el-button @click="onSave" :loading="saving" type="primary"> 保存 </el-button>
+                        <!-- <el-button @click="onDelete" :disabled="saving" type="danger" v-if="formData.id">
+                            删除
+                        </el-button> -->
+                        <el-button @click="$router.go(-1)" :disabled="saving">取消</el-button>
+                    </el-form-item>
+                </el-form>
+            </div>
+            <el-dialog title="添加藏品" :visible.sync="showCollectionDialog" width="500px">
+                <div style="margin-bottom: 10px">如果找到没有藏品,请先查看藏品是否审核通过</div>
+                <el-form :model="{ collectionId }" ref="collectionForm" inline>
+                    <el-form-item prop="collectionId" :rules="{ required: true, message: '请选择藏品' }">
+                        <el-select v-model="collectionId" style="width: 350px">
+                            <el-option
+                                v-for="item in collections"
+                                :key="item.id"
+                                :label="item.name"
+                                :value="item.id"
+                            ></el-option>
+                        </el-select>
+                    </el-form-item>
+                </el-form>
+                <div slot="footer">
+                    <el-button @click="showCollectionDialog = false">取消</el-button>
+                    <el-button type="primary" size="mini" @click="saveCollection">确定</el-button>
+                </div>
+            </el-dialog>
+        </div>
+    </div>
+</template>
+<script>
+import { mapState } from 'vuex';
+
+export default {
+    name: 'ShowroomEdit',
+    created() {
+        if (this.$route.query.id) {
+            this.$http
+                .get('showroom/get/' + this.$route.query.id)
+                .then(res => {
+                    this.formData = res;
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.$message.error(e.error);
+                });
+        }
+        this.$http
+            .post('/setting/byFlag', { flag: 2 })
+            .then(res => {
+                this.settings = res;
+            })
+            .catch(e => {
+                console.log(e);
+                this.$message.error(e.error);
+            });
+        this.$http
+            .post(
+                '/collection/all',
+                {
+                    sort: 'sort,desc;createdAt,desc',
+                    query: { del: false, ownerId: this.userInfo.id }
+                },
+                { body: 'json' }
+            )
+            .then(res => {
+                this.collections = res.content;
+            });
+    },
+    data() {
+        return {
+            saving: false,
+            formData: {
+                showCollections: []
+            },
+            rules: {},
+            statusOptions: [
+                { label: '未审核', value: 'NOT_AUTH' },
+                { label: '审核中', value: 'PENDING' },
+                { label: '已审核', value: 'SUCCESS' },
+                { label: '失败', value: 'FAIL' }
+            ],
+            settings: [],
+            collections: [],
+            showCollectionDialog: false,
+            collectionId: []
+        };
+    },
+    computed: {
+        ...mapState(['userInfo'])
+    },
+    methods: {
+        onSave() {
+            this.$refs.form.validate(valid => {
+                if (valid) {
+                    this.$confirm('提交后将重新审核,确认提交吗', '提示', {
+                        confirmButtonText: '确定',
+                        cancelButtonText: '取消',
+                        type: 'warning'
+                    })
+                        .then(() => {
+                            this.submit();
+                        })
+                        .catch(() => {});
+                } else {
+                    return false;
+                }
+            });
+        },
+        submit() {
+            let data = { ...this.formData };
+            data.status = 'PENDING';
+
+            this.saving = true;
+            this.$http
+                .post('/showroom/update', data, { body: 'json' })
+                .then(res => {
+                    this.saving = false;
+                    this.$message.success('成功');
+                    this.$router.go(-1);
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.saving = false;
+                    this.$message.error(e.error);
+                });
+        },
+        onDelete() {
+            this.$confirm('删除将无法恢复,确认要删除么?', '警告', { type: 'error' })
+                .then(() => {
+                    return this.$http.post(`/showroom/del/${this.formData.id}`);
+                })
+                .then(() => {
+                    this.$message.success('删除成功');
+                    this.$router.go(-1);
+                })
+                .catch(e => {
+                    if (e !== 'cancel') {
+                        console.log(e);
+                        this.$message.error((e || {}).error || '删除失败');
+                    }
+                });
+        },
+        changeBg(res) {
+            let data = this.settings.find(item => {
+                return item.id === res;
+            });
+            this.formData.headBg = data.pic;
+            this.formData.showroomBg = data.description;
+        },
+        addCollection() {
+            this.showCollectionDialog = true;
+            if (this.$refs.collectionForm) {
+                this.$nextTick(() => {
+                    this.$refs.collectionForm.clearValidate();
+                });
+            }
+        },
+        delCollection(index) {
+            this.formData.collections.splice(index, 1);
+        },
+        saveCollection() {
+            this.$refs.collectionForm
+                .validate()
+                .then(() => {
+                    if (!this.formData.collections.find(i => i.id === this.collectionId)) {
+                        let data = this.collections.find(i => i.id === this.collectionId);
+                        if (data.pic[0].type == 'video/mp4') {
+                            this.formData.collections.push({
+                                showroomId: this.formData.id,
+                                collectionId: data.id,
+                                pic: data.pic[0].thumb,
+                                name: data.name
+                            });
+                        } else {
+                            this.formData.collections.push({
+                                showroomId: this.formData.id,
+                                collectionId: data.id,
+                                pic: data.pic[0].url,
+                                name: data.name
+                            });
+                        }
+                        this.showCollectionDialog = false;
+                    } else {
+                        this.$message.error('请勿重复添加');
+                    }
+                })
+                .catch(() => {});
+        }
+    }
+};
+</script>
+<style lang="less" scoped>
+/deep/.el-input.is-disabled .el-input__inner {
+    color: #7c7e7e;
+}
+/deep/.el-textarea.is-disabled .el-textarea__inner {
+    color: #7c7e7e;
+}
+</style>

+ 189 - 0
src/main/vue/src/views/company/CompanyRoomList.vue

@@ -0,0 +1,189 @@
+<template>
+    <div class="list-view">
+        <page-title> </page-title>
+        <div class="filters-container">
+            <el-input
+                placeholder="搜索..."
+                v-model="search"
+                clearable
+                class="filter-item search"
+                @keyup.enter.native="getData"
+            >
+                <el-button @click="getData" slot="append" icon="el-icon-search"> </el-button>
+            </el-input>
+        </div>
+        <el-table
+            :data="tableData"
+            row-key="id"
+            ref="table"
+            header-row-class-name="table-header-row"
+            header-cell-class-name="table-header-cell"
+            row-class-name="table-row"
+            cell-class-name="table-cell"
+            :height="tableHeight"
+            v-loading="fetchingData"
+        >
+            <el-table-column v-if="multipleMode" align="center" type="selection" width="50"> </el-table-column>
+            <el-table-column prop="id" label="ID" width="100"> </el-table-column>
+            <el-table-column prop="userId" label="用户ID"> </el-table-column>
+            <el-table-column prop="nickname" label="昵称"> </el-table-column>
+            <el-table-column prop="pic" label="logo">
+                <template slot-scope="{ row }">
+                    <el-image
+                        style="width: 30px; height: 30px"
+                        :src="row.pic"
+                        fit="cover"
+                        :preview-src-list="[row.pic]"
+                    ></el-image>
+                </template>
+            </el-table-column>
+            <el-table-column prop="introduction" label="简介"> </el-table-column>
+            <!-- <el-table-column prop="publish" label="发布">
+                <template slot-scope="{ row }">
+                    <el-tag :type="row.publish ? 'success' : 'info'">{{ row.publish ? '是' : '否' }}</el-tag>
+                </template>
+            </el-table-column> -->
+            <el-table-column prop="likes" label="点赞"> </el-table-column>
+            <el-table-column prop="status" label="状态" :formatter="statusFormatter"> </el-table-column>
+            <el-table-column label="操作" align="center" fixed="right" width="150">
+                <template slot-scope="{ row }">
+                    <el-button @click="editRow(row)" type="primary" size="mini" plain>编辑</el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+        <div class="pagination-wrapper">
+            <!-- <div class="multiple-mode-wrapper">
+                <el-button v-if="!multipleMode" @click="toggleMultipleMode(true)">批量编辑</el-button>
+                <el-button-group v-else>
+                    <el-button @click="operation1">批量操作1</el-button>
+                    <el-button @click="operation2">批量操作2</el-button>
+                    <el-button @click="toggleMultipleMode(false)">取消</el-button>
+                </el-button-group>
+            </div> -->
+            <el-pagination
+                background
+                @size-change="onSizeChange"
+                @current-change="onCurrentChange"
+                :current-page="page"
+                :page-sizes="[10, 20, 30, 40, 50]"
+                :page-size="pageSize"
+                layout="total, sizes, prev, pager, next, jumper"
+                :total="totalElements"
+            >
+            </el-pagination>
+        </div>
+    </div>
+</template>
+<script>
+import { mapState } from 'vuex';
+import pageableTable from '@/mixins/pageableTable';
+
+export default {
+    name: 'ShowroomList',
+    mixins: [pageableTable],
+    data() {
+        return {
+            multipleMode: false,
+            search: '',
+            url: '/showroom/all',
+            downloading: false,
+            statusOptions: [
+                { label: '未审核', value: 'NOT_AUTH' },
+                { label: '审核中', value: 'PENDING' },
+                { label: '已审核', value: 'SUCCESS' },
+                { label: '失败', value: 'FAIL' }
+            ]
+        };
+    },
+    computed: {
+        selection() {
+            return this.$refs.table.selection.map(i => i.id);
+        },
+        ...mapState(['userInfo'])
+    },
+    methods: {
+        statusFormatter(row, column, cellValue, index) {
+            let selectedOption = this.statusOptions.find(i => i.value === cellValue);
+            if (selectedOption) {
+                return selectedOption.label;
+            }
+            return '';
+        },
+        beforeGetData() {
+            return { search: this.search, query: { del: false, userId: this.userInfo.id } };
+        },
+        toggleMultipleMode(multipleMode) {
+            this.multipleMode = multipleMode;
+            if (!multipleMode) {
+                this.$refs.table.clearSelection();
+            }
+        },
+        addRow() {
+            this.$router.push({
+                path: '/showroomEdit',
+                query: {
+                    ...this.$route.query
+                }
+            });
+        },
+        editRow(row) {
+            this.$router.push({
+                path: '/companyRoomEdit',
+                query: {
+                    id: row.id
+                }
+            });
+        },
+        download() {
+            this.downloading = true;
+            this.$axios
+                .get('/showroom/excel', {
+                    responseType: 'blob',
+                    params: { size: 10000 }
+                })
+                .then(res => {
+                    console.log(res);
+                    this.downloading = false;
+                    const downloadUrl = window.URL.createObjectURL(new Blob([res.data]));
+                    const link = document.createElement('a');
+                    link.href = downloadUrl;
+                    link.setAttribute('download', res.headers['content-disposition'].split('filename=')[1]);
+                    document.body.appendChild(link);
+                    link.click();
+                    link.remove();
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.downloading = false;
+                    this.$message.error(e.error);
+                });
+        },
+        operation1() {
+            this.$notify({
+                title: '提示',
+                message: this.selection
+            });
+        },
+        operation2() {
+            this.$message('操作2');
+        },
+        deleteRow(row) {
+            this.$alert('删除将无法恢复,确认要删除么?', '警告', { type: 'error' })
+                .then(() => {
+                    return this.$http.post(`/showroom/del/${row.id}`);
+                })
+                .then(() => {
+                    this.$message.success('删除成功');
+                    this.getData();
+                })
+                .catch(e => {
+                    if (e !== 'cancel') {
+                        this.$message.error(e.error);
+                    }
+                });
+        }
+    }
+};
+</script>
+<style lang="less" scoped>
+</style>

+ 1 - 1
src/main/vue/src/widgets/PriceWidget.vue

@@ -3,7 +3,7 @@
         <i class="fa-fw fas fa-shopping-basket fa-3x" style="color: #40c9c6;"></i>
         <div class="info">
             <div class="text">官方交易额/二手市场(万元)</div>
-            <div class="num">{{ getNum(info.officialPrice) }}/{{ getNum(info.transferPrice) }}</div>
+            <div class="num">{{ getNum(info.officialPrice || 0) }}/{{ getNum(info.transferPrice || 0) }}</div>
         </div>
     </widget-card>
 </template>

+ 1 - 1
src/main/vue/src/widgets/TopWidget.vue

@@ -33,7 +33,7 @@
                 <div class="no">NO.{{ index + 1 }}</div>
                 <div class="content">
                     <div class="text1">{{ item.to_user }}</div>
-                    <div class="text2">{{ item.total }}元</div>
+                    <div class="text2">{{ item.total || 0 }}元</div>
                 </div>
             </router-link>
         </div>

+ 9 - 1
src/test/java/com/izouma/nineth/service/AssetServiceTest.java

@@ -18,6 +18,7 @@ import org.apache.rocketmq.spring.core.RocketMQTemplate;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 
+import java.math.BigDecimal;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -244,7 +245,14 @@ class AssetServiceTest extends ApplicationTests {
     }
 
     @Test
-    public void cancelPublicShow(){
+    public void cancelPublicShow() {
         assetService.cancelPublic(202159L);
     }
+
+    @Test
+    public void transfer() {
+        Asset asset = assetRepo.findById(662035L).orElse(null);
+        User user = userRepo.findById(9972L).orElse(null);
+        assetService.transfer(asset, BigDecimal.ZERO, user, "转赠", null);
+    }
 }

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov