Bläddra i källkod

管理后台创建应用-接入支付平台

wangqi49 2 veckor sedan
förälder
incheckning
6ba4e09382

BIN
.DS_Store


+ 10 - 6
webchat-front-admin/src/App.vue

@@ -22,15 +22,19 @@
           <router-link to="/account-admin-management" class="black-text"><a-menu-item key="02">管理员账号</a-menu-item></router-link>
           <router-link to="/account-group-management" class="black-text"><a-menu-item key="03">群组管理</a-menu-item></router-link>
           <router-link to="/account-robot-management" class="black-text"><a-menu-item key="04">机器人</a-menu-item></router-link>
-          <router-link to="/account-offical-management" class="black-text"><a-menu-item key="05">公众号</a-menu-item></router-link>
-          <router-link to="/account-black-management" class="black-text"><a-menu-item key="06">黑名单</a-menu-item></router-link>
+          <router-link to="/account-server-management" class="black-text"><a-menu-item key="05">服务号</a-menu-item></router-link>
+          <router-link to="/account-offical-management" class="black-text"><a-menu-item key="06">公众号</a-menu-item></router-link>
+          <router-link to="/account-black-management" class="black-text"><a-menu-item key="07">黑名单</a-menu-item></router-link>
         </a-sub-menu>
-        <a-menu-item key="2">
+        <a-sub-menu key="sub5">
           <template #icon>
-            <CalendarOutlined />
+            <SettingOutlined />
           </template>
-          <router-link to="/wallet-management" class="black-text">钱包管理</router-link>
-        </a-menu-item>
+          <template #title>支付平台</template>
+          <router-link to="/payment-app-management" class="black-text"><a-menu-item key="9">应用管理</a-menu-item></router-link>
+          <router-link to="/payment-order-management" class="black-text"><a-menu-item key="10">交易订单</a-menu-item></router-link>
+          <router-link to="/payment-detail-management" class="black-text"><a-menu-item key="11">交易明细</a-menu-item></router-link>
+        </a-sub-menu>
         <a-sub-menu key="sub1">
           <template #icon>
             <AppstoreOutlined />

+ 8 - 2
webchat-front-admin/src/router/index.js

@@ -5,7 +5,10 @@ import AccountGroupManagement from '../views/accountGroupManagement.vue'
 import AccountRobotManagement from '../views/accountRobotManagement.vue'
 import AccountOfficalManagement from '../views/accountOfficalManagement.vue'
 import AccountBlackManagement from '../views/accountBlackManagement.vue'
-import WalletManagement from '../views/walletManagement.vue'
+import AccountServerManagement from '../views/accountServerManagement.vue'
+import PaymentAppManagement from '../views/paymentAppManagement.vue'
+import PaymentOrderManagement from '../views/paymentOrderManagement.vue'
+import PaymentDetailManagement from '../views/paymentDetailManagement.vue'
 import OfficialArticleManagement from '../views/officialArticleManagement.vue'
 import EditArticle from '../views/editArticle.vue'
 
@@ -18,8 +21,11 @@ const router = createRouter({
     {path: '/account-group-management', component: AccountGroupManagement},
     {path: '/account-robot-management', component: AccountRobotManagement},
     {path: '/account-offical-management', component: AccountOfficalManagement},
+    {path: '/account-server-management', component: AccountServerManagement},
     {path: '/account-black-management', component: AccountBlackManagement},
-    {path: '/wallet-management', component: WalletManagement},
+    {path: '/payment-app-management', component: PaymentAppManagement},
+    {path: '/payment-order-management', component: PaymentOrderManagement},
+    {path: '/payment-detail-management', component: PaymentDetailManagement},
     {path: '/article-management', name: 'officialArticleManagement', component: OfficialArticleManagement},
     {path: '/editArticle', name: 'editArticle', component: EditArticle},
   ],

+ 308 - 0
webchat-front-admin/src/views/accountServerManagement.vue

@@ -0,0 +1,308 @@
+<template>
+    <div>
+      <a-breadcrumb>
+        <a-breadcrumb-item>WebChat</a-breadcrumb-item>
+        <a-breadcrumb-item>控制台</a-breadcrumb-item>
+        <a-breadcrumb-item>账号管理</a-breadcrumb-item>
+        <a-breadcrumb-item>服务号</a-breadcrumb-item>
+      </a-breadcrumb>
+  
+      <a-form :layout="formLayout" style="margin: 20px 0px 10px 0px; width: 100%">
+        <a-row :gutter="20"> 
+          <a-col :span="3">
+            <a-input v-model:value="userId" placeholder="用户ID"/>
+          </a-col>
+          <a-col :span="3">
+            <a-input v-model:value="mobile" placeholder="账号" />
+          </a-col>
+          <a-col :span="3">
+            <a-input v-model:value="userName" placeholder="用户名"/>
+          </a-col>
+          <a-col :span="5">
+            <a-button @click="resetSearch">重置</a-button>
+            <a-button type="primary" @click="handleSearch" style="margin-left: 20px; background-color: black;">搜索</a-button>
+          </a-col>
+          <a-col :span="3">
+            <a-button type="primary" @click="showRobotModal" style="margin-left: 50px; background-color: black;">创建服务号</a-button>
+          </a-col>
+        </a-row>
+      </a-form>
+  
+      <a-table :dataSource="dataSource" :columns="columns" :pagination="false" />
+    <a-pagination
+      :current="currentPage"
+      :pageSize="pageSize"
+      :total="total"
+      :showTotal="showTotal"
+      @change="handlePageChange"
+      style="float: right; margin-top: 20px"
+    />
+    <!-- 机器人弹窗组件 -->
+    <a-modal
+      v-model:visible="robotModalVisible"
+      title="创建公众号"
+      @ok="handleRobotCreate"
+      @cancel="handleRobotCancel"
+      okText="创建"
+      cancelText="关闭"
+    >
+      <a-form :form="robotForm">
+        <a-form-item label="上传头像">
+          <a-upload
+            name="avatar"
+            :action="uploadUrl"
+            list-type="picture-card"
+            :show-upload-list="false"
+            :before-upload="beforeUpload"
+          >
+            <div v-if="!robotAvatar">
+              <a-icon type="plus" />
+              <div style="margin-top: 8px">上传头像</div>
+            </div>
+            <div v-else>
+              <img :src="robotAvatar" alt="avatar" style="width: 100%; display: block" />
+            </div>
+          </a-upload>
+        </a-form-item>
+        <a-form-item label="创建账号">
+          <a-input v-model:value="robotAccount" placeholder="如:server_java"/>
+        </a-form-item>
+        <a-form-item label="账号名称">
+          <a-input v-model:value="robotAccountName" placeholder="如:微信支付助手"/>
+        </a-form-item>
+        <a-form-item label="账号描述">
+          <a-textarea v-model:value="robotDescription"  placeholder="如:动张提醒"/>
+        </a-form-item>
+      </a-form>
+    </a-modal>
+    </div>
+  </template>
+  
+  <script>
+import axios from 'axios';
+import { defineComponent, ref, onMounted, watch, nextTick } from 'vue';
+import { useRoute } from 'vue-router';
+import { Avatar, Modal, Form, FormItem, Input, Upload, message } from 'ant-design-vue';
+
+export default defineComponent({
+  setup() {
+    const route = useRoute();
+    const oauthCode = ref(route.query.oauthCode || new URLSearchParams(window.location.search).get('oauthCode'));
+    const dataSource = ref([]);
+    const userId = ref(''); 
+    const userName = ref(''); 
+    const mobile = ref(''); 
+    const columns = [
+      {
+        title: '账号ID',
+        dataIndex: 'userId', 
+        key: 'userId',
+      },
+      {
+        title: '账号',
+        dataIndex: 'mobile',
+        key: 'mobile',
+      },
+      {
+        title: '用户名',
+        dataIndex: 'userName',
+        key: 'userName',
+      },
+      {
+        title: '签名',
+        dataIndex: 'signature',
+        key: 'signature',
+      }
+    ];
+    const formLayout = 'horizontal'; 
+    const currentPage = ref(1); // 当前页码
+    const pageSize = ref(10); // 每页显示的记录数
+    const total = ref(0); // 总记录数
+    const pageTotal = ref(0); // 总页数
+    const robotModalVisible = ref(false); // 机器人弹窗的可见性
+    const robotAvatar = ref(null); // 机器人头像
+    const robotAccount = ref(''); // 机器人账号
+    const robotAccountName = ref(''); // 机器人名称
+    const robotDescription = ref(''); // 机器人描述
+    const robotForm = ref(null); // 机器人弹窗中的表单引用
+    const uploadUrl = '/admin-service/file/upload'; // 上传头像的 URL
+    const fetchData = async (page = 1, pageSize = 10, userId = '', userName = '', mobile = '') => {
+      try {
+        console.log("Fetching data with params:", userId, userName, mobile); 
+        const response = await axios.get(`/admin-service/account/page`, {
+          params: {
+            roleCode: 7,
+            pageNo: page,
+            pageSize: pageSize,
+            userId: userId, 
+            userName: userName, 
+            mobile: mobile 
+          },
+          headers: {
+            'oauth-code': oauthCode.value,
+            'origin-url': window.location.href
+          }
+        });
+        if (response.data.code === 40001) {
+          // 未登录
+          window.location.href = response.data.redirect_url;
+        }
+        dataSource.value = response.data.data;
+        // 更新分页信息
+        total.value = response.data.total;
+        pageTotal.value = response.data.pageTotal;
+        currentPage.value = response.data.pageNo;
+      } catch (error) {
+        console.error('Error fetching data:', error);
+      }
+    };
+
+    const handleSearch = async () => {
+      await nextTick();
+      await fetchData(1, pageSize.value, userId.value, userName.value, mobile.value);
+    };
+
+    const resetSearch = () => {
+      userId.value = '';
+      userName.value = '';
+      mobile.value = '';
+      handleSearch();
+    };
+
+    const handlePageChange = (page) => {
+      fetchData(page, pageSize.value, userId.value, userName.value, mobile.value);
+    };
+
+    const showTotal = (total) => {
+      return `共 ${total} 条`;
+    };
+
+    const showRobotModal = () => {
+      robotModalVisible.value = true; // 显示机器人弹窗
+    };
+
+    const handleRobotCancel = () => {
+      robotModalVisible.value = false; // 关闭机器人弹窗
+    };
+
+    const handleRobotCreate = async () => {
+        if (!robotAvatar.value) {
+            message.error('请上传头像');
+            return;
+        }
+        if (!robotAccount.value) {
+            message.error('请设置公众号账号');
+            return;
+        }
+        if (!robotAccountName.value) {
+            message.error('公众号名称为空');
+            return;
+        }
+        if (!robotDescription.value) {
+            message.error('公众号描述为空');
+            return;
+        }
+        try {
+            const response = await axios.post('/admin-service/account/createServerAccount', {
+                account: robotAccount.value,
+                accountName: robotAccountName.value,
+                signature: robotDescription.value,
+                accountPhoto: robotAvatar.value
+            });
+            if (response.data.code === 200) {
+                robotModalVisible.value = false;
+                message.success('服务号创建成功');
+                // 刷新列表
+                fetchData(); 
+            } else {
+                message.error(response.data.message || '服务号创建失败,请检查输入信息');
+            }
+        } catch (error) {
+            message.error('服务号创建失败,请稍后重试');
+        }
+    };
+
+    const handleAvatarUpload = async (file) => {
+        const formData = new FormData();
+        formData.append('file', file);
+        try {
+          const response = await axios.post(uploadUrl, formData, {
+            headers: {
+              'Content-Type': 'multipart/form-data',
+              'oauth-code': oauthCode.value,
+              'origin-url': window.location.href,
+              'upload-path': 'images/avatar'
+            }
+          });
+          if (response.data.code === 40001) {
+            window.location.href = response.data.redirect_url;
+          }
+          // 假设服务器返回头像的 URL
+          robotAvatar.value = response.data.data.url; 
+        } catch (error) {
+          console.error('Error uploading avatar:', error);
+        }
+      };
+
+      const beforeUpload = async (file) => {
+        // 文件验证逻辑,例如限制文件大小和类型
+        const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
+        const isLt2M = file.size / 1024 / 1024 < 2;
+
+        if (!isJpgOrPng) {
+          message.error('请上传 JPG 或 PNG 格式的图片');
+          return false;
+        }
+        if (!isLt2M) {
+          message.error('图片大小不能超过 2MB');
+          return false;
+        }
+        await handleAvatarUpload(file);
+        return false;
+      };
+
+
+    onMounted(() => {
+      fetchData();
+    });
+
+    watch([userId, userName, mobile], ([newUserId, newUserName, newMobile]) => {
+      console.log("Input values changed:", newUserId, newUserName, newMobile);
+    }, { immediate: true });
+
+
+    return {
+      dataSource,
+      columns,
+      fetchData,
+      handleSearch,
+      resetSearch, 
+      userId,
+      userName,
+      mobile,
+      formLayout,
+      currentPage,
+      pageSize,
+      total,
+      pageTotal,
+      handlePageChange,
+      showTotal,
+      robotModalVisible,
+      robotAvatar,
+      robotAccount,
+      robotAccountName,
+      robotDescription,
+      robotForm,
+      showRobotModal,
+      handleRobotCancel,
+      handleRobotCreate,
+      beforeUpload,
+      uploadUrl
+    };
+  },
+});
+  </script>
+  
+  <style scoped>
+  
+  </style>

+ 426 - 0
webchat-front-admin/src/views/paymentAppManagement.vue

@@ -0,0 +1,426 @@
+<template>
+  <div>
+    <a-breadcrumb>
+      <a-breadcrumb-item>WebChat</a-breadcrumb-item>
+      <a-breadcrumb-item>控制台</a-breadcrumb-item>
+      <a-breadcrumb-item>应用管理</a-breadcrumb-item>
+    </a-breadcrumb>
+
+    <a-form :layout="formLayout" style="margin: 20px 0px 10px 0px; width: 100%">
+      <a-row :gutter="20"> 
+        <a-col :span="3">
+          <a-input v-model:value="keywords" placeholder="关键词"/>
+        </a-col>
+        <a-col :span="3">
+            <a-button type="primary" @click="handleSearch" style="margin-left: 20px; background-color: black;">搜索</a-button>
+          </a-col>
+        <a-col :span="3">
+          <a-button type="primary" @click="showRobotModal" style="margin-left: 50px; background-color: black;">创建新应用</a-button>
+        </a-col>
+      </a-row>
+    </a-form>
+
+    <a-table :dataSource="dataSource" :columns="columns" :pagination="false" />
+  <a-pagination
+    :current="currentPage"
+    :pageSize="pageSize"
+    :total="total"
+    :showTotal="showTotal"
+    @change="handlePageChange"
+    style="float: right; margin-top: 20px"
+  />
+
+
+  <!-- 机器人弹窗组件 -->
+  <a-modal
+    v-model:visible="robotModalVisible"
+    title="创建应用,允许接入WebChat-Pay"
+    @ok="handleRobotCreate"
+    @cancel="handleRobotCancel"
+    okText="创建"
+    cancelText="取消"
+  >
+    <a-form :form="robotForm">
+      <a-form-item label="上传Logo">
+        <a-upload
+          name="avatar"
+          :action="uploadUrl"
+          list-type="picture-card"
+          :show-upload-list="false"
+          :before-upload="beforeUpload"
+        >
+          <div v-if="!robotAvatar">
+            <a-icon type="plus" />
+            <div style="margin-top: 8px">上传Logo</div>
+          </div>
+          <div v-else>
+            <img :src="robotAvatar" alt="avatar" style="width: 100%; display: block" />
+          </div>
+        </a-upload>
+      </a-form-item>
+      <a-form-item label="应用名称">
+        <a-input v-model:value="appName" placeholder="如:WebChat红包"/>
+      </a-form-item>
+      <a-form-item label="应用描述">
+        <a-textarea v-model:value="description"  placeholder="WebChat-UGC服务申请加入WebChat支付"/>
+      </a-form-item>
+    </a-form>
+  </a-modal>
+
+  <!-- 凭证弹窗 -->
+  <a-modal
+    v-model:visible="credentialVisible"
+    title="应用创建成功"
+    :footer="null"
+    :closable="false"
+    width="450px"
+    centered
+  >
+    <a-result
+      status="success"
+      title="应用创建成功!"
+      sub-title="请立即复制身份凭证,关闭后将无法再次查看"
+    >
+      <div class="credential-box">
+        <!-- 动态渲染凭证信息 -->
+        <div v-for="item in credentialsDisplay" :key="item.key" class="credential-item">
+          <div class="credential-label">{{ item.label }}</div>
+          <a-input-group compact>
+            <a-input 
+              :value="item.value" 
+              readonly 
+              class="credential-value"
+              :data-clipboard-text="item.value"
+            />
+            <a-tooltip title="复制">
+              <a-button @click="copyText(item.value)" type="link">
+                <template #icon><CopyOutlined /></template>
+              </a-button>
+            </a-tooltip>
+          </a-input-group>
+        </div>
+
+        <!-- 一键复制按钮 -->
+        <div class="copy-all">
+          <a-button 
+            type="primary" 
+            size="large"
+            @click="copyAllCredentials"
+            icon="copy"
+            block
+          >
+            一键复制全部凭证
+          </a-button>
+        </div>
+      </div>
+    </a-result>
+  </a-modal>
+  </div>
+</template>
+
+<script>
+import { h } from 'vue';
+import axios from 'axios';
+import { defineComponent, ref, onMounted, watch, nextTick } from 'vue';
+import { useRoute } from 'vue-router';
+import { Avatar, Modal, Form, FormItem, Input, Upload, message } from 'ant-design-vue';
+
+export default defineComponent({
+setup() {
+  const route = useRoute();
+  const oauthCode = ref(route.query.oauthCode || new URLSearchParams(window.location.search).get('oauthCode'));
+  const dataSource = ref([]);
+  const keywords = ref(''); 
+  const columns = [
+    {
+      title: '应用ID',
+      dataIndex: 'id', 
+      key: 'id',
+    },
+    {
+      title: '应用图标',
+      dataIndex: 'logo',
+      customRender: ({ text: logo }) => { 
+        return h('img', {
+          src: logo,
+          alt: '应用图标', 
+          style: { 
+            width: '40px',
+            height: '40px',
+            objectFit: 'cover' // 保持图片比例
+          },
+          onError: (e) => { // 处理图片加载失败
+            e.target.style.display = 'none' // 隐藏损坏图片
+            // 可选:显示默认占位图
+            // e.target.src = 'default-logo.png'
+          }
+        });
+      }
+    },
+    {
+      title: '应用名称',
+      dataIndex: 'name',
+      key: 'name',
+    },
+    {
+      title: '身份凭证',
+      dataIndex: 'accessKey',
+      key: 'accessKey',
+    },
+    {
+        title: '应用状态',
+        dataIndex: 'status',
+        customRender: ({ text }) => {
+          const statusMap = {
+            0: { text: '待发布', color: '#000' },
+            1: { text: '正常', color: '#017D6F' },
+            3: { text: '已删除', color: '#666' }
+          };
+          const status = statusMap[text] || {};
+          return h('span', { style: { color: status.color } }, status.text);
+        }
+      }
+  ];
+  const credentialVisible = ref(false);
+  const formLayout = 'horizontal'; 
+  const currentPage = ref(1); // 当前页码
+  const pageSize = ref(10); // 每页显示的记录数
+  const total = ref(0); // 总记录数
+  const pageTotal = ref(0); // 总页数
+  const robotModalVisible = ref(false); // 机器人弹窗的可见性
+  const robotAvatar = ref(null); // 机器人头像
+  const appName = ref(''); // 机器人账号
+  const description = ref(''); // 机器人描述
+  const robotForm = ref(null); // 机器人弹窗中的表单引用
+  const uploadUrl = '/admin-service/file/upload'; // 上传头像的 URL
+  // 格式化显示结构
+  const credentialsDisplay = ref([]);
+  const apiCredentials = ref({});
+
+
+  // 复制单个
+  const copyText = (text) => {
+    navigator.clipboard.writeText(text)
+      .then(() => message.success('复制成功'))
+      .catch(() => message.error('请手动选择内容复制'));
+  };
+
+  // 复制全部(带格式)
+  const copyAllCredentials = () => {
+    const content = [
+      `appId: ${apiCredentials.value.appId}`,
+      `access-key: ${apiCredentials.value.accessKey}`,
+      `secret-key: ${apiCredentials.value.secretKey}`
+    ].join('\n');
+
+    navigator.clipboard.writeText(content)
+      .then(() => message.success('已复制全部凭证'))
+      .catch(() => message.error('复制失败,请手动操作'));
+  };
+
+  const fetchData = async (page = 1, pageSize = 10, keywords = '') => {
+    try {
+      const response = await axios.get(`/admin-service/payment/app/page`, {
+        params: {
+          pageNo: page,
+          pageSize: pageSize,
+          keywords: keywords
+        },
+        headers: {
+          'oauth-code': oauthCode.value,
+          'origin-url': window.location.href
+        }
+      });
+      if (response.data.code === 40001) {
+        // 未登录
+        window.location.href = response.data.redirect_url;
+      }
+      dataSource.value = response.data.data;
+      // 更新分页信息
+      total.value = response.data.total;
+      pageTotal.value = response.data.pageTotal;
+      currentPage.value = response.data.pageNo;
+    } catch (error) {
+      console.error('Error fetching data:', error);
+    }
+  };
+
+  const handleSearch = async () => {
+    await nextTick();
+    await fetchData(1, pageSize.value, keywords.value);
+  };
+
+  const handlePageChange = (page) => {
+    fetchData(page, pageSize.value, keywords.value);
+  };
+
+  const showTotal = (total) => {
+    return `共 ${total} 条`;
+  };
+
+  const showRobotModal = () => {
+    robotModalVisible.value = true; // 显示机器人弹窗
+  };
+
+  const handleRobotCancel = () => {
+    robotModalVisible.value = false; // 关闭机器人弹窗
+  };
+
+  const handleRobotCreate = async () => {
+      if (!robotAvatar.value) {
+          message.error('请上传logo');
+          return;
+      }
+      if (!appName.value) {
+          message.error('应用名称');
+          return;
+      }
+      if (!description.value) {
+          message.error('应用描述');
+          return;
+      }
+      try {
+          const response = await axios.post('/admin-service/payment/app/create', {
+              name: appName.value,
+              description: description.value,
+              logo: robotAvatar.value
+          });
+          if (response.data.code === 200) {
+              robotModalVisible.value = false; // 关闭机器人弹窗
+              message.success('应用创建成功');  // 正确调用 message 组件
+              // 刷新列表
+              fetchData(); 
+              apiCredentials.value.appId = response.data.data.id;
+              apiCredentials.value.accessKey = response.data.data.accessKey;
+              apiCredentials.value.secretKey = response.data.data.secretKey;
+              credentialsDisplay.value = [{ 
+                                            key: 'appId',
+                                            label: '应用 ID',
+                                            value: response.data.data.id
+                                          },
+                                          { 
+                                            key: 'accessKey',
+                                            label: 'Access Key',
+                                            value: response.data.data.accessKey
+                                          },
+                                          { 
+                                            key: 'secretKey',
+                                            label: 'Secret Key',
+                                            value: response.data.data.secretKey
+                                          }]
+              // 显示凭证弹窗
+              credentialVisible.value = true;
+          } else {
+              message.error(response.data.message || '应用创建失败,请检查输入信息');
+          }
+      } catch (error) {
+          message.error('应用创建失败,请稍后重试');
+      }
+  };
+
+  const handleAvatarUpload = async (file) => {
+      const formData = new FormData();
+      formData.append('file', file);
+      try {
+        const response = await axios.post(uploadUrl, formData, {
+          headers: {
+            'Content-Type': 'multipart/form-data',
+            'oauth-code': oauthCode.value,
+            'origin-url': window.location.href,
+            'upload-path': 'images/avatar'
+          }
+        });
+        if (response.data.code === 40001) {
+          window.location.href = response.data.redirect_url;
+        }
+        // 假设服务器返回头像的 URL
+        robotAvatar.value = response.data.data.url; 
+      } catch (error) {
+        console.error('Error uploading avatar:', error);
+      }
+    };
+
+    const beforeUpload = async (file) => {
+      // 文件验证逻辑,例如限制文件大小和类型
+      const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
+      const isLt2M = file.size / 1024 / 1024 < 2;
+
+      if (!isJpgOrPng) {
+        message.error('请上传 JPG 或 PNG 格式的图片');
+        return false;
+      }
+      if (!isLt2M) {
+        message.error('图片大小不能超过 2MB');
+        return false;
+      }
+      await handleAvatarUpload(file);
+      return false;
+    };
+
+
+  onMounted(() => {
+    fetchData();
+  });
+
+  return {
+    credentialsDisplay,
+    credentialVisible,
+    dataSource,
+    columns,
+    keywords,
+    formLayout,
+    currentPage,
+    pageSize,
+    total,
+    pageTotal,
+    uploadUrl,
+    robotModalVisible,
+    robotAvatar,
+    appName,
+    description,
+    robotForm,
+    showRobotModal,
+    handleRobotCancel,
+    handleRobotCreate,
+    beforeUpload,
+    copyAllCredentials,
+    copyText,
+    handlePageChange,
+    showTotal,
+    fetchData,
+    handleSearch
+  };
+},
+});
+</script>
+
+<style scoped>
+/* 样式优化 */
+.credential-box {
+  margin: 24px 0;
+}
+
+.credential-item {
+  margin-bottom: 16px;
+}
+
+.credential-label {
+  color: rgba(0, 0, 0, 0.85);
+  margin-bottom: 8px;
+  font-weight: 500;
+}
+
+.credential-value {
+  width: 85%;
+  font-family: monospace;
+}
+
+.copy-all {
+  margin-top: 32px;
+}
+
+:deep(.ant-result-subtitle) {
+  color: #ff4d4f;
+  font-weight: 500;
+}
+</style>

+ 0 - 0
webchat-front-admin/src/views/walletManagement.vue → webchat-front-admin/src/views/paymentDetailManagement.vue


+ 279 - 0
webchat-front-admin/src/views/paymentOrderManagement.vue

@@ -0,0 +1,279 @@
+<template>
+  <div>
+    <a-breadcrumb>
+      <a-breadcrumb-item>WebChat</a-breadcrumb-item>
+      <a-breadcrumb-item>控制台</a-breadcrumb-item>
+      <a-breadcrumb-item>钱包管理</a-breadcrumb-item>
+    </a-breadcrumb>
+
+    <a-form :layout="formLayout" style="margin: 20px 0px 10px 0px; width: 100%">
+      <a-row :gutter="20"> 
+          <a-select v-model:value="transType" style="width: 120px; margin-left: 10px;">
+            <a-select-option value="">全部类型</a-select-option>
+            <a-select-option value="1">收入</a-select-option>
+            <a-select-option value="-1">支出</a-select-option>
+          </a-select>
+        <a-select v-model:value="eventType" style="width: 120px; margin-left: 10px;">
+            <a-select-option value="">全部事件</a-select-option>
+            <a-select-option value="1">系统发放</a-select-option>
+            <a-select-option value="2">发红包</a-select-option>
+            <a-select-option value="3">收红包</a-select-option>
+            <a-select-option value="4">参与抽奖</a-select-option>
+          </a-select>
+          <a-auto-complete 
+            style="margin-left: 10px; width: 200px;"
+            v-model:value="searchUserId"
+            :options="suggestUserOptions"
+            placeholder="请输入用户名称"
+            @select="handleSuggestUserSelect"
+            @change="handleSuggestUserChange"
+          />
+        <a-col :span="5">
+          <a-button @click="resetSearch">重置</a-button>
+          <a-button type="primary" @click="handleSearch" style="margin-left: 20px; background-color: black;">搜索</a-button>
+        </a-col>
+        <a-col :span="3">
+          <a-button type="primary" @click="showGiveModal" style="margin-left: 50px; background-color: black;">系统发放</a-button>
+        </a-col>
+      </a-row>
+    </a-form>
+
+    <a-table :dataSource="dataSource" :columns="columns" :pagination="false" />
+    <a-pagination
+      :current="currentPage"
+      :pageSize="pageSize"
+      :total="total"
+      :showTotal="showTotal"
+      @change="handlePageChange"
+      style="float: right; margin-top: 20px"
+    />
+
+    <!-- 机器人弹窗组件 -->
+  <a-modal
+    v-model:visible="giveModalVisible"
+    title="系统发放"
+    @ok="handleGiveCreate"
+    @cancel="handleGiveCancel"
+    okText="发放"
+    cancelText="取消"
+  >
+    <a-form :form="giveForm">
+      <a-form-item label="授予用户">
+        <a-input v-model:value="giveMoneyCount" placeholder="如:授予用户"/>
+      </a-form-item>
+      <a-form-item label="授予金额">
+        <a-input v-model:value="giveMoneyCount" placeholder="如:1000"/>
+      </a-form-item>
+    </a-form>
+  </a-modal>
+  </div>
+</template>
+
+<script>
+import axios from 'axios';
+import { defineComponent, ref, onMounted, watch, nextTick } from 'vue';
+import { useRoute } from 'vue-router';
+import { Avatar, Modal, Form, FormItem, Input, Upload, message } from 'ant-design-vue';
+
+export default defineComponent({
+  setup() {
+    const route = useRoute();
+    const oauthCode = ref(route.query.oauthCode || new URLSearchParams(window.location.search).get('oauthCode'));
+    const dataSource = ref([]);
+    const searchUserId = ref('');
+    const suggestUserOptions = ref([]);
+    const selectedUserId = ref('');
+    const userId = ref(''); 
+    const userName = ref(''); 
+    const mobile = ref(''); 
+    const transType = ref('');
+    const eventType = ref('');
+    const giveForm = ref(null);
+    const giveModalVisible = ref(false);
+    const formLayout = 'horizontal'; 
+    const currentPage = ref(1); // 当前页码
+    const pageSize = ref(10); // 每页显示的记录数
+    const total = ref(0); // 总记录数
+    const pageTotal = ref(0); // 总页数
+    const columns = [
+      {
+        title: '账号ID',
+        dataIndex: 'userId', 
+        key: 'userId',
+      },
+      {
+        title: '用户名',
+        customRender: ({ text, record }) => {
+          // 使用 record.user.userName 来访问嵌套属性
+          return record.user?.userName || '';
+        }
+      },
+      {
+        title: '交易事件',
+        dataIndex: 'eventName',
+        key: 'eventName',
+      },
+      {
+        title: '交易类型',
+        dataIndex: 'typeName',
+        key: 'typeName',
+      },
+      {
+        title: '金额',
+        dataIndex: 'money',
+        key: 'money',
+      },
+      {
+        title: '交易时间',
+        dataIndex: 'timeStr',
+        key: 'timeStr',
+      }
+    ];
+    const fetchData = async (page = 1, pageSize = 10, userId = '', userName = '', mobile = '') => {
+      try {
+        console.log("Fetching data with params:", userId, userName, mobile); 
+        const response = await axios.get(`/admin-service/wallet/page`, {
+          params: {
+            pageNo: page,
+            pageSize: pageSize,
+            transType: transType.value, 
+            eventType: eventType.value 
+          },
+          headers: {
+            'oauth-code': oauthCode.value,
+            'origin-url': window.location.href
+          }
+        });
+        if (response.data.code === 40001) {
+          // 未登录
+          window.location.href = response.data.redirect_url;
+        }
+        dataSource.value = response.data.data;
+        // 更新分页信息
+        total.value = response.data.total;
+        pageTotal.value = response.data.pageTotal;
+        currentPage.value = response.data.pageNo;
+      } catch (error) {
+        console.error('Error fetching data:', error);
+      }
+    };
+
+    const handleSearch = async () => {
+      await nextTick();
+      await fetchData(1, pageSize.value, userId.value, userName.value, mobile.value);
+    };
+
+    const resetSearch = () => {
+      eventType.value = '';
+      transType.value = '';
+      handleSearch();
+    };
+
+    const handlePageChange = (page) => {
+      fetchData(page, pageSize.value, userId.value, userName.value, mobile.value);
+    };
+
+    const showTotal = (total) => {
+      return `共 ${total} 条`;
+    };
+
+    const showGiveModal = () => {
+      giveModalVisible.value = true; // 显示机器人弹窗
+    };
+
+    const handleGiveCancel = () => {
+      giveModalVisible.value = false; // 关闭机器人弹窗
+    };
+
+    const handleGiveCreate = async () => {
+        try {
+            const response = await axios.post('/admin-service/account/createRobot', {
+                
+            });
+            if (response.data.code === 200) {
+              giveModalVisible.value = false; // 关闭机器人弹窗
+                message.success('发放成功');  // 正确调用 message 组件
+                // 刷新列表
+                fetchData(); 
+            } else {
+                message.error(response.data.message || '机器人创建失败,请检查输入信息');
+            }
+        } catch (error) {
+            message.error('机器人创建失败,请稍后重试');
+        }
+    };
+
+    const handleSuggestUser = async (searchText) => {
+      try {
+        // 使用 axios 进行请求
+        const response = await axios.get('/admin-service/account/suggest', {
+          params: { keyword: searchText }
+        });
+        // 假设后端返回的数据结构为 [{ id: 'user1', name: 'User One' }, { id: 'user2', name: 'User Two' }]
+        suggestUserOptions.value = response.data.map(user => ({ value: user.name, label: user.name }));
+      } catch (error) {
+        console.error('搜索用户时出错:', error);
+      }
+    };
+
+    const handleSuggestUserChange = (val) => {
+      // 使用绑定的 searchUserId 作为搜索关键词
+      handleSuggestUser(searchUserId.value);
+    };
+
+    const handleSuggestUserSelect = (value) => {
+      // 查找选中用户的 ID
+      const selectedOption = suggestUserOptions.value.find(option => option.value === value);
+      if (selectedOption) {
+        selectedUserId.value = selectedOption.id;
+        // 将选中用户的 ID 存储到 userId 中,以便后续作为搜索条件使用
+        userId.value = selectedUserId.value;
+        console.log('选中用户的 ID:', selectedUserId.value);
+      }
+    };
+
+    onMounted(() => {
+      fetchData();
+    });
+
+    watch([userId, userName, mobile], ([newUserId, newUserName, newMobile]) => {
+      console.log("Input values changed:", newUserId, newUserName, newMobile);
+    }, { immediate: true });
+
+    return {
+      transType,
+      eventType,
+      dataSource,
+      columns,
+      giveForm,
+      userId,
+      userName,
+      mobile,
+      formLayout,
+      currentPage,
+      pageSize,
+      total,
+      pageTotal,
+      giveModalVisible,
+      searchUserId,
+      suggestUserOptions,
+      selectedUserId,
+      fetchData,
+      handleSearch,
+      resetSearch, 
+      showGiveModal,
+      handleGiveCancel,
+      handleGiveCreate,
+      handlePageChange,
+      showTotal,
+      handleSuggestUser,
+      handleSuggestUserChange,
+      handleSuggestUserSelect
+    };
+  },
+});
+</script>
+
+<style scoped>
+
+</style>

BIN
webchat-front-client/src/static/images/redpacket.png


+ 3 - 0
webchat-front-client/src/views/ChatCore.vue

@@ -17,6 +17,7 @@
   import ChatOfficial from './ChatOfficial.vue';
   import ChatGroup from './ChatGroup.vue';
   import ChatFile from './ChatFile.vue'
+  import ChatServerAccount from './ChatServerAccount.vue'
   // 定义接收的 props
   const props = defineProps({
     selectChatUser: {
@@ -45,6 +46,8 @@
       return ChatRobot;
     } else if (props.selectChatUser.roleCode === 6) {
       return ChatOfficial;
+    } else if (props.selectChatUser.roleCode === 7) {
+      return ChatServerAccount;
     }
   });
   </script>

+ 346 - 7
webchat-front-client/src/views/ChatGroup.vue

@@ -12,22 +12,50 @@
         chatMessage.senderId === loginUser.userId || chatMessage.proxySenderId === loginUser.userId ? 'my-message' : 'other-message']">
             <!-- 自己的消息,头像在右,消息在左 -->
             <template v-if="chatMessage.senderId === loginUser.userId || chatMessage.proxySenderId === loginUser.userId">
-                <div class="message-content my-message-content" :style="{ backgroundColor: '#a9ea7a' }">
+                <div v-if="chatMessage.type === 1" class="message-content my-message-content" :style="{ backgroundColor: '#a9ea7a' }">
                     {{ chatMessage.message }}
                 </div>
+                <div v-if="chatMessage.type === 3" class="message-content my-message-content" :style="{ padding: 0 }">
+                    <!-- 红包消息 --> 
+                    <div class="red-packet-message-card" @click="openRedPacket(chatMessage.messageExt.id)">
+                        <img src="../static/images/redpacket.png" class="red-packet-icon">
+                        <div class="red-packet-blessing">
+                            {{chatMessage.messageExt.blessing ? chatMessage.messageExt.blessing : '恭喜发财, 大吉大利'}}
+                        </div>
+                        <div class="red-packet-name">
+                            WebChat 红包
+                        </div>
+                    </div>
+                </div>
                 <div class="message-avatar my-avatar">
                     <img :src="loginUser.photo" alt="avatar" />
+                    <div v-if="chatMessage.type === 3"  class="chat-mess-triangle-right" style="border-left: 16px solid #e99f4f"></div>
+                    <div v-if="chatMessage.type != 3" class="chat-mess-triangle-right" style="border-left: 16px solid #a9ea7a"></div>
                 </div>
             </template>
             <!-- 其他人的消息,头像在左,消息在右 -->
             <template v-else>
                 <div class="message-avatar other-avatar">
                     <img :src="chatMessage.proxySender != null ? chatMessage.proxySender?.photo : selectChatUser.photo" alt="avatar" />
+                    <div v-if="chatMessage.type === 3"  class="chat-mess-triangle-left" style="border-right: 16px solid #e99f4f"></div>
+                    <div v-if="chatMessage.type != 3" class="chat-mess-triangle-left" style="border-right: 16px solid #f5f5f5"></div>
                 </div>
                 <div class="proxy-user-name">{{ chatMessage.proxySender.userName }}</div>
-                <div class="message-content other-message-content" :style="{ backgroundColor: '#f5f5f5' }">
+                <div v-if="chatMessage.type === 1"  class="message-content other-message-content" :style="{ backgroundColor: '#f5f5f5' }">
                     {{ chatMessage.message }}
                 </div>
+                <div v-if="chatMessage.type === 3" class="message-content other-message-content" :style="{ padding: 0 }">
+                    <!-- 红包消息 --> 
+                    <div class="red-packet-message-card" @click="openRedPacket(chatMessage.messageExt.id)">
+                        <img src="../static/images/redpacket.png" class="red-packet-icon">
+                        <div class="red-packet-blessing">
+                            {{chatMessage.messageExt.blessing ? chatMessage.messageExt.blessing : '恭喜发财, 大吉大利'}}
+                        </div>
+                        <div class="red-packet-name">
+                            WebChat 红包
+                        </div>
+                    </div>
+                </div>
             </template>
             <!-- 消息内容出发特效关键词 -->
             <ChatEmojiAnimations 
@@ -96,11 +124,97 @@
             :openVideo="openVideo"  
             @close="handleVideoClose"/>
     </a-modal>
+
+
+    <a-modal
+        v-model:visible="openRedPacketModalVisible"
+        :width="300"
+        :height="300"
+        :okButtonProps="{ style: { display: 'none' } }"
+        :cancelButtonProps="{ style: { display: 'none' } }"
+        class="openRedPacket"
+        >
+        <div class="open-red-packet-header" style="text-align: center;">
+            <div style="color: #666; font-size: 18px; line-height: 50px;">{{openRedPacketText}}</div>
+            <div style="font-size: 35px; color: #c1a36e;">{{openRedPacketAmount}}</div>
+        </div>
+    </a-modal>
+
+    <a-modal
+        v-model:visible="sendRedPacketModalVisible"
+        :width="400"
+        :height="450"
+        class="red-packet-modal"
+        :okButtonProps="{ style: { display: 'none' } }"
+        :cancelButtonProps="{ style: { display: 'none' } }"
+        >
+        <template #title>
+            <div class="modal-title" style="margin-bottom: 10px;">
+                发红包
+            </div>
+        </template>
+          <!-- 隐藏的文件上传input -->
+            <input 
+                type="file"
+                ref="redPacketFileInput"
+                accept="image/*"
+                style="display: none;"
+                @change="handleFileUpload"
+            />
+            <a-dropdown>
+                <a class="ant-dropdown-link" @click.prevent  style="color: #ff5e4b; font-weight: 600;">
+                    选择红包类型
+                    <RightOutlined />
+                </a>
+                <template #overlay>
+                <a-menu>
+                    <a-menu-item>
+                    <a href="javascript:;">拼手气红包</a>
+                    </a-menu-item>
+                    <a-menu-item>
+                    <a href="javascript:;">固定红包</a>
+                    </a-menu-item>
+                </a-menu>
+                </template>
+            </a-dropdown>
+        <a-input prefix="¥" suffix="RMB" class="red-packet-input" placeholder="红包金额 ¥0.00" v-model:value="sendTotalMoney"/>
+        <a-input suffix="个" class="red-packet-input" placeholder="红包个数" v-model:value="sendTotalCount"/>
+        <a-input suffix="😊"  class="red-packet-input" placeholder="恭喜发财,大吉大利" v-model:value="sendBlessing"/>
+        <a-dropdown style="width: 100%">
+            <template #overlay>
+                <a-menu @click="uploadRedPacketCover">
+                    <a-menu-item key="1">
+                        <PictureOutlined />
+                        上传红包红面
+                    </a-menu-item>
+                </a-menu>
+            </template>
+            <a-button class="red-packet-cover-button">
+                红包封面 <RightOutlined />
+                 <!-- 显示预览图 -->
+                <img 
+                    v-if="redPacketCover" 
+                    :src="redPacketCover" 
+                    class="cover-preview"
+                />
+            </a-button>
+        </a-dropdown>
+        <div class="red-packet-money">
+            ¥ {{sendTotalMoney}}
+        </div>
+        <a-button type="primary" class="send-red-packet-button" @click="sendRedPacket">塞钱进红包</a-button>
+
+        <div class="red-packet-tip">
+            使用WebChat钱包余额直接发红包
+        </div>
+
+    </a-modal>
   </template>
   
   <script>
   import axios from 'axios';
   import { inject, defineComponent, ref, onMounted, onUnmounted, watch } from 'vue';
+  import { message } from 'ant-design-vue';
   import Video2Group from './video2group.vue';
   import ChatEmojiAnimations from './animation/emojiAnimations.vue';
   export default defineComponent({
@@ -126,9 +240,23 @@
         const groupDetailVisible = ref(false)
         const groupVideoModalVisible = ref(false);
 
+        const openRedPacketModalVisible = ref(false)
+        const sendRedPacketModalVisible = ref(false)
+        const redPacketMoney = ref(0.00)
+        const redPacketCover = ref(null);
+        const redPacketFileInput = ref(null);
+        const uploadUrl = `/client-service/chat/file/upload`; // 上传API
+
         const showChatEmojiAnimations = ref(false);
         const emojis = ref([""]);
 
+        const redPacketType = ref(2);
+        const sendTotalMoney = ref(0.0);
+        const sendTotalCount = ref(1);
+        const sendBlessing = ref("恭喜发财,大吉大利");
+        const openRedPacketText = ref("恭喜你,获得");
+        const openRedPacketAmount = ref("¥ 0.00");
+
         // 维护关键词,emoji特效
         const messageKeywords = ["生日快乐", "比心", "爱你", "礼物", "红包", "💣", "炸弹", "💩", "屎"];
         const messageKeywordsEmojis = ["🍰", "♥️", "😘", "🎁", "🧧", "💣", "💣", "💩", "💩"];
@@ -176,10 +304,12 @@
                 // 在此处理收到的消息
                 const socketMessage = JSON.parse(event.data);
                 if(socketMessage.senderId != selectChatUser.value.userId) {
-                    return;
+                    if (messageType === 1) {
+                        return;
+                    }
                 }
                 const messageType = socketMessage.type;
-                if (messageType === 1) {
+                if (messageType === 1 || messageType === 3) {
                     // 处理对话
                     chatMessageArr.value.push(socketMessage);
                      // 关键词出发emoji飘落特效
@@ -293,6 +423,102 @@
             }
         }
 
+        // 点击上传菜单项
+        const uploadRedPacketCover = ({ key }) => {
+            if (key === '1') {
+                // 触发隐藏的input点击
+                redPacketFileInput.value.click();
+            }
+        };
+
+        // 处理文件选择
+        const handleFileUpload = async (e) => {
+            const file = e.target.files[0];
+            if (!file) return;
+            // 校验文件类型
+            if (!file.type.startsWith('image/')) {
+                message.error('请选择图片文件');
+                return;
+            }
+            // 校验文件大小(示例限制2MB)
+            if (file.size > 10 * 1024 * 1024) {
+                message.error('图片大小不能超过10MB');
+                return;
+            }
+            // 方式2:上传到服务器(示例使用axios)
+            try {
+                const formData = new FormData();
+                formData.append('file', file);
+                const response = await axios.post(uploadUrl, formData, {
+                    headers: {
+                    'Content-Type': 'multipart/form-data',
+                    'origin-url': window.location.href,
+                    'upload-path': 'images/redPacket'
+                    }
+                });
+                if (response.data.code === 40001) {
+                    window.location.href = response.data.redirect_url;
+                }
+                // 预览红包封面图
+                redPacketCover.value = response.data.data.url;
+            } catch (error) {
+                message.error('上传失败');
+                console.error('Upload error:', error);
+            }
+        };
+
+        const sendRedPacket = async () => {
+            if (!sendTotalMoney.value || sendTotalMoney.value < 1 || sendTotalMoney.value > 200) {
+                message.error("红包金额控制在1 ~ 200元以内");
+                return ;
+            }
+            if (!sendTotalCount.value || sendTotalCount.value < 1) {
+                message.error("最少发出一个红包");
+                return ;
+            }
+
+            try {
+                // 调用接口发送登录请求
+                const response = await axios.post(`/client-service/chat/red-packet/send`, 
+                {
+                    receiver: selectChatUser.value.userId,
+                    totalMoney: sendTotalMoney.value,
+                    blessing: sendBlessing.value,
+                    type: 2,
+                    count: sendTotalCount.value
+                });
+                if (response.data.code === 40001) {
+                    window.location.href = response.data.redirect_url;
+                } else if (response.data.code != 200) {
+                    message.error(response.data.msg);
+                } else {
+                    sendRedPacketModalVisible.value = false;
+                }
+            } catch (error) {
+                // 处理错误,例如显示错误消息
+                console.log("====>" + error);
+                message.error("服务异常,稍后重试", error);
+            }
+        };
+
+        const openRedPacket = async (redPacketId) => {
+            const response = await axios.get(`/client-service/chat/red-packet/open/` + redPacketId, {
+                // 这里可以添加请求的配置,例如 headers 或 params
+            })
+            if (response.data.code === 40001) {
+                window.location.href = response.data.redirect_url;
+                return;
+            } else if (response.data.code === 200) {
+                openRedPacketText.value = "🎉 恭喜你,获得 ";
+                openRedPacketAmount.value = "¥ " + response.data.data;
+                openRedPacketModalVisible.value = true;
+            } else {
+                openRedPacketText.value = "😅 哎呀~";
+                openRedPacketAmount.value = response.data.msg;
+                openRedPacketModalVisible.value = true;
+            }
+        }
+
         const handleClick = (type) => {
             switch (type) {
                 case 'emoji':
@@ -301,11 +527,10 @@
                     break;
                 case 'redPacket':
                     console.log('点击了红包按钮');
-                    // 在此添加红包按钮的逻辑,例如打开红包发送界面等
+                    sendRedPacketModalVisible.value = true;
                     break;
                 case 'audioVideo':
                     console.log('点击了音视频按钮');
-                    // 在此添加音视频按钮的逻辑,例如打开音视频通话界面等
                     groupVideoModalVisible.value = true;
                     break;
                 case 'file':
@@ -325,6 +550,16 @@
 
       
       return {
+        sendTotalCount,
+        openRedPacketModalVisible,
+        sendTotalMoney,
+        sendBlessing,
+        redPacketType,
+        uploadUrl,
+        redPacketCover,
+        redPacketFileInput,
+        sendRedPacketModalVisible,
+        redPacketMoney,
         showChatEmojiAnimations,
         emojis,
         openVideo,
@@ -338,6 +573,10 @@
         heartbeatInterval,
         chatMessageArr,
         groupDetailVisible,
+        openRedPacketText,
+        openRedPacketAmount,
+        openRedPacket,
+        sendRedPacket,
         handleVideoModalVisibleChange,
         handleClick,
         handleSendMessage,
@@ -346,7 +585,9 @@
         afterVisibleChange,
         showDrawer,
         loadChatMessages,
-        handleVideoClose
+        handleVideoClose,
+        handleFileUpload,
+        uploadRedPacketCover
       };
     }
   });
@@ -444,6 +685,7 @@
   justify-content: flex-start;
 }
 .message-avatar {
+  position: relative;
   margin-right: 10px;
 }
 .message-avatar img {
@@ -509,4 +751,101 @@
     height: 100%;
     z-index: 999;
 }
+.red-packet-input, .red-packet-cover-button {
+    height: 50px;
+    margin-top: 15px;
+    background-color: white;
+}
+.red-packet-cover-button {
+    width: 100%;
+    text-align: left;
+}
+.send-red-packet-button {
+    background-color: #ff5e4b;
+    color: white;
+    width: 170px;
+    height: 45px;
+    margin-top: 30px;
+    margin-left: 100px;
+}
+.send-red-packet-button:hover {
+    background-color:rgb(252, 68, 48);
+}
+.red-packet-money {
+    position: relative;
+    width: 100%;
+    line-height: 50px;
+    font-size: 35px;
+    margin-top: 40px;
+    text-align: center;
+    font-weight: 700;
+}
+.red-packet-tip {
+    color: gray;
+    font-size: 12px;
+    margin-top: 100px;
+    text-align: center;
+}
+.cover-preview {
+    position: relative;
+    float: right;
+    height: 20px;
+}
+.red-packet-message-card {
+    position: relative;
+    width: 270px;
+    height: 85px;
+    color: white;
+    border-radius: 5px;
+    background-color: #e99f4f;
+}
+.red-packet-name {
+    position: absolute;
+    left: 0px;
+    bottom: 0px;
+    border-top: 1px solid #d48a3a;
+    width: 100%;
+    height: 20px;
+    line-height: 20px;
+    color: white;
+    text-align: left;
+    text-indent: 1em;
+    font-size: 9px;
+}
+.red-packet-icon {
+    position: absolute;
+    left: 20px;
+    top: 15px;
+    height: 40px;
+    border-radius: 3px;
+}
+.red-packet-blessing {
+    position: absolute;
+    left: 70px;
+    top: 15px;
+    width: 170px;
+    white-space: nowrap; /* 确保文本不会换行 */  
+    overflow: hidden; /* 隐藏超出容器的文本 */  
+    text-overflow: ellipsis; /* 当文本超出容器时显示省略号 */  
+  }
+  .chat-mess-triangle-left {
+    position: absolute;
+    right: -20px;
+    top: 17px;
+    width: 0;
+    height: 0;
+    border-top: 8px solid transparent;
+    border-right: 16px solid #f5f5f5;
+    border-bottom: 8px solid transparent;
+}
+.chat-mess-triangle-right {
+    position: absolute;
+    left: -20px;
+    top: 10px;
+    width: 0;
+    height: 0;
+    border-top: 8px solid transparent;
+    border-left: 16px solid #a9ea7a;
+    border-bottom: 8px solid transparent;
+}
   </style>

+ 228 - 0
webchat-front-client/src/views/ChatServerAccount.vue

@@ -0,0 +1,228 @@
+<template>
+    <div class="chat-right-header-container" style="text-align: center; height: 49px; line-height: 49px;">
+
+        {{selectChatUser.userName}} (服务号)
+
+    </div>
+    <div class="chat-core-container" style="background-color: whitesmoke; height: 660px; text-align: center; padding-top: 20px;">
+        <div class="message-card-item" v-for="messageCard in messageCardArr" @click="viewArticle(messageCard.messageExt?.redirectUrl)">
+            <div class="message-avatar other-avatar">
+                    <img :src="selectChatUser.photo" alt="avatar" />
+                </div>
+            <div class="message-card">
+                <div class="message-card-title">
+                    <img :src="messageCard.messageExt?.logo" v-if="messageCard.messageExt?.logo" style="width: 20px; height; 20px;">
+                    {{messageCard.messageExt?.title}}
+                </div>
+                <div class="message-card-template" v-html="messageCard.messageExt?.content"></div>
+                <div v-if="messageCard.messageExt?.redirectName && messageCard.messageExt?.redirectUrl" class="message-card-redirect">
+                    {{ messageCard.messageExt?.redirectName }}
+                    <RightOutlined />
+                </div>
+            </div>
+            
+        </div>
+    </div>
+  </template>
+  
+  <script>
+  import { useRouter } from 'vue-router';
+  import axios from 'axios';
+  import { inject, defineComponent, ref, onMounted, onUnmounted, watch } from 'vue';
+  export default defineComponent({
+    props: {
+        selectChatUserRef: Object
+    },
+    setup(props) {
+
+        const router = useRouter();
+
+        const selectChatUser = ref(props.selectChatUserRef);
+
+        const socket = ref(null);
+        const messageQueue = ref([]); // 存储未发送的消息队列
+        const isConnected = ref(false);
+        const loginUser = ref(inject('loginUser'));
+        const reconnectInterval = ref(5000); // 重连间隔时间,单位:毫秒
+        const heartbeatInterval = ref(30000); // 心跳间隔时间,单位:毫秒
+        const messageCardArr = ref([]);
+
+        const loadChatMessages = (chatUserId) => {
+            // 先清空
+            axios.get(`/client-service/chat/message/list/`+chatUserId, {})
+            .then(function (response) {
+                if (response.data.code === 40001) {
+                    // 未登录
+                    window.location.href = response.data.redirect_url;
+                } else if (response.data.code === 200) {
+                    if (response.data.data.length > 0) {
+                        messageCardArr.value = response.data.data;
+                    }
+                }
+            }).catch(function (error) {
+                // 处理错误
+                console.error(error);
+            });
+        }
+        loadChatMessages( selectChatUser.value.userId);
+
+        const redirect = (redirectUrl) => {
+            
+
+
+        }
+
+        const connectWebSocket = () => {
+            socket.value = new WebSocket(`/connect-service/ws/chat/PC_WEB_CHAT/` + loginUser.value.userId);
+            socket.value.onopen = () => {
+                console.log('WebSocket 已连接');
+                isConnected.value = true;
+                // 发送队列中的消息
+                messageQueue.value.forEach(message => socket.value.send(message));
+                messageQueue.value = [];
+                startHeartbeat();
+            };
+
+            socket.value.onmessage = (event) => {
+                console.log('收到WS消息:', event.data);
+                // 在此处理收到的消息
+                const socketMessage = JSON.parse(event.data);
+                if(socketMessage.senderId != selectChatUser.value.userId) {
+                    return;
+                }
+                const messageType = socketMessage.type;
+                if (messageType === 11) {
+                    // 处理对话
+                    messageCardArr.value.push(socketMessage);
+                }
+            };
+
+            socket.value.onerror = (error) => {
+                console.error('WebSocket 错误:', error);
+                isConnected.value = false;
+                // 关闭当前连接
+                socket.value.close();
+                // 尝试重连
+                setTimeout(connectWebSocket, reconnectInterval.value);
+            };
+
+            socket.value.onclose = () => {
+                console.log('WebSocket 已关闭');
+                isConnected.value = false;
+                // 尝试重连
+                setTimeout(connectWebSocket, reconnectInterval.value);
+            };
+        };
+
+        const startHeartbeat = () => {
+            const heartbeat = () => {
+                if (isConnected.value) {
+                socket.value.send('ping'); // 发送心跳包
+                }
+            };
+            setInterval(heartbeat, heartbeatInterval.value);
+        };
+        // 链接ws
+        connectWebSocket();
+
+        const sendMessage = (message) => {
+            if (isConnected.value) {
+                socket.value.send(message);
+            } else {
+                messageQueue.value.push(message);
+            }
+        };
+
+        // 切换对话用户,父组件传递选中用户信息,监听选中用户变化
+        watch(() => props.selectChatUserRef, (newValue) => {
+          selectChatUser.value = newValue;
+        });
+        onMounted(() => {
+            
+        });
+        onUnmounted(() => {
+        
+        });
+        
+        return {
+            selectChatUser,
+            isConnected,
+            messageQueue,
+            loginUser,
+            reconnectInterval,
+            heartbeatInterval,
+            messageCardArr,
+            connectWebSocket,
+            sendMessage,
+            startHeartbeat,
+            loadChatMessages,
+            redirect
+        };
+    }
+  });
+  </script>
+  
+  <style scoped>
+  .message-card-item {
+    margin-bottom: 30px;
+  }
+  .message-card {
+        position: relative;
+        margin-left: 50px;
+        width: 310px;
+        height: auto;
+        padding: 0px 20px;
+        background-color: white;
+        border: 1px solid #e5e5e5;   
+        border-radius: 8px;    
+  }
+  .message-card-title {
+    position: relative;
+    width: 100%;
+    height: 50px;
+    line-height: 50px;
+    text-align: left;
+    border-bottom: 1px solid #e5e5e5;   
+    text-align: left;
+    margin-bottom: 10px;
+  }
+  .message-card-redirect {
+    position: relative;
+    width: 100%;
+    height: 50px;
+    font-size: 13px;
+    line-height: 50px;
+    text-align: left;
+    margin-top: 15px;
+    border-top: 1px solid #e5e5e5;   
+  }
+  .chat-core-container::-webkit-scrollbar {/*滚动条整体样式*/
+        width: 0px;     /*高宽分别对应横竖滚动条的尺寸*/
+        height: 0px;
+    }
+    .chat-core-container::-webkit-scrollbar-thumb {/*滚动条里面小方块*/
+        border-radius: 10px;
+        background-color: transparent;
+        background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .2) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .2) 50%, rgba(255, 255, 255, .2) 75%, transparent 75%, transparent);
+    }
+    .chat-core-container::-webkit-scrollbar-track {/*滚动条里面轨道*/
+        -webkit-box-shadow: inset 0 0 1px rgba(0,0,0,0.2);
+        /*border-radius: 10px;*/
+        background: #EDEDED;
+    }
+    .message-card-item {
+        position: relative;
+        width: 370px;
+        height: auto;
+    }
+    .message-avatar {
+        position: absolute;
+        top: 0px;
+        left: 0px;
+    }
+    .message-avatar img {
+        width: 35px;
+        height: 35px;
+        border-radius: 5px;
+    }
+  </style>

+ 182 - 17
webchat-front-client/src/views/ChatUser.vue

@@ -10,21 +10,49 @@
         chatMessage.senderId === loginUser.userId ? 'my-message' : 'other-message']">
             <!-- 自己的消息,头像在右,消息在左 -->
             <template v-if="chatMessage.senderId === loginUser.userId">
-                <div class="message-content my-message-content" :style="{ backgroundColor: '#a9ea7a' }">
+                <div v-if="chatMessage.type === 1" class="message-content my-message-content" :style="{ backgroundColor: '#a9ea7a' }">
                     {{ chatMessage.message }}
                 </div>
+                <div v-if="chatMessage.type === 3" class="message-content my-message-content" :style="{ padding: 0 }">
+                    <!-- 红包消息 --> 
+                    <div class="red-packet-message-card" @click="openRedPacket(chatMessage.messageExt.id)">
+                        <img src="../static/images/redpacket.png" class="red-packet-icon">
+                        <div class="red-packet-blessing">
+                            {{chatMessage.messageExt.blessing ? chatMessage.messageExt.blessing : '恭喜发财, 大吉大利'}}
+                        </div>
+                        <div class="red-packet-name">
+                            WebChat 红包
+                        </div>
+                    </div>
+                </div>
                 <div class="message-avatar my-avatar">
-                    <img :src="loginUser.photo" alt="avatar" />
+                    <img :src="loginUser.photo" alt="avatar" /> 
+                    <div v-if="chatMessage.type === 3"  class="chat-mess-triangle-right" style="border-left: 16px solid #e99f4f"></div>
+                    <div v-if="chatMessage.type != 3" class="chat-mess-triangle-right" style="border-left: 16px solid #a9ea7a"></div>
                 </div>
             </template>
             <!-- 其他人的消息,头像在左,消息在右 -->
             <template v-else>
                 <div class="message-avatar other-avatar">
                     <img :src="selectChatUser.photo" alt="avatar" />
+                    <div v-if="chatMessage.type === 3"  class="chat-mess-triangle-left" style="border-right: 16px solid #e99f4f"></div>
+                    <div v-if="chatMessage.type != 3" class="chat-mess-triangle-left" style="border-right: 16px solid #f5f5f5"></div>
                 </div>
-                <div class="message-content other-message-content" :style="{ backgroundColor: '#f5f5f5' }">
+                <div v-if="chatMessage.type === 1" class="message-content other-message-content" :style="{ backgroundColor: '#f5f5f5' }">
                     {{ chatMessage.message }}
                 </div>
+                <div v-if="chatMessage.type === 3" class="message-content other-message-content" :style="{ padding: 0 }">
+                    <!-- 红包消息 --> 
+                    <div class="red-packet-message-card" @click="openRedPacket(chatMessage.messageExt.id)">
+                        <img src="../static/images/redpacket.png" class="red-packet-icon">
+                        <div class="red-packet-blessing">
+                            {{chatMessage.messageExt.blessing ? chatMessage.messageExt.blessing : '恭喜发财, 大吉大利'}}
+                        </div>
+                        <div class="red-packet-name">
+                            WebChat 红包
+                        </div>
+                    </div>
+                </div>
             </template>
             <!-- 消息内容出发特效关键词 -->
             <ChatEmojiAnimations 
@@ -84,6 +112,20 @@
     </a-modal>
 
     <a-modal
+        v-model:visible="openRedPacketModalVisible"
+        :width="300"
+        :height="300"
+        :okButtonProps="{ style: { display: 'none' } }"
+        :cancelButtonProps="{ style: { display: 'none' } }"
+        class="openRedPacket"
+        >
+        <div class="open-red-packet-header" style="text-align: center;">
+            <div style="color: #666; font-size: 18px; line-height: 50px;">{{openRedPacketText}}</div>
+            <div style="font-size: 35px; color: #c1a36e;">{{openRedPacketAmount}}</div>
+        </div>
+    </a-modal>
+
+    <a-modal
         v-model:visible="sendRedPacketModalVisible"
         :width="400"
         :height="450"
@@ -104,8 +146,8 @@
                 style="display: none;"
                 @change="handleFileUpload"
             />
-        <a-input prefix="¥" suffix="RMB" class="red-packet-input" placeholder="红包金额 ¥0.00"/>
-        <a-input suffix="😊"  class="red-packet-input" placeholder="恭喜发财,大吉大利"/>
+        <a-input prefix="¥"  suffix="RMB" class="red-packet-input" placeholder="红包金额 ¥0.00" v-model:value="sendTotalMoney"/>
+        <a-input suffix="😊"  class="red-packet-input" placeholder="恭喜发财,大吉大利" v-model:value="sendBlessing"/>
         <a-dropdown style="width: 100%">
             <template #overlay>
                 <a-menu @click="uploadRedPacketCover">
@@ -126,9 +168,9 @@
             </a-button>
         </a-dropdown>
         <div class="red-packet-money">
-            ¥ {{redPacketMoney}}
+            ¥ {{sendTotalMoney}}
         </div>
-        <a-button type="primary" class="send-red-packet-button">塞钱进红包</a-button>
+        <a-button type="primary" class="send-red-packet-button" @click="sendRedPacket">塞钱进红包</a-button>
 
         <div class="red-packet-tip">
             使用WebChat钱包余额直接发红包
@@ -166,17 +208,24 @@
         const chatMessageArr = ref([]);
         const videoModalVisible = ref(false)
         const sendRedPacketModalVisible = ref(false)
+        const openRedPacketModalVisible = ref(false)
         const redPacketMoney = ref(0.00)
         const redPacketCover = ref(null);
         const redPacketFileInput = ref(null);
         const uploadUrl = `/client-service/chat/file/upload`; // 上传API
         const showChatEmojiAnimations = ref(false);
         const emojis = ref([""]);
-
         // 维护关键词,emoji特效
         const messageKeywords = ["生日快乐", "比心", "爱你", "礼物", "红包", "💣", "炸弹", "💩", "屎"];
         const messageKeywordsEmojis = ["🍰", "♥️", "😘", "🎁", "🧧", "💣", "💣", "💩", "💩"];
 
+        const sendTotalMoney = ref(0.0);
+        const sendTotalCount = ref(1);
+        const sendBlessing = ref("恭喜发财,大吉大利");
+
+        const openRedPacketText = ref("恭喜你,获得");
+        const openRedPacketAmount = ref("¥ 0.00");
+
         const loadChatMessages = (chatUserId) => {
             // 先清空
             axios.get(`/client-service/chat/message/list/`+chatUserId, {})
@@ -236,11 +285,13 @@
                 console.log('收到WS消息:', event.data);
                 // 在此处理收到的消息
                 const socketMessage = JSON.parse(event.data);
+                const messageType = socketMessage.type;
                 if(socketMessage.senderId != selectChatUser.value.userId) {
-                    return;
+                    if (messageType === 1) {
+                        return;
+                    }
                 }
-                const messageType = socketMessage.type;
-                if (messageType === 1) {
+                if (messageType === 1 || messageType === 3) {
                     chatMessageArr.value.push(socketMessage);
                     handleMessageKeyword(socketMessage.message);
                 }
@@ -374,6 +425,54 @@
             }
         };
 
+        const sendRedPacket = async () => {
+
+            if (!sendTotalMoney.value || sendTotalMoney.value < 1 || sendTotalMoney.value > 200) {
+                message.error("红包金额控制在1 ~ 200元以内");
+                return ;
+            }
+            try {
+                // 调用接口发送登录请求
+                const response = await axios.post(`/client-service/chat/red-packet/send`, 
+                {
+                    receiver: selectChatUser.value.userId,
+                    totalMoney: sendTotalMoney.value,
+                    blessing: sendBlessing.value,
+                    type: 1,
+                    count: 1
+                });
+                if (response.data.code === 40001) {
+                    window.location.href = response.data.redirect_url;
+                } else if (response.data.code != 200) {
+                    message.error(response.data.msg);
+                } else {
+                    sendRedPacketModalVisible.value = false;
+                }
+            } catch (error) {
+                // 处理错误,例如显示错误消息
+                console.log("====>" + error);
+                message.error("服务异常,稍后重试", error);
+            }
+        };
+
+        const openRedPacket = async (redPacketId) => {
+            const response = await axios.get(`/client-service/chat/red-packet/open/` + redPacketId, {
+                // 这里可以添加请求的配置,例如 headers 或 params
+            })
+            if (response.data.code === 40001) {
+                window.location.href = response.data.redirect_url;
+                return;
+            } else if (response.data.code === 200) {
+                openRedPacketText.value = "🎉 恭喜你,获得 ";
+                openRedPacketAmount.value = "¥ " + response.data.data;
+                openRedPacketModalVisible.value = true;
+            } else {
+                openRedPacketText.value = "😅 哎呀~";
+                openRedPacketAmount.value = response.data.msg;
+                openRedPacketModalVisible.value = true;
+            }
+        }
+
         const handleClick = (type) => {
             switch (type) {
                 case 'emoji':
@@ -405,9 +504,14 @@
       });
       
       return {
+        openRedPacketModalVisible,
+        sendTotalMoney,
+        sendBlessing,
         uploadUrl,
         redPacketCover,
         redPacketFileInput,
+        sendRedPacketModalVisible,
+        redPacketMoney,
         openVideo,
         videoModalVisible,
         inputValue,
@@ -418,10 +522,11 @@
         reconnectInterval,
         heartbeatInterval,
         chatMessageArr,
-        sendRedPacketModalVisible,
-        redPacketMoney,
         showChatEmojiAnimations,
         emojis,
+        openRedPacketText,
+        openRedPacketAmount,
+        openRedPacket,
         handleVideoModalVisibleChange,
         handleClick,
         handleSendMessage,
@@ -432,6 +537,7 @@
         handleFileUpload,
         uploadRedPacketCover,
         sendMessageObj,
+        sendRedPacket
       };
     }
   });
@@ -529,12 +635,13 @@
   justify-content: flex-start;
 }
 .message-avatar {
-  margin-right: 10px;
+    position: relative;
+    margin-right: 10px;
 }
 .message-avatar img {
-  width: 35px;
-  height: 35px;
-  border-radius: 5px;
+    width: 35px;
+    height: 35px;
+    border-radius: 5px;
 }
 .message-content {
   line-height: 35px;
@@ -623,4 +730,62 @@
     height: 100%;
     z-index: 999;
 }
+.red-packet-message-card {
+    position: relative;
+    width: 270px;
+    height: 85px;
+    color: white;
+    border-radius: 5px;
+    background-color: #e99f4f;
+}
+.red-packet-name {
+    position: absolute;
+    left: 0px;
+    bottom: 0px;
+    border-top: 1px solid #d48a3a;
+    width: 100%;
+    height: 20px;
+    line-height: 20px;
+    color: white;
+    text-align: left;
+    text-indent: 1em;
+    font-size: 9px;
+}
+.red-packet-icon {
+    position: absolute;
+    left: 20px;
+    top: 15px;
+    height: 40px;
+    border-radius: 3px;
+}
+.red-packet-blessing {
+    position: absolute;
+    left: 70px;
+    top: 15px;
+    width: 170px;
+    white-space: nowrap; /* 确保文本不会换行 */  
+    overflow: hidden; /* 隐藏超出容器的文本 */  
+    text-overflow: ellipsis; /* 当文本超出容器时显示省略号 */  
+  }
+  .chat-mess-triangle-left {
+    position: absolute;
+    right: -21px;
+    top: 15px;
+    width: 0;
+    height: 0;
+    border-top: 8px solid transparent;
+    border-right: 16px solid #f5f5f5;
+    border-bottom: 8px solid transparent;
+}
+.chat-mess-triangle-right {
+    position: absolute;
+    left: -20px;
+    top: 10px;
+    width: 0;
+    height: 0;
+    border-top: 8px solid transparent;
+    border-left: 16px solid #a9ea7a;
+    border-bottom: 8px solid transparent;
+}
+
   </style>

+ 3 - 1
webchat-front-client/src/views/Chatting.vue

@@ -18,7 +18,9 @@
               }">
               {{ 
                   chatting.user.roleCode === 4 ? '群' : 
-                  ( chatting.user.roleCode === 5 ? '机器人' : '号') 
+                  (chatting.user.roleCode === 5 ? '机器人' : 
+                   (chatting.user.roleCode === 6 ? '公众号' : 
+                   (chatting.user.roleCode === 0 ? 'AI' : '服务号')))
               }}
             </span>
             {{ chatting.user.userName }}