licailing hace 3 años
padre
commit
fee0711339

+ 1 - 0
src/main/java/com/izouma/nineth/domain/Asset.java

@@ -211,6 +211,7 @@ public class Asset extends BaseEntity {
                 .ownerId(user.getId())
                 .ownerAvatar(user.getAvatar())
                 .projectId(item.getProjectId())
+                .type(item.getType())
                 .build();
     }
 }

+ 19 - 3
src/main/java/com/izouma/nineth/domain/AssetPost.java

@@ -1,5 +1,7 @@
 package com.izouma.nineth.domain;
 
+import com.izouma.nineth.converter.FileObjectConverter;
+import com.izouma.nineth.converter.FileObjectListConverter;
 import com.izouma.nineth.enums.PostStatus;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
@@ -8,9 +10,8 @@ import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 
-import javax.persistence.Entity;
-import javax.persistence.EnumType;
-import javax.persistence.Enumerated;
+import javax.persistence.*;
+import java.util.List;
 
 @Data
 @Entity
@@ -23,6 +24,21 @@ public class AssetPost extends BaseEntity {
 
     private Long assetId;
 
+    @ApiModelProperty("名称")
+    private String name;
+
+    @ApiModelProperty("编号")
+    private Integer number;
+
+    @ApiModelProperty("图片")
+    @Convert(converter = FileObjectListConverter.class)
+    @Column(columnDefinition = "TEXT")
+    private List<FileObject> pic;
+
+    @Column(columnDefinition = "TEXT")
+    @Convert(converter = FileObjectConverter.class)
+    private FileObject model3d;
+
     @ApiModelProperty("收货人")
     private String contactName;
 

+ 8 - 4
src/main/java/com/izouma/nineth/domain/BlindBoxItem.java

@@ -1,18 +1,18 @@
 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.CollectionType;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 
-import javax.persistence.Column;
-import javax.persistence.Convert;
-import javax.persistence.Entity;
+import javax.persistence.*;
 import java.math.BigDecimal;
 import java.util.List;
 
@@ -37,7 +37,7 @@ public class BlindBoxItem extends BaseEntity {
     private List<FileObject> pic;
 
     @Column(columnDefinition = "TEXT")
-    @Convert(converter = FileObjectListConverter.class)
+    @Convert(converter = FileObjectConverter.class)
     private FileObject model3d;
 
     @ApiModelProperty("铸造者")
@@ -93,4 +93,8 @@ public class BlindBoxItem extends BaseEntity {
     private boolean rare;
 
     private int projectId;
+
+    @ApiModelProperty("类型")
+    @Enumerated(EnumType.STRING)
+    private CollectionType type;
 }

+ 13 - 5
src/main/java/com/izouma/nineth/service/AssetPostService.java

@@ -37,8 +37,11 @@ public class AssetPostService {
     衍生品邮寄不支付快递费
     衍生品暂不可以取消邮寄
     */
-    public void post(Long userId, Long assetId, Long addressId) {
+    public AssetPost assetPost(Long userId, Long assetId, Long addressId) {
         Asset asset = assetRepo.findById(assetId).orElseThrow(new BusinessException("无此衍生品"));
+        if (userId.equals(asset.getUserId())) {
+            throw new BusinessException("此衍生品不属于你");
+        }
         if (asset.isPublicShow()) {
             if (asset.isConsignment()) {
                 throw new BusinessException("请先取消寄售");
@@ -60,6 +63,9 @@ public class AssetPostService {
 
         AssetPost build = AssetPost.builder()
                 .assetId(assetId)
+                .name(asset.getName())
+                .number(asset.getNumber())
+                .pic(asset.getPic())
                 .userId(userId)
                 .contactName(userAddress.getName())
                 .contactPhone(userAddress.getPhone())
@@ -68,14 +74,16 @@ public class AssetPostService {
                 .status(PostStatus.DELIVERY)
                 .build();
 
-        assetPostRepo.save(build);
+
         asset.setStatus(AssetStatus.POSTING);
+        assetRepo.save(asset);
+        return assetPostRepo.save(build);
     }
 
-    public void updateStatus(Long assetPostId, PostStatus status) {
-        AssetPost assetPost = assetPostRepo.findById(assetPostId).orElseThrow(new BusinessException("无邮寄信息"));
+    public void updateStatus(Long id, PostStatus status) {
+        AssetPost assetPost = assetPostRepo.findById(id).orElseThrow(new BusinessException("无邮寄信息"));
         assetPost.setStatus(status);
-        if (PostStatus.FINISH.equals(status)) {
+        if (PostStatus.RECEIVE.equals(status)) {
             Asset asset = assetRepo.findById(assetPost.getAssetId()).orElseThrow(new BusinessException("无衍生品"));
             asset.setStatus(AssetStatus.POSTED);
             assetRepo.save(asset);

+ 14 - 1
src/main/java/com/izouma/nineth/web/AssetPostController.java

@@ -1,10 +1,13 @@
 package com.izouma.nineth.web;
+
 import com.izouma.nineth.domain.AssetPost;
+import com.izouma.nineth.enums.PostStatus;
 import com.izouma.nineth.service.AssetPostService;
 import com.izouma.nineth.dto.PageQuery;
 import com.izouma.nineth.exception.BusinessException;
 import com.izouma.nineth.repo.AssetPostRepo;
 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;
@@ -20,7 +23,7 @@ import java.util.List;
 @AllArgsConstructor
 public class AssetPostController extends BaseController {
     private AssetPostService assetPostService;
-    private AssetPostRepo assetPostRepo;
+    private AssetPostRepo    assetPostRepo;
 
     //@PreAuthorize("hasRole('ADMIN')")
     @PostMapping("/save")
@@ -56,5 +59,15 @@ public class AssetPostController extends BaseController {
         List<AssetPost> data = all(pageQuery).getContent();
         ExcelUtils.export(response, data);
     }
+
+    @PostMapping("/assetPost")
+    public void assetPost(@RequestParam Long assetId, @RequestParam Long addressId) {
+        assetPostService.assetPost(SecurityUtils.getAuthenticatedUser().getId(), assetId, addressId);
+    }
+
+    @PostMapping("/updateStatus")
+    public void updateStatus(@RequestParam Long id, @RequestParam PostStatus status) {
+        assetPostService.updateStatus(id, status);
+    }
 }
 

+ 21 - 5
src/main/vue/src/router.js

@@ -444,17 +444,33 @@ const router = new Router({
                     name: 'AssetPostEdit',
                     component: () => import(/* webpackChunkName: "assetPostEdit" */ '@/views/AssetPostEdit.vue'),
                     meta: {
-                       title: '邮寄衍生品编辑',
-                    },
+                        title: '邮寄衍生品编辑'
+                    }
                 },
                 {
                     path: '/assetPostList',
                     name: 'AssetPostList',
                     component: () => import(/* webpackChunkName: "assetPostList" */ '@/views/AssetPostList.vue'),
                     meta: {
-                       title: '邮寄衍生品',
-                    },
-               }
+                        title: '邮寄衍生品'
+                    }
+                },
+                {
+                    path: '/derivativeEdit',
+                    name: 'DerivativeEdit',
+                    component: () => import(/* webpackChunkName: "derivativeEdit" */ '@/views/DerivativeEdit.vue'),
+                    meta: {
+                        title: '藏品管理编辑'
+                    }
+                },
+                {
+                    path: '/derivativeList',
+                    name: 'DerivativeList',
+                    component: () => import(/* webpackChunkName: "derivativeList" */ '@/views/DerivativeList.vue'),
+                    meta: {
+                        title: '藏品管理'
+                    }
+                }
                 /**INSERT_LOCATION**/
             ]
         },

+ 93 - 89
src/main/vue/src/views/AssetPostEdit.vue

@@ -2,38 +2,40 @@
     <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="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="80px" label-position="right"
-                         size="small"
-                         style="max-width: 500px;">
-                        <el-form-item prop="assetId" label="资产ID">
-                                    <el-input-number type="number" v-model="formData.assetId"></el-input-number>
-                        </el-form-item>
-                        <el-form-item prop="contactName" label="收货人">
-                                    <el-input v-model="formData.contactName"></el-input>
-                        </el-form-item>
-                        <el-form-item prop="contactPhone" label="收货电话">
-                                    <el-input v-model="formData.contactPhone"></el-input>
-                        </el-form-item>
-                        <el-form-item prop="address" label="收货地址">
-                                    <el-input v-model="formData.address"></el-input>
-                        </el-form-item>
-                        <el-form-item prop="courier" label="快递">
-                                    <el-input v-model="formData.courier"></el-input>
-                        </el-form-item>
-                        <el-form-item prop="courierId" label="快递单号">
-                                    <el-input v-model="formData.courierId"></el-input>
-                        </el-form-item>
+                <el-form
+                    :model="formData"
+                    :rules="rules"
+                    ref="form"
+                    label-width="80px"
+                    label-position="right"
+                    size="small"
+                    style="max-width: 500px"
+                >
+                    <!-- <el-form-item prop="assetId" label="资产ID">
+                        <el-input-number type="number" v-model="formData.assetId"></el-input-number>
+                    </el-form-item> -->
+                    <el-form-item prop="contactName" label="收货人">
+                        <el-input v-model="formData.contactName"></el-input>
+                    </el-form-item>
+                    <el-form-item prop="contactPhone" label="收货电话">
+                        <el-input v-model="formData.contactPhone"></el-input>
+                    </el-form-item>
+                    <el-form-item prop="address" label="收货地址">
+                        <el-input v-model="formData.address"></el-input>
+                    </el-form-item>
+                    <el-form-item prop="courier" label="快递">
+                        <el-input v-model="formData.courier"></el-input>
+                    </el-form-item>
+                    <el-form-item prop="courierId" label="快递单号">
+                        <el-input v-model="formData.courierId"></el-input>
+                    </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>
@@ -45,78 +47,80 @@
     </div>
 </template>
 <script>
-    export default {
-        name: 'AssetPostEdit',
-        created() {
-            if (this.$route.query.id) {
-                this.$http
-                    .get('assetPost/get/' + this.$route.query.id)
-                    .then(res => {
-                        this.formData = res;
-                    })
-                    .catch(e => {
-                        console.log(e);
-                        this.$message.error(e.error);
-                    });
-            }
-        },
-        data() {
-            return {
-                saving: false,
-                formData: {
-                },
-                rules: {
-                    contactPhone: [
-                        {
-                            pattern: /^1[3-9]\d{9}$/,
-                            message: "请输入正确的手机号",
-                            trigger: "blur"
-                        },
-                    ],
-                },
+export default {
+    name: 'AssetPostEdit',
+    created() {
+        if (this.$route.query.id) {
+            this.$http
+                .get('assetPost/get/' + this.$route.query.id)
+                .then(res => {
+                    this.formData = res;
+                })
+                .catch(e => {
+                    console.log(e);
+                    this.$message.error(e.error);
+                });
+        }
+    },
+    data() {
+        return {
+            saving: false,
+            formData: {},
+            rules: {
+                contactPhone: [
+                    {
+                        pattern: /^1[3-9]\d{9}$/,
+                        message: '请输入正确的手机号',
+                        trigger: 'blur'
+                    }
+                ]
             }
+        };
+    },
+    methods: {
+        onSave() {
+            this.$refs.form.validate(valid => {
+                if (valid) {
+                    this.submit();
+                } else {
+                    return false;
+                }
+            });
         },
-        methods: {
-            onSave() {
-                this.$refs.form.validate((valid) => {
-                    if (valid) {
-                        this.submit();
-                    } else {
-                        return false;
-                    }
-                });
-            },
-            submit() {
-                let data = {...this.formData};
+        submit() {
+            let data = { ...this.formData };
 
-                this.saving = true;
-                this.$http
-                    .post('/assetPost/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(`/assetPost/del/${this.formData.id}`)
-                }).then(() => {
+            this.saving = true;
+            this.$http
+                .post('/assetPost/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(`/assetPost/del/${this.formData.id}`);
+                })
+                .then(() => {
                     this.$message.success('删除成功');
                     this.$router.go(-1);
-                }).catch(e => {
+                })
+                .catch(e => {
                     if (e !== 'cancel') {
                         console.log(e);
                         this.$message.error((e || {}).error || '删除失败');
                     }
-                })
-            },
+                });
         }
     }
+};
 </script>
 <style lang="less" scoped></style>

+ 142 - 128
src/main/vue/src/views/AssetPostList.vue

@@ -1,58 +1,68 @@
 <template>
-    <div  class="list-view">
+    <div class="list-view">
         <page-title>
-            <el-button @click="addRow" type="primary" icon="el-icon-plus" :disabled="fetchingData || downloading" class="filter-item">
+            <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
+                @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"
+                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
+            :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="assetId" label="资产ID"> </el-table-column> -->
+             <el-table-column prop="name" label="名称" show-overflow-tooltip> </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="assetId" label="资产ID"
->
-                    </el-table-column>
-                    <el-table-column prop="contactName" label="收货人"
->
-                    </el-table-column>
-                    <el-table-column prop="contactPhone" label="收货电话"
->
-                    </el-table-column>
-                    <el-table-column prop="address" label="收货地址"
->
-                    </el-table-column>
-                    <el-table-column prop="courier" label="快递"
->
-                    </el-table-column>
-                    <el-table-column prop="courierId" label="快递单号"
->
-                    </el-table-column>
-            <el-table-column
-                    label="操作"
-                    align="center"
-                    fixed="right"
-                    width="150">
-                <template slot-scope="{row}">
+            <el-table-column prop="contactName" label="收货人"> </el-table-column>
+            <el-table-column prop="contactPhone" label="收货电话"> </el-table-column>
+            <el-table-column prop="address" label="收货地址"> </el-table-column>
+            <el-table-column prop="courier" label="快递"> </el-table-column>
+            <el-table-column prop="courierId" label="快递单号"> </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>
@@ -67,112 +77,116 @@
                     <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
+                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 { mapState } from 'vuex';
+import pageableTable from '@/mixins/pageableTable';
 
-    export default {
-        name: 'AssetPostList',
-        mixins: [pageableTable],
-        data() {
-            return {
-                multipleMode: false,
-                search: "",
-                url: "/assetPost/all",
-                downloading: false,
-            }
+export default {
+    name: 'AssetPostList',
+    mixins: [pageableTable],
+    data() {
+        return {
+            multipleMode: false,
+            search: '',
+            url: '/assetPost/all',
+            downloading: false
+        };
+    },
+    computed: {
+        selection() {
+            return this.$refs.table.selection.map(i => i.id);
+        }
+    },
+    methods: {
+        beforeGetData() {
+            return { search: this.search, query: { del: false } };
         },
-        computed: {
-            selection() {
-                return this.$refs.table.selection.map(i => i.id);
+        toggleMultipleMode(multipleMode) {
+            this.multipleMode = multipleMode;
+            if (!multipleMode) {
+                this.$refs.table.clearSelection();
             }
         },
-        methods: {
-            beforeGetData() {
-                return { search: this.search, query: { del: false } };
-            },
-            toggleMultipleMode(multipleMode) {
-                this.multipleMode = multipleMode;
-                if (!multipleMode) {
-                    this.$refs.table.clearSelection();
+        addRow() {
+            this.$router.push({
+                path: '/assetPostEdit',
+                query: {
+                    ...this.$route.query
                 }
-            },
-            addRow() {
-                this.$router.push({
-                    path: "/assetPostEdit",
-                    query: {
-                        ...this.$route.query
-                    }
-                });
-            },
-            editRow(row) {
-                this.$router.push({
-                    path: "/assetPostEdit",
-                    query: {
+            });
+        },
+        editRow(row) {
+            this.$router.push({
+                path: '/assetPostEdit',
+                query: {
                     id: row.id
-                    }
-                });
-            },
-            download() {
-                this.downloading = true;
-                this.$axios
-                    .get("/assetPost/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
+                }
+            });
+        },
+        download() {
+            this.downloading = true;
+            this.$axios
+                .get('/assetPost/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);
                 });
-            },
-            operation2() {
-                this.$message('操作2');
-            },
-            deleteRow(row) {
-                this.$alert('删除将无法恢复,确认要删除么?', '警告', {type: 'error'}).then(() => {
-                    return this.$http.post(`/assetPost/del/${row.id}`)
-                }).then(() => {
+        },
+        operation1() {
+            this.$notify({
+                title: '提示',
+                message: this.selection
+            });
+        },
+        operation2() {
+            this.$message('操作2');
+        },
+        deleteRow(row) {
+            this.$alert('删除将无法恢复,确认要删除么?', '警告', { type: 'error' })
+                .then(() => {
+                    return this.$http.post(`/assetPost/del/${row.id}`);
+                })
+                .then(() => {
                     this.$message.success('删除成功');
                     this.getData();
-                }).catch(e => {
+                })
+                .catch(e => {
                     if (e !== 'cancel') {
                         this.$message.error(e.error);
                     }
-                })
-            },
+                });
         }
     }
+};
 </script>
 <style lang="less" scoped>
 </style>

+ 15 - 41
src/main/vue/src/views/BlindBoxEdit.vue

@@ -174,8 +174,14 @@
                 ref="addItemForm"
                 :rules="addItemFormRules"
             >
+                <el-form-item label="类型">
+                    <el-select v-model="type" @change="changeCollections" style="width: 300px">
+                        <el-option label="默认" value="DEFAULT"> </el-option>
+                        <el-option label="衍生品" value="DERIVATIVE"> </el-option>
+                    </el-select>
+                </el-form-item>
                 <el-form-item prop="collectionId" label="作品">
-                    <el-select v-model="addItemForm.collectionId">
+                    <el-select v-model="addItemForm.collectionId" style="width: 300px">
                         <el-option v-for="item in collections" :label="item.name" :value="item.id" :key="item.id">
                             <span style="float: left">{{ item.name }}</span>
                             <span style="float: right; color: #8492a6; font-size: 13px">#{{ item.id }}</span>
@@ -225,7 +231,7 @@ export default {
             .post(
                 '/collection/all',
                 {
-                    query: { del: false, source: 'OFFICIAL', projectId: 2 },
+                    query: { del: false, source: 'OFFICIAL', type: ['DEFAULT', 'DERIVATIVE'], projectId: 2 },
                     size: 10000,
                     sort: 'sort,desc;createdAt,desc'
                 },
@@ -233,45 +239,8 @@ export default {
             )
             .then(res => {
                 this.collections = res.content;
+                this.addCollections = res.content;
             });
-
-        // this.formData = {
-        //     name: 'OASISPUNK绿洲朋克',
-        //     pic: ['https://awesomeadmin.oss-cn-hangzhou.aliyuncs.com/image/2021-10-21-16-44-52kZqxuwhH.gif'],
-        //     minter: '管理员',
-        //     minterId: 1,
-        //     minterAvatar: 'https://awesomeadmin.oss-cn-hangzhou.aliyuncs.com/image/avatar_male.png',
-        //     detail:
-        //         '<div class="content-item" data-v-38285332="">\n<div data-v-38285332="">RAEX绿洲数字藏品中心首次携手星火爱心公益基金及火链Labs,联合发行公益型数字藏品: OASISPUNK绿洲朋克。OASISPUNK绿洲朋克是完全使用算法合成的加密人物头像,仅铸造发行3100枚,每一枚全都不同,更有稀缺度之分。绿洲朋克共分为3种类型:初代目(1500枚),次代目(1500枚),旗帜版(100枚)。每售出一枚绿洲朋克,收益所得将捐赠一定比例给&ldquo;星火爱心公益基金&rdquo;,用于扶贫助困类公益项目,同时买家将收到由星火爱心公益基金颁发的捐款证明,获得投身公益事业的荣誉感。欢迎你来到绿洲元宇宙,共建绿洲生态,共享绿洲文明荣耀。</div>\n</div>',
-        //     type: 'BLIND_BOX',
-        //     source: 'OFFICIAL',
-        //     sale: 0,
-        //     stock: 0,
-        //     total: 23,
-        //     likes: 0,
-        //     onShelf: true,
-        //     salable: true,
-        //     price: 0.01,
-        //     properties: [],
-        //     canResale: false,
-        //     royalties: 0,
-        //     serviceCharge: 0,
-        // };
-        // this.blindBoxItems = [
-        //     {
-        //         collectionId: 212,
-        //         total: 3,
-        //         rare: true
-        //     },
-        //     {
-        //         collectionId: 207,
-        //         total: 6
-        //     },
-        //     {
-        //         collectionId: 201,
-        //         total: 14
-        //     }
-        // ];
     },
     computed: {
         canEdit() {
@@ -454,7 +423,9 @@ export default {
                 id: [{ required: true, message: '请选择作品' }],
                 total: [{ required: true, message: '请输入数量' }]
             },
-            cateogories: ['勋章', '收藏品', '数字艺术', '门票', '游戏', '音乐', '使用', '其他']
+            cateogories: ['勋章', '收藏品', '数字艺术', '门票', '游戏', '音乐', '使用', '其他'],
+            type: '',
+            addCollections: []
         };
     },
     methods: {
@@ -548,6 +519,9 @@ export default {
         },
         removeItem(i) {
             this.blindBoxItems.splice(i, 1);
+        },
+        changeCollections(val) {
+            this.collections = this.addCollections.filter(i => i.type === val);
         }
     },
     watch: {

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

@@ -178,7 +178,7 @@ export default {
         beforeGetData() {
             return {
                 search: this.search,
-                query: { del: false, source: 'OFFICIAL', createdAt: this.createdAt, minterId: this.minterId }
+                query: { del: false, source: 'OFFICIAL', type: 'DEFAULT', createdAt: this.createdAt, minterId: this.minterId }
             };
         },
         toggleMultipleMode(multipleMode) {

+ 607 - 0
src/main/vue/src/views/DerivativeEdit.vue

@@ -0,0 +1,607 @@
+<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="94px"
+                    label-position="right"
+                    size="small"
+                    style="max-width: 500px"
+                >
+                    <!-- <el-form-item label="项目" prop="projectId">
+                        <el-select v-model="formData.projectId">
+                            <el-option :value="0" label="第九空间"></el-option>
+                            <el-option :value="1" label="cosmos art"></el-option>
+                            <el-option :value="2" label="modern point"></el-option>
+                        </el-select>
+                    </el-form-item> -->
+                    <el-form-item prop="name" label="名称">
+                        <el-input v-model="formData.name" :disabled="!canEdit"></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 prop="minterId" label="艺术家">
+                        <minter-select
+                            v-model="formData.minterId"
+                            @detail="onMinterDetail"
+                            :disabled="!canEdit"
+                            :projectId="formData.projectId"
+                        ></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>
+                    <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="{ row, $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" :disabled="!canEdit"> 添加特性 </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>
+                    <!-- <el-form-item prop="type" label="类型">
+                        <el-select v-model="formData.type" clearable filterable placeholder="请选择">
+                            <el-option
+                                v-for="item in typeOptions"
+                                :key="item.value"
+                                :label="item.label"
+                                :value="item.value"
+                            >
+                            </el-option>
+                        </el-select>
+                    </el-form-item>
+                    <el-form-item prop="source" label="来源">
+                        <el-select v-model="formData.source" clearable filterable placeholder="请选择">
+                            <el-option
+                                v-for="item in sourceOptions"
+                                :key="item.value"
+                                :label="item.label"
+                                :value="item.value"
+                            >
+                            </el-option>
+                        </el-select>
+                    </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="likes" label="点赞">
+                        <el-input-number v-model="formData.likes"></el-input-number>
+                    </el-form-item> -->
+                    <el-form-item prop="startTime" label="定时发布">
+                        <el-switch v-model="formData.scheduleSale" active-text="是" inactive-text="否"></el-switch>
+                        <div style="margin-top: 10px" v-if="formData.scheduleSale">
+                            <el-date-picker
+                                v-model="formData.startTime"
+                                type="datetime"
+                                value-format="yyyy-MM-dd HH:mm:ss"
+                                placeholder="发布时间"
+                            ></el-date-picker>
+                        </div>
+                    </el-form-item>
+                    <el-form-item prop="onShelf" label="上架" v-if="!formData.scheduleSale">
+                        <el-switch v-model="formData.onShelf" active-text="上架" inactive-text="下架"></el-switch>
+                    </el-form-item>
+                    <el-form-item prop="salable" label="可售">
+                        <el-switch v-model="formData.salable" active-text="可销售" inactive-text="仅展示"></el-switch>
+                    </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>
+                    <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>
+
+                <div v-if="privilegeForm.type === 'anywhere'">
+                    <el-form-item label="类型">
+                        <el-select v-model="privilegeForm.anywhereType">
+                            <el-option
+                                v-for="item in privilegeTypeOptions"
+                                :key="item.value"
+                                :label="item.label"
+                                :value="item.value"
+                            ></el-option>
+                        </el-select>
+                    </el-form-item>
+                    <el-form-item prop="detail" label="详细内容" v-if="privilegeForm.anywhereType === 'text'">
+                        <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.anywhereType === 'image'">
+                        <single-upload v-model="privilegeForm.detail"></single-upload>
+                    </el-form-item>
+                    <el-form-item prop="detail" label="视频" v-if="privilegeForm.anywhereType === 'video'">
+                        <video-upload-url v-model="privilegeForm.detail"></video-upload-url>
+                    </el-form-item>
+                    <el-form-item prop="detail" label="视频" v-if="privilegeForm.anywhereType === 'audio'">
+                        <file-upload :limit="1" v-model="privilegeForm.detail"></file-upload>
+                    </el-form-item>
+                </div>
+            </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 FileUpload from '../components/FileUpload.vue';
+export default {
+    name: 'CollectionEdit',
+    components: { ModelUpload, FileUpload },
+    created() {
+        Promise.all([
+            new Promise((resolve, reject) => {
+                if (this.$route.query.id) {
+                    return this.$http
+                        .get('collection/get/' + this.$route.query.id)
+                        .then(res => {
+                            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(() => {
+            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: {
+                projectId: this.$store.state.projectId,
+                onShelf: true,
+                salable: true,
+                properties: [],
+                type: 'DERIVATIVE',
+                source: 'OFFICIAL',
+                pic: [],
+                scheduleSale: false,
+                sort: 0,
+                privileges: []
+            },
+            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: '请填手续费' }],
+                projectId: [{ required: true, message: '请选择项目' }]
+            },
+            typeOptions: [
+                { label: '默认', value: 'DEFAULT' },
+                { label: '盲盒', value: 'BLIND_BOX' },
+                { label: '拍卖', value: 'AUCTION' }
+            ],
+            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'),
+            privilegeType: '',
+            privilegeTypeOptions: [
+                { label: '文本', value: 'text' },
+                { label: '视频', value: 'video' },
+                { label: '音频', value: 'audio' },
+                { label: '图片', value: 'image' }
+            ]
+        };
+    },
+    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(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);
+                });
+        }
+    }
+};
+</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;
+        width: 250px;
+    }
+}
+</style>

+ 259 - 0
src/main/vue/src/views/DerivativeList.vue

@@ -0,0 +1,259 @@
+<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">
+            <created-at-picker v-model="createdAt" @input="getData" name="创建"></created-at-picker>
+            <minter-filter v-model="minterId" @input="getData" :projectId="$store.state.projectId"></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="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';
+
+export default {
+    components: { SortableHeader },
+    name: 'DerivativeList',
+    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: 'DERIVATIVE' }
+            ],
+            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: 'DERIVATIVE', createdAt: this.createdAt, minterId: this.minterId }
+            };
+        },
+        toggleMultipleMode(multipleMode) {
+            this.multipleMode = multipleMode;
+            if (!multipleMode) {
+                this.$refs.table.clearSelection();
+            }
+        },
+        addRow() {
+            this.$router.push({
+                path: '/derivativeEdit',
+                query: {
+                    ...this.$route.query
+                }
+            });
+        },
+        editRow(row) {
+            this.$router.push({
+                path: '/derivativeEdit',
+                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>