Преглед на файлове

Merge branch 'master' of http://1.14.104.71:10880/root/ylrz_his_scrm_adminUI

caoliqin преди 6 дни
родител
ревизия
708568097a
променени са 30 файла, в които са добавени 2735 реда и са изтрити 105 реда
  1. 2 1
      package.json
  2. 1 0
      public/index.html
  3. 18 0
      src/api/company/companyConfig.js
  4. 18 0
      src/api/course/userCourseComment.js
  5. 78 0
      src/api/crm/customerProperty.js
  6. 47 0
      src/api/crm/propertyTemplate.js
  7. 9 0
      src/api/his/courserCoupon.js
  8. 9 0
      src/api/his/integralGoods.js
  9. 84 0
      src/api/qw/companySession.js
  10. 9 0
      src/api/qw/user.js
  11. 9 1
      src/api/system/config.js
  12. 13 0
      src/router/index.js
  13. 54 2
      src/views/components/course/userCourseCatalogDetails.vue
  14. 97 3
      src/views/components/course/userCourseCatalogDetailsZM.vue
  15. 246 0
      src/views/components/course/userCourseSectionComment.vue
  16. 29 9
      src/views/course/courseRedPacketLog/index.vue
  17. 30 1
      src/views/course/courseWatchLog/index.vue
  18. 2 2
      src/views/course/courseWatchLog/myCourseWatchLog.vue
  19. 25 2
      src/views/course/courseWatchLog/watchLog.vue
  20. 388 0
      src/views/crm/components/AiTagPanel.vue
  21. 19 6
      src/views/crm/components/customerDetails.vue
  22. 392 0
      src/views/crm/propertyTemplate/index.vue
  23. 179 0
      src/views/his/company/index.vue
  24. 149 73
      src/views/his/courserCoupon/index.vue
  25. 99 2
      src/views/his/integralGoods/index.vue
  26. 393 0
      src/views/qw/companySession/ConversationPanel.vue
  27. 244 0
      src/views/qw/companySession/index.vue
  28. 36 3
      src/views/statistics/section/inline.vue
  29. 55 0
      src/views/system/config/config.vue
  30. 1 0
      vue.config.js

+ 2 - 1
package.json

@@ -90,6 +90,7 @@
     "@huaweicloud/huaweicloud-sdk-core": "^3.1.105",
     "@huaweicloud/huaweicloud-sdk-vod": "^3.1.105",
     "@riophae/vue-treeselect": "0.4.0",
+    "@wecom/jssdk": "^2.4.0",
     "axios": "0.21.0",
     "chart.js": "^2.9.4",
     "clipboard": "2.0.6",
@@ -136,8 +137,8 @@
     "eslint": "7.15.0",
     "eslint-plugin-vue": "7.2.0",
     "lint-staged": "10.5.3",
-    "node-sass": "4.14.1",
     "runjs": "4.4.2",
+    "sass": "^1.98.0",
     "sass-loader": "8.0.2",
     "script-ext-html-webpack-plugin": "2.1.5",
     "svg-sprite-loader": "5.1.1",

+ 1 - 0
public/index.html

@@ -5,6 +5,7 @@
     <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
     <meta name="renderer" content="webkit">
     <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
+    <script src="https://open.work.weixin.qq.com/wwopen/js/jwxwork-1.0.0.js"></script>
     <!-- <link rel="icon" href="<%= BASE_URL %>favicon.ico"> -->
     <!-- <title><%= webpackConfig.name %></title> -->
     <!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->

+ 18 - 0
src/api/company/companyConfig.js

@@ -33,6 +33,24 @@ export function addCompanyConfig(data) {
   })
 }
 
+export function editCompanyConfig(data) {
+  return request({
+    url: '/company/companyConfig/editCompanyConfig',
+    method: 'post',
+    data: data
+  })
+}
+
+// 查询参数配置详细
+export function getCompanyConfigKey(configKey,companyId) {
+  return request({
+    url: '/company/companyConfig/getCompanyConfigKey/' + configKey+'/'+companyId,
+    method: 'get'
+  })
+}
+
+
+
 // 修改参数配置
 export function updateCompanyConfig(data) {
   return request({

+ 18 - 0
src/api/course/userCourseComment.js

@@ -83,4 +83,22 @@ export function listFeaturedComments(courseId, videoId, query) {
     method: 'get',
     params: { videoId: videoId || undefined, ...query }
   })
+}
+
+// 查询指定小节下的用户留言(排除精选留言)
+export function listSectionUserComments(query) {
+  return request({
+    url: '/course/userCourseComment/sectionUserComments',
+    method: 'get',
+    params: query
+  })
+}
+
+// 修改用户留言可见范围:visibleAll 0自己可见 1全部人可见
+export function updateCommentVisibleAll(data) {
+  return request({
+    url: '/course/userCourseComment/updateVisibleAll',
+    method: 'put',
+    data: data
+  })
 }

+ 78 - 0
src/api/crm/customerProperty.js

@@ -0,0 +1,78 @@
+import request from '@/utils/request'
+
+export function listByCustomerId(customerId) {
+  return request({
+    url: '/crm/customerProperty/list/' + customerId,
+    method: 'get'
+  })
+}
+
+export function getById(id) {
+  return request({
+    url: '/crm/customerProperty/' + id,
+    method: 'get'
+  })
+}
+
+export function add(data) {
+  return request({
+    url: '/crm/customerProperty/add',
+    method: 'post',
+    data: data
+  })
+}
+
+export function addOrUpdate(data) {
+  return request({
+    url: '/crm/customerProperty/addOrUpdate',
+    method: 'post',
+    data: data
+  })
+}
+
+export function addByTemplateId(customerId, templateId, propertyValue) {
+  return request({
+    url: '/crm/customerProperty/addByTemplateId',
+    method: 'post',
+    params: { customerId, templateId, propertyValue }
+  })
+}
+
+export function addOrUpdateByTemplateId(customerId, templateId, propertyValue) {
+  return request({
+    url: '/crm/customerProperty/addOrUpdateByTemplateId',
+    method: 'post',
+    params: { customerId, templateId, propertyValue }
+  })
+}
+
+export function batchAddByTemplateIds(customerId, propertyMap) {
+  return request({
+    url: '/crm/customerProperty/batchAddByTemplateIds/' + customerId,
+    method: 'post',
+    data: propertyMap
+  })
+}
+
+export function update(data) {
+  return request({
+    url: '/crm/customerProperty',
+    method: 'put',
+    data: data
+  })
+}
+
+export function del(ids) {
+  return request({
+    url: '/crm/customerProperty/' + ids,
+    method: 'delete'
+  })
+}
+
+export function deleteByPropertyId(customerId, propertyId) {
+  return request({
+    url: '/crm/customerProperty/deleteByPropertyId',
+    method: 'delete',
+    params: { customerId, propertyId }
+  })
+}

+ 47 - 0
src/api/crm/propertyTemplate.js

@@ -0,0 +1,47 @@
+import request from '@/utils/request'
+
+export function listPropertyTemplate(query) {
+  return request({
+    url: '/crm/customerPropertyTemplate/list',
+    method: 'get',
+    params: query
+  })
+}
+
+export function getPropertyTemplate(id) {
+  return request({
+    url: '/crm/customerPropertyTemplate/' + id,
+    method: 'get'
+  })
+}
+
+export function addPropertyTemplate(data) {
+  return request({
+    url: '/crm/customerPropertyTemplate',
+    method: 'post',
+    data: data
+  })
+}
+
+export function updatePropertyTemplate(data) {
+  return request({
+    url: '/crm/customerPropertyTemplate',
+    method: 'put',
+    data: data
+  })
+}
+
+export function delPropertyTemplate(id) {
+  return request({
+    url: '/crm/customerPropertyTemplate/' + id,
+    method: 'delete'
+  })
+}
+
+export function exportPropertyTemplate(query) {
+  return request({
+    url: '/crm/customerPropertyTemplate/export',
+    method: 'get',
+    params: query
+  })
+}

+ 9 - 0
src/api/his/courserCoupon.js

@@ -50,4 +50,13 @@ export function exportCourserCoupon(query) {
     method: 'get',
     params: query
   })
+}
+
+
+// 课程优惠券选项列表
+export function options() {
+  return request({
+    url: '/his/courserCoupon/options',
+    method: 'get'
+  })
 }

+ 9 - 0
src/api/his/integralGoods.js

@@ -65,3 +65,12 @@ export function getChooseIntegralGoodsList(query) {
     params: query
   })
 }
+
+// 批量修改积分商品所需积分和支付金额
+export function batchUpdateIntegralGoods(data) {
+  return request({
+    url: '/his/integralGoods/batchUpdateIntegralGoods',
+    method: 'post',
+    data: data
+  })
+}

+ 84 - 0
src/api/qw/companySession.js

@@ -0,0 +1,84 @@
+// src/api/qw/companySession.js
+// 统一封装企微会话相关接口
+
+import request from '@/utils/request';
+
+// ---------- 登录 & 签名(走 SCRM 后端) ----------
+
+/**
+ * 企微扫码登录(用 code 换取用户信息)
+ * @param {object} param { code: string }
+ * @returns {Promise}
+ */
+export function qwLogin(param) {
+  return request({
+    url: '/weChatSpace/login',
+    method: 'post',
+    data: param
+  });
+}
+
+/**
+ * 获取 agentConfig 签名
+ * @param {object} query { url: string }
+ * @returns {Promise}
+ */
+export function qwSignature(query) {
+  return request({
+    url: '/weChatSpace/getAgentConfigSignature',
+    method: 'get',
+    params: query
+  });
+}
+
+// ----------获取企微专区会话配置 ----------
+export function getQwSessionConfig() {
+  return request({
+    url: '/weChatSpace/getQwSessionConfig',
+    method: 'get'
+  });
+}
+
+
+// ---------- 会话记录(暂走测试服务器代理) ----------
+
+/**
+ * 获取会话记录列表
+ * @param {object} params
+ * @returns {Promise}
+ */
+// export async function qwConversations({ seq = 0, limit = 50, customerId, staffUserId } = {}) {
+//   const BASE = '/wecom-api';
+//   let url = `${BASE}/api/conversations?seq=${seq}&limit=${limit}&customerId=${customerId}`;
+//   if (staffUserId) {
+//     url += `&staffUserId=${staffUserId}`;
+//   }
+//   const res = await fetch(url);
+//   const data = await res.json();
+//   if (data.errcode !== 0) {
+//     throw new Error(data.errmsg || '获取会话失败');
+//   }
+//   return data;
+// }
+
+// 改用 request 调用中转接口
+export async function qwConversations(params = {}) {
+  const res = await request({
+    url: '/weChatSpace/conversations',
+    method: 'get',
+    params: {
+      seq: params.seq || 0,
+      limit: params.limit || 50,
+      proxy: params.proxy || 0,
+      timeout: params.timeout || 30,
+      customerId: params.customerId,
+      staffUserId: params.staffUserId
+    }
+  });
+  const resp = res.data || res;
+  if (resp.errcode !== 0) {
+    throw new Error(resp.errmsg || '获取会话失败');
+  }
+  return resp;
+}
+

+ 9 - 0
src/api/qw/user.js

@@ -33,6 +33,15 @@ export function userList(query) {
     params: query
   })
 }
+
+// 查询全部企微用户列表
+export function listAllQwUserList(query) {
+  return request({
+    url: '/qw/user/listAllQwUserList',
+    method: 'get',
+    params: query
+  })
+}
 export function qwUserList(id) {
   return request({
     url: '/qw/user/qwUserList/' + id,

+ 9 - 1
src/api/system/config.js

@@ -23,6 +23,14 @@ export function getConfigByKey(configKey) {
     method: 'get'
   })
 }
+
+export function getCourseConfigByRewardType() {
+  return request({
+    url: '/system/config/getCourseConfigByRewardType',
+    method: 'get'
+  })
+}
+
 // 根据参数键名查询参数值
 export function getConfigKey(configKey) {
   return request({
@@ -96,4 +104,4 @@ export function getGatewayList(query) {
     method: 'get',
     params: query
   })
-}
+}

+ 13 - 0
src/router/index.js

@@ -264,6 +264,19 @@ export const constantRoutes = [
     }
   ]
   },
