licailing 3 лет назад
Родитель
Сommit
2b3dff24a2

+ 6 - 0
src/main/java/com/izouma/nineth/domain/Collection.java

@@ -188,4 +188,10 @@ public class Collection extends BaseEntity {
 
     @ApiModelProperty("开启抢白名单")
     private Boolean openQuota;
+
+    @ApiModelProperty("展厅背景")
+    private String showroomBg;
+
+    @ApiModelProperty("最多可放藏品数量")
+    private Integer maxCollection;
 }

+ 49 - 29
src/main/vue/src/router.js

@@ -444,80 +444,100 @@ const router = new Router({
                     name: 'MintActivityEdit',
                     component: () => import(/* webpackChunkName: "mintActivityEdit" */ '@/views/MintActivityEdit.vue'),
                     meta: {
-                       title: '铸造活动编辑',
-                    },
+                        title: '铸造活动编辑'
+                    }
                 },
                 {
                     path: '/mintActivityList',
                     name: 'MintActivityList',
                     component: () => import(/* webpackChunkName: "mintActivityList" */ '@/views/MintActivityList.vue'),
                     meta: {
-                       title: '铸造活动',
-                    },
-               },
+                        title: '铸造活动'
+                    }
+                },
                 {
                     path: '/mintOrderEdit',
                     name: 'MintOrderEdit',
                     component: () => import(/* webpackChunkName: "mintOrderEdit" */ '@/views/MintOrderEdit.vue'),
                     meta: {
-                       title: '铸造订单编辑',
-                    },
+                        title: '铸造订单编辑'
+                    }
                 },
                 {
                     path: '/mintOrderList',
                     name: 'MintOrderList',
                     component: () => import(/* webpackChunkName: "mintOrderList" */ '@/views/MintOrderList.vue'),
                     meta: {
-                       title: '铸造订单',
-                    },
-               },
+                        title: '铸造订单'
+                    }
+                },
                 {
                     path: '/purchaseLevelEdit',
                     name: 'PurchaseLevelEdit',
-                    component: () => import(/* webpackChunkName: "purchaseLevelEdit" */ '@/views/PurchaseLevelEdit.vue'),
+                    component: () =>
+                        import(/* webpackChunkName: "purchaseLevelEdit" */ '@/views/PurchaseLevelEdit.vue'),
                     meta: {
-                       title: '会员等级编辑',
-                    },
+                        title: '会员等级编辑'
+                    }
                 },
                 {
                     path: '/purchaseLevelList',
                     name: 'PurchaseLevelList',
-                    component: () => import(/* webpackChunkName: "purchaseLevelList" */ '@/views/PurchaseLevelList.vue'),
+                    component: () =>
+                        import(/* webpackChunkName: "purchaseLevelList" */ '@/views/PurchaseLevelList.vue'),
                     meta: {
-                       title: '会员等级',
-                    },
-               },
+                        title: '会员等级'
+                    }
+                },
                 {
                     path: '/newsEdit',
                     name: 'NewsEdit',
                     component: () => import(/* webpackChunkName: "newsEdit" */ '@/views/NewsEdit.vue'),
                     meta: {
-                       title: '新闻管理编辑',
-                    },
+                        title: '新闻管理编辑'
+                    }
                 },
                 {
                     path: '/newsList',
                     name: 'NewsList',
                     component: () => import(/* webpackChunkName: "newsList" */ '@/views/NewsList.vue'),
                     meta: {
-                       title: '新闻管理',
-                    },
-               },
+                        title: '新闻管理'
+                    }
+                },
                 {
                     path: '/adapayMerchantEdit',
                     name: 'AdapayMerchantEdit',
-                    component: () => import(/* webpackChunkName: "adapayMerchantEdit" */ '@/views/AdapayMerchantEdit.vue'),
+                    component: () =>
+                        import(/* webpackChunkName: "adapayMerchantEdit" */ '@/views/AdapayMerchantEdit.vue'),
                     meta: {
-                        title: 'ada编辑',
-                    },
+                        title: 'ada编辑'
+                    }
                 },
                 {
                     path: '/adapayMerchantList',
                     name: 'AdapayMerchantList',
-                    component: () => import(/* webpackChunkName: "adapayMerchantList" */ '@/views/AdapayMerchantList.vue'),
+                    component: () =>
+                        import(/* webpackChunkName: "adapayMerchantList" */ '@/views/AdapayMerchantList.vue'),
                     meta: {
-                        title: 'ada',
-                    },
+                        title: 'ada'
+                    }
+                },
+                {
+                    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: '展厅'
+                    }
                 }
                 /**INSERT_LOCATION**/
             ]
@@ -559,7 +579,7 @@ router.beforeEach((to, from, next) => {
         window.open(url);
         return;
     }
-    if (!store.state.userInfo && to.path !== '/login' && to.path!=='/photoProcessing') {
+    if (!store.state.userInfo && to.path !== '/login' && to.path !== '/photoProcessing') {
         http.axios
             .get('/user/my')
             .then(res => {

+ 779 - 0
src/main/vue/src/views/ShowroomEdit.vue

@@ -0,0 +1,779 @@
+<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="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/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;
+                            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/ShowroomList.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: '/showroomEdit',
+                query: {
+                    ...this.$route.query
+                }
+            });
+        },
+        editRow(row) {
+            this.$router.push({
+                path: 'showroomEdit',
+                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>