Răsfoiți Sursa

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

leichen 6 luni în urmă
părinte
comite
5a9a3b35a5

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

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

@@ -0,0 +1,83 @@
+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)
+    },
+    /**
+     * 退款详情 
+     * @param {String} params.ProductOrderNo
+     * @returns 
+     */
+    getRefundDetail:params=>{
+        return get('/order/refundDetail',params)
+    },
+    /**
+     * 退款
+     * @param {String} params.ProductOrderNo
+     * @param {String} params.Remark
+     * @returns 
+     */
+    postOrderRefund:params=>{
+        return post('/order/refund',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)
+    }
+};

+ 4 - 0
src/api/system/common.js

@@ -12,5 +12,9 @@ export default{
     // 上传文件
     uploadFile:params=>{
         return post('/report_pdf/uploadPdf',params)
+    },
+    // 上传文件
+    getSysConfig:params=>{
+        return get('/sys/config',params)
     }
 }

+ 2 - 2
src/api/system/message.js

@@ -3,10 +3,10 @@ import { get, post } from "@/api/index";
 export default {
   //系统消息
   list: () => {
-    return get("/sys_message/list", {});
+    return get("/user/message", {});
   },
   // 消息已读
   msgRead: (params) => {
-    return post("/sys_message/read", params);
+    return post("/user/readMessage", params);
   },
 };

+ 3 - 0
src/assets/svg/failure.svg

@@ -0,0 +1,3 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M14 7C14 3.13401 10.866 1.18292e-06 7 0C3.13401 -2.14186e-06 1.18292e-06 3.134 0 7C-2.14186e-06 10.866 3.134 14 7 14C10.866 14 14 10.866 14 7ZM4.67091 3.94754L7.00001 6.29075L9.32912 3.94754L10.0384 4.65251L7.70499 7L10.0383 9.34749L9.32911 10.0525L7.00001 7.70925L4.67092 10.0525L3.96168 9.34749L6.29503 7L3.96167 4.65251L4.67091 3.94754Z" fill="#D54941"/>
+</svg>

+ 10 - 2
src/assets/svg/notification-filled.svg

@@ -1,3 +1,11 @@
-<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M8.49993 2.02746V1H7.49993V2.02746C5.24996 2.27619 3.49993 4.18372 3.49993 6.5V11L2.59993 12.2C2.35272 12.5296 2.58791 13 2.99993 13H5.54994C5.78158 14.1411 6.79045 15 7.99993 15C9.20941 15 10.2183 14.1411 10.4499 13H12.9999C13.412 13 13.6471 12.5296 13.3999 12.2L12.4999 11V6.5C12.4999 4.18372 10.7499 2.27619 8.49993 2.02746Z" fill="white"/>
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_2563_19773)">
+<path d="M17.0965 13.9594L16.0431 12.2232C15.7945 11.8145 15.663 11.3452 15.6631 10.8668V6.91367C15.6631 3.79082 13.1226 1.25 9.99977 1.25C6.87692 1.25 4.3361 3.79082 4.3361 6.91367V10.8668C4.3361 11.3453 4.20466 11.8143 3.95622 12.2232L2.92438 13.9201L2.9027 13.9594C2.73571 14.2941 2.75368 14.6838 2.95055 15.0021C3.14743 15.3201 3.48825 15.51 3.86227 15.51H6.26325C6.52438 17.3381 8.10016 18.7482 9.99958 18.7482C11.8988 18.7482 13.4746 17.3381 13.7357 15.51H16.1371C16.5111 15.51 16.8517 15.3201 17.0484 15.0021C17.2453 14.6842 17.2634 14.2941 17.0965 13.9594ZM9.99977 17.6762C8.69313 17.6762 7.60016 16.7441 7.35075 15.51H12.6486C12.3992 16.7441 11.3064 17.6762 9.99977 17.6762ZM3.86442 14.4379L4.87243 12.7801C5.2228 12.2035 5.40812 11.5417 5.40817 10.867V6.91387C5.40817 4.38203 7.46794 2.32227 9.99977 2.32227C12.5314 2.32227 14.591 4.38203 14.591 6.91387V10.867C14.591 11.542 14.7763 12.2037 15.1271 12.7803L16.1349 14.4379H3.86442Z" fill="black"/>
+<path d="M17.0965 13.9594L16.0431 12.2232C15.7945 11.8145 15.663 11.3452 15.6631 10.8668V6.91367C15.6631 3.79082 13.1226 1.25 9.99977 1.25C6.87692 1.25 4.3361 3.79082 4.3361 6.91367V10.8668C4.3361 11.3453 4.20466 11.8143 3.95622 12.2232L2.92438 13.9201L2.9027 13.9594C2.73571 14.2941 2.75368 14.6838 2.95055 15.0021C3.14743 15.3201 3.48825 15.51 3.86227 15.51H6.26325C6.52438 17.3381 8.10016 18.7482 9.99958 18.7482C11.8988 18.7482 13.4746 17.3381 13.7357 15.51H16.1371C16.5111 15.51 16.8517 15.3201 17.0484 15.0021C17.2453 14.6842 17.2634 14.2941 17.0965 13.9594ZM9.99977 17.6762C8.69313 17.6762 7.60016 16.7441 7.35075 15.51H12.6486C12.3992 16.7441 11.3064 17.6762 9.99977 17.6762ZM3.86442 14.4379L4.87243 12.7801C5.2228 12.2035 5.40812 11.5417 5.40817 10.867V6.91387C5.40817 4.38203 7.46794 2.32227 9.99977 2.32227C12.5314 2.32227 14.591 4.38203 14.591 6.91387V10.867C14.591 11.542 14.7763 12.2037 15.1271 12.7803L16.1349 14.4379H3.86442Z" fill="black"/>
+</g>
+<defs>
+<clipPath id="clip0_2563_19773">
+<rect width="20" height="20" fill="white"/>
+</clipPath>
+</defs>
 </svg>

+ 3 - 0
src/assets/svg/success.svg

@@ -0,0 +1,3 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M7 14C10.866 14 14 10.866 14 7C14 3.13401 10.866 0 7 0C3.13401 0 0 3.13401 0 7C0 10.866 3.13401 14 7 14ZM3.5 7.2065L4.2065 6.5L6 8.293L9.7925 4.5L10.5 5.2075L6 9.707L3.5 7.2065Z" fill="#2BA471"/>
+</svg>

+ 15 - 17
src/layout/components/HeaderWrap.vue

@@ -2,7 +2,6 @@
 import { useLayoutState } from '../hooks/index'
 import { useRoute, useRouter } from 'vue-router'
 import NoticeWrap from './NoticeWrap.vue'
-import {apiSystemMessage} from '@/api/system'
 
 const { menuClose, menuCloseChange } = useLayoutState()
 
@@ -36,14 +35,6 @@ function handleClickBreadcrumb(e){
 }
 
 
-// 阅读消息
-function handleReadNotice(){
-  apiSystemMessage.msgRead().then(res=>{
-    if(res.Ret===200){
-      hasUnRead.value=false
-    }
-  })
-}
 const hasUnRead=ref(false)//是否有未读
 
 const userName=computed(()=>{
@@ -90,14 +81,17 @@ function handleCommand(command){
     </el-breadcrumb>
     <div class="content"></div>
     <!-- 通知 -->
-    <el-popover placement="bottom" :width="500" trigger="click" @show="handleReadNotice" v-if="hasPermission('message:info')">
-      <template #reference>
-        <div :class="['system-notice-box',hasUnRead?'system-notice-box_red':'']">
-          <svg-icon name="notification-filled" size="18px"></svg-icon>
-        </div>
-      </template>
-      <NoticeWrap @change="e=>hasUnRead=e"/>
-    </el-popover>
+     <!-- v-if="hasPermission('message:info')"  @show="handleReadNotice" -->
+    <div class="popover">
+      <el-popover placement="bottom" :width="800" trigger="click">
+        <template #reference>
+          <div :class="['system-notice-box',hasUnRead?'system-notice-box_red':'']">
+            <svg-icon name="notification-filled" size="18px"></svg-icon>
+          </div>
+        </template>
+        <NoticeWrap @change="e=>hasUnRead=e"/>
+      </el-popover>
+    </div>
     <!-- 用户信息 -->
     <el-dropdown @command="handleCommand">
         <div class="system-user-box">
@@ -142,6 +136,10 @@ function handleCommand(command){
   .content {
     flex: 1;
   }
+  .popover{
+    padding-top: 5px;
+    margin-right: 15px;
+  }
   .system-notice-box{
     position: relative;
     &.system-notice-box_red::after{

+ 51 - 10
src/layout/components/NoticeWrap.vue

@@ -5,39 +5,78 @@ import { useRouter } from 'vue-router'
 const router=useRouter()
 
 const emits=defineEmits(['change'])
-
+const tableQuery = reactive({
+    currentPage:1,
+    pageSize:10,
+    totals:0,
+})
 const list = ref([])
 function getMsgList() {
-  apiSystemMessage.list().then(res => {
+  apiSystemMessage.list({
+    CurrentIndex:tableQuery.currentPage,
+    PageSize:tableQuery.pageSize,
+  }).then(res => {
     if (res.Ret === 200) {
       list.value = res.Data.List || []
-      const hasUnRead=list.value.some(item=>!item.IsRead)
+      list.value.map(item=>{
+        // item.isTrue = item.Message.substr(item.Message.length - 5) === '退款成功!' ? true : false
+        item.isTrue = item.Message.search("退款成功") != -1
+      })
+      tableQuery.totals = res.Data.Paging.Totals||0
+      const hasUnRead=list.value.some(item=>item.Status === 'UNREAD')
       emits('change',hasUnRead)
     }
   })
 }
 getMsgList()
 
-function handleGoCustomerDetail(e){
-  router.push('/customer/userDetail?id=' + e.UserId)
+function handlePageChange(page){
+    tableQuery.currentPage = page
+    getMsgList()
 }
 
+function handleGoCustomerDetail(e){
+  handleReadNotice(e.Id)
+  // router.push('/order/orderList')
+}
+// 阅读消息
+function handleReadNotice(id){
+  apiSystemMessage.msgRead({
+    messageId: id
+  }).then(res=>{
+    if(res.Ret===200){
+      router.push('/order/orderList')
+    }
+  })
+}
 
 </script>
 
 <template>
   <div class="notice-wrap">
-    <div class="title">客户到期提醒</div>
+    <div class="title">消息提醒</div>
     <div class="list-wrap">
       <div
-        :class="['item', item.IsRead ? 'item-read' : '']"
+        :class="['item', item.Status !=='UNREAD' ? 'item-read' : '']"
         v-for="item in list"
         :key="item.SysMessageReportId"
         @click="handleGoCustomerDetail(item)"
       >
-        <span style="margin-right: 20px">{{ item.Content }}</span>
-        <span>{{ item.CreateTime }}</span>
+        <svg-icon name="success" size="16px" v-if="item.isTrue"></svg-icon>
+        <svg-icon name="failure" size="16px" v-if="!item.isTrue"></svg-icon>
+        <span style="margin-left: 10px">{{ item.Message }}</span>
+        <span style="margin-left: 20px">{{ item.CreatedTime }}</span>
+        <!-- <div v-html="item.Message"></div> -->
       </div>
+      <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;"
+      />
     </div>
   </div>
 </template>
@@ -45,7 +84,7 @@ function handleGoCustomerDetail(e){
 <style lang="scss" scoped>
 .notice-wrap {
   .title {
-    padding: 20px 0;
+    padding: 15px 0;
     border-bottom: 1px solid #e7e7e7;
   }
   .list-wrap {
@@ -54,6 +93,8 @@ function handleGoCustomerDetail(e){
     .item {
       padding: 10px;
       cursor: pointer;
+      display: flex;
+      align-items: center;
       span {
         display: inline-block;
       }

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

+ 47 - 16
src/views/author/List.vue

@@ -16,8 +16,11 @@ const tableQuery = reactive({
 
 const tableColumns = [
     {label:'研究员名称',key:'Name',},
+    {label:'职位',key:'Position',},
     {label:'简介',key:'Introduction'},
     {label:'照片',key:'HeadImgURL',},
+    {label:'从业资格号',key:'ProfessionalCertificate',},
+    {label:'投资咨询号',key:'InvestmentCertificate',},
     {label:'添加时间',key:'CreatedTime',width:250},
 ]
 let modifyAuthorShow = ref(false)
@@ -76,9 +79,9 @@ async function handleSaveAuthor(){
     try{
         await formRef.value?.validate()
     }catch(e){ return }
-    const {Id,HeadImgURL,Introduction,Name} = curAuthorData.value
+    const {Id,HeadImgURL,Introduction,Name,ProfessionalCertificate,Position,InvestmentCertificate} = curAuthorData.value
     apiAuthor.editAuthor({
-        Id,HeadImgUrl:HeadImgURL,Introduction,Name
+        Id,HeadImgUrl:HeadImgURL,Introduction,Name,ProfessionalCertificate,Position,InvestmentCertificate
     }).then(res=>{
         if(res.Ret!==200) return 
         ElMessage.success('配置成功')
@@ -135,25 +138,40 @@ async function handleSaveAuthor(){
             />
         </div>
     </div>
-    <el-dialog v-model="modifyAuthorShow" title="配置作者" width="646px" draggable>
+    <el-dialog v-model="modifyAuthorShow" title="配置作者" width="60%" draggable>
         <div class="content-wrap">
             <el-form label-width="95px" label-position="left" :rules="rules" ref="formRef" :model="curAuthorData">
                 <el-form-item label="研究员名称">
                     {{ curAuthorData.Name||'-' }}
                 </el-form-item>
-                <el-form-item label="简介" prop="Introduction">
-                    <el-input type="textarea" placeholder="请输入作者简介" v-model="curAuthorData.Introduction"></el-input>
-                </el-form-item>
-                <el-form-item label="照片" prop="HeadImgURL">
-                    <ImageUpload
-                        :imgUrl="curAuthorData.HeadImgURL"
-                        uploadHint="支持jpg、jpeg、png等格式,建议上传宽高比例为1:1的图片"
-                        width="120px"
-                        height="120px"
-                        @upload="handleUploadImg"
-                        @remove="curAuthorData.HeadImgURL=''"
-                    ></ImageUpload>
-                </el-form-item>
+                <div class="content-box">
+                    <div class="content-item">
+                        <el-form-item label="简介" prop="Introduction" class="form-item">
+                            <el-input type="textarea" placeholder="请输入作者简介" :rows="5" v-model="curAuthorData.Introduction"></el-input>
+                        </el-form-item>
+                        <el-form-item label="从业资格号" class="form-item">
+                            <el-input placeholder="请输入作者从业资格号" :rows="5" v-model="curAuthorData.ProfessionalCertificate"></el-input>
+                        </el-form-item>
+                        <el-form-item label="照片" prop="HeadImgURL" class="form-item">
+                            <ImageUpload
+                                :imgUrl="curAuthorData.HeadImgURL"
+                                uploadHint="支持jpg、jpeg、png等格式,建议上传宽高比例为1:1的图片"
+                                width="120px"
+                                height="120px"
+                                @upload="handleUploadImg"
+                                @remove="curAuthorData.HeadImgURL=''"
+                            ></ImageUpload>
+                        </el-form-item>                 
+                    </div>
+                    <div class="content-item">
+                        <el-form-item label="职位" class="form-item">
+                            <el-input placeholder="请输入作者职位" :rows="5" v-model="curAuthorData.Position"></el-input>
+                        </el-form-item>
+                        <el-form-item label="投资咨询号" class="form-item">
+                            <el-input placeholder="请输入作者投资咨询号" :rows="5" v-model="curAuthorData.InvestmentCertificate"></el-input>
+                        </el-form-item>
+                    </div>
+                </div>
             </el-form>
         </div>
         <template #footer>
@@ -174,4 +192,17 @@ async function handleSaveAuthor(){
         margin-top: 25px;
     }
 }
+.content-box {
+    display: flex;
+    justify-content: space-between;
+    .content-item {
+        width: 49%;
+        .form-item {
+            margin-bottom: 15px;
+            :deep(.el-input__wrapper){ 
+            padding:5px 10px;
+        }
+        }
+    }
+}
 </style>

+ 94 - 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 = [
     {
@@ -31,7 +33,9 @@ const tableQuery = reactive({
     sortParam:'',
     sortType:''
 })
+
 const tableData = ref([])
+const show = ref(false)
 function getTableData(){
     apiCustomerUser.getTempCustomList({
         Keyword:tableQuery.keyWord,
@@ -47,6 +51,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 +76,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" v-permission="'tempUser:export'">导出表格</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)" v-permission="'tempUser:readCount'">
+                                {{ 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>

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

@@ -0,0 +1,241 @@
+<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:'CompanyName',
+        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 userRow = 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) {
+    if (!val) {
+        tableQuery.RegisterBeginDate = '';
+        tableQuery.RegisterEndDate = '';
+    } else {
+        tableQuery.RegisterBeginDate = val[0];
+        tableQuery.RegisterEndDate = val[1];
+    }
+    getTableData();
+}
+function userDetails(row) {
+    userRow.value = 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" v-permission="'user:export'">导出表格</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 === 'FollowingGzh'">
+                            <span>{{ scope.row.FollowingGzh ? '是' : '否' }}</span>
+                        </template>
+                        <template #default="scope" v-else-if="column.key === 'ReadCount'">
+                            <span class="ReadCount" @click="Details(scope.row)" v-permission="'user:count'">
+                                {{ scope.row[column.key] }}
+                            </span>
+                        </template>
+                        <template #default="scope" v-else-if="column.key === 'RealName'">
+                            <span class="ReadCount" @click="userDetails(scope.row)" v-permission="'user:details'">
+                                {{ 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" :userRow="userRow"></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>

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

@@ -0,0 +1,192 @@
+<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) {
+    tableQuery.PermissionIds = null
+    tableQuery.ProductType = ''
+    tableQuery.currentPage = 1
+    tableQuery.sortParam = '',
+    tableQuery.sortType = '',
+    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:'SourceTitle',
+        sortable:false
+    },{
+        label:'产品类型',
+        key:'SourceName',
+        sortable:false,
+        widthsty:100
+    },{
+        label:'品种',
+        key:'PermissionNames',
+        sortable:false,
+        widthsty:100
+    },{
+        label:'最近一次点击时间',
+        key:'ClickTime',
+        sortable:true,
+        widthsty:180
+    },{
+        label:'停留时长',
+        key:'ReadDurationMinutes',
+        sortable:true,
+        widthsty:140
+    }
+]
+
+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
+                }"
+                placeholder="请选择品种"
+                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":width="column.widthsty"
+                    :min-width="column.minwidthsty">
+                    <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>

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

@@ -0,0 +1,173 @@
+<script setup>
+const show = defineModel('show', { type: Boolean, default: false })
+const props=defineProps({
+    userRow:{
+        type:Object,
+        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:'OrderID',
+        sortable:false
+    },
+    {
+        label:'商品名称',
+        key:'ProductName',
+        sortable:false
+    },{
+        label:'商品类型',
+        key:'ProductType',
+        sortable:false,
+        widthsty:100
+    },{
+        label:'有效期',
+        key:'ReadCount',
+        sortable:false
+    },{
+        label:'订单状态',
+        key:'Status',
+        sortable:false,
+        widthsty:100
+    },{
+        label:'支付金额',
+        key:'PaymentAmount',
+        sortable:false,
+        widthsty:100
+    },{
+        label:'下单时间',
+        key:'RefundFinishTime',
+        sortable:false
+    },{
+        label:'支付渠道',
+        key:'PaymentWay',
+        sortable:false,
+        widthsty:90
+    }
+]
+
+const tableQuery = reactive({
+    sortParam:'',
+    sortType:'',
+    pageSize:10,
+    currentPage:1,
+    totals:0
+})
+const tableData = ref([])
+function getTableData(){
+    apiOrderConfig.getProductOrderList({
+        TemplateUserId:props.userRow.TemplateUserId,
+        CurrentIndex:tableQuery.currentPage,
+        PageSize:tableQuery.pageSize,
+    }).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="75%"
+    draggable
+    title="用户详情"
+  >
+    <div class="dialog-content">
+        <div class="dialog-content-top">
+            <div class="title">基本信息</div>
+            <div class="info">{{ props.userRow.RealName }}  {{ props.userRow.Mobile }}</div>
+            <div class="title">商品订单</div>
+        </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" :width="column.widthsty"
+                    :min-width="column.minwidthsty">
+                    <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 {
+        // padding-bottom: 10px;
+        color: #333;
+        .title {
+            font-size: 16px;
+            padding-bottom: 10px;
+        }
+        .info {
+            font-size: 14px;
+            margin-bottom: 20px;
+        }
+    }
+    .table {
+        padding-bottom: 30px;
+    }
+}
+
+</style>

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

@@ -0,0 +1,378 @@
+<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'
+import { apiSystemCommon } from '@/api/system'
+
+const tableColumns = [
+    {
+        label:'订单编号',
+        key:'OrderID',
+        sortable:false
+    },
+    {
+        label:'姓名',
+        key:'RealName',
+        sortable:false,
+        widthsty:90,
+    },
+    {
+        label:'手机号',
+        key:'Mobile',
+        sortable:false
+    },
+    {
+        label:'商品名称',
+        key:'ProductName',
+        sortable:false
+    },{
+        label:'商品类型',
+        key:'ProductType',
+        sortable:false,
+        widthsty:90,
+    },{
+        label:'商品价格',
+        key:'TotalAmount',
+        sortable:false,
+        widthsty:110,
+    },{
+        label:'有效期',
+        key:'ValidDuration',
+        sortable:false
+    },{
+        label:'订单状态',
+        key:'Status',
+        sortable:false,
+        widthsty:90,
+    },{
+        label:'支付渠道',
+        key:'PaymentWay',
+        sortable:false,
+        widthsty:90,
+    },{
+        label:'支付金额',
+        key:'PaymentAmount',
+        sortable:false,
+        widthsty:110,
+    },{
+        label:'售后状态',
+        key:'RefundStatus',
+        sortable:false,
+        widthsty:90,
+    },{
+        label:'付款时间',
+        key:'PaymentTime',
+        sortable:false
+    },{
+        label:'下单时间',
+        key:'CreatedTime',
+        sortable:false
+    },
+    {
+        label:'操作',
+        key:'handle',
+        align: 'center',
+        widthsty:100,
+    }
+]
+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)
+const isRefund = 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 refundRow = ref({})
+function getLableList(){
+    apiSystemCommon.getSysConfig({
+        ConfigKey:'paymentWay'
+    }).then(res=>{
+        if(res.Ret!==200) return
+        labelOptions.value = res.Data.paymentWay.map(item => {
+            const key = Object.keys(item)[0]; // 获取支付方式的中文名称
+            const value = item[key]; // 获取支付方式的英文名称
+            return {
+                value: value, // 将英文名称设置为 value
+                label: key // 将中文名称设置为 label
+            };
+        })
+    })
+}
+
+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() {
+    if (tableQuery.orderStatus !== 'refund' && tableQuery.refundStatus !== '') {
+        tableQuery.refundStatus = '';
+    }
+    getTableData();
+}
+
+/* 下载数据 */
+async function downloadExcel() {
+    const res = await apiOrderConfig.getExportProductOrder(
+        {
+            KeyWord:tableQuery.keyWord,
+            SortType:tableQuery.sortType,
+            PaymentDate:tableQuery.paymentDate,
+            PaymentWay:tableQuery.paymentWay,
+            CreatedDate:tableQuery.productType,
+            ProductType:tableQuery.createdDate,
+            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); // 释放标
+}
+
+function operation (handle, row) {
+    refundRow.value = row
+    switch (handle) {
+        case 'details':
+            showUserDialog.value = true;
+            isRefund.value = false
+        break
+        case 'refund':
+            showUserDialog.value = true;
+            isRefund.value = true
+        break;
+        default:
+        return false;
+    }
+}
+
+function success() {
+    showUserDialog.value = false;
+    getTableData()
+}
+</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.paymentWay" clearable @change="handleSelectChange()" placeholder="支付渠道" style="width: 150px; margin-right: 20px;">
+                        <el-option
+                            v-for="item in labelOptions"
+                            :key="item.value"
+                            :label="item.label"
+                            :value="item.value">
+                        </el-option>
+                    </el-select>
+                    <el-select v-model="tableQuery.refundStatus" clearable @change="handleSelectChange()" placeholder="售后状态" style="width: 150px; margin-right: 20px;" :disabled="tableQuery.orderStatus !== 'refund'">
+                        <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" v-permission="'order:export'">导出表格</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":width="column.widthsty"
+                        :min-width="column.minwidthsty">
+                        <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 !== '已支付' && scope.row.RefundStatus !== '退款成功'">-</span>
+                            <span class="edit" v-if="scope.row.Status === '售后' && scope.row.RefundStatus === '退款成功'"  @click="operation('details', scope.row)" v-permission="'order:refundDetails'">退款详情</span>
+                            <span class="edit" v-if="scope.row.Status === '已支付'" @click="operation('refund', scope.row)" v-permission="'order:refund'">退款</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" :isRefund="isRefund" :row="refundRow" @success="success"></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>

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

@@ -0,0 +1,294 @@
+<script setup>
+import { ref, reactive } from 'vue'
+import { Search } from '@element-plus/icons-vue'
+import {apiOrderConfig} from '@/api/order'
+import { apiSystemCommon } from '@/api/system'
+
+const tableColumns = [
+    {
+        label:'支付单号',
+        key:'TransactionID',
+        sortable:false
+    },
+    {
+        label:'订单编号',
+        key:'ProductOrderID',
+        sortable:false
+    },
+    {
+        label:'姓名',
+        key:'RealName',
+        sortable:false,
+        widthsty:80,
+    },
+    {
+        label:'手机号',
+        key:'Mobile',
+        sortable:false
+    },
+    {
+        label:'商品名称',
+        key:'ProductName',
+        sortable:false
+    },{
+        label:'支付金额',
+        key:'Amount',
+        sortable:false,
+        widthsty:100,
+    },{
+        label:'支付状态',
+        key:'PaymentStatus',
+        sortable:false,
+        widthsty:100,
+    },{
+        label:'支付渠道',
+        key:'PaymentWay',
+        sortable:false,
+        widthsty:90,
+    },{
+        label:'支付账号',
+        key:'ReadCount',
+        sortable:false
+    },{
+        label:'收款方',
+        key:'MerchantID',
+        sortable:false,
+        widthsty:120,
+    },{
+        label:'完成支付时间',
+        key:'DealTime',
+        sortable:false
+    },{
+        label:'创建时间',
+        key:'CreatedTime',
+        sortable:false
+    }
+]
+const orderStatusList = ref([
+    {
+        value: 'pending',
+        label: '待支付'
+    }, {
+        value: 'done',
+        label: '已支付'
+    }, {
+        value: 'failed',
+        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(){
+    apiSystemCommon.getSysConfig({
+        ConfigKey:'paymentWay'
+    }).then(res=>{
+        if(res.Ret!==200) return
+        labelOptions.value = res.Data.paymentWay.map(item => {
+            const key = Object.keys(item)[0]; // 获取支付方式的中文名称
+            const value = item[key]; // 获取支付方式的英文名称
+            return {
+                value: value, // 将英文名称设置为 value
+                label: key // 将中文名称设置为 label
+            };
+        })
+    })
+}
+
+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.paymentWay" clearable @change="handleSelectChange()" placeholder="支付渠道" style="width: 150px; margin-right: 20px;">
+                        <el-option
+                            v-for="item in labelOptions"
+                            :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" v-permission="'payment:export'">导出表格</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" :width="column.widthsty"
+                        :min-width="column.minwidthsty">
+                        <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>

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

@@ -0,0 +1,237 @@
+<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:'TransactionID',
+        sortable:false
+    },
+    {
+        label:'订单编号',
+        key:'ProductOrderID',
+        sortable:false
+    },
+    {
+        label:'姓名',
+        key:'RealName',
+        sortable:false,
+        widthsty:80,
+    },
+    {
+        label:'手机号',
+        key:'Mobile',
+        sortable:false
+    },
+    {
+        label:'商品名称',
+        key:'ProductName',
+        sortable:false
+    },{
+        label:'支付金额',
+        key:'Amount',
+        sortable:false
+    },{
+        label:'支付状态',
+        key:'PaymentStatus',
+        sortable:false,
+        widthsty:90,
+    },{
+        label:'支付渠道',
+        key:'PaymentWay',
+        sortable:false,
+        widthsty:90,
+    },{
+        label:'支付账号',
+        key:'ReadCount',
+        sortable:false
+    },{
+        label:'收款方',
+        key:'MerchantID',
+        sortable:false,
+        widthsty:100,
+    },{
+        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" v-permission="'refund:export'">导出表格</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" :width="column.widthsty"
+                        :min-width="column.minwidthsty">
+                        <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>

+ 137 - 0
src/views/order/components/UserDialog.vue

@@ -0,0 +1,137 @@
+<script setup>
+import { ref, reactive } from 'vue'
+import {apiOrderConfig} from '@/api/order'
+import { InfoFilled } from '@element-plus/icons-vue'
+const show = defineModel('show', { type: Boolean, default: false })
+const props=defineProps({
+    isRefund:{
+        type:Boolean,
+        default:false
+    },
+    row:{
+        type:Object,
+        default:()=>{}
+    },
+})
+const emits = defineEmits(["success"])
+emits("success")
+
+watch(() => props.show, (newval) => {
+  if (newval) {
+    if (!props.isRefund) {
+        getRefundData()
+    } else {
+        refundInfo.value = props.row||{}
+    }
+  }
+})
+
+const refundInfo = ref({})
+const remark = ref('')
+function getRefundData(){
+    apiOrderConfig.getRefundDetail({
+        ProductOrderNo:props.row.OrderID,
+    }).then(res=>{
+        if(res.Ret!==200) return
+        refundInfo.value = res.Data||{}
+        refundInfo.value.PaymentAmount = res.Data.RefundAmount
+        remark.value = refundInfo.value.Remark||''
+    })
+}
+
+function confirm() {
+    remark
+    if (!remark.value) {
+        ElMessage.error('请输入退款说明')
+        return
+    }
+    apiOrderConfig.postOrderRefund({
+        ProductOrderNo:props.row.OrderID,
+        Remark:remark.value
+    }).then(res=>{
+        if(res.Ret!==200) return
+        emits("success")
+    })
+}
+</script>
+
+<template>
+  <el-dialog
+    v-model="show"
+    :close-on-click-modal="false"
+    :modal-append-to-body="false"
+    width="30%"
+    draggable
+    :title="props.isRefund ? '退款' : '退款详情'"
+  >
+    <div class="dialog-top">
+        <div class="dialog-content-top">
+            <div class="dialog-top-item">
+                退款用户:{{ refundInfo.RealName }}
+            </div>
+            <div class="dialog-top-item">
+                退回账号:{{  }}
+            </div>
+            <div class="dialog-top-item">
+                退款金额:{{ refundInfo.PaymentAmount }}
+            </div>
+        </div>
+        <div class="dialog-content">
+            <div class="dialog-content-item">
+                退款说明
+            </div>
+            <el-input
+                type="textarea"
+                :rows="5"
+                placeholder="请输入内容"
+                v-model="remark">
+            </el-input>
+        </div>
+    </div>
+    <div class="dialog-footer" v-if="props.isRefund">
+        <el-button @click="show = false">取 消</el-button>
+        <div class="button">
+            <el-button type="primary" @click="confirm()" style="margin-right: 10px;">确定</el-button>
+            <el-tooltip
+                class="box-item"
+                effect="dark"
+                content="原支付渠道退款"
+                placement="top"
+            >
+                <el-icon><InfoFilled /></el-icon>
+            </el-tooltip>
+        </div>
+    </div>
+  </el-dialog>
+</template>
+
+<style scoped lang="scss">
+.dialog-top {
+    padding-bottom: 40px;
+    .dialog-content-top {
+        display: flex;
+        justify-content: flex-start;
+        padding-bottom: 30px;
+        .dialog-top-item {
+            flex: 1;
+            padding-right: 20px;
+        }
+    }
+    .dialog-content{
+        .dialog-content-item {
+            padding-bottom: 10px;
+        }
+    }
+}
+.dialog-footer {
+    display: flex;
+    justify-content: space-around;
+    padding-bottom: 40px;
+    .button {
+        display: flex;
+        justify-content: space-around;
+        align-items: center;
+    }
+}
+
+</style>

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

@@ -0,0 +1,317 @@
+<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:'Creator',
+        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:'',
+    SortColumn:'created_time',
+    sortType:'desc',
+})
+
+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,
+        SortType:tableQuery.sortType,
+        SortColumn:tableQuery.SortColumn,
+    }).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}){
+    const propMap = {
+        'PublishedTime':'created_time',
+        'UpdatedTime':'updated_time',
+    }
+   tableQuery.SortColumn = propMap[prop]
+    tableQuery.sortType = order==='ascending' ? 'asc': 'desc'
+    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')" v-permission="'package: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)" v-permission="'package:stock'">上架</span>
+                            <span class="edit" v-if="scope.row.SaleStatus === '已上架'"  @click="operation('delist', scope.row)" v-permission="'package:delist'">下架</span>
+                            <span class="edit" v-if="scope.row.SaleStatus !== '已上架'" @click="operation('edit', scope.row)" v-permission="'package:edit'">编辑</span>
+                            <span class="delete" v-if="scope.row.SaleStatus !== '已上架'" @click="operation('delete', scope.row)" v-permission="'package:delete'">删除</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>

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

@@ -0,0 +1,311 @@
+<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:'Creator',
+        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:'',
+    SortColumn:'created_time',
+    sortType:'desc',
+})
+
+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,
+        SortType:tableQuery.sortType,
+        SortColumn:tableQuery.SortColumn,
+    }).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}){
+    const propMap = {
+        'PublishedTime':'created_time',
+        'UpdatedTime':'updated_time',
+    }
+    tableQuery.SortColumn = propMap[prop]
+    tableQuery.sortType = order==='ascending' ? 'asc': 'desc'
+    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')" v-permission="'single:add'">添加单品</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)" v-permission="'single:stock'">上架</span>
+                            <span class="edit" v-if="scope.row.SaleStatus === '已上架'"  @click="operation('delist', scope.row)" v-permission="'single:delist'">下架</span>
+                            <span class="edit" v-if="scope.row.SaleStatus !== '已上架'" @click="operation('edit', scope.row)" v-permission="'single:edit'">编辑</span>
+                            <span class="delete" v-if="scope.row.SaleStatus !== '已上架'" @click="operation('delete', scope.row)" v-permission="'single:delete'">删除</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>

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

@@ -0,0 +1,221 @@
+<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:'PermissionNames',
+        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 handleSelectType(type){
+    tableQuery.keyWord = ''
+    tableQuery.currentPage = 1
+    tableQuery.PermissionIds = []
+    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': row.ProductType === '视频'?'video':'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="handleSelectType()">
+                    <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>

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

@@ -0,0 +1,233 @@
+<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 rules = {
+  SourceId:[{required:true,message:'请选择品种'}],
+  Price:[{required:true,message:'请输入价格'}],
+  ProductName:[{required:true,message:'请输入套餐名称'}],
+  ValidDays:[{required:true,message:'请输入有效时长'}],
+}
+const emits = defineEmits(["close"])
+const formRef = ref(null)
+const packageData = reactive({
+    SourceId:null,
+    Type:'',
+    ProductName:'',
+    CoverSrc:'',
+    ValidDays:null,
+    Price:'',
+    Description:'',
+    RiskLevel:'',
+    Id:null
+})
+watch(show,(newval)=>{
+  if(newval){
+    if (!props.isAdd) {
+      formRef.value?.clearValidate()
+      packageData.SourceId = [packageData.SourceId] || []
+      Object.assign(packageData,props.productInfo)
+    }
+  }else{
+    Object.assign(packageData,{
+      SourceId:null,
+      Type:'',
+      ProductName:'',
+      CoverSrc:'',
+      ValidDays:null,
+      Price:'',
+      Description:'',
+      RiskLevel:'',
+      Id:null
+    })
+    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() {
+  try{
+  await formRef.value?.validate()
+  }catch(e){ return }
+  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 || '',
+    ProductId:packageData.Id || null
+  }
+  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" :model="packageData" class="form" :rules="rules">
+        <div class="left">
+          <el-form-item label="品种" prop="SourceId" :label-width="150">
+            <!-- <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="请选择品种"
+              :disabled="!isAdd"
+              clearable>
+            </el-cascader>
+          </el-form-item>
+          <el-form-item label="套餐名称" prop="ProductName" :label-width="150">
+            <el-input v-model="packageData.ProductName" placeholder="请输入套餐名称"/>
+          </el-form-item>
+          <el-form-item label="商品价格(元)" prop="Price" :label-width="150">
+            <el-input v-model="packageData.Price" placeholder="请输入价格" @keyup="handleValidPrice()"/>
+          </el-form-item>
+          <el-form-item label="有效时长(天)" prop="ValidDays" :label-width="150">
+            <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>

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

@@ -0,0 +1,78 @@
+<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.SourceId,
+    Type:props.productInfo.ProductType === '报告'?'report':props.productInfo.ProductType === '音频' ? 'audio' : 'video',
+    Price:props.productInfo.Price,
+    ProductId:props.productInfo.Id,
+  })
+  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>