+  {
+    path: '/qw/companySession',
+    component: Layout,
+    meta: { title: '企微会话', icon: 'chat' },
+    children: [
+      {
+        path: '',
+        component: () => import('@/views/qw/companySession/index'),
+        name: 'CompanySession',
+        meta: { title: '企微会话' }
+      }
+    ]
+  }
 ]
 
 export default new Router({

+ 54 - 2
src/views/components/course/userCourseCatalogDetails.vue

@@ -344,6 +344,16 @@
             <el-radio :label="1">下架</el-radio>
           </el-radio-group>
         </el-form-item>
+        <el-form-item label="课程优惠券" prop="courseCouponId">
+          <el-select v-model="form.courseCouponId" clearable placeholder="请选择课程优惠券">
+            <el-option
+              v-for="item in courseCouponList"
+              :key="item.id"
+              :label="item.title"
+              :value="item.id">
+            </el-option>
+          </el-select>
+        </el-form-item>
         <el-form-item label="商品选择" v-if="form.isProduct === 1">
           <el-button size="small" type="primary" @click="chooseCourseProduct">选取商品</el-button>
           <el-table border width="100%" style="margin-top:5px;" :data="form.courseProducts">
@@ -414,6 +424,7 @@
         </el-form-item>
         <el-form-item label="精选留言">
           <el-button size="small" type="primary" @click="openCommentImportDialog">导入留言</el-button>
+          <el-button size="small" style="margin-left: 8px;" @click="openUserCommentDialog">用户留言</el-button>
         </el-form-item>
       </el-form>
       <div slot="footer" class="dialog-footer">
@@ -601,6 +612,15 @@
                             v-if="commentDialog.open">
       </course-watch-comment>
     </el-dialog>
+    <el-dialog :title="userCommentDialog.title" :visible.sync="userCommentDialog.open" width="1000px" append-to-body
+               :close-on-click-modal="false" @opened="onUserCommentDialogOpened">
+      <user-course-section-comment
+        v-if="userCommentDialog.open"
+        ref="userCourseSectionComment"
+        :course-id="userCommentDialog.courseId"
+        :video-id="userCommentDialog.videoId"
+      />
+    </el-dialog>
 
 
     <el-dialog title="修改课节排序" :visible.sync="openVideoSort" style="width: 1600px;" append-to-body>
@@ -689,14 +709,16 @@ import VideoUpload from "@/components/VideoUpload/index.vue";
 import {listVideoResource, listPublicVideoResource} from '@/api/course/videoResource';
 import {getByIds} from '@/api/course/courseQuestionBank'
 import CourseWatchComment from "./courseWatchComment.vue";
+import UserCourseSectionComment from "./userCourseSectionComment.vue";
 import {getCateListByPid, getCatePidList, getPublicCateListByPid, getPublicCatePidList} from '@/api/course/userCourseCategory'
 import {downloadCommentImportTemplate, importComments, listFeaturedComments, delUserCourseComment} from '@/api/course/userCourseComment'
 import draggable from 'vuedraggable'
 import { getConfigByKey } from '@/api/system/config'
+import { options as courserCouponOptions} from "@/api/his/courserCoupon";
 
 export default {
   name: "userCourseCatalog",
-  components: {VideoUpload, QuestionBank, CourseWatchComment, CourseProduct, draggable},
+  components: {VideoUpload, QuestionBank, CourseWatchComment, UserCourseSectionComment, CourseProduct, draggable},
   props: {
     /** 为 true 时「视频库选择」使用公域分类 + 公域素材接口;默认 false 走原 list / 原分类下拉 */
     usePublicVideoLibrary: {
@@ -716,6 +738,7 @@ export default {
   },
   data() {
     return {
+      courseCouponList: [],
       duration: null,
       packageList: [],
       //课题
@@ -851,6 +874,12 @@ export default {
         videoId: null,
         title: ""
       },
+      userCommentDialog: {
+        open: false,
+        courseId: null,
+        videoId: null,
+        title: ""
+      },
       enableRandomRedPacket:false,
       // 批量修改封面
       batchEditCoverDialog: {
@@ -896,7 +925,13 @@ export default {
         }
     }).catch(res=>{
 
-    })
+    });
+    courserCouponOptions().then(res=>{ 
+      this.courseCouponList = res.data;
+    }).catch(res=>{
+
+    });
+    
   },
   methods: {
     getPickerOptions() {
@@ -1740,6 +1775,23 @@ export default {
       return isLt10M && isImage;
     },
     // 打开精选留言导入弹窗
+    openUserCommentDialog() {
+      if (!this.form.courseId || !this.form.videoId) {
+        this.$message.warning('请先保存课节后再查看用户留言')
+        return
+      }
+      this.userCommentDialog.courseId = this.form.courseId
+      this.userCommentDialog.videoId = this.form.videoId
+      this.userCommentDialog.title = `用户留言 - ${this.form.title || ''}`
+      this.userCommentDialog.open = true
+    },
+    onUserCommentDialogOpened() {
+      this.$nextTick(() => {
+        if (this.$refs.userCourseSectionComment) {
+          this.$refs.userCourseSectionComment.handleQuery()
+        }
+      })
+    },
     openCommentImportDialog() {
       this.commentImportDialog.visible = true;
       this.commentImportDialog.file = null;

+ 97 - 3
src/views/components/course/userCourseCatalogDetailsZM.vue

@@ -389,11 +389,25 @@
                 ></el-time-picker>
               </template>
             </el-table-column>
-            <el-table-column label="操作" align="center" width="100px" fixed="right">
+            <el-table-column label="热卖标签" align="center" prop="hotSaleTags" min-width="140">
+              <template slot-scope="scope">
+                <el-tag
+                  v-for="(tag, tagIdx) in getHotSaleTagList(scope.row)"
+                  :key="tagIdx"
+                  size="mini"
+                  type="danger"
+                  style="margin: 2px;"
+                >{{ tag }}</el-tag>
+              </template>
+            </el-table-column>
+            <el-table-column label="操作" align="center" width="160px" fixed="right">
               <template slot-scope="scope">
                 <el-button size="mini" type="text" icon="el-icon-delete"
                            @click="handleCourseProductDelete(scope.row)">删除
                 </el-button>
+                <el-button size="mini" type="text" icon="el-icon-price-tag"
+                           @click="handleOpenHotSaleTagDialog(scope.row, scope.$index)">添加热卖标签
+                </el-button>
               </template>
             </el-table-column>
           </el-table>
@@ -426,6 +440,7 @@
         </el-form-item>
         <el-form-item label="精选留言">
           <el-button size="small" type="primary" @click="openCommentImportDialog">导入留言</el-button>
+          <el-button size="small" style="margin-left: 8px;" @click="openUserCommentDialog">用户留言</el-button>
         </el-form-item>
       </el-form>
       <div slot="footer" class="dialog-footer">
@@ -492,6 +507,21 @@
     <el-dialog :title="courseProduct.title" :visible.sync="courseProduct.open" width="1000px" append-to-body>
       <course-product ref="courseProduct" @courseProductResult="courseProductResult"></course-product>
     </el-dialog>
+    <el-dialog title="热卖标签" :visible.sync="hotSaleTagDialog.visible" width="480px" append-to-body>
+      <p v-if="hotSaleTagDialog.productName" style="margin-bottom: 10px; color: #606266;">
+        商品:{{ hotSaleTagDialog.productName }}
+      </p>
+      <el-input
+        v-model="hotSaleTagDialog.tagInput"
+        type="textarea"
+        :rows="3"
+        placeholder="多个标签用逗号分隔;留空表示清除标签"
+      />
+      <template slot="footer">
+        <el-button @click="hotSaleTagDialog.visible = false">取 消</el-button>
+        <el-button type="primary" @click="confirmHotSaleTag">确 定</el-button>
+      </template>
+    </el-dialog>
     <el-dialog title="视频库选择" :visible.sync="addBatchData.open" width="900px" append-to-body>
       <!-- 搜索条件 -->
       <el-form :inline="true" :model="addBatchData.queryParams" class="library-search">
@@ -581,6 +611,15 @@
                             v-if="commentDialog.open">
       </course-watch-comment>
     </el-dialog>
+    <el-dialog :title="userCommentDialog.title" :visible.sync="userCommentDialog.open" width="1000px" append-to-body
+               :close-on-click-modal="false" @opened="onUserCommentDialogOpened">
+      <user-course-section-comment
+        v-if="userCommentDialog.open"
+        ref="userCourseSectionComment"
+        :course-id="userCommentDialog.courseId"
+        :video-id="userCommentDialog.videoId"
+      />
+    </el-dialog>
 
 
     <el-dialog title="修改课节排序" :visible.sync="openVideoSort" style="width: 1600px;" append-to-body>
@@ -645,6 +684,7 @@ import VideoUpload from "@/components/VideoUpload/index.vue";
 import {listVideoResource} from '@/api/course/videoResource';
 import {getByIds} from '@/api/course/courseQuestionBank'
 import CourseWatchComment from "./courseWatchComment.vue";
+import UserCourseSectionComment from "./userCourseSectionComment.vue";
 import {getCateListByPid, getCatePidList} from '@/api/course/userCourseCategory'
 import {downloadCommentImportTemplate, importComments, listFeaturedComments, delUserCourseComment} from '@/api/course/userCourseComment'
 import draggable from 'vuedraggable'
@@ -652,7 +692,7 @@ import { getConfigByKey } from '@/api/system/config'
 
 export default {
   name: "userCourseCatalog",
-  components: {VideoUpload, QuestionBank, CourseWatchComment, CourseProduct, draggable},
+  components: {VideoUpload, QuestionBank, CourseWatchComment, UserCourseSectionComment, CourseProduct, draggable},
   props: {
     usePublicVideoLibrary: {
       type: Boolean,
@@ -688,6 +728,12 @@ export default {
         title: '',
         open: false,
       },
+      hotSaleTagDialog: {
+        visible: false,
+        index: -1,
+        productName: '',
+        tagInput: ''
+      },
       isPrivate: null,
       videoUrl: "",
       uploadTypeOptions: [
@@ -798,6 +844,12 @@ export default {
         videoId: null,
         title: ""
       },
+      userCommentDialog: {
+        open: false,
+        courseId: null,
+        videoId: null,
+        title: ""
+      },
       enableRandomRedPacket:false,
       // 精选留言导入弹窗
       commentImportDialog: {
@@ -929,6 +981,7 @@ export default {
         cardPopupTime: val.cardPopupTime || '00:00:00',  // 卡片弹出时间,默认 00:00:00
         cardCloseTime: val.cardCloseTime || '00:00:00',  // 卡片关闭时间,默认 00:00:00
         offShelfTime: val.offShelfTime || '00:00:00',  // 下架时间,默认 00:00:00
+        hotSaleTags: val.hotSaleTags || '',
       };
       // 检查商品是否已存在
       const exists = this.form.courseProducts.some(item => item.productId === val.productId);
@@ -1122,6 +1175,29 @@ export default {
       this.form.questionBankList.splice(this.form.questionBankList.findIndex(item => item.id === row.id), 1)
     },
 
+    getHotSaleTagList(row) {
+      if (!row || !row.hotSaleTags) {
+        return [];
+      }
+      return row.hotSaleTags.split(/[,,]/).map(t => t.trim()).filter(t => t);
+    },
+    handleOpenHotSaleTagDialog(row, index) {
+      this.hotSaleTagDialog.index = index;
+      this.hotSaleTagDialog.productName = row.productName || '';
+      this.hotSaleTagDialog.tagInput = row.hotSaleTags || '';
+      this.hotSaleTagDialog.visible = true;
+    },
+    confirmHotSaleTag() {
+      const tags = (this.hotSaleTagDialog.tagInput || '')
+        .split(/[,,]/)
+        .map(t => t.trim())
+        .filter(t => t);
+      const index = this.hotSaleTagDialog.index;
+      if (index >= 0 && this.form.courseProducts[index]) {
+        this.$set(this.form.courseProducts[index], 'hotSaleTags', tags.length ? tags.join(',') : '');
+      }
+      this.hotSaleTagDialog.visible = false;
+    },
     //删除商品
     handleCourseProductDelete(row) {
       // 优先使用 productId 查找
@@ -1376,7 +1452,8 @@ export default {
                 onShelfTime: product.onShelfTime || '00:00:00',
                 cardPopupTime: product.cardPopupTime || '00:00:00',
                 cardCloseTime: product.cardCloseTime || '00:00:00',
-                offShelfTime: product.offShelfTime || '00:00:00'
+                offShelfTime: product.offShelfTime || '00:00:00',
+                hotSaleTags: product.hotSaleTags || ''
               }));
             } else {
               // 否则按照原有逻辑设置到 packageList
@@ -1825,6 +1902,23 @@ export default {
       }
       return isLt10M && isImage;
     },
+    openUserCommentDialog() {
+      if (!this.form.courseId || !this.form.videoId) {
+        this.$message.warning('请先保存课节后再查看用户留言')
+        return
+      }
+      this.userCommentDialog.courseId = this.form.courseId
+      this.userCommentDialog.videoId = this.form.videoId
+      this.userCommentDialog.title = `用户留言 - ${this.form.title || ''}`
+      this.userCommentDialog.open = true
+    },
+    onUserCommentDialogOpened() {
+      this.$nextTick(() => {
+        if (this.$refs.userCourseSectionComment) {
+          this.$refs.userCourseSectionComment.handleQuery()
+        }
+      })
+    },
     // 打开精选留言导入弹窗
     openCommentImportDialog() {
       this.commentImportDialog.visible = true;

+ 246 - 0
src/views/components/course/userCourseSectionComment.vue

@@ -0,0 +1,246 @@
+<template>
+  <div class="app-container user-section-comment">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" label-width="72px">
+      <el-form-item label="用户昵称" prop="nickName">
+        <el-input
+          v-model="queryParams.nickName"
+          placeholder="请输入用户昵称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-table v-loading="loading" :data="commentList" border>
+      <el-table-column label="用户昵称" prop="nickName" width="120" show-overflow-tooltip />
+      <el-table-column label="类型" width="72" align="center">
+        <template slot-scope="scope">
+          <el-tag size="mini" :type="scope.row.type === 2 ? 'info' : ''">
+            {{ scope.row.type === 2 ? '回复' : '评论' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="留言内容" min-width="360">
+        <template slot-scope="scope">
+          <div class="comment-content-cell">
+            <template v-for="(part, idx) in parseCommentContent(scope.row.content)">
+              <p v-if="part.kind === 'text'" :key="'t-' + scope.row.commentId + '-' + idx" class="comment-text">
+                {{ part.value }}
+              </p>
+              <el-image
+                v-else-if="part.kind === 'image'"
+                :key="'i-' + scope.row.commentId + '-' + idx"
+                class="comment-media comment-image"
+                :src="part.value"
+                :preview-src-list="[part.value]"
+                fit="contain"
+              />
+              <video
+                v-else-if="part.kind === 'video'"
+                :key="'v-' + scope.row.commentId + '-' + idx"
+                class="comment-media comment-video"
+                :src="part.value"
+                controls
+                preload="metadata"
+              />
+              <a
+                v-else-if="part.kind === 'link'"
+                :key="'l-' + scope.row.commentId + '-' + idx"
+                :href="part.value"
+                target="_blank"
+                rel="noopener"
+                class="comment-link"
+              >{{ part.value }}</a>
+            </template>
+            <span v-if="!scope.row.content" class="comment-empty">—</span>
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column label="点赞" prop="likes" width="70" align="center" />
+      <el-table-column label="可见范围" width="100" align="center">
+        <template slot-scope="scope">
+          <el-tag size="mini" :type="scope.row.visibleAll === 1 ? 'success' : 'info'">
+            {{ scope.row.visibleAll === 1 ? '全部人可见' : '自己可见' }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="留言时间" prop="createTime" width="160" align="center" />
+      <el-table-column label="操作" width="180" align="center" fixed="right">
+        <template slot-scope="scope">
+          <el-button
+            type="text"
+            size="mini"
+            :disabled="scope.row.visibleAll === 1"
+            @click="handleUpdateVisible(scope.row, 1)"
+          >全部人可见</el-button>
+          <el-button
+            type="text"
+            size="mini"
+            :disabled="scope.row.visibleAll === 0 || scope.row.visibleAll == null"
+            @click="handleUpdateVisible(scope.row, 0)"
+          >自己可见</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total > 0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </div>
+</template>
+
+<script>
+import { listSectionUserComments, updateCommentVisibleAll } from '@/api/course/userCourseComment'
+
+const IMAGE_EXT = /\.(png|jpe?g|gif|webp|bmp|svg)(\?.*)?$/i
+const VIDEO_EXT = /\.(mp4|webm|ogg|mov|m4v|m3u8)(\?.*)?$/i
+
+export default {
+  name: 'UserCourseSectionComment',
+  props: {
+    courseId: {
+      type: [String, Number],
+      required: true
+    },
+    videoId: {
+      type: [String, Number],
+      required: true
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      total: 0,
+      commentList: [],
+      visibleUpdatingId: null,
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        courseId: null,
+        videoId: null,
+        nickName: null
+      }
+    }
+  },
+  watch: {
+    courseId: {
+      immediate: true,
+      handler(val) {
+        this.queryParams.courseId = val
+      }
+    },
+    videoId: {
+      immediate: true,
+      handler(val) {
+        this.queryParams.videoId = val
+      }
+    }
+  },
+  methods: {
+    getList() {
+      if (!this.queryParams.courseId || !this.queryParams.videoId) {
+        return
+      }
+      this.loading = true
+      listSectionUserComments(this.queryParams).then(response => {
+        this.commentList = response.rows || []
+        this.total = response.total || 0
+        this.loading = false
+      }).catch(() => {
+        this.loading = false
+      })
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1
+      this.getList()
+    },
+    resetQuery() {
+      this.queryParams.nickName = null
+      this.resetForm('queryForm')
+      this.handleQuery()
+    },
+    handleUpdateVisible(row, visibleAll) {
+      const label = visibleAll === 1 ? '全部人可见' : '自己可见'
+      this.$confirm(`确认将该留言设置为「${label}」?`, '提示', { type: 'warning' }).then(() => {
+        this.visibleUpdatingId = row.commentId
+        updateCommentVisibleAll({
+          commentId: row.commentId,
+          visibleAll: visibleAll
+        }).then(() => {
+          this.$message.success('修改成功')
+          this.getList()
+        }).finally(() => {
+          this.visibleUpdatingId = null
+        })
+      }).catch(() => {})
+    },
+    parseCommentContent(content) {
+      if (!content || !String(content).trim()) {
+        return []
+      }
+      const lines = String(content).split(/\r?\n/).map(s => s.trim()).filter(Boolean)
+      const parts = []
+      lines.forEach(line => {
+        if (!/^https?:\/\//i.test(line)) {
+          parts.push({ kind: 'text', value: line })
+          return
+        }
+        if (VIDEO_EXT.test(line) || /\/comment\/video\//i.test(line)) {
+          parts.push({ kind: 'video', value: line })
+        } else if (IMAGE_EXT.test(line) || /\/comment\/image\//i.test(line)) {
+          parts.push({ kind: 'image', value: line })
+        } else {
+          parts.push({ kind: 'link', value: line })
+        }
+      })
+      return parts
+    }
+  }
+}
+</script>
+
+<style scoped>
+.user-section-comment {
+  padding: 0;
+}
+.comment-content-cell {
+  text-align: left;
+  line-height: 1.6;
+}
+.comment-text {
+  margin: 0 0 6px;
+  white-space: pre-wrap;
+  word-break: break-word;
+}
+.comment-media {
+  display: block;
+  margin: 6px 0;
+  max-width: 280px;
+  border-radius: 4px;
+}
+.comment-image {
+  max-height: 160px;
+}
+.comment-video {
+  max-height: 200px;
+  background: #000;
+}
+.comment-link {
+  display: block;
+  margin: 4px 0;
+  color: #409eff;
+  word-break: break-all;
+}
+.comment-empty {
+  color: #909399;
+}
+</style>

+ 29 - 9
src/views/course/courseRedPacketLog/index.vue

@@ -71,7 +71,7 @@
 	      size="small"
 	      @keyup.enter.native="handleQuery"
 	    />
-	  </el-form-item>      
+	  </el-form-item>
 
 		<el-form-item label="课程" prop="courseId">
 		     <el-select filterable  v-model="queryParams.courseId" placeholder="请选择课程"  clearable size="small" @change="courseChange(queryParams.courseId)">
@@ -98,8 +98,8 @@
                              range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" @change="change"></el-date-picker>
 	 </el-form-item>
    <el-form-item label="发课方式" prop="sendType">
-	           <el-select 
-    v-model="sendType" 
+	           <el-select
+    v-model="sendType"
     placeholder="请选择发课方式"
     size="small"
     style="width: 220px"
@@ -172,7 +172,7 @@
           />
         </el-select>
       </el-form-item>
-      
+
       <el-form-item label="营期" prop="sopId" v-if="!periodShow">
         <el-select
           v-model="queryParams.sopId"
@@ -249,7 +249,12 @@
       <el-table-column label="所属员工" align="center" prop="companyUserName" />
       <el-table-column label="所属公司" align="center" prop="companyName" />
       <el-table-column label="员工部门" align="center" prop="deptName" />
-      <el-table-column label="转帐金额" align="center" prop="amount" />
+<!--      <el-table-column label="转帐金额" align="center" prop="amount" />-->
+      <el-table-column :label="RewardType === 1 ? '转账金额' : '积分发放(红包转)'" align="center">
+        <template slot-scope="scope">
+          <span>{{ RewardType === 1 ? scope.row.amount : scope.row.amount * 1000 }}</span>
+        </template>
+      </el-table-column>
       <el-table-column label="状态" align="center" prop="status" >
         <template slot-scope="scope">
           <el-tag>{{ scope.row.status === 0 ? "发送中" : "已完成" }}</el-tag>
@@ -310,12 +315,16 @@ import { listCamp } from "@/api/course/userCourseCamp"
 import {treeselect} from "../../../api/company/companyDept";
 import SelectTree from '@/components/TreeSelect/index.vue'
 import { getDeptData } from '@/api/system/employeeStats'
+import { getCourseConfigByRewardType } from '@/api/system/config'
 
 export default {
   name: "CourseRedPacketLog",
   components: {SelectTree},
   data() {
     return {
+
+    RewardType: 1,
+
 	  companys:[],
     selectedCompanyList: [],
     deptList: [],
@@ -380,7 +389,7 @@ export default {
       },
       //发课方式-手动,自动
       sendType:"",
-      periodShow:"", 
+      periodShow:"",
       sopLists:[],
       campLists:[],
       queryCamp:
@@ -394,7 +403,11 @@ export default {
     };
   },
   created() {
+
+    this.getCourseByRewardType();
+
     this.getList();
+
 	    getCompanyList().then(response => {
 	    this.companys = response.data;
 	  });
@@ -417,6 +430,13 @@ export default {
     // this.getList();
   },
   methods: {
+
+    getCourseByRewardType(){
+      getCourseConfigByRewardType().then(res=>{
+        this.RewardType=res.rewardType;
+      })
+    },
+
     // 远程搜索方法
     async remoteMethod(query) {
       this.searchKeyword = query
@@ -462,7 +482,7 @@ export default {
     },
 handleSopChange(value) {
     console.log('选择的SOP ID:', value);
-  
+
   // 清除可能的表单验证错误
   this.$nextTick(() => {
     if (this.$refs.queryForm) {
@@ -505,7 +525,7 @@ handleSopChange(value) {
         console.error('获取营期列表失败:', error)
         this.periodListsNew = []
       }
-    }, 
+    },
     async getSopList(keyword = '') {
       try {
         const params = {
@@ -532,7 +552,7 @@ handleSopChange(value) {
         this.campLists = []
       }
     },
-    
+
     // 下拉框获取焦点时加载数据
     async handleFocus() {
       if (this.periodLists.length === 0) {

+ 30 - 1
src/views/course/courseWatchLog/index.vue

@@ -59,6 +59,15 @@
           @keyup.enter.native="handleQuery"
         />
       </el-form-item>
+      <el-form-item label="企微客户id" prop="nickName" v-if="queryParams.sendType == 2">
+        <el-input
+          v-model="queryParams.qwExternalContactId"
+          placeholder="请输入企微客户id"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
       <!-- 营期名称  periodIds-->
       <el-form-item label="营期名称" prop="periodId">
         <el-select filterable  v-model="queryParams.periodId" placeholder="请选择营期名称"  clearable size="small">
@@ -523,7 +532,13 @@
         <!--        <el-table-column label="会员电话" align="center" prop="phone" />-->
         <!--        <el-table-column label="所属销售" align="center" prop="companyUserName" />-->
         <!--        <el-table-column label="所属公司" align="center" prop="companyName" />-->
-        <el-table-column label="转账金额" align="center" prop="amount" />
+<!--        <el-table-column label="转账金额" align="center" prop="amount" />-->
+
+        <el-table-column :label="RewardType === 1 ? '转账金额' : '积分发放(红包转)'" align="center">
+          <template slot-scope="scope">
+            <span>{{ RewardType === 1 ? scope.row.amount : scope.row.amount * 1000 }}</span>
+          </template>
+        </el-table-column>
         <el-table-column label="状态" align="center" prop="status" >
           <template slot-scope="scope">
             <el-tag>
@@ -669,6 +684,7 @@ import Treeselect from "@riophae/vue-treeselect";
 import "@riophae/vue-treeselect/dist/vue-treeselect.css";
 import {getCompanyList} from "../../../api/company/company";
 import {treeselectByCompanyId} from "../../../api/company/companyDept";
+import { getCourseConfigByRewardType } from '@/api/system/config'
 Vue.use(Calendar)
 
 export default {
@@ -676,6 +692,9 @@ export default {
   components: {Treeselect },
   data() {
     return {
+
+      RewardType: 1,
+
       watchTypeList: [
         { dictLabel: 'app', dictValue: 1 },
         { dictLabel: '小程序', dictValue: 2 }
@@ -881,6 +900,9 @@ export default {
     };
   },
   created() {
+
+    this.getCourseByRewardType();
+
     // 首先查询公司列表
     getCompanyList().then(response => {
       this.myCompanyList = response.data;
@@ -897,6 +919,13 @@ export default {
     });
   },
   methods: {
+
+    getCourseByRewardType(){
+      getCourseConfigByRewardType().then(res=>{
+        this.RewardType=res.rewardType;
+      })
+    },
+
       // 加载依赖companyId的数据
       loadDependentData() {
         // 使用Promise.all并行执行不依赖companyId的请求

+ 2 - 2
src/views/course/courseWatchLog/myCourseWatchLog.vue

@@ -11,10 +11,10 @@
           />
         </el-select>
       </el-form-item>
-      <el-form-item label="客户ID" prop="qwExternalContactId">
+      <el-form-item label="企微客户id" prop="qwExternalContactId">
         <el-input
           v-model="queryParams.qwExternalContactId"
-          placeholder="请输入会员ID"
+          placeholder="请输入企微客户id"
           clearable
           size="small"
           @keyup.enter.native="handleQuery"

+ 25 - 2
src/views/course/courseWatchLog/watchLog.vue

@@ -234,7 +234,12 @@
         <el-table-column label="会员电话" align="center" prop="phone" />
         <el-table-column label="所属销售" align="center" prop="companyUserName" />
         <el-table-column label="所属公司" align="center" prop="companyName" />
-        <el-table-column label="转帐金额" align="center" prop="amount" />
+<!--        <el-table-column label="转帐金额" align="center" prop="amount" />-->
+        <el-table-column :label="RewardType === 1 ? '转账金额' : '积分发放(红包转)'" align="center">
+          <template slot-scope="scope">
+            <span>{{ RewardType === 1 ? scope.row.amount : scope.row.amount * 1000 }}</span>
+          </template>
+        </el-table-column>
         <el-table-column label="状态" align="center" prop="status" >
           <template slot-scope="scope">
             <el-tag>{{ scope.row.status === 0 ? "发送中" : "已完成" }}</el-tag>
@@ -269,12 +274,16 @@ import {courseList, myListCourseRedPacketLog, videoListByWatch} from '@/api/cour
 import {myListLogs} from "@/api/course/courseAnswerlogs";
 import {getMyQwUserList} from "@/api/qw/user";
 import {getFsUserList} from "@/api/users/user";
+import { getCourseConfigByRewardType } from '@/api/system/config'
 
 
 export default {
   name: "CourseWatchLog",
   data() {
     return {
+
+      RewardType: 1,
+
       userSourceTypeOptions: [],
       activeName:"00",
       createTime: null,
@@ -367,6 +376,9 @@ export default {
     }
   },
   created() {
+
+    this.getCourseByRewardType();
+
     this.getDicts('user_source_type').then(response => {
       this.userSourceTypeOptions = response.data;
     })
@@ -374,7 +386,11 @@ export default {
     courseList().then(response => {
       this.courseLists = response.list;
     });
-    this.getList();
+
+    setTimeout(() => {
+      this.getList();
+    }, 200);
+
     this.getDicts("sys_course_watch_log_type").then(response => {
       this.logTypeOptions = response.data;
     });
@@ -387,6 +403,13 @@ export default {
     });
   },
   methods: {
+
+    getCourseByRewardType(){
+      getCourseConfigByRewardType().then(res=>{
+        this.RewardType=res.rewardType;
+      })
+    },
+
     handleClear(){
       this.queryUserLoading = false;
       this.fsUserList = [];

+ 388 - 0
src/views/crm/components/AiTagPanel.vue

@@ -0,0 +1,388 @@
+<template>
+  <div class="ai-tag-container">
+    <div class="ai-tag-header">
+      <div class="header-left">
+        <i class="el-icon-magic-sticker" style="color: #6b5fff; font-size: 18px; margin-right: 6px;"></i>
+        <span class="tag-title">AI 标签</span>
+        <el-tooltip content="AI 自动打标签" placement="bottom">
+          <span class="tag-subtitle">(AI 自动打标签)</span>
+        </el-tooltip>
+      </div>
+      <div class="header-right">
+        <el-button type="text" icon="el-icon-plus" size="mini" @click="handleAddTag" style="color: #ff9800;">
+          点击增加 AI 标签维度
+        </el-button>
+        <el-button type="text" icon="el-icon-view" size="mini" @click="toggleView" style="margin-left: 10px;">
+          <i class="el-icon-view"></i>
+        </el-button>
+      </div>
+    </div>
+
+    <div class="ai-tag-content" v-loading="loading">
+      <div v-if="tagList.length === 0" class="empty-tips">
+        <i class="el-icon-info"></i>
+        <span>暂无标签,点击"添加标签"开始设置</span>
+      </div>
+
+      <div v-else class="tag-list">
+        <div v-for="(tag, index) in tagList" :key="tag.id" class="tag-item">
+          <div class="tag-name">
+            <i class="el-icon-more"></i>
+            <span>{{ tag.propertyName }}</span>
+          </div>
+          <div class="tag-values">
+            <el-tag
+              v-if="tag.propertyValue"
+              size="small"
+              closable
+              @close="handleDeleteTag(tag)"
+              @click="handleEditTag(tag)"
+              style="cursor: pointer; margin-right: 8px; margin-bottom: 8px;"
+            >
+              {{ tag.propertyValue }}
+            </el-tag>
+            <span v-else class="no-value">未设置</span>
+            <el-button
+              v-if="!tag.propertyValue"
+              type="text"
+              size="mini"
+              icon="el-icon-edit"
+              @click="handleEditTag(tag)"
+            >
+              批注
+            </el-button>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 添加/编辑标签弹窗 -->
+    <el-dialog
+      :title="dialogTitle"
+      :visible.sync="dialogVisible"
+      width="600px"
+      append-to-body
+      :close-on-click-modal="false"
+    >
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="标签维度" prop="propertyId">
+          <el-select
+            v-model="form.propertyId"
+            placeholder="请选择标签维度"
+            style="width: 100%"
+            :disabled="isEdit"
+            @change="handlePropertyChange"
+          >
+            <el-option
+              v-for="item in propertyTemplateList"
+              :key="item.id"
+              :label="item.name"
+              :value="item.id"
+            >
+              <span style="float: left">{{ item.name }}</span>
+              <span style="float: right; color: #8492a6; font-size: 13px">{{ getValueTypeText(item.valueType) }}</span>
+            </el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="标签值" prop="propertyValue">
+          <el-input
+            v-if="currentValueType === 'text'"
+            v-model="form.propertyValue"
+            placeholder="请输入标签值"
+            maxlength="100"
+            show-word-limit
+          />
+          <el-select
+            v-else-if="currentValueType === 'select'"
+            v-model="form.propertyValue"
+            placeholder="请选择标签值"
+            style="width: 100%"
+            clearable
+          >
+            <el-option label="是" value="是" />
+            <el-option label="否" value="否" />
+          </el-select>
+          <el-input
+            v-else-if="currentValueType === 'number'"
+            v-model="form.propertyValue"
+            placeholder="请输入数字"
+            maxlength="20"
+          />
+          <el-input
+            v-else-if="currentValueType === 'date'"
+            v-model="form.propertyValue"
+            type="date"
+            placeholder="选择日期"
+            style="width: 100%"
+          />
+          <span v-else class="unknown-type">未知的属性类型</span>
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input
+            v-model="form.remark"
+            type="textarea"
+            :rows="2"
+            placeholder="请输入备注"
+            maxlength="200"
+            show-word-limit
+          />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="dialogVisible = false">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listPropertyTemplate } from "@/api/crm/propertyTemplate";
+import { listByCustomerId, addOrUpdateByTemplateId, deleteByPropertyId } from "@/api/crm/customerProperty";
+
+export default {
+  name: "AiTagPanel",
+  props: {
+    customerId: {
+      type: Number,
+      required: true,
+      default: null
+    }
+  },
+  data() {
+    return {
+      loading: false,
+      tagList: [],
+      propertyTemplateList: [],
+      dialogVisible: false,
+      dialogTitle: "",
+      isEdit: false,
+      currentValueType: "text",
+      form: {
+        customerId: null,
+        propertyId: null,
+        propertyName: null,
+        propertyValue: null,
+        propertyValueType: null,
+        tradeType: null,
+        remark: null
+      },
+      rules: {
+        propertyId: [
+          { required: true, message: "请选择标签维度", trigger: "change" }
+        ],
+        propertyValue: [
+          { required: true, message: "请输入标签值", trigger: "blur" }
+        ]
+      }
+    };
+  },
+  created() {
+    if (this.customerId) {
+      this.loadTags();
+    }
+  },
+  watch: {
+    customerId(newVal) {
+      if (newVal) {
+        this.loadTags();
+      }
+    }
+  },
+  methods: {
+    loadTags() {
+      this.loading = true;
+      listByCustomerId(this.customerId).then(response => {
+        this.tagList = response.data || [];
+        this.loading = false;
+      }).catch(() => {
+        this.loading = false;
+      });
+    },
+    loadPropertyTemplates() {
+      listPropertyTemplate({ pageNum: 1, pageSize: 100 }).then(response => {
+        this.propertyTemplateList = response.rows || [];
+      });
+    },
+    toggleView() {
+      this.$emit("toggle-view");
+    },
+    handleAddTag() {
+      this.loadPropertyTemplates();
+      this.reset();
+      this.dialogTitle = "添加标签";
+      this.isEdit = false;
+      this.dialogVisible = true;
+    },
+    handleEditTag(tag) {
+      this.loadPropertyTemplates();
+      this.form = {
+        customerId: tag.customerId,
+        propertyId: tag.propertyId,
+        propertyName: tag.propertyName,
+        propertyValue: tag.propertyValue,
+        propertyValueType: tag.propertyValueType,
+        tradeType: tag.tradeType,
+        remark: tag.remark
+      };
+      this.currentValueType = tag.propertyValueType || "text";
+      this.dialogTitle = "编辑标签";
+      this.isEdit = true;
+      this.dialogVisible = true;
+    },
+    handleDeleteTag(tag) {
+      this.$confirm(`确认删除标签"${tag.propertyName}"吗?`, "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        return deleteByPropertyId(this.customerId, tag.propertyId);
+      }).then(() => {
+        this.msgSuccess("删除成功");
+        this.loadTags();
+        this.$emit("tag-change");
+      }).catch(() => {});
+    },
+    handlePropertyChange(propertyId) {
+      const template = this.propertyTemplateList.find(item => item.id === propertyId);
+      if (template) {
+        this.form.propertyName = template.name;
+        this.form.propertyValueType = template.valueType;
+        this.form.tradeType = template.tradeType;
+        this.currentValueType = template.valueType;
+      }
+    },
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          this.form.customerId = this.customerId;
+          addOrUpdateByTemplateId(
+            this.customerId,
+            this.form.propertyId,
+            this.form.propertyValue
+          ).then(response => {
+            this.msgSuccess("保存成功");
+            this.dialogVisible = false;
+            this.loadTags();
+            this.$emit("tag-change");
+          });
+        }
+      });
+    },
+    reset() {
+      this.form = {
+        customerId: this.customerId,
+        propertyId: null,
+        propertyName: null,
+        propertyValue: null,
+        propertyValueType: null,
+        tradeType: null,
+        remark: null
+      };
+      this.currentValueType = "text";
+      this.isEdit = false;
+      if (this.$refs.form) {
+        this.$refs.form.clearValidate();
+      }
+    },
+    getValueTypeText(valueType) {
+      const typeMap = {
+        text: "文本",
+        number: "数字",
+        date: "日期",
+        select: "单选",
+        multiSelect: "多选"
+      };
+      return typeMap[valueType] || valueType;
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.ai-tag-container {
+  padding: 16px;
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+
+  .ai-tag-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 16px;
+    padding-bottom: 12px;
+    border-bottom: 1px solid #ebeef5;
+
+    .header-left {
+      display: flex;
+      align-items: center;
+
+      .tag-title {
+        font-size: 16px;
+        font-weight: 600;
+        color: #303133;
+      }
+
+      .tag-subtitle {
+        font-size: 12px;
+        color: #909399;
+        margin-left: 8px;
+      }
+    }
+
+    .header-right {
+      display: flex;
+      align-items: center;
+    }
+  }
+
+  .ai-tag-content {
+    min-height: 200px;
+
+    .empty-tips {
+      text-align: center;
+      color: #909399;
+      padding: 40px 0;
+
+      i {
+        font-size: 40px;
+        display: block;
+        margin-bottom: 8px;
+      }
+    }
+
+    .tag-list {
+      .tag-item {
+        margin-bottom: 16px;
+
+        .tag-name {
+          display: flex;
+          align-items: center;
+          margin-bottom: 8px;
+          font-size: 14px;
+          font-weight: 500;
+          color: #303133;
+
+          i {
+            color: #409eff;
+            margin-right: 6px;
+            font-size: 12px;
+          }
+        }
+
+        .tag-values {
+          display: flex;
+          align-items: center;
+          flex-wrap: wrap;
+
+          .no-value {
+            color: #c0c4cc;
+            font-size: 13px;
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 19 - 6
src/views/crm/components/customerDetails.vue

@@ -107,6 +107,14 @@
             </el-descriptions-item>
         </el-descriptions>
 
+        <!-- AI 标签模块 -->
+        <div style="margin-top: 20px;" v-if="item && item.customerId">
+            <ai-tag-panel ref="aiTagPanel" :customer-id="item.customerId" @tag-change="handleTagChange"></ai-tag-panel>
+        </div>
+        <div v-else style="margin-top: 20px; color: #999; text-align: center;">
+            客户信息加载中...
+        </div>
+
         <el-tabs style="margin-top:20px;"  z-index = "99" type="border-card" v-model="activeName" @tab-click="handleClick">
             <el-tab-pane label="联系人" name="contacts">
                 <customer-contacts ref="contacts"></customer-contacts>
@@ -139,10 +147,11 @@
     import customerVoiceLogsList from '../components/customerVoiceLogsList.vue';
     import customerStoreOrderList from '../components/customerStoreOrderList.vue';
     import customerContacts from './customerContacts.vue';
+    import AiTagPanel from './AiTagPanel.vue';
     import { getCustomer } from "@/api/crm/customer";
     export default {
         name: "customer",
-        components: { customerContacts,customerVisitList,customerLogsList,customerVoiceLogsList,customerStoreOrderList,customerSmsLogsList },
+        components: { customerContacts,customerVisitList,customerLogsList,customerVoiceLogsList,customerStoreOrderList,customerSmsLogsList,AiTagPanel },
         data() {
             return {
                 customerExts:[],
@@ -222,12 +231,16 @@
                         });
                     }
                     this.activeName="contacts"
-                    setTimeout(() => {
-                        that.$refs.contacts.getData(this.item.customerId);
-                    }, 500);
+            setTimeout(() => {
+                that.$refs.contacts.getData(this.item.customerId);
+            }, 500);
 
-                });
-            },
+        });
+    },
+    handleTagChange() {
+      // 标签变化时的回调
+      console.log("标签发生变化");
+    }
         }
     };
 </script>

+ 392 - 0
src/views/crm/propertyTemplate/index.vue

@@ -0,0 +1,392 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="属性名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入属性名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="属性类型" prop="valueType">
+        <el-select v-model="queryParams.valueType" placeholder="请选择属性类型" clearable size="small">
+          <el-option label="文本" value="text" />
+          <el-option label="数字" value="number" />
+          <el-option label="日期" value="date" />
+          <el-option label="单选" value="select" />
+          <el-option label="多选" value="multiSelect" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="行业类型" prop="tradeType">
+        <el-select v-model="queryParams.tradeType" placeholder="请选择行业类型" clearable size="small">
+          <el-option
+            v-for="dict in tradeTypeOptions"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="cyan" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          icon="el-icon-plus"
+          size="mini"
+          @click="handleAdd"
+          v-hasPermi="['crm:customerPropertyTemplate:add']"
+        >新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="danger"
+          icon="el-icon-delete"
+          size="mini"
+          :disabled="multiple"
+          @click="handleDelete"
+          v-hasPermi="['crm:customerPropertyTemplate:remove']"
+        >删除</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table v-loading="loading" :data="templateList" @selection-change="handleSelectionChange" border stripe>
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column type="index" label="序号" width="60" align="center" />
+      <el-table-column label="属性名称" align="center" prop="name" min-width="120">
+        <template slot-scope="scope">
+          <span class="property-name">{{ scope.row.name }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="属性类型" align="center" prop="valueType" width="100">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.valueType === 'text'" type="info" size="small">文本</el-tag>
+          <el-tag v-else-if="scope.row.valueType === 'number'" type="warning" size="small">数字</el-tag>
+          <el-tag v-else-if="scope.row.valueType === 'date'" type="success" size="small">日期</el-tag>
+          <el-tag v-else-if="scope.row.valueType === 'select'" type="primary" size="small">单选</el-tag>
+          <el-tag v-else-if="scope.row.valueType === 'multiSelect'" type="danger" size="small">多选</el-tag>
+          <span v-else>{{ scope.row.valueType }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="行业类型" align="center" prop="tradeType" width="120">
+        <template slot-scope="scope">
+          <dict-tag v-if="scope.row.tradeType" :options="tradeTypeOptions" :value="scope.row.tradeType"/>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="AI提取提示" align="left" prop="aiHint" min-width="200" show-overflow-tooltip>
+        <template slot-scope="scope">
+          <span class="ai-hint-text">{{ scope.row.aiHint || '-' }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="意向开关" align="center" prop="intention" width="120">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.intention === 1 || scope.row.intention === '1'" type="success" size="small">开启</el-tag>
+          <el-tag v-else-if="scope.row.intention === 0 || scope.row.intention === '0'" type="info" size="small">关闭</el-tag>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" align="center" prop="createTime" width="160">
+        <template slot-scope="scope">
+          <i class="el-icon-time"></i>
+          <span style="margin-left: 5px">{{ parseTime(scope.row.createTime, '{y}-{m}-{d} {h}:{i}') }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="150" fixed="right">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate(scope.row)"
+            v-hasPermi="['crm:customerPropertyTemplate:edit']"
+          >修改</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            style="color: #f56c6c"
+            @click="handleDelete(scope.row)"
+            v-hasPermi="['crm:customerPropertyTemplate:remove']"
+          >删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <el-dialog :title="title" :visible.sync="open" width="700px" append-to-body :close-on-click-modal="false">
+      <el-form ref="form" :model="form" :rules="rules" label-width="110px" class="property-form">
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="属性名称" prop="name">
+              <el-input v-model="form.name" placeholder="请输入属性名称" maxlength="50" show-word-limit />
+            </el-form-item>
+          </el-col>
+          <el-col :span="12">
+            <el-form-item label="属性类型" prop="valueType">
+              <el-select v-model="form.valueType" placeholder="请选择属性类型" style="width: 100%">
+                <el-option label="文本" value="text" />
+                <el-option label="数字" value="number" />
+                <el-option label="日期" value="date" />
+                <el-option label="单选" value="select" />
+                <el-option label="多选" value="multiSelect" />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="行业类型" prop="tradeType">
+              <el-select v-model="form.tradeType" placeholder="请选择行业类型" style="width: 100%" clearable>
+                <el-option
+                  v-for="dict in tradeTypeOptions"
+                  :key="dict.dictValue"
+                  :label="dict.dictLabel"
+                  :value="dict.dictValue"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-divider content-position="left">AI配置</el-divider>
+        <el-row>
+          <el-col :span="24">
+            <el-form-item label="AI提取提示" prop="aiHint">
+              <el-input 
+                v-model="form.aiHint" 
+                type="textarea" 
+                :rows="4" 
+                placeholder="请输入AI属性提取提示,用于指导AI从对话中提取该属性的值。例如:请从对话中提取患者的年龄信息,注意识别数字和单位" 
+                maxlength="500"
+                show-word-limit
+              />
+            </el-form-item>
+          </el-col>
+        </el-row>
+        <el-divider content-position="left">其他信息</el-divider>
+        <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="意向开关" prop="intention">
+              <el-switch v-model="form.intention" active-value="1" inactive-value="0" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="24">
+            <el-form-item label="备注" prop="remark">
+              <el-input v-model="form.remark" type="textarea" :rows="2" placeholder="请输入备注信息" maxlength="200" show-word-limit />
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listPropertyTemplate, getPropertyTemplate, delPropertyTemplate, addPropertyTemplate, updatePropertyTemplate, exportPropertyTemplate } from "@/api/crm/propertyTemplate";
+
+export default {
+  name: "PropertyTemplate",
+  data() {
+    return {
+      loading: true,
+      ids: [],
+      single: true,
+      multiple: true,
+      showSearch: true,
+      total: 0,
+      templateList: [],
+      title: "",
+      open: false,
+      tradeTypeOptions: [],
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        name: null,
+        valueType: null,
+        tradeType: null
+      },
+      form: {},
+      rules: {
+        name: [
+          { required: true, message: "属性名称不能为空", trigger: "blur" }
+        ],
+        valueType: [
+          { required: true, message: "属性类型不能为空", trigger: "change" }
+        ]
+      }
+    };
+  },
+  created() {
+    this.getList();
+    this.getDicts("trade_type").then(response => {
+      this.tradeTypeOptions = response.data;
+    });
+  },
+  methods: {
+    getList() {
+      this.loading = true;
+      listPropertyTemplate(this.queryParams).then(response => {
+        this.templateList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    reset() {
+      this.form = {
+        id: null,
+        name: null,
+        valueType: null,
+        aiHint: null,
+        tradeType: null,
+        intention: null,
+        remark: null
+      };
+      this.resetForm("form");
+    },
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.single = selection.length !== 1
+      this.multiple = !selection.length
+    },
+    handleAdd() {
+      this.reset();
+      this.open = true;
+      this.title = "添加客户属性模板";
+    },
+    handleUpdate(row) {
+      this.reset();
+      const id = row.id || this.ids
+      getPropertyTemplate(id).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = "修改客户属性模板";
+      });
+    },
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.id != null) {
+            updatePropertyTemplate(this.form).then(response => {
+              if (response.code === 200) {
+                this.msgSuccess("修改成功");
+                this.open = false;
+                this.getList();
+              }
+            });
+          } else {
+            addPropertyTemplate(this.form).then(response => {
+              if (response.code === 200) {
+                this.msgSuccess("新增成功");
+                this.open = false;
+                this.getList();
+              }
+            });
+          }
+        }
+      });
+    },
+    handleDelete(row) {
+      const ids = row.id || this.ids;
+      this.$confirm('是否确认删除客户属性模板编号为"' + ids + '"的数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return delPropertyTemplate(ids);
+        }).then(() => {
+          this.getList();
+          this.msgSuccess("删除成功");
+        }).catch(function() {});
+    },
+    handleExport() {
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出所有客户属性模板数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return exportPropertyTemplate(queryParams);
+        }).then(response => {
+          this.download(response.msg);
+        }).catch(function() {});
+    }
+  }
+};
+</script>
+
+<style scoped>
+.app-container {
+  padding: 20px;
+}
+
+.property-form {
+  padding: 10px 20px;
+}
+
+.property-form .el-divider {
+  margin: 15px 0;
+}
+
+.property-form .el-divider__text {
+  font-weight: 500;
+  color: #606266;
+}
+
+.property-form .el-form-item {
+  margin-bottom: 18px;
+}
+
+.property-form .el-textarea ::v-deep .el-textarea__inner {
+  resize: none;
+}
+
+.property-name {
+  font-weight: 500;
+  color: #303133;
+}
+
+.ai-hint-text {
+  color: #606266;
+  font-size: 13px;
+  line-height: 1.5;
+}
+
+.el-table ::v-deep .el-table__row {
+  height: 50px;
+}
+
+.el-table ::v-deep .el-tag {
+  border-radius: 4px;
+}
+</style>

+ 179 - 0
src/views/his/company/index.vue

@@ -238,6 +238,23 @@
           >解绑销售易
           </el-button>
 
+
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleRedPacket(scope.row)"
+            v-hasPermi="['company:company:redPacket']"
+          >绑定红包商户</el-button>
+
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleAppRedPacket(scope.row)"
+            v-hasPermi="['company:company:AppRedPacket']"
+          >绑定app提现商户</el-button>
+
         </template>
       </el-table-column>
     </el-table>
@@ -786,6 +803,103 @@
       </div>
     </el-dialog>
 
+    <!-- 红包商户配置   -->
+    <el-dialog :title="redPacketConfigOpen.title" :visible.sync="redPacketConfigOpen.open" width="800px" append-to-body>
+      <el-form ref="redPacketConfig" :model="redPacketConfig"  label-width="150px">
+        <el-form-item   label="红包接口类型" prop="isNew">
+          <el-radio-group v-model="redPacketConfig.isNew">
+            <el-radio label="0">商家转账到零钱(旧)</el-radio>
+            <el-radio label="1">商家转账(新)</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item   label="公众号appid" prop="appId">
+          <el-input   v-model="redPacketConfig.appId"  label="请输入appId"></el-input>
+        </el-form-item>
+        <el-form-item   label="小程序appid" prop="appId">
+          <el-input   v-model="redPacketConfig.miniappId"  label="请输入appId"></el-input>
+        </el-form-item>
+        <el-form-item   label="商户号" prop="mchId">
+          <el-input   v-model="redPacketConfig.mchId"  label="请输入mchId"></el-input>
+        </el-form-item>
+        <el-form-item   label="商户密钥" prop="mchKey">
+          <el-input   v-model="redPacketConfig.mchKey"  label="mchKey"></el-input>
+        </el-form-item>
+        <el-form-item   label="p12证书路径" prop="keyPath">
+          <el-input   v-model="redPacketConfig.keyPath"  label="请输入keyPath"></el-input>
+        </el-form-item>
+        <el-form-item   label="apiV3密钥" prop="apiV3Key">
+          <el-input   v-model="redPacketConfig.apiV3Key"  label="请输入apiV3Key"></el-input>
+        </el-form-item>
+        <el-form-item   label="公钥ID" prop="publicKeyId">
+          <el-input   v-model="redPacketConfig.publicKeyId"  label="请输入公钥ID"></el-input>
+        </el-form-item>
+        <el-form-item   label="公钥证书" prop="publicKeyPath">
+          <el-input   v-model="redPacketConfig.publicKeyPath"  label="请输入publicKeyPath"></el-input>
+        </el-form-item>
+        <el-form-item   label="key路径" prop="privateKeyPath">
+          <el-input   v-model="redPacketConfig.privateKeyPath"  label="请输入"></el-input>
+        </el-form-item>
+        <el-form-item   label="cert路径" prop="privateCertPath">
+          <el-input   v-model="redPacketConfig.privateCertPath"  label="请输入"></el-input>
+        </el-form-item>
+        <el-form-item   label="回调地址" prop="notifyUrl">
+          <el-input   v-model="redPacketConfig.notifyUrl"  label="请输入"></el-input>
+        </el-form-item>
+
+        </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="onSubmitCompanyRedPacket">提交</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- app提现配置   -->
+    <el-dialog :title="hisAppRedPacketOpen.title" :visible.sync="hisAppRedPacketOpen.open" width="800px" append-to-body>
+      <el-form ref="hisAppRedPacket" :model="hisAppRedPacket"  label-width="150px">
+        <el-form-item   label="红包接口类型" prop="isNew">
+          <el-radio-group v-model="hisAppRedPacket.isNew">
+            <el-radio label="0">商家转账到零钱(旧)</el-radio>
+            <el-radio label="1">商家转账(新)</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item   label="公众号appid" prop="appId">
+          <el-input   v-model="hisAppRedPacket.appId"  label="请输入appId"></el-input>
+        </el-form-item>
+        <el-form-item   label="小程序appid" prop="appId">
+          <el-input   v-model="hisAppRedPacket.miniappId"  label="请输入appId"></el-input>
+        </el-form-item>
+        <el-form-item   label="商户号" prop="mchId">
+          <el-input   v-model="hisAppRedPacket.mchId"  label="请输入mchId"></el-input>
+        </el-form-item>
+        <el-form-item   label="商户密钥" prop="mchKey">
+          <el-input   v-model="hisAppRedPacket.mchKey"  label="mchKey"></el-input>
+        </el-form-item>
+        <el-form-item   label="p12证书路径" prop="keyPath">
+          <el-input   v-model="hisAppRedPacket.keyPath"  label="请输入keyPath"></el-input>
+        </el-form-item>
+        <el-form-item   label="apiV3密钥" prop="apiV3Key">
+          <el-input   v-model="hisAppRedPacket.apiV3Key"  label="请输入apiV3Key"></el-input>
+        </el-form-item>
+        <el-form-item   label="公钥ID" prop="publicKeyId">
+          <el-input   v-model="hisAppRedPacket.publicKeyId"  label="请输入公钥ID"></el-input>
+        </el-form-item>
+        <el-form-item   label="公钥证书" prop="publicKeyPath">
+          <el-input   v-model="hisAppRedPacket.publicKeyPath"  label="请输入publicKeyPath"></el-input>
+        </el-form-item>
+        <el-form-item   label="key路径" prop="privateKeyPath">
+          <el-input   v-model="hisAppRedPacket.privateKeyPath"  label="请输入"></el-input>
+        </el-form-item>
+        <el-form-item   label="cert路径" prop="privateCertPath">
+          <el-input   v-model="hisAppRedPacket.privateCertPath"  label="请输入"></el-input>
+        </el-form-item>
+        <el-form-item   label="回调地址" prop="notifyUrl">
+          <el-input   v-model="hisAppRedPacket.notifyUrl"  label="请输入"></el-input>
+        </el-form-item>
+
+        </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="onSubmitAppRedPacket">提交</el-button>
+      </div>
+    </el-dialog>
   </div>
 </template>
 
@@ -808,6 +922,7 @@ import {
 import { getFollowDoctorList } from '@/api/his/doctor'
 import { docList } from '@/api/his/doctor'
 import { getVoiceApiList } from '@/api/company/companyVoiceApi'
+import { editCompanyConfig,getCompanyConfigKey } from '@/api/company/companyConfig'
 import { getCitysArea } from '../../../api/company/company'
 import { cateList } from '@/api/his/packageCate'
 import { getConfigByKey } from '@/api/system/config'
@@ -819,6 +934,23 @@ export default {
   name: 'Company',
   data() {
     return {
+
+      redPacketConfigOpen:{
+        open: false,
+        title: '红包商户配置'
+      },
+
+      hisAppRedPacketOpen:{
+        open: false,
+        title: 'app提现商户配置'
+      },
+
+      redPacketConfig:{},
+      hisAppRedPacket:{},
+
+      redPacketConfigForm:{},
+      hisAppRedPacketForm:{},
+
       gatewayList:[],
       projectFrom:process.env.VUE_APP_HSY_SPACE,
 
@@ -1222,6 +1354,26 @@ export default {
       })
     },
 
+    onSubmitAppRedPacket(){
+      this.hisAppRedPacketForm.configValue=JSON.stringify(this.hisAppRedPacket);
+          editCompanyConfig(this.hisAppRedPacketForm).then(response => {
+            if (response.code === 200) {
+              this.msgSuccess("修改成功");
+              this.hisAppRedPacketOpen.open=false;
+            }
+          })
+
+    },
+
+    onSubmitCompanyRedPacket(){
+      this.redPacketConfigForm.configValue=JSON.stringify(this.redPacketConfig);
+          editCompanyConfig(this.redPacketConfigForm).then(response => {
+            if (response.code === 200) {
+              this.msgSuccess("修改成功");
+              this.redPacketConfigOpen.open=false;
+            }
+          })
+    },
     getList() {
       this.loading = true
       listCompany(this.queryParams).then(response => {
@@ -1516,6 +1668,8 @@ export default {
       })
     },
 
+
+
     // 解绑销售易账号
     handleUnbindXsy(row) {
       this.$confirm('是否确认解绑当前销售易账号?', '提示', {
@@ -1534,6 +1688,31 @@ export default {
         }
       }).catch(() => {})
     },
+
+    handleRedPacket(row){
+      const companyId=row.companyId;
+
+      getCompanyConfigKey('redPacket:config',companyId).then(res=>{
+        if(res.data.configValue!=null){
+          this.redPacketConfig=JSON.parse(res.data.configValue);
+        }
+        this.redPacketConfigForm.configId=res.data.configId;
+        this.redPacketConfigOpen.open=true;
+      })
+    },
+
+    handleAppRedPacket(row){
+      const companyId=row.companyId;
+
+      getCompanyConfigKey('his:AppRedPacket',companyId).then(res=>{
+        if(res.data.configValue!=null){
+          this.hisAppRedPacket=JSON.parse(res.data.configValue);
+        }
+        this.hisAppRedPacketForm.configId=res.data.configId;
+        this.hisAppRedPacketOpen.open=true;
+      })
+    }
+
   }
 }
 </script>

+ 149 - 73
src/views/his/courserCoupon/index.vue

@@ -1,6 +1,12 @@
 <template>
   <div class="app-container">
-    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+    <el-form
+      :model="queryParams"
+      ref="queryForm"
+      :inline="true"
+      v-show="showSearch"
+      label-width="68px"
+    >
       <!-- <el-form-item label="有效期" prop="limitTime">
         <el-date-picker clearable size="small"
           v-model="queryParams.limitTime"
@@ -65,8 +71,16 @@
         />
       </el-form-item>
       <el-form-item>
-        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
-        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+        <el-button
+          type="primary"
+          icon="el-icon-search"
+          size="mini"
+          @click="handleQuery"
+          >搜索</el-button
+        >
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery"
+          >重置</el-button
+        >
       </el-form-item>
     </el-form>
 
@@ -79,7 +93,8 @@
           size="mini"
           @click="handleAdd"
           v-hasPermi="['his:courserCoupon:add']"
-        >新增</el-button>
+          >新增</el-button
+        >
       </el-col>
       <el-col :span="1.5">
         <el-button
@@ -90,7 +105,8 @@
           :disabled="single"
           @click="handleUpdate"
           v-hasPermi="['his:courserCoupon:edit']"
-        >修改</el-button>
+          >修改</el-button
+        >
       </el-col>
       <el-col :span="1.5">
         <el-button
@@ -101,7 +117,8 @@
           :disabled="multiple"
           @click="handleDelete"
           v-hasPermi="['his:courserCoupon:remove']"
-        >删除</el-button>
+          >删除</el-button
+        >
       </el-col>
       <el-col :span="1.5">
         <el-button
@@ -112,27 +129,51 @@
           :loading="exportLoading"
           @click="handleExport"
           v-hasPermi="['his:courserCoupon:export']"
-        >导出</el-button>
+          >导出</el-button
+        >
       </el-col>
-      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+      <right-toolbar
+        :showSearch.sync="showSearch"
+        @queryTable="getList"
+      ></right-toolbar>
     </el-row>
 
-    <el-table border v-loading="loading" :data="courserCouponList" @selection-change="handleSelectionChange">
+    <el-table
+      border
+      v-loading="loading"
+      :data="courserCouponList"
+      @selection-change="handleSelectionChange"
+    >
       <el-table-column type="selection" width="55" align="center" />
       <el-table-column label="标题" align="center" prop="id" />
-      <el-table-column label="有效期" align="center" prop="limitTime" width="180">
+      <el-table-column
+        label="有效期"
+        align="center"
+        prop="limitTime"
+        width="180"
+      >
         <template slot-scope="scope">
-          <span>{{ parseTime(scope.row.limitTime, '{y}-{m}-{d}') }}</span>
+          <span>{{ parseTime(scope.row.limitTime, "{y}-{m}-{d}") }}</span>
         </template>
       </el-table-column>
       <el-table-column label="数量" align="center" prop="number" />
       <el-table-column label="剩余数量" align="center" prop="remainNumber" />
-      <el-table-column label="状态" align="center" prop="status" />
-      <el-table-column label="领取后有效期" align="center" prop="limitDay" />
-      <el-table-column label="有效期类别 1 过期时间 2 领取后有效期" align="center" prop="limitType" />
-      <el-table-column label="每人可领取数量" align="center" prop="limitCount" />
+      <el-table-column label="状态" align="center" prop="status">
+        <template slot-scope="scope">
+          <dict-tag :options="statusOptions" :value="scope.row.status" />
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="每人可领取数量"
+        align="center"
+        prop="limitCount"
+      />
       <el-table-column label="标题" align="center" prop="title" />
-      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+      <el-table-column
+        label="操作"
+        align="center"
+        class-name="small-padding fixed-width"
+      >
         <template slot-scope="scope">
           <el-button
             size="mini"
@@ -140,20 +181,22 @@
             icon="el-icon-edit"
             @click="handleUpdate(scope.row)"
             v-hasPermi="['his:courserCoupon:edit']"
-          >修改</el-button>
+            >修改</el-button
+          >
           <el-button
             size="mini"
             type="text"
             icon="el-icon-delete"
             @click="handleDelete(scope.row)"
             v-hasPermi="['his:courserCoupon:remove']"
-          >删除</el-button>
+            >删除</el-button
+          >
         </template>
       </el-table-column>
     </el-table>
 
     <pagination
-      v-show="total>0"
+      v-show="total > 0"
       :total="total"
       :page.sync="queryParams.pageNum"
       :limit.sync="queryParams.pageSize"
@@ -161,41 +204,42 @@
     />
 
     <!-- 添加或修改课程优惠券对话框 -->
-    <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
-      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
-        <el-form-item label="有效期" prop="limitTime">
-          <el-date-picker clearable size="small"
+    <el-dialog :title="title" :visible.sync="open" width="700px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="120px">
+        <el-form-item label="优惠券名称" prop="title">
+          <el-input v-model="form.title" placeholder="请输入优惠券名称" />
+        </el-form-item>
+        <el-form-item label="过期时间" prop="limitTime">
+          <el-date-picker
+            clearable
+            size="small"
             v-model="form.limitTime"
             type="date"
             value-format="yyyy-MM-dd"
-            placeholder="选择有效期">
+            placeholder="选择过期时间"
+          >
           </el-date-picker>
         </el-form-item>
         <el-form-item label="数量" prop="number">
-          <el-input v-model="form.number" placeholder="请输入数量" />
+          <el-input-number
+            v-model="form.number"
+            :min="0"
+            label="请输入数量"
+          ></el-input-number>
         </el-form-item>
-        <el-form-item label="剩余数量" prop="remainNumber">
-          <el-input v-model="form.remainNumber" placeholder="请输入剩余数量" />
+        <el-form-item label="用户可领取数量" prop="limitCount">
+          <el-input-number
+            v-model="form.limitCount"
+            :min="1"
+            label="请输入用户可领取数量"
+          ></el-input-number>
         </el-form-item>
-        <el-form-item label="状态">
+        <el-form-item label="状态" prop="status">
           <el-radio-group v-model="form.status">
-            <el-radio label="1">请选择字典生成</el-radio>
+            <el-radio :label="1">正常</el-radio>
+            <el-radio :label="0">停用</el-radio>
           </el-radio-group>
         </el-form-item>
-        <el-form-item label="领取后有效期" prop="limitDay">
-          <el-input v-model="form.limitDay" placeholder="请输入领取后有效期" />
-        </el-form-item>
-        <el-form-item label="有效期类别 1 过期时间 2 领取后有效期" prop="limitType">
-          <el-select v-model="form.limitType" placeholder="请选择有效期类别 1 过期时间 2 领取后有效期">
-            <el-option label="请选择字典生成" value="" />
-          </el-select>
-        </el-form-item>
-        <el-form-item label="每人可领取数量" prop="limitCount">
-          <el-input v-model="form.limitCount" placeholder="请输入每人可领取数量" />
-        </el-form-item>
-        <el-form-item label="标题" prop="title">
-          <el-input v-model="form.title" placeholder="请输入标题" />
-        </el-form-item>
       </el-form>
       <div slot="footer" class="dialog-footer">
         <el-button type="primary" @click="submitForm">确 定</el-button>
@@ -206,7 +250,14 @@
 </template>
 
 <script>
-import { listCourserCoupon, getCourserCoupon, delCourserCoupon, addCourserCoupon, updateCourserCoupon, exportCourserCoupon } from "@/api/his/courserCoupon";
+import {
+  listCourserCoupon,
+  getCourserCoupon,
+  delCourserCoupon,
+  addCourserCoupon,
+  updateCourserCoupon,
+  exportCourserCoupon,
+} from "@/api/his/courserCoupon";
 
 export default {
   name: "CourserCoupon",
@@ -243,23 +294,37 @@ export default {
         limitDay: null,
         limitType: null,
         limitCount: null,
-        title: null
+        title: null,
       },
       // 表单参数
       form: {},
       // 表单校验
       rules: {
-      }
+        title: [{ required: true, message: "标题不能为空", trigger: "blur" }],
+        limitTime: [
+          { required: true, message: "有效期不能为空", trigger: "blur" },
+        ],
+        status: [
+          { required: true, message: "状态不能为空", trigger: "change" },
+        ],
+      },
+      statusOptions: [
+        { dictLabel: "正常", dictValue: 1 },
+        { dictLabel: "停用", dictValue: 0 },
+      ],
     };
   },
   created() {
     this.getList();
+    this.getDicts("sys_company_status").then((response) => {
+      this.statusOptions = response.data;
+    });
   },
   methods: {
     /** 查询课程优惠券列表 */
     getList() {
       this.loading = true;
-      listCourserCoupon(this.queryParams).then(response => {
+      listCourserCoupon(this.queryParams).then((response) => {
         this.courserCouponList = response.rows;
         this.total = response.total;
         this.loading = false;
@@ -278,12 +343,12 @@ export default {
         limitTime: null,
         number: null,
         remainNumber: null,
-        status: 0,
+        status: 1,
         updateTime: null,
-        limitDay: null,
+        limitDay: 1,
         limitType: null,
-        limitCount: null,
-        title: null
+        limitCount: 1,
+        title: null,
       };
       this.resetForm("form");
     },
@@ -299,9 +364,9 @@ export default {
     },
     // 多选框选中数据
     handleSelectionChange(selection) {
-      this.ids = selection.map(item => item.id)
-      this.single = selection.length!==1
-      this.multiple = !selection.length
+      this.ids = selection.map((item) => item.id);
+      this.single = selection.length !== 1;
+      this.multiple = !selection.length;
     },
     /** 新增按钮操作 */
     handleAdd() {
@@ -309,11 +374,12 @@ export default {
       this.open = true;
       this.title = "添加课程优惠券";
     },
+
     /** 修改按钮操作 */
     handleUpdate(row) {
       this.reset();
-      const id = row.id || this.ids
-      getCourserCoupon(id).then(response => {
+      const id = row.id || this.ids;
+      getCourserCoupon(id).then((response) => {
         this.form = response.data;
         this.open = true;
         this.title = "修改课程优惠券";
@@ -321,16 +387,16 @@ export default {
     },
     /** 提交按钮 */
     submitForm() {
-      this.$refs["form"].validate(valid => {
+      this.$refs["form"].validate((valid) => {
         if (valid) {
           if (this.form.id != null) {
-            updateCourserCoupon(this.form).then(response => {
+            updateCourserCoupon(this.form).then((response) => {
               this.msgSuccess("修改成功");
               this.open = false;
               this.getList();
             });
           } else {
-            addCourserCoupon(this.form).then(response => {
+            addCourserCoupon(this.form).then((response) => {
               this.msgSuccess("新增成功");
               this.open = false;
               this.getList();
@@ -342,32 +408,42 @@ export default {
     /** 删除按钮操作 */
     handleDelete(row) {
       const ids = row.id || this.ids;
-      this.$confirm('是否确认删除课程优惠券编号为"' + ids + '"的数据项?', "警告", {
+      this.$confirm(
+        '是否确认删除课程优惠券编号为"' + ids + '"的数据项?',
+        "警告",
+        {
           confirmButtonText: "确定",
           cancelButtonText: "取消",
-          type: "warning"
-        }).then(function() {
+          type: "warning",
+        }
+      )
+        .then(function () {
           return delCourserCoupon(ids);
-        }).then(() => {
+        })
+        .then(() => {
           this.getList();
           this.msgSuccess("删除成功");
-        }).catch(() => {});
+        })
+        .catch(() => {});
     },
     /** 导出按钮操作 */
     handleExport() {
       const queryParams = this.queryParams;
-      this.$confirm('是否确认导出所有课程优惠券数据项?', "警告", {
-          confirmButtonText: "确定",
-          cancelButtonText: "取消",
-          type: "warning"
-        }).then(() => {
+      this.$confirm("是否确认导出所有课程优惠券数据项?", "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning",
+      })
+        .then(() => {
           this.exportLoading = true;
           return exportCourserCoupon(queryParams);
-        }).then(response => {
+        })
+        .then((response) => {
           this.download(response.msg);
           this.exportLoading = false;
-        }).catch(() => {});
-    }
-  }
+        })
+        .catch(() => {});
+    },
+  },
 };
 </script>

+ 99 - 2
src/views/his/integralGoods/index.vue

@@ -101,12 +101,24 @@
           v-hasPermi="['his:integralGoods:handleImport']"
               >导入</el-button>
       </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          icon="el-icon-edit-outline"
+          size="mini"
+          :disabled="multiple"
+          @click="handleBatchUpdate"
+          v-hasPermi="['his:integralGoods:edit']"
+        >批量修改积分和金额</el-button>
+      </el-col>
 
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
     <el-table v-loading="loading" border :data="integralGoodsList" @selection-change="handleSelectionChange">
       <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="编号" align="center" prop="goodsId" />
       <el-table-column label="封面图" align="center" prop="imgUrl" >
         <template slot-scope="scope">
           <el-popover
@@ -197,7 +209,7 @@
            <el-input-number v-model="form.otPrice" :precision="2" :step="0.1" ></el-input-number>
         </el-form-item>
         <el-form-item label="所需积分" prop="integral">
-          <el-input-number v-model="form.integral"  :min="0"  label="所需积分"></el-input-number>
+          <el-input-number v-model="form.integral"  :min="1"  label="所需积分"></el-input-number>
         </el-form-item>
         <el-form-item label="支付金额" prop="cash">
           <el-input-number v-model="form.cash"  :min="0" :precision="2" :step="0.1"  label="需支付金额"></el-input-number>
@@ -276,12 +288,34 @@
           </div>
         </el-dialog>
 
+    <!-- 批量修改积分和金额对话框 -->
+    <el-dialog :title="batchTitle" :visible.sync="batchOpen" width="500px" append-to-body>
+      <el-form ref="batchForm" :model="batchForm" :rules="batchRules" label-width="120px">
+        <el-form-item label="所需积分" prop="integral">
+          <el-input-number v-model="batchForm.integral" :min="1" placeholder="请输入所需积分"></el-input-number>
+        </el-form-item>
+        <el-form-item label="需支付金额" prop="cash">
+          <el-input-number v-model="batchForm.cash" :min="0" :precision="2" :step="0.1" placeholder="请输入支付金额"></el-input-number>
+        </el-form-item>
+        <el-alert
+          title="至少填写一项,留空项将不修改"
+          type="info"
+          :closable="false"
+          show-icon
+        />
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitBatchForm" :loading="batchLoading">确 定</el-button>
+        <el-button @click="batchOpen = false">取 消</el-button>
+      </div>
+    </el-dialog>
+
 
   </div>
 </template>
 
 <script>
-import { importTemplate,listIntegralGoods, getIntegralGoods, delIntegralGoods, addIntegralGoods, updateIntegralGoods, exportIntegralGoods } from "@/api/his/integralGoods";
+import { importTemplate,listIntegralGoods, getIntegralGoods, delIntegralGoods, addIntegralGoods, updateIntegralGoods, exportIntegralGoods, batchUpdateIntegralGoods } from "@/api/his/integralGoods";
 import Material from '@/components/Material';
 import Editor from '@/components/Editor/wang';
 import { getToken } from "@/utils/auth";
@@ -322,6 +356,40 @@ export default {
               // 上传的地址
               url: process.env.VUE_APP_BASE_API + "/his/integralGoods/importData"
             },
+      // 批量修改
+      batchOpen: false,
+      batchTitle: "批量修改积分和金额",
+      batchLoading: false,
+      batchForm: {
+        integral: null,
+        cash: null
+      },
+      batchRules: {
+        integral: [
+          {
+            validator: (rule, value, callback) => {
+              if (this.batchForm.integral === null && this.batchForm.cash === null) {
+                callback(new Error('至少需要填写所需积分或需支付金额中的一项'));
+              } else {
+                callback();
+              }
+            },
+            trigger: 'change'
+          }
+        ],
+        cash: [
+          {
+            validator: (rule, value, callback) => {
+              if (this.batchForm.integral === null && this.batchForm.cash === null) {
+                callback(new Error('至少需要填写所需积分或需支付金额中的一项'));
+              } else {
+                callback();
+              }
+            },
+            trigger: 'change'
+          }
+        ]
+      },
       photoArr:[],
       imageArr:[],
       // 遮罩层
@@ -559,6 +627,35 @@ export default {
           this.msgSuccess("删除成功");
         }).catch(() => {});
     },
+    /** 批量修改按钮操作 */
+    handleBatchUpdate() {
+      this.batchForm = {
+        integral: null,
+        cash: null
+      };
+      this.batchOpen = true;
+    },
+    /** 提交批量修改 */
+    submitBatchForm() {
+      this.$refs["batchForm"].validate(valid => {
+        if (valid) {
+          const params = {
+            goodsIds: this.ids,
+            integral: this.batchForm.integral,
+            cash: this.batchForm.cash
+          };
+          this.batchLoading = true;
+          batchUpdateIntegralGoods(params).then(response => {
+            this.msgSuccess("批量修改成功");
+            this.batchOpen = false;
+            this.batchLoading = false;
+            this.getList();
+          }).catch(() => {
+            this.batchLoading = false;
+          });
+        }
+      });
+    },
     /** 导出按钮操作 */
     handleExport() {
       const queryParams = this.queryParams;

+ 393 - 0
src/views/qw/companySession/ConversationPanel.vue

@@ -0,0 +1,393 @@
+<template>
+  <div class="conversation-panel">
+    <!-- 配置未加载 -->
+    <div v-if="!configReady" class="loading-tip">
+      <i class="el-icon-loading"></i>
+      <p>正在加载配置...</p>
+    </div>
+
+    <!-- 未登录 -->
+    <div v-else-if="!isLoggedIn" class="login-area">
+      <div class="login-tip">请先扫码登录企微</div>
+      <div id="login-container"></div>
+    </div>
+
+    <!-- 已登录但未选择员工 -->
+    <div v-else-if="!staffUserId" class="empty-tip">
+      <i class="el-icon-info"></i>
+      <p>请在左侧选择一个企微员工</p>
+    </div>
+
+    <!-- 已登录但未选择客户 -->
+    <div v-else-if="!customerId" class="empty-tip">
+      <i class="el-icon-info"></i>
+      <p>请在左侧选择一个客户查看会话</p>
+    </div>
+
+    <!-- 加载中 -->
+    <div v-else-if="!isReady" class="loading-tip">
+      <i class="el-icon-loading"></i>
+      <p>正在加载会话记录...</p>
+    </div>
+
+    <!-- 会话记录为空 -->
+    <div v-else-if="msgList.length === 0" class="empty-tip">
+      <i class="el-icon-info"></i>
+      <p>暂无数据</p>
+    </div>
+
+    <!-- 聊天窗口 -->
+    <div v-else id="chat-container"></div>
+  </div>
+</template>
+
+<script>
+import * as ww from '@wecom/jssdk';
+import defaultStaffAvatar from '@/assets/images/user.png';
+import { qwLogin, qwSignature, qwConversations, getQwSessionConfig } from '@/api/qw/companySession';
+
+const LOGIN_STORAGE_KEY = 'wecom_session_expire';
+
+export default {
+  name: 'ConversationPanel',
+  props: {
+    customerId: { type: String, default: null },
+    customerAvatar: { type: String, default: '' },
+    staffUserId: { type: String, default: null }
+  },
+  data() {
+    return {
+      isLoggedIn: false,
+      isReady: false,
+      msgList: [],
+      chatInstance: null,
+      _sdkInited: false,
+      defaultStaffAvatar: defaultStaffAvatar,
+      // 从后台获取的企微配置
+      config: {
+        corpid: '',
+        agentid: '',
+        agentSecret: '',
+        domain:''
+      },
+      configReady: false,
+    };
+  },
+  watch: {
+    customerId(newId, oldId) {
+      if (newId && newId !== oldId && this.isLoggedIn && this.staffUserId && this.configReady) {
+        this.reloadChat();
+      }
+    },
+    staffUserId(newStaffId, oldStaffId) {
+      if (newStaffId && newStaffId !== oldStaffId && this.customerId && this.isLoggedIn && this.configReady) {
+        this.reloadChat();
+      }
+    }
+  },
+  async mounted() {
+    // 第一步:获取配置
+    try {
+      const configRes = await getQwSessionConfig();
+      const configData = this._extractResponse(configRes);
+      if (!configData.corpid || !configData.agentid) {
+        throw new Error('配置数据不完整');
+      }
+      this.config = {
+        corpid: configData.corpid,
+        agentid: String(configData.agentid),
+        agentSecret: configData.agentSecret || '',
+        domain: configData.domain || ''
+      };
+      this.configReady = true;
+    } catch (e) {
+      console.error('获取企微配置失败:', e);
+      this.$message.error('获取企微配置失败,请刷新重试');
+      return;
+    }
+
+    // 第二步:后续登录流程
+    const urlParams = new URLSearchParams(window.location.search);
+    const code = urlParams.get('code');
+
+    if (code) {
+      const newUrl = window.location.origin + window.location.pathname;
+      window.history.replaceState({}, '', newUrl);
+      await this.handleLogin(code);
+      this.storeLoginState();
+      if (this.customerId && this.staffUserId) {
+        await this.loadAndRender();
+      }
+    } else {
+      if (this.checkLoginState()) {
+        this.isLoggedIn = true;
+        if (this.customerId && this.staffUserId) {
+          await this.loadAndRender();
+        }
+      } else {
+        this.$nextTick(() => this.createLoginPanel());
+      }
+    }
+  },
+  async activated() {
+    if (this.isLoggedIn && this.configReady && this.customerId && this.staffUserId) {
+      await this.loadAndRender();
+    }
+  },
+  methods: {
+    // 刷新并重新渲染(客户/员工切换时)
+    async reloadChat() {
+      this.isReady = false;
+      await this.fetchMsgList(this.customerId, this.staffUserId);
+      this.isReady = true;
+      this.$nextTick(() => this.renderOrUpdateChat());
+    },
+    // 从接口加载数据并渲染(activated / 首次加载)
+    async loadAndRender() {
+      this.isReady = false;
+      try {
+        await this.fetchMsgList(this.customerId, this.staffUserId);
+        this.isReady = true;
+        await this.$nextTick();
+        const container = document.getElementById('chat-container');
+        if (!container || container.offsetHeight === 0) {
+          await new Promise(resolve => setTimeout(resolve, 300));
+          await this.$nextTick();
+          const retryContainer = document.getElementById('chat-container');
+          if (!retryContainer || retryContainer.offsetHeight === 0) {
+            console.error('容器不可见,放弃渲染');
+            return;
+          }
+        }
+        this._sdkInited = false;
+        await this.renderOrUpdateChat();
+      } catch (err) {
+        console.error('初始化失败', err);
+        this.isReady = true;
+      }
+    },
+
+    // ---------- 登录相关 ----------
+    async handleLogin(code) {
+      const res = await qwLogin({ code });
+      const resp = this._extractResponse(res);
+      if (resp.errcode !== 0) {
+        throw new Error(resp.errmsg || '登录失败');
+      }
+      this.isLoggedIn = true;
+    },
+    createLoginPanel() {
+      //const redirectUri = 'http://sestest.ylrzcloud.com/companySale/companySession';
+      const redirectUri = this.config.domain+'/companySale/companySession';
+      console.log("回调地址:",redirectUri);
+      ww.createWWLoginPanel({
+        el: document.getElementById('login-container'),
+        params: {
+          login_type: 'CorpApp',
+          appid: this.config.corpid,
+          agentid: this.config.agentid,
+          redirect_uri: redirectUri,
+          redirect_type: 'callback',
+          state: 'state_' + Date.now()
+        },
+        onLoginSuccess: async ({ code }) => {
+          await this.handleLogin(code);
+          this.storeLoginState();
+          if (this.customerId && this.staffUserId) {
+            await this.loadAndRender();
+          }
+        },
+        onLoginError: (err) => {
+          console.error('登录面板错误:', err);
+          this.clearLoginState();
+        }
+      });
+    },
+
+    // ---------- 本地存储 ----------
+    checkLoginState() {
+      const stored = localStorage.getItem(LOGIN_STORAGE_KEY);
+      if (!stored) return false;
+      return Date.now() < parseInt(stored, 10);
+    },
+    storeLoginState() {
+      localStorage.setItem(LOGIN_STORAGE_KEY, (Date.now() + 30 * 60 * 1000).toString());
+    },
+    clearLoginState() {
+      localStorage.removeItem(LOGIN_STORAGE_KEY);
+    },
+
+    // ---------- 数据获取 ----------
+    async fetchMsgList(customerId, staffUserId) {
+      try {
+        const data = await qwConversations({ customerId, staffUserId });
+        this.msgList = data.msgList || [];
+      } catch (e) {
+        console.error('获取会话记录失败:', e);
+        this.msgList = [];
+      }
+    },
+
+    // ---------- 签名 ----------
+    async getAgentConfigSignature() {
+      const currentUrl = window.location.href.split('#')[0];
+      const res = await qwSignature({ url: currentUrl });
+      const data = this._extractResponse(res);
+      return {
+        timestamp: data.timestamp,
+        nonceStr: data.nonceStr,
+        signature: data.signature
+      };
+    },
+
+    // ---------- SDK 初始化 ----------
+    async initSDKOnce() {
+      if (this._sdkInited) return true;
+      try {
+        await ww.register({
+          corpId: this.config.corpid,
+          agentId: this.config.agentid,
+          jsApiList: ['selectExternalContact', 'shareAppMessage', 'wwapp.invokeJsApiByCallInfo'],
+          getAgentConfigSignature: () => this.getAgentConfigSignature()
+        });
+        await ww.initOpenData();
+        this._sdkInited = true;
+        return true;
+      } catch (e) {
+        console.error('SDK 初始化失败:', e);
+        return false;
+      }
+    },
+
+    // ---------- 渲染聊天组件 ----------
+    async renderOrUpdateChat() {
+      const container = document.getElementById('chat-container');
+      if (!container) return;
+
+      if (this.chatInstance) {
+        try {
+          this.chatInstance.setData({ msgList: this.msgList });
+          return;
+        } catch (e) {
+          console.warn('setData 失败,重建组件', e);
+          this.chatInstance = null;
+        }
+      }
+
+      const ok = await this.initSDKOnce();
+      if (!ok) return;
+
+      const factory = ww.createOpenDataFrameFactory();
+      if (!factory) return;
+
+      const templateData = {
+        msgList: this.msgList,
+        customerAvatar: this.customerAvatar,
+        defaultStaffAvatar: this.defaultStaffAvatar
+      };
+
+      this.chatInstance = factory.createOpenDataFrame({
+        el: container,
+        template: `
+          <view wx:for="{{data.msgList}}" wx:for-item="msg" wx:key="msgid" style="margin-bottom: 15px;">
+            <view style="text-align: center; margin-bottom: 8px;">
+              <text style="background:red; color:white; padding:4px;">{{ msg.displayTime }}</text>
+            </view>
+            <view
+              style="display: flex; flex-direction: row; {{msg.sender.type == 1 ? 'justify-content: flex-end;' : 'justify-content: flex-start;'}}">
+              <image wx:if="{{msg.sender.type == 2}}" src="{{data.customerAvatar}}"
+                     style="width: 36px; height: 36px; border-radius: 4px; margin-right: 10px; background: #fff;"></image>
+              <view
+                style="max-width: 70%; padding: 10px 14px; border-radius: 8px; word-break: break-all; background: {{msg.sender.type == 1 ? '#95ec69' : '#ffffff'}}; box-shadow: 0 1px 3px rgba(0,0,0,0.1); display: flex; align-items: center;">
+                <ww-open-message message-id="{{msg.msgid}}" secret-key="{{msg.secretKey}}" open-type="viewMessage"/>
+              </view>
+              <image wx:if="{{msg.sender.type == 1}}" src="{{data.defaultStaffAvatar}}"
+                     style="width: 36px; height: 36px; border-radius: 4px; margin-left: 10px; background: #fff;"></image>
+            </view>
+          </view>
+        `,
+        data: templateData,
+        error: (e) => {
+          console.error('[企微组件] 错误', e);
+          if (e && e.errCode === 42006) {
+            this.clearLoginState();
+            this.isLoggedIn = false;
+            this.isReady = false;
+            this.$nextTick(() => this.createLoginPanel());
+          }
+        },
+        handleModal({ modalUrl }) {
+          window.open(modalUrl, '_blank');
+          return true;
+        }
+      });
+    },
+
+    // 通用提取:兼容多种若依返回结构
+    _extractResponse(res) {
+      if (!res) return {};
+      // 直接就是目标对象(包含 errcode、timestamp 或 corpid)
+      if (res.errcode !== undefined || res.timestamp || res.corpid) return res;
+      // res.data 是目标对象
+      if (res.data && (res.data.errcode !== undefined || res.data.timestamp || res.data.corpid)) return res.data;
+      // res.data.data 是目标对象(少数多层包装)
+      if (res.data && res.data.data && (res.data.data.errcode !== undefined || res.data.data.timestamp || res.data.data.corpid)) return res.data.data;
+      // 兜底
+      return res.data || res;
+    }
+  }
+};
+</script>
+
+<style>
+#chat-container,
+#chat-container > div,
+#chat-container iframe {
+  width: 100% !important;
+  height: 100% !important;
+  min-width: 100% !important;
+  min-height: 100% !important;
+  overflow: hidden;
+}
+
+.conversation-panel {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  background: #f5f6f7;
+  position: relative;
+}
+
+.login-area, .empty-tip, .loading-tip {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  height: 100%;
+  flex: 1;
+}
+
+.login-tip {
+  font-size: 18px;
+  color: #666;
+  margin-bottom: 24px;
+}
+
+.empty-tip, .loading-tip {
+  color: #999;
+  font-size: 16px;
+}
+
+.empty-tip i, .loading-tip i {
+  font-size: 48px;
+  margin-bottom: 16px;
+}
+
+#chat-container {
+  flex: 1;
+  width: 100%;
+  height: 100%;
+  background: #f0f2f5;
+}
+</style>

+ 244 - 0
src/views/qw/companySession/index.vue

@@ -0,0 +1,244 @@
+<template>
+  <div class="session-layout">
+    <!-- 左侧:客户列表 -->
+    <div class="left-panel">
+      <div class="left-panel-content">
+        <el-form :model="queryParams" ref="queryForm" :inline="true" label-width="100px">
+          <el-form-item label="企微主体" prop="corpId">
+            <el-select v-model="queryParams.corpId" placeholder="企微主体" size="small" clearable @change="handleCorpChange">
+              <el-option
+                v-for="corp in corpList"
+                :key="corp.corpId"
+                :label="corp.corpName"
+                :value="corp.corpId"
+              />
+            </el-select>
+          </el-form-item>
+
+          <el-form-item label="企微员工" prop="qwUserId">
+            <el-select
+              v-model="queryParams.qwUserId"
+              placeholder="请选择员工"
+              size="small"
+              clearable
+              filterable
+              :disabled="!queryParams.corpId"
+            >
+              <el-option
+                v-for="staff in staffList"
+                :key="staff.id"
+                :label="staff.qwUserName"
+                :value="staff.id"
+              />
+            </el-select>
+          </el-form-item>
+
+          <el-form-item>
+            <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+          </el-form-item>
+        </el-form>
+
+        <div class="left-table-wrapper">
+          <el-table
+            v-loading="loading"
+            :data="externalContactList"
+            border
+            highlight-current-row
+            @row-click="handleRowClick"
+            height="100%"
+          >
+            <el-table-column label="头像" align="center" width="80px">
+              <template slot-scope="scope">
+                <el-popover placement="right" trigger="hover">
+                  <img slot="reference" :src="scope.row.avatar" width="40px" style="border-radius:50%" />
+                  <img :src="scope.row.avatar" style="max-width:200px" />
+                </el-popover>
+              </template>
+            </el-table-column>
+            <el-table-column label="客户名称" prop="name" width="100px" />
+            <el-table-column label="销售昵称" prop="qwUserName" width="100px" />
+            <el-table-column label="添加时间" prop="createTime" width="100px" />
+          </el-table>
+        </div>
+
+        <pagination
+          v-show="total > 0"
+          :total="total"
+          :page.sync="queryParams.pageNum"
+          :limit.sync="queryParams.pageSize"
+          @pagination="getList"
+        />
+      </div>
+    </div>
+
+    <!-- 右侧:会话详情面板 -->
+    <div class="right-panel">
+      <ConversationPanel
+        :key="conversationPanelKey"
+        :customerId="selectedCustomerId"
+        :customerAvatar="selectedCustomerAvatar"
+        :staffUserId="selectedStaffQwUserId"
+      />
+    </div>
+  </div>
+</template>
+
+<script>
+import {listExternalContact} from "@/api/qw/externalContact";
+import {getCompanyList} from "@/api/company/company";
+import {getAllUserlist} from "@/api/company/companyUser";
+import {allCorp} from "@/api/qw/qwCompany";
+import {listAllQwUserList} from "@/api/qw/user";
+import ConversationPanel from "./ConversationPanel.vue";
+
+export default {
+  name: "CompanySession",
+  components: {ConversationPanel},
+  data() {
+    return {
+      loading: false,
+      total: 0,
+      externalContactList: [],
+      companyUserNameList: [],
+      corpList: [],
+      selectedCustomerId: null,
+      selectedCustomerAvatar: null,
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        companyId: null,
+        companyUserName: null,
+        corpId: null,
+        extId: null,
+        qwUserId: null
+      },
+      qwCompanyList: [],
+      staffList: [],
+      // 用于强制重建子组件
+      conversationPanelKey: 0
+    };
+  },
+  computed: {
+    selectedStaffQwUserId() {
+      if (!this.queryParams.qwUserId) return null;
+      const staff = this.staffList.find(s => s.id === this.queryParams.qwUserId);
+      return staff ? staff.qwUserId : null;
+    }
+  },
+  created() {
+    getCompanyList().then(response => {
+      this.qwCompanyList = response.data;
+      if (this.qwCompanyList && this.qwCompanyList.length > 0) {
+        this.queryParams.companyId = this.qwCompanyList[0].companyId;
+        this.getAllUserlist(this.queryParams.companyId);
+      }
+    });
+    allCorp().then(response => {
+      this.corpList = response.data || [];
+      if (this.corpList.length > 0) {
+        this.queryParams.corpId = this.corpList[0].corpId;
+        this.handleCorpChange(this.queryParams.corpId);
+      }
+    });
+  },
+  // 当从其他页面切回时,强制重建右侧会话面板
+  activated() {
+    this.conversationPanelKey++;
+  },
+  methods: {
+    handleCorpChange(corpId) {
+      this.queryParams.qwUserId = null;
+      if (corpId) {
+        this.fetchQwUserList(corpId);
+      } else {
+        this.staffList = [];
+      }
+    },
+    async fetchQwUserList(corpId) {
+      try {
+        const res = await listAllQwUserList({corpId});
+        if (res.code === 200) {
+          this.staffList = res.data || [];
+        } else if (Array.isArray(res)) {
+          this.staffList = res;
+        } else {
+          this.staffList = [];
+          console.warn('获取员工列表失败', res.msg);
+        }
+      } catch (e) {
+        console.error('获取员工列表异常', e);
+        this.staffList = [];
+      }
+    },
+    getAllUserlist(companyId) {
+      if (companyId) {
+        getAllUserlist({companyId}).then(response => {
+          this.companyUserNameList = response.data;
+        });
+      }
+    },
+    getList() {
+      this.loading = true;
+      listExternalContact(this.queryParams).then(response => {
+        this.externalContactList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      }).catch(() => {
+        this.loading = false;
+      });
+    },
+    handleQuery() {
+      if (!this.queryParams.qwUserId) {
+        this.$message.warning('请先选择企微员工');
+        return;
+      }
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    handleRowClick(row) {
+      this.selectedCustomerId = row.externalUserId;
+      this.selectedCustomerAvatar = row.avatar;
+    },
+  }
+};
+</script>
+
+<style scoped>
+.session-layout {
+  display: flex;
+  height: calc(100vh - 90px);
+  overflow: hidden;
+}
+
+.left-panel {
+  width: 420px;
+  min-width: 380px;
+  flex-shrink: 0;
+  border-right: 1px solid #e8e8e8;
+  background: #fff;
+  display: flex;
+  flex-direction: column;
+}
+
+.left-panel-content {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+  padding: 16px;
+  box-sizing: border-box;
+}
+
+.left-table-wrapper {
+  flex: 1;
+  overflow: hidden;
+  margin-bottom: 12px;
+}
+
+.right-panel {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+  position: relative;
+}
+</style>

+ 36 - 3
src/views/statistics/section/inline.vue

@@ -91,7 +91,11 @@
       <el-table-column prop="deletedNum" label="删除数"  align="center" />
       <el-table-column prop="orderNum" label="订单总数"  align="center" />
       <el-table-column prop="orderMoneyTotal" label="订单总金额(元)"  align="center" />
-      <el-table-column prop="redPackageMoneyTotal" label="红包总金额(元)"  align="center" />
+      <el-table-column :label="RewardType === 1 ? '红包总金额(元)' : '积分总消耗(红包转)'" align="center">
+        <template slot-scope="scope">
+          <span>{{ RewardType === 1 ? scope.row.redPackageMoneyTotal : scope.row.redPackageMoneyTotal * 1000 }}</span>
+        </template>
+      </el-table-column>
       <el-table-column prop="callNum" label="总拨打数"  align="center" />
       <el-table-column prop="receivePassNum" label="接通数"  align="center" />
       <el-table-column prop="receiveNotNum" label="未接通数"  align="center" />
@@ -154,6 +158,7 @@ import {getCompanyList} from "@/api/company/company";
 import {getUserList} from "@/api/company/companyUser";
 import SelectTree from "@/components/TreeSelect/index.vue";
 import { exportLineCustomer } from '@/api/crm/customer'
+import { getCourseConfigByRewardType } from '@/api/system/config'
 
 export default {
   name: "EmployeeStats",
@@ -174,6 +179,7 @@ export default {
       // 显示搜索条件
       showSearch: true,
       // 总条数
+      RewardType: 1,
       total: 0,
       // 员工统计表格数据
       statsList: [],
@@ -207,7 +213,15 @@ export default {
     };
   },
   created() {
-    this.getList();
+
+
+    this.getCourseByRewardType();
+
+    setTimeout(() => {
+      this.getList();
+    }, 200);
+
+
 
     // getCompanyList().then(response => {
     //   this.companys = response.data;
@@ -265,9 +279,16 @@ export default {
           const values = data.map(item => Number(item[column.property]) || 0);
           sums[index] = values.reduce((prev, curr) => prev + curr, 0);
 
-          if(['orderMoneyTotal', 'redPackageMoneyTotal'].includes(column.property)){
+          if(column.property === 'redPackageMoneyTotal'){
+            if(this.RewardType === 2){
+              sums[index] = sums[index] * 1000;
+            } else {
+              sums[index] = sums[index].toFixed(2);
+            }
+          } else if(column.property === 'orderMoneyTotal'){
             sums[index] = sums[index].toFixed(2);
           }
+
         }
         else if (rateColumns.includes(column.property)) {
           if (column.property === 'regRate') {
@@ -320,6 +341,12 @@ export default {
         this.queryParams.companyUserId = null;
       }
     },
+    getCourseByRewardType(){
+      getCourseConfigByRewardType().then(res=>{
+         this.RewardType=res.rewardType;
+      })
+    },
+
     /** 查询员工统计列表 */
     getList() {
       this.loading = true;
@@ -353,6 +380,12 @@ export default {
     },
     /** 搜索按钮操作 */
     handleQuery() {
+
+      if (!this.dateRange || this.dateRange.length !== 2) {
+        this.$message.warning('请先选择时间范围');
+        return;
+      }
+
       this.queryParams.pageNum = 1;
       this.getList();
     },

+ 55 - 0
src/views/system/config/config.vue

@@ -3059,6 +3059,35 @@
         </el-form>
       </el-tab-pane>
 
+      <el-tab-pane label="企微专区会话配置" name="qw.sessionConfig">
+        <el-form ref="form41" :model="form41" label-width="160px">
+          <el-form-item label="企业ID" prop="corpid">
+            <el-input v-model="form41.corpid" placeholder="请输入企业ID" style="width:400px"></el-input>
+          </el-form-item>
+          <el-form-item label="自建应用ID" prop="agentid">
+            <el-input v-model="form41.agentid" placeholder="请输入自建应用ID" style="width:400px"></el-input>
+          </el-form-item>
+          <el-form-item label="自建应用可信域名" prop="domain">
+            <el-input v-model="form41.domain" placeholder="请输入自建应用可信域名" style="width:400px"></el-input>
+          </el-form-item>
+          <el-form-item label="自建应用密钥" prop="agentSecret">
+            <el-input v-model="form41.agentSecret" placeholder="请输入自建应用密钥" style="width:400px"></el-input>
+          </el-form-item>
+          <el-form-item label="专区程序id" prop="programId">
+            <el-input v-model="form41.programId" placeholder="请输入专区程序id" style="width:400px"></el-input>
+          </el-form-item>
+          <el-form-item label="程序查询会话能力id" prop="fetchConversationAbilityId">
+            <el-input v-model="form41.fetchConversationAbilityId" placeholder="请输入查询会话能力id" style="width:400px"></el-input>
+          </el-form-item>
+          <el-form-item label="程序能力action" prop="abilityAction">
+            <el-input v-model="form41.abilityAction" placeholder="请输入专区程序能力action" style="width:400px"></el-input>
+          </el-form-item>
+          <div class="footer">
+            <el-button type="primary" @click="submitQwSessionConfig41">提交</el-button>
+          </div>
+        </el-form>
+      </el-tab-pane>
+
     </el-tabs>
 
 
@@ -3281,6 +3310,15 @@ export default {
         endIndex: 11,
         numberCalls:1,
       },
+      form41: {
+        corpid: '',
+        agentid: '',
+        domain: '',
+        agentSecret: '',
+        fetchConversationAbilityId: '',
+        programId: '',
+        abilityAction: '',
+      },
       storeProductScrmColumns:[],
       storeScrmColumns: [],
       photoArr: [],
@@ -3773,6 +3811,11 @@ export default {
         if (key == 'qw:config') {
           this.form15 = JSON.parse(response.data.configValue)
         }
+        if (key == 'qw.sessionConfig') {
+          if (response.data && response.data.configValue) {
+            this.form41 = JSON.parse(response.data.configValue);
+          }
+        }
         if (key == 'his.brand') {
           this.form16 = JSON.parse(response.data.configValue)
           console.log(this.form16)
@@ -4322,6 +4365,18 @@ export default {
 
       this.saveConfig40();
     },
+    submitQwSessionConfig41() {
+      const param = {
+        configId: this.configId,
+        configKey: this.configKey,
+        configValue: JSON.stringify(this.form41)
+      }
+      updateConfigByKey(param).then(response => {
+        if (response.code === 200) {
+          this.msgSuccess('修改成功')
+        }
+      })
+    },
 
     saveConfig40() {
       const param = {

+ 1 - 0
vue.config.js

@@ -13,6 +13,7 @@ const port = process.env.port || process.env.npm_config_port || 80 // 端口
 //官方vue.config.js 参考文档 https://cli.vuejs.org/zh/config/#css-loaderoptions
 // 这里只列一部分,具体配置参考文档
 module.exports = {
+  transpileDependencies: ['@wecom/jssdk'],
   // 部署生产环境和开发环境下的URL。
   // 默认情况下,Vue CLI 会假设你的应用是被部署在一个域名的根路径上
   // 例如 https://www.test.vip/。如果应用被部署在一个子路径上,你就需要用这个选项指定这个子路径。例如,如果你的应用被部署在 https://www.test.vip/admin/,则设置 baseUrl 为 /admin/。