Pārlūkot izejas kodu

Merge branch 'ch/ht_3.0' of eta_mini/eta_mini_crm_front into debug_ht

leichen 7 mēneši atpakaļ
vecāks
revīzija
91f9544860

+ 61 - 1
src/api/customer/user.js

@@ -23,5 +23,65 @@ export default {
      */
     getFeedbackList:params=>{
         return get('/feedBack/feedBackList',params)
-    }
+    },
+    /**
+     * 阅读记录列表 
+     * @param {Object} params
+     * @param {String} params.PageSize
+     * @param {String} params.CurrentIndex
+     * @param {String} params.ProductType 类型
+     * @param {String} params.UserId 用户id
+     * @param {String} params.PermissionIds 品种id
+     * @returns 
+     */
+    getUserReadRecordList:params=>{
+        return get('/user/readRecordList',params)
+    },
+    /**
+     * 正式用户列表
+     * @param {Object} params
+     * @param {Number} params.PageSize
+     * @param {Number} params.CurrentIndex
+     * @param {Number} params.Keyword
+     * @param {Number} params.SortParam
+     * @param {Number} params.SortType
+     * @param {Number} params.FollowingGzh
+     * @param {Number} params.RegisterBeginDate 开始时间
+     * @param {Number} params.RegisterEndDate 结束时间
+     * @returns 
+     */
+    getOfficialList:params=>{
+        return get('/user/official/list',params)
+    },
+    /**
+     * 导出用户列表
+     * @param {Object} params
+     * @param {Number} params.Keyword
+     * @param {Number} params.SortParam
+     * @param {Number} params.SortType
+
+     * @returns 
+     */
+    getTemporaryExport:params=>{
+        return get('/user/temporary/export',params)
+    },
+     /**
+     * 导出用户列表
+     * @param {Object} params
+     * @param {Number} params.Keyword
+     * @param {Number} params.SortParam
+     * @param {Number} params.SortType
+
+     * @returns 
+     */
+     getOfficialExport:params=>{
+        return get('/user/official/export',params)
+    },
+    /**
+     * @param {Number} params.UserId
+     * @returns 
+     */
+    getUserDetail:params=>{
+        return get('/user/detail',params)
+    },
 };

+ 12 - 4
src/api/index.js

@@ -19,6 +19,10 @@ const _axios = axios.create(config);
 
 _axios.interceptors.request.use(
   function (config) {
+    // 检查请求路径并设置responseType
+    if (config.url.endsWith('/export')) {
+      config.responseType = 'blob'; // 如果是导出excel的请求,则设置responseType为blob
+    }
     // Do something before request is sent
     let auth = localStorage.getItem('token')
     if(auth) {
@@ -38,10 +42,14 @@ _axios.interceptors.response.use(
   function (response) {
     // Do something with response data
     let data
-    if(import.meta.env.MODE==='production'){
-      data=JSON.parse(CryptoJS.Des3Decrypt(response.data,import.meta.env.VITE_APP_RESPONSE_DES_KEY));//解密
-    }else{
-      data=response.data
+    let isExportRequest = response.config.url.endsWith('/export');
+    if (import.meta.env.MODE === 'production' && !isExportRequest) {
+      data = JSON.parse(CryptoJS.Des3Decrypt(response.data, import.meta.env.VITE_APP_RESPONSE_DES_KEY)); // 解密
+    } else {
+      data = response.data;
+      if (isExportRequest) {
+        data.fileName = decodeURIComponent((response.headers['content-disposition'].split("=utf-8''"))[1]);
+      }
     }
     
 

+ 6 - 0
src/api/order/index.js

@@ -0,0 +1,6 @@
+import apiOrderConfig from './order'
+
+export {
+  apiOrderConfig,
+}
+

+ 66 - 0
src/api/order/order.js

@@ -0,0 +1,66 @@
+import { get, post } from "@/api/index";
+
+export default {
+    /**
+     * 商品订单列表 
+     * @param {String} params.PageSize
+     * @param {String} params.CurrentIndex
+     * @param {String} params.SortType
+     * @param {String} params.KeyWord
+     * @param {String} params.PaymentDate
+     * @param {String} params.PaymentWay
+     * @param {String} params.CreatedDate
+     * @param {String} params.ProductType
+     * @param {String} params.RefundStatus
+     * @param {String} params.OrderStatus
+     * @returns 
+     */
+    getProductOrderList:params=>{
+        return get('/order/productOrderList',params)
+    },
+    /**
+     * 支付明细列表 
+     * @param {String} params.PageSize
+     * @param {String} params.CurrentIndex
+     * @param {String} params.SortType
+     * @param {String} params.KeyWord
+     * @param {String} params.PaymentDate
+     * @param {String} params.PaymentWay
+     * @param {String} params.CreatedDate
+     * @param {String} params.OrderStatus
+     * @param {String} params.IsRefund
+     * @returns 
+     */
+    getTradeOrderList:params=>{
+        return get('/order/tradeOrderList',params)
+    },
+    /**
+     * 导出商品订单列表 
+     * @param {String} params.SortType
+     * @param {String} params.KeyWord
+     * @param {String} params.PaymentDate
+     * @param {String} params.PaymentWay
+     * @param {String} params.CreatedDate
+     * @param {String} params.ProductType
+     * @param {String} params.RefundStatus
+     * @param {String} params.OrderStatus
+     * @returns 
+     */
+    getExportProductOrder:params=>{
+        return get('/order/productOrder/export',params)
+    },
+    /**
+     * 导出支付明细列表 
+     * @param {String} params.SortType
+     * @param {String} params.KeyWord
+     * @param {String} params.PaymentDate
+     * @param {String} params.PaymentWay
+     * @param {String} params.CreatedDate
+     * @param {String} params.OrderStatus
+     * @param {String} params.IsRefund
+     * @returns 
+     */
+    getExportTradeOrder:params=>{
+        return get('/order/tradeOrder/export',params)
+    },
+};

+ 6 - 0
src/api/products/index.js

@@ -0,0 +1,6 @@
+import apiProductsConfig from './products'
+
+export {
+  apiProductsConfig,
+}
+

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

@@ -0,0 +1,108 @@
+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
+     * @param {String} params.ProductType 类型
+     * @param {String} params.PermissionIds 品种id
+     * @param {String} params.KeyWord 搜索关键字
+     * @returns 
+     */
+    getUnSetProductList:params=>{
+        return get('/product/unSetProductList',params)
+    },
+    /**
+     * 增加单品产品 
+     * @param {Object} params
+     * @param {String} params.Type 类型
+     * @param {String} params.Price 价格
+     * @param {String} params.SourceId 产品id
+     * @returns 
+     */
+    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)
+    }
+};

+ 8 - 0
src/router/modules/customer.js

@@ -17,6 +17,14 @@ export default[
             title:'临时用户列表'
         }
       },
+      {
+        path:'userList',
+        component:()=>import('@/views/customer/UserList.vue'),
+        name:'UserList',
+        meta:{
+            title:'用户列表'
+        }
+      },
       {
         path:'feedbackList',
         component:()=>import('@/views/customer/FeedbackList.vue'),

+ 38 - 0
src/router/modules/order.js

@@ -0,0 +1,38 @@
+import LayoutIndex from '@/layout/Index.vue'
+
+export default[
+  {
+    path:'/order',
+    component:LayoutIndex,
+    name:'OrderIndex',
+    meta:{
+      title:'统计报表'
+    },
+    children:[
+      {
+        path:'orderList',
+        component:()=>import('@/views/order/OrderList.vue'),
+        name:'OrderList',
+        meta:{
+            title:'商品订单'
+        }
+      },
+      {
+        path:'paymentList',
+        component:()=>import('@/views/order/PaymentList.vue'),
+        name:'PaymentList',
+        meta:{
+            title:'支付明细'
+        }
+      },
+      {
+        path:'refundList',
+        component:()=>import('@/views/order/RefundList.vue'),
+        name:'RefundList',
+        meta:{
+            title:'退款明细'
+        }
+      }
+    ]
+  }
+]

+ 38 - 0
src/router/modules/products.js

@@ -0,0 +1,38 @@
+import LayoutIndex from '@/layout/Index.vue'
+
+export default[
+  {
+    path:'/products',
+    component:LayoutIndex,
+    name:'ProductsIndex',
+    meta:{
+      title:'付费产品配置'
+    },
+    children:[
+      {
+        path:'singleConfigList',
+        component:()=>import('@/views/products/SingleConfigList.vue'),
+        name:'SingleConfigList',
+        meta:{
+            title:'单品配置'
+        }
+      },
+      {
+        path:'addSingle',
+        component:()=>import('@/views/products/addSingleList.vue'),
+        name:'addSingle',
+        meta:{
+            title:'添加单品'
+        }
+      },
+      {
+        path:'packageConfigList',
+        component:()=>import('@/views/products/PackageConfigList.vue'),
+        name:'PackageConfigList',
+        meta:{
+            title:'套餐配置'
+        }
+      }
+    ]
+  }
+]

+ 1 - 0
src/views/Login.vue

@@ -29,6 +29,7 @@ async function submitForm(formEl) {
         Username: formState.account,
         password: md5(md5(formState.pwd) + md5key + `${t}`),
         ReqTime: `${t}`,
+        LoginType:"password",
       }).then(res => {
         if (res.Ret === 200) {
           if (formState.checked) {

+ 98 - 36
src/views/customer/TempUserList.vue

@@ -2,6 +2,8 @@
 import { ref, reactive } from 'vue'
 import { Search } from '@element-plus/icons-vue'
 import {apiCustomerUser} from '@/api/customer'
+import { apiMediaCommon } from '@/api/media'
+import ReadDialog from './components/ReadDialog.vue'
 
 const tableColumns = [
     {
@@ -12,6 +14,10 @@ const tableColumns = [
         label:'最近一次阅读时间',
         key:'LastReadTime',
         sortable:true
+    },{
+        label:'开户状态',
+        key:'AccountStatus',
+        sortable:false,
     },{
         label:'累计阅读次数',
         key:'ReadCount',
@@ -31,7 +37,9 @@ const tableQuery = reactive({
     sortParam:'',
     sortType:''
 })
+
 const tableData = ref([])
+const show = ref(false)
 function getTableData(){
     apiCustomerUser.getTempCustomList({
         Keyword:tableQuery.keyWord,
@@ -47,6 +55,16 @@ function getTableData(){
     })
 }
 getTableData()
+getLableList()
+const labelOptions = ref([])
+const userId = ref('')
+function getLableList(){
+    apiMediaCommon.getPermissionList().then(res=>{
+        if(res.Ret!==200) return
+        labelOptions.value = res.Data.List||[]
+    })
+}
+
 function handlePageChange(page){
     tableQuery.currentPage = page
     getTableData()
@@ -62,55 +80,99 @@ function handleSortChange({order,prop}){
     tableQuery.sortType = order==='ascending'?1:0
     getTableData()
 }
+
+// 查看用户详情
+function Details(row) {
+    if (row.ReadCount <= 0) return;
+    show.value = true;
+    userId.value = row.Id + '';
+}
+
+/* 下载数据 */
+async function downloadExcel() {
+    const res = await apiCustomerUser.getTemporaryExport(
+        {
+            Keyword:tableQuery.keyWord,
+            SortParam:tableQuery.sortParam,
+            SortType:tableQuery.sortType,
+        }
+    )
+    const blob = new Blob([res], {
+        type: "application/vnd.ms-excel;charset=utf-8",
+    });
+    let fileName = res.fileName;
+
+    const elink = document.createElement("a");
+    elink.download = fileName; //命名下载名称
+    elink.style.display = "none";
+    elink.href = URL.createObjectURL(blob);
+    document.body.appendChild(elink);
+    elink.click(); // 点击下载
+    URL.revokeObjectURL(elink.href); // 释放URL 对象
+    document.body.removeChild(elink); // 释放标
+}
 </script>
 
 <template>
-    <div class="temp-user-list-wrap">
-        <div class="search-box">
-            <el-input 
-                v-model="tableQuery.keyWord"
-                :prefix-icon="Search" clearable
-                style="width:400px"
-                placeholder="请输入手机号" 
-                @input="handlePageChange(1)"
+    <el-card class="box-card">
+        <div class="temp-user-list-wrap">
+            <div class="search-box">
+                <el-button type="primary" style="margin-right: 20px;"  @click="downloadExcel">导出表格</el-button>
+                <el-input 
+                    v-model="tableQuery.keyWord"
+                    :prefix-icon="Search" clearable
+                    style="width:400px"
+                    placeholder="请输入手机号" 
+                    @input="handlePageChange(1)"
+                    />
+            </div>
+            <div class="table-box">
+                <el-table stripe 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">
+                        <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>
+                        </template>
+                        <template #default="scope" v-else-if="column.key === 'ReadCount'">
+                            <span class="ReadCount" @click="Details(scope.row)">
+                                {{ scope.row[column.key] }}
+                            </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>
-        <div class="table-box">
-            <el-table stripe 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"/>
-            </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;border:1px solid #ebeef5;border-top: none;"
-            />
-        </div>
-    </div>
+        <ReadDialog v-model:show="show" :labelOptions="labelOptions" :userId="userId"></ReadDialog>
+    </el-card>
 </template>
 
 <style scoped lang="scss">
 .temp-user-list-wrap{
-    height: calc(100vh - 108px); //layout padding 30*2 headHeight 48
+    height: calc(100vh - 208px); //layout padding 30*2 headHeight 48
     /* display: flex;
     flex-direction: column; */
     .search-box{
         text-align: right;
-        margin-bottom: 20px;
+        margin: 20px 0 40px 0;
+    }
+    .table-box{
+       .ReadCount {
+        color: rgba(8, 108, 224, 1);
+        cursor: pointer;
+       }
     }
-    /* .table-box{
-        flex: 1;
-        display: flex;
-        flex-direction: column;
-        .el-table{
-            flex:1;
-            overflow-y: auto;
-        }
-    } */
 }
 </style>

+ 234 - 0
src/views/customer/UserList.vue

@@ -0,0 +1,234 @@
+<script setup>
+import { ref, reactive } from 'vue'
+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:'RealName',
+        sortable:false
+    },
+    {
+        label:'手机号',
+        key:'Mobile',
+        sortable:false
+    },
+    {
+        label:'公司名称',
+        key:'Mobile',
+        sortable:false
+    },
+    {
+        label:'注册时间',
+        key:'CreatedTime',
+        sortable:false
+    },{
+        label:'是否关注公众号',
+        key:'FollowingGzh',
+        sortable:false,
+    },{
+        label:'最近一次阅读时间',
+        key:'LastReadTime',
+        sortable:false
+    },{
+        label:'累计阅读次数',
+        key:'ReadCount',
+        sortable:false
+    }
+]
+
+const tableQuery = reactive({
+    keyWord:'',
+    currentPage:1,
+    pageSize:10,
+    totals:0,
+    sortParam:'',
+    sortType:'',
+    FollowingGzh:'',
+    RegisterBeginDate:'',
+    RegisterEndDate:'',
+})
+
+const tableData = ref([])
+const show = ref(false)
+const showUserDialog = ref(false)
+function getTableData(){
+    apiCustomerUser.getOfficialList({
+        Keyword:tableQuery.keyWord,
+        CurrentIndex:tableQuery.currentPage,
+        PageSize:tableQuery.pageSize,
+        SortParam:tableQuery.sortParam,
+        SortType:tableQuery.sortType,
+        FollowingGzh:tableQuery.FollowingGzh,
+        RegisterBeginDate:tableQuery.RegisterBeginDate,
+        RegisterEndDate:tableQuery.RegisterEndDate,
+    }).then(res=>{
+        if(res.Ret!==200) return 
+
+        tableData.value = res.Data.List||[]
+        tableQuery.totals = res.Data.Paging.Totals||0
+    })
+}
+getTableData()
+getLableList()
+const labelOptions = ref([])
+const userId = ref('')
+const value1 = ref(['',''])
+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 Details(row) {
+    if (row.ReadCount <= 0) return;
+    show.value = true;
+    userId.value = row.TemplateUserId + '';
+}
+function handleSelectChange() {
+    getTableData();
+}
+function changeDatePicker(val) {
+    tableQuery.RegisterBeginDate = val[0];
+    tableQuery.RegisterEndDate = val[1];
+    getTableData();
+}
+function userDetails(row) {
+    showUserDialog.value = true;
+}
+
+/* 下载数据 */
+async function downloadExcel() {
+    const res = await apiCustomerUser.getOfficialExport(
+        {
+            Keyword:tableQuery.keyWord,
+            SortParam:tableQuery.sortParam,
+            SortType:tableQuery.sortType,
+        }
+    )
+    const blob = new Blob([res], {
+        type: "application/vnd.ms-excel;charset=utf-8",
+    });
+    let fileName = res.fileName;
+    const elink = document.createElement("a");
+    elink.download = fileName; //命名下载名称
+    elink.style.display = "none";
+    elink.href = URL.createObjectURL(blob);
+    document.body.appendChild(elink);
+    elink.click(); // 点击下载
+    URL.revokeObjectURL(elink.href); // 释放URL 对象
+    document.body.removeChild(elink); // 释放标
+}
+</script>
+
+<template>
+    <el-card class="box-card">
+        <div class="temp-user-list-wrap">
+            <div class="top-box">
+                <div>
+                    <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>
+                    <el-date-picker
+                        v-model="value1"
+                        value-format="YYYY-MM-DD"
+                        type="daterange"
+                        @change="changeDatePicker"
+                        range-separator="至"
+                        start-placeholder="开始日期"
+                        end-placeholder="结束日期">
+                    </el-date-picker>
+                </div>
+                <div class="search-box">
+                    <el-button type="primary" style="margin-right: 20px;" @click="downloadExcel">导出表格</el-button>
+                    <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 stripe 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">
+                        <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>
+                        </template>
+                        <template #default="scope" v-else-if="column.key === 'ReadCount'">
+                            <span class="ReadCount" @click="Details(scope.row)">
+                                {{ 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>
+                    </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>
+        <ReadDialog v-model:show="show" :labelOptions="labelOptions" :userId="userId"></ReadDialog>
+        <UserDialog v-model:show="showUserDialog" :userId="userId"></UserDialog>
+    </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;
+        .search-box{
+            text-align: right;
+            margin: 20px 0 40px 0;
+        }
+    }
+    .table-box{
+       .ReadCount {
+        color: rgba(8, 108, 224, 1);
+        cursor: pointer;
+       }
+    }
+}
+</style>

+ 181 - 0
src/views/customer/components/ReadDialog.vue

@@ -0,0 +1,181 @@
+<script setup>
+const show = defineModel('show', { type: Boolean, default: false })
+const props=defineProps({
+    labelOptions:{
+        type:Array,
+        default:[]
+    },
+    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:'',
+        sortable:false
+    },{
+        label:'产品类型',
+        key:'SourceName',
+        sortable:false
+    },{
+        label:'品种',
+        key:'PermissionNames',
+        sortable:false,
+    },{
+        label:'最近一次点击时间',
+        key:'ClickTime',
+        sortable:true
+    },{
+        label:'停留时长',
+        key:'ReadDurationMinutes',
+        sortable:true
+    }
+]
+
+const tableQuery = reactive({
+    currentPage:1,
+    pageSize:10,
+    totals:0,
+    sortParam:'',
+    sortType:'',
+    PermissionIds:null,
+    ProductType:'',
+})
+const tableData = ref([])
+function getTableData(){
+    apiCustomerUser.getUserReadRecordList({
+        CurrentIndex:tableQuery.currentPage,
+        PageSize:tableQuery.pageSize,
+        SortParam:tableQuery.sortParam,
+        SortType:tableQuery.sortType,
+        ProductType:tableQuery.ProductType,
+        PermissionIds:tableQuery.PermissionIds?.join(','),
+        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">
+            <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"
+                :label="item.label"
+                :value="item.value">
+                </el-option>
+            </el-select>
+            <el-cascader
+                filterable
+                :options="props.labelOptions"
+                collapse-tags
+                v-model="tableQuery.PermissionIds" 
+                 @change="handleSelectChange()"
+                :props="{
+                    value:'id',
+                    label:'name',
+                    emitPath:false,
+                    multiple:true
+                }"
+                style="width: 250px;"
+                clearable>
+            </el-cascader>
+        </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>

+ 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>

+ 340 - 0
src/views/order/OrderList.vue

@@ -0,0 +1,340 @@
+<script setup>
+import { ref, reactive } from 'vue'
+import { Search } from '@element-plus/icons-vue'
+import {apiOrderConfig} from '@/api/order'
+import { apiMediaCommon } from '@/api/media'
+import UserDialog from './components/UserDialog.vue'
+
+const tableColumns = [
+    {
+        label:'订单编号',
+        key:'OrderID',
+        sortable:false
+    },
+    {
+        label:'姓名',
+        key:'RealName',
+        sortable:false
+    },
+    {
+        label:'手机号',
+        key:'Mobile',
+        sortable:false
+    },
+    {
+        label:'商品名称',
+        key:'ProductName',
+        sortable:false
+    },{
+        label:'商品类型',
+        key:'ProductType',
+        sortable:false,
+    },{
+        label:'商品价格',
+        key:'TotalAmount',
+        sortable:false
+    },{
+        label:'有效期',
+        key:'ReadCount',
+        sortable:false
+    },{
+        label:'订单状态',
+        key:'Status',
+        sortable:false
+    },{
+        label:'支付渠道',
+        key:'PaymentWay',
+        sortable:false
+    },{
+        label:'支付金额',
+        key:'ReadCount',
+        sortable:false
+    },{
+        label:'售后状态',
+        key:'RefundStatus',
+        sortable:false
+    },{
+        label:'付款时间',
+        key:'PaymentTime',
+        sortable:false
+    },{
+        label:'下单时间',
+        key:'RefundFinishTime',
+        sortable:false
+    },
+    {
+        label:'操作',
+        key:'handle',
+        align: 'center'
+    }
+]
+const productTypeList = ref([
+    {
+        value: 'report',
+        label: '报告'
+    }, {
+        value: 'audio',
+        label: '音频'
+    }, {
+        value: 'video',
+        label: '视频'
+    }, {
+        value: 'package',
+        label: '套餐'
+    }
+])
+const orderStatusList = ref([
+    {
+        value: 'pending',
+        label: '待支付'
+    }, {
+        value: 'paid',
+        label: '已支付'
+    }, {
+        value: 'closed',
+        label: '已关闭'
+    }, {
+        value: 'refund',
+        label: '售后'
+    }
+])
+const refundListStatusList = ref([
+    {
+        value: 'processing',
+        label: '退款中'
+    }, {
+        value: 'failure',
+        label: '退款失败'
+    }, {
+        value: 'success',
+        label: '退款成功'
+    }
+])
+
+const tableQuery = reactive({
+    keyWord:'',
+    currentPage:1,
+    pageSize:10,
+    totals:0,
+    sortType:'',
+    paymentDate:'',
+    paymentWay:'',
+    createdDate:'',
+    productType:'',
+    refundStatus:'',
+    orderStatus:''
+})
+
+const tableData = ref([])
+const show = ref(false)
+const showUserDialog = ref(false)
+function getTableData(){
+    apiOrderConfig.getProductOrderList({
+        Keyword:tableQuery.keyWord,
+        CurrentIndex:tableQuery.currentPage,
+        PageSize:tableQuery.pageSize,
+        SortType:tableQuery.sortType,
+        PaymentDate:tableQuery.paymentDate,
+        PaymentWay:tableQuery.paymentWay,
+        CreatedDate:tableQuery.createdDate,
+        ProductType:tableQuery.productType,
+        RefundStatus:tableQuery.refundStatus,
+        OrderStatus:tableQuery.orderStatus
+    }).then(res=>{
+        if(res.Ret!==200) return 
+
+        tableData.value = res.Data.List||[]
+        tableQuery.totals = res.Data.Paging.Totals||0
+    })
+}
+getTableData()
+getLableList()
+const labelOptions = ref([])
+const userId = ref('')
+const value1 = ref(['',''])
+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 Details(row) {
+    if (row.ReadCount <= 0) return;
+    show.value = true;
+    userId.value = row.TemplateUserId + '';
+}
+function handleSelectChange() {
+    getTableData();
+}
+function userDetails(row) {
+    showUserDialog.value = true;
+}
+
+/* 下载数据 */
+async function downloadExcel() {
+    const res = await apiOrderConfig.getExportProductOrder(
+        {
+            Keyword:tableQuery.keyWord,
+            SortType:tableQuery.sortType,
+            PaymentDate:tableQuery.paymentDate,
+            PaymentWay:tableQuery.paymentWay,
+            CreatedDate:tableQuery.createdDate,
+            ProductType:tableQuery.productType,
+            RefundStatus:tableQuery.refundStatus,
+            OrderStatus:tableQuery.orderStatus
+        }
+    )
+    const blob = new Blob([res], {
+        type: "application/vnd.ms-excel;charset=utf-8",
+    });
+    let fileName = res.fileName;
+    const elink = document.createElement("a");
+    elink.download = fileName; //命名下载名称
+    elink.style.display = "none";
+    elink.href = URL.createObjectURL(blob);
+    document.body.appendChild(elink);
+    elink.click(); // 点击下载
+    URL.revokeObjectURL(elink.href); // 释放URL 对象
+    document.body.removeChild(elink); // 释放标
+}
+</script>
+
+<template>
+    <el-card class="box-card">
+        <div class="temp-user-list-wrap">
+            <div class="top-box">
+                <div class="search-box">
+                    <el-select clearable v-model="tableQuery.productType" @change="handleSelectChange()" placeholder="产品类型" style="width: 150px; margin-right: 20px;">
+                        <el-option
+                        v-for="item in productTypeList"
+                        :key="item.value"
+                        :label="item.label"
+                        :value="item.value">
+                        </el-option>
+                    </el-select>
+                    <el-select clearable v-model="tableQuery.orderStatus" @change="handleSelectChange()" placeholder="订单状态" style="width: 150px; margin-right: 20px;">
+                        <el-option
+                        v-for="item in orderStatusList"
+                        :key="item.value"
+                        :label="item.label"
+                        :value="item.value">
+                        </el-option>
+                    </el-select>
+                    <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>
+                    <el-select v-model="tableQuery.refundStatus" clearable @change="handleSelectChange()" placeholder="售后状态" style="width: 150px; margin-right: 20px;">
+                        <el-option
+                        v-for="item in refundListStatusList"
+                        :key="item.value"
+                        :label="item.label"
+                        :value="item.value">
+                        </el-option>
+                    </el-select>
+                    <el-date-picker
+                        v-model="tableQuery.createdDate"
+                        type="date"
+                        placeholder="付款时间"
+                        format="YYYY-MM-DD"
+                        style="margin-right: 20px;width: 150px;"
+                        value-format="YYYY-MM-DD"
+                        @change="handlePageChange(1)"
+                    />
+                    <el-date-picker
+                        v-model="tableQuery.paymentDate"
+                        type="date"
+                        placeholder="下单时间"
+                        style="margin-right: 20px;width: 150px;"
+                        format="YYYY-MM-DD"
+                        value-format="YYYY-MM-DD"
+                        @change="handlePageChange(1)"
+                    />
+                </div>
+                <div class="search-box">
+                    <el-button type="primary" style="margin-right: 20px;" @click="downloadExcel">导出表格</el-button>
+                    <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 stripe 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">
+                        <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>
+                        </template>
+                        <template #default="scope" v-else-if="column.key === 'handle'">
+                            <span v-if="scope.row.Status !== '已支付'" @click="operation('stock', scope.row)">-</span>
+                            <span class="edit" v-if="scope.row.Status === '已支付' && scope.row.RefundStatus === '退款成功'"  @click="operation('delist', scope.row)" >退款详情</span>
+                            <span class="edit" v-if="scope.row.Status === '已支付'" @click="operation('edit', scope.row)">退款</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: 20px;"
+                />
+            </div>
+        </div>
+        <UserDialog v-model:show="showUserDialog" :userId="userId"></UserDialog>
+    </el-card>
+</template>
+
+<style scoped lang="scss">
+.temp-user-list-wrap{
+    // height: calc(100vh); //layout padding 30*2 headHeight 48
+    .top-box{
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        .search-box{
+            text-align: right;
+            margin: 10px 0 20px 0;
+        }
+    }
+    .table-box{
+    //    .ReadCount {
+    //     color: rgba(8, 108, 224, 1);
+    //     cursor: pointer;
+    //    }
+       .edit {
+            cursor:pointer;
+            color: rgba(8, 108, 224, 1);
+            margin-right: 10px;
+       }
+    }
+}
+</style>

+ 278 - 0
src/views/order/PaymentList.vue

@@ -0,0 +1,278 @@
+<script setup>
+import { ref, reactive } from 'vue'
+import { Search } from '@element-plus/icons-vue'
+import {apiOrderConfig} from '@/api/order'
+import { apiMediaCommon } from '@/api/media'
+
+const tableColumns = [
+    {
+        label:'支付单号',
+        key:'transaction_id',
+        sortable:false
+    },
+    {
+        label:'订单编号',
+        key:'product_order_id',
+        sortable:false
+    },
+    {
+        label:'姓名',
+        key:'RealName',
+        sortable:false
+    },
+    {
+        label:'手机号',
+        key:'Mobile',
+        sortable:false
+    },
+    {
+        label:'商品名称',
+        key:'ProductName',
+        sortable:false
+    },{
+        label:'支付金额',
+        key:'payment_account',
+        sortable:false
+    },{
+        label:'支付状态',
+        key:'payment_status',
+        sortable:false
+    },{
+        label:'支付渠道',
+        key:'payment_way',
+        sortable:false
+    },{
+        label:'支付账号',
+        key:'ReadCount',
+        sortable:false
+    },{
+        label:'收款方',
+        key:'merchant_id',
+        sortable:false
+    },{
+        label:'完成支付时间',
+        key:'DealTime',
+        sortable:false
+    },{
+        label:'创建时间',
+        key:'CreatedTime',
+        sortable:false
+    }
+]
+const orderStatusList = ref([
+    {
+        value: 'pending',
+        label: '待支付'
+    }, {
+        value: 'paid',
+        label: '已支付'
+    }, {
+        value: 'closed',
+        label: '已关闭'
+    }, {
+        value: 'refund',
+        label: '售后'
+    }
+])
+
+const tableQuery = reactive({
+    keyWord:'',
+    currentPage:1,
+    pageSize:10,
+    totals:0,
+    sortType:'',
+    paymentDate:'',
+    paymentWay:'',
+    createdDate:'',
+    orderStatus:''
+})
+
+const tableData = ref([])
+const show = ref(false)
+const showUserDialog = ref(false)
+function getTableData(){
+    apiOrderConfig.getTradeOrderList({
+        Keyword:tableQuery.keyWord,
+        CurrentIndex:tableQuery.currentPage,
+        PageSize:tableQuery.pageSize,
+        SortType:tableQuery.sortType,
+        PaymentDate:tableQuery.paymentDate,
+        PaymentWay:tableQuery.paymentWay,
+        CreatedDate:tableQuery.createdDate,
+        OrderStatus:tableQuery.orderStatus,
+        IsRefund:false,
+    }).then(res=>{
+        if(res.Ret!==200) return 
+
+        tableData.value = res.Data.List||[]
+        tableQuery.totals = res.Data.Paging.Totals||0
+    })
+}
+getTableData()
+getLableList()
+const labelOptions = ref([])
+const userId = ref('')
+const value1 = ref(['',''])
+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 Details(row) {
+    if (row.ReadCount <= 0) return;
+    show.value = true;
+    userId.value = row.TemplateUserId + '';
+}
+function handleSelectChange() {
+    getTableData();
+}
+function userDetails(row) {
+    showUserDialog.value = true;
+}
+
+/* 下载数据 */
+async function downloadExcel() {
+    const res = await apiOrderConfig.getExportTradeOrder(
+        {
+            Keyword:tableQuery.keyWord,
+            SortType:tableQuery.sortType,
+            PaymentDate:tableQuery.paymentDate,
+            PaymentWay:tableQuery.paymentWay,
+            CreatedDate:tableQuery.createdDate,
+            OrderStatus:tableQuery.orderStatus,
+            IsRefund:false,
+        }
+    )
+    const blob = new Blob([res], {
+        type: "application/vnd.ms-excel;charset=utf-8",
+    });
+    let fileName = res.fileName;
+    const elink = document.createElement("a");
+    elink.download = fileName; //命名下载名称
+    elink.style.display = "none";
+    elink.href = URL.createObjectURL(blob);
+    document.body.appendChild(elink);
+    elink.click(); // 点击下载
+    URL.revokeObjectURL(elink.href); // 释放URL 对象
+    document.body.removeChild(elink); // 释放标
+}
+</script>
+
+<template>
+    <el-card class="box-card">
+        <div class="temp-user-list-wrap">
+            <div class="top-box">
+                <div class="search-box">
+                    <el-select clearable v-model="tableQuery.orderStatus" @change="handleSelectChange()" placeholder="支付状态" style="width: 150px; margin-right: 20px;">
+                        <el-option
+                        v-for="item in orderStatusList"
+                        :key="item.value"
+                        :label="item.label"
+                        :value="item.value">
+                        </el-option>
+                    </el-select>
+                    <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>
+                    <el-date-picker
+                        v-model="tableQuery.createdDate"
+                        type="date"
+                        placeholder="付款时间"
+                        format="YYYY-MM-DD"
+                        style="margin-right: 20px;width: 150px;"
+                        value-format="YYYY-MM-DD"
+                        @change="handlePageChange(1)"
+                    />
+                    <el-date-picker
+                        v-model="tableQuery.paymentDate"
+                        type="date"
+                        placeholder="下单时间"
+                        style="margin-right: 20px;width: 150px;"
+                        format="YYYY-MM-DD"
+                        value-format="YYYY-MM-DD"
+                        @change="handlePageChange(1)"
+                    />
+                </div>
+                <div class="search-box">
+                    <el-button type="primary" style="margin-right: 20px;" @click="downloadExcel">导出表格</el-button>
+                    <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 stripe 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">
+                        <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>
+                        </template>
+                        <template #default="scope" v-else-if="column.key === 'ReadCount'">
+                            <span class="ReadCount" @click="Details(scope.row)">
+                                {{ scope.row[column.key] }}
+                            </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: 20px;"
+                />
+            </div>
+        </div>
+    </el-card>
+</template>
+
+<style scoped lang="scss">
+.temp-user-list-wrap{
+    // height: calc(100vh); //layout padding 30*2 headHeight 48
+    .top-box{
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        .search-box{
+            text-align: right;
+            margin: 10px 0 20px 0;
+        }
+    }
+    .table-box{
+       .ReadCount {
+        color: rgba(8, 108, 224, 1);
+        cursor: pointer;
+       }
+    }
+}
+</style>

+ 232 - 0
src/views/order/RefundList.vue

@@ -0,0 +1,232 @@
+<script setup>
+import { ref, reactive } from 'vue'
+import { Search } from '@element-plus/icons-vue'
+import {apiOrderConfig} from '@/api/order'
+import { apiMediaCommon } from '@/api/media'
+
+const tableColumns = [
+    {
+        label:'支付单号',
+        key:'transaction_id',
+        sortable:false
+    },
+    {
+        label:'订单编号',
+        key:'product_order_id',
+        sortable:false
+    },
+    {
+        label:'姓名',
+        key:'RealName',
+        sortable:false
+    },
+    {
+        label:'手机号',
+        key:'Mobile',
+        sortable:false
+    },
+    {
+        label:'商品名称',
+        key:'ProductName',
+        sortable:false
+    },{
+        label:'支付金额',
+        key:'payment_account',
+        sortable:false
+    },{
+        label:'支付状态',
+        key:'payment_status',
+        sortable:false
+    },{
+        label:'支付渠道',
+        key:'payment_way',
+        sortable:false
+    },{
+        label:'支付账号',
+        key:'ReadCount',
+        sortable:false
+    },{
+        label:'收款方',
+        key:'merchant_id',
+        sortable:false
+    },{
+        label:'完成支付时间',
+        key:'DealTime',
+        sortable:false
+    },{
+        label:'创建时间',
+        key:'CreatedTime',
+        sortable:false
+    }
+]
+const orderStatusList = ref([
+    {
+        value: 'pending',
+        label: '退款中'
+    }, {
+        value: 'failed',
+        label: '退款失败'
+    }, {
+        value: 'done',
+        label: '退款成功'
+    }
+])
+
+const tableQuery = reactive({
+    keyWord:'',
+    currentPage:1,
+    pageSize:10,
+    totals:0,
+    sortType:'',
+    paymentDate:'',
+    paymentWay:'',
+    createdDate:'',
+    orderStatus:''
+})
+
+const tableData = ref([])
+function getTableData(){
+    apiOrderConfig.getTradeOrderList({
+        Keyword:tableQuery.keyWord,
+        CurrentIndex:tableQuery.currentPage,
+        PageSize:tableQuery.pageSize,
+        SortType:tableQuery.sortType,
+        PaymentDate:tableQuery.paymentDate,
+        PaymentWay:tableQuery.paymentWay,
+        CreatedDate:tableQuery.createdDate,
+        OrderStatus:tableQuery.orderStatus,
+        IsRefund:true,
+    }).then(res=>{
+        if(res.Ret!==200) return 
+        tableData.value = res.Data.List||[]
+        tableQuery.totals = res.Data.Paging.Totals||0
+    })
+}
+getTableData()
+
+function handlePageChange(page){
+    tableQuery.currentPage = page
+    getTableData()
+}
+function handleSelectChange() {
+    getTableData();
+}
+
+/* 下载数据 */
+async function downloadExcel() {
+    const res = await apiOrderConfig.getExportTradeOrder(
+        {
+            Keyword:tableQuery.keyWord,
+            SortType:tableQuery.sortType,
+            PaymentDate:tableQuery.paymentDate,
+            PaymentWay:tableQuery.paymentWay,
+            CreatedDate:tableQuery.createdDate,
+            OrderStatus:tableQuery.orderStatus,
+            IsRefund:true,
+        }
+    )
+    const blob = new Blob([res], {
+        type: "application/vnd.ms-excel;charset=utf-8",
+    });
+    let fileName = res.fileName;
+    const elink = document.createElement("a");
+    elink.download = fileName; //命名下载名称
+    elink.style.display = "none";
+    elink.href = URL.createObjectURL(blob);
+    document.body.appendChild(elink);
+    elink.click(); // 点击下载
+    URL.revokeObjectURL(elink.href); // 释放URL 对象
+    document.body.removeChild(elink); // 释放标
+}
+</script>
+
+<template>
+    <el-card class="box-card">
+        <div class="temp-user-list-wrap">
+            <div class="top-box">
+                <div class="search-box">
+                    <el-select clearable v-model="tableQuery.orderStatus" @change="handleSelectChange()" placeholder="退款状态" style="width: 150px; margin-right: 20px;">
+                        <el-option
+                        v-for="item in orderStatusList"
+                        :key="item.value"
+                        :label="item.label"
+                        :value="item.value">
+                        </el-option>
+                    </el-select>
+                    <el-date-picker
+                        v-model="tableQuery.createdDate"
+                        type="date"
+                        placeholder="完成退款时间"
+                        format="YYYY-MM-DD"
+                        style="margin-right: 20px;width: 200px;"
+                        value-format="YYYY-MM-DD"
+                        @change="handlePageChange(1)"
+                    />
+                    <el-date-picker
+                        v-model="tableQuery.paymentDate"
+                        type="date"
+                        placeholder="下单时间"
+                        style="margin-right: 20px;width: 200px;"
+                        format="YYYY-MM-DD"
+                        value-format="YYYY-MM-DD"
+                        @change="handlePageChange(1)"
+                    />
+                </div>
+                <div class="search-box">
+                    <el-button type="primary" style="margin-right: 20px;" @click="downloadExcel">导出表格</el-button>
+                    <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 stripe border :data="tableData">
+                    <el-table-column 
+                        v-for="column in tableColumns" :key="column.key"
+                        :prop="column.key" :label="column.label" :sortable="column.sortable">
+                        <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>
+                        </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: 20px;"
+                />
+            </div>
+        </div>
+    </el-card>
+</template>
+
+<style scoped lang="scss">
+.temp-user-list-wrap{
+    // height: calc(100vh); //layout padding 30*2 headHeight 48
+    .top-box{
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        .search-box{
+            text-align: right;
+            margin: 10px 0 20px 0;
+        }
+    }
+    .table-box{
+       .ReadCount {
+        color: rgba(8, 108, 224, 1);
+        cursor: pointer;
+       }
+    }
+}
+</style>

+ 143 - 0
src/views/order/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 {apiOrderConfig} from '@/api/order'
+
+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(){
+    apiOrderConfig.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>

+ 315 - 0
src/views/products/PackageConfigList.vue

@@ -0,0 +1,315 @@
+<script setup>
+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>
+        <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>

+ 309 - 0
src/views/products/SingleConfigList.vue

@@ -0,0 +1,309 @@
+<script setup>
+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 editSingle from './components/editSingle.vue'
+const router=useRouter()
+const tableColumns = [
+    {
+        label:'产品名称',
+        key:'ProductName',
+        align: 'center',
+        minwidthsty:250
+    },
+    {
+        label:'产品类型',
+        key:'ProductType',
+        align:'center',
+        widthsty:100,
+    },
+    {
+        label:'商品价格',
+        key:'Price',
+        align: 'center',
+        widthsty:120,
+    },
+    {
+        label:'风险等级',
+        key:'RiskLevel',
+        align: 'center',
+        widthsty:100,
+    },{
+        label:'状态',
+        key:'SaleStatus',
+        align: 'center',
+        widthsty:120,
+    },{
+        label:'创建人',
+        key:'LastReadTime',
+        align: 'center'
+    },{
+        label:'创建时间',
+        key:'PublishedTime',
+        sortable:true,
+        align: 'center'
+    },
+    {
+        label:'更新时间',
+        key:'UpdatedTime',
+        sortable:true,
+        align: 'center'
+    },
+    {
+        label:'操作',
+        key:'handle',
+        align: 'center'
+    }
+]
+const options = ref([
+    {
+        value: 'report',
+        label: '报告'
+    }, {
+        value: 'audio',
+        label: '音频'
+    }, {
+        value: 'video',
+        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:'',
+    SaleStatus:null,
+    CreatedTime:'',
+    UpdatedTime:'',
+})
+
+function getTableData(){
+    apiProductsConfig.getproductList({
+        IsSingle:true, // 是否是单品
+        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 '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>
+    <el-card>
+        <div class="temp-user-list-wrap">
+            <div class="top-box">
+                <div>
+                    <el-button type="primary" style="margin-right: 20px;" @click="router.push('/products/addSingle')">添加单品</el-button>
+                    <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"
+                        :label="item.label"
+                        :value="item.value">
+                        </el-option>
+                    </el-select>
+                    <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>
+                            {{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>
+        <editSingle v-model:show="showModal" :productInfo="productInfo" @close="closeEdit()"></editSingle>
+    </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>

+ 215 - 0
src/views/products/addSingleList.vue

@@ -0,0 +1,215 @@
+<script setup>
+import { ref, reactive } from 'vue'
+import {apiProductsConfig} from '@/api/products'
+import { apiMediaCommon } from '@/api/media'
+import { Search } from '@element-plus/icons-vue'
+import { useRouter } from 'vue-router'
+const router=useRouter()
+const tableColumns = [
+    {
+        label:'产品名称',
+        key:'ProductName',
+        sortable:false,
+        align:'center',
+        minwidthsty:150
+    },{
+        label:'发布时间',
+        key:'PublishedTime',
+        sortable:false,
+        align:'center',
+        widthsty:200
+    },{
+        label:'产品类型',
+        key:'ProductType',
+        sortable:false,
+        align:'center',
+        widthsty:100
+    },{
+        label:'品种',
+        key:'ClickTime',
+        sortable:false,
+        align:'center'
+    },{
+        label:'风险等级',
+        key:'RiskLevel',
+        sortable:false,
+        align:'center',
+        widthsty:100
+    },
+    {
+        label:'商品价格(元)',
+        key:'Price',
+        sortable:false,
+        align:'center'
+    },
+    {
+        label:'操作',
+        key:'handle',
+        align:'center'
+    },
+]
+const tableData = ref([])
+const labelOptions = ref([])
+const tableQuery = reactive({
+    keyWord:'',
+    currentPage:1,
+    pageSize:10,
+    totals:0,
+    ProductType:'report',
+    PermissionIds:null,
+})
+
+getTableData()
+getLableList()
+function getTableData(){
+    apiProductsConfig.getUnSetProductList({
+        Keyword:tableQuery.keyWord,
+        CurrentIndex:tableQuery.currentPage,
+        PageSize:tableQuery.pageSize,
+        ProductType:tableQuery.ProductType,
+        PermissionIds:tableQuery.PermissionIds?.join(','),
+    }).then(res=>{
+        if(res.Ret!==200) return 
+        tableData.value = res.Data.List||[]
+        tableQuery.totals = res.Data.Paging.Totals||0
+    })
+}
+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}){
+    const propMap = {
+        0:'ClickTime',
+        1:'Price',
+    }
+    tableQuery.sortParam = propMap[prop]||2
+    tableQuery.sortType = order==='ascending'?1:0
+    getTableData()
+}
+function handleSelectChange(){
+    getTableData()
+}
+function close() {
+    router.push('/products/singleConfigList')
+}
+function stockProducts(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>
+
+<template>
+    <el-card>
+        <div class="dialog-content">
+            <div>
+                <el-radio-group v-model="tableQuery.ProductType" size="large" @change="handleSelectChange()">
+                    <el-radio-button label="报告" value="report" />
+                    <el-radio-button label="音视频" value="media" />
+                </el-radio-group>
+            </div>
+            <div class="top-box">
+                <div>
+                    <!-- <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"
+                        :label="item.label"
+                        :value="item.value">
+                        </el-option>
+                    </el-select> -->
+                    <el-cascader
+                        filterable
+                        :options="labelOptions"
+                        collapse-tags
+                        placeholder="请选择品种"
+                        v-model="tableQuery.PermissionIds" 
+                        @change="handleSelectChange()"
+                        :props="{
+                            value:'id',
+                            label:'name',
+                            emitPath:false,
+                            multiple:true
+                        }"
+                        style="width: 250px; margin-right: 20px;"
+                        clearable>
+                    </el-cascader>
+                    <el-input 
+                        v-model="tableQuery.keyWord"
+                        :prefix-icon="Search" clearable
+                        style="width:250px"
+                        placeholder="产品名称" 
+                        @input="handlePageChange(1)"
+                    />
+                </div>
+                <div>
+                    <el-button type="primary" plain @click="close">返回</el-button>
+                </div>
+            </div>
+            <div class="table">
+                <el-table :data="tableData" @sort-change="handleSortChange" border>
+                    <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==='Price'">
+                            <el-input v-model="scope.row[column.key]" placeholder="请输入数字" />
+                        </template>
+                        <template #default="scope" v-else-if="column.key==='handle'">
+                            <span class="stock" @click="stockProducts(scope.row)">保存并上架</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>
+    </el-card>
+</template>
+
+<style scoped lang="scss">
+.dialog-content {
+    padding: 20px;
+    .top-box {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        padding: 20px 0 20px 0;
+    }
+    .table {
+        padding-bottom: 30px;
+        .stock {
+            cursor:pointer;
+            color: rgba(8, 108, 224, 1);
+        }
+    }
+}
+</style>

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

@@ -0,0 +1,222 @@
+<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 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>