Browse Source

付费产品配置模块

chenlei 6 tháng trước cách đây
mục cha
commit
aaf2287e1e

+ 7 - 0
src/api/customer/user.js

@@ -53,4 +53,11 @@ export default {
     getOfficialList:params=>{
         return get('/user/official/list',params)
     },
+    /**
+     * @param {Number} params.UserId
+     * @returns 
+     */
+    getUserDetail:params=>{
+        return get('/user/detail',params)
+    },
 };

+ 80 - 0
src/api/products/products.js

@@ -3,6 +3,22 @@ import { get, post } from "@/api/index";
 export default {
     /**
      * 单品列表 
+     * @param {String} params.PageSize
+     * @param {String} params.CurrentIndex
+     * @param {String} params.ProductType 产品类型
+     * @param {String} params.KeyWord 搜索关键字
+     * @param {String} params.SortType 排序类型
+     * @param {String} params.SaleStatus 状态
+     * @param {String} params.IsSingle 是否是单品产品
+     * @param {String} params.UpdatedTime 更新时间
+     * @param {String} params.CreatedTime 创建时间
+     * @returns 
+     */
+    getproductList:params=>{
+        return get('/product/productList',params)
+    },
+    /**
+     * 获取未设置的产品列表
      * @param {Object} params
      * @param {String} params.PageSize
      * @param {String} params.CurrentIndex
@@ -25,4 +41,68 @@ export default {
     postAddProduct:params=>{
         return post('/product/addProduct',params)
     },
+    /**
+     * 上下架产品
+     * @param {String} params.SaleStatus 状态
+     * @param {String} params.ProductId 产品id
+     * @returns 
+     */
+    postUpdateSaleStatus:params=>{
+        return post('/product/updateSaleStatus',params)
+    },
+    /**
+     * 删除产品
+     * @param {String} params.ProductId 产品id
+     * @returns 
+     */
+    postDeleteProduct:params=>{
+        return post('/product/deleteProduct',params)
+    },
+    /**
+     * 增加产品 
+     * @param {String} params.SourceId
+     * @param {String} params.Type
+     * @param {String} params.ProductName 类型
+     * @param {String} params.CoverSrc 品种id
+     * @param {String} params.ValidDays 搜索关键字
+     * @param {String} params.Price 搜索关键字
+     * @param {String} params.Description 搜索关键字
+     * @returns 
+     */
+    postAddProduct:params=>{
+        return post('/product/addProduct',params)
+    },
+    /**
+     * 编辑产品 
+     * @param {String} params.SourceId
+     * @param {String} params.Type
+     * @param {String} params.ProductName 类型
+     * @param {String} params.CoverSrc 品种id
+     * @param {String} params.ValidDays 搜索关键字
+     * @param {String} params.Price 搜索关键字
+     * @param {String} params.Description 搜索关键字
+     * @returns 
+     */
+    postEditProduct:params=>{
+        return post('/product/editProduct',params)
+    },
+    /**
+     * 上传图片
+     * @param {Object} params
+     * @param {String} params.file 文件
+     * @returns 
+     * */
+    postUploadFile:params=>{
+        return post('/product/uploadFile',params)
+    },
+    /**
+     * 获取产品最新风险等级 
+     * @param {Object} params
+     * @param {String} params.SourceId 品种id
+     * @param {String} params.ProductType 产品类型
+     * @returns 
+     * */
+    getRiskLevel:params=>{
+        return get('/product/productRisk',params)
+    }
 };

+ 1 - 1
src/router/modules/products.js

@@ -19,7 +19,7 @@ export default[
       },
       {
         path:'addSingle',
-        component:()=>import('@/views/products/components/addSingleList.vue'),
+        component:()=>import('@/views/products/addSingleList.vue'),
         name:'addSingle',
         meta:{
             title:'添加单品'

+ 1 - 1
src/views/customer/TempUserList.vue

@@ -131,7 +131,7 @@ function Details(row) {
                 />
             </div>
         </div>
-        <ReadDialog :show="show" :labelOptions="labelOptions" :userId="userId"></ReadDialog>
+        <ReadDialog v-model:show="show" :labelOptions="labelOptions" :userId="userId"></ReadDialog>
     </el-card>
 </template>
 

+ 18 - 8
src/views/customer/UserList.vue

@@ -4,11 +4,12 @@ import { Search } from '@element-plus/icons-vue'
 import {apiCustomerUser} from '@/api/customer'
 import { apiMediaCommon } from '@/api/media'
 import ReadDialog from './components/ReadDialog.vue'
+import UserDialog from './components/UserDialog.vue'
 
 const tableColumns = [
     {
         label:'姓名',
-        key:'Mobile',
+        key:'RealName',
         sortable:false
     },
     {
@@ -23,11 +24,11 @@ const tableColumns = [
     },
     {
         label:'注册时间',
-        key:'LastReadTime',
+        key:'CreatedTime',
         sortable:false
     },{
         label:'是否关注公众号',
-        key:'AccountStatus',
+        key:'FollowingGzh',
         sortable:false,
     },{
         label:'最近一次阅读时间',
@@ -47,13 +48,14 @@ const tableQuery = reactive({
     totals:0,
     sortParam:'',
     sortType:'',
-    FollowingGzh:true,
+    FollowingGzh:'',
     RegisterBeginDate:'',
     RegisterEndDate:'',
 })
 
 const tableData = ref([])
 const show = ref(false)
+const showUserDialog = ref(false)
 function getTableData(){
     apiCustomerUser.getOfficialList({
         Keyword:tableQuery.keyWord,
@@ -103,17 +105,19 @@ function handleSortChange({order,prop}){
 function Details(row) {
     if (row.ReadCount <= 0) return;
     show.value = true;
-    userId.value = row.Id + '';
+    userId.value = row.TemplateUserId + '';
 }
 function handleSelectChange() {
     getTableData();
 }
 function changeDatePicker(val) {
-    console.log(value1.value);
     tableQuery.RegisterBeginDate = val[0];
     tableQuery.RegisterEndDate = val[1];
     getTableData();
 }
+function userDetails(row) {
+    showUserDialog.value = true;
+}
 </script>
 
 <template>
@@ -121,7 +125,7 @@ function changeDatePicker(val) {
         <div class="temp-user-list-wrap">
             <div class="top-box">
                 <div>
-                    <el-select v-model="tableQuery.FollowingGzh" @change="handleSelectChange()" placeholder="是否关注公众号" style="width: 150px; margin-right: 20px;">
+                    <el-select v-model="tableQuery.FollowingGzh" clearable @change="handleSelectChange()" placeholder="是否关注公众号" style="width: 150px; margin-right: 20px;">
                         <el-option label="是" :value="true"></el-option>
                         <el-option label="否" :value="false"></el-option>
                     </el-select>
@@ -159,6 +163,11 @@ function changeDatePicker(val) {
                                 {{ scope.row[column.key] }}
                             </span>
                         </template>
+                        <template #default="scope" v-else-if="column.key === 'RealName'">
+                            <span class="ReadCount" @click="userDetails(scope.row)">
+                                {{ scope.row[column.key] }}
+                            </span>
+                        </template>
                         <template #default="scope" v-else>
                             {{scope.row[column.key]}}
                         </template>
@@ -175,7 +184,8 @@ function changeDatePicker(val) {
                 />
             </div>
         </div>
-        <ReadDialog :show="show" :labelOptions="labelOptions" :userId="userId"></ReadDialog>
+        <ReadDialog v-model:show="show" :labelOptions="labelOptions" :userId="userId"></ReadDialog>
+        <UserDialog v-model:show="showUserDialog" :userId="userId"></UserDialog>
     </el-card>
 </template>
 

+ 3 - 3
src/views/customer/components/ReadDialog.vue

@@ -37,11 +37,11 @@ import {apiCustomerUser} from '@/api/customer'
 const tableColumns = [
     {
         label:'标题',
-        key:'SourceName',
+        key:'',
         sortable:false
     },{
         label:'产品类型',
-        key:'SourceId',
+        key:'SourceName',
         sortable:false
     },{
         label:'品种',
@@ -114,7 +114,7 @@ function handleSelectChange(){
   >
     <div class="dialog-content">
         <div class="dialog-content-top">
-            <el-select v-model="tableQuery.ProductType"  @change="handleSelectChange()" placeholder="请选择" style="width: 250px; margin-right: 20px;">
+            <el-select v-model="tableQuery.ProductType" clearable @change="handleSelectChange()" placeholder="请选择" style="width: 250px; margin-right: 20px;">
                 <el-option
                 v-for="item in options"
                 :key="item.value"

+ 143 - 0
src/views/customer/components/UserDialog.vue

@@ -0,0 +1,143 @@
+<script setup>
+const show = defineModel('show', { type: Boolean, default: false })
+const props=defineProps({
+    userId:{
+        type:String,
+        default:''
+    },
+})
+const emits = defineEmits(["success"])
+emits("success")
+
+watch(() => props.show, (newval) => {
+  if (newval) {
+    getTableData()
+  }
+})
+const options = ref([
+    {
+        value: 'report',
+        label: '报告'
+    }, {
+        value: 'audio',
+        label: '音频'
+    }, {
+        value: 'video',
+        label: '视频'
+    }
+])
+
+import { ref, reactive } from 'vue'
+import {apiCustomerUser} from '@/api/customer'
+
+const tableColumns = [
+    {
+        label:'标题',
+        key:'SourceName',
+        sortable:false
+    },{
+        label:'产品类型',
+        key:'SourceId',
+        sortable:false
+    },{
+        label:'品种',
+        key:'PermissionNames',
+        sortable:false,
+    },{
+        label:'最近一次点击时间',
+        key:'ClickTime',
+        sortable:true
+    },{
+        label:'停留时长',
+        key:'ReadDurationMinutes',
+        sortable:true
+    }
+]
+
+const tableQuery = reactive({
+    sortParam:'',
+    sortType:'',
+})
+const tableData = ref([])
+function getTableData(){
+    apiCustomerUser.getUserDetail({
+        UserId:props.userId,
+    }).then(res=>{
+        if(res.Ret!==200) return
+        tableData.value = res.Data.List||[]
+        tableQuery.totals = res.Data.Paging && res.Data.Paging.Totals||0
+    })
+}
+// function handlePageChange(page){
+//     tableQuery.currentPage = page
+//     getTableData()
+// }
+function handleSortChange({order,prop}){
+    const propMap = {
+        0:'ClickTime',
+        1:'ReadDurationMinutes',
+    }
+    tableQuery.sortParam = propMap[prop]||2
+    tableQuery.sortType = order==='ascending'?1:0
+    getTableData()
+}
+
+function handleSelectChange(){
+    getTableData()
+}
+
+</script>
+
+<template>
+  <el-dialog
+    v-model="show"
+    :close-on-click-modal="false"
+    :modal-append-to-body="false"
+    width="60%"
+    draggable
+    title="阅读详情"
+  >
+    <div class="dialog-content">
+        <div class="dialog-content-top">
+        </div>
+        <div class="table">
+            <el-table :data="tableData" @sort-change="handleSortChange">
+                <el-table-column 
+                    v-for="column in tableColumns" :key="column.key"
+                    :prop="column.key" :label="column.label" :sortable="column.sortable">
+                    <template #default="scope">
+                        {{scope.row[column.key]}}
+                    </template>
+                </el-table-column>
+            </el-table>
+            <!-- <el-pagination
+                background
+                layout="total,prev,pager,next,jumper"
+                :current-page="tableQuery.currentPage"
+                :page-size="tableQuery.pageSize"
+                :total="tableQuery.totals"
+                @current-change="handlePageChange"
+                style=" justify-content: flex-end; margin-top: 10px;"
+            /> -->
+        </div>
+    </div>
+  </el-dialog>
+</template>
+
+<style scoped lang="scss">
+.dialog-content {
+    // padding: 10px 50px 50px 50px;
+    .dialog-content-top {
+        display: flex;
+        justify-content: flex-start;
+        padding-bottom: 30px;
+        :deep(.el-cascader-node__label) {
+            max-width: 111px;
+        }
+    }
+    .table {
+        padding-bottom: 30px;
+    }
+}
+
+</style>

+ 306 - 3
src/views/products/PackageConfigList.vue

@@ -1,12 +1,315 @@
 <script setup>
-import { ref } from "vue";
+import { ref, reactive } from 'vue'
+import { Search } from '@element-plus/icons-vue'
+import {apiProductsConfig} from '@/api/products'
+import { apiMediaCommon } from '@/api/media'
+import { useRouter } from 'vue-router'
+import { ElMessage, ElMessageBox  } from 'element-plus'
+import PackageDialog from './components/PackageDialog.vue'
+const router=useRouter()
+const tableColumns = [
+    {
+        label:'套餐名称',
+        key:'ProductName',
+        align: 'center',
+        minwidthsty:120
+    },
+    {
+        label:'套餐简介',
+        key:'Description',
+        align:'center',
+        minwidthsty:130,
+    },
+    {
+        label:'封面',
+        key:'CoverSrc',
+        align:'center',
+        widthsty:100,
+    },
+    {
+        label:'商品价格',
+        key:'Price',
+        align: 'center',
+        widthsty:100,
+    },
+    {
+        label:'风险等级',
+        key:'RiskLevel',
+        align: 'center',
+        widthsty:90,
+    },{
+        label:'有效时长(天)',
+        key:'ValidDays',
+        align: 'center',
+        widthsty:120,
+    },
+    {
+        label:'状态',
+        key:'SaleStatus',
+        align: 'center',
+        widthsty:90,
+    }
+    ,{
+        label:'创建人',
+        key:'LastReadTime',
+        align: 'center',
+        widthsty:100,
+    },{
+        label:'创建时间',
+        key:'PublishedTime',
+        sortable:true,
+        align: 'center'
+    },
+    {
+        label:'更新时间',
+        key:'UpdatedTime',
+        sortable:true,
+        align: 'center'
+    },
+    {
+        label:'操作',
+        key:'handle',
+        align: 'center'
+    }
+]
+const saleStatusList = ref([
+    {
+        value: 'onSale',
+        label: '已上架'
+    }, {
+        value: 'offSale',
+        label: '未上架'
+    }
+])
+const tableData = ref([])
+const labelOptions = ref([])
+const showModal = ref(false)
+const isAdd = ref(false)
+const productInfo = ref({})
+const tableQuery = reactive({
+    keyWord:'',
+    currentPage:1,
+    pageSize:10,
+    totals:0,
+    ProductType:'',
+    SaleStatus:null,
+    CreatedTime:'',
+    UpdatedTime:'',
+})
+
+function getTableData(){
+    apiProductsConfig.getproductList({
+        IsSingle:false, // 是否是单品
+        Keyword:tableQuery.keyWord,
+        CurrentIndex:tableQuery.currentPage,
+        PageSize:tableQuery.pageSize,
+        ProductType:tableQuery.ProductType,
+        CreatedTime:tableQuery.CreatedTime,
+        UpdatedTime:tableQuery.UpdatedTime,
+        SaleStatus:tableQuery.SaleStatus,
+    }).then(res=>{
+        if(res.Ret!==200) return 
+        tableData.value = res.Data.List||[]
+        tableQuery.totals = res.Data.Paging.Totals||0
+    })
+}
+
+getTableData()
+getLableList()
+
+function getLableList(){
+    apiMediaCommon.getPermissionList().then(res=>{
+        if(res.Ret!==200) return
+        labelOptions.value = res.Data.List||[]
+    })
+}
+function handlePageChange(page){
+    tableQuery.currentPage = page
+    getTableData()
+}
+function handleSortChange({order,prop}){
+    // ascending 
+    const propMap = {
+        0:'CreatedTime',
+        1:'ReadCount',
+        2:'LastReadTime',
+    }
+    tableQuery.sortParam = propMap[prop]||2
+    tableQuery.sortType = order==='ascending'?1:0
+    getTableData()
+}
+function handleSelectChange() {
+    getTableData();
+}
+
+// 操作
+function operation(handle, row) {
+    switch (handle) {
+        case 'add':
+            showModal.value = true
+            isAdd.value = true
+        break
+        case 'stock':
+            apiProductsConfig.postUpdateSaleStatus({
+                ProductId:row.Id,
+                SaleStatus:'onSale'
+            }).then(res=>{
+                if(res.Ret!==200) return
+                ElMessage.success('上架成功')
+                getTableData()
+            })
+        break;
+        case 'delist':
+            apiProductsConfig.postUpdateSaleStatus({
+                ProductId:row.Id,
+                SaleStatus:'offSale'
+            }).then(res=>{
+                if(res.Ret!==200) return
+                ElMessage.success('下架成功')
+                getTableData()
+            })
+        break;
+        case 'edit':
+            row.Price = row.Price.split('¥').join("")
+            productInfo.value = row
+            showModal.value = true
+            isAdd.value = false
+        break;
+        case 'delete':
+            ElMessageBox.confirm(
+                '删除产品后,该产品转为免费,不影响该产品的历史购买记录,确认删除吗?',
+                '提示',
+                {
+                confirmButtonText: '确认',
+                cancelButtonText: '取消',
+                type: 'warning',
+                }
+            )
+                .then(() => {
+                    apiProductsConfig.postDeleteProduct({
+                        ProductId:row.Id,
+                    }).then(res=>{
+                        if(res.Ret!==200) return
+                        ElMessage.success('删除成功')
+                        getTableData()
+                    })
+                })
+        break;
+        default:
+            return false;
+    }
+}
+function closeEdit() {
+    showModal.value = false
+    getTableData();
+}
 </script>
 
 <template>
-    <el-card class="box-card">
-        PackageConfigList
+    <el-card>
+        <div class="temp-user-list-wrap">
+            <div class="top-box">
+                <div>
+                    <el-button type="primary" style="margin-right: 20px;" @click="operation('add')">添加套餐</el-button>
+                    <el-select v-model="tableQuery.SaleStatus" clearable @change="handleSelectChange()" placeholder="状态" style="width: 200px; margin-right: 20px;">
+                        <el-option
+                        v-for="item in saleStatusList"
+                        :key="item.value"
+                        :label="item.label"
+                        :value="item.value">
+                        </el-option>
+                    </el-select>
+                    <el-date-picker
+                        v-model="tableQuery.CreatedTime"
+                        type="date"
+                        placeholder="创建时间"
+                        format="YYYY-MM-DD"
+                        style="margin-right: 20px;"
+                        value-format="YYYY-MM-DD"
+                        @change="handlePageChange(1)"
+                    />
+                    <el-date-picker
+                        v-model="tableQuery.UpdatedTime"
+                        type="date"
+                        placeholder="更新时间"
+                        format="YYYY-MM-DD"
+                        value-format="YYYY-MM-DD"
+                        @change="handlePageChange(1)"
+                    />
+                </div>
+                <div class="search-box">
+                    <el-input 
+                        v-model="tableQuery.keyWord"
+                        :prefix-icon="Search" clearable
+                        style="width:400px"
+                        placeholder="套餐名称" 
+                        @input="handlePageChange(1)"
+                        />
+                </div>
+            </div>
+            <div class="table-box">
+                <el-table border :data="tableData" @sort-change="handleSortChange">
+                    <el-table-column
+                        v-for="column in tableColumns" :key="column.key"
+                        :prop="column.key" :label="column.label" :sortable="column.sortable" :align="column.align" :width="column.widthsty"
+                        :min-width="column.minwidthsty">
+                        <template #default="scope" v-if="column.key === 'SaleStatus'">
+                            <el-tag :type="scope.row[column.key]=== '已上架' ?'success':'info'">{{ scope.row[column.key] }}</el-tag>
+                        </template>
+                        <template #default="scope" v-else-if="column.key === 'handle'">
+                            <span class="edit" v-if="scope.row.SaleStatus !== '已上架'" @click="operation('stock', scope.row)">上架</span>
+                            <span class="edit" v-if="scope.row.SaleStatus === '已上架'"  @click="operation('delist', scope.row)" >下架</span>
+                            <span class="edit" v-if="scope.row.SaleStatus !== '已上架'" @click="operation('edit', scope.row)">编辑</span>
+                            <span class="delete" v-if="scope.row.SaleStatus !== '已上架'" @click="operation('delete', scope.row)">删除</span>
+                        </template>
+                        <template #default="scope" v-else-if="column.key === 'CoverSrc'">
+                            <el-image 
+                                v-if="scope.row[column.key]"
+                                fit="cover"
+                                :src="scope.row[column.key]||''" 
+                                :preview-src-list="[scope.row[column.key]||'']" 
+                                style="display: inline-block;width:30px;height: 30px;" preview-teleported/>
+                            <span v-else style="display: inline-block;width:30px;height: 30px;line-height: 30px;">-</span> 
+                        </template>
+                        <template #default="scope" v-else>
+                            {{scope.row[column.key]}}
+                        </template>
+                    </el-table-column>
+                </el-table>
+                <el-pagination
+                    background
+                    layout="total,prev,pager,next,jumper"
+                    :current-page="tableQuery.currentPage"
+                    :page-size="tableQuery.pageSize"
+                    :total="tableQuery.totals"
+                    @current-change="handlePageChange"
+                    style=" justify-content: flex-end;margin-top: 50px;"
+                />
+            </div>
+        </div>
+        <PackageDialog v-model:show="showModal" :isAdd="isAdd" :productInfo="productInfo" :labelOptions="labelOptions" @close="closeEdit()"></PackageDialog>
     </el-card>
 </template>
 
 <style scoped lang="scss">
+.temp-user-list-wrap{
+    height: calc(100vh - 208px); //layout padding 30*2 headHeight 48
+    .top-box{
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        padding: 20px 0 40px 0;
+    }
+    .table-box{
+       .edit {
+            cursor:pointer;
+            color: rgba(8, 108, 224, 1);
+            margin-right: 10px;
+       }
+       .delete {
+            cursor:pointer;
+            color: rgba(240, 61, 57, 1);
+       }
+    }
+}
 </style>

+ 136 - 56
src/views/products/SingleConfigList.vue

@@ -4,57 +4,57 @@ import { Search } from '@element-plus/icons-vue'
 import {apiProductsConfig} from '@/api/products'
 import { apiMediaCommon } from '@/api/media'
 import { useRouter } from 'vue-router'
+import { ElMessage, ElMessageBox  } from 'element-plus'
+import editSingle from './components/editSingle.vue'
 const router=useRouter()
 const tableColumns = [
     {
         label:'产品名称',
-        key:'Mobile',
-        sortable:false,
-        align: 'center'
+        key:'ProductName',
+        align: 'center',
+        minwidthsty:250
     },
     {
         label:'产品类型',
-        key:'Mobile',
-        sortable:false,
-        align: 'center'
+        key:'ProductType',
+        align:'center',
+        widthsty:100,
     },
     {
         label:'商品价格',
-        key:'Mobile',
-        sortable:false,
-        align: 'center'
+        key:'Price',
+        align: 'center',
+        widthsty:120,
     },
     {
         label:'风险等级',
-        key:'LastReadTime',
-        sortable:false,
-        align: 'center'
+        key:'RiskLevel',
+        align: 'center',
+        widthsty:100,
     },{
         label:'状态',
-        key:'AccountStatus',
-        sortable:false,
-        align: 'center'
+        key:'SaleStatus',
+        align: 'center',
+        widthsty:120,
     },{
         label:'创建人',
         key:'LastReadTime',
-        sortable:false,
         align: 'center'
     },{
         label:'创建时间',
-        key:'ReadCount',
-        sortable:false,
+        key:'PublishedTime',
+        sortable:true,
         align: 'center'
     },
     {
         label:'更新时间',
-        key:'ReadCount',
-        sortable:false,
+        key:'UpdatedTime',
+        sortable:true,
         align: 'center'
     },
     {
         label:'操作',
-        key:'ReadCount',
-        sortable:false,
+        key:'handle',
         align: 'center'
     }
 ]
@@ -70,28 +70,40 @@ const options = ref([
         label: '视频'
     }
 ])
+const saleStatusList = ref([
+    {
+        value: 'onSale',
+        label: '已上架'
+    }, {
+        value: 'offSale',
+        label: '未上架'
+    }
+])
 const tableData = ref([])
 const labelOptions = ref([])
+const showModal = ref(false)
+const productInfo = ref({})
 const tableQuery = reactive({
     keyWord:'',
     currentPage:1,
     pageSize:10,
     totals:0,
-    ProductType:'report',
-    PermissionIds:null,
-    RegisterBeginDate:'',
-    RegisterEndDate:'',
+    ProductType:'',
+    SaleStatus:null,
+    CreatedTime:'',
+    UpdatedTime:'',
 })
 
 function getTableData(){
-    apiProductsConfig.getUnSetProductList({
+    apiProductsConfig.getproductList({
+        IsSingle:true, // 是否是单品
         Keyword:tableQuery.keyWord,
         CurrentIndex:tableQuery.currentPage,
         PageSize:tableQuery.pageSize,
         ProductType:tableQuery.ProductType,
-        RegisterBeginDate:tableQuery.RegisterBeginDate,
-        RegisterEndDate:tableQuery.RegisterEndDate,
-        PermissionIds:tableQuery.PermissionIds?.join(','),
+        CreatedTime:tableQuery.CreatedTime,
+        UpdatedTime:tableQuery.UpdatedTime,
+        SaleStatus:tableQuery.SaleStatus,
     }).then(res=>{
         if(res.Ret!==200) return 
         tableData.value = res.Data.List||[]
@@ -126,6 +138,66 @@ function handleSortChange({order,prop}){
 function handleSelectChange() {
     getTableData();
 }
+
+// 操作
+function operation(handle, row) {
+    switch (handle) {
+        case 'stock':
+            apiProductsConfig.postUpdateSaleStatus({
+                ProductId:row.Id,
+                SaleStatus:'onSale'
+            }).then(res=>{
+                if(res.Ret!==200) return
+                ElMessage.success('上架成功')
+                getTableData()
+            })
+            break;
+        case 'delist':
+            apiProductsConfig.postUpdateSaleStatus({
+                ProductId:row.Id,
+                SaleStatus:'offSale'
+            }).then(res=>{
+                if(res.Ret!==200) return
+                ElMessage.success('下架成功')
+                getTableData()
+            })
+            break;
+        case 'edit':
+            console.log(row);
+            row.Price = row.Price.split('¥').join("")
+            console.log(row);
+            
+            productInfo.value = row
+            showModal.value = true
+            break;
+        case 'delete':
+            ElMessageBox.confirm(
+                '删除产品后,该产品转为免费,不影响该产品的历史购买记录,确认删除吗?',
+                '提示',
+                {
+                confirmButtonText: '确认',
+                cancelButtonText: '取消',
+                type: 'warning',
+                }
+            )
+                .then(() => {
+                    apiProductsConfig.postDeleteProduct({
+                        ProductId:row.Id,
+                    }).then(res=>{
+                        if(res.Ret!==200) return
+                        ElMessage.success('删除成功')
+                        getTableData()
+                    })
+                })
+            break;
+        default:
+            return false;
+    }
+}
+function closeEdit() {
+    showModal.value = false
+    getTableData();
+}
 </script>
 
 <template>
@@ -134,7 +206,7 @@ function handleSelectChange() {
             <div class="top-box">
                 <div>
                     <el-button type="primary" style="margin-right: 20px;" @click="router.push('/products/addSingle')">添加单品</el-button>
-                    <el-select v-model="tableQuery.ProductType"  @change="handleSelectChange()" placeholder="请选择" style="width: 200px; margin-right: 20px;">
+                    <el-select clearable v-model="tableQuery.ProductType" @change="handleSelectChange()" placeholder="产品类型" style="width: 200px; margin-right: 20px;">
                         <el-option
                         v-for="item in options"
                         :key="item.value"
@@ -142,35 +214,30 @@ function handleSelectChange() {
                         :value="item.value">
                         </el-option>
                     </el-select>
-                    <el-cascader
-                        filterable
-                        :options="labelOptions"
-                        collapse-tags
-                        v-model="tableQuery.PermissionIds" 
-                        @change="handleSelectChange()"
-                        :props="{
-                            value:'id',
-                            label:'name',
-                            emitPath:false,
-                            multiple:true
-                        }"
-                        style=" margin-right: 20px"
-                        clearable>
-                    </el-cascader>
+                    <el-select v-model="tableQuery.SaleStatus" clearable @change="handleSelectChange()" placeholder="状态" style="width: 200px; margin-right: 20px;">
+                        <el-option
+                        v-for="item in saleStatusList"
+                        :key="item.value"
+                        :label="item.label"
+                        :value="item.value">
+                        </el-option>
+                    </el-select>
                     <el-date-picker
-                        v-model="tableQuery.RegisterBeginDate"
+                        v-model="tableQuery.CreatedTime"
                         type="date"
                         placeholder="创建时间"
                         format="YYYY-MM-DD"
                         style="margin-right: 20px;"
                         value-format="YYYY-MM-DD"
+                        @change="handlePageChange(1)"
                     />
                     <el-date-picker
-                        v-model="tableQuery.RegisterEndDate"
+                        v-model="tableQuery.UpdatedTime"
                         type="date"
                         placeholder="更新时间"
                         format="YYYY-MM-DD"
                         value-format="YYYY-MM-DD"
+                        @change="handlePageChange(1)"
                     />
                 </div>
                 <div class="search-box">
@@ -178,18 +245,25 @@ function handleSelectChange() {
                         v-model="tableQuery.keyWord"
                         :prefix-icon="Search" clearable
                         style="width:400px"
-                        placeholder="请输入手机号" 
+                        placeholder="产品名称" 
                         @input="handlePageChange(1)"
                         />
                 </div>
             </div>
             <div class="table-box">
                 <el-table border :data="tableData" @sort-change="handleSortChange">
-                    <el-table-column 
+                    <el-table-column
                         v-for="column in tableColumns" :key="column.key"
-                        :prop="column.key" :label="column.label" :sortable="column.sortable" :align="column.align">
-                        <template #default="scope" v-if="column.key === 'AccountStatus'">
-                            <el-tag :type="scope.row[column.key]=== 'Open' ?'success':'info'">{{scope.row[column.key]=== 'Open'?'已开户':'未开户' }}</el-tag>
+                        :prop="column.key" :label="column.label" :sortable="column.sortable" :align="column.align" :width="column.widthsty"
+                        :min-width="column.minwidthsty">
+                        <template #default="scope" v-if="column.key === 'SaleStatus'">
+                            <el-tag :type="scope.row[column.key]=== '已上架' ?'success':'info'">{{ scope.row[column.key] }}</el-tag>
+                        </template>
+                        <template #default="scope" v-else-if="column.key === 'handle'">
+                            <span class="edit" v-if="scope.row.SaleStatus !== '已上架'" @click="operation('stock', scope.row)">上架</span>
+                            <span class="edit" v-if="scope.row.SaleStatus === '已上架'"  @click="operation('delist', scope.row)" >下架</span>
+                            <span class="edit" v-if="scope.row.SaleStatus !== '已上架'" @click="operation('edit', scope.row)">编辑</span>
+                            <span class="delete" v-if="scope.row.SaleStatus !== '已上架'" @click="operation('delete', scope.row)">删除</span>
                         </template>
                         <template #default="scope" v-else>
                             {{scope.row[column.key]}}
@@ -207,6 +281,7 @@ function handleSelectChange() {
                 />
             </div>
         </div>
+        <editSingle v-model:show="showModal" :productInfo="productInfo" @close="closeEdit()"></editSingle>
     </el-card>
 </template>
 
@@ -220,9 +295,14 @@ function handleSelectChange() {
         padding: 20px 0 40px 0;
     }
     .table-box{
-       .ReadCount {
-        color: rgba(8, 108, 224, 1);
-        cursor: pointer;
+       .edit {
+            cursor:pointer;
+            color: rgba(8, 108, 224, 1);
+            margin-right: 10px;
+       }
+       .delete {
+            cursor:pointer;
+            color: rgba(240, 61, 57, 1);
        }
     }
 }

+ 18 - 5
src/views/products/components/addSingleList.vue → src/views/products/addSingleList.vue

@@ -38,7 +38,7 @@ const tableColumns = [
     },
     {
         label:'商品价格(元)',
-        key:'ReadDurationMinutes',
+        key:'Price',
         sortable:false,
         align:'center'
     },
@@ -87,7 +87,7 @@ function handlePageChange(page){
 function handleSortChange({order,prop}){
     const propMap = {
         0:'ClickTime',
-        1:'ReadDurationMinutes',
+        1:'Price',
     }
     tableQuery.sortParam = propMap[prop]||2
     tableQuery.sortType = order==='ascending'?1:0
@@ -100,7 +100,20 @@ function close() {
     router.push('/products/singleConfigList')
 }
 function stockProducts(row){
-    console.log(row);
+    if(!row.Price) return ElMessage.warning('请输入价格')
+    apiProductsConfig.postAddProduct({
+        SourceId:row.SourceId,
+        Type:row.ProductType === '报告'?'report':'audio',
+        // ProductName:,
+        // CoverSrc:,
+        // ValidDays:,
+        Price:row.Price,
+        // Description:,
+    }).then(res=>{
+        if(res.Ret!==200) return 
+        ElMessage.success('保存并上架成功')
+        getTableData()
+    })
 }
 </script>
 
@@ -110,7 +123,7 @@ function stockProducts(row){
             <div>
                 <el-radio-group v-model="tableQuery.ProductType" size="large" @change="handleSelectChange()">
                     <el-radio-button label="报告" value="report" />
-                    <el-radio-button label="音视频" value="audio" />
+                    <el-radio-button label="音视频" value="media" />
                 </el-radio-group>
             </div>
             <div class="top-box">
@@ -157,7 +170,7 @@ function stockProducts(row){
                         v-for="column in tableColumns" :key="column.key"
                         :prop="column.key" :label="column.label" :sortable="column.sortable" :align="column.align" :width="column.widthsty"
                         :min-width="column.minwidthsty || ''">
-                        <template #default="scope" v-if="column.key==='ReadDurationMinutes'">
+                        <template #default="scope" v-if="column.key==='Price'">
                             <el-input v-model="scope.row[column.key]" placeholder="请输入数字" />
                         </template>
                         <template #default="scope" v-else-if="column.key==='handle'">

+ 235 - 0
src/views/products/components/PackageDialog.vue

@@ -0,0 +1,235 @@
+<script setup>
+import {apiProductsConfig} from '@/api/products'
+const show = defineModel('show', { type: Boolean, default: false })
+const props = defineProps({
+  productInfo:{
+    type:Object,
+    default:{}
+  },
+  isAdd:{
+    type:Boolean,
+    default:false
+  },
+  labelOptions:{
+    type:Array,
+    default:[]
+  },
+})
+const emits = defineEmits(["close"])
+const formRef = ref(null)
+const packageData = reactive({
+    SourceId:'',
+    Type:'',
+    ProductName:'',
+    CoverSrc:'',
+    ValidDays:'',
+    Price:'',
+    Description:'',
+    RiskLevel:''
+})
+watch(show,(newval)=>{
+  if(newval){
+    if (!props.isAdd) {
+      formRef.value?.clearValidate()
+      packageData.SourceId = [packageData.SourceId] || []
+      Object.assign(packageData,props.productInfo)
+      console.log(packageData);
+      
+    }
+  }else{
+    Object.assign(packageData,{
+      SourceId:'',
+      Type:'',
+      ProductName:'',
+      CoverSrc:'',
+      ValidDays:null,
+      Price:'',
+      Description:'',
+      RiskLevel:''
+    })
+    formRef.value?.resetFields()
+    console.log(packageData);
+  }
+})
+
+function getRiskLevel() {
+  apiProductsConfig.getRiskLevel({
+    SourceId:packageData.SourceId[packageData.SourceId.length-1],
+    ProductType:'package'
+  }).then(res=>{
+    if(res.Ret!==200) return
+    console.log(res.Data);
+    packageData.RiskLevel = res.Data?.RiskLevel||''
+  })
+}
+
+
+
+
+async function handleSubmitForm() {
+  const packageParams = {
+    SourceId:Array.isArray(packageData.SourceId) ? packageData.SourceId[packageData.SourceId.length-1] : packageData.SourceId,
+    Type: 'package',
+    ProductName:packageData.ProductName,
+    CoverSrc:packageData.CoverSrc,
+    ValidDays:parseInt(packageData.ValidDays),
+    Price:packageData.Price,
+    Description:packageData.Description,
+    RiskLevel:packageData.RiskLevel || ''
+  }
+  const res = props.isAdd ? await apiProductsConfig.postAddProduct(packageParams) : await apiProductsConfig.postEditProduct(packageParams)
+  if(res.Ret!==200) return
+  ElMessage.success(res.Msg)
+  handleClose()
+}
+function handleClose() {
+  emits("close")
+}
+
+
+function handleUploadImg(file){
+    //图片大小和格式限制
+    const {size,type} = file.file
+    const sizeLimit = 500*1024
+    if(!['image/png','image/jpeg'].includes(type)){
+        ElMessage.warning('仅支持png、jpg格式的图片')
+        return
+    }
+    if(size>sizeLimit){
+        ElMessage.warning('套餐封面不能超过500kb')
+        return
+    }
+    let form = new FormData();
+    form.append('File',file.file);
+    apiProductsConfig.postUploadFile(form).then(res=>{
+        if(res.Ret!==200) return 
+        packageData.CoverSrc = res.Data?.Url||''
+    })
+}
+function handleValidValidDays() {
+  packageData.ValidDays=packageData.ValidDays.replace(/[^\.\d]/g,'').replace('.','').replace(/^0/,'');
+}
+function handleValidPrice() {
+  packageData.Price=packageData.Price.replace(/[^\.\d]/g,'').replace(/^00/,'');
+}
+</script>
+
+<template>
+  <el-dialog
+    v-model="show"
+    :close-on-click-modal="false"
+    :modal-append-to-body="false"
+    width="50%"
+    :title="isAdd?'添加套餐':'编辑'"
+  >
+    <div class="dialog-content">
+      <el-form label-width="100px" ref="formRef" class="form">
+        <div class="left">
+          <el-form-item label="品种">
+            <!-- <el-input v-model="packageData.Price" placeholder="请选择品种" /> -->
+            <el-cascader
+              filterable
+              :options="props.labelOptions"
+              collapse-tags
+              v-model="packageData.SourceId" 
+              @change="getRiskLevel"
+              :props="{
+                value:'id',
+                label:'name',
+              }"
+              style="width: 100%;"
+              placeholder="请选择品种"
+              clearable>
+            </el-cascader>
+          </el-form-item>
+          <el-form-item label="套餐名称">
+            <el-input v-model="packageData.ProductName" placeholder="请输入套餐名称"/>
+          </el-form-item>
+          <el-form-item label="商品价格(元)">
+            <el-input v-model="packageData.Price" placeholder="请输入价格" @keyup="handleValidPrice()"/>
+          </el-form-item>
+          <el-form-item label="有效时长(天)">
+            <el-input v-model="packageData.ValidDays" min="0" class="no-spinner" placeholder="请输入有效时长" @keyup="handleValidValidDays()" />
+          </el-form-item>
+          <div class="tips" v-if="!isAdd">
+            提示:修改价格和有效时长不影响历史已支付订单,仅对未来订单生效!
+          </div>
+        </div>
+        <div class="left">
+          <el-form-item label="风险等级" v-if="packageData.RiskLevel">
+            <span>{{ packageData.RiskLevel }}</span>
+          </el-form-item>
+          <el-form-item label="套餐简介">
+            <el-input
+              v-model="packageData.Description"
+              style="width: 240px"
+              :rows="4"
+              type="textarea"
+              placeholder="请输入套餐简介"
+            />
+          </el-form-item>
+          <el-form-item label="套餐封面">
+            <ImageUpload
+              :imgUrl="packageData.CoverSrc"
+              uploadHint="支持jpg、jpeg、png等格式,建议上传宽高比例为3:4的图片"
+              width="120px"
+              height="148px"
+              @upload="handleUploadImg"
+              @remove="packageData.CoverSrc=''"
+          ></ImageUpload>
+          </el-form-item>
+        </div>
+      </el-form>
+    </div>
+    <template #footer>
+      <div class="btn-content">
+        <el-button style="width: 80px" @click="handleClose()">取消</el-button>
+        <el-button
+          type="primary"
+          style="width: 80px; margin-right: 24px"
+          @click="handleSubmitForm"
+          >保存</el-button
+        >
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+
+<style scoped>
+.no-spinner::-webkit-inner-spin-button,
+.no-spinner::-webkit-outer-spin-button {
+  -webkit-appearance: none;
+  margin: 0;
+}
+/* 对于 Firefox */
+.no-spinner {
+  -moz-appearance: textfield;
+}
+</style>
+
+<style scoped lang="scss">
+.dialog-content {
+  width: 100%;
+  .form {
+    height: 350px;
+    display: flex;
+    align-items: center;
+    .left {
+      width: 50%;
+      height: 100%;
+      display: inline-block;
+    }
+  }
+  .tips {
+    margin-top: 50px;
+    line-height: 22px;
+    padding-left: 50px;
+    color: #909399;
+  }
+}
+.btn-content {
+  text-align: center;
+  padding-bottom: 20px;
+}
+</style>

+ 77 - 0
src/views/products/components/editSingle.vue

@@ -0,0 +1,77 @@
+<script setup>
+import {apiProductsConfig} from '@/api/products'
+const show = defineModel('show', { type: Boolean, default: false })
+const props = defineProps({
+  productInfo:{
+    type:Object,
+    default:{}
+  },
+})
+const emits = defineEmits(["close"])
+
+async function handleSubmitForm() {
+  if(!props.productInfo.Price) return ElMessage.warning('请输入商品价格')
+  const res=await apiProductsConfig.postEditProduct({
+    SourceId:props.productInfo.Id,
+    Type:props.productInfo.ProductType === '报告'?'report':'audio',
+    Price:props.productInfo.Price,
+  })
+  if(res.Ret!==200) return
+  ElMessage.success('保存成功')
+  handleClose()
+}
+function handleClose() {
+  emits("close")
+}
+</script>
+
+<template>
+  <el-dialog
+    v-model="show"
+    :close-on-click-modal="false"
+    :modal-append-to-body="false"
+    width="30%"
+    title="编辑"
+  >
+    <div class="dialog-content">
+      <el-form label-width="150px">
+        <el-form-item label="产品名称">
+          <span>{{ props.productInfo.ProductName }}</span>
+        </el-form-item>
+        <el-form-item label="商品价格(元)" prop="depart">
+          <el-input v-model="props.productInfo.Price" placeholder="请输入商品价格" />
+        </el-form-item>
+        <el-form-item label="风险等级">
+          <span>{{ props.productInfo.RiskLevel }}</span>
+        </el-form-item>
+      </el-form>
+      <div class="tips">
+        提示:修改价格和有效时长不影响历史已支付订单,仅对未来订单生效!
+      </div>
+    </div>
+    <template #footer>
+      <div class="btn-content">
+        <el-button style="width: 80px" @click="handleClose()">取消</el-button>
+        <el-button
+          type="primary"
+          style="width: 80px; margin-right: 24px"
+          @click="handleSubmitForm"
+          >保存</el-button
+        >
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<style scoped lang="scss">
+.tips {
+  line-height: 22px;
+  padding-left: 50px;
+  color: #909399;
+}
+
+.btn-content {
+  text-align: center;
+  padding-bottom: 20px;
+}
+</style>