first commit

This commit is contained in:
2026-06-02 10:42:33 +08:00
commit dd4975fd2c
1084 changed files with 442416 additions and 0 deletions

541
pages/Chat/Chat.css Normal file
View File

@@ -0,0 +1,541 @@
.chat-page {
position: relative;
}
/* 笼罩层样式 */
.mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 998;
animation: fadeIn 1s ease;
}
/* 自定义淡入动画 */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* ================会话侧边栏样式================== */
.chat-sidebar {
position: fixed;
top: 0;
left: 0;
width: calc(100% - 100rpx);
max-width: 680rpx;
height: 100%;
transform: translateX(-100%);
transition: transform 0.5s ease;
z-index: 999;
}
.chat-sidebar.sidebar-show {
transform: translateX(0);
}
/* =============================新建会话弹窗样式================== */
.ncd-overlay {
position: fixed;
width: 100%;
height: 100%;
left: 0;
top: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(193, 193, 193, 0.4);
z-index: 1000;
/* 毛玻璃核心代码 👇 */
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(6px);
}
.ncd-card {
width: 90%;
height: auto;
background-color: #ffffff;
border-radius: 50rpx;
padding: 50rpx;
box-sizing: border-box;
}
.ncd-header {
display: flex;
justify-content: center;
width: 100%;
}
.ncd-title-eng {
display: flex;
flex-shrink: 0;
background-color: #000;
padding: 5rpx 8rpx;
border-radius: 10rpx;
justify-content: center;
align-items: center;
}
.ncd-title-eng text {
color: #ffffff;
font-weight: bold;
font-size: 30rpx;
}
.ncd-title-zh {
margin-left: 10rpx;
display: flex;
flex: 1;
height: 0;
font-size: 38rpx;
font-weight: bold;
color: #000;
}
.ncd-header uni-icons {
display: flex;
flex-shrink: 0;
}
.ncd-options {
display: flex;
flex-direction: column;
width: 100%;
justify-content: center;
align-items: center;
margin-top: 10rpx;
box-sizing: border-box;
}
.ncd-option {
width: 100%;
background: #f8fafc;
padding: 50rpx 30rpx;
display: flex;
border-radius: 32rpx;
margin-top: 20rpx;
box-sizing: border-box;
justify-content: center;
align-items: center;
}
.ncd-normal {
border: 1rpx solid #e8edf3;
}
.ncd-opt-icon {
width: 80rpx;
height: 80rpx;
border-radius: 30rpx;
padding: 10rpx;
box-sizing: border-box;
display: flex;
flex-shrink: 0;
justify-content: center;
align-items: center;
}
.ncd-normal .ncd-opt-icon {
background-color: #e2e8f0;
}
.ncd-opt-title {
display: flex;
flex-direction: column;
margin: 0rpx 16rpx;
box-sizing: border-box;
justify-content: center;
flex: 1;
height: 0;
}
.ncd-opt-title-1 {
font-size: 30rpx;
font-weight: bold;
color: #000000;
}
.ncd-opt-title-2 {
font-size: 26rpx;
color: #666666;
}
.arrow-right-style {
display: flex;
flex-shrink: 0;
font-weight: bold !important;
font-size: 30rpx !important;
color: #666666 !important;
}
.ncd-intelligence {
background: linear-gradient(to right, #f5f3ff, #ede9fe);
border: 1rpx solid #c4b5fd;
}
.ncd-intelligence .ncd-opt-icon {
background-color: #8b5cf6;
}
/* ==========新建聊天列表相关====== */
.close-option-card {
width: 100%;
display: flex;
justify-content: flex-end;
}
.close-option-card .iconfont {
color: #ff0000 !important;
}
.chat-option-list {
display: flex;
width: 100%;
flex-direction: column;
}
.chat-option {
display: flex;
justify-content: flex-start;
align-items: center;
margin: 10rpx 0;
box-sizing: border-box;
}
.chat-option .iconfont {
font-size: 30px;
}
.option-name {
font-size: 18px;
font-weight: 400;
margin-left: 10px;
box-sizing: border-box;
}
/* ================聊天容器============== */
.chat-wrapper {
position: relative;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
}
/* ================聊天顶部样式============== */
.chat-hearder {
width: 100%;
height: auto;
padding: 20rpx 10rpx;
box-sizing: border-box;
display: flex;
flex-shrink: 0;
justify-content: flex-start;
align-items: center;
border-bottom: 1rpx solid #ede9fe;
}
.chat-btn-group {
width: 100%;
height: auto;
display: flex;
justify-content: flex-start;
align-items: center;
}
.head-btn {
padding: 10rpx;
border: 1rpx solid #ede9fe;
border-radius: 20rpx;
margin: 0 10rpx;
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
}
.chat-btn-group .head-btn:nth-child(1) .iconfont {
color: #c4b5fd;
}
.chat-btn-group .head-btn:nth-child(2) .iconfont {
color: cornflowerblue;
}
.head-btn .iconfont {
font-size: 50rpx;
}
.log-out {
/* background: #ffd4d4; */
}
.log-out .iconfont {
color: #ff0000;
}
/* ==========聊天主体部分========== */
.main-chat {
display: flex;
flex: 1;
height: 0;
width: 100%;
flex-direction: column;
}
/* ==========聊天对话展示部分========== */
.chat-messages {
display: flex;
flex: 1;
height: 0;
width: 100%;
padding: 0rpx 20rpx;
box-sizing: border-box;
flex-direction: column;
overflow: auto;
}
.chat-message {
display: flex;
align-items: flex-start;
margin: 10rpx 0;
box-sizing: border-box;
}
.message-user {
flex-direction: row-reverse;
}
.chat-avatar {
display: flex;
justify-content: center;
align-items: center;
width: 80rpx;
height: 80rpx;
box-sizing: border-box;
border-radius: 10rpx;
margin-right: 10rpx;
}
.friend-avatar {
display: flex;
justify-content: center;
align-items: center;
border-radius: 20rpx;
width: 100%;
height: 100%;
overflow: hidden;
}
.chat-avatar-user {
margin-right: 0;
margin-left: 10rpx;
}
.chat-content {
width: auto;
max-width: calc(100% - 100rpx);
height: auto;
padding: 20rpx;
box-sizing: border-box;
border: 1rpx solid #999;
overflow-x: auto;
}
.chat-content-user {
background-color: #c4b5fd;
color: #fff;
border-radius: 20rpx;
}
/* 图片消息 */
.message-image {
max-width: 300rpx;
border-radius: 8rpx;
margin: 6rpx 0;
}
/* 文件列表 */
.message-file-list {
margin: 8rpx 0;
}
/* 文件项 */
.file-item {
margin: 6rpx 0;
}
/* 文件样式 */
.message-file {
display: flex;
align-items: center;
padding: 12rpx 16rpx;
background: #f7f8fa;
border-radius: 8rpx;
max-width: 350rpx;
}
.file-icon {
font-size: 32rpx;
margin-right: 12rpx;
}
.file-name {
flex: 1;
font-size: 26rpx;
color: #333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.file-size {
font-size: 22rpx;
color: #999;
margin-left: 8rpx;
}
/* ==========聊天输入容器部分========== */
.chat-interactive-container {
display: flex;
flex-shrink: 0;
width: 100%;
padding: 20rpx;
box-sizing: border-box;
flex-direction: column;
}
.chat-interactive-group {
display: flex;
width: 100%;
padding: 20rpx;
box-sizing: border-box;
justify-content: flex-start;
align-items: center;
}
.chat-interactive-btn {
display: flex;
align-items: center;
justify-content: center;
border: 1rpx solid #000000;
border-radius: 10rpx;
padding: 10rpx 16rpx;
margin-right: 20rpx;
box-sizing: border-box;
}
.chat-input-container {
display: flex;
width: 100%;
padding: 20rpx;
box-sizing: border-box;
border: 1rpx solid #c4b5fd;
border-radius: 30rpx;
align-items: flex-end;
}
.message-input {
flex: 1;
max-height: 150rpx;
min-height: 60rpx;
overflow-y: auto;
margin: 10rpx;
box-sizing: border-box;
}
.input-btn-group {
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
padding: 16rpx;
box-sizing: border-box;
background: #ffd4d4;
border: 1rpx solid #ff0000;
border-radius: 20rpx;
width: 80rpx;
height: 80rpx;
}
.input-btn-group .iconfont {
color: #ff0000;
font-size: 40rpx;
}
/* ai思考弹窗卡片 */
.ai-card {
width: auto;
border: 1rpx solid #409eff;
box-shadow: 0 0 20rpx #c4b5fd;
}
/* 内容布局 */
.thinking-content {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
/* 加载点点动画 */
.loading-dots {
display: flex;
width: 100%;
align-items: center;
justify-content: center;
}
.dot {
width: 16rpx;
height: 16rpx;
margin: 6rpx;
border-radius: 50%;
background: #409eff;
animation: dotBlink 1.2s infinite ease-in-out;
}
/* 点点呼吸动画 */
@keyframes dotBlink {
0%,
100% {
opacity: 0.3;
transform: scale(0.8);
}
50% {
opacity: 1;
transform: scale(1);
}
}
.dot:nth-child(2) {
animation-delay: 0.2s;
}
.dot:nth-child(3) {
animation-delay: 0.4s;
}
/* 文字 */
.thinking-text {
margin: 20rpx 0;
font-size: 22px;
font-weight: 500;
color: #409eff;
}

1135
pages/Chat/Chat.vue Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,395 @@
.tabbar-page {
display: flex;
flex-direction: column;
}
.content-swiper {
width: 100%;
height: calc(100% - 100rpx);
}
.tabpage-content {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
}
.tabpage-containner {
width: 100%;
height: 100%;
display: flex;
align-items: center;
flex-direction: column;
}
.tabpage-header {
width: 100%;
display: flex;
align-items: center;
flex-shrink: 0;
}
.tabpage-header .iconfont {
font-size: 50rpx;
margin-right: 10rpx;
}
.tabpage-title {
font-size: 40rpx;
}
.tabpage-body {
width: 100%;
display: flex;
flex: 1;
min-height: 0;
align-items: center;
flex-direction: column;
padding: 20rpx;
box-sizing: border-box;
}
/* 新建群聊 */
.group-info {
width: 100%;
height: auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex-shrink: 0;
}
.group-name {
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.group-name .input {
width: 100%;
height: auto;
padding: 20rpx;
box-sizing: border-box;
border: 1rpx solid #007aff;
border-radius: 30rpx;
}
.group-member-wrapper {
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 20rpx;
box-sizing: border-box;
background-color: rgba(16, 185, 129, .05);
border: 1rpx solid rgba(16, 185, 129, .2);
border-radius: 30rpx;
margin: 10rpx 0;
}
.group-member-wrapper .member-title {
color: #059669;
font-weight: bold;
}
.group-ismember-list {
display: flex;
flex-wrap: wrap;
margin-top: 10rpx;
box-sizing: border-box;
max-height: 200rpx;
overflow-y: auto;
}
.group-ismember-info {
padding: 10rpx 16rpx;
box-sizing: border-box;
border: 1rpx solid rgba(16, 185, 129, 0.8);
background-color: #fff;
border-radius: 30rpx;
margin: 5rpx 10rpx;
}
.group-ismember-name {
font-size: 22rpx;
color: #059669;
}
.create-group-btn {
width: 100%;
background: #007aff;
border-radius: 30rpx;
padding: 20rpx;
box-sizing: border-box;
font-weight: bold;
color: #fff;
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 10rpx;
}
.group-member {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
padding: 10rpx;
margin-top: 10rpx;
box-sizing: border-box;
}
.friend-list {
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
}
.friend-card {
display: flex;
justify-content: flex-start;
align-items: center;
width: 100%;
padding: 20rpx;
margin: 10rpx 0;
box-sizing: border-box;
border: 1rpx solid #aaaaff;
border-radius: 30rpx;
}
.friend-card-left {
flex-shrink: 0;
margin-right: 10rpx;
}
.friend-avatar {
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
width: 100rpx;
height: 100rpx;
border-radius: 50%;
overflow: hidden;
}
.friend-card-middle {
flex: 1;
height: auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
}
.friend-name {
font-weight: 500;
font-size: 32rpx;
}
.friend-card-right {
width: auto;
flex-shrink: 0;
display: flex;
justify-content: center;
align-items: center;
}
.friend-checkbox {
width: 30rpx;
height: 30rpx;
border-radius: 10rpx;
display: flex;
justify-content: center;
align-items: center;
border: 1rpx solid #3b86ff;
}
/* 添加好友 */
.search-warpper {
width: 100%;
display: flex;
align-items: center;
}
.search-warpper .search-file-input {
flex: 1;
background-color: #f9fafb;
border: 2rpx solid rgba(139, 195, 232, .2);
padding: 20rpx;
margin-right: 20rpx;
height: auto;
box-sizing: border-box;
border-radius: 30rpx;
}
.search-file-input.active {
background-color: #ffffff;
box-shadow: 0 0 15rpx #aaaaff;
border-color: #007aff;
}
.search-warpper .iconfont {
flex-shrink: 0;
font-size: 38rpx;
font-weight: bold;
color: #fff;
background-color: #007aff;
padding: 18rpx;
box-sizing: border-box;
border-radius: 20rpx;
}
.search-result {
width: 100%;
}
.add-btn {
background: #3b86ff;
color: #fff;
font-size: 14px;
font-weight: 500;
}
/* 添加好友卡片 */
.nickname-wrapper {
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin: 20rpx 0;
box-sizing: border-box;
}
.input-nickname {
width: 100%;
height: auto;
padding: 20rpx;
box-sizing: border-box;
border: 1rpx solid #007aff;
border-radius: 30rpx;
margin: 20rpx 0;
box-sizing: border-box;
}
.confirm-btn {
background: #3b86ff;
color: #fff;
font-size: 16px;
font-weight: 500;
padding: 10px;
}
/* 视频会议 */
.create-meeting-wrapper,
.join-meeting-wrapper {
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 20rpx 30rpx;
box-sizing: border-box;
}
.input-meeting {
width: 100%;
height: auto;
padding: 20rpx;
box-sizing: border-box;
border: 1rpx solid #007aff;
border-radius: 30rpx;
margin: 10rpx 0;
box-sizing: border-box;
}
.meeting-btn {
width: 100%;
background: #007aff;
margin: 10rpx 0;
color: #fff;
}
.meeting-field {
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
}
.meeting-toggles {
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.meeting-toggles-item {
width: 100%;
display: flex;
justify-content: space-between;
}
/* 好友申请 */
.friend-option {
display: flex;
justify-content: center;
align-items: center;
}
.agree-btn,
.refuse-btn {
font-size: 14px;
font-weight: 500;
border: 1px solid #666;
margin-left: 10rpx;
box-sizing: border-box;
}
/* 通讯录 */
.contact-list {
display: flex;
height: 100%;
padding: 10rpx;
margin-top: 10rpx;
box-sizing: border-box;
}
/* 底部导航 */
.bottom-tabbar {
width: 100%;
height: 140rpx;
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx;
box-sizing: border-box;
background: rgba(200, 200, 200, 0.1);
}
.tab-item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.tab-label {
font-size: 28rpx;
}

View File

@@ -0,0 +1,778 @@
<template>
<view class="status-bar"></view>
<!-- 添加好友弹窗 -->
<view class="popup-overlay" v-if="currentAddFriend">
<view class="popup-card">
<view class="close-popup-card" @click="closeAddFriendCard">
<view class="iconfont icon-quxiao"></view>
</view>
<view class="popup-title">{{isAgreeBeFriend ? '通过好友申请' : '申请添加朋友'}}</view>
<view class="friend-card">
<view class="friend-card-left">
<view class="friend-avatar">{{currentAddFriend.avatar}}</view>
</view>
<view class="friend-card-middle">
<view class="friend-name">{{currentAddFriend.username}}</view>
<view class="friend-label">{{currentAddFriend.sessionId}}</view>
</view>
</view>
<view class="nickname-wrapper">
<view>好友备注</view>
<input class="input-nickname" placeholder="请输入好友名称" />
</view>
<view class="text-btn confirm-btn">{{isAgreeBeFriend ? '确认通过' : '发送申请'}}</view>
</view>
</view>
<view class="tabbar-page page-container">
<swiper class="content-swiper" :current="currentTab" :duration="300" @change="onSwiperChange">
<swiper-item class="content-swiper-item">
<view class="tabpage-containner">
<view class="tabpage-header">
<view class="iconfont icon-fanhui" @click="handleBack"></view>
<view class="tabpage-title">通讯录</view>
</view>
<view class="tabpage-body">
<scroll-view class="group-member" direction="vertical" scroll-y="true">
<view class="friend-list">
<view class="friend-card" @click="changeChatType(0)">
<view class="friend-card-left">
<view class="friend-avatar" style=" background-color: #38bdf8;">
<view class="iconfont icon-Robot" style="font-size: 80rpx; color: #fff;">
</view>
</view>
</view>
<view class="friend-card-middle">
<view class="friend-name">{{FriendAI.name}}</view>
<view class="friend-label"></view>
</view>
<view class="friend-card-right">
<uni-icons type="right"></uni-icons>
</view>
</view>
<view class="friend-card" v-for="friend in FriendInfoList" :key="friend.sessionId" @click="changeChatType(friend.type,friend.sessionId)">
<view class="friend-card-left">
<!-- <view class="friend-avatar">{{friend.avatar}}</view> -->
<!-- 显示头像图片 -->
<image v-if="friend.avatar" :src="friend.avatar" class="friend-avatar"
mode="aspectFill"></image>
<!-- 如果没有头像显示默认头像 -->
<view v-else class="friend-avatar">
<uni-icons type="person-filled" size="100rpx" color="#ffffff"></uni-icons>
</view>
</view>
<view class="friend-card-middle">
<view class="friend-name">{{friend.friendNickName}}</view>
<view class="friend-label">{{friend.sessionId}}</view>
</view>
<view class="friend-card-right">
<uni-icons type="right"></uni-icons>
</view>
</view>
<view>群聊</view>
<view class="solid-line"></view>
<view class="friend-card" v-for="group in GroupList" :key="group.id" @click="changeChatType(group.type,group.id)">
<view class="friend-card-left">
<!-- 群聊只有默认头像 -->
<view class="friend-avatar" style="background-color: #ffaaff;">
<view class="iconfont icon-qunliao" style="font-size: 80rpx; color: #fff;">
</view>
</view>
</view>
<view class="friend-card-middle">
<view class="friend-name">{{group.name}}</view>
<view class="friend-label">{{group.createTime}}</view>
</view>
<view class="friend-card-right">
<uni-icons type="right"></uni-icons>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</swiper-item>
<swiper-item class="content-swiper-item">
<view class="tabpage-containner">
<view class="tabpage-header">
<view class="iconfont icon-fanhui" @click="handleBack"></view>
<view class="tabpage-title">新建群聊</view>
</view>
<view class="tabpage-body">
<view class="group-info">
<view class="group-name">
<view>群名称</view>
<input class="input" placeholder="请输入群名称" v-model="createGroupName" />
</view>
<view class="group-member-wrapper">
<view class="member-title">群成员</view>
<view class="group-ismember-list">
<view class="group-ismember-info" v-for="(item,index) in selectGroupMember"
:key="index">
<view class="group-ismember-name">{{item.friendNickName}}</view>
</view>
</view>
</view>
<view class="create-group-btn" @click="createGroup">创建群聊</view>
</view>
<view class="solid-line"></view>
<scroll-view class="group-member" direction="vertical" scroll-y="true">
<view class="friend-list">
<view class="friend-card" v-for="friend in FriendInfoList" :key="friend.receiver">
<view class="friend-card-left">
<image v-if="friend.avatar" :src="friend.avatar" class="friend-avatar"
mode="aspectFill"></image>
<!-- 如果没有头像显示默认头像 -->
<view v-else class="friend-avatar">
<uni-icons type="person-filled" size="100rpx" color="#ffffff"></uni-icons>
</view>
</view>
<view class="friend-card-middle">
<view class="friend-name">{{friend.friendNickName}}</view>
<view class="friend-label">{{friend.email}}</view>
</view>
<view class="friend-card-right" @click="handlCreateEmember(friend.receiver)">
<view class="friend-checkbox">
<uni-icons type="checkmarkempty"
v-if="selectGroupMember?.find(item => item.id === friend.receiver)"></uni-icons>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</swiper-item>
<swiper-item class="content-swiper-item">
<view class="tabpage-containner">
<view class="tabpage-header">
<view class="iconfont icon-fanhui" @click="handleBack"></view>
<view class="tabpage-title">添加好友</view>
</view>
<view class="tabpage-body">
<view class="search-warpper">
<input class="search-file-input" :class="{active:isSearchFriend}"
@focus="handleFriendSearchFocus" @blur="handleFriendSearchFocus"
v-model="searchFriendName" />
<view class="iconfont icon-sousuo" @click="searchFriend"></view>
</view>
<view v-if="!searchFriendName" class="search-tip">请输入用户名或邮箱进行搜索</view>
<view v-else-if="searchResultList.length" class="search-result">
<scroll-view class="group-member" direction="vertical" scroll-y="true">
<view class="friend-list">
<view class="friend-card" v-for="friend in searchResultList" :key="friend.id">
<view class="friend-card-left">
<view class="friend-avatar">{{friend.avatar}}</view>
</view>
<view class="friend-card-middle">
<view class="friend-name">{{friend.nickname}}</view>
<view class="friend-label">{{friend.email}}</view>
</view>
<view class="friend-card-right" @click="addNewFriend(friend.id)">
<view class="add-btn text-btn">添加</view>
</view>
</view>
</view>
</scroll-view>
</view>
<view v-else class="no-result">未找到相关用户请尝试其他关键词</view>
</view>
</view>
</swiper-item>
<swiper-item class="content-swiper-item">
<view class="tabpage-containner">
<view class="tabpage-header">
<view class="iconfont icon-fanhui" @click="handleBack"></view>
<view class="tabpage-title">视频会议</view>
</view>
<view class="tabpage-body">
<view class="join-meeting-wrapper">
<view>加入会议</view>
<input class="input-meeting" placeholder="请输入会议号" />
<view class="meeting-btn text-btn">加入会议</view>
</view>
<view class="solid-line"></view>
<view class="create-meeting-wrapper">
<view>创建会议</view>
<view class="meeting-field">
<view class="meeting-label">会议主题</view>
<input class="input-meeting" placeholder="例如XXX" />
</view>
<view class="meeting-field">
<view class="meeting-label">会议密码</view>
<input class="input-meeting" placeholder="留空则无密码" />
</view>
<view class="meeting-field">
<view class="meeting-label">邀请好友</view>
<view class="friend-card">
<view class="friend-card-left">
<view class="friend-avatar">avatar</view>
</view>
<view class="friend-card-middle">
<view class="friend-name">friend.nickname</view>
<view class="friend-label">friend.email</view>
</view>
<view class="friend-card-right" @click="addNewFriend(friend.id)">
<view class="add-btn text-btn">添加</view>
</view>
</view>
</view>
<view class="meeting-toggles">
<view class="meeting-toggles-item">
<view class="meeting-toggles-icon icon-btn">
<uni-icons type="mic-filled"></uni-icons>开启麦克风入会
</view>
<view class="meeting-toggles-switch" @click="togglesVoice">
<view class="switch-box" :class="{active : isVoice}">
<view class="switch-track"></view>
</view>
</view>
</view>
<view class="meeting-toggles-item">
<view class="meeting-toggles-icon icon-btn">
<uni-icons type="eye-filled"></uni-icons>开启摄像头入会
</view>
<view class="meeting-toggles-switch" @click="togglesCamera">
<view class="switch-box" :class="{active : isCamera}">
<view class="switch-track"></view>
</view>
</view>
</view>
</view>
<view class="meeting-btn text-btn">加入会议</view>
</view>
</view>
</view>
</swiper-item>
<swiper-item class="content-swiper-item">
<view class="tabpage-containner">
<view class="tabpage-header">
<view class="iconfont icon-fanhui" @click="handleBack"></view>
<view class="tabpage-title">好友申请</view>
</view>
<view class="tabpage-body">
<scroll-view class="group-member" direction="vertical" scroll-y="true">
<view class="friend-list">
<view class="friend-card" v-for="friend in mockFriendDB" :key="friend.id">
<view class="friend-card-left">
<view class="friend-avatar">{{friend.avatar}}</view>
</view>
<view class="friend-card-middle">
<view class="friend-name">{{friend.nickname}}</view>
<view class="friend-label">{{friend.email}}</view>
</view>
<view class="friend-card-right">
<view class="friend-option">
<view class="refuse-btn text-btn" @click="refuseBeFriend(friend)">拒绝</view>
<view class="agree-btn text-btn" @click="agreeBeFriend(friend)">同意</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</swiper-item>
</swiper>
<view class="bottom-tabbar">
<view v-for="(tab, index) in tabList" :key="tab.name" @click="switchTab(index)" class="tab-item">
<view class="tab-icon">{{tab.icon}}</view>
<view class="tab-label">{{tab.label}}</view>
</view>
</view>
</view>
</template>
<script setup>
import {
onMounted,
reactive,
ref,
watch
} from 'vue';
import {
getToken
} from '@/utils/user-info.js'
import {
getChatFriend,
getGroup
} from '@/utils/friend-api.js'
import {
getUserInfo,
getUserAvatar,
searchUsers
} from '@/utils/cloud-api.js'
import {
useFriendSocketStore
} from '@/stores/friend-socket.js'
const friendSocketStore = useFriendSocketStore()
// 连接实时通讯socket
const handleFriendConnect = () => {
if (friendSocketStore.isConnected) return
if (!userToken.value || !UserId.value) {
console.warn('Token或UserId未准备好');
return
}
friendSocketStore.connect({
token: userToken.value,
UserId: UserId.value
})
}
// 返回聊天界面
const handleBack = () => {
uni.navigateBack({
delta: 1,
fail() {
uni.reLaunch({
url: '/pages/Chat/Chat'
})
}
})
}
// 选择群聊或对话时,修改聊天类型并返回聊天页面
const changeChatType = (type,sessionId='') =>{
uni.setStorageSync('chatType', type)
if(sessionId){
uni.setStorageSync('currentSessionId',sessionId)
}
uni.reLaunch({
url: '/pages/Chat/Chat'
})
}
// 底部Tab相关
// 当前选中的tab索引 (0:新建群聊, 1:添加好友, 2:视频会议, 3:好友申请, 4:通讯录)
const currentTab = ref(0)
// Tab列表
const tabList = reactive([{
name: 'contacts',
label: '通讯录',
icon: '📞'
},
{
name: 'createGroup',
label: '新建群聊',
icon: '👥'
},
{
name: 'addFriend',
label: '添加好友',
icon: ''
},
{
name: 'videoCall',
label: '视频会议',
icon: '📹'
},
{
name: 'friendRequest',
label: '好友申请',
icon: '📨'
}
])
const onSwiperChange = (e) => {
currentTab.value = e.detail.current
}
const switchTab = (index) => {
currentTab.value = index
}
// 用户token
const userToken = ref('')
// 用户id
const UserId = ref('')
// 通讯录相关
// AI好友
const FriendAI = {
id: 'ai_robot',
name: '宇恒一号',
avatar: '',
type: 'ai',
description: 'AI智能助手',
lastMessage: null
}
// 好友列表
// const FriendList = ref([])
// 好友消息列表(不包括头像)
const FriendInfoList = ref([])
// 获取用户好友列表
const takeFriendList = async () => {
try {
console.log("开始获取好友列表");
userToken.value = getToken();
const userInfo = await getUserInfo(userToken.value)
UserId.value = userInfo._id
console.log("UserId.value", UserId.value);
const friendList = await getChatFriend(UserId.value)
console.log("friendList:", friendList);
if (friendList && friendList.length) {
FriendInfoList.value = await takeUserAvatar(friendList)
} else {
FriendInfoList.value = []
console.log("好友列表为空");
}
} catch (error) {
console.error("获取好友列表失败:", error);
FriendInfoList.value = []
}
}
// 获取好友头像
const takeUserAvatar = async (friendList) => {
if (!friendList?.length) return []
try {
const friendIds = friendList.map(item => item.receiver)
const friendAvatarList = await getUserAvatar(userToken.value, friendIds)
// 创建map,用于快速查找
const userMap = new Map(friendAvatarList.map(user => [user.user_id, user]) || [])
// 合并数据
return friendList.map(friend => {
const userInfo = userMap.get(friend.receiver)
return {
...friend,
avatar: userInfo?.avatar || null
}
})
} catch (err) {
console.error('获取好友头像失败', err);
return friendList
}
}
onMounted(async () => {
await takeFriendList()
await takeGroupList()
handleFriendConnect()
})
// 群聊相关
// 群聊列表
const GroupList = ref([])
// 获取用户群聊列表
const takeGroupList = async () => {
try {
console.log("开始获取群聊列表");
userToken.value = getToken();
const userInfo = await getUserInfo(userToken.value)
UserId.value = userInfo._id
console.log("UserId.value", UserId.value);
GroupList.value = await getGroup(UserId.value)
} catch (error) {
console.error("获取群聊列表失败:", error);
GroupList.value = []
}
}
// 新建群聊相关
const createGroupName = ref('')
const createGroupData = ref()
// 确保 friendSocket 已连接
const ensureFriendSocketConnected = () => {
return new Promise((resolve, reject) => {
// 已连接则直接返回
if (friendSocketStore.isConnected) {
resolve()
return
}
// 未连接,先建立连接
if (!userToken.value || !UserId.value) {
reject(new Error('Token或UserId未准备好'))
return
}
console.log('friendSocket 未连接,正在建立连接...')
friendSocketStore.connect({
token: userToken.value,
UserId: UserId.value
})
// 等待连接建立
const checkInterval = setInterval(() => {
if (friendSocketStore.isConnected) {
clearInterval(checkInterval)
console.log('friendSocket 连接已建立')
resolve()
}
}, 100)
// 最多等待10秒
setTimeout(() => {
clearInterval(checkInterval)
if (!friendSocketStore.isConnected) {
reject(new Error('连接超时'))
}
}, 10000)
})
}
const createGroup = async () => {
try {
//检查是否有群成员和群名称
if(!createGroupName.value) {
uni.showToast({
title:'群名称不能为空',
icon:'none'
})
return
}
//检查是否有群成员和群名称
if(selectGroupMember.value.length === 0) {
uni.showToast({
title:'请选择群成员',
icon:'none'
})
return
}
// 确保 socket 已连接
await ensureFriendSocketConnected()
const memberIds = selectGroupMember.value.map(member => member.id)
createGroupData.value = {
"version": "1.1",
"body": {
"command": 3,
"userIds": memberIds,
"message": "我来邀请你们了",
"sender": UserId.value,
"callBackMessage": true,
"groupName": createGroupName.value,
"teamCreateReq": {
"access_token": userToken.value
}
}
}
const isSend = await friendSocketStore.send(createGroupData.value)
if (isSend) {
await sendCreateGroup();
} else {
console.log("发送新建群聊消息失败");
}
} catch (err) {
console.log(err);
uni.showToast({
title: `新建群聊失败${err}`,
icon: 'none'
})
}
}
// 发送创建群聊的socket消息
const sendCreateGroup = () => {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('超时未返回群聊创建结果'))
}, 180000)
// 创建一个一次性监听器, // 监听command的值如果是3就检查有没有创建群聊成功
const unwatch =
watch(() => friendSocketStore.command, async (newVal) => {
console.log("创建一个一次性监听器");
if (newVal && friendSocketStore.command === 3) {
console.log("监听到command的值是3,检查群聊是否创建成功", friendSocketStore.command);
try {
await takeGroupList()
const newGroup = GroupList.value.find(g => g.id === friendSocketStore
.groupId)
// 2. 判断是否新建成功
if (newGroup) {
console.log("✅ 新建群聊成功", newGroup);
selectGroupMember.value = []
currentTab.value = 0
} else {
console.log("❌ 新建群聊失败 / 群组不存在");
}
} catch (error) {
console.error('保存AI回复失败', error);
reject(error);
} finally {
friendSocketStore.command = null
friendSocketStore.groupId = null
unwatch();
clearTimeout(timeout);
}
}
})
})
}
// 已选群成员列表
const selectGroupMember = ref([]);
const handlCreateEmember = (id) => {
console.log("选择的好友id",id);
console.log("selectGroupMember.value=",JSON.stringify(selectGroupMember.value));
const index = selectGroupMember.value.findIndex(item => item.id === id);
if (index === -1) {
const friendItem = FriendInfoList.value.find(item => item.receiver === id);
if (friendItem) {
selectGroupMember.value.push({
id: friendItem.receiver,
friendNickName: friendItem.friendNickName
})
}
} else {
selectGroupMember.value.splice(index, 1);
}
}
// 添加好友相关
const isSearchFriend = ref(false)
const searchFriendName = ref('')
const searchResultList = ref([]);
const searchFriend = async() => {
if (!searchFriendName.value.trim()) {
searchResultList.value = []
return
}
// 模糊查询:遍历 mockFriendDB匹配 nickname、username 或 email
const keyword = searchFriendName.value.toLowerCase().trim();
searchResultList.value = await searchUsers(userToken.value,keyword)
console.log("searchResultList.value",searchResultList.value);
// const results = mockFriendDB.value.filter(friend => {
// const isMatch = friend.nickname.toLowerCase().includes(keyword) || friend.username.toLowerCase()
// .includes(keyword) || friend.email.toLowerCase().includes(keyword);
// return isMatch
// })
// searchResultList.value = results
}
const handleFriendSearchFocus = () => {
isSearchFriend.value = !isSearchFriend.value
}
const currentAddFriend = ref(null)
const addNewFriend = (id) => {
const friend = mockFriendDB.value.find(friend => friend.id === id)
if (friend) {
// 创建副本避免引用问题
currentAddFriend.value = {
...friend
}
} else {
console.warn('未找到该好友')
currentAddFriend.value = null
}
}
const closeAddFriendCard = () => {
currentAddFriend.value = null
isAgreeBeFriend.value = false
}
// 视频会议相关
const isVoice = ref(false);
const isCamera = ref(false)
const togglesVoice = () => {
isVoice.value = !isVoice.value
}
const togglesCamera = () => {
isCamera.value = !isCamera.value
}
// 好友申请相关
const isAgreeBeFriend = ref(false)
const refuseBeFriend = (friend) => {
}
const agreeBeFriend = (friend) => {
isAgreeBeFriend.value = true
currentAddFriend.value = {
...friend
}
}
// 模拟好友数据库
const mockFriendDB = ref([{
id: 1,
nickname: '张三',
username: 'zhangsan',
email: 'zhangsan@example.com',
avatar: '👤'
},
{
id: 2,
nickname: '李四',
username: 'lisi',
email: 'lisi@example.com',
avatar: '👥'
},
{
id: 3,
nickname: '王小明',
username: 'wangxm',
email: 'wang@example.com',
avatar: '👪'
},
{
id: 4,
nickname: '赵磊',
username: 'zhaolei',
email: 'zhao@example.com',
avatar: '🗣️'
},
{
id: 5,
nickname: '张三2',
username: 'zhangsan',
email: 'zhangsan@example.com',
avatar: '👤'
},
{
id: 6,
nickname: '李四2',
username: 'lisi',
email: 'lisi@example.com',
avatar: '👥'
},
{
id: 7,
nickname: '王小明2',
username: 'wangxm',
email: 'wang@example.com',
avatar: '👪'
},
{
id: 8,
nickname: '赵磊2',
username: 'zhaolei',
email: 'zhao@example.com',
avatar: '🗣️'
}
])
// 通讯录相关
</script>
<style scoped>
@import url("ContactPages.css");
</style>

373
pages/Login/Login.vue Normal file
View File

@@ -0,0 +1,373 @@
<template>
<view class="status-bar"></view>
<view class="login-container page-container">
<view class="login-card">
<!-- <view class="conner conner-tl"></view>
<view class="conner conner-tr"></view>
<view class="conner conner-bl"></view>
<view class="conner conner-br"></view> -->
<view class="login-header">
<view class="login-header-logo">
<view class="icon-wrapper">
<view class="iconfont icon-brain-2-fill danao-style"></view>
</view>
<text>YXD</text>
</view>
<view class="sub-title">File Handling Chat</view>
</view>
<!-- 表单 -->
<view class="form-wrapper">
<view class="form-item">
<view class="form-label">
<!-- <uni-icons type="person" size="24" color="#aaaaff"></uni-icons> -->
<text>用户名</text>
</view>
<input class="form-input" :class="{focused: isUsernameFocused, error: isUsernameError}"
v-model="UsernameValue" @focus="usernameFocused" @blur="handleUsernameBlur"
@input="validateUsername" placeholder="请输入用户名" />
</view>
<view class="form-item">
<view class="form-label">
<!-- <uni-icons type="locked" size="24" color="#aaaaff"></uni-icons> -->
<text>密码</text>
</view>
<input type="password" class="form-input"
:class="{focused: isPasswordFocused, error: isPasswordError}" v-model="PasswordValue"
@focus="passwordFocused" @blur="handlePasswordBlur" placeholder="请输入密码" />
</view>
<text v-if="formDataHasNull" class="error-info">用户名或密码不能为空</text>
<view class="form-option">
<view class="checkbox-wrapper" @click="form.remember = !form.remember">
<view class="checkbox" :class="{ checked: form.remember}">
<text v-if="form.remember"></text>
</view>
<text class="checkbox-text">记住密码</text>
</view>
<text class="forget-pwd">忘记密码</text>
</view>
<button class="login-btn" :disabled="loading" :loading="loading"
@click="handleLogin">{{loading ? 'Loading...' : '登录'}}</button>
</view>
</view>
</view>
</template>
<script setup>
import {
reactive,
ref,
computed,
onMounted
} from 'vue';
import {
login
} from '../../utils/cloud-api';
// 表单用户名变量
const UsernameValue = ref('');
const isUsernameFocused = ref(false);
const isUsernameError = ref(false)
const usernameFocused = () => {
isUsernameFocused.value = true;
formDataHasNull.value = false;
}
const handleUsernameBlur = () => {
isUsernameFocused.value = false;
}
// 用户名实时校验
const validateUsername = () => {
// if (UsernameValue.value && UsernameValue.value.length < 3) {
// isUsernameError.value = true;
// } else {
// isUsernameError.value = false;
// }
};
// 表单密码变量
const PasswordValue = ref('');
const isPasswordFocused = ref(false);
const isPasswordError = ref(false);
const passwordFocused = () => {
isPasswordFocused.value = true;
formDataHasNull.value = false;
}
const handlePasswordBlur = () => {
isPasswordFocused.value = false;
}
// 表单数据
const form = reactive({
username: computed(() => UsernameValue.value),
password: computed(() => PasswordValue.value),
remember: true
})
// 登录状态
const loading = ref(false)
//用户名密码是否非空
const formDataHasNull = ref(false)
const handleLogin = async() => {
checkFormData();
if (formDataHasNull.value) return
loading.value = true
try {
const token = await login(UsernameValue.value, PasswordValue.value)
// 登陆成功,保存登录数据到手机
saveLoginInfo();
uni.reLaunch({
url: '/pages/Chat/Chat'
})
} catch(err) {
console.log('登录失败',err);
uni.showToast({
title:err || '失败',
icon:'error'
})
}finally {
loading.value = false
}
}
// 检查用户名或密码是否为空
const checkFormData = () => {
if (!UsernameValue.value || !PasswordValue.value) {
formDataHasNull.value = true;
}
}
// 保存登录信息到手机
const saveLoginInfo = () => {
if (form.remember) {
const loginInfo = {
username: form.username,
password: form.password,
remember: true
}
uni.setStorageSync('login_info', loginInfo)
uni.setStorageSync('chatType', 0)
} else {
uni.removeStorageSync('login_info')
}
}
// 从手机加载登录消息
const loadLoginInfo = () => {
try {
const loginInfo = uni.getStorageSync('login_info')
if (loginInfo) {
UsernameValue.value = loginInfo.username || '';
PasswordValue.value = loginInfo.password || '';
form.remember = loginInfo.remember ?? true;
}
} catch (e) {
console.error('读取登录信息失败', e)
}
}
onMounted(() => {
loadLoginInfo();
})
</script>
<style scoped>
.login-container {
padding: 140rpx 60rpx 100rpx 60rpx;
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.login-card {
position: relative;
flex: 1;
width: 100%;
height: 100%;
padding: 20rpx;
box-sizing: border-box;
background: #ffffff;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.conner {
position: absolute;
width: 40rpx;
height: 40rpx;
}
.conner-tl {
top: 0;
left: 0;
border-top: 2px solid #aaaaff;
border-left: 2px solid #aaaaff;
}
.conner-tr {
top: 0;
right: 0;
border-top: 2px solid #aaaaff;
border-right: 2px solid #aaaaff;
}
.conner-bl {
bottom: 0;
left: 0;
border-bottom: 2px solid #aaaaff;
border-left: 2px solid #aaaaff;
}
.conner-br {
bottom: 0;
right: 0;
border-bottom: 2px solid #aaaaff;
border-right: 2px solid #aaaaff;
}
.login-header {
/* position: absolute;
top: 80rpx; */
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
.login-header-logo {
display: flex;
justify-content: center;
align-items: center;
}
.login-header-logo text {
margin-left: 8rpx;
font-size: 60rpx;
font-weight: bold;
color: #aaaaff;
text-shadow: 0 0 10rpx rgba(170, 170, 255, 0.4),
0 0 20rpx rgba(170, 170, 255, 0.3);
}
.icon-wrapper {
width: 90rpx;
height: 90rpx;
border-radius: 50%;
background-color: rgb(255, 255, 255);
display: flex;
align-items: center;
justify-content: center;
animation: glow 2s ease-in-out infinite;
}
@keyframes glow {
0%,
100% {
box-shadow: 0 0 20rpx rgba(170, 170, 255, 0.3);
}
50% {
box-shadow: 0 0 20rpx rgba(170, 170, 255, 0.4),
0 0 30rpx rgba(170, 170, 255, 0.3),
0 0 50rpx rgba(170, 170, 255, 0.2);
}
}
.icon-wrapper .danao-style {
font-size: 70rpx;
color: #aaaaff;
}
.sub-title {
margin-top: 20rpx;
font-size: 36rpx;
font-weight: 500;
color: #666;
text-shadow: 0 0 10rpx #999;
}
.form-wrapper {
margin-top: 50rpx;
width: 500rpx;
padding: 80rpx 30rpx;
background-color: rgba(170, 170, 255, 0.05);
border-radius: 20rpx;
/* border: 1rpx solid #aaaaff; */
box-shadow: 0 0 10rpx rgba(170, 170, 255, 0.5);
display: flex;
flex-direction: column;
}
.form-item {
margin-top: 20rpx;
}
.form-item .form-input {
width: 100%;
height: 80rpx;
padding: 0 10px;
box-sizing: border-box;
border-radius: 30rpx;
border: 1rpx solid #e5e5e5;
}
.form-item .focused {
border-color: #aaaaff;
box-shadow: 0 0 10rpx rgba(170, 170, 255, 0.5);
}
.form-item .error {
border-color: #ff0000;
box-shadow: 0 0 10rpx rgba(255, 0, 0, 0.5);
}
.error-info {
font-size: 24rpx;
color: #ff0000;
}
.form-option {
margin-top: 10rpx;
display: flex;
justify-content: space-between;
}
.form-option .checkbox-wrapper {
display: flex;
justify-content: start;
align-items: center;
}
.form-option .checkbox-wrapper .checkbox {
width: 30rpx;
height: 30rpx;
border: 1rpx solid #666;
display: flex;
justify-content: center;
align-items: center;
}
.form-option .checkbox-wrapper .checked {
background-color: #aaaaff;
}
.form-option .checkbox-wrapper .checkbox text {
color: #ffffff;
}
:deep(.login-btn) {
margin-top: 30rpx;
width: 500rpx;
height: 100rpx;
border-radius: 80rpx;
background: linear-gradient(to right, #aaaaff, #2969ed);
}
</style>

View File

@@ -0,0 +1,276 @@
<template>
<view class="status-bar"></view>
<view class="user-profile page-container">
<view class="modal-header">
<view class="modal-left" @click="closeUserCard">
<view class="iconfont icon-fanhui custom-navbar-icon"></view>
</view>
<view class="modal-title">
<view class="title-text">编辑资料</view>
</view>
<view class="modal-right" @click="saveUpdate">
<view class="save-btn">保存</view>
</view>
</view>
<view class="modal-body">
<view class="avatar-upload-section" @click="triggerAvatarUpload">
<view class="avatar-upload-container">
<view v-if="editUserAvatar" class="avatar-preview">
<image :src="editUserAvatar" mode="aspectFill"></image>
</view>
<view v-else class="avatar-placeholder">
<uni-icons type="person-filled" size="100rpx" color="#ffffff"></uni-icons>
</view>
</view>
</view>
<view class="form-card">
<view class="form-field">
<view class="form-text">用户名</view>
<input adjust-position="false" class="form-field-input form-field-username"
:class="{'edit-input-focus':isNameFocus}" v-model="editUsername" @focus="isNameFocus=true"
@blur="isNameFocus=false" />
</view>
<view class="solid-line"></view>
<view class="form-field">
<view class="form-text">邮箱</view>
<input adjust-position="false" class="form-field-input form-field-emile"
:class="{'edit-input-focus':isEmailFocus}" v-model="editEmail" @focus="isEmailFocus=true"
@blur="isEmailFocus=false" />
</view>
</view>
<view class="prompt-content">*保存后再返回哟</view>
</view>
</view>
</template>
<script setup>
import {
onMounted,
ref
} from 'vue';
import { getUserInfo } from '@/utils/cloud-api.js'
import {getToken} from '@/utils/user-info.js'
const closeUserCard = () => {
console.log("点击了返回")
uni.navigateBack({
delta: 1,
fail() {
uni.reLaunch({
url: '/pages/Chat/Chat'
})
}
})
}
const saveUpdate = () => {
uni.navigateBack({
url: '/pages/Chat/Chat'
})
}
const editUserAvatar = ref('')
const editUsername = ref('')
const editEmail = ref('')
const isNameFocus = ref(false)
const isEmailFocus = ref(false)
// 上传头像
const triggerAvatarUpload = () => {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
const tempFilePath = res.tempFilePaths[0]
handlrImageSelected(tempFilePath)
},
fail: (err) => {
console.log('选择图片失败', err);
}
})
}
// 处理选择的图片
const handlrImageSelected = (filePath) => {
editUserAvatar.value = filePath
}
onMounted(async()=>{
const token = getToken()
const UserInfo = await getUserInfo(token)
editUserAvatar.value = UserInfo.avatar || ''
editUsername.value = UserInfo.username || ''
editEmail.value = UserInfo.email || ''
})
</script>
<style lang="scss" scoped>
.user-profile {
display: flex;
flex-direction: column;
align-items: center;
background-color: rgba(193, 193, 193, 0.1);
}
.modal-header {
position: relative;
width: 100%;
height: auto;
padding: 20rpx 20rpx;
box-sizing: border-box;
display: flex;
justify-content: space-between;
align-items: center;
.modal-left {
display: flex;
align-items: center;
justify-content: center;
z-index: 2;
}
.modal-title {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
width: 100%;
height: auto;
left: 50%;
transform: translate(-50%);
.title-text {
font-size: 16px;
font-weight: 500;
}
}
.modal-right {
display: flex;
align-items: center;
justify-content: center;
size: 40rpx;
z-index: 2;
.save-btn {
background-color: #3b82f6;
color: #ffffff;
padding: 8rpx 20rpx;
box-sizing: border-box;
border-radius: 10rpx;
}
}
}
.modal-body {
width: 100%;
height: auto;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 50rpx;
box-sizing: border-box;
.avatar-upload-section {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
margin-bottom: 10rpx;
box-sizing: border-box;
.avatar-upload-container {
width: 200rpx;
height: 200rpx;
border-radius: 50%;
padding: 5rpx;
margin-top: 20rpx;
box-sizing: border-box;
background: #f3f4f6;
display: flex;
justify-content: center;
align-items: center;
border: 5rpx solid #dddddd;
box-sizing: border-box;
.avatar-preview {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
border-radius: 50%;
overflow: hidden;
image {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.avatar-placeholder {
display: flex;
justify-content: center;
align-items: center;
padding: 10rpx;
box-sizing: border-box;
border-radius: 50%;
background: #9ca3af;
}
}
}
.form-card {
width: 100%;
padding: 20rpx;
background: #ffffff;
box-sizing: border-box;
border-radius: 20rpx;
margin: 20rpx 0;
}
.form-field {
display: flex;
justify-content: center;
align-items: flex-start;
flex-direction: column;
margin: 20rpx 0rpx;
width: 100%;
.form-text {
font-size: 15px;
font-weight: 300;
}
.form-field-input {
width: 100%;
height: 80rpx
}
.edit-input-focus {
background: rgba(170, 255, 255, 0.1);
}
.form-field-username {
}
.form-field-emile {}
}
.prompt-content {
width: 100%;
font-size: 10px;
color: #9ca3af;
}
}
</style>

View File

@@ -0,0 +1,100 @@
/* 编辑模板弹窗 */
.edit-template-wrapper {
}
.template-grid {
width: 100%;
display: grid;
grid-template-columns: repeat(2, 1fr);
padding: 20rpx;
box-sizing: border-box;
gap: 20rpx;
align-items: start;
}
.template-card {
display: flex;
flex-direction: column;
box-sizing: border-box;
border-radius: 20rpx;
border: 1rpx solid #ddd;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
overflow: hidden;
background-color: #fff;
}
.template-name {
width: 100%;
background: linear-gradient(to right, rgba(0, 122, 255, 0.8), rgba(170, 170, 255, 0.8));
color: #fff;
font-weight: bold;
font-size: 18px;
padding: 10rpx 20rpx;
box-sizing: border-box;
}
.template-content {
display: flex;
flex-wrap: wrap;
min-height: 200rpx;
padding: 18rpx;
margin: 6rpx 6rpx 0 6rpx;
box-sizing: border-box;
border: 1px solid #eee;
border-radius: 20rpx;
box-shadow: 2rpx 5rpx 8rpx rgba(170, 170, 255, 0.3);
overflow: hidden;
}
.template-content-text {
width: 100%;
color: #666;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 4;
/* 限制显示行数 */
overflow: hidden;
text-overflow: ellipsis;
white-space: normal;
word-break: break-all;
line-height: 1.4;
}
.template-info-wrapper {
padding: 0 16rpx;
}
.template-item {
display: flex;
flex-direction: column;
color: #007aff;
font-size: 12px;
}
.template-item .background-style {
display: flex;
box-sizing: border-box;
border-radius: 10rpx;
align-self: flex-start;
margin-top: 6rpx;
}
.template-option {
display: flex;
justify-content: space-between;
align-items: center;
margin: 10rpx;
}
.template-option .iconfont {
color: #007aff;
}
.iconfont.icon-favorite-fill {
color: #ffaa00;
}

View File

@@ -0,0 +1,482 @@
<template>
<view class="status-bar"></view>
<!-- 编辑模板弹窗 -->
<view class="popup-overlay">
<view class="popup-card">
<view class="close-popup-card"><view class="iconfont icon-quxiao"></view></view>
<view class="popup-title">编辑模板</view>
<view class="edit-template-wrapper">
<view class="edit-template-name">
<view>模板名称</view>
<input placeholder-style="请输入模板名称" v-model="templateName"/>
</view>
<view class="edit-template-name">
<view>模板内容</view>
<view class="markdown-editor-pane">
<view class="markdown-pane-header">
编辑
</view>
<textarea>
</textarea>
</view>
<view class="markdown-preview-pane">
<view class="markdown-pane-header">
预览
</view>
<view></view>
</view>
</view>
</view>
</view>
</view>
<view class="workspace-container page-container">
<view class="workspace-header">
<view class="custom-navbar">
<view class="navbar-left" @click="handleBackOrCheck ">
<view class="iconfont icon-fanhui custom-navbar-icon"></view>
<view class="navbar-before-title workspace-text">{{isSelectFolder?'全选':navbarBeforeTitle}}</view>
</view>
<view class="navbar-title workspace-text">{{navbarTitle}}</view>
<view class="navbar-right" v-if="checkTemplate">
<view v-if="isSelectFolder" class="navbar-right-text workspace-text" @click="handleSelectFolder()">
完成</view>
<view v-else class="iconfont icon-gengduo" :class="isMenuOpen ?'menu-open':'custom-navbar-icon'"
@click="handleMenu"></view>
</view>
<view v-if="isMenuOpen" class="menu-card">
<view class="menu-card-item" @click="handleSelectFolder();handleMenu()">选择</view>
<view class="solid-line"></view>
<view class="menu-card-item">新建模板</view>
<view class="solid-line"></view>
<view class="menu-card-item">上传模板</view>
</view>
</view>
<view class="search-file-warpper">
<view class="search-file">
<view class="iconfont icon-sousuo"></view>
<input class="search-file-input" placeholder="搜索" @focus="handleSearchFocus"
v-model="searchKeyword" />
</view>
<view v-if="isSearchFocus" class="cancel-search" @click="handleSearchFocus">取消</view>
</view>
</view>
<view class="folder-grid" v-if="!checkTemplate">
<view class="folder-item" @click="checkShowTemplate('all')">
<view class="iconfont icon-a-wenjianjiawenjian folder-item-style"></view>
<view class="folder-name">全部模板</view>
</view>
<view class="folder-item" @click="checkShowTemplate('like')">
<view class="iconfont icon-a-wenjianjiawenjian folder-item-style"></view>
<view class="folder-name">收藏模板</view>
</view>
</view>
<view class="workspace-content" v-if="checkTemplate">
<view class="folder-null" v-if="currentTemplateList.length===0">
<view class="iconfont icon-wenjianjia"></view>
暂无模板
</view>
<view class="template-grid" v-if="currentTemplateList.length>0">
<view class="template-card" v-for="(item,index) in currentTemplateList" :key="index">
<view class="template-name">{{item.name}}</view>
<view class="template-content">
<view class="template-content-text">
{{item.content}}
</view>
</view>
<view class="template-info-wrapper">
<view class="template-item">
<view class="template-time background-style">{{formatTime(item.modified_time)}}</view>
<view class="template-cache background-style">{{formatSize(item.size)}}</view>
</view>
<view class="template-option">
<view class="iconfont" :class="item.is_favorite ? 'icon-favorite-fill':'icon-favorite'"
@click="handleLikeTemplate(index)"></view>
<view class="iconfont icon-edit-fill" @click="editTemplate"></view>
<view class="iconfont icon-choose-fill"></view>
</view>
</view>
</view>
</view>
</view>
<view v-if="isSelectFolder" class="workspace-footer" :class="{'folder-actions':selectFileList.length>0}">
<view class="iconfont icon-shangchuan"></view>
<view class="iconfont icon-fuzhi"></view>
<view class="iconfont icon-wenjian"></view>
<view class="iconfont icon-del"></view>
<view class="iconfont icon-gengduo"></view>
</view>
</view>
</template>
<script setup>
import {
onMounted,
ref
} from 'vue';
// 格式化时间函数
const formatTime = (timestamp) => {
if (!timestamp) return ''
// 处理包含毫秒的时间戳
const seconds = Math.floor(timestamp)
const date = new Date(seconds * 1000)
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
const hours = date.getHours().toString().padStart(2, '0')
const minutes = date.getMinutes().toString().padStart(2, '0')
const seconds_part = date.getSeconds().toString().padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds_part}`
}
// 转换字节大小
const formatSize = (bytes) => {
if (!bytes || bytes === 0) return '0 B'
const units = ['B', 'KB', 'MB', 'GB', 'TB']
let size = bytes
let unitIndex = 0
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024
unitIndex++
}
// 保留一位或两位小数
return `${size.toFixed(1)} ${units[unitIndex]}`
}
// 核心:点赞/取消点赞
const handleLikeTemplate = (index) => {
let targetItem
// 1. 根据当前展示类型,找到真实被点击的项
if (checkTemplate.value === 'all') {
// 全部列表:直接用 index
targetItem = allTemplate.value[index]
} else if (checkTemplate.value === 'like') {
// 收藏列表:先过滤出收藏项,再用 index 找到
const favoriteList = allTemplate.value.filter(item => item.is_favorite)
targetItem = favoriteList[index]
}
// 2. 切换收藏状态
if (targetItem) {
targetItem.is_favorite = !targetItem.is_favorite
}
// 3. 关键:切换后重新刷新当前列表(否则页面不更新)
if (checkTemplate.value === 'all') {
currentTemplateList.value = [...allTemplate.value]
} else {
currentTemplateList.value = allTemplate.value.filter(item => item.is_favorite)
}
}
// 编辑模板
const templateName = ref('aaa')
const editTemplate = () => {
console.log('点击了编辑模板');
}
// 选择的模板区全部or收藏
const checkTemplate = ref('')
const currentTemplateList = ref([])
const checkShowTemplate = (type) => {
checkTemplate.value = type;
if (type === 'all') {
navbarTitle.value = '全部模板'
currentTemplateList.value = allTemplate.value
}
if (type === 'like') {
navbarTitle.value = '收藏模板'
currentTemplateList.value = allTemplate.value.filter(item => item.is_favorite === true)
}
}
const navbarBeforeTitle = ref('');
const navbarTitle = ref('模板区');
// 选择文件模式
const isSelectFolder = ref(false);
const selectFileList = ref([]);
const handleSelectFolder = () => {
if (isSelectFolder.value) {
selectFileList.value = []
}
isSelectFolder.value = !isSelectFolder.value;
}
const selectFolder = (id) => {
const index = selectFileList.value.indexOf(id);
if (index !== -1) {
selectFileList.value.splice(index, 1);
} else {
selectFileList.value.push(id);
}
}
const handleLongPress = (folder) => {
if (isSelectFolder.value) return;
uni.showActionSheet({
itemList: ['重命名', '删除', '下载', '压缩'],
success: (res) => {
switch (res.tapIndex) {
case 0:
handleRename(folder);
break;
case 1:
handleDelete(folder);
break;
case 2:
handleDownload(folder);
break;
case 3:
handleDownload(folder);
break;
}
}
})
}
// 重命名
const handleRename = (folder) => {
uni.showModal({
title: '重命名',
content: '请输入新名称',
editable: true,
placeholderText: folder.name,
success: (res) => {
if (res.confirm && res.content) {
// 更新文件夹名称
updateFolderName(folder.id, res.content);
}
}
});
};
// 移动
const handleDownload = (folder) => {
// 可以选择跳转到移动页面或显示选择器
uni.showToast({
title: '移动功能开发中',
icon: 'none'
});
};
// 删除
const handleDelete = (folder) => {
uni.showModal({
title: '提示',
content: `确定要删除文件夹"${folder.name}"吗?`,
success: (res) => {
if (res.confirm) {
// deleteFolder(folder.id);
console.log('确认删除文件夹');
}
}
});
};
// const handleFolderClick = (id) => {
// console.log(id);
// }
// 菜单
const isMenuOpen = ref(false)
const handleMenu = () => {
isMenuOpen.value = !isMenuOpen.value;
}
// 搜索框
const searchKeyword = ref('')
const isSearchFocus = ref(false)
const handleSearchFocus = () => {
isSearchFocus.value = !isSearchFocus.value
}
const allTemplate = ref([{
"name": "SKILL.md",
"type": "file",
"path": "SKILL.md",
"children": [],
"file_count": 0,
"directory_count": 0,
"size": 4768,
"modified_time": 1776827361.9123552,
"extension": ".md",
"is_favorite": false,
"usage_count": 0,
"sort": 0,
"content": 'khgvshbbthvgbihepiubjhrviifdhiuhrviuehrviuawhiuvrhy'
},
{
"name": "222.md",
"type": "file",
"path": "222.md",
"children": [],
"file_count": 0,
"directory_count": 0,
"size": 5558,
"modified_time": 1776827666.9123552,
"extension": ".md",
"is_favorite": false,
"usage_count": 0,
"sort": 0,
"content": 'khgvshbbthvgbihepiubjhiuvrhy'
},
{
"name": "222.md",
"type": "file",
"path": "222.md",
"children": [],
"file_count": 0,
"directory_count": 0,
"size": 5558,
"modified_time": 1776827666.9123552,
"extension": ".md",
"is_favorite": false,
"usage_count": 0,
"sort": 0,
"content": '该回家看了的法国红酒看来法帝国海军快来尝尝v吧给v复仇计划曝光vi计划日方提供与i哦的风格和健康'
},
{
"name": "222.md",
"type": "file",
"path": "222.md",
"children": [],
"file_count": 0,
"directory_count": 0,
"size": 5558,
"modified_time": 1776827666.9123552,
"extension": ".md",
"is_favorite": false,
"usage_count": 0,
"sort": 0,
"content": '的法国红酒看来法帝国海军快来尝尝v吧给'
},
{
"name": "222.md",
"type": "file",
"path": "222.md",
"children": [],
"file_count": 0,
"directory_count": 0,
"size": 5558,
"modified_time": 1776827666.9123552,
"extension": ".md",
"is_favorite": false,
"usage_count": 0,
"sort": 0,
"content": ''
},
{
"name": "222.md",
"type": "file",
"path": "222.md",
"children": [],
"file_count": 0,
"directory_count": 0,
"size": 5558,
"modified_time": 1776827666.9123552,
"extension": ".md",
"is_favorite": false,
"usage_count": 0,
"sort": 0,
"content": 'khgvshbbthvgbihepiubjhrviifdhiuhrviuehrviuawhiuvrhy'
},
{
"name": "222.md",
"type": "file",
"path": "222.md",
"children": [],
"file_count": 0,
"directory_count": 0,
"size": 5558,
"modified_time": 1776827666.9123552,
"extension": ".md",
"is_favorite": false,
"usage_count": 0,
"sort": 0,
"content": 'khgvshbbthvgbihepiubjhrviifdhiuhrviuehrviuawhiuvrhy'
},
{
"name": "222.md",
"type": "file",
"path": "222.md",
"children": [],
"file_count": 0,
"directory_count": 0,
"size": 5558,
"modified_time": 1776827666.9123552,
"extension": ".md",
"is_favorite": false,
"usage_count": 0,
"sort": 0,
"content": 'khgvshbbthvgbihepiubjhrviifdhiuhrviuehrviuawhiuvrhy'
},
{
"name": "222.md",
"type": "file",
"path": "222.md",
"children": [],
"file_count": 0,
"directory_count": 0,
"size": 5558,
"modified_time": 1776827666.9123552,
"extension": ".md",
"is_favorite": false,
"usage_count": 0,
"sort": 0,
"content": 'khgvshbbthvgbihepiubjhrviifdhiuhrviuehrviuawhiuvrhy'
}
])
// 全选/取消全选
const handleSelectAll = () => {
if (selectFileList.value.length === currentTemplateList.value.length) {
selectFileList.value = [];
} else {
selectFileList.value = currentTemplateList.value.map(f => f.id);
}
};
// 返回上一级
const handleBackOrCheck = () => {
if (isSelectFolder.value) {
// 选择模式下点击全选/取消全选
handleSelectAll();
return;
}
if (!isSelectFolder.value && currentTemplateList.value) {
currentTemplateList.value = []
checkTemplate.value = ''
navbarTitle.value = '模板区'
}
if (!isSelectFolder.value && !currentTemplateList.value) {
uni.navigateBack({
delta: 1,
fail() {
console.log("返回失败,进入兜底跳转")
uni.reLaunch({
url: '/pages/Chat/Chat'
})
}
})
}
};
</script>
<style scoped>
@import url('../WorkSpace.css');
@import url('TemplateSpace.css');
</style>

View File

@@ -0,0 +1,219 @@
.workspace-container {
display: flex;
flex-direction: column;
background: rgba(140, 140, 140, 0.1);
}
.workspace-text {
color: #0073ff;
font-weight: bold;
}
.workspace-header {
width: 100%;
display: flex;
flex-shrink: 0;
flex-direction: column;
align-items: center;
padding: 16rpx 0;
box-sizing: border-box;
border-bottom: 1rpx solid #ddd;
}
.custom-navbar {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 10rpx;
box-sizing: border-box;
position: relative;
}
.navbar-left {
display: flex;
justify-content: center;
align-items: center;
}
.navbar-before-title {
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
.navbar-title {
display: flex;
flex: 1;
justify-content: center;
align-items: center;
position: absolute;
left: 50%;
transform: translateX(-50%);
}
.navbar-right {
display: flex;
justify-content: center;
align-items: center;
margin-right: 20rpx;
box-sizing: border-box;
}
.navbar-right .menu-open {
color: #3ab0ff !important;
font-size: 40rpx !important;
}
.custom-navbar-icon {
color: #0073ff !important;
font-size: 40rpx !important;
}
.search-file-warpper {
display: flex;
width: 90%;
align-items: center;
}
.search-file {
display: flex;
flex: 1;
align-items: center;
background: rgba(255, 255, 255, 1);
padding: 16rpx;
box-sizing: border-box;
border-radius: 30rpx;
}
.icon-sousuo {
font-weight: bold;
}
.search-file-input {
margin-left: 20rpx;
}
.cancel-search {
display: flex;
flex-shrink: 0;
padding: 0 0 0 20rpx;
box-sizing: border-box;
color: #0073ff;
}
.workspace-content {
width: 100%;
display: block;
flex: 1;
overflow: auto;
box-sizing: border-box;
position: relative;
}
/* 菜单卡片 */
.menu-card {
position: absolute;
top: 100%;
right: 20rpx;
width: 300rpx;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
background: #fff;
border-radius: 30rpx;
z-index: 1;
}
.menu-card-item {
width: 100%;
padding: 20rpx;
box-sizing: border-box;
}
.folder-null {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
position: absolute;
left: 50%;
top: 30%;
transform: translateX(-50%);
font-size: 32rpx;
}
.folder-null .iconfont {
font-size: 180rpx;
}
.folder-grid {
width: 100%;
display: grid;
grid-template-columns: repeat(3,1fr);
padding: 20rpx;
box-sizing: border-box;
gap: 20rpx;
align-items: start;
}
.folder-item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
}
.folder-item.select-folder {
border-radius: 30rpx;
background: rgba(9, 9, 9, 0.1);
}
.folder-item-style {
font-size: 160rpx !important;
font-weight: 10rpx !important;
color: #3ab0ff !important;
}
.folder-checkbox {
width: 50rpx;
height: 50rpx;
border: 6rpx solid #fff;
border-radius: 50%;
box-shadow: 0 2rpx 3rpx #333;
background: rgba(255, 255, 255, 0.2);
box-sizing: border-box;
position: absolute;
left: 50%;
top: 30%;
transform: translate(-50%);
display: flex;
justify-content: center;
align-items: center;
}
.folder-checkbox.select-folder {
background-color: #0073ff;
}
.workspace-footer {
width: 100%;
display: flex;
flex-shrink: 0;
justify-content: space-between;
padding: 30rpx;
box-sizing: border-box;
color: #999999;
background: #eeeeee;
}
.workspace-footer.folder-actions {
color: #0073ff;
}
.workspace-footer .iconfont {
font-size: 40rpx !important;
}

View File

@@ -0,0 +1,384 @@
<template>
<view class="status-bar"></view>
<view class="workspace-container page-container">
<view class="workspace-header">
<view class="custom-navbar">
<view class="navbar-left" @click="handleBackOrCheck ">
<view class="iconfont icon-fanhui custom-navbar-icon"></view>
<view class="navbar-before-title workspace-text">{{isSelectFolder?'全选':navbarBeforeTitle}}</view>
</view>
<view class="navbar-title workspace-text">{{navbarTitle}}</view>
<view class="navbar-right">
<view v-if="isSelectFolder" class="navbar-right-text workspace-text" @click="handleSelectFolder()">
完成</view>
<view v-else class="iconfont icon-gengduo" :class="isMenuOpen ?'menu-open':'custom-navbar-icon'"
@click="handleMenu"></view>
</view>
<view v-if="isMenuOpen" class="menu-card">
<view class="menu-card-item" @click="handleSelectFolder();handleMenu()">选择</view>
<view class="solid-line"></view>
<view class="menu-card-item">新建文件夹</view>
</view>
</view>
<view class="search-file-warpper">
<view class="search-file">
<view class="iconfont icon-sousuo"></view>
<input class="search-file-input" placeholder="搜索" @focus="handleSearchFocus"
v-model="searchKeyword" />
</view>
<view v-if="isSearchFocus" class="cancel-search" @click="handleSearchFocus">取消</view>
</view>
</view>
<view class="workspace-content">
<view class="folder-null" v-if="currentFolderList.length===0">
<view class="iconfont icon-wenjianjia"></view>
文件夹为空
</view>
<view class="folder-grid" v-if="currentFolderList.length>0">
<view class="folder-item" :class="{'select-folder':selectFileList.includes(folder.id)}"
v-for="folder in currentFolderList" :key="folder.id"
@click="isSelectFolder?selectFolder(folder.id):handleFolderClick(folder)"
@longpress="handleLongPress(folder)">
<view class="iconfont icon-a-wenjianjiawenjian folder-item-style"></view>
<view class="folder-name">{{folder.name}}</view>
<view v-if="isSelectFolder" class="folder-checkbox"
:class="{'select-folder':selectFileList.includes(folder.id)}">
<uni-icons v-if="selectFileList.includes(folder.id)" type="checkmarkempty"
color="#fff"></uni-icons>
</view>
</view>
</view>
</view>
<view v-if="isSelectFolder" class="workspace-footer" :class="{'folder-actions':selectFileList.length>0}">
<view class="iconfont icon-shangchuan"></view>
<view class="iconfont icon-fuzhi"></view>
<view class="iconfont icon-wenjian"></view>
<view class="iconfont icon-del"></view>
<view class="iconfont icon-gengduo"></view>
</view>
</view>
</template>
<script setup>
import {
onMounted,
ref
} from 'vue';
import {getWorkspaceId} from '@/utils/user-info.js'
import {getWorkspaceList} from '@/utils/cloud-api.js'
const workspaceId = ref('')
const navbarBeforeTitle = ref('');
const navbarTitle = ref('工作区');
// 选择文件模式
const isSelectFolder = ref(false);
const selectFileList = ref([]);
const handleSelectFolder = () => {
if (isSelectFolder.value) {
selectFileList.value = []
}
isSelectFolder.value = !isSelectFolder.value;
}
const selectFolder = (id) => {
const index = selectFileList.value.indexOf(id);
if (index !== -1) {
selectFileList.value.splice(index, 1);
} else {
selectFileList.value.push(id);
}
}
const handleLongPress = (folder) => {
if (isSelectFolder.value) return;
uni.showActionSheet({
itemList:['重命名','删除','下载','压缩'],
success: (res) => {
switch(res.tapIndex) {
case 0:
handleRename(folder);
break;
case 1:
handleDelete(folder);
break;
case 2:
handleDownload(folder);
break;
case 3:
handleDownload(folder);
break;
}
}
})
}
// 重命名
const handleRename = (folder) => {
uni.showModal({
title: '重命名',
content: '请输入新名称',
editable: true,
placeholderText: folder.name,
success: (res) => {
if (res.confirm && res.content) {
// 更新文件夹名称
updateFolderName(folder.id, res.content);
}
}
});
};
// 移动
const handleDownload = (folder) => {
// 可以选择跳转到移动页面或显示选择器
uni.showToast({
title: '移动功能开发中',
icon: 'none'
});
};
// 删除
const handleDelete = (folder) => {
uni.showModal({
title: '提示',
content: `确定要删除文件夹"${folder.name}"吗?`,
success: (res) => {
if (res.confirm) {
// deleteFolder(folder.id);
console.log('确认删除文件夹');
}
}
});
};
// const handleFolderClick = (id) => {
// console.log(id);
// }
// 菜单
const isMenuOpen = ref(false)
const handleMenu = () => {
isMenuOpen.value = !isMenuOpen.value;
}
// 搜索框
const searchKeyword = ref('')
const isSearchFocus = ref(false)
const handleSearchFocus = () => {
isSearchFocus.value = !isSearchFocus.value
}
const allFoldersData = ref({
// 根目录下的文件夹
'root': [{
id: 1,
name: '文档资料',
parentId: 'root',
children: [{
id: 11,
name: '工作文档',
parentId: 1,
children: []
},
{
id: 12,
name: '学习笔记',
parentId: 1,
children: []
},
{
id: 13,
name: '合同模板',
parentId: 1,
children: []
}
]
},
{
id: 2,
name: '图片素材',
parentId: 'root',
children: [{
id: 21,
name: '风景图片',
parentId: 2,
children: []
},
{
id: 22,
name: '人物照片',
parentId: 2,
children: []
},
{
id: 23,
name: 'UI图标',
parentId: 2,
children: []
}
]
},
{
id: 3,
name: '视频文件',
parentId: 'root',
children: [{
id: 31,
name: '教程视频',
parentId: 3,
children: []
},
{
id: 32,
name: '会议录像',
parentId: 3,
children: []
}
]
},
{
id: 4,
name: '项目代码',
parentId: 'root',
children: [{
id: 41,
name: '前端项目',
parentId: 4,
children: []
},
{
id: 42,
name: '后端服务',
parentId: 4,
children: []
}
]
},
{
id: 5,
name: '安装包',
parentId: 'root',
children: []
},
{
id: 6,
name: '备份文件',
parentId: 'root',
children: []
},
{
id: 7,
name: '临时文件',
parentId: 'root',
children: []
},
{
id: 8,
name: '个人收藏',
parentId: 'root',
children: []
}
]
});
// 文件夹路径栈
const folderStack = ref([]);
// 当前显示的文件夹列表
const currentFolderList = ref([]);
const getCurrentFolderList = () => {
const currentPath = folderStack.value.length > 0 ? folderStack.value[folderStack.value.length - 1].id : 'root';
if (currentPath === "root") {
return allFoldersData.value['root'] || [];
}
const findFolderById = (folders, id) => {
for (const folder of folders) {
if (folder.id === id) return folder;
if (folder.children && folder.children.length > 0) {
const found = findFolderById(folder.children, id);
if (found) return found;
}
}
return null;
};
const currentFolder = findFolderById(allFoldersData.value['root'], currentPath);
return currentFolder ? currentFolder.children : [];
}
// 初始化当前文件夹列表
const initCurrentFolderList = () => {
workspaceId.value = getWorkspaceId();
console.log("getWorkspaceId:",workspaceId.value);
currentFolderList.value = getCurrentFolderList();
};
// 处理文件夹点击(非选择模式下进入文件夹)
const handleFolderClick = (folder) => {
folderStack.value.push({
id: folder.id,
name: folder.name
});
navbarTitle.value = folder.name;
navbarBeforeTitle.value = folderStack.value.length > 1 ? folderStack.value[folderStack.value.length - 2].name :
'工作区';
initCurrentFolderList();
searchKeyword.value = '';
isSearchFocus.value = false;
};
// 全选/取消全选
const handleSelectAll = () => {
if (selectFileList.value.length === currentFolderList.value.length) {
selectFileList.value = [];
} else {
selectFileList.value = currentFolderList.value.map(f => f.id);
}
};
// 返回上一级
const handleBackOrCheck = () => {
if (isSelectFolder.value) {
// 选择模式下点击全选/取消全选
handleSelectAll();
return;
}
if (folderStack.value.length > 0) {
folderStack.value.pop();
if (folderStack.value.length === 0) {
navbarTitle.value = '工作区';
navbarBeforeTitle.value = null;
} else {
navbarTitle.value = folderStack.value[folderStack.value.length - 1].name;
navbarBeforeTitle.value = folderStack.value.length > 1 ?
folderStack.value[folderStack.value.length - 2].name :
'工作区';
}
initCurrentFolderList();
// 返回后清空搜索
searchKeyword.value = '';
isSearchFocus.value = false;
} else {
// 已在根目录,返回到聊天界面
uni.navigateBack({
delta: 1,
fail() {
console.log("返回失败,进入兜底跳转")
uni.reLaunch({
url: '/pages/Chat/Chat'
})
}
})
}
};
onMounted(() => {
initCurrentFolderList();
})
</script>
<style scoped>
@import url("WorkSpace.css");
</style>

160
pages/text/text.vue Normal file
View File

@@ -0,0 +1,160 @@
<template>
<view>
<view v-html="sanitizeContent(formHtml)"></view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const formHtml = ref('')
const sanitizeContent = (str) => {
return str.replace(/<\/?script>/gi, (match) => {
// 将匹配到的标签转换为十六进制 Unicode
let result = '';
for (let i = 0; i < match.length; i++) {
result += '\\u' + match.charCodeAt(i).toString(16).padStart(4, '0');
}
return result;
});
}
onMounted(() => {
// 直接设置 HTML
formHtml.value = `
明白了!您希望在我的回复中直接嵌入可编辑的表单卡片。让我试试:
---
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 20px; padding: 28px; color: white; font-family: 'Microsoft YaHei', sans-serif; box-shadow: 0 10px 40px rgba(0,0,0,0.25); margin: 20px 0;">
<h2 style="margin: 0 0 20px 0; font-size: 20px; display: flex; align-items: center; gap: 12px;">
<span style="font-size: 26px;">✏️</span> 直接在下方编辑任务
</h2>
<form id="inlineTaskForm" style="background: rgba(255,255,255,0.1); border-radius: 16px; padding: 24px;">
<div style="margin-bottom: 18px;">
<label style="display: block; font-size: 12px; opacity: 0.8; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 1px;">任务名称</label>
<input type="text" id="inlineTitle" placeholder="输入任务名称..." style="width: 100%; padding: 14px 16px; background: #1e1e2e; border: 2px solid rgba(255,255,255,0.2); border-radius: 10px; color: #fff; font-size: 15px;">
</div>
<div style="margin-bottom: 18px;">
<label style="display: block; font-size: 12px; opacity: 0.8; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 1px;">任务描述</label>
<textarea id="inlineDesc" placeholder="输入详细描述..." style="width: 100%; padding: 14px 16px; background: #1e1e2e; border: 2px solid rgba(255,255,255,0.2); border-radius: 10px; color: #fff; font-size: 14px; resize: vertical; min-height: 60px;"></textarea>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 18px;">
<div>
<label style="display: block; font-size: 12px; opacity: 0.8; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 1px;">开始日期</label>
<input type="date" id="inlineStart" style="width: 100%; padding: 14px 16px; background: #1e1e2e; border: 2px solid rgba(255,255,255,0.2); border-radius: 10px; color: #fff; font-size: 15px;">
</div>
<div>
<label style="display: block; font-size: 12px; opacity: 0.8; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 1px;">截止日期</label>
<input type="date" id="inlineEnd" style="width: 100%; padding: 14px 16px; background: #1e1e2e; border: 2px solid rgba(255,255,255,0.2); border-radius: 10px; color: #fff; font-size: 15px;">
</div>
</div>
<div style="margin-bottom: 18px;">
<label style="display: block; font-size: 12px; opacity: 0.8; margin-bottom: 8px; text-transform: uppercase; letter-spacing: 1px;">时间</label>
<input type="time" id="inlineTime" value="09:00" style="width: 100%; padding: 14px 16px; background: #1e1e2e; border: 2px solid rgba(255,255,255,0.2); border-radius: 10px; color: #fff; font-size: 15px;">
</div>
<div style="margin-bottom: 18px;">
<label style="display: block; font-size: 12px; opacity: 0.8; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 1px;">优先级</label>
<div style="display: flex; gap: 10px;">
<label style="flex: 1; text-align: center; padding: 12px; background: rgba(254, 215, 215, 0.2); border: 2px solid transparent; border-radius: 10px; cursor: pointer; transition: all 0.3s;" onclick="selectPriority(this, 'high')">
🔴 高
<input type="radio" name="priority" value="high" checked style="display: none;">
</label>
<label style="flex: 1; text-align: center; padding: 12px; background: rgba(254, 235, 200, 0.2); border: 2px solid transparent; border-radius: 10px; cursor: pointer; transition: all 0.3s;" onclick="selectPriority(this, 'medium')">
🟡 中
<input type="radio" name="priority" value="medium" style="display: none;">
</label>
<label style="flex: 1; text-align: center; padding: 12px; background: rgba(198, 246, 213, 0.2); border: 2px solid transparent; border-radius: 10px; cursor: pointer; transition: all 0.3s;" onclick="selectPriority(this, 'low')">
🟢 低
<input type="radio" name="priority" value="low" style="display: none;">
</label>
</div>
</div>
<div style="margin-bottom: 20px;">
<label style="display: block; font-size: 12px; opacity: 0.8; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 1px;">分类</label>
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<label style="padding: 10px 16px; background: rgba(233, 216, 253, 0.2); border: 2px solid #667eea; border-radius: 20px; cursor: pointer; font-size: 13px;" onclick="selectCategory(this, 'work')">
💼 工作
<input type="radio" name="category" value="work" checked style="display: none;">
</label>
<label style="padding: 10px 16px; background: rgba(190, 227, 248, 0.2); border: 2px solid transparent; border-radius: 20px; cursor: pointer; font-size: 13px;" onclick="selectCategory(this, 'life')">
🏠 生活
<input type="radio" name="category" value="life" style="display: none;">
</label>
<label style="padding: 10px 16px; background: rgba(254, 215, 226, 0.2); border: 2px solid transparent; border-radius: 20px; cursor: pointer; font-size: 13px;" onclick="selectCategory(this, 'study')">
📚 学习
<input type="radio" name="category" value="study" style="display: none;">
</label>
<label style="padding: 10px 16px; background: rgba(198, 246, 213, 0.2); border: 2px solid transparent; border-radius: 20px; cursor: pointer; font-size: 13px;" onclick="selectCategory(this, 'health')">
💪 健康
<input type="radio" name="category" value="health" style="display: none;">
</label>
</div>
</div>
<div style="display: flex; gap: 12px;">
<button type="button" onclick="clearInlineForm()" style="flex: 1; padding: 14px; background: rgba(255,255,255,0.2); border: none; border-radius: 12px; color: white; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.3s;">
清空
</button>
<button type="button" onclick="submitInlineForm()" style="flex: 2; padding: 14px; background: white; border: none; border-radius: 12px; color: #667eea; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.3s;">
✅ 添加任务
</button>
</div>
</form>
</div>
---
**✅ 您现在可以直接在上方的卡片中填写任务信息,点击"添加任务"即可!**
填写完成后告诉我"已填好"或"添加",我会帮您确认是否成功!
`
// 初始化事件
// initForm()
})
const initForm = () => {
setTimeout(() => {
const today = new Date().toISOString().split('T')[0]
const startInput = document.getElementById('inlineStart')
const endInput = document.getElementById('inlineEnd')
if (startInput) startInput.value = today
if (endInput) endInput.value = today
const clearBtn = document.getElementById('clearBtn')
const submitBtn = document.getElementById('submitBtn')
if (clearBtn) {
clearBtn.onclick = () => {
const titleInput = document.getElementById('inlineTitle')
const descInput = document.getElementById('inlineDesc')
if (titleInput) titleInput.value = ''
if (descInput) descInput.value = ''
}
}
if (submitBtn) {
submitBtn.onclick = () => {
const titleInput = document.getElementById('inlineTitle')
const title = titleInput?.value.trim()
if (!title) {
alert('请输入任务名称')
return
}
alert(`任务已添加:${title}`)
if (titleInput) titleInput.value = ''
if (document.getElementById('inlineDesc')) document.getElementById('inlineDesc').value = ''
}
}
}, 100)
}
</script>

538
pages/text/text2.vue Normal file
View File

@@ -0,0 +1,538 @@
<template>
<view class="container">
<!-- 连接状态卡片 -->
<view class="status-card">
<text class="status-label">连接状态</text>
<view class="status-value" :class="connectionStatusClass">
<text>{{ socketStore.getStatusText() }}</text>
</view>
<view class="status-info" v-if="socketStore.reconnectAttempts > 0">
<text class="reconnect-info">重连次数: {{ socketStore.reconnectAttempts }}</text>
</view>
</view>
<!-- 配置区域 -->
<view class="config-card">
<text class="section-title">连接配置</text>
<view class="input-group">
<text class="input-label">Token</text>
<input class="input-field" v-model="config.token" placeholder="请输入Token" />
</view>
<view class="input-group">
<text class="input-label">Conversation ID</text>
<input class="input-field" v-model="config.conversationId" placeholder="请输入会话ID(可选)" />
</view>
</view>
<!-- 连接控制 -->
<view class="control-card">
<button class="btn btn-primary" :disabled="socketStore.isConnected || socketStore.isConnecting"
@click="handleConnect">
{{ socketStore.isConnecting ? '连接中...' : '连接' }}
</button>
<button class="btn btn-danger" :disabled="!socketStore.isConnected" @click="handleDisconnect">
断开
</button>
<button class="btn btn-secondary" @click="handleSendPing" :disabled="!socketStore.isConnected">
Ping
</button>
</view>
<!-- 发送消息区域 -->
<view class="send-card">
<text class="section-title">发送消息</text>
<view class="send-input-wrapper">
<textarea class="send-input" v-model="sendMessage" placeholder="输入要发送的消息(JSON格式)"
:disabled="!socketStore.isConnected"></textarea>
<view class="send-buttons">
<button class="btn btn-primary btn-small"
:disabled="!socketStore.isConnected || !sendMessage.trim()" @click="handleSend">
发送
</button>
<button class="btn btn-outline btn-small" :disabled="!sendMessage.trim()" @click="formatJson">
格式化
</button>
</view>
</view>
<!-- 快捷消息 -->
<view class="quick-messages">
<text class="quick-label">快捷消息:</text>
<view class="quick-btns">
<button v-for="item in quickMessages" :key="item.label" class="quick-btn"
:disabled="!socketStore.isConnected" @click="sendQuickMessage(item.data)">
{{ item.label }}
</button>
</view>
</view>
</view>
<!-- 日志区域 -->
<view class="log-card">
<view class="log-header">
<view class="log-title-wrapper">
<text class="section-title">日志</text>
<text class="log-count">({{ socketStore.logs.length }})</text>
</view>
<view class="log-actions">
<button class="btn btn-small btn-secondary" @click="socketStore.clearLogs">清空</button>
</view>
</view>
<scroll-view class="log-list" scroll-y @scrolltoupper="loadMoreLogs">
<view v-for="(log, index) in socketStore.logs" :key="index" class="log-item" :class="'log-' + log.type">
<text class="log-time">{{ log.time }}</text>
<text class="log-content">{{ log.content }}</text>
</view>
</scroll-view>
</view>
</view>
</template>
<script setup>
import {
ref,
computed,
onMounted,
onUnmounted,
watch
} from 'vue'
import {
useSocketStore
} from '@/stores/socket.js'
import {
getToken,
getTaskCallId,
getCurrentSessionId
} from '@/utils/user-info.js'
// Store
const socketStore = useSocketStore()
// 响应式数据
const config = ref({
token: getToken(),
conversationId: getCurrentSessionId()
})
const sendMessage = ref('')
const quickMessages = ref([{
label: 'Ping',
data: {
ws_event: 'ping'
}
},
{
label: '认证',
data: {
ws_event: 'auth',
data: {
token: '',
conversation_id: ''
}
}
},
{
label: '测试消息',
data: {
ws_event: 'message',
data: {
task_call_id: getTaskCallId(),
token: getToken(),
conversation_id: getCurrentSessionId()
}
}
}
])
// 计算属性
const connectionStatusClass = computed(() => {
const status = socketStore.connectionStatus
return {
'status-connected': status === 'connected',
'status-connecting': status === 'connecting',
'status-error': status === 'error',
'status-disconnected': status === 'disconnected'
}
})
// 方法
const handleConnect = () => {
if (!config.value.token) {
uni.showToast({
title: '请输入Token',
icon: 'none'
})
return
}
socketStore.connect({
token: config.value.token,
conversationId: getCurrentSessionId()
})
}
const handleDisconnect = () => {
socketStore.disconnect()
}
const handleSendPing = async () => {
try {
await socketStore.sendPing()
} catch (e) {
uni.showToast({
title: '发送失败',
icon: 'none'
})
}
}
const handleSend = async () => {
if (!sendMessage.value.trim()) return
try {
const messageData = {
ws_event: 'message',
data: {
task_call_id: getTaskCallId(),
result: {
tools: []
}
}
}
await socketStore.send(messageData)
sendMessage.value = ''
} catch (e) {
uni.showToast({
title: '发送失败',
icon: 'none'
})
}
}
const sendQuickMessage = async (template) => {
// console.log('getCurrentSessionId:', getCurrentSessionId())
const messageData = {
ws_event: 'message',
data: {
task_call_id: getTaskCallId(),
token: getToken(),
conversation_id: getCurrentSessionId()
}
}
try {
await socketStore.send(messageData)
} catch (e) {
uni.showToast({
title: '发送失败',
icon: 'none'
})
}
}
const formatJson = () => {
if (!sendMessage.value.trim()) return
try {
const obj = JSON.parse(sendMessage.value)
sendMessage.value = JSON.stringify(obj, null, 2)
} catch {
uni.showToast({
title: '不是有效的JSON',
icon: 'none'
})
}
}
const loadMoreLogs = () => {
// 预留扩展
}
watch(() => socketStore.isDisconnected, (newVal) => {
if (newVal && socketStore.messageString) {
console.log(socketStore.messageString);
}
})
// 生命周期
onMounted(() => {
socketStore.addLog('info', '=== Socket测试页面 ===')
socketStore.addLog('info', '页面已加载')
if (socketStore.isConnected) {
socketStore.addLog('info', '当前已处于连接状态')
}
})
onUnmounted(() => {
socketStore.addLog('info', '页面卸载')
// 注意这里不主动断开连接由App级别管理
})
</script>
<style scoped>
.container {
padding: 20rpx;
background-color: #f5f5f5;
min-height: 100vh;
}
.status-card,
.config-card,
.control-card,
.send-card,
.log-card {
background-color: #fff;
border-radius: 16rpx;
padding: 30rpx;
margin-bottom: 20rpx;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
}
.status-label {
font-size: 28rpx;
color: #666;
margin-bottom: 16rpx;
}
.status-value {
display: inline-block;
padding: 12rpx 32rpx;
border-radius: 8rpx;
font-size: 28rpx;
font-weight: 500;
}
.status-connected {
background-color: #e6f7e6;
color: #52c41a;
}
.status-connecting {
background-color: #fff7e6;
color: #faad14;
}
.status-disconnected {
background-color: #f5f5f5;
color: #999;
}
.status-error {
background-color: #fff1f0;
color: #ff4d4f;
}
.status-info {
margin-top: 16rpx;
}
.reconnect-info {
font-size: 24rpx;
color: #faad14;
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 24rpx;
display: block;
}
.input-group {
margin-bottom: 24rpx;
}
.input-label {
font-size: 28rpx;
color: #666;
margin-bottom: 12rpx;
display: block;
}
.input-field {
border: 2rpx solid #e8e8e8;
border-radius: 8rpx;
padding: 20rpx;
font-size: 28rpx;
background-color: #fafafa;
}
.control-card {
display: flex;
gap: 20rpx;
}
.btn {
flex: 1;
height: 88rpx;
line-height: 88rpx;
border-radius: 12rpx;
font-size: 32rpx;
text-align: center;
border: none;
}
.btn-primary {
background-color: #1890ff;
color: #fff;
}
.btn-danger {
background-color: #ff4d4f;
color: #fff;
}
.btn-secondary {
background-color: #f0f0f0;
color: #666;
}
.btn-outline {
background-color: transparent;
color: #1890ff;
border: 2rpx solid #1890ff;
}
.btn-small {
height: 64rpx;
line-height: 64rpx;
font-size: 26rpx;
padding: 0 24rpx;
}
.btn[disabled] {
opacity: 0.5;
}
.send-input-wrapper {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.send-input {
border: 2rpx solid #e8e8e8;
border-radius: 8rpx;
padding: 20rpx;
font-size: 28rpx;
min-height: 160rpx;
background-color: #fafafa;
box-sizing: border-box;
}
.send-buttons {
display: flex;
gap: 20rpx;
}
.quick-messages {
margin-top: 24rpx;
padding-top: 24rpx;
border-top: 2rpx solid #f0f0f0;
}
.quick-label {
font-size: 26rpx;
color: #999;
margin-bottom: 16rpx;
display: block;
}
.quick-btns {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.quick-btn {
padding: 12rpx 24rpx;
background-color: #f0f5ff;
color: #1890ff;
border-radius: 8rpx;
font-size: 26rpx;
border: none;
}
.quick-btn[disabled] {
opacity: 0.5;
}
.log-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
}
.log-title-wrapper {
display: flex;
align-items: center;
gap: 12rpx;
}
.log-title-wrapper .section-title {
margin-bottom: 0;
}
.log-count {
font-size: 24rpx;
color: #999;
}
.log-actions {
display: flex;
gap: 12rpx;
}
.log-list {
max-height: 500rpx;
background-color: #1e1e1e;
border-radius: 8rpx;
padding: 20rpx;
}
.log-item {
display: flex;
margin-bottom: 12rpx;
font-family: 'Courier New', monospace;
font-size: 24rpx;
line-height: 1.6;
}
.log-time {
color: #888;
margin-right: 16rpx;
flex-shrink: 0;
}
.log-content {
word-break: break-all;
flex: 1;
color: #fff;
}
.log-info .log-content {
color: #fff;
}
.log-success .log-content {
color: #52c41a;
}
.log-error .log-content {
color: #ff4d4f;
}
.log-warn .log-content {
color: #faad14;
}
.log-send .log-content {
color: #1890ff;
}
.log-receive .log-content {
color: #52c41a;
}
</style>