chore: 重写初始提交(清空历史,整理后全量提交)
@@ -0,0 +1,62 @@
|
||||
<script setup lang="uts">
|
||||
import userState, { initStore } from "@/stores/user.uts"
|
||||
import appState, { initApp } from "@/stores/app.uts"
|
||||
import messageService from "@/services/websocket/message-service.uts"
|
||||
// #ifdef APP-ANDROID
|
||||
UTSAndroid.setPrivacyAgree(true)
|
||||
// #endif
|
||||
// #ifdef APP-ANDROID || APP-HARMONY
|
||||
let firstBackTime = 0
|
||||
// #endif
|
||||
const instance = getCurrentInstance()!.proxy!
|
||||
|
||||
onLaunch((options) => {
|
||||
console.log('App onLaunch')
|
||||
initStore();
|
||||
// 注册 WebSocket 消息监听(即使未登录也先就绪)
|
||||
messageService.init()
|
||||
(async () => {
|
||||
try {
|
||||
await initApp()
|
||||
// 其他异步初始化操作
|
||||
} catch (e) {
|
||||
console.error('初始化失败', e)
|
||||
}
|
||||
})()
|
||||
|
||||
})
|
||||
onShow(() => {
|
||||
console.log('App onShow')
|
||||
const res = uni.getSystemInfoSync()
|
||||
console.log(res)
|
||||
})
|
||||
onHide(() => {
|
||||
console.log('App onHide')
|
||||
})
|
||||
// #ifdef APP-ANDROID || APP-HARMONY
|
||||
onLastPageBackPress(() => {
|
||||
console.log('App LastPageBackPress')
|
||||
if (firstBackTime == 0) {
|
||||
uni.showToast({
|
||||
title: '再按一次退出应用',
|
||||
position: 'bottom',
|
||||
})
|
||||
firstBackTime = Date.now()
|
||||
setTimeout(() => {
|
||||
firstBackTime = 0
|
||||
}, 2000)
|
||||
} else if (Date.now() - firstBackTime < 2000) {
|
||||
firstBackTime = Date.now()
|
||||
uni.exit()
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
onExit(() => {
|
||||
console.log('App Exit')
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/*每个页面公共css */
|
||||
@import url("@/static/common.scss");
|
||||
</style>
|
||||
@@ -0,0 +1,234 @@
|
||||
// api-service.uts
|
||||
// 缘友(wxchat)插件 前端 API 服务层
|
||||
// 统一对接 addon/wxchat 后端接口。
|
||||
//
|
||||
// 约定:
|
||||
// - 基础路径前缀:/wxchat (完整 URL 形如 /wxchat/<controller>/<action>)
|
||||
// - 真实网络客户端:@/utlis/http-api.uts 的 httpApi(已内置 token 注入与无感刷新)
|
||||
// - 后端统一响应结构:{ code, message, data, timestamp }
|
||||
// - 成功判定:code == 0(success())或 code == 200(部分自定义接口),其余视为业务失败
|
||||
//
|
||||
// 使用前请确保 utlis/http-api.uts 中的 baseURL 指向正确的后端域名。
|
||||
|
||||
import httpApi, { type IResponse } from '@/utlis/http-api.uts'
|
||||
import { setUserToken, setUserInfo, clearToken } from '@/stores/user.uts'
|
||||
import { IUserInfo } from '@/types/user.uts'
|
||||
|
||||
// ===================== 业务异常 =====================
|
||||
class ApiError extends Error {
|
||||
public code : number
|
||||
constructor(message : string, code : number) {
|
||||
super(message)
|
||||
this.name = 'ApiError'
|
||||
this.code = code
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 基础请求封装 =====================
|
||||
// 统一处理成功/失败:成功返回 data(UTSJSONObject),失败抛出 ApiError
|
||||
function handleResponse(res : IResponse) : UTSJSONObject {
|
||||
const code = res.code ?? 1
|
||||
if (code == 0 || code == 200) {
|
||||
return res.data as UTSJSONObject
|
||||
}
|
||||
throw new ApiError(res.message ?? '请求失败', code)
|
||||
}
|
||||
|
||||
async function getJSON(url : string, data : any = {}, needToken : boolean = false) : Promise<UTSJSONObject> {
|
||||
const res = await httpApi.get(url, data, { url: '', needToken: needToken })
|
||||
return handleResponse(res)
|
||||
}
|
||||
|
||||
async function postJSON(url : string, data : any = {}, needToken : boolean = false) : Promise<UTSJSONObject> {
|
||||
const res = await httpApi.post(url, data, { url: '', needToken: needToken })
|
||||
return handleResponse(res)
|
||||
}
|
||||
|
||||
// 把任意对象安全序列化为目标类型(兼容 APP / 非APP 平台的 JSON 解析差异)
|
||||
function toObject<T>(source : any) : T | null {
|
||||
if (source == null) return null
|
||||
// #ifndef APP
|
||||
const str = JSON.stringify(source)
|
||||
const obj = JSON.parse(str) as T
|
||||
return obj
|
||||
// #endif
|
||||
// #ifdef APP
|
||||
return source as T
|
||||
// #endif
|
||||
}
|
||||
|
||||
// ===================== 1. 认证模块(登录/注册) =====================
|
||||
const auth = {
|
||||
// 1.1 发送短信验证码(登录/注册场景,event: login|register)
|
||||
sendSmsCode(phone : string, event : string = 'login') : Promise<UTSJSONObject> {
|
||||
return postJSON('/wxchat/api/login/sendSmsCode', { phone: phone, event: event }, false)
|
||||
},
|
||||
|
||||
// 1.2 短信验证码登录
|
||||
// 成功后后端返回用户信息(code=0),此处持久化用户信息
|
||||
async smsLogin(mobile : string, captcha : string, event : string = 'login') : Promise<IUserInfo | null> {
|
||||
const data = await postJSON('/wxchat/api/login/smsLogin', { mobile: mobile, captcha: captcha, event: event }, false)
|
||||
// 后端在响应顶层附带 access/refresh token,拦截器已自动持久化;这里仅解析 user_info
|
||||
const info = toObject<IUserInfo>(data.get('user_info'))
|
||||
if (info != null) {
|
||||
setUserInfo(info)
|
||||
}
|
||||
return info
|
||||
},
|
||||
|
||||
// 1.3 本机号码一键登录:携带预凭证 pre_token 换取登录态(token 由拦截器自动持久化)
|
||||
async oneClickLogin(preToken : string) : Promise<UTSJSONObject> {
|
||||
const data = await postJSON('/wxchat/api/login/oneClickLogin', { pre_token: preToken }, false)
|
||||
const info = toObject<IUserInfo>(data.get('user_info'))
|
||||
if (info != null) {
|
||||
setUserInfo(info)
|
||||
}
|
||||
return data
|
||||
},
|
||||
|
||||
// 1.3.1 获取一键登录预凭证(模拟运营商 SDK 下发的本机号码凭证)
|
||||
getOneClickPreToken(phone : string) : Promise<UTSJSONObject> {
|
||||
return postJSON('/wxchat/api/login/getOneClickPreToken', { phone: phone }, false)
|
||||
},
|
||||
|
||||
// 1.4 完善用户信息(新用户首次登录后补全资料)
|
||||
completeUserInfo(profile : any) : Promise<UTSJSONObject> {
|
||||
return postJSON('/wxchat/api/login/completeUserInfo', profile, true)
|
||||
},
|
||||
|
||||
// 1.5 退出登录(清理本地 token / 用户信息)
|
||||
logout() : void {
|
||||
clearToken()
|
||||
}
|
||||
}
|
||||
|
||||
// ===================== 2. 用户模块 =====================
|
||||
const user = {
|
||||
profile() : Promise<UTSJSONObject> { return getJSON('/wxchat/api/user/profile', {}, true) },
|
||||
// 查看他人公开资料(资料卡页)
|
||||
detail(uid : number) : Promise<UTSJSONObject> { return getJSON('/wxchat/api/user/detail', { uid: uid }, true) },
|
||||
updateProfile(profile : any) : Promise<UTSJSONObject> { return postJSON('/wxchat/api/user/updateProfile', profile, true) },
|
||||
nearby(params : any = {}) : Promise<UTSJSONObject> { return getJSON('/wxchat/api/user/nearby', params, true) },
|
||||
local() : Promise<UTSJSONObject> { return getJSON('/wxchat/api/user/local', {}, true) },
|
||||
online() : Promise<UTSJSONObject> { return postJSON('/wxchat/api/user/online', {}, true) },
|
||||
heartbeat() : Promise<UTSJSONObject> { return postJSON('/wxchat/api/user/heartbeat', {}, true) },
|
||||
privacy() : Promise<UTSJSONObject> { return getJSON('/wxchat/api/user/privacy', {}, true) },
|
||||
updatePrivacy(privacy : any) : Promise<UTSJSONObject> { return postJSON('/wxchat/api/user/updatePrivacy', privacy, true) },
|
||||
cancelAccount() : Promise<UTSJSONObject> { return postJSON('/wxchat/api/user/cancelAccount', {}, true) },
|
||||
ranking(params : any = {}) : Promise<UTSJSONObject> { return getJSON('/wxchat/api/user/ranking', params, true) }
|
||||
// 用户概要(首页/我的 初始化使用;后端若未实现该 action 会走失败,调用方需自行兜底)
|
||||
summary() : Promise<UTSJSONObject> { return getJSON('/wxchat/api/user/summary', {}, true) },
|
||||
// 我的钱包(余额/金币/汇总/明细)
|
||||
wallet() : Promise<UTSJSONObject> { return getJSON('/wxchat/api/user/wallet', {}, true) }
|
||||
}
|
||||
|
||||
// ===================== 3. 关注模块 =====================
|
||||
const follow = {
|
||||
// 切换关注状态(关注/取消)。
|
||||
// 后端 Follow::toggleFollow 通过 uid(当前用户) + target_uid(目标) + action(1关注/0取消) 判定,
|
||||
// 注意它从请求参数读取 uid,而非登录态,调用方需传入当前用户 uid。
|
||||
toggle(uid : number, targetUid : number, action : number = 1) : Promise<UTSJSONObject> {
|
||||
return postJSON('/wxchat/api/follow/toggleFollow', { uid: uid, target_uid: targetUid, action: action }, true)
|
||||
},
|
||||
followingList(params : any = {}) : Promise<UTSJSONObject> { return getJSON('/wxchat/api/follow/getFollowingList', params, true) },
|
||||
followerList(params : any = {}) : Promise<UTSJSONObject> { return getJSON('/wxchat/api/follow/getFollowerList', params, true) },
|
||||
mutualList(params : any = {}) : Promise<UTSJSONObject> { return getJSON('/wxchat/api/follow/getMutualList', params, true) }
|
||||
}
|
||||
|
||||
// ===================== 4. 好友模块 =====================
|
||||
const friend = {
|
||||
list(params : any = {}) : Promise<UTSJSONObject> { return getJSON('/wxchat/api/friend', params, true) },
|
||||
requests(params : any = {}) : Promise<UTSJSONObject> { return getJSON('/wxchat/api/friend/requests', params, true) },
|
||||
create(data : any) : Promise<UTSJSONObject> { return postJSON('/wxchat/api/friend/create', data, true) },
|
||||
detail(id : number) : Promise<UTSJSONObject> { return getJSON('/wxchat/api/friend/read', { id: id }, true) },
|
||||
respond(id : number, action : number = 1) : Promise<UTSJSONObject> { return postJSON('/wxchat/api/friend/save', { id: id, action: action }, true) },
|
||||
update(id : number, data : any) : Promise<UTSJSONObject> { return postJSON('/wxchat/api/friend/update', { id: id, ...data }, true) },
|
||||
delete(id : number) : Promise<UTSJSONObject> { return postJSON('/wxchat/api/friend/delete', { id: id }, true) }
|
||||
}
|
||||
|
||||
// ===================== 5. 动态/朋友圈模块 =====================
|
||||
const moment = {
|
||||
// 动态广场列表:scope = recommend|follow|nearby|hot
|
||||
list(params : any = {}) : Promise<UTSJSONObject> { return getJSON('/wxchat/api/moment/index', params, true) },
|
||||
detail(id : number) : Promise<UTSJSONObject> { return getJSON('/wxchat/api/moment/red', { id: id }, true) },
|
||||
create(data : any) : Promise<UTSJSONObject> { return postJSON('/wxchat/api/moment/create', data, true) },
|
||||
comment(data : any) : Promise<UTSJSONObject> { return postJSON('/wxchat/api/moment/comment', data, true) },
|
||||
like(data : any) : Promise<UTSJSONObject> { return postJSON('/wxchat/api/moment/like', data, true) },
|
||||
collect(data : any) : Promise<UTSJSONObject> { return postJSON('/wxchat/api/moment/collect', data, true) },
|
||||
follow(data : any) : Promise<UTSJSONObject> { return postJSON('/wxchat/api/moment/follow', data, true) },
|
||||
collectedList(params : any = {}) : Promise<UTSJSONObject> { return getJSON('/wxchat/api/moment/collectedList', params, true) },
|
||||
historyList(params : any = {}) : Promise<UTSJSONObject> { return getJSON('/wxchat/api/moment/historyList', params, true) },
|
||||
mine(params : any = {}) : Promise<UTSJSONObject> { return getJSON('/wxchat/api/moment/mine', params, true) }
|
||||
upload(data : any) : Promise<UTSJSONObject> { return postJSON('/wxchat/api/moment/upload', data, true) }
|
||||
}
|
||||
|
||||
// ===================== 6. 消息模块 =====================
|
||||
const message = {
|
||||
send(data : any) : Promise<UTSJSONObject> { return postJSON('/wxchat/api/message/send', data, true) },
|
||||
history(params : any = {}) : Promise<UTSJSONObject> { return getJSON('/wxchat/api/message/history', params, true) },
|
||||
// 会话列表(聚合最新一条消息 + 未读 + 对方资料/在线)
|
||||
conversations() : Promise<UTSJSONObject> { return getJSON('/wxchat/api/message/conversations', {}, true) },
|
||||
unread() : Promise<UTSJSONObject> { return getJSON('/wxchat/api/message/unread', {}, true) },
|
||||
recall(id : number) : Promise<UTSJSONObject> { return postJSON('/wxchat/api/message/recall', { id: id }, true) }
|
||||
}
|
||||
|
||||
// ===================== 7. 礼物模块 =====================
|
||||
const gift = {
|
||||
list(params : any = {}) : Promise<UTSJSONObject> { return getJSON('/wxchat/api/gift/index', params, true) },
|
||||
send(data : any) : Promise<UTSJSONObject> { return postJSON('/wxchat/api/gift/send', data, true) },
|
||||
received(params : any = {}) : Promise<UTSJSONObject> { return getJSON('/wxchat/api/gift/received', params, true) }
|
||||
}
|
||||
|
||||
// ===================== 8. 抽奖模块 =====================
|
||||
const lottery = {
|
||||
config() : Promise<UTSJSONObject> { return getJSON('/wxchat/api/lottery/config', {}, true) },
|
||||
draw() : Promise<UTSJSONObject> { return postJSON('/wxchat/api/lottery/draw', {}, true) }
|
||||
}
|
||||
|
||||
// ===================== 8.1 每日任务模块 =====================
|
||||
const task = {
|
||||
// 任务列表(含真实进度与领取状态)
|
||||
list() : Promise<UTSJSONObject> { return getJSON('/wxchat/api/task/index', {}, true) },
|
||||
// 领取任务奖励
|
||||
receive(taskId : number) : Promise<UTSJSONObject> { return postJSON('/wxchat/api/task/receive', { task_id: taskId }, true) }
|
||||
}
|
||||
|
||||
// ===================== 9. 推荐模块 =====================
|
||||
const recommend = {
|
||||
index(params : any = {}) : Promise<UTSJSONObject> { return getJSON('/wxchat/api/recommend/index', params, true) }
|
||||
}
|
||||
|
||||
// ===================== 10. 搜索模块 =====================
|
||||
const search = {
|
||||
index(params : any = {}) : Promise<UTSJSONObject> { return getJSON('/wxchat/api/search/index', params, true) }
|
||||
}
|
||||
|
||||
// ===================== 11. 未读消息模块 =====================
|
||||
const unread = {
|
||||
count() : Promise<UTSJSONObject> { return getJSON('/wxchat/api/unread/count', {}, true) },
|
||||
read(data : any) : Promise<UTSJSONObject> { return postJSON('/wxchat/api/unread/read', data, true) }
|
||||
}
|
||||
|
||||
// ===================== 12. 应用基础配置模块 =====================
|
||||
const app = {
|
||||
appConf() : Promise<UTSJSONObject> { return getJSON('/wxchat/api/index/appConf', {}, false) },
|
||||
launchData() : Promise<UTSJSONObject> { return getJSON('/wxchat/api/index/launchData', {}, false) },
|
||||
baseData() : Promise<UTSJSONObject> { return getJSON('/wxchat/api/index/baseData', {}, false) }
|
||||
}
|
||||
|
||||
// ===================== 13. 意见反馈模块 =====================
|
||||
const feedback = {
|
||||
// 提交反馈
|
||||
save(data : any) : Promise<UTSJSONObject> { return postJSON('/wxchat/api/feedback/save', data, true) },
|
||||
// 我的反馈列表
|
||||
list() : Promise<UTSJSONObject> { return getJSON('/wxchat/api/feedback/index', {}, true) }
|
||||
}
|
||||
|
||||
// ===================== 统一导出 =====================
|
||||
const Api = {
|
||||
auth, user, follow, friend, moment, message, gift, lottery, task, recommend, search, unread, app, feedback
|
||||
}
|
||||
|
||||
export default Api
|
||||
export { Api, ApiError }
|
||||
export type { IUserInfo }
|
||||
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<view class="avatar-container">
|
||||
<image class="avatar" :src="user.avatar" mode="aspectFill"></image>
|
||||
<view v-if="user.online" class="online-badge"></view>
|
||||
<text v-if="user.vip" class="vip-badge">VIP</text>
|
||||
<text v-if="badge !== null && badge > 0" class="message-badge">
|
||||
{{ badge !== null && badge > 99 ? '99+' : badge }}
|
||||
</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ISender } from '@/types/chatType.uts'
|
||||
type TUser = {
|
||||
id : number
|
||||
nickname : string
|
||||
avatar : string
|
||||
online : boolean
|
||||
vip : boolean
|
||||
}
|
||||
const props = defineProps({
|
||||
badge: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
user: {
|
||||
type: Object as PropType<ISender>,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
const { badge, user } = props
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.avatar-container {
|
||||
position: relative;
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
margin-right: 30rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 20rpx;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.online-badge {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
background-color: #4CD964;
|
||||
border-radius: 50%;
|
||||
border: 4rpx solid #fff;
|
||||
}
|
||||
|
||||
.vip-badge {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
padding: 4rpx 10rpx;
|
||||
background: linear-gradient(to right, #FFD700, #FFA500);
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
border-radius: 0 0 20rpx 0;
|
||||
}
|
||||
|
||||
.message-badge {
|
||||
position: absolute;
|
||||
top: -10rpx;
|
||||
right: -10rpx;
|
||||
min-width: 36rpx;
|
||||
height: 36rpx;
|
||||
line-height: 36rpx;
|
||||
text-align: center;
|
||||
background-color: #FF3B30;
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
border-radius: 18rpx;
|
||||
padding: 0 8rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,219 @@
|
||||
<template>
|
||||
<view class="card-panel">
|
||||
<view class="card-panel-header">
|
||||
<image :src="itemData?.avatar ?? '/static/logo.png'" class="dynamic-card-avatar" alt="用户头像" />
|
||||
<view class="dynamic-card-user-info">
|
||||
<text class="dynamic-card-username">{{ itemData.nickname }}</text>
|
||||
<text class="dynamic-post-time">{{ formatTime(itemData.createTime) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-panel-body">
|
||||
<text :class="['dynamic-text-content', { collapsed: itemData.expanded==true }]">
|
||||
{{ itemData.content }}
|
||||
</text>
|
||||
<text v-if="itemData.content.length > 100" class="expand-btn" @click="toggleExpand(itemData)">
|
||||
{{ itemData.expanded ==true ? '收起' : '展开全文' }}
|
||||
</text>
|
||||
<!-- 媒体内容 -->
|
||||
<view v-if="itemData.media !=null && itemData.media.length>0" class="media-container">
|
||||
<view v-for="(media, index) in itemData.media" :key="index" class="media-item">
|
||||
<image :src="media.url" class="media-item-image" />
|
||||
<view v-if="media.type === 'video'" class="video-indicator">
|
||||
<uni-icons type="videocam" size="32"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 位置标签 -->
|
||||
<view v-if="itemData.location !=null" class="location-tag">
|
||||
<uni-icons type="location" size="18"></uni-icons> <text
|
||||
class="location-tag-text">{{ itemData.location }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-panel-footer" style="justify-content: space-between;">
|
||||
<view class="action-btn like-btn" :class="{ active: itemData.likede }" @click="toggleLike(itemData)">
|
||||
<uni-icons :type="itemData.likede ? 'heart-filled' : 'heart'" size="22"
|
||||
:color="itemData.likede ? '#f74c31' : ''"></uni-icons>
|
||||
<text class="action-text">{{ itemData.like }}</text>
|
||||
</view>
|
||||
<view class="action-btn comment-btn">
|
||||
<uni-icons type="chatbubble" size="22"></uni-icons>
|
||||
<text class="action-text">{{ itemData.comment }}</text>
|
||||
</view>
|
||||
<view class="action-btn share-btn">
|
||||
<uni-icons type="redo" size="22"></uni-icons>
|
||||
<text class="action-text">{{ itemData.share }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { type IMoment } from '@/types/moment.uts'
|
||||
|
||||
const props = defineProps<{
|
||||
moment : IMoment
|
||||
}>()
|
||||
|
||||
const itemData = computed(() : IMoment => {
|
||||
return props.moment
|
||||
})
|
||||
|
||||
onPageShow(() => {
|
||||
console.log('page-props-composition onPageShow')
|
||||
console.log(itemData)
|
||||
})
|
||||
// 切换展开/收起
|
||||
const toggleExpand = (item : IMoment) => {
|
||||
item.expanded = item.expanded != null && !item.expanded;
|
||||
};
|
||||
|
||||
// 切换点赞状态
|
||||
const toggleLike = (item : IMoment) => {
|
||||
item.likede = !item.likede;
|
||||
item.like += item.likede ? 1 : -1;
|
||||
};
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timestamp : number) => {
|
||||
const now = Date.now();
|
||||
const diff = now - timestamp;
|
||||
const minute = 60 * 1000;
|
||||
const hour = 60 * minute;
|
||||
const day = 24 * hour;
|
||||
|
||||
if (diff < hour) {
|
||||
return `${Math.floor(diff / minute)}分钟前`;
|
||||
} else if (diff < day) {
|
||||
return `${Math.floor(diff / hour)}小时前`;
|
||||
} else if (diff < 2 * day) {
|
||||
return '昨天';
|
||||
} else {
|
||||
const date = new Date(timestamp);
|
||||
return `${date.getMonth() + 1}月${date.getDate()}日`;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.dynamic-card-avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
margin-right: 20rpx;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.dynamic-card-user-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.dynamic-card-username {
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.dynamic-post-time {
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.dynamic-card-content {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.dynamic-text-content {
|
||||
font-size: 16px;
|
||||
line-height: 1.7;
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* .text-content.collapsed {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
} */
|
||||
|
||||
.expand-btn {
|
||||
color: #576b95;
|
||||
font-size: 14px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.media-container {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.media-item {
|
||||
margin: 5rpx;
|
||||
position: relative;
|
||||
flex-basis: 210rpx;
|
||||
height: 230rpx;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
.media-item-image {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.video-indicator {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.location-tag {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
align-self: flex-start;
|
||||
background: #f0f8ff;
|
||||
padding: 4px 10px;
|
||||
border-radius: 20px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.location-tag-text {
|
||||
font-size: 13px;
|
||||
color: #576b95;
|
||||
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding: 15rpx 25rpx;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-flow: row nowrap;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
|
||||
color: #666;
|
||||
font-size: 24rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<view class="content">
|
||||
<view class="top-line">
|
||||
<text class="nickname">{{ senderNickname }}</text>
|
||||
<text class="time">{{ formatTime(messageTime) }}</text>
|
||||
</view>
|
||||
<view class="bottom-line">
|
||||
<text class="message" :class="{ 'unread': hasUnread }">
|
||||
{{ messageContent }}
|
||||
</text>
|
||||
<text v-if="hasUnread" class="unread-count">
|
||||
{{ displayUnreadCount }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { IMessage, ISender } from '@/types/chatType.uts'
|
||||
|
||||
const props = defineProps<{
|
||||
message : IMessage
|
||||
}>()
|
||||
|
||||
// 计算属性提取,避免模板中过多可选链
|
||||
const senderNickname = computed(() => props.message?.sender?.nickname ?? '')
|
||||
const messageContent = computed(() => props.message?.content ?? '')
|
||||
const messageTime = computed(() => props.message?.timestamp ?? 0)
|
||||
const unreadCount = computed(() => props.message?.unreadCount ?? 0)
|
||||
const hasUnread = computed(() => unreadCount.value > 0)
|
||||
const displayUnreadCount = computed(() => {
|
||||
const count = unreadCount.value
|
||||
return count > 99 ? '99+' : count.toString()
|
||||
})
|
||||
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timestamp : number) : string => {
|
||||
const date = new Date(timestamp)
|
||||
const now = new Date()
|
||||
const diffHours = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60))
|
||||
|
||||
if (diffHours < 24) {
|
||||
// 当天消息显示时间
|
||||
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`
|
||||
} else if (diffHours < 48) {
|
||||
// 昨天
|
||||
return '昨天'
|
||||
} else if (diffHours < 168) {
|
||||
// 一周内
|
||||
const days = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
|
||||
return days[date.getDay()]
|
||||
} else {
|
||||
// 更早
|
||||
return `${date.getMonth() + 1}月${date.getDate()}日`
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.top-line {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.nickname {
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
max-width: 400rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.bottom-line {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.message {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
max-width: 500rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.message.unread {
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.unread-count {
|
||||
min-width: 36rpx;
|
||||
height: 36rpx;
|
||||
line-height: 36rpx;
|
||||
text-align: center;
|
||||
background-color: #FF3B30;
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
border-radius: 18rpx;
|
||||
padding: 0 8rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script>
|
||||
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
|
||||
CSS.supports('top: constant(a)'))
|
||||
document.write(
|
||||
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
|
||||
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
|
||||
</script>
|
||||
<title></title>
|
||||
<!--preload-links-->
|
||||
<!--app-context-->
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"><!--app-html--></div>
|
||||
<script type="module" src="/main"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,11 @@
|
||||
import App from './App.uvue'
|
||||
|
||||
import { wsManager } from './services/websocket/websocket.uts';
|
||||
import { createSSRApp } from 'vue'
|
||||
export function createApp() {
|
||||
const app = createSSRApp(App)
|
||||
app.config.globalProperties.$ws = wsManager;
|
||||
return {
|
||||
app
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
{
|
||||
"name": "缘友",
|
||||
"appid": "__UNI__B54FC01",
|
||||
"description": "",
|
||||
"versionName": "1.0.0",
|
||||
"versionCode": "100",
|
||||
"uni-app-x": {},
|
||||
/* 快应用特有相关 */
|
||||
"quickapp": {},
|
||||
/* 小程序特有相关 */
|
||||
"mp-weixin": {
|
||||
"appid": "",
|
||||
"setting": {
|
||||
"urlCheck": false
|
||||
},
|
||||
"usingComponents": true
|
||||
},
|
||||
"mp-alipay": {
|
||||
"usingComponents": true
|
||||
},
|
||||
"mp-baidu": {
|
||||
"usingComponents": true
|
||||
},
|
||||
"mp-toutiao": {
|
||||
"usingComponents": true
|
||||
},
|
||||
"uniStatistics": {
|
||||
"enable": false
|
||||
},
|
||||
"vueVersion": "3",
|
||||
"app": {
|
||||
"distribute": {
|
||||
"icons": {
|
||||
"android": {
|
||||
"hdpi": "",
|
||||
"xhdpi": "",
|
||||
"xxhdpi": "",
|
||||
"xxxhdpi": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"app-android": {
|
||||
"distribute": {
|
||||
"modules": {
|
||||
"uni-location": {
|
||||
"system": {}
|
||||
},
|
||||
"uni-map": {},
|
||||
"uni-payment": {},
|
||||
"uni-barcode-scanning": {}
|
||||
},
|
||||
"icons": {
|
||||
"hdpi": "D:/User/Desktop/缘友/icon72.png",
|
||||
"xhdpi": "D:/User/Desktop/缘友/icon96.png",
|
||||
"xxhdpi": "D:/User/Desktop/缘友/icon144.png",
|
||||
"xxxhdpi": "D:/User/Desktop/缘友/icon192.png"
|
||||
},
|
||||
"splashScreens": {
|
||||
"default": {
|
||||
"xhdpi": "D:/User/Desktop/缘友/缘友-launch.png"
|
||||
},
|
||||
"background": "#F4A5C5",
|
||||
"icon": {
|
||||
"xhdpi": "D:/User/Desktop/缘友/缘友-logo.png",
|
||||
"xxhdpi": "D:/User/Desktop/缘友/缘友-logo.png",
|
||||
"xxxhdpi": "D:/User/Desktop/缘友/缘友-logo.png"
|
||||
},
|
||||
"background@night": "#D19FD8"
|
||||
},
|
||||
"abiFilters": [
|
||||
"armeabi-v7a",
|
||||
"arm64-v8a",
|
||||
"x86",
|
||||
"x86_64"
|
||||
],
|
||||
"permissions": [
|
||||
"<uses-permission android:name=\"com.unionpay.merchant.permission\" />"
|
||||
]
|
||||
}
|
||||
},
|
||||
"app-ios": {
|
||||
"distribute": {
|
||||
"modules": {},
|
||||
"icons": {},
|
||||
"splashScreens": {}
|
||||
}
|
||||
},
|
||||
"web": {
|
||||
"router": {
|
||||
"mode": ""
|
||||
}
|
||||
},
|
||||
"app-harmony": {
|
||||
"distribute": {
|
||||
"deviceTypes": [
|
||||
"phone"
|
||||
],
|
||||
"bundleName": "cn.ywxapp.yuanyou",
|
||||
"targetSdkVersion": "6.0.0(20)",
|
||||
"compatibleSdkVersion": "6.0.0(20)",
|
||||
"splashScreens": {
|
||||
"startWindowIcon": "D:/User/Desktop/icon192.png"
|
||||
},
|
||||
"modules": {
|
||||
"uni-location": {
|
||||
"system": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
{
|
||||
"pages": [ //pages数组中第一项表示应用启动页,参考:https://doc.dcloud.net.cn/uni-app-x/collocation/pagesjson.html
|
||||
|
||||
{
|
||||
"path": "pages/guide/guide",
|
||||
"style": {
|
||||
"navigationBarTitleText": "",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "缘分",
|
||||
"disableScroll": true,
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/moment/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/ent/ent",
|
||||
"style": {
|
||||
"navigationBarTitleText": "",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/chat/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/chat/chat",
|
||||
"style": {
|
||||
"navigationBarTitleText": "",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/user/profile",
|
||||
"style": {
|
||||
"navigationBarTitleText": "",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/profile",
|
||||
"style": {
|
||||
"navigationBarTitleText": "",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/moment/publish",
|
||||
"style": {
|
||||
"navigationBarTitleText": "",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/login/register",
|
||||
"style": {
|
||||
"navigationBarTitleText": "",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/login/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/feedback",
|
||||
"style": {
|
||||
"navigationBarTitleText": "反馈",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/notification/notification",
|
||||
"style": {
|
||||
"navigationBarTitleText": "消息通知",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/help",
|
||||
"style": {
|
||||
"navigationBarTitleText": "帮助",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/about",
|
||||
"style": {
|
||||
"navigationBarTitleText": "关于我们",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/task",
|
||||
"style": {
|
||||
"navigationBarTitleText": "任务",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/wallet",
|
||||
"style": {
|
||||
"navigationBarTitleText": "钱包",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/income",
|
||||
"style": {
|
||||
"navigationBarTitleText": "收入",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/collect",
|
||||
"style": {
|
||||
"navigationBarTitleText": "收藏",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/dynamic",
|
||||
"style": {
|
||||
"navigationBarTitleText": "我的动态",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/history",
|
||||
"style": {
|
||||
"navigationBarTitleText": "历史记录",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/gift",
|
||||
"style": {
|
||||
"navigationBarTitleText": "礼物",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/fans",
|
||||
"style": {
|
||||
"navigationBarTitleText": "粉丝团",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/ranking",
|
||||
"style": {
|
||||
"navigationBarTitleText": "排名",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/lottery",
|
||||
"style": {
|
||||
"navigationBarTitleText": "抽奖",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/security",
|
||||
"style": {
|
||||
"navigationBarTitleText": "安全",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/privacy",
|
||||
"style": {
|
||||
"navigationBarTitleText": "隐私",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/likes",
|
||||
"style": {
|
||||
"navigationBarTitleText": "喜欢",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/following",
|
||||
"style": {
|
||||
"navigationBarTitleText": "关注",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/my/followers",
|
||||
"style": {
|
||||
"navigationBarTitleText": "粉丝",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/friend/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "好友",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/chatroom/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "心动聊天",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/group-chat/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "免费群聊",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/pet-social/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "神兽宠物",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/real-match/index",
|
||||
"style": {
|
||||
"navigationBarTitleText": "真人匹配",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/moment/details",
|
||||
"style": {
|
||||
"navigationBarTitleText": "",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
}
|
||||
],
|
||||
"globalStyle": {
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationBarTitleText": "uni-app x",
|
||||
"navigationBarBackgroundColor": "#F8F8F8",
|
||||
"backgroundColor": "#F8F8F8"
|
||||
|
||||
},
|
||||
"condition": {
|
||||
"current": 0,
|
||||
"list": [{
|
||||
"name": "引导页",
|
||||
"path": "pages/guide/guide"
|
||||
}]
|
||||
},
|
||||
"tabBar": {
|
||||
"list": [{
|
||||
"pagePath": "pages/index/index",
|
||||
"iconPath": "/static/images/tabbar/index.png",
|
||||
"selectedIconPath": "/static/images/tabbar/indexa.png",
|
||||
"text": "缘分"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/moment/index",
|
||||
"iconPath": "/static/images/tabbar/dynamic.png",
|
||||
"selectedIconPath": "/static/images/tabbar/dynamica.png",
|
||||
"text": "动态"
|
||||
}, {
|
||||
"pagePath": "pages/ent/ent",
|
||||
"iconPath": "/static/images/tabbar/ent.png",
|
||||
"selectedIconPath": "/static/images/tabbar/enta.png",
|
||||
"text": "娱乐"
|
||||
}, {
|
||||
"pagePath": "pages/chat/index",
|
||||
"iconPath": "/static/images/tabbar/chat.png",
|
||||
"selectedIconPath": "/static/images/tabbar/chata.png",
|
||||
"text": "消息"
|
||||
}, {
|
||||
"pagePath": "pages/my/index",
|
||||
"iconPath": "/static/images/tabbar/my.png",
|
||||
"selectedIconPath": "/static/images/tabbar/mya.png",
|
||||
"text": "我的"
|
||||
}
|
||||
]
|
||||
},
|
||||
"uniIdRouter": {}
|
||||
}
|
||||
@@ -0,0 +1,808 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<!-- 顶部导航栏 -->
|
||||
<view class="nav-bar">
|
||||
<view class="nav-left" @click="goBack">
|
||||
<uni-icons type="left" size="24" color="#333"></uni-icons>
|
||||
</view>
|
||||
<view class="nav-center">
|
||||
<text class="nav-title">{{ contactName }}</text>
|
||||
<text class="nav-sub">{{ contactOnline ? '在线' : '离线' }}</text>
|
||||
</view>
|
||||
<view class="nav-right" @click="onMoreClick">
|
||||
<uni-icons type="more-filled" size="22" color="#333"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 聊天消息区域 -->
|
||||
<scroll-view class="msg-scroll" direction="vertical" :scroll-into-view="scrollToId"
|
||||
:scroll-with-animation="true" id="msgScroll">
|
||||
<view v-for="(msg, idx) in messages" :key="msg.id" :id="'msg-' + msg.id"
|
||||
class="msg-row" :class="{ self: msg.senderId == myUid }">
|
||||
<image class="msg-avatar" :src="msg.senderId == myUid ? myAvatar : contactAvatar" mode="aspectFill"></image>
|
||||
<view class="msg-content">
|
||||
<view v-if="msg.type == 'image'" class="img-bubble" @click="previewImage(msg.content)">
|
||||
<image class="msg-img" :src="msg.content" mode="widthFix"></image>
|
||||
</view>
|
||||
<view v-else class="text-bubble" :class="{ self: msg.senderId == myUid }">
|
||||
<text class="text-inner">{{ msg.content }}</text>
|
||||
</view>
|
||||
<text class="msg-time">{{ fmtClock(msg.timestamp) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="messages.length == 0" class="empty">
|
||||
<text class="empty-text">还没有消息,打个招呼吧~</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部输入区域 -->
|
||||
<view class="input-bar">
|
||||
<view class="action-btn" @click="toggleEmoji">
|
||||
<uni-icons :type="showEmoji ? 'keyboard' : 'smile'" size="26" color="#666"></uni-icons>
|
||||
</view>
|
||||
<view class="input-box">
|
||||
<textarea v-model="inputText" class="text-input" placeholder="说点什么..." :auto-height="true"
|
||||
:adjust-position="true" @focus="closePanels" @confirm="sendText"></textarea>
|
||||
</view>
|
||||
<view class="action-btn" @click="toggleMore">
|
||||
<uni-icons type="plus" size="26" color="#666"></uni-icons>
|
||||
</view>
|
||||
<text v-if="inputText.trim().length > 0" class="send-btn" @click="sendText">发送</text>
|
||||
</view>
|
||||
|
||||
<!-- 表情面板 -->
|
||||
<view v-if="showEmoji" class="emoji-panel">
|
||||
<scroll-view direction="vertical" class="emoji-scroll">
|
||||
<view class="emoji-list">
|
||||
<text v-for="(e, i) in emojis" :key="i" class="emoji-item" @click="insertEmoji(e)">{{ e }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 更多面板 -->
|
||||
<view v-if="showMore" class="more-panel">
|
||||
<view class="more-grid">
|
||||
<view class="more-item" @click="chooseImage">
|
||||
<uni-icons type="image" size="30" color="#666"></uni-icons>
|
||||
<text class="more-txt">图片</text>
|
||||
</view>
|
||||
<view class="more-item" @click="chooseImage(true)">
|
||||
<uni-icons type="camera" size="30" color="#666"></uni-icons>
|
||||
<text class="more-txt">拍摄</text>
|
||||
</view>
|
||||
<view class="more-item" @click="toast('位置功能开发中')">
|
||||
<uni-icons type="location" size="30" color="#666"></uni-icons>
|
||||
<text class="more-txt">位置</text>
|
||||
</view>
|
||||
<view class="more-item" @click="toast('红包功能开发中')">
|
||||
<uni-icons type="redpacket" size="30" color="#FF6B6B"></uni-icons>
|
||||
<text class="more-txt">红包</text>
|
||||
</view>
|
||||
<view class="more-item" @click="openGift">
|
||||
<uni-icons type="gift" size="30" color="#FF9500"></uni-icons>
|
||||
<text class="more-txt">礼物</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 送礼弹窗 -->
|
||||
<view v-if="showGift" class="gift-mask" @click="closeGift">
|
||||
<view class="gift-sheet" @click.stop>
|
||||
<view class="gift-sheet-head">
|
||||
<text class="gift-sheet-title">赠送礼物</text>
|
||||
<uni-icons type="close" size="22" color="#999" @click="closeGift"></uni-icons>
|
||||
</view>
|
||||
<scroll-view class="gift-sheet-list" direction="vertical" :show-scrollbar="false">
|
||||
<view v-for="(g, i) in gifts" :key="g.id" class="gift-sheet-cell"
|
||||
:class="{ active: selectedGift != null && selectedGift.id == g.id }" @click="selectGift(g)">
|
||||
<text class="gift-sheet-name">{{ g.name }}</text>
|
||||
<text class="gift-sheet-price">{{ g.price }}金币</text>
|
||||
</view>
|
||||
<view v-if="gifts.length == 0" class="gift-sheet-empty">
|
||||
<text class="empty-text">暂无可赠送的礼物</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="gift-sheet-count">
|
||||
<text class="count-label">数量</text>
|
||||
<view class="count-ctrl">
|
||||
<text class="count-btn" @click="giftCount > 1 ? giftCount = giftCount - 1 : null">-</text>
|
||||
<text class="count-num">{{ giftCount }}</text>
|
||||
<text class="count-btn" @click="giftCount = giftCount + 1">+</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="gift-sheet-send" @click="sendGift">
|
||||
<text class="send-text">赠送({{ giftTotal }}金币)</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, computed, onLoad, onMounted, onUnmounted } from 'vue'
|
||||
import Api from '@/common/api-service.uts'
|
||||
import { getUserInfo } from '@/stores/user.uts'
|
||||
|
||||
type ChatMessage = {
|
||||
id : number
|
||||
senderId : number
|
||||
receiverId : number
|
||||
content : string
|
||||
timestamp : number
|
||||
type : 'text' | 'image'
|
||||
read : boolean
|
||||
sending ?: boolean
|
||||
}
|
||||
|
||||
const myUid = ref<number>(0)
|
||||
const myAvatar = ref<string>('')
|
||||
const contactUid = ref<number>(0)
|
||||
const contactName = ref<string>('')
|
||||
const contactAvatar = ref<string>('')
|
||||
const contactOnline = ref<boolean>(false)
|
||||
|
||||
const messages = ref<ChatMessage[]>([])
|
||||
const inputText = ref<string>('')
|
||||
const showEmoji = ref<boolean>(false)
|
||||
const showMore = ref<boolean>(false)
|
||||
const scrollToId = ref<string>('')
|
||||
let pollTimer : number | null = null
|
||||
|
||||
const emojis = ref<string[]>([
|
||||
'😀', '😃', '😄', '😁', '😆', '😅', '😂', '🤣', '😊', '😇',
|
||||
'🙂', '😉', '😍', '🥰', '😘', '😋', '😜', '🤪', '🤩', '🥳',
|
||||
'😎', '🤔', '🤗', '😴', '😭', '😡', '👍', '👏', '💕', '🎉',
|
||||
'🌹', '❤️', '🔥', '✨', '🍻', '🎁', '🌟', '😏', '🙄', '💔'
|
||||
])
|
||||
|
||||
const findIdx = (id : number) : number => {
|
||||
for (let i = 0; i < messages.value.length; i++) {
|
||||
if (messages.value[i].id == id) return i
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// 提交真实消息:若轮询已拉到同一条(真实 id 已存在),则丢弃临时消息避免重复
|
||||
const commitMessage = (tempId : number, realId : number, content : string, type : 'text' | 'image', createAt : number) => {
|
||||
const tempIdx = findIdx(tempId)
|
||||
const realIdx = findIdx(realId)
|
||||
if (realIdx >= 0) {
|
||||
if (tempIdx >= 0) messages.value.splice(tempIdx, 1)
|
||||
return
|
||||
}
|
||||
if (tempIdx >= 0) {
|
||||
messages.value[tempIdx] = {
|
||||
id : realId,
|
||||
senderId : myUid.value,
|
||||
receiverId : contactUid.value,
|
||||
content : content,
|
||||
timestamp : createAt,
|
||||
type : type,
|
||||
read : true
|
||||
} as ChatMessage
|
||||
}
|
||||
}
|
||||
|
||||
const loadContact = () => {
|
||||
Api.user.detail(contactUid.value)
|
||||
.then((res : UTSJSONObject) => {
|
||||
contactName.value = String(res.get('nickname') ?? '')
|
||||
contactAvatar.value = String(res.get('avatar') ?? '')
|
||||
contactOnline.value = Boolean(res.get('is_online') ?? false)
|
||||
})
|
||||
.catch(() => {
|
||||
contactName.value = '用户' + contactUid.value
|
||||
})
|
||||
}
|
||||
|
||||
const mapMsg = (item : UTSJSONObject) : ChatMessage => {
|
||||
const ct = Number(item.get('content_type') ?? 0)
|
||||
return {
|
||||
id : Number(item.get('id')),
|
||||
senderId : Number(item.get('sender_id')),
|
||||
receiverId : Number(item.get('receiver_id')),
|
||||
content : String(item.get('content') ?? ''),
|
||||
timestamp : Number(item.get('create_at') ?? 0) * 1000,
|
||||
type : ct == 1 ? 'image' : 'text',
|
||||
read : true
|
||||
} as ChatMessage
|
||||
}
|
||||
|
||||
// 初始化历史(倒序返回 -> 反转成正序)
|
||||
const loadHistory = () => {
|
||||
Api.message.history({ contact_id: contactUid.value, contact_type: 0, page: 1, limit: 50 })
|
||||
.then((res : UTSJSONObject) => {
|
||||
const list = res.get('list') as UTSJSONObject[] | null
|
||||
if (list == null) return
|
||||
const arr : ChatMessage[] = []
|
||||
for (const it of list) {
|
||||
arr.push(mapMsg(it))
|
||||
}
|
||||
arr.reverse()
|
||||
messages.value = arr
|
||||
scrollToBottom()
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
// 轮询:仅追加缺失的新消息
|
||||
const pollNew = () => {
|
||||
Api.message.history({ contact_id: contactUid.value, contact_type: 0, page: 1, limit: 50 })
|
||||
.then((res : UTSJSONObject) => {
|
||||
const list = res.get('list') as UTSJSONObject[] | null
|
||||
if (list == null) return
|
||||
let changed = false
|
||||
for (const it of list) {
|
||||
const id = Number(it.get('id'))
|
||||
if (findIdx(id) >= 0) continue
|
||||
messages.value.push(mapMsg(it))
|
||||
changed = true
|
||||
}
|
||||
if (changed) scrollToBottom()
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
const scrollToBottom = () => {
|
||||
if (messages.value.length == 0) return
|
||||
const lastId = messages.value[messages.value.length - 1].id
|
||||
// 延迟一帧确保渲染完成
|
||||
setTimeout(() => {
|
||||
scrollToId.value = 'msg-' + lastId
|
||||
}, 50)
|
||||
}
|
||||
|
||||
let tempCounter = 0
|
||||
const sendText = () => {
|
||||
const text = inputText.value.trim()
|
||||
if (text.length == 0) return
|
||||
inputText.value = ''
|
||||
closePanels()
|
||||
const tempId = pushOptimistic(text, 'text')
|
||||
Api.message.send({ receiver_id: contactUid.value, receiver_type: 0, content: text, content_type: 0 })
|
||||
.then((msgObj : UTSJSONObject) => {
|
||||
commitMessage(tempId, Number(msgObj.get('id')), text, 'text', Number(msgObj.get('create_at') ?? 0) * 1000)
|
||||
})
|
||||
.catch(() => {
|
||||
uni.showToast({ title: '发送失败', icon: 'none' })
|
||||
})
|
||||
}
|
||||
|
||||
const pushOptimistic = (content : string, type : 'text' | 'image') : number => {
|
||||
tempCounter--
|
||||
const tempId = tempCounter
|
||||
messages.value.push({
|
||||
id : tempId,
|
||||
senderId : myUid.value,
|
||||
receiverId : contactUid.value,
|
||||
content : content,
|
||||
timestamp : Date.now(),
|
||||
type : type,
|
||||
read : true,
|
||||
sending : true
|
||||
} as ChatMessage)
|
||||
scrollToBottom()
|
||||
return tempId
|
||||
}
|
||||
|
||||
const chooseImage = (camera : boolean = false) => {
|
||||
closePanels()
|
||||
uni.chooseImage({
|
||||
count : 1,
|
||||
sourceType : camera ? ['camera'] : ['album'],
|
||||
success : (res) => {
|
||||
const path = res.tempFilePaths[0]
|
||||
const tempId = pushOptimistic(path, 'image')
|
||||
Api.message.send({ receiver_id: contactUid.value, receiver_type: 0, content: path, content_type: 1 })
|
||||
.then((msgObj : UTSJSONObject) => {
|
||||
commitMessage(tempId, Number(msgObj.get('id')), path, 'image', Number(msgObj.get('create_at') ?? 0) * 1000)
|
||||
})
|
||||
.catch(() => {
|
||||
uni.showToast({ title: '图片发送失败', icon: 'none' })
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const previewImage = (url : string) => {
|
||||
uni.previewImage({ urls: [url], current: url })
|
||||
}
|
||||
|
||||
const toggleEmoji = () => {
|
||||
showEmoji.value = !showEmoji.value
|
||||
showMore.value = false
|
||||
}
|
||||
|
||||
const toggleMore = () => {
|
||||
showMore.value = !showMore.value
|
||||
showEmoji.value = false
|
||||
}
|
||||
|
||||
const closePanels = () => {
|
||||
showEmoji.value = false
|
||||
showMore.value = false
|
||||
}
|
||||
|
||||
const insertEmoji = (e : string) => {
|
||||
inputText.value += e
|
||||
}
|
||||
|
||||
const fmtClock = (ts : number) : string => {
|
||||
if (! ts) return ''
|
||||
const d = new Date(ts)
|
||||
const h = d.getHours().toString().padStart(2, '0')
|
||||
const m = d.getMinutes().toString().padStart(2, '0')
|
||||
return `${h}:${m}`
|
||||
}
|
||||
|
||||
const toast = (t : string) => {
|
||||
uni.showToast({ title: t, icon: 'none' })
|
||||
closePanels()
|
||||
}
|
||||
|
||||
// ===== 送礼弹窗 =====
|
||||
type IGift = {
|
||||
id : number
|
||||
name : string
|
||||
icon : string
|
||||
price : number
|
||||
}
|
||||
const showGift = ref<boolean>(false)
|
||||
const gifts = reactive<IGift[]>([])
|
||||
const selectedGift = ref<IGift | null>(null)
|
||||
const giftCount = ref<number>(1)
|
||||
const giftLoading = ref<boolean>(false)
|
||||
|
||||
const giftTotal = computed<number>(() => {
|
||||
const p = selectedGift.value?.price ?? 0
|
||||
return p * giftCount.value
|
||||
})
|
||||
|
||||
const openGift = () => {
|
||||
closePanels()
|
||||
showGift.value = true
|
||||
loadGifts()
|
||||
}
|
||||
|
||||
const loadGifts = () => {
|
||||
if (giftLoading.value) return
|
||||
giftLoading.value = true
|
||||
Api.gift.list()
|
||||
.then((res : any) => {
|
||||
const list = (res as Array<IGift>) ?? []
|
||||
gifts.splice(0, gifts.length)
|
||||
list.forEach((g : IGift) => gifts.push(g))
|
||||
if (gifts.length > 0 && selectedGift.value == null) {
|
||||
selectedGift.value = gifts[0]
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
uni.showToast({ title: '礼物加载失败', icon: 'none' })
|
||||
})
|
||||
.finally(() => { giftLoading.value = false })
|
||||
}
|
||||
|
||||
const selectGift = (g : IGift) => {
|
||||
selectedGift.value = g
|
||||
}
|
||||
|
||||
const sendGift = () => {
|
||||
if (selectedGift.value == null) {
|
||||
uni.showToast({ title: '请选择礼物', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (contactUid.value <= 0) return
|
||||
uni.showLoading({ title: '赠送中...', mask: true })
|
||||
Api.gift.send({
|
||||
to_uid: contactUid.value,
|
||||
gift_id: selectedGift.value.id,
|
||||
count: giftCount.value,
|
||||
message: ''
|
||||
})
|
||||
.then(() => {
|
||||
uni.hideLoading()
|
||||
showGift.value = false
|
||||
uni.showToast({ title: '赠送成功', icon: 'success' })
|
||||
})
|
||||
.catch((e : any) => {
|
||||
uni.hideLoading()
|
||||
const msg = (e as UTSJSONObject)?.get('message') as string
|
||||
uni.showToast({ title: msg ?? '赠送失败', icon: 'none' })
|
||||
})
|
||||
}
|
||||
|
||||
const closeGift = () => {
|
||||
showGift.value = false
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
const onMoreClick = () => {
|
||||
uni.showActionSheet({
|
||||
itemList: ['清空本地聊天记录', '举报用户'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex == 0) {
|
||||
messages.value = []
|
||||
uni.showToast({ title: '已清空', icon: 'none' })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onLoad((options : any) => {
|
||||
const uid = Number(options?.uid ?? options?.userId ?? 0)
|
||||
contactUid.value = uid
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
const me = getUserInfo()
|
||||
if (me != null) {
|
||||
myUid.value = me.uid
|
||||
myAvatar.value = me.avatar
|
||||
}
|
||||
loadContact()
|
||||
loadHistory()
|
||||
pollTimer = setInterval(() => {
|
||||
pollNew()
|
||||
}, 4000) as number
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (pollTimer != null) {
|
||||
clearInterval(pollTimer)
|
||||
pollTimer = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #ededed;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
padding-top: var(--status-bar-height);
|
||||
height: 90rpx;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background-color: #f7f7f7;
|
||||
border-bottom: 1rpx solid #e0e0e0;
|
||||
}
|
||||
|
||||
.nav-left, .nav-right {
|
||||
width: 90rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-center {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.nav-sub {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.msg-scroll {
|
||||
flex: 1;
|
||||
padding: 20rpx 16rpx 10rpx;
|
||||
}
|
||||
|
||||
.msg-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
|
||||
.msg-row.self {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.msg-avatar {
|
||||
width: 76rpx;
|
||||
height: 76rpx;
|
||||
border-radius: 12rpx;
|
||||
background-color: #ccc;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.msg-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 70%;
|
||||
margin: 0 16rpx;
|
||||
}
|
||||
|
||||
.msg-row.self .msg-content {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.text-bubble {
|
||||
padding: 18rpx 22rpx;
|
||||
border-radius: 16rpx;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.text-bubble.self {
|
||||
background-color: #95EC69;
|
||||
}
|
||||
|
||||
.text-inner {
|
||||
font-size: 30rpx;
|
||||
color: #1a1a1a;
|
||||
line-height: 1.45;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.img-bubble {
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
background-color: #fff;
|
||||
max-width: 360rpx;
|
||||
}
|
||||
|
||||
.msg-img {
|
||||
width: 300rpx;
|
||||
}
|
||||
|
||||
.msg-time {
|
||||
font-size: 20rpx;
|
||||
color: #b2b2b2;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
|
||||
.empty {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 80rpx;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.input-bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-end;
|
||||
padding: 12rpx 16rpx;
|
||||
background-color: #f7f7f7;
|
||||
border-top: 1rpx solid #e0e0e0;
|
||||
padding-bottom: calc(12rpx + var(--uni-safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
|
||||
.input-box {
|
||||
flex: 1;
|
||||
margin: 0 12rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 12rpx;
|
||||
padding: 12rpx 16rpx;
|
||||
max-height: 180rpx;
|
||||
}
|
||||
|
||||
.text-input {
|
||||
width: 100%;
|
||||
font-size: 30rpx;
|
||||
min-height: 40rpx;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
background-color: #07C160;
|
||||
color: #fff;
|
||||
font-size: 26rpx;
|
||||
padding: 12rpx 24rpx;
|
||||
border-radius: 12rpx;
|
||||
margin-bottom: 6rpx;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.emoji-panel {
|
||||
height: 360rpx;
|
||||
background-color: #fff;
|
||||
border-top: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.emoji-scroll {
|
||||
height: 360rpx;
|
||||
}
|
||||
|
||||
.emoji-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
padding: 10rpx;
|
||||
}
|
||||
|
||||
.emoji-item {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
font-size: 40rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.more-panel {
|
||||
background-color: #fff;
|
||||
border-top: 1rpx solid #eee;
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.more-grid {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.more-item {
|
||||
width: 130rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 10rpx 10rpx;
|
||||
}
|
||||
|
||||
.more-txt {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.gift-mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.gift-sheet {
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
border-top-left-radius: 28rpx;
|
||||
border-top-right-radius: 28rpx;
|
||||
padding: 24rpx 30rpx calc(30rpx + var(--uni-safe-area-inset-bottom));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.gift-sheet-head {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.gift-sheet-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.gift-sheet-list {
|
||||
max-height: 420rpx;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
|
||||
.gift-sheet-cell {
|
||||
width: 22%;
|
||||
margin: 10rpx 1.5%;
|
||||
padding: 20rpx 0;
|
||||
border-radius: 16rpx;
|
||||
background-color: #f7f7f7;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.gift-sheet-cell.active {
|
||||
background-color: #fff0ec;
|
||||
border: 2rpx solid #FF6B6B;
|
||||
}
|
||||
|
||||
.gift-sheet-name {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.gift-sheet-price {
|
||||
font-size: 22rpx;
|
||||
color: #FF6B6B;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
|
||||
.gift-sheet-empty {
|
||||
width: 100%;
|
||||
padding: 60rpx 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.gift-sheet-count {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 20rpx 0;
|
||||
}
|
||||
|
||||
.count-label {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.count-ctrl {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.count-btn {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 30rpx;
|
||||
background-color: #f0f0f0;
|
||||
text-align: center;
|
||||
line-height: 60rpx;
|
||||
font-size: 36rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.count-num {
|
||||
min-width: 60rpx;
|
||||
text-align: center;
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.gift-sheet-send {
|
||||
height: 92rpx;
|
||||
border-radius: 46rpx;
|
||||
background: linear-gradient(135deg, #FF6B6B 0%, #FF8E53 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.send-text {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,362 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<!-- 顶部导航栏 -->
|
||||
<view class="nav-bar">
|
||||
<view class="nav-tabs">
|
||||
<view class="nav-tab" :class="{ active: activeTab == 0 }" @click="switchTab(0)">
|
||||
<text class="nav-tab-txt" :class="{ active: activeTab == 0 }">消息</text>
|
||||
</view>
|
||||
<view class="nav-tab" :class="{ active: activeTab == 1 }" @click="switchTab(1)">
|
||||
<text class="nav-tab-txt" :class="{ active: activeTab == 1 }">缘友</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 消息 Tab:会话列表 -->
|
||||
<scroll-view v-if="activeTab == 0" class="list-scroll" direction="vertical"
|
||||
:refresher-enabled="true" :refresher-triggered="refreshing"
|
||||
@refresherrefresh="loadConversations">
|
||||
<view v-if="conversations.length == 0" class="empty">
|
||||
<text class="empty-icon">💬</text>
|
||||
<text class="empty-text">还没有聊天,去「缘友」找人聊聊吧</text>
|
||||
</view>
|
||||
<view v-for="(item, idx) in conversations" :key="idx" class="conv-item"
|
||||
@click="openChat(item.peer_id)">
|
||||
<view class="avatar-wrap">
|
||||
<image class="avatar" :src="item.avatar || defaultAvatar" mode="aspectFill"></image>
|
||||
<view v-if="item.online" class="online-dot"></view>
|
||||
</view>
|
||||
<view class="conv-main">
|
||||
<view class="conv-row">
|
||||
<text class="conv-name">{{ item.nickname || '用户' + item.peer_id }}</text>
|
||||
<text class="conv-time">{{ fmtTime(item.last_time) }}</text>
|
||||
</view>
|
||||
<view class="conv-row">
|
||||
<text class="conv-last">{{ item.is_self ? '我: ' : '' }}{{ previewContent(item) }}</text>
|
||||
<view v-if="item.unread_count > 0" class="badge">
|
||||
<text class="badge-txt">{{ item.unread_count > 99 ? '99+' : item.unread_count }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 缘友 Tab:在线用户,点击直接发起聊天 -->
|
||||
<scroll-view v-else class="list-scroll" direction="vertical"
|
||||
:refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="loadOnline">
|
||||
<view v-if="onlineUsers.length == 0" class="empty">
|
||||
<text class="empty-icon">🌟</text>
|
||||
<text class="empty-text">暂无在线缘友,稍后再来看看</text>
|
||||
</view>
|
||||
<view v-for="(u, idx) in onlineUsers" :key="idx" class="conv-item" @click="openChat(u.uid)">
|
||||
<view class="avatar-wrap">
|
||||
<image class="avatar" :src="u.avatar || defaultAvatar" mode="aspectFill"></image>
|
||||
<view v-if="u.online" class="online-dot"></view>
|
||||
</view>
|
||||
<view class="conv-main">
|
||||
<view class="conv-row">
|
||||
<text class="conv-name">{{ u.nickname || '用户' + u.uid }}</text>
|
||||
<text class="conv-time online-tag">在线</text>
|
||||
</view>
|
||||
<view class="conv-row">
|
||||
<text class="conv-last">点击发起聊天</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted, onShow } from 'vue'
|
||||
import Api from '@/common/api-service.uts'
|
||||
|
||||
type Convo = {
|
||||
peer_id : number
|
||||
nickname : string
|
||||
avatar : string
|
||||
online : boolean
|
||||
last_content : string
|
||||
last_content_type : number
|
||||
last_time : number
|
||||
unread_count : number
|
||||
is_self : boolean
|
||||
}
|
||||
|
||||
type OnlineUser = {
|
||||
uid : number
|
||||
nickname : string
|
||||
avatar : string
|
||||
online : boolean
|
||||
}
|
||||
|
||||
const defaultAvatar = 'https://randomuser.me/api/portraits/lego/1.jpg'
|
||||
const activeTab = ref<number>(0)
|
||||
const refreshing = ref<boolean>(false)
|
||||
const conversations = ref<Convo[]>([])
|
||||
const onlineUsers = ref<OnlineUser[]>([])
|
||||
|
||||
const switchTab = (i : number) => {
|
||||
activeTab.value = i
|
||||
if (i == 0) {
|
||||
loadConversations()
|
||||
} else {
|
||||
loadOnline()
|
||||
}
|
||||
}
|
||||
|
||||
const loadConversations = () => {
|
||||
refreshing.value = true
|
||||
Api.message.conversations()
|
||||
.then((res : UTSJSONObject) => {
|
||||
const list = res.get('list') as UTSJSONObject[] | null
|
||||
const arr : Convo[] = []
|
||||
if (list != null) {
|
||||
for (const it of list) {
|
||||
arr.push({
|
||||
peer_id : Number(it.get('peer_id')),
|
||||
nickname : String(it.get('nickname') ?? ''),
|
||||
avatar : String(it.get('avatar') ?? ''),
|
||||
online : Boolean(it.get('online') ?? false),
|
||||
last_content : String(it.get('last_content') ?? ''),
|
||||
last_content_type : Number(it.get('last_content_type') ?? 0),
|
||||
last_time : Number(it.get('last_time') ?? 0),
|
||||
unread_count : Number(it.get('unread_count') ?? 0),
|
||||
is_self : Boolean(it.get('is_self') ?? false)
|
||||
} as Convo)
|
||||
}
|
||||
}
|
||||
conversations.value = arr
|
||||
})
|
||||
.catch(() => {
|
||||
conversations.value = []
|
||||
})
|
||||
.finally(() => {
|
||||
refreshing.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const loadOnline = () => {
|
||||
refreshing.value = true
|
||||
Api.user.online()
|
||||
.then((res : UTSJSONObject) => {
|
||||
let list : UTSJSONObject[] | null = null
|
||||
if (res.containsKey('list')) {
|
||||
list = res.get('list') as UTSJSONObject[] | null
|
||||
} else {
|
||||
list = res as any as UTSJSONObject[]
|
||||
}
|
||||
const arr : OnlineUser[] = []
|
||||
if (list != null) {
|
||||
for (const it of list) {
|
||||
const uid = Number(it.get('uid') ?? it.get('id') ?? 0)
|
||||
if (uid <= 0) continue
|
||||
const online = Boolean(it.get('online') ?? it.get('is_online') ?? (Number(it.get('online_status') ?? 0) == 1))
|
||||
arr.push({
|
||||
uid : uid,
|
||||
nickname : String(it.get('nickname') ?? ''),
|
||||
avatar : String(it.get('avatar') ?? ''),
|
||||
online : online
|
||||
} as OnlineUser)
|
||||
}
|
||||
}
|
||||
onlineUsers.value = arr
|
||||
})
|
||||
.catch(() => {
|
||||
onlineUsers.value = []
|
||||
})
|
||||
.finally(() => {
|
||||
refreshing.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const previewContent = (item : Convo) : string => {
|
||||
if (item.last_content_type == 1) return '[图片]'
|
||||
if (item.last_content_type == 2) return '[语音]'
|
||||
return item.last_content || '[暂无消息]'
|
||||
}
|
||||
|
||||
const fmtTime = (sec : number) : string => {
|
||||
if (! sec) return ''
|
||||
const d = new Date(sec * 1000)
|
||||
const now = new Date()
|
||||
const diff = (now.getTime() - d.getTime()) / 1000
|
||||
if (diff < 60) return '刚刚'
|
||||
if (diff < 3600) return Math.floor(diff / 60) + '分钟前'
|
||||
if (diff < 86400) return Math.floor(diff / 3600) + '小时前'
|
||||
if (diff < 86400 * 2) return '昨天'
|
||||
const y = d.getFullYear()
|
||||
const m = d.getMonth() + 1
|
||||
const day = d.getDate()
|
||||
if (y == now.getFullYear()) return `${m}月${day}日`
|
||||
return `${y}年${m}月${day}日`
|
||||
}
|
||||
|
||||
const openChat = (uid : number) => {
|
||||
uni.navigateTo({ url: `/pages/chat/chat?uid=${uid}` })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadConversations()
|
||||
})
|
||||
|
||||
// 从聊天页返回时刷新会话(更新未读/最新消息)
|
||||
onShow(() => {
|
||||
if (activeTab.value == 0) {
|
||||
loadConversations()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: #f5f6f8;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
padding-top: var(--status-bar-height);
|
||||
height: 90rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #fff;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.nav-tabs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.nav-tab {
|
||||
margin: 0 24rpx;
|
||||
padding: 10rpx 0;
|
||||
}
|
||||
|
||||
.nav-tab-txt {
|
||||
font-size: 32rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.nav-tab-txt.active {
|
||||
font-size: 38rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.list-scroll {
|
||||
flex: 1;
|
||||
padding: 10rpx 20rpx;
|
||||
}
|
||||
|
||||
.conv-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 24rpx 20rpx;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
margin-bottom: 16rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.avatar-wrap {
|
||||
position: relative;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.online-dot {
|
||||
position: absolute;
|
||||
right: 2rpx;
|
||||
bottom: 2rpx;
|
||||
width: 18rpx;
|
||||
height: 18rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #4CD964;
|
||||
border: 3rpx solid #fff;
|
||||
}
|
||||
|
||||
.conv-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.conv-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.conv-name {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
max-width: 360rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.conv-time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.online-tag {
|
||||
color: #4CD964;
|
||||
}
|
||||
|
||||
.conv-last {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.badge {
|
||||
min-width: 32rpx;
|
||||
height: 32rpx;
|
||||
padding: 0 8rpx;
|
||||
border-radius: 16rpx;
|
||||
background-color: #FF4D4F;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.badge-txt {
|
||||
font-size: 22rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: 160rpx;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 80rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="nav-bar">
|
||||
<view class="nav-back" @click="goBack">
|
||||
<uni-icons type="left" size="22" color="#333"></uni-icons>
|
||||
</view>
|
||||
<text class="nav-title">心动聊天</text>
|
||||
<view class="nav-right"></view>
|
||||
</view>
|
||||
|
||||
<view class="hero">
|
||||
<text class="hero-emoji">💬</text>
|
||||
<text class="hero-title">心动聊天室</text>
|
||||
<text class="hero-sub">玩游戏、破冰互动,轻松交到好朋友</text>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<text class="section-title">热门玩法</text>
|
||||
<view class="cell" v-for="(item, i) in rooms" :key="i" @click="enterRoom(item)">
|
||||
<text class="cell-name">{{ item.name }}</text>
|
||||
<text class="cell-online">{{ item.online }}人在线</text>
|
||||
<uni-icons type="right" size="16" color="#ccc"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tip">
|
||||
<text>更多互动房间即将上线,敬请期待~</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const rooms = ref([
|
||||
{ name: '真心话大冒险', online: 128 },
|
||||
{ name: '你画我猜', online: 86 },
|
||||
{ name: '语音速配', online: 203 }
|
||||
] as UTSJSONObject[])
|
||||
|
||||
const enterRoom = (item : UTSJSONObject) => {
|
||||
uni.showToast({ title: '进入「' + (item.getString('name') ?? '') + '」', icon: 'none' })
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #f6f6f6;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 20rpx 24rpx;
|
||||
padding-top: 60rpx;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
width: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 34rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.nav-right {
|
||||
width: 60rpx;
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 60rpx 0 40rpx;
|
||||
background: linear-gradient(to bottom right, #FF6B6B, #FF8E53);
|
||||
}
|
||||
|
||||
.hero-emoji {
|
||||
font-size: 90rpx;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.hero-sub {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.section {
|
||||
background-color: #fff;
|
||||
margin: 24rpx;
|
||||
border-radius: 20rpx;
|
||||
padding: 10rpx 24rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
padding: 20rpx 0 10rpx;
|
||||
}
|
||||
|
||||
.cell {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 26rpx 0;
|
||||
border-top: 1rpx solid #f2f2f2;
|
||||
}
|
||||
|
||||
.cell-name {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.cell-online {
|
||||
font-size: 24rpx;
|
||||
color: #FF6B6B;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.tip {
|
||||
text-align: center;
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.tip text {
|
||||
font-size: 24rpx;
|
||||
color: #aaa;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,897 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<!-- 顶部导航栏 -->
|
||||
<view class="page-nav">
|
||||
<view class="page-nav-left">
|
||||
<view class="tabsbox">
|
||||
<view v-for="(tab, index) in filters" :key="index" class="tabsbox-item"
|
||||
:class="{ 'tabsbox-active': tabActive == index }" @click="switchTab(index)">
|
||||
<text class="tabsbox-txt"
|
||||
:class="{'tabsbox-active-txt': tabActive == index }">{{ tab.text }}</text>
|
||||
<!-- <text v-if="tab.count" class="ywx-tabs-badge">{{ tab.count }}</text> -->
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="page-nav-right">
|
||||
<view class="icon-btn" @click="onSearchClick">
|
||||
<uni-icons type="search" size="32" color="#333"></uni-icons>
|
||||
</view>
|
||||
<view class="icon-btn" @click="onMessageClick">
|
||||
<uni-icons type="chat" size="32" color="#333"></uni-icons>
|
||||
<text v-if="unreadCount > 0" class="message-badge">{{ unreadCount > 99 ? '99+' : unreadCount }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="page-body" style=" padding-bottom: 90rpx;">
|
||||
<!-- 轮播图 -->
|
||||
<swiper class="banner-swiper" :indicator-dots="true" :autoplay="true" :interval="3000" :duration="500">
|
||||
<swiper-item v-for="(banner, index) in banners" :key="index" @click="onBannerClick(banner)">
|
||||
<image class="banner-image" :src="banner.image" mode="aspectFill"></image>
|
||||
<text v-if="banner.tag" class="banner-tag">{{ banner.tag }}</text>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<!-- 快速入口 -->
|
||||
<!-- <scroll-view class="quick-entrance" scroll-x>
|
||||
<view v-for="(entrance, index) in quickEntrances" :key="index" class="entrance-item"
|
||||
@click="onEntranceClick(entrance)">
|
||||
<view class="entrance-icon" :style="{ backgroundColor: entrance.bgColor }">
|
||||
<uni-icons :type="entrance.icon" size="28"
|
||||
:color="entrance.iconColor != null || '#fff'"></uni-icons>
|
||||
</view>
|
||||
<text class="entrance-text">{{ entrance.text }}</text>
|
||||
<view v-if="entrance.hot" class="hot-badge">HOT</view>
|
||||
</view>
|
||||
</scroll-view> -->
|
||||
<!-- 筛选栏 -->
|
||||
<!-- <view class="filter-bar">
|
||||
<view v-for="(filter, index) in filters" :key="index" class="filter-item"
|
||||
:class="{ active: tabActive == index }" @click="changeFilter(index)">
|
||||
<text class="filter-text">{{ filter.text }}</text>
|
||||
<view v-if="filter.count" class="filter-count">{{ filter.count }}</view>
|
||||
</view>
|
||||
<view class="filter-more" @click="showMoreFilters">
|
||||
<uni-icons type="more" size="20" color="#666"></uni-icons>
|
||||
</view>
|
||||
</view> -->
|
||||
<!-- 用户列表 -->
|
||||
<scroll-view class="user-list" direction="vertical" :show-scrollbar="false" :refresher-enabled="true"
|
||||
:refresher-triggered="refreshing" @refresherrefresh="onRefresh" @scrolltolower="onLoadMore">
|
||||
<!-- 直播 -->
|
||||
<view v-if="tabActive == 0" class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">语音房</text>
|
||||
<text class="section-subtitle">{{ voiceRooms.length }}个房间在线</text>
|
||||
</view>
|
||||
<view class="voice-room-list">
|
||||
<view v-for="room in voiceRooms" :key="room.id" class="voice-room" @click="joinVoiceRoom(room)">
|
||||
<view class="room-header">
|
||||
<image class="room-cover" :src="room.cover" mode="aspectFill"></image>
|
||||
<view class="room-status">
|
||||
<uni-icons type="sound" size="16" color="#fff"></uni-icons>
|
||||
<text class="room-status-text">{{ room.onlineCount }}/{{ room.maxCount }}</text>
|
||||
</view>
|
||||
<text v-if="room.hot" class="room-hot">🔥{{ room.hot }}</text>
|
||||
</view>
|
||||
<view class="room-info">
|
||||
<text class="room-title">{{ room.title }}</text>
|
||||
<view class="room-tags">
|
||||
<text v-for="(tag, tagIndex) in room.tags" :key="tagIndex" class="room-tag">
|
||||
{{ tag }}
|
||||
</text>
|
||||
</view>
|
||||
<view class="room-users">
|
||||
<view v-for="(user, userIndex) in room.users" :key="userIndex"
|
||||
class="room-user-avatar">
|
||||
<image class="user-avatar-small" :src="user.avatar" mode="aspectFill"></image>
|
||||
</view>
|
||||
<text v-if="room.users.length < room.onlineCount" class="room-user-more">
|
||||
+{{ room.onlineCount - room.users.length }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 语音 -->
|
||||
<view v-if="tabActive == 1" class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">游戏中心</text>
|
||||
<text class="section-subtitle">边玩边交友</text>
|
||||
</view>
|
||||
<view class="game-list">
|
||||
<view v-for="game in games" :key="game.id" class="game-item" @click="playGame(game)">
|
||||
<view class="game-cover">
|
||||
<image class="game-image" :src="game.image" mode="aspectFill"></image>
|
||||
<view v-if="game.playing" class="game-playing">
|
||||
<uni-icons type="play" size="16" color="#fff"></uni-icons>
|
||||
<text class="playing-text">{{ formatNumber(game.playing) }}在线</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="game-info">
|
||||
<view class="game-header">
|
||||
<text class="game-title">{{ game.title }}</text>
|
||||
<view v-if="game.reward" class="game-reward">+{{ game.reward }}</view>
|
||||
</view>
|
||||
<text class="game-desc">{{ game.description }}</text>
|
||||
<view class="game-tags">
|
||||
<view v-for="(tag, tagIndex) in game.tags" :key="tagIndex" class="game-tag">
|
||||
{{ tag }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 畅聊 -->
|
||||
<view v-if="tabActive == 2" class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">游戏中心</text>
|
||||
<text class="section-subtitle">边玩边交友</text>
|
||||
</view>
|
||||
<view class="game-list">
|
||||
<view v-for="game in games" :key="game.id" class="game-item" @click="playGame(game)">
|
||||
<view class="game-cover">
|
||||
<image class="game-image" :src="game.image" mode="aspectFill"></image>
|
||||
<view v-if="game.playing" class="game-playing">
|
||||
<uni-icons type="play" size="16" color="#fff"></uni-icons>
|
||||
<text class="playing-text">{{ formatNumber(game.playing) }}在线</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="game-info">
|
||||
<view class="game-header">
|
||||
<text class="game-title">{{ game.title }}</text>
|
||||
<view v-if="game.reward" class="game-reward">+{{ game.reward }}</view>
|
||||
</view>
|
||||
<text class="game-desc">{{ game.description }}</text>
|
||||
<view class="game-tags">
|
||||
<view v-for="(tag, tagIndex) in game.tags" :key="tagIndex" class="game-tag">
|
||||
{{ tag }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 游戏 -->
|
||||
<view v-if="tabActive == 3" class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">游戏中心</text>
|
||||
<text class="section-subtitle">边玩边交友</text>
|
||||
</view>
|
||||
<view class="game-list">
|
||||
<view v-for="game in games" :key="game.id" class="game-item" @click="playGame(game)">
|
||||
<view class="game-cover">
|
||||
<image class="game-image" :src="game.image" mode="aspectFill"></image>
|
||||
<view v-if="game.playing" class="game-playing">
|
||||
<uni-icons type="play" size="16" color="#fff"></uni-icons>
|
||||
<text class="playing-text">{{ formatNumber(game.playing) }}在线</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="game-info">
|
||||
<view class="game-header">
|
||||
<text class="game-title">{{ game.title }}</text>
|
||||
<view v-if="game.reward" class="game-reward">+{{ game.reward }}</view>
|
||||
</view>
|
||||
<text class="game-desc">{{ game.description }}</text>
|
||||
<view class="game-tags">
|
||||
<view v-for="(tag, tagIndex) in game.tags" :key="tagIndex" class="game-tag">
|
||||
{{ tag }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<!-- <view class="load-status">
|
||||
<uni-load-more :status="loadStatus" />
|
||||
</view> -->
|
||||
</scroll-view>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { IBanner, IFilter, IVoiceRoom, IGame, IMenu } from '@/types/entType.uts'
|
||||
|
||||
// 响应式数据
|
||||
const unreadCount = ref(3)
|
||||
const refreshing = ref(false)
|
||||
const loading = ref(false)
|
||||
const noMore = ref(false)
|
||||
const page = ref(1)
|
||||
const tabActive = ref(0)
|
||||
|
||||
// 轮播图数据
|
||||
const banners = reactive<IBanner[]>([
|
||||
{
|
||||
id: 1,
|
||||
image: 'https://images.unsplash.com/photo-1511795409834-ef04bbd61622?ixlib=rb-4.0.3&auto=format&fit=crop&w=1200&q=80',
|
||||
tag: '新活动',
|
||||
url: '/pages/activity/activity'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
image: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?ixlib=rb-4.0.3&auto=format&fit=crop&w=1200&q=80',
|
||||
tag: 'VIP专区',
|
||||
url: '/pages/vip/vip'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
image: 'https://images.unsplash.com/photo-1529626455594-4ff0802cfb7e?ixlib=rb-4.0.3&auto=format&fit=crop&w=1200&q=80',
|
||||
tag: '热门推荐',
|
||||
url: '/pages/hot/hot'
|
||||
}
|
||||
] as IBanner[])
|
||||
|
||||
// 快速入口数据
|
||||
//语音 畅聊 游戏
|
||||
const quickEntrances = reactive<IMenu[]>([
|
||||
{ id: 2, icon: 'sound', text: '语音房', bgColor: '#5AC8FA', url: '/pages/voice-room/voice-room', hot: true },
|
||||
{ id: 3, icon: 'videocam', text: '视频', bgColor: '#FF9500', url: '/pages/video/video' },
|
||||
{ id: 4, icon: 'game', text: '游戏', bgColor: '#34C759', url: '/pages/game/game' },
|
||||
{ id: 5, icon: 'gift', text: '礼物', bgColor: '#AF52DE', url: '/pages/gift/gift' },
|
||||
{ id: 6, icon: 'location', text: '同城', bgColor: '#5856D6', url: '/pages/city/city' },
|
||||
{ id: 7, icon: 'star', text: '推荐', bgColor: '#FFCC00', url: '/pages/recommend/recommend' },
|
||||
{ id: 8, icon: 'fire', text: '热门', bgColor: '#FF3B30', url: '/pages/hot/hot' }
|
||||
] as IMenu[])
|
||||
|
||||
// 筛选数据
|
||||
const filters = reactive<IFilter[]>([
|
||||
{ id: 0, text: '附近', count: 128 },
|
||||
{ id: 1, text: '推荐', count: 56 },
|
||||
{ id: 2, text: '语音房', count: 12 },
|
||||
{ id: 3, text: '游戏', count: 8 }
|
||||
] as IFilter[])
|
||||
|
||||
// 语音房数据
|
||||
const voiceRooms = reactive<IVoiceRoom[]>([
|
||||
{
|
||||
id: 301,
|
||||
title: '情感聊天室',
|
||||
cover: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80',
|
||||
onlineCount: 8,
|
||||
maxCount: 10,
|
||||
hot: 125,
|
||||
tags: ['情感', '聊天', '治愈'],
|
||||
users: [
|
||||
{ id: 1, avatar: 'https://randomuser.me/api/portraits/women/44.jpg' },
|
||||
{ id: 2, avatar: 'https://randomuser.me/api/portraits/men/32.jpg' },
|
||||
{ id: 3, avatar: 'https://randomuser.me/api/portraits/women/68.jpg' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 302,
|
||||
title: '游戏开黑房',
|
||||
cover: 'https://images.unsplash.com/photo-1550745165-9bc0b252726f?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80',
|
||||
onlineCount: 6,
|
||||
maxCount: 8,
|
||||
hot: 89,
|
||||
tags: ['游戏', '开黑', '娱乐'],
|
||||
users: [
|
||||
{ id: 4, avatar: 'https://randomuser.me/api/portraits/men/75.jpg' },
|
||||
{ id: 5, avatar: 'https://randomuser.me/api/portraits/women/65.jpg' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 303,
|
||||
title: '音乐分享会',
|
||||
cover: 'https://images.unsplash.com/photo-1511379938547-c1f69419868d?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80',
|
||||
onlineCount: 4,
|
||||
maxCount: 6,
|
||||
hot: 67,
|
||||
tags: ['音乐', '分享', '放松'],
|
||||
users: [
|
||||
{ id: 6, avatar: 'https://randomuser.me/api/portraits/women/23.jpg' },
|
||||
{ id: 7, avatar: 'https://randomuser.me/api/portraits/men/22.jpg' }
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
// 游戏数据
|
||||
const games = reactive<IGame[]>([
|
||||
{
|
||||
id: 401,
|
||||
title: '你画我猜',
|
||||
image: 'https://images.unsplash.com/photo-1511512578047-dfb367046420?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80',
|
||||
description: '经典互动游戏,发挥你的想象力',
|
||||
playing: 1250,
|
||||
reward: 50,
|
||||
tags: ['互动', '休闲', '社交']
|
||||
},
|
||||
{
|
||||
id: 402,
|
||||
title: '真心话大冒险',
|
||||
image: 'https://images.unsplash.com/photo-1535223289827-42f1e9919769?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80',
|
||||
description: '增进了解的经典游戏',
|
||||
playing: 890,
|
||||
reward: 30,
|
||||
tags: ['互动', '冒险', '社交']
|
||||
},
|
||||
{
|
||||
id: 403,
|
||||
title: '狼人杀',
|
||||
image: 'https://images.unsplash.com/photo-1542751371-adc38448a05e?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80',
|
||||
description: '考验推理和口才的社交游戏',
|
||||
playing: 1560,
|
||||
reward: 80,
|
||||
tags: ['推理', '策略', '社交']
|
||||
}
|
||||
])
|
||||
|
||||
// 计算属性
|
||||
const loadStatus = computed(() => {
|
||||
if (loading.value) return 'loading'
|
||||
if (noMore.value) return 'noMore'
|
||||
return 'more'
|
||||
})
|
||||
|
||||
// 加载数据
|
||||
const loadData = () => {
|
||||
console.log('加载数据...')
|
||||
// 这里可以调用API加载数据
|
||||
}
|
||||
|
||||
// 页面加载
|
||||
onMounted(() => {
|
||||
console.log('娱乐交友页面加载完成')
|
||||
loadData()
|
||||
})
|
||||
|
||||
// 格式化距离
|
||||
const formatDistance = (distance : number) : string => {
|
||||
if (distance < 1) {
|
||||
return '<1km'
|
||||
} else if (distance < 10) {
|
||||
return `${distance.toFixed(1)}km`
|
||||
} else {
|
||||
return `${Math.floor(distance)}km`
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化数字
|
||||
const formatNumber = (num : number) : string => {
|
||||
if (num >= 10000) {
|
||||
return (num / 10000).toFixed(1) + 'w'
|
||||
} else if (num >= 1000) {
|
||||
return (num / 1000).toFixed(1) + 'k'
|
||||
}
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
// 点击搜索
|
||||
const onSearchClick = () => {
|
||||
console.log('点击搜索')
|
||||
uni.navigateTo({
|
||||
url: '/pages/search/search'
|
||||
})
|
||||
}
|
||||
|
||||
// 点击消息
|
||||
const onMessageClick = () => {
|
||||
console.log('点击消息')
|
||||
unreadCount.value = 0
|
||||
uni.navigateTo({
|
||||
url: '/pages/message/message'
|
||||
})
|
||||
}
|
||||
|
||||
// 点击轮播图
|
||||
const onBannerClick = (banner : IBanner) => {
|
||||
console.log('点击轮播图:', banner.id)
|
||||
uni.navigateTo({
|
||||
url: banner.url
|
||||
})
|
||||
}
|
||||
|
||||
// 点击快速入口
|
||||
const onEntranceClick = (entrance : IMenu) => {
|
||||
console.log('点击快速入口:', entrance.text)
|
||||
if (entrance.url != null) {
|
||||
uni.navigateTo({
|
||||
url: entrance.url
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: `进入${entrance.text}`,
|
||||
icon: 'none',
|
||||
duration: 1500
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 切换筛选
|
||||
const switchTab = (index : number) => {
|
||||
console.log('切换筛选:', filters[index].text)
|
||||
tabActive.value = index
|
||||
page.value = 1
|
||||
noMore.value = false
|
||||
// 这里应该重新加载对应筛选的数据
|
||||
}
|
||||
|
||||
// 显示更多筛选
|
||||
const showMoreFilters = () => {
|
||||
console.log('显示更多筛选')
|
||||
uni.showActionSheet({
|
||||
itemList: ['最新', '热门', '在线', 'VIP'],
|
||||
success: (res) => {
|
||||
console.log('选择了筛选:', res.tapIndex)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 加入语音房
|
||||
const joinVoiceRoom = (room : IVoiceRoom) => {
|
||||
console.log('加入语音房:', room.title)
|
||||
uni.showModal({
|
||||
title: '加入语音房',
|
||||
content: `确定要加入"${room.title}"吗?`,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showToast({
|
||||
title: '正在加入房间...',
|
||||
icon: 'none'
|
||||
})
|
||||
// 实际应该跳转到语音房页面
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 玩游戏
|
||||
const playGame = (game : IGame) => {
|
||||
console.log('玩游戏:', game.title)
|
||||
uni.showToast({
|
||||
title: `开始${game.title}`,
|
||||
icon: 'none',
|
||||
duration: 1500
|
||||
})
|
||||
// 实际应该跳转到游戏页面
|
||||
}
|
||||
|
||||
// 下拉刷新
|
||||
const onRefresh = () => {
|
||||
console.log('下拉刷新')
|
||||
refreshing.value = true
|
||||
|
||||
setTimeout(() => {
|
||||
console.log('刷新完成')
|
||||
refreshing.value = false
|
||||
page.value = 1
|
||||
noMore.value = false
|
||||
uni.showToast({
|
||||
title: '刷新成功',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
}, 1500)
|
||||
}
|
||||
|
||||
// 上拉加载更多
|
||||
const onLoadMore = () => {
|
||||
console.log('上拉加载更多')
|
||||
if (loading.value || noMore.value) return
|
||||
|
||||
loading.value = true
|
||||
|
||||
setTimeout(() => {
|
||||
page.value++
|
||||
|
||||
// 模拟没有更多数据
|
||||
if (page.value >= 3) {
|
||||
noMore.value = true
|
||||
}
|
||||
|
||||
loading.value = false
|
||||
uni.showToast({
|
||||
title: '加载成功',
|
||||
icon: 'none',
|
||||
duration: 1500
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 语音通话
|
||||
const onVoiceCall = () => {
|
||||
console.log('语音通话')
|
||||
uni.showActionSheet({
|
||||
itemList: ['随机语音', '好友语音', '创建语音房'],
|
||||
success: (res) => {
|
||||
const actions = ['randomVoice', 'friendVoice', 'createRoom']
|
||||
const action = actions[res.tapIndex]
|
||||
console.log('选择了:', action)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 视频通话
|
||||
const onVideoCall = () => {
|
||||
console.log('视频通话')
|
||||
uni.showModal({
|
||||
title: '视频通话',
|
||||
content: '开始视频通话需要消耗流量,是否继续?',
|
||||
confirmText: '开始通话',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.navigateTo({
|
||||
url: '/pages/video-call/video-call'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.header-right {
|
||||
flex-flow: row nowrap;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
position: relative;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
.message-badge {
|
||||
position: absolute;
|
||||
top: -5rpx;
|
||||
right: -5rpx;
|
||||
min-width: 32rpx;
|
||||
height: 32rpx;
|
||||
line-height: 32rpx;
|
||||
text-align: center;
|
||||
background-color: #FF3B30;
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
border-radius: 16rpx;
|
||||
padding: 0 8rpx;
|
||||
}
|
||||
|
||||
/* 轮播图 */
|
||||
.banner-swiper {
|
||||
height: 300rpx;
|
||||
margin: 10rpx;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.banner-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.banner-tag {
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
right: 20rpx;
|
||||
padding: 8rpx 20rpx;
|
||||
background-color: rgba(255, 107, 107, 0.9);
|
||||
color: #fff;
|
||||
font-size: 24rpx;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 筛选栏 */
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-flow: row nowrap;
|
||||
padding: 10rpx 20rpx;
|
||||
background-color: #fff;
|
||||
margin: 10rpx;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 5rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.filter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 15rpx 30rpx;
|
||||
margin-right: 20rpx;
|
||||
border-radius: 40rpx;
|
||||
background-color: #f8f8f8;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.filter-item.active {
|
||||
background: linear-gradient(135deg, #FF6B6B 0%, #FF8E53 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.filter-item.active .filter-count {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.filter-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.filter-count {
|
||||
margin-left: 10rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
background-color: #e0e0e0;
|
||||
color: #666;
|
||||
font-size: 20rpx;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.filter-more {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
background-color: #f8f8f8;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* 用户列表 */
|
||||
.user-list {
|
||||
flex: 1;
|
||||
padding: 10rpx 20rpx;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.section {
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 5rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 语音房列表 */
|
||||
.voice-room-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.voice-room {
|
||||
margin-bottom: 15rpx;
|
||||
background-color: #f8f8f8;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.voice-room:active {
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
.room-header {
|
||||
position: relative;
|
||||
height: 200rpx;
|
||||
}
|
||||
|
||||
.room-cover {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.room-status {
|
||||
position: absolute;
|
||||
bottom: 20rpx;
|
||||
right: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8rpx 20rpx;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.room-status .uni-icons {
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.room-hot {
|
||||
position: absolute;
|
||||
top: 20rpx;
|
||||
left: 20rpx;
|
||||
padding: 8rpx 20rpx;
|
||||
background-color: rgba(255, 107, 107, 0.9);
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.room-info {
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.room-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 15rpx;
|
||||
}
|
||||
|
||||
.room-tags {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.room-tag {
|
||||
padding: 8rpx 20rpx;
|
||||
background-color: #e0e0e0;
|
||||
color: #666;
|
||||
font-size: 20rpx;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.room-users {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.room-user-avatar {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
border: 4rpx solid #fff;
|
||||
margin-right: -10rpx;
|
||||
}
|
||||
|
||||
.room-user-avatar:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.user-avatar-small {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.room-user-more {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
line-height: 60rpx;
|
||||
text-align: center;
|
||||
background-color: #f0f0f0;
|
||||
color: #999;
|
||||
font-size: 20rpx;
|
||||
border-radius: 50%;
|
||||
border: 4rpx solid #fff;
|
||||
margin-left: 10rpx;
|
||||
}
|
||||
|
||||
/* 游戏列表 */
|
||||
.game-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
}
|
||||
|
||||
.game-item {
|
||||
display: flex;
|
||||
background-color: #f8f8f8;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
transition: transform 0.3s ease;
|
||||
margin-top: 15rpx;
|
||||
}
|
||||
|
||||
.game-item:active {
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
.game-cover {
|
||||
position: relative;
|
||||
height: 200rpx;
|
||||
}
|
||||
|
||||
.game-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.game-playing {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10rpx;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
}
|
||||
|
||||
.game-playing .uni-icons {
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
|
||||
.game-info {
|
||||
flex: 1;
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.game-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-flow: row wrap;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.game-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.game-reward {
|
||||
padding: 8rpx 20rpx;
|
||||
background-color: #FF9500;
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.game-desc {
|
||||
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
margin-bottom: 15rpx;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.game-tags {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.game-tag {
|
||||
padding: 8rpx 20rpx;
|
||||
background-color: #e0e0e0;
|
||||
color: #666;
|
||||
font-size: 20rpx;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.load-status {
|
||||
padding: 30rpx 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,305 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="nav-bar">
|
||||
<view class="nav-back" @click="goBack">
|
||||
<uni-icons type="left" size="22" color="#333"></uni-icons>
|
||||
</view>
|
||||
<text class="nav-title">我的好友</text>
|
||||
<view class="nav-right"></view>
|
||||
</view>
|
||||
|
||||
<view class="add-bar">
|
||||
<input class="add-input" v-model="newUid" type="number" placeholder="输入对方用户ID添加好友" />
|
||||
<button class="add-btn" @click="sendRequest">添加</button>
|
||||
</view>
|
||||
|
||||
<scroll-view class="tabs" direction="horizontal" :show-scrollbar="false">
|
||||
<text class="tab" :class="{ active: tab == 0 }" @click="tab = 0">好友 ({{ friends.length }})</text>
|
||||
<text class="tab" :class="{ active: tab == 1 }" @click="tab = 1">请求 ({{ requests.length }})</text>
|
||||
</scroll-view>
|
||||
|
||||
<list-view class="list" v-if="tab == 0">
|
||||
<list-item v-for="(f, i) in friends" :key="f.uid" class="item">
|
||||
<image class="avatar" :src="f.avatar" mode="aspectFill"></image>
|
||||
<view class="info">
|
||||
<text class="name">{{ f.nickname }}</text>
|
||||
<text class="meta">{{ f.remark && f.remark.length > 0 ? f.remark : '好友' }}</text>
|
||||
</view>
|
||||
<button class="del-btn" @click="removeFriend(f.friend_id)">删除</button>
|
||||
</list-item>
|
||||
<view v-if="friends.length == 0" class="empty">
|
||||
<text>还没有好友,去真人匹配认识一下吧</text>
|
||||
</view>
|
||||
</list-view>
|
||||
|
||||
<list-view class="list" v-else>
|
||||
<list-item v-for="(r, i) in requests" :key="r.uid" class="item">
|
||||
<image class="avatar" :src="r.avatar" mode="aspectFill"></image>
|
||||
<view class="info">
|
||||
<text class="name">{{ r.nickname }}</text>
|
||||
<text class="meta">请求添加你为好友</text>
|
||||
</view>
|
||||
<button class="ok-btn" @click="respond(r.friend_id, 1)">接受</button>
|
||||
<button class="del-btn" @click="respond(r.friend_id, 2)">拒绝</button>
|
||||
</list-item>
|
||||
<view v-if="requests.length == 0" class="empty">
|
||||
<text>暂无新的好友请求</text>
|
||||
</view>
|
||||
</list-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { Api } from '@/common/api-service.uts'
|
||||
|
||||
const friends = ref<UTSJSONObject[]>([])
|
||||
const requests = ref<UTSJSONObject[]>([])
|
||||
const tab = ref(0)
|
||||
const newUid = ref('')
|
||||
|
||||
const loadFriends = async () => {
|
||||
try {
|
||||
const data = await Api.friend.list({})
|
||||
const list = data.getJSONArray('list')
|
||||
const arr : UTSJSONObject[] = []
|
||||
if (list != null) {
|
||||
for (let i = 0; i < list.size(); i++) {
|
||||
const it = list.get(i) as UTSJSONObject
|
||||
if (it != null) {
|
||||
arr.push({
|
||||
uid: it.getNumber('uid') ?? 0,
|
||||
nickname: it.getString('nickname') ?? '',
|
||||
avatar: it.getString('avatar') ?? '',
|
||||
remark: it.getString('remark') ?? '',
|
||||
friend_id: it.getNumber('friend_id') ?? 0
|
||||
} as UTSJSONObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
friends.value = arr
|
||||
} catch (e) {
|
||||
console.warn('加载好友列表失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
const loadRequests = async () => {
|
||||
try {
|
||||
const data = await Api.friend.requests({})
|
||||
const list = data.getJSONArray('list')
|
||||
const arr : UTSJSONObject[] = []
|
||||
if (list != null) {
|
||||
for (let i = 0; i < list.size(); i++) {
|
||||
const it = list.get(i) as UTSJSONObject
|
||||
if (it != null) {
|
||||
arr.push({
|
||||
uid: it.getNumber('uid') ?? 0,
|
||||
nickname: it.getString('nickname') ?? '',
|
||||
avatar: it.getString('avatar') ?? '',
|
||||
friend_id: it.getNumber('friend_id') ?? 0
|
||||
} as UTSJSONObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
requests.value = arr
|
||||
} catch (e) {
|
||||
console.warn('加载好友请求失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
const sendRequest = () => {
|
||||
const fid = parseInt(newUid.value)
|
||||
if (!fid || fid <= 0) {
|
||||
uni.showToast({ title: '请输入有效ID', icon: 'none' })
|
||||
return
|
||||
}
|
||||
Api.friend.create({ fid: fid }).then(() => {
|
||||
uni.showToast({ title: '请求已发送', icon: 'success' })
|
||||
newUid.value = ''
|
||||
loadRequests()
|
||||
}).catch((err : any) => {
|
||||
uni.showToast({ title: err?.message ?? '失败', icon: 'none' })
|
||||
})
|
||||
}
|
||||
|
||||
const respond = (id : number, action : number) => {
|
||||
Api.friend.respond(id, action).then(() => {
|
||||
uni.showToast({ title: action == 1 ? '已添加' : '已拒绝', icon: 'success' })
|
||||
loadRequests()
|
||||
loadFriends()
|
||||
}).catch((err : any) => {
|
||||
uni.showToast({ title: err?.message ?? '失败', icon: 'none' })
|
||||
})
|
||||
}
|
||||
|
||||
const removeFriend = (id : number) => {
|
||||
Api.friend.delete(id).then(() => {
|
||||
uni.showToast({ title: '已删除', icon: 'success' })
|
||||
loadFriends()
|
||||
}).catch((err : any) => {
|
||||
uni.showToast({ title: err?.message ?? '失败', icon: 'none' })
|
||||
})
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadFriends()
|
||||
loadRequests()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #f6f6f6;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 20rpx 24rpx;
|
||||
padding-top: 60rpx;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
width: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 34rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.nav-right {
|
||||
width: 60rpx;
|
||||
}
|
||||
|
||||
.add-bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding: 20rpx 24rpx;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.add-input {
|
||||
flex: 1;
|
||||
background-color: #f2f2f2;
|
||||
border-radius: 40rpx;
|
||||
padding: 18rpx 28rpx;
|
||||
font-size: 26rpx;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
background: linear-gradient(to bottom right, #FF6B6B, #FF8E53);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 40rpx;
|
||||
font-size: 26rpx;
|
||||
padding: 0 36rpx;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background-color: #fff;
|
||||
padding: 10rpx 24rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.tab {
|
||||
font-size: 28rpx;
|
||||
color: #888;
|
||||
margin-right: 40rpx;
|
||||
padding: 12rpx 0;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: #FF6B6B;
|
||||
font-weight: bold;
|
||||
border-bottom: 4rpx solid #FF6B6B;
|
||||
}
|
||||
|
||||
.list {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background-color: #fff;
|
||||
margin: 16rpx 24rpx;
|
||||
padding: 24rpx;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 24rpx;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.meta {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.ok-btn {
|
||||
background-color: #4CD964;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 40rpx;
|
||||
font-size: 24rpx;
|
||||
padding: 8rpx 24rpx;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.del-btn {
|
||||
background-color: #ff3b30;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 40rpx;
|
||||
font-size: 24rpx;
|
||||
padding: 8rpx 24rpx;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 80rpx 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.empty text {
|
||||
font-size: 26rpx;
|
||||
color: #aaa;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="nav-bar">
|
||||
<view class="nav-back" @click="goBack">
|
||||
<uni-icons type="left" size="22" color="#333"></uni-icons>
|
||||
</view>
|
||||
<text class="nav-title">免费群聊</text>
|
||||
<view class="nav-right"></view>
|
||||
</view>
|
||||
|
||||
<view class="hero">
|
||||
<text class="hero-emoji">👥</text>
|
||||
<text class="hero-title">免费群聊广场</text>
|
||||
<text class="hero-sub">加入兴趣群,和同好畅聊不停</text>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<text class="section-title">推荐群聊</text>
|
||||
<view class="cell" v-for="(item, i) in groups" :key="i" @click="joinGroup(item)">
|
||||
<view class="avatar">{{ item.tag }}</view>
|
||||
<view class="cell-info">
|
||||
<text class="cell-name">{{ item.name }}</text>
|
||||
<text class="cell-sub">{{ item.member }}人 · {{ item.desc }}</text>
|
||||
</view>
|
||||
<button class="join-btn">加入</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tip">
|
||||
<text>创建群聊、群管理功能即将上线~</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const groups = ref([
|
||||
{ name: '同城脱单互助', tag: '脱', member: 326, desc: '缘分从这里开始' },
|
||||
{ name: '深夜树洞', tag: '夜', member: 198, desc: '倾诉你的小秘密' },
|
||||
{ name: '游戏开黑交友', tag: '游', member: 512, desc: '边玩边聊' }
|
||||
] as UTSJSONObject[])
|
||||
|
||||
const joinGroup = (item : UTSJSONObject) => {
|
||||
uni.showToast({ title: '申请加入「' + (item.getString('name') ?? '') + '」', icon: 'none' })
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #f6f6f6;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 20rpx 24rpx;
|
||||
padding-top: 60rpx;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
width: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 34rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.nav-right {
|
||||
width: 60rpx;
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 60rpx 0 40rpx;
|
||||
background: linear-gradient(to bottom right, #4CD964, #34C759);
|
||||
}
|
||||
|
||||
.hero-emoji {
|
||||
font-size: 90rpx;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.hero-sub {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.section {
|
||||
background-color: #fff;
|
||||
margin: 24rpx;
|
||||
border-radius: 20rpx;
|
||||
padding: 10rpx 24rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
padding: 20rpx 0 10rpx;
|
||||
}
|
||||
|
||||
.cell {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 24rpx 0;
|
||||
border-top: 1rpx solid #f2f2f2;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 20rpx;
|
||||
background-color: #4CD964;
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 24rpx;
|
||||
}
|
||||
|
||||
.cell-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.cell-name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.cell-sub {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
|
||||
.join-btn {
|
||||
background-color: #4CD964;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 40rpx;
|
||||
font-size: 24rpx;
|
||||
padding: 8rpx 28rpx;
|
||||
}
|
||||
|
||||
.tip {
|
||||
text-align: center;
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.tip text {
|
||||
font-size: 24rpx;
|
||||
color: #aaa;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,113 @@
|
||||
<template>
|
||||
<view class="guide-container">
|
||||
<!-- 轮播图形式的引导页 -->
|
||||
<swiper class="swiper" :indicator-dots="true" :autoplay="false" @change="onChange">
|
||||
<swiper-item v-for="(item, index) in guideList" :key="index">
|
||||
<image :src="item.image" mode="aspectFill" class="guide-image" />
|
||||
<text class="guide-text">{{ item.text }}</text>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<!-- 最后一张显示进入按钮 -->
|
||||
<button v-if="currentIndex === guideList.length - 1" class="enter-btn" @click="enterApp">
|
||||
立即体验
|
||||
</button>
|
||||
<!-- 跳过按钮 -->
|
||||
<text class="skip-btn" @click="enterApp">跳过</text>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import userState, { isLoggedIn } from '@/stores/user.uts'
|
||||
import { appState } from '@/stores/app.uts'
|
||||
import messageService from '@/services/websocket/message-service.uts'
|
||||
// 响应式数据
|
||||
const currentIndex = ref<number>(0)
|
||||
type IList = { image : string, text : string }
|
||||
// 引导页数据
|
||||
const guideList = ref<Array<IList>>([
|
||||
{ image: '/static/guide1.png', text: '功能介绍一' },
|
||||
{ image: '/static/guide2.png', text: '功能介绍二' },
|
||||
{ image: '/static/guide3.png', text: '功能介绍三' }
|
||||
])
|
||||
|
||||
const isLogin = computed(() : boolean => {
|
||||
return isLoggedIn.value
|
||||
})
|
||||
|
||||
// 轮播切换事件
|
||||
const onChange = (e : UniSwiperChangeEvent) => {
|
||||
currentIndex.value = e.detail.current
|
||||
}
|
||||
// 进入应用
|
||||
const enterApp = () => {
|
||||
uni.setStorageSync('hasGuide', true)
|
||||
if (isLoggedIn.value) {
|
||||
// 连接实时消息通道(使用后端 appConf.socketUrl 与真实 token)
|
||||
messageService.connectIfNeeded(appState.systemConf?.socketUrl ?? null)
|
||||
uni.switchTab({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
} else {
|
||||
uni.redirectTo({
|
||||
url: '/pages/login/index'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 页面生命周期 - 检查是否首次启动
|
||||
onLoad(() => {
|
||||
const hasGuide = uni.getStorageSync('hasGuide') ?? null
|
||||
if (hasGuide != null) {
|
||||
// 非首次启动,直接跳转首页
|
||||
enterApp()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.guide-container {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.swiper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.guide-image {
|
||||
width: 100%;
|
||||
height: 80%;
|
||||
}
|
||||
|
||||
.guide-text {
|
||||
text-align: center;
|
||||
padding: 30rpx;
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.enter-btn {
|
||||
position: absolute;
|
||||
bottom: 200rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 300rpx;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
background: #007aff;
|
||||
color: #fff;
|
||||
border-radius: 40rpx;
|
||||
}
|
||||
|
||||
.skip-btn {
|
||||
position: absolute;
|
||||
top: 80rpx;
|
||||
right: 40rpx;
|
||||
padding: 10rpx 30rpx;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
color: #fff;
|
||||
border-radius: 30rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,623 @@
|
||||
<template>
|
||||
<!-- #ifdef APP -->
|
||||
<scroll-view style="flex: 1;" enable-back-to-top="true">
|
||||
<!-- #endif -->
|
||||
<view class="page">
|
||||
<view class="-status-bar"></view>
|
||||
<!-- 功能模块入口(彩色区块) -->
|
||||
<scroll-view class="function-modules" direction="horizontal" :show-scrollbar="false">
|
||||
<view v-for="(module, index) in functionModules" :key="index" class="module-item"
|
||||
:style="{ backgroundColor: module.bgColor }" @click="navigateToModule(module.path)">
|
||||
<image class="module-icon" :src="module.icon" mode="aspectFit"></image>
|
||||
<text class="module-name">{{ module.name }}</text>
|
||||
<text class="module-desc">{{ module.desc }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<!-- 同城/附近切换栏 -->
|
||||
<view class="tabsbox" style="justify-content: space-between;">
|
||||
<text v-for="(tab, index) in tabs" :key="index" class="tabsbox-txt" style="flex:1"
|
||||
:class="{ ' tabsbox-active-txt': activeTab === index }" @click="activeTab = index">
|
||||
{{ tab }}
|
||||
</text>
|
||||
</view>
|
||||
<list-view style="flex: 1;">
|
||||
<list-item v-for="(user, index) in filteredUsers" :key="user.id" @click="viewUserProfile(user.id)">
|
||||
<view class="chat-item">
|
||||
<view class="chat-item-left">
|
||||
<image class="avatar-square" :src="user.avatar" mode="aspectFill"></image>
|
||||
</view>
|
||||
<view class="chat-item-center" style="flex-direction: column; align-items: flex-start;">
|
||||
<view style="flex-flow: row;">
|
||||
<text class="user-name">{{ user.nickname }}</text>
|
||||
<view class="user-badges">
|
||||
<text v-if="user.vip" class="user-badge" style="background-color: #FFCC00;">V</text>
|
||||
<text v-if="user.verified" class="user-badge" style="background-color: #09b400;">
|
||||
R</text>
|
||||
<text v-if="user.live" class="user-badge"
|
||||
style="background-color: #FF3B30;">L</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="user-info">
|
||||
<text v-if="user.location" class="user-info-i"> {{ user.location }} </text>
|
||||
<text v-if="user.age" class="user-info-i"> {{ user.age }}岁 </text>
|
||||
<text v-if="user.height" class="user-info-i"> {{ user.height }}cm </text>
|
||||
<text v-if="user.profession" class="user-info-i"> {{ user.profession }} </text>
|
||||
</view>
|
||||
<text v-if="user.signature" class="user-signature">{{ user.signature }}</text>
|
||||
</view>
|
||||
<view class="chat-item-right">
|
||||
<button class="user-chat-btn" @click.stop="startChat(user.id)">搭讪</button>
|
||||
</view>
|
||||
</view>
|
||||
</list-item>
|
||||
</list-view>
|
||||
<!-- 你可能感兴趣的人(右下角提示框) -->
|
||||
<view class="interest-tip" @click="showInterestUsers">
|
||||
<text class="tip-text">你可能感兴趣的人</text>
|
||||
<uni-icons type="arrowright" size="16" color="#fff"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
<!-- #ifdef APP -->
|
||||
</scroll-view>
|
||||
<!-- #endif -->
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { IGrid, IUser } from '@/types/index.uts'
|
||||
|
||||
import { Api } from "@/common/api-service.uts"
|
||||
// 功能模块数据(参考第1张图彩色模块)
|
||||
const functionModules = reactive<IGrid[]>([
|
||||
{
|
||||
name: '心动聊天',
|
||||
desc: '玩游戏交朋友',
|
||||
bgColor: '#FF6B6B',
|
||||
icon: '/static/icons/chatroom.png',
|
||||
path: '/pages/chatroom/index'
|
||||
},
|
||||
{
|
||||
name: '免费群聊',
|
||||
desc: '多人实时聊天',
|
||||
bgColor: '#4CD964',
|
||||
icon: '/static/icons/group.png',
|
||||
path: '/pages/group-chat/index'
|
||||
},
|
||||
{
|
||||
name: '神兽宠物',
|
||||
desc: '养宠物交朋友',
|
||||
bgColor: '#5AC8FA',
|
||||
icon: '/static/icons/pet.png',
|
||||
path: '/pages/pet-social/index'
|
||||
},
|
||||
{
|
||||
name: '真人匹配',
|
||||
desc: '真人线上奔现',
|
||||
bgColor: '#FFCC00',
|
||||
icon: '/static/icons/match.png',
|
||||
path: '/pages/real-match/index'
|
||||
},
|
||||
{
|
||||
name: '我的好友',
|
||||
desc: '好友与请求',
|
||||
bgColor: '#9B59B6',
|
||||
icon: '/static/logo.png',
|
||||
path: '/pages/friend/index'
|
||||
}
|
||||
] as IGrid[])
|
||||
|
||||
// 同城/附近切换标签
|
||||
const tabs = ref(['同城', '附近'])
|
||||
const activeTab = ref(0) // 0:同城, 1:附近
|
||||
|
||||
// 在线匹配数据
|
||||
const matchInfo = reactive({
|
||||
distance: '25.05km',
|
||||
onlineCount: 473
|
||||
})
|
||||
|
||||
// 用户卡片数据(整合三张图用户信息)
|
||||
const users = reactive<IUser[]>([
|
||||
{
|
||||
id: 1,
|
||||
nickname: '萌小依',
|
||||
realname: true,
|
||||
location: '南充',
|
||||
age: 21,
|
||||
height: 170,
|
||||
profession: '研发',
|
||||
signature: '我是个坚定的缘分信徒。相信在茫茫人海中...',
|
||||
avatar: 'https://randomuser.me/api/portraits/women/44.jpg',
|
||||
vip: false,
|
||||
verified: true,
|
||||
live: false
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
nickname: '董小姐',
|
||||
realname: true,
|
||||
location: '德阳',
|
||||
age: 26,
|
||||
height: 163,
|
||||
profession: '',
|
||||
signature: '人生苦短,三万天而已,烂事就得...',
|
||||
avatar: 'https://randomuser.me/api/portraits/women/68.jpg',
|
||||
vip: false,
|
||||
verified: true,
|
||||
live: false
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
nickname: '贝儿妹妹',
|
||||
realname: true,
|
||||
location: '沈阳',
|
||||
age: 27,
|
||||
height: 163,
|
||||
profession: '',
|
||||
signature: '',
|
||||
avatar: 'https://randomuser.me/api/portraits/women/23.jpg',
|
||||
vip: false,
|
||||
verified: true,
|
||||
live: true // 直播中
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
nickname: '积极的红薯',
|
||||
realname: true,
|
||||
location: '南充',
|
||||
age: 31,
|
||||
height: 167,
|
||||
profession: '',
|
||||
signature: '',
|
||||
avatar: 'https://randomuser.me/api/portraits/men/32.jpg',
|
||||
vip: true,
|
||||
verified: true,
|
||||
live: false
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
nickname: '萌小依',
|
||||
realname: true,
|
||||
location: '南充',
|
||||
age: 21,
|
||||
height: 170,
|
||||
profession: '研发',
|
||||
signature: '我是个坚定的缘分信徒。相信在茫茫人海中...',
|
||||
avatar: 'https://randomuser.me/api/portraits/women/44.jpg',
|
||||
vip: false,
|
||||
verified: true,
|
||||
live: false
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
nickname: '董小姐',
|
||||
realname: true,
|
||||
location: '德阳',
|
||||
age: 26,
|
||||
height: 163,
|
||||
profession: '',
|
||||
signature: '人生苦短,三万天而已,烂事就得...',
|
||||
avatar: 'https://randomuser.me/api/portraits/women/68.jpg',
|
||||
vip: false,
|
||||
verified: true,
|
||||
live: false
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
nickname: '贝儿妹妹',
|
||||
realname: true,
|
||||
location: '沈阳',
|
||||
age: 27,
|
||||
height: 163,
|
||||
profession: '',
|
||||
signature: '',
|
||||
avatar: 'https://randomuser.me/api/portraits/women/23.jpg',
|
||||
vip: false,
|
||||
verified: true,
|
||||
live: true // 直播中
|
||||
},
|
||||
{
|
||||
id: 8,
|
||||
nickname: '积极的红薯',
|
||||
realname: true,
|
||||
location: '南充',
|
||||
age: 31,
|
||||
height: 167,
|
||||
profession: '',
|
||||
signature: '',
|
||||
avatar: 'https://randomuser.me/api/portraits/men/32.jpg',
|
||||
vip: true,
|
||||
verified: true,
|
||||
live: false
|
||||
},
|
||||
{
|
||||
id: 9,
|
||||
nickname: '萌小依',
|
||||
realname: true,
|
||||
location: '南充',
|
||||
age: 21,
|
||||
height: 170,
|
||||
profession: '研发',
|
||||
signature: '我是个坚定的缘分信徒。相信在茫茫人海中...',
|
||||
avatar: 'https://randomuser.me/api/portraits/women/44.jpg',
|
||||
vip: false,
|
||||
verified: true,
|
||||
live: false
|
||||
},
|
||||
{
|
||||
id: 10,
|
||||
nickname: '董小姐',
|
||||
realname: true,
|
||||
location: '德阳',
|
||||
age: 26,
|
||||
height: 163,
|
||||
profession: '',
|
||||
signature: '人生苦短,三万天而已,烂事就得...',
|
||||
avatar: 'https://randomuser.me/api/portraits/women/68.jpg',
|
||||
vip: false,
|
||||
verified: true,
|
||||
live: false
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
nickname: '贝儿妹妹',
|
||||
realname: true,
|
||||
location: '沈阳',
|
||||
age: 27,
|
||||
height: 163,
|
||||
profession: '',
|
||||
signature: '',
|
||||
avatar: 'https://randomuser.me/api/portraits/women/23.jpg',
|
||||
vip: false,
|
||||
verified: true,
|
||||
live: true // 直播中
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
nickname: '积极的红薯',
|
||||
realname: true,
|
||||
location: '南充',
|
||||
age: 31,
|
||||
height: 167,
|
||||
profession: '',
|
||||
signature: '',
|
||||
avatar: 'https://randomuser.me/api/portraits/men/32.jpg',
|
||||
vip: true,
|
||||
verified: true,
|
||||
live: false
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
nickname: '积极的红薯',
|
||||
realname: true,
|
||||
location: '南充',
|
||||
age: 31,
|
||||
height: 167,
|
||||
profession: '',
|
||||
signature: '',
|
||||
avatar: 'https://randomuser.me/api/portraits/men/32.jpg',
|
||||
vip: true,
|
||||
verified: true,
|
||||
live: false
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
nickname: '积极的红薯',
|
||||
realname: true,
|
||||
location: '南充',
|
||||
age: 31,
|
||||
height: 167,
|
||||
profession: '',
|
||||
signature: '',
|
||||
avatar: 'https://randomuser.me/api/portraits/men/32.jpg',
|
||||
vip: true,
|
||||
verified: true,
|
||||
live: false
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
nickname: '积极的红薯',
|
||||
realname: true,
|
||||
location: '南充',
|
||||
age: 31,
|
||||
height: 167,
|
||||
profession: '',
|
||||
signature: '',
|
||||
avatar: 'https://randomuser.me/api/portraits/men/32.jpg',
|
||||
vip: true,
|
||||
verified: true,
|
||||
live: false
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
nickname: '积极的红薯',
|
||||
realname: true,
|
||||
location: '南充',
|
||||
age: 31,
|
||||
height: 167,
|
||||
profession: '',
|
||||
signature: '',
|
||||
avatar: 'https://randomuser.me/api/portraits/men/32.jpg',
|
||||
vip: true,
|
||||
verified: true,
|
||||
live: false
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
nickname: '积极的红薯',
|
||||
realname: true,
|
||||
location: '南充',
|
||||
age: 31,
|
||||
height: 167,
|
||||
profession: '',
|
||||
signature: '',
|
||||
avatar: 'https://randomuser.me/api/portraits/men/32.jpg',
|
||||
vip: true,
|
||||
verified: true,
|
||||
live: false
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
nickname: '积极的红薯',
|
||||
realname: true,
|
||||
location: '南充',
|
||||
age: 31,
|
||||
height: 167,
|
||||
profession: '',
|
||||
signature: '',
|
||||
avatar: 'https://randomuser.me/api/portraits/men/32.jpg',
|
||||
vip: true,
|
||||
verified: true,
|
||||
live: false
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
nickname: '积极的红薯',
|
||||
realname: true,
|
||||
location: '南充',
|
||||
age: 31,
|
||||
height: 167,
|
||||
profession: '',
|
||||
signature: '',
|
||||
avatar: 'https://randomuser.me/api/portraits/men/32.jpg',
|
||||
vip: true,
|
||||
verified: true,
|
||||
live: false
|
||||
}
|
||||
] as IUser[])
|
||||
|
||||
// 默认选中"交友"
|
||||
const activeNav = ref(0)
|
||||
|
||||
// 计算属性:根据切换标签过滤用户(简化示例)
|
||||
const filteredUsers = computed<IUser[]>(() => {
|
||||
return users
|
||||
})
|
||||
|
||||
// 将后端 user_profile 列表项映射为前端展示用的 IUser
|
||||
const mapUser = (item : UTSJSONObject) : IUser => {
|
||||
const city = item.getString('residecity') ?? ''
|
||||
const province = item.getString('resideprovince') ?? ''
|
||||
const location = city.length > 0 ? city : province
|
||||
const age = item.getNumber('age') ?? 0
|
||||
const height = item.getNumber('height') ?? 0
|
||||
const isOnline = item.getBoolean('is_online') ?? false
|
||||
return {
|
||||
id: item.getNumber('uid') ?? 0,
|
||||
nickname: item.getString('nickname') ?? '',
|
||||
realname: false,
|
||||
location: location,
|
||||
age: age,
|
||||
height: height,
|
||||
profession: '',
|
||||
signature: item.getString('bio') ?? '',
|
||||
avatar: item.getString('avatar') ?? '',
|
||||
vip: false,
|
||||
verified: false,
|
||||
live: isOnline
|
||||
} as IUser
|
||||
}
|
||||
|
||||
// 拉取附近/同城真实用户,成功则替换列表,失败保留默认假数据
|
||||
const loadNearbyUsers = async () => {
|
||||
try {
|
||||
let data : UTSJSONObject | null = null
|
||||
try {
|
||||
data = await Api.user.local({})
|
||||
} catch (e) {
|
||||
console.warn('本地同城加载失败,尝试在线列表', e)
|
||||
}
|
||||
// 本地同城为空(如未设置城市)时,回退到在线活跃用户
|
||||
const localList = data == null ? null : data.getJSONArray('list')
|
||||
if (localList == null || localList.size() == 0) {
|
||||
try {
|
||||
data = await Api.user.online({})
|
||||
} catch (e) {
|
||||
console.warn('在线列表加载失败', e)
|
||||
}
|
||||
}
|
||||
const list = data == null ? null : data.getJSONArray('list')
|
||||
if (list != null && list.size() > 0) {
|
||||
const mapped : IUser[] = []
|
||||
for (let i = 0; i < list.size(); i++) {
|
||||
const item = list.get(i) as UTSJSONObject
|
||||
if (item != null) {
|
||||
mapped.push(mapUser(item))
|
||||
}
|
||||
}
|
||||
if (mapped.length > 0) {
|
||||
users.splice(0, users.length, ...mapped)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('加载附近用户失败,使用默认数据', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 方法:导航到功能模块
|
||||
const navigateToModule = (path : string) => {
|
||||
uni.navigateTo({
|
||||
"url": path
|
||||
})
|
||||
}
|
||||
|
||||
// 方法:开始匹配
|
||||
const startMatching = () => {
|
||||
uni.showToast({ title: '开始匹配附近的人...', icon: 'loading' })
|
||||
// 实际项目中调用匹配API
|
||||
}
|
||||
|
||||
// 方法:发起聊天
|
||||
const startChat = (userId : number) => {
|
||||
|
||||
uni.navigateTo({ url: `/pages/chat/chat?userId=${userId}` })
|
||||
}
|
||||
|
||||
// 方法:查看用户资料
|
||||
const viewUserProfile = (userId : number) => {
|
||||
uni.navigateTo({ url: `/pages/user/profile?id=${userId}` })
|
||||
}
|
||||
|
||||
// 方法:显示感兴趣的人
|
||||
const showInterestUsers = () => {
|
||||
uni.showToast({ title: '显示你可能感兴趣的人', icon: 'none' })
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
console.log('社交交友页面加载完成')
|
||||
loadNearbyUsers()
|
||||
})
|
||||
onLoad(() => {
|
||||
loadNearbyUsers()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 功能模块区(彩色区块) */
|
||||
.function-modules {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
}
|
||||
|
||||
.module-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-shadow: 0 5rpx 15rpx rgba(0, 0, 0, 0.1);
|
||||
border-radius: 20rpx;
|
||||
margin: 20rpx 10rpx;
|
||||
padding: 15rpx;
|
||||
}
|
||||
|
||||
.module-icon {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.module-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.module-desc {
|
||||
font-size: 24rpx;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* 同城/附近切换栏 */
|
||||
.tab-switch {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0 10rpx;
|
||||
margin-bottom: 10rpx;
|
||||
border-radius: 50rpx;
|
||||
padding: 10rpx;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 10rpx 0;
|
||||
border-radius: 40rpx;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.tabactive {
|
||||
background-color: #FF6B6B;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* 用户列表 */
|
||||
.user-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
margin-right: 15rpx;
|
||||
color: #5f5f5f;
|
||||
}
|
||||
|
||||
.user-badges {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
}
|
||||
|
||||
.user-badge {
|
||||
padding: 3rpx 9rpx;
|
||||
margin: 3rpx;
|
||||
border-radius: 15rpx;
|
||||
font-size: 14rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
|
||||
flex-flow: row nowrap;
|
||||
}
|
||||
|
||||
.user-info-i {
|
||||
margin: 5rpx 3rpx;
|
||||
padding: 3rpx 6rpx;
|
||||
border-radius: 25rpx;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
font-size: 18rpx;
|
||||
}
|
||||
|
||||
.user-signature {
|
||||
font-size: 16rpx;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.user-chat-btn {
|
||||
background-color: #FF6B6B;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 50rpx;
|
||||
padding: 5rpx 15rpx;
|
||||
font-size: 16rpx;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 你可能感兴趣的人提示框 */
|
||||
.interest-tip {
|
||||
position: fixed;
|
||||
right: 30rpx;
|
||||
bottom: 150rpx;
|
||||
background-color: #FF6B6B;
|
||||
padding: 15rpx 25rpx;
|
||||
border-radius: 50rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 5rpx 15rpx rgba(255, 107, 107, 0.3);
|
||||
}
|
||||
|
||||
.tip-text {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<!-- #ifdef APP -->
|
||||
<scroll-view style="flex:1">
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- #ifdef APP -->
|
||||
</scroll-view>
|
||||
<!-- #endif -->
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,473 @@
|
||||
<template>
|
||||
<view class="card-panel">
|
||||
<!-- 用户信息 -->
|
||||
<view class="card-panel-header">
|
||||
<view class="user-info" @click="onUserClick">
|
||||
<image class="avatar" :src="moment.userAvatar" mode="aspectFill"></image>
|
||||
<view class="user-details">
|
||||
<view class="user-name-row">
|
||||
<text class="user-name">{{ moment.userName }}</text>
|
||||
<text v-if="moment.userVip" class="vip-badge">V</text>
|
||||
<text v-if="moment.hot" class="hot-badge">🔥</text>
|
||||
</view>
|
||||
<view class="meta-info">
|
||||
<text class="location">{{ moment.location }}</text>
|
||||
<text class="time">{{ formatTime(moment.time) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="more-btn" @click="onMoreClick">
|
||||
<uni-icons type="more" size="20" color="#999"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 动态内容 -->
|
||||
<view class="card-panel-body" @click="onContentClick">
|
||||
<text class="content-text">{{ moment.content }}</text>
|
||||
|
||||
<!-- 图片内容 -->
|
||||
<view v-if="moment.type === 'image' && moment.images.length > 0" class="image-content">
|
||||
<view v-if="moment.images.length == 1" class="single-image" @click="previewImage(0)">
|
||||
<image class="image-item" :src="moment.images[0]" mode="aspectFill"></image>
|
||||
</view>
|
||||
<view v-else-if="moment.images.length < 3" class="multi-images-row">
|
||||
<view v-for="(img, index) in moment.images" :key="index" class="image-item-small"
|
||||
@click="previewImage(index)">
|
||||
<image class="image" :src="img" mode="aspectFill"></image>
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="multi-images-grid">
|
||||
<view v-for="(img, index) in moment.images.slice(0, 4)" :key="index" class="grid-image"
|
||||
:class="{ 'last-image': index == 3 && moment.images.length > 4 }" @click="previewImage(index)">
|
||||
<image class="image" :src="img" mode="aspectFill"></image>
|
||||
<view v-if="index == 3 && moment.images.length > 4" class="image-count">
|
||||
+{{ moment.images.length - 4 }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 视频内容 -->
|
||||
<view v-else-if="moment.type === 'video'" class="video-content" @click="playVideo">
|
||||
<image v-if="moment.images.length > 0" class="video-cover" :src="moment.images[0] " mode="aspectFill">
|
||||
</image>
|
||||
<view class="video-play-btn">
|
||||
<uni-icons type="play" size="40" color="#fff"></uni-icons>
|
||||
</view>
|
||||
<view class="video-duration">03:25</view>
|
||||
</view>
|
||||
|
||||
<!-- 标签 -->
|
||||
<view v-if="moment.tags != null" class="moment-tags">
|
||||
<view v-for="(tag, index) in moment.tags" :key="index" class="tag-item">
|
||||
<text class="tag-text">#{{ tag }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 互动操作栏 -->
|
||||
<view class="card-panel-footer">
|
||||
<view class="action-item" @click="onLikeClick">
|
||||
<uni-icons :type="moment.isLiked ? 'heart-filled' : 'heart'" size="20"
|
||||
:color="moment.isLiked ? '#FF2D55' : '#666'"></uni-icons>
|
||||
<text class="action-text" :style="{ color: moment.isLiked ? '#FF2D55' : '#666' }">
|
||||
{{ formatNumber(moment.likes) }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<view class="action-item" @click="onCommentClick">
|
||||
<uni-icons type="chat" size="20" color="#666"></uni-icons>
|
||||
<text class="action-text">{{ formatNumber(moment.comments) }}</text>
|
||||
</view>
|
||||
|
||||
<view class="action-item" @click="onShareClick">
|
||||
<uni-icons type="redo" size="20" color="#666"></uni-icons>
|
||||
<text class="action-text">{{ formatNumber(moment.shares) }}</text>
|
||||
</view>
|
||||
|
||||
<view class="action-item distance" v-if="moment.distance !=0 && moment.distance < 50">
|
||||
<uni-icons type="location" size="16" color="#999"></uni-icons>
|
||||
<text class="distance-text">{{ formatDistance(moment.distance) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { IMoment } from '@/types/dynamicType.uts'
|
||||
|
||||
const props = defineProps<{
|
||||
moment : IMoment
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
like : [id: number]
|
||||
comment : [id: number]
|
||||
share : [id: number]
|
||||
more : [id: number]
|
||||
}>()
|
||||
|
||||
// 格式化时间
|
||||
const formatTime = (timestamp : number) : string => {
|
||||
const now = Date.now()
|
||||
const diff = now - timestamp
|
||||
if (diff < 60000) { // 1分钟内
|
||||
return '刚刚'
|
||||
} else if (diff < 3600000) { // 1小时内
|
||||
return Math.floor(diff / 60000) + '分钟前'
|
||||
} else if (diff < 86400000) { // 1天内
|
||||
return Math.floor(diff / 3600000) + '小时前'
|
||||
} else if (diff < 604800000) { // 1周内
|
||||
return Math.floor(diff / 86400000) + '天前'
|
||||
} else {
|
||||
const date = new Date(timestamp)
|
||||
return `${date.getMonth() + 1}月${date.getDate()}日`
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化数字
|
||||
const formatNumber = (num : number) : string => {
|
||||
if (num >= 10000) {
|
||||
return (num / 10000).toFixed(1) + 'w'
|
||||
} else if (num >= 1000) {
|
||||
return (num / 1000).toFixed(1) + 'k'
|
||||
}
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
// 格式化距离
|
||||
const formatDistance = (distance : number) : string => {
|
||||
if (distance < 1) {
|
||||
return '<1km'
|
||||
} else if (distance < 10) {
|
||||
return distance.toFixed(1) + 'km'
|
||||
} else {
|
||||
return Math.floor(distance) + 'km'
|
||||
}
|
||||
}
|
||||
|
||||
// 点击用户
|
||||
const onUserClick = () => {
|
||||
console.log('点击用户:', props.moment.userName)
|
||||
uni.navigateTo({
|
||||
url: `/pages/user-detail/user-detail?id=${props.moment.userId}`
|
||||
})
|
||||
}
|
||||
|
||||
// 点击更多
|
||||
const onMoreClick = () => {
|
||||
emit('more', props.moment.id)
|
||||
}
|
||||
|
||||
// 点击内容
|
||||
const onContentClick = () => {
|
||||
console.log('点击动态内容:', props.moment.id)
|
||||
let page = `/pages/moment/details?id=${props.moment.id}`;
|
||||
console.log(page)
|
||||
uni.navigateTo({
|
||||
url: page
|
||||
})
|
||||
}
|
||||
|
||||
// 预览图片
|
||||
const previewImage = (index : number) => {
|
||||
console.log('预览图片:', index)
|
||||
uni.previewImage({
|
||||
current: index,
|
||||
urls: props.moment.images
|
||||
})
|
||||
}
|
||||
|
||||
// 播放视频
|
||||
const playVideo = () => {
|
||||
console.log('播放视频')
|
||||
uni.navigateTo({
|
||||
url: `/pages/video-player/video-player?url=${encodeURIComponent(props.moment.videoUrl)}`
|
||||
})
|
||||
}
|
||||
|
||||
// 点赞
|
||||
const onLikeClick = () => {
|
||||
emit('like', props.moment.id)
|
||||
}
|
||||
|
||||
// 评论
|
||||
const onCommentClick = () => {
|
||||
emit('comment', props.moment.id)
|
||||
}
|
||||
|
||||
// 分享
|
||||
const onShareClick = () => {
|
||||
emit('share', props.moment.id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 用户信息 */
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
margin-right: 20rpx;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.user-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.user-name-row {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-right: 10rpx;
|
||||
max-width: 200rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.vip-badge {
|
||||
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
padding: 3rpx 10rpx;
|
||||
border-radius: 20rpx;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.hot-badge {
|
||||
background-color: #f30f025c;
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
padding: 3rpx 10rpx;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.meta-info {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.location,
|
||||
.time {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.more-btn {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
/* 动态内容 */
|
||||
.moment-content {
|
||||
margin-bottom: 25rpx;
|
||||
}
|
||||
|
||||
.content-text {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 20rpx;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 图片内容 */
|
||||
.image-content {
|
||||
margin: 20rpx 0;
|
||||
}
|
||||
|
||||
.single-image {
|
||||
width: 100%;
|
||||
height: 400rpx;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image-item {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.multi-images-row {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
|
||||
.image-item-small {
|
||||
flex: 1;
|
||||
height: 375rpx;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
padding: 5rpx;
|
||||
}
|
||||
|
||||
.image-item-small .image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.multi-images-grid {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.grid-image {
|
||||
margin: 5rpx;
|
||||
position: relative;
|
||||
flex-basis: 210rpx;
|
||||
height: 230rpx;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.grid-image .image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.grid-image.last-image::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.image-count {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 40rpx;
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 视频内容 */
|
||||
.video-content {
|
||||
position: relative;
|
||||
height: 400rpx;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
margin: 20rpx 0;
|
||||
}
|
||||
|
||||
.video-cover {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.video-play-btn {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
background-color: rgba(0, 0, 0, 0.6);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.video-duration {
|
||||
position: absolute;
|
||||
bottom: 20rpx;
|
||||
right: 20rpx;
|
||||
padding: 8rpx 20rpx;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
/* 标签 */
|
||||
.moment-tags {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
padding: 8rpx 20rpx;
|
||||
background-color: #f0f0f0;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.tag-text {
|
||||
font-size: 24rpx;
|
||||
color: #007AFF;
|
||||
}
|
||||
|
||||
/* 互动操作栏 */
|
||||
.action-bar {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
padding-top: 20rpx;
|
||||
border-top: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.action-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 40rpx;
|
||||
padding: 10rpx 0;
|
||||
}
|
||||
|
||||
.action-item .uni-icons {
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.action-item.distance {
|
||||
margin-left: auto;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.distance-text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,451 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<!-- 顶部导航栏 -->
|
||||
<view class="page-nav">
|
||||
<view class="page-nav-left">
|
||||
<scroll-view class="tabsbox" :show-scrollbar="false" direction="horizontal">
|
||||
<view v-for="(tab, index) in tabs" :key="index" class="tabsbox-item"
|
||||
:class="{ 'tabsbox-active': activeTab === index }" @click="switchTab(index)">
|
||||
<text class="tabsbox-text" :class="{'tabsbox-active-txt': activeTab == index }">{{ tab.name }}</text>
|
||||
<!-- <text v-if="tab.badge" class="badge">{{ tab.badge }}</text> -->
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
<view class="page-nav-right">
|
||||
<view class="icon-btn" @click="onSearchClick">
|
||||
<uni-icons type="search" size="24" color="#333"></uni-icons>
|
||||
</view>
|
||||
<view class="icon-btn" @click="onPublishClick">
|
||||
<uni-icons type="plusempty" size="24" color="#333"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="page-body" style="">
|
||||
<scroll-view class="moment-list" :show-scrollbar="false" direction="vertical" :refresher-enabled="true" :refresher-triggered="refreshing"
|
||||
@refresherrefresh="onRefresh" @scrolltolower="onLoadMore" style="padding-bottom: 96rpx;">
|
||||
<!-- 置顶动态 -->
|
||||
<view v-if="pinnedMoments.length > 0" class="pinned-section">
|
||||
<text class="section-title">置顶动态</text>
|
||||
<view v-for="moment in pinnedMoments" :key="'pinned-' + moment.id" class="moment-item pinned">
|
||||
<moment-card :moment="moment" @like="onLike" @comment="onComment" @share="onShare"
|
||||
@more="onMore" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 推荐动态 -->
|
||||
<view class="recommend-section" v-if="activeTab == 0">
|
||||
<text class="section-title">推荐动态</text>
|
||||
<view v-for="moment in recommendMoments" :key="'recommend-' + moment.id" class="moment-item">
|
||||
<moment-card :moment="moment" @like="onLike" @comment="onComment" @share="onShare"
|
||||
@more="onMore" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 关注动态 -->
|
||||
<view class="follow-section" v-if="activeTab == 1">
|
||||
<view v-if="followMoments.length > 0">
|
||||
<view v-for="moment in followMoments" :key="'follow-' + moment.id" class="moment-item">
|
||||
<moment-card :moment="moment" @like="onLike" @comment="onComment" @share="onShare"
|
||||
@more="onMore" />
|
||||
</view>
|
||||
</view>
|
||||
<view v-else class="empty-state">
|
||||
<uni-icons type="person" size="60" color="#ccc"></uni-icons>
|
||||
<text class="empty-text">还没有关注的好友动态</text>
|
||||
<button class="empty-btn" @click="findFriends">去发现好友</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 附近动态 -->
|
||||
<view class="nearby-section" v-if="activeTab == 2">
|
||||
<view class="section-header">
|
||||
<text class="section-title">附近动态</text>
|
||||
<text class="section-subtitle">{{ nearbyCount }}条动态</text>
|
||||
</view>
|
||||
<view v-for="moment in nearbyMoments" :key="'nearby-' + moment.id" class="moment-item">
|
||||
<moment-card :moment="moment" @like="onLike" @comment="onComment" @share="onShare"
|
||||
@more="onMore" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 热门动态 -->
|
||||
<view class="hot-section" v-if="activeTab == 3">
|
||||
<view class="section-header">
|
||||
<text class="section-title">热门动态</text>
|
||||
<view class="hot-tags">
|
||||
<view v-for="(tag, index) in hotTags" :key="index" class="hot-tag"
|
||||
:class="{ active: activeHotTag == index }" @click="switchHotTag(index)">
|
||||
<text class="tag-text">{{ tag }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view v-for="moment in hotMoments" :key="'hot-' + moment.id" class="moment-item">
|
||||
<moment-card :moment="moment" @like="onLike" @comment="onComment" @share="onShare"
|
||||
@more="onMore" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<!-- <view class="load-status">
|
||||
<uni-load-more :status="loadStatus" />
|
||||
</view> -->
|
||||
</scroll-view>
|
||||
|
||||
<!-- 发布按钮 -->
|
||||
<view class="publish-fab" @click="onPublishClick">
|
||||
<uni-icons type="plus" size="30" color="#fff"></uni-icons>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import MomentCard from './components/MomentCard.uvue'
|
||||
import {IMoment,ITab} from '@/types/dynamicType.uts'
|
||||
import Api from '@/common/api-service.uts'
|
||||
|
||||
// 响应式数据
|
||||
const activeTab = ref(0)
|
||||
const activeHotTag = ref(0)
|
||||
const refreshing = ref(false)
|
||||
const loading = ref(false)
|
||||
const noMore = ref(false)
|
||||
const page = ref(1)
|
||||
const pageSize = 10
|
||||
const moments = reactive<IMoment[]>([])
|
||||
const scopes = ['recommend', 'follow', 'nearby', 'hot']
|
||||
const nearbyCount = computed(() => moments.length)
|
||||
|
||||
// 标签页数据
|
||||
const tabs = reactive<ITab[]>([
|
||||
{ name: '推荐', badge: 0 },
|
||||
{ name: '关注', badge: 3 },
|
||||
{ name: '附近', badge: 12 },
|
||||
{ name: '热门', badge: 0 }
|
||||
])
|
||||
|
||||
// 热门标签
|
||||
const hotTags = reactive(['全部', '生活', '情感', '旅行', '美食', '游戏'])
|
||||
|
||||
// 计算属性
|
||||
const pinnedMoments = computed<IMoment[]>(() => moments.filter(m => m.isPinned))
|
||||
const recommendMoments = computed<IMoment[]>(() => moments)
|
||||
const followMoments = computed<IMoment[]>(() => moments)
|
||||
const nearbyMoments = computed<IMoment[]>(() => moments)
|
||||
const hotMoments = computed<IMoment[]>(() => moments)
|
||||
|
||||
const loadStatus = computed(() => {
|
||||
if (loading.value) return 'loading'
|
||||
if (noMore.value) return 'noMore'
|
||||
return 'more'
|
||||
})
|
||||
|
||||
// 加载数据
|
||||
const loadData = (reset : boolean = false) => {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
const p = reset ? 1 : page.value
|
||||
Api.moment.list({ scope: scopes[activeTab.value], page: p, page_size: pageSize })
|
||||
.then((res : UTSJSONObject) => {
|
||||
const list = (res.get('list') as Array<IMoment>) ?? []
|
||||
if (reset) {
|
||||
moments.splice(0, moments.length)
|
||||
page.value = 1
|
||||
}
|
||||
list.forEach((m : IMoment) => moments.push(m))
|
||||
page.value = p + 1
|
||||
noMore.value = list.length < pageSize
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => { loading.value = false })
|
||||
}
|
||||
|
||||
// 页面加载
|
||||
onMounted(() => { loadData(true) })
|
||||
onShow(() => { loadData(true) })
|
||||
|
||||
// 切换标签页
|
||||
const switchTab = (index : number) => {
|
||||
activeTab.value = index
|
||||
page.value = 1
|
||||
noMore.value = false
|
||||
loadData(true)
|
||||
}
|
||||
|
||||
// 切换热门标签
|
||||
const switchHotTag = (index : number) => {
|
||||
activeHotTag.value = index
|
||||
console.log('切换热门标签:', hotTags[index])
|
||||
// 这里应该根据标签筛选热门动态
|
||||
}
|
||||
|
||||
// 点击搜索
|
||||
const onSearchClick = () => {
|
||||
console.log('点击搜索')
|
||||
uni.navigateTo({
|
||||
url: '/pages/search/search'
|
||||
})
|
||||
}
|
||||
|
||||
// 点击发布
|
||||
const onPublishClick = () => {
|
||||
console.log('点击发布')
|
||||
uni.navigateTo({
|
||||
url: '/pages/moment/publish'
|
||||
})
|
||||
}
|
||||
|
||||
// 点赞动态
|
||||
const onLike = (momentId : number) => {
|
||||
const m = moments.find(x => x.id === momentId)
|
||||
if (! m) return
|
||||
const willLike = ! m.isLiked
|
||||
m.isLiked = willLike
|
||||
m.likes += willLike ? 1 : -1
|
||||
Api.moment.like({ dynamic_id: momentId })
|
||||
.catch(() => { m.isLiked = !willLike; m.likes += willLike ? -1 : 1 })
|
||||
}
|
||||
|
||||
// 评论动态
|
||||
const onComment = (momentId : number) => {
|
||||
uni.navigateTo({ url: `/pages/moment/details?id=${momentId}` })
|
||||
}
|
||||
|
||||
// 分享动态
|
||||
const onShare = (momentId : number) => {
|
||||
console.log('分享动态:', momentId)
|
||||
uni.showActionSheet({
|
||||
itemList: ['分享给好友', '分享到朋友圈', '复制链接', '保存图片'],
|
||||
success: (res) => {
|
||||
console.log('选择了分享方式:', res.tapIndex)
|
||||
uni.showToast({
|
||||
title: '分享成功',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 更多操作
|
||||
const onMore = (momentId : number) => {
|
||||
console.log('更多操作:', momentId)
|
||||
uni.showActionSheet({
|
||||
itemList: ['举报', '不感兴趣', '屏蔽用户', '保存到相册'],
|
||||
success: (res) => {
|
||||
const actions = ['report', 'notInterested', 'block', 'save']
|
||||
const action = actions[res.tapIndex]
|
||||
console.log('选择了:', action)
|
||||
|
||||
if (action === 'report') {
|
||||
uni.navigateTo({
|
||||
url: '/pages/report/report?momentId=' + momentId
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '操作成功',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 下拉刷新
|
||||
const onRefresh = () => {
|
||||
refreshing.value = true
|
||||
page.value = 1
|
||||
loadData(true)
|
||||
setTimeout(() => { refreshing.value = false }, 600)
|
||||
}
|
||||
|
||||
// 上拉加载更多
|
||||
const onLoadMore = () => {
|
||||
if (loading.value || noMore.value) return
|
||||
loadData(false)
|
||||
}
|
||||
|
||||
// 发现好友
|
||||
const findFriends = () => {
|
||||
console.log('去发现好友')
|
||||
uni.switchTab({
|
||||
url: '/pages/home/home'
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
}
|
||||
|
||||
.icon-btn {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
/* 分类标签 */
|
||||
.category-tabs {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
padding: 20rpx 0;
|
||||
background-color: #fff;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 0 30rpx;
|
||||
padding: 10rpx 0;
|
||||
}
|
||||
|
||||
.tab-text {
|
||||
font-size: 30rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.tabactive {
|
||||
color: #FF6B6B;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 32rpx;
|
||||
text-align: center;
|
||||
background-color: #FF6B6B;
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
border-radius: 16rpx;
|
||||
transform: translate(50%, -50%);
|
||||
}
|
||||
|
||||
/* 动态列表 */
|
||||
.moment-list {
|
||||
flex: 1;
|
||||
padding: 0 20rpx 20rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
padding: 20rpx 0 10rpx 20rpx;
|
||||
}
|
||||
|
||||
.pinned-section .section-title {
|
||||
color: #FF9500;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 100rpx 0;
|
||||
background-color: #fff;
|
||||
border-radius: 20rpx;
|
||||
margin: 20rpx;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
margin: 20rpx 0 30rpx;
|
||||
}
|
||||
|
||||
.empty-btn {
|
||||
background: linear-gradient(135deg, #FF6B6B 0%, #FF8E53 100%);
|
||||
color: #fff;
|
||||
padding: 20rpx 50rpx;
|
||||
border-radius: 40rpx;
|
||||
font-size: 28rpx;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.empty-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* 附近动态头部 */
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20rpx 0 10rpx 20rpx;
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
/* 热门标签 */
|
||||
.hot-tags {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
padding: 10rpx;
|
||||
}
|
||||
|
||||
.hot-tag {
|
||||
padding: 10rpx;
|
||||
background-color: #f8f8f8;
|
||||
border-radius: 30rpx;
|
||||
font-size: 24rpx;
|
||||
color: #666;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.hot-tag.active {
|
||||
background: linear-gradient(135deg, #FF6B6B 0%, #FF8E53 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.load-status {
|
||||
padding: 30rpx 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 发布按钮 */
|
||||
.publish-fab {
|
||||
position: fixed;
|
||||
right: 40rpx;
|
||||
bottom: 140rpx;
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
background: linear-gradient(to bottom right , #FF6B6B , #FF8E53);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
box-shadow: 0 10rpx 30rpx rgba(255, 107, 107, 0.3);
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,655 @@
|
||||
// pages/dynamic/publish.uts
|
||||
<template>
|
||||
<view class="page">
|
||||
<!-- 顶部导航栏 -->
|
||||
<view class="page-nav">
|
||||
<view class="page-nav-left" @click="handleCancel">
|
||||
<text class="cancel-text">取消</text>
|
||||
</view>
|
||||
<view class="page-nav-center">
|
||||
<text class="nav-title">发布动态</text>
|
||||
</view>
|
||||
<view class="page-nav-right">
|
||||
<button class="publish-btn" :class="{ active: canPublish }" @click="handlePublish">发布</button>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 内容输入区 -->
|
||||
<view class="content-area">
|
||||
<textarea class="content-input" placeholder="记录这一刻,晒给懂你的人" v-model="content" maxlength="250"
|
||||
@input="updateCharCount"></textarea>
|
||||
<text class="char-count">{{ charCount }}/250</text>
|
||||
</view>
|
||||
<!-- 图片上传区 -->
|
||||
<view class="media-area">
|
||||
<view class="upload-grid">
|
||||
<view v-for="(file, index) in files" :key="index" class="grid-item">
|
||||
<image :src="file.path" class="grid-image" mode="aspectFill" />
|
||||
<view class="delete-btn" @click="removeImage(index)">
|
||||
<uni-icons type="closeempty" color="#fff"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="files.length < 9" class="grid-item add-item" @click="chooseMedia">
|
||||
<!-- <view class="add-icon">+</view> -->
|
||||
<uni-icons type="plusempty" size="48"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 功能选项区 -->
|
||||
<view class="function-area">
|
||||
<view class="function-item" @click="chooseLocation">
|
||||
<view class="item-left">
|
||||
<image src="/static/icons/location.png" class="item-icon" />
|
||||
<text class="item-label">所在位置</text>
|
||||
</view>
|
||||
<view class="item-right">
|
||||
<text class="item-value">{{ location ?? '未选择' }}</text>
|
||||
<image src="/static/icons/arrow-right.png" class="arrow-icon" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="function-item" @click="showTopicDialog">
|
||||
<view class="item-left">
|
||||
<image src="/static/icons/topic.png" class="item-icon" />
|
||||
<text class="item-label">添加话题</text>
|
||||
</view>
|
||||
<view class="item-right">
|
||||
<text class="item-value">{{ topics.length > 0 ? `#${topics.join(' #')}` : '选择话题会获得更多互动哦' }}</text>
|
||||
<image src="/static/icons/arrow-right.png" class="arrow-icon" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="function-item voice-item" @click="toggleRecording">
|
||||
<view class="item-left">
|
||||
<image :src="isRecording ? '/static/icons/voice-recording.png' : '/static/icons/voice.png'"
|
||||
class="item-icon" />
|
||||
<text class="item-label">{{ isRecording ? '正在录音...' : '语音描述' }}</text>
|
||||
</view>
|
||||
<view class="item-right">
|
||||
<view v-if="audioPath" class="audio-preview">
|
||||
<image src="/static/icons/play.png" class="play-icon" @click.stop="playAudio" />
|
||||
<text class="audio-duration">{{ audioDuration }}"</text>
|
||||
</view>
|
||||
<image v-else src="/static/icons/arrow-right.png" class="arrow-icon" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 话题选择弹窗 -->
|
||||
<view v-if="showTopicSelector" class="topic-dialog">
|
||||
<view class="dialog-mask" @click="hideTopicDialog"></view>
|
||||
<view class="dialog-content">
|
||||
<view class="dialog-header">
|
||||
<text class="dialog-title">选择话题</text>
|
||||
<text class="dialog-close" @click="hideTopicDialog">×</text>
|
||||
</view>
|
||||
<view class="topic-list">
|
||||
<view v-for="(topic, index) in availableTopics" :key="index" class="topic-item"
|
||||
@click="toggleTopic(topic)">
|
||||
<text class="topic-text">#{{ topic }}</text>
|
||||
<view class="topic-check" v-if="topics.includes(topic)">
|
||||
<image src="/static/icons/checked.png" class="check-icon" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="dialog-footer">
|
||||
<button class="confirm-btn" @click="hideTopicDialog">完成</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 位置选择弹窗 -->
|
||||
<view v-if="showLocationSelector" class="location-dialog">
|
||||
<view class="dialog-mask" @click="hideLocationDialog"></view>
|
||||
<view class="dialog-content">
|
||||
<view class="dialog-header">
|
||||
<text class="dialog-title">选择位置</text>
|
||||
<text class="dialog-close" @click="hideLocationDialog">×</text>
|
||||
</view>
|
||||
<view class="search-box">
|
||||
<input v-model="locationSearch" placeholder="搜索地点" class="search-input" />
|
||||
</view>
|
||||
<view class="location-list">
|
||||
<view v-for="(loc, index) in filteredLocations" :key="index" class="location-item"
|
||||
@click="selectLocation(loc)">
|
||||
<text class="location-name">{{ loc.name }}</text>
|
||||
<text class="location-address">{{ loc.address }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import httpApi, { type IFileInfo } from '@/utlis/http-api'
|
||||
import type { IUploadOption } from '@/types/http.uts'
|
||||
import Api from '@/common/api-service.uts'
|
||||
// 响应式数据
|
||||
type locationItem = { name : string, address : string }
|
||||
const content = ref<string>('')
|
||||
const charCount = ref<number>(0)
|
||||
const location = ref<string>('')
|
||||
const topics = ref<Array<string>>([])
|
||||
const audioPath = ref<string>('')
|
||||
const audioDuration = ref<number>(0)
|
||||
const isRecording = ref<boolean>(false)
|
||||
const showTopicSelector = ref<boolean>(false)
|
||||
const showLocationSelector = ref<boolean>(false)
|
||||
const locationSearch = ref<string>('')
|
||||
const recorderManager = ref(null as RecorderManager | null)
|
||||
const files = ref<IFileInfo[]>([])
|
||||
// 可用话题列表
|
||||
const availableTopics = ref<Array<string>>([
|
||||
'美食分享', '厨房日记', '烹饪技巧', '家常菜', '烘焙时光',
|
||||
'美食探店', '食材百科', '健康饮食', '快手菜', '创意料理'
|
||||
])
|
||||
|
||||
// 位置列表
|
||||
const locations = ref<Array<locationItem>>([
|
||||
{ name: '北京市朝阳区三里屯', address: '北京市朝阳区三里屯街道' },
|
||||
{ name: '上海市黄浦区外滩', address: '上海市黄浦区外滩街道' },
|
||||
{ name: '广州市天河区珠江新城', address: '广州市天河区珠江新城' },
|
||||
{ name: '深圳市南山区科技园', address: '深圳市南山区科技园' },
|
||||
{ name: '成都市锦江区春熙路', address: '成都市锦江区春熙路' }
|
||||
])
|
||||
|
||||
// 计算属性
|
||||
const canPublish = computed(() => {
|
||||
return content.value.trim().length > 0 || files.value.length > 0 || audioPath.value !== ''
|
||||
})
|
||||
|
||||
const filteredLocations = computed<locationItem[]>(() => {
|
||||
if (locationSearch.value != null) return locations.value
|
||||
return locations.value.filter(loc =>
|
||||
loc.name.includes(locationSearch.value) || loc.address.includes(locationSearch.value)
|
||||
)
|
||||
})
|
||||
|
||||
// 生命周期钩子
|
||||
onLoad(() => {
|
||||
// 页面加载时的初始化操作
|
||||
//updateCharCount()
|
||||
})
|
||||
|
||||
// 方法定义
|
||||
function updateCharCount() {
|
||||
charCount.value = content.value.length
|
||||
}
|
||||
|
||||
function chooseMedia() {
|
||||
// #ifndef WEB
|
||||
uni.chooseMedia({
|
||||
count: 9 - files.value.length,
|
||||
mediaType: ['image', 'video'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: (res) => {
|
||||
console.log(res)
|
||||
res.tempFiles.forEach((path : ChooseMediaTempFile) => {
|
||||
files.value.push({
|
||||
index: files.value.length ,
|
||||
name: 'file' + (files.value.length ),
|
||||
path: path.tempFilePath,
|
||||
} as IFileInfo)
|
||||
})
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
// #ifdef WEB
|
||||
uni.chooseImage({
|
||||
count: 9 - files.value.length,
|
||||
sizeType: ['compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: (res) => {
|
||||
console.log(res)
|
||||
res.tempFilePaths.forEach((file) => {
|
||||
files.value.push({
|
||||
index: files.value.length ,
|
||||
name: 'file' + (files.value.length ),
|
||||
path: file,
|
||||
} )
|
||||
})
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
}
|
||||
|
||||
function removeImage(index : number) {
|
||||
files.value.splice(index, 1)
|
||||
}
|
||||
|
||||
function chooseLocation() {
|
||||
showLocationSelector.value = true
|
||||
}
|
||||
function showTopicDialog() {
|
||||
showTopicSelector.value = true
|
||||
}
|
||||
|
||||
function hideTopicDialog() {
|
||||
showTopicSelector.value = false
|
||||
}
|
||||
|
||||
function hideLocationDialog() {
|
||||
showLocationSelector.value = false
|
||||
locationSearch.value = ''
|
||||
}
|
||||
|
||||
function selectLocation(loc : locationItem) {
|
||||
location.value = loc.name
|
||||
hideLocationDialog()
|
||||
}
|
||||
|
||||
function toggleTopic(topic : string) {
|
||||
const index = topics.value.indexOf(topic)
|
||||
if (index > -1) {
|
||||
topics.value.splice(index, 1)
|
||||
} else {
|
||||
topics.value.push(topic)
|
||||
}
|
||||
}
|
||||
let innerAudioContext : any | null = null
|
||||
function startRecording() {
|
||||
isRecording.value = true
|
||||
audioPath.value = ''
|
||||
audioDuration.value = 0
|
||||
// 创建录音管理器
|
||||
recorderManager.value = uni.getRecorderManager();
|
||||
|
||||
// 录音结束事件
|
||||
recorderManager.value!.onStop((res) => {
|
||||
console.log(res);
|
||||
// isRecording.value = false
|
||||
// audioPath.value = res.tempFilePath
|
||||
// audioDuration.value = Math.floor(res.duration as number / 1000) // 转换为秒
|
||||
})
|
||||
|
||||
// 开始录音
|
||||
recorderManager.value!.start({
|
||||
duration: 60000, // 最长60秒
|
||||
sampleRate: 44100,
|
||||
numberOfChannels: 1,
|
||||
encodeBitRate: 192000,
|
||||
format: 'mp3'
|
||||
})
|
||||
}
|
||||
|
||||
function stopRecording() {
|
||||
if (recorderManager != null) {
|
||||
recorderManager.value!.stop()
|
||||
}
|
||||
}
|
||||
|
||||
function toggleRecording() {
|
||||
if (isRecording.value) {
|
||||
stopRecording()
|
||||
} else {
|
||||
startRecording()
|
||||
}
|
||||
}
|
||||
function playAudio() {
|
||||
// if (!innerAudioContext) {
|
||||
// innerAudioContext = uni.createInnerAudioContext()
|
||||
// }
|
||||
|
||||
// if (innerAudioContext.paused) {
|
||||
// innerAudioContext.src = audioPath.value
|
||||
// innerAudioContext.play()
|
||||
// } else {
|
||||
// innerAudioContext.pause()
|
||||
// }
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
uni.navigateBack()
|
||||
}
|
||||
async function handlePublish() {
|
||||
if (!canPublish.value) return
|
||||
uni.showLoading({ title: '发布中...', mask: true })
|
||||
try {
|
||||
// 1. 上传图片/视频,拿到可访问 URL
|
||||
let media : UTSJSONObject[] = []
|
||||
let hasVideo = false
|
||||
if (files.value.length > 0) {
|
||||
const uploaded = await httpApi.uploadMultipleFilesParallel({
|
||||
url: '/wxchat/api/moment/upload'
|
||||
} as IUploadOption, files.value)
|
||||
uploaded.forEach((f) => {
|
||||
const p = f.path
|
||||
const isVideo = p.endsWith('.mp4') || p.endsWith('.mov') || p.endsWith('.m4v') || p.endsWith('.avi')
|
||||
if (isVideo) hasVideo = true
|
||||
media.push({ type: isVideo ? 2 : 1, url: p } as UTSJSONObject)
|
||||
})
|
||||
}
|
||||
// 2. 计算动态类型:1 文字 / 2 图片 / 3 视频
|
||||
let type = 1
|
||||
if (hasVideo) {
|
||||
type = 3
|
||||
} else if (media.length > 0) {
|
||||
type = 2
|
||||
}
|
||||
// 3. 提交发布
|
||||
await Api.moment.create({
|
||||
content: content.value.trim(),
|
||||
type: type,
|
||||
is_public: 1,
|
||||
location: location.value,
|
||||
media: media,
|
||||
hashtags: topics.value
|
||||
})
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '发布成功', icon: 'success' })
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1200)
|
||||
} catch (error) {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '发布失败', icon: 'none' })
|
||||
console.log('发布失败:', error)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* 内容输入区 */
|
||||
.content-area {
|
||||
margin-top: 100rpx;
|
||||
position: relative;
|
||||
padding: 16px;
|
||||
background-color: #fff;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.content-input {
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
color: #333;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.char-count {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
bottom: 8px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
/* 图片上传区 */
|
||||
.media-area {
|
||||
padding: 16px;
|
||||
background-color: #fff;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.upload-grid {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
border-radius: 25rpx;
|
||||
overflow: hidden;
|
||||
background: none;
|
||||
|
||||
.grid-item {
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
margin: 3rpx;
|
||||
overflow: hidden;
|
||||
background-color: #f5f5f5;
|
||||
flex: 0 0 222rpx;
|
||||
height: 222rpx;
|
||||
background-color: #f5f5f5;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
|
||||
.grid-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
position: absolute;
|
||||
top: 10rpx;
|
||||
right: 10rpx;
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&.add-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px dashed #ccc;
|
||||
|
||||
.add-icon {
|
||||
font-size: 32px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 功能选项区 */
|
||||
.function-area {
|
||||
background-color: #fff;
|
||||
margin-bottom: 8px;
|
||||
|
||||
.function-item {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.item-left {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
|
||||
.item-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.item-label {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
.item-right {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
|
||||
.item-value {
|
||||
font-size: 14px;
|
||||
color: #999;
|
||||
margin-right: 8px;
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.audio-preview {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
|
||||
.play-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.audio-duration {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.voice-item {
|
||||
.item-label {
|
||||
color: #ff2442;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 弹窗样式 */
|
||||
.topic-dialog,
|
||||
.location-dialog {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 1000;
|
||||
|
||||
.dialog-mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background-color: #fff;
|
||||
border-radius: 12px 12px 0 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.dialog-header {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
.dialog-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.dialog-close {
|
||||
font-size: 24px;
|
||||
color: #999;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.topic-list,
|
||||
.location-list {
|
||||
flex: 1;
|
||||
padding: 16px;
|
||||
|
||||
.topic-item,
|
||||
.location-item {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.topic-text {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.topic-check {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background-color: #ff2442;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.check-icon {
|
||||
width: 12px;
|
||||
height: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.location-name {
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.location-address {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-box {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
padding: 0 12px;
|
||||
border-radius: 18px;
|
||||
background-color: #f5f5f5;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-footer {
|
||||
padding: 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
|
||||
.confirm-btn {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
background-color: #ff2442;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
border-radius: 22px;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,133 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<scroll-view direction="vertical" :show-scrollbar="false" style="flex:1;">
|
||||
<view class="logo-box">
|
||||
<view class="logo">
|
||||
<uni-icons type="heart-filled" size="56" color="#fff"></uni-icons>
|
||||
</view>
|
||||
<text class="app-name">缘友</text>
|
||||
<text class="app-slogan">让相遇更有温度</text>
|
||||
</view>
|
||||
|
||||
<view class="list-container">
|
||||
<view class="list-item" @click="toast('用户协议')">
|
||||
<text class="list-text">用户协议</text>
|
||||
<uni-icons type="right" size="16" color="#999"></uni-icons>
|
||||
</view>
|
||||
<view class="list-item" @click="toast('隐私政策')">
|
||||
<text class="list-text">隐私政策</text>
|
||||
<uni-icons type="right" size="16" color="#999"></uni-icons>
|
||||
</view>
|
||||
<view class="list-item" @click="toast('去评分')">
|
||||
<text class="list-text">给我们评分</text>
|
||||
<uni-icons type="right" size="16" color="#999"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="intro">
|
||||
<text class="intro-text">
|
||||
缘友是一款专注于真实社交的交友应用,基于同城与兴趣为你推荐合适的人。
|
||||
我们倡导真诚、友善的互动,帮助你遇见那个对的人。
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<text class="version">缘友 v{{ appVersion }}</text>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
const appVersion = ref('2.5.0')
|
||||
|
||||
function toast(msg : string) {
|
||||
uni.showToast({ title: msg, icon: 'none' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page {
|
||||
flex: 1;
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
.logo-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 80rpx 0 50rpx;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
border-radius: 36rpx;
|
||||
background: linear-gradient(135deg, #FF6B6B 0%, #FF8E53 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 10rpx 30rpx rgba(255, 107, 107, 0.3);
|
||||
}
|
||||
|
||||
.app-name {
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.app-slogan {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.list-container {
|
||||
margin: 0 30rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.list-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.list-item:active {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
.list-text {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.intro {
|
||||
margin: 30rpx;
|
||||
padding: 30rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.intro-text {
|
||||
font-size: 26rpx;
|
||||
color: #888;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.version {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
color: #bbb;
|
||||
margin: 40rpx 0 80rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<scroll-view direction="vertical" :show-scrollbar="false" style="flex:1;"
|
||||
:refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
|
||||
<view class="list">
|
||||
<view v-for="(m, idx) in list" :key="idx" class="moment-item" @click="openDetail(m)">
|
||||
<view class="m-head">
|
||||
<image class="m-avatar" :src="imgUrl(m.userAvatar)" mode="aspectFill"></image>
|
||||
<view class="m-meta">
|
||||
<text class="m-name">{{ m.userName }}</text>
|
||||
<text class="m-time">{{ formatTime(m.time) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="m-content">{{ m.content }}</text>
|
||||
<view v-if="m.images && m.images.length > 0" class="m-images">
|
||||
<image v-for="(img, i) in m.images.slice(0,3)" :key="i" class="m-img"
|
||||
:src="imgUrl(img)" mode="aspectFill"></image>
|
||||
</view>
|
||||
<view class="m-foot">
|
||||
<view class="m-stat"><uni-icons type="heart" size="22" color="#FF2D55"></uni-icons>
|
||||
<text>{{ m.likes }}</text></view>
|
||||
<view class="m-stat"><uni-icons type="chatbubble" size="22" color="#999"></uni-icons>
|
||||
<text>{{ m.comments }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="list.length === 0 && !loading" class="empty-state">
|
||||
<uni-icons type="star" size="60" color="#ddd"></uni-icons>
|
||||
<text class="empty-text">还没有收藏任何动态</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import Api from '@/common/api-service.uts'
|
||||
|
||||
const BASE = 'https://dev.xixingwl.cn'
|
||||
|
||||
type IMoment = {
|
||||
id : number
|
||||
uid : number
|
||||
userName : string
|
||||
userAvatar : string
|
||||
content : string
|
||||
images : string[]
|
||||
videoUrl : string
|
||||
type : string
|
||||
time : number
|
||||
likes : number
|
||||
comments : number
|
||||
shares : number
|
||||
}
|
||||
|
||||
const list = reactive<IMoment[]>([])
|
||||
const loading = ref<boolean>(false)
|
||||
const refreshing = ref<boolean>(false)
|
||||
|
||||
function imgUrl(src : string) : string {
|
||||
if (! src) return ''
|
||||
return src.startsWith('http') ? src : (BASE + src)
|
||||
}
|
||||
|
||||
function formatTime(ts : number) : string {
|
||||
if (! ts) return ''
|
||||
const d = new Date(ts)
|
||||
const pad = (n : number) : string => n < 10 ? '0' + n : '' + n
|
||||
return `${d.getMonth() + 1}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
}
|
||||
|
||||
function openDetail(m : IMoment) {
|
||||
uni.navigateTo({ url: '/pages/moment/details?id=' + m.id })
|
||||
}
|
||||
|
||||
function loadData() {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
Api.moment.collectedList({ page: 1, page_size: 20 })
|
||||
.then((res : UTSJSONObject) => {
|
||||
const arr = (res.get('list') as Array<IMoment>) ?? []
|
||||
list.splice(0, list.length)
|
||||
arr.forEach((m : IMoment) => list.push(m))
|
||||
})
|
||||
.catch(() => {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
refreshing.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function onRefresh() {
|
||||
refreshing.value = true
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(() => { loadData() })
|
||||
onShow(() => { loadData() })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page {
|
||||
flex: 1;
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin: 24rpx 30rpx;
|
||||
}
|
||||
|
||||
.moment-item {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.m-head {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.m-avatar {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 36rpx;
|
||||
background-color: #f0f0f0;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.m-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.m-name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.m-time {
|
||||
font-size: 22rpx;
|
||||
color: #bbb;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.m-content {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.m-images {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.m-img {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
border-radius: 12rpx;
|
||||
margin: 0 12rpx 12rpx 0;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.m-foot {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.m-stat {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
margin-right: 40rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.m-stat text {
|
||||
margin-left: 6rpx;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 120rpx 0;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #bbb;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,212 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<scroll-view direction="vertical" :show-scrollbar="false" style="flex:1;"
|
||||
:refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
|
||||
<view class="list">
|
||||
<view v-for="(m, idx) in list" :key="idx" class="moment-item" @click="openDetail(m)">
|
||||
<view class="m-head">
|
||||
<image class="m-avatar" :src="imgUrl(m.userAvatar)" mode="aspectFill"></image>
|
||||
<view class="m-meta">
|
||||
<text class="m-name">{{ m.userName }}</text>
|
||||
<text class="m-time">{{ formatTime(m.time) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="m-content">{{ m.content }}</text>
|
||||
<view v-if="m.images && m.images.length > 0" class="m-images">
|
||||
<image v-for="(img, i) in m.images.slice(0,3)" :key="i" class="m-img"
|
||||
:src="imgUrl(img)" mode="aspectFill"></image>
|
||||
</view>
|
||||
<view class="m-foot">
|
||||
<view class="m-stat"><uni-icons type="heart" size="22" color="#FF2D55"></uni-icons>
|
||||
<text>{{ m.likes }}</text></view>
|
||||
<view class="m-stat"><uni-icons type="chatbubble" size="22" color="#999"></uni-icons>
|
||||
<text>{{ m.comments }}</text></view>
|
||||
<view class="m-stat"><uni-icons type="star" size="22" color="#FF9500"></uni-icons>
|
||||
<text>{{ m.collectCount }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="list.length === 0 && !loading" class="empty-state">
|
||||
<uni-icons type="pyq" size="60" color="#ddd"></uni-icons>
|
||||
<text class="empty-text">你还没有发布动态</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import Api from '@/common/api-service.uts'
|
||||
|
||||
const BASE = 'https://dev.xixingwl.cn'
|
||||
|
||||
type IMoment = {
|
||||
id : number
|
||||
uid : number
|
||||
userName : string
|
||||
userAvatar : string
|
||||
content : string
|
||||
images : string[]
|
||||
videoUrl : string
|
||||
type : string
|
||||
time : number
|
||||
likes : number
|
||||
comments : number
|
||||
shares : number
|
||||
collectCount : number
|
||||
}
|
||||
|
||||
const list = reactive<IMoment[]>([])
|
||||
const loading = ref<boolean>(false)
|
||||
const refreshing = ref<boolean>(false)
|
||||
|
||||
function imgUrl(src : string) : string {
|
||||
if (! src) return ''
|
||||
return src.startsWith('http') ? src : (BASE + src)
|
||||
}
|
||||
|
||||
function formatTime(ts : number) : string {
|
||||
if (! ts) return ''
|
||||
const d = new Date(ts)
|
||||
const pad = (n : number) : string => n < 10 ? '0' + n : '' + n
|
||||
return `${d.getMonth() + 1}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
}
|
||||
|
||||
function openDetail(m : IMoment) {
|
||||
uni.navigateTo({ url: '/pages/moment/details?id=' + m.id })
|
||||
}
|
||||
|
||||
function loadData() {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
Api.moment.mine({ page: 1, page_size: 50 })
|
||||
.then((res : UTSJSONObject) => {
|
||||
const arr = (res.get('list') as Array<IMoment>) ?? []
|
||||
list.splice(0, list.length)
|
||||
arr.forEach((m : IMoment) => {
|
||||
const obj = (m as UTSJSONObject)
|
||||
const stats = obj.get('stats') as UTSJSONObject | null
|
||||
m.collectCount = stats != null ? ((stats.get('collect_count') as number) ?? 0) : 0
|
||||
list.push(m)
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
refreshing.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function onRefresh() {
|
||||
refreshing.value = true
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(() => { loadData() })
|
||||
onShow(() => { loadData() })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page {
|
||||
flex: 1;
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin: 24rpx 30rpx;
|
||||
}
|
||||
|
||||
.moment-item {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.m-head {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.m-avatar {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 36rpx;
|
||||
background-color: #f0f0f0;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.m-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.m-name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.m-time {
|
||||
font-size: 22rpx;
|
||||
color: #bbb;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.m-content {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.m-images {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.m-img {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
border-radius: 12rpx;
|
||||
margin: 0 12rpx 12rpx 0;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.m-foot {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.m-stat {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
margin-right: 40rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.m-stat text {
|
||||
margin-left: 6rpx;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 120rpx 0;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #bbb;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,212 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<scroll-view direction="vertical" :show-scrollbar="false" style="flex:1;"
|
||||
:refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh"
|
||||
@scrolltolower="onReachBottom">
|
||||
<view class="list">
|
||||
<view v-for="(item, index) in records" :key="index" class="user-item" @click="goProfile(item.id)">
|
||||
<image class="avatar" :src="item.avatar" mode="aspectFill"></image>
|
||||
<view class="info">
|
||||
<view class="name-row">
|
||||
<text class="nickname">{{ item.nickname }}</text>
|
||||
<view v-if="item.is_mutual == 1" class="mutual-tag">
|
||||
<text class="mutual-txt">互相关注</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="time">{{ item.follow_time }}</text>
|
||||
</view>
|
||||
<view v-if="item.is_mutual == 0" class="follow-btn" @click.stop="follow(item)">
|
||||
<text class="follow-txt">回关</text>
|
||||
</view>
|
||||
<uni-icons v-else type="right" size="18" color="#ccc"></uni-icons>
|
||||
</view>
|
||||
|
||||
<view v-if="records.length == 0 && !loading" class="empty-state">
|
||||
<uni-icons type="staff" size="60" color="#ddd"></uni-icons>
|
||||
<text class="empty-text">还没有粉丝</text>
|
||||
</view>
|
||||
<view v-if="loading" class="loading-tip">
|
||||
<text class="loading-txt">加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import Api from '@/common/api-service.uts'
|
||||
import { getUserInfo } from '@/stores/user.uts'
|
||||
|
||||
type IFollowUser = {
|
||||
id : number
|
||||
nickname : string
|
||||
avatar : string
|
||||
is_mutual : number
|
||||
follow_time : string
|
||||
}
|
||||
|
||||
const records = reactive<IFollowUser[]>([])
|
||||
const loading = ref<boolean>(false)
|
||||
const refreshing = ref<boolean>(false)
|
||||
const page = ref<number>(1)
|
||||
const size = ref<number>(20)
|
||||
const total = ref<number>(0)
|
||||
const uid = ref<number>(0)
|
||||
|
||||
function loadData(reset : boolean) {
|
||||
if (loading.value) return
|
||||
if (uid.value <= 0) {
|
||||
const info = getUserInfo()
|
||||
uid.value = info?.uid ?? 0
|
||||
}
|
||||
if (uid.value <= 0) return
|
||||
loading.value = true
|
||||
if (reset) page.value = 1
|
||||
Api.follow.followerList({ uid: uid.value, page: page.value, size: size.value })
|
||||
.then((res : UTSJSONObject) => {
|
||||
total.value = (res.get('total') as number) ?? 0
|
||||
const list = (res.get('list') as Array<IFollowUser>) ?? []
|
||||
if (reset) records.splice(0, records.length)
|
||||
list.forEach((r : IFollowUser) => records.push(r))
|
||||
})
|
||||
.catch(() => {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
refreshing.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function onRefresh() {
|
||||
refreshing.value = true
|
||||
loadData(true)
|
||||
}
|
||||
|
||||
function onReachBottom() {
|
||||
if (records.length >= total.value) return
|
||||
page.value = page.value + 1
|
||||
loadData(false)
|
||||
}
|
||||
|
||||
function follow(item : IFollowUser) {
|
||||
if (uid.value <= 0 || item.id <= 0) return
|
||||
Api.follow.toggle(uid.value, item.id, 1)
|
||||
.then(() => {
|
||||
item.is_mutual = 1
|
||||
uni.showToast({ title: '已回关', icon: 'success' })
|
||||
})
|
||||
.catch(() => {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
})
|
||||
}
|
||||
|
||||
function goProfile(targetUid : number) {
|
||||
if (targetUid <= 0) return
|
||||
uni.navigateTo({ url: '/pages/user/profile?uid=' + targetUid })
|
||||
}
|
||||
|
||||
onMounted(() => { loadData(true) })
|
||||
onShow(() => { loadData(true) })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page {
|
||||
flex: 1;
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin: 24rpx 30rpx;
|
||||
}
|
||||
|
||||
.user-item {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
padding: 20rpx 24rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 44rpx;
|
||||
background-color: #eee;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.name-row {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nickname {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.mutual-tag {
|
||||
margin-left: 14rpx;
|
||||
padding: 2rpx 12rpx;
|
||||
border-radius: 16rpx;
|
||||
background-color: #fff0ec;
|
||||
}
|
||||
|
||||
.mutual-txt {
|
||||
font-size: 20rpx;
|
||||
color: #FF6B6B;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 22rpx;
|
||||
color: #bbb;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.follow-btn {
|
||||
padding: 10rpx 28rpx;
|
||||
border-radius: 30rpx;
|
||||
background: linear-gradient(135deg, #FF6B6B 0%, #FF8E53 100%);
|
||||
}
|
||||
|
||||
.follow-txt {
|
||||
font-size: 24rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 120rpx 0;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #bbb;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.loading-tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 30rpx 0;
|
||||
}
|
||||
|
||||
.loading-txt {
|
||||
font-size: 24rpx;
|
||||
color: #bbb;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,181 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="-status-bar"></view>
|
||||
<view class="header">
|
||||
<text class="title">意见反馈</text>
|
||||
</view>
|
||||
<scroll-view direction="vertical" :show-scrollbar="false" style="flex:1;" class="form">
|
||||
<view class="section">
|
||||
<text class="label">反馈类型</text>
|
||||
<view class="type-row">
|
||||
<view v-for="(t, idx) in types" :key="t.value" class="type-item" :class="{ active: type == t.value }" @click="type = t.value">
|
||||
<text class="type-text">{{ t.label }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<text class="label">反馈内容</text>
|
||||
<textarea class="textarea" v-model="content" placeholder="请描述您遇到的问题或建议(最多500字)" maxlength="500" />
|
||||
<text class="counter">{{ content.length }}/500</text>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<text class="label">联系方式(选填)</text>
|
||||
<input class="input" v-model="contact" placeholder="手机号/微信,方便我们回复您" />
|
||||
</view>
|
||||
|
||||
<view class="submit-btn" :class="{ disabled: submitting }" @click="onSubmit">
|
||||
<text class="submit-text">{{ submitting ? '提交中...' : '提交反馈' }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import Api from '@/common/api-service.uts'
|
||||
|
||||
type IType = { label : string, value : number }
|
||||
|
||||
const types = reactive<IType[]>([
|
||||
{ label: '功能建议', value: 1 },
|
||||
{ label: '投诉举报', value: 2 },
|
||||
{ label: '其他', value: 3 }
|
||||
])
|
||||
const type = ref<number>(1)
|
||||
const content = ref<string>('')
|
||||
const contact = ref<string>('')
|
||||
const submitting = ref<boolean>(false)
|
||||
|
||||
function onSubmit() {
|
||||
if (submitting.value) return
|
||||
if (content.value.trim() == '') {
|
||||
uni.showToast({ title: '请输入反馈内容', icon: 'none' })
|
||||
return
|
||||
}
|
||||
submitting.value = true
|
||||
Api.feedback.save({ type: type.value, content: content.value, contact: contact.value })
|
||||
.then(() => {
|
||||
uni.showToast({ title: '提交成功', icon: 'success' })
|
||||
content.value = ''
|
||||
contact.value = ''
|
||||
})
|
||||
.catch((e : any) => {
|
||||
const msg = e && e.message ? e.message : '提交失败'
|
||||
uni.showToast({ title: msg, icon: 'none' })
|
||||
})
|
||||
.finally(() => { submitting.value = false })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page {
|
||||
flex: 1;
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
.-status-bar {
|
||||
height: var(--status-bar-height);
|
||||
background: linear-gradient(135deg, #FF6B6B 0%, #FF8E53 100%);
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #FF6B6B 0%, #FF8E53 100%);
|
||||
padding: 24rpx 30rpx 36rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.form {
|
||||
padding: 24rpx 30rpx;
|
||||
}
|
||||
|
||||
.section {
|
||||
background-color: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.type-row {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
|
||||
.type-item {
|
||||
padding: 14rpx 36rpx;
|
||||
border-radius: 30rpx;
|
||||
background-color: #f2f2f2;
|
||||
margin: 0 20rpx 16rpx 0;
|
||||
}
|
||||
|
||||
.type-item.active {
|
||||
background: linear-gradient(135deg, #FF6B6B 0%, #FF8E53 100%);
|
||||
}
|
||||
|
||||
.type-text {
|
||||
font-size: 26rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.type-item.active .type-text {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.textarea {
|
||||
width: 100%;
|
||||
height: 200rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.counter {
|
||||
font-size: 22rpx;
|
||||
color: #bbb;
|
||||
float: right;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.input {
|
||||
width: 100%;
|
||||
height: 70rpx;
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
margin: 40rpx 30rpx 60rpx;
|
||||
height: 92rpx;
|
||||
border-radius: 46rpx;
|
||||
background: linear-gradient(135deg, #FF6B6B 0%, #FF8E53 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 10rpx 30rpx rgba(255, 107, 107, 0.3);
|
||||
}
|
||||
|
||||
.submit-btn.disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.submit-text {
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,212 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<scroll-view direction="vertical" :show-scrollbar="false" style="flex:1;"
|
||||
:refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh"
|
||||
@scrolltolower="onReachBottom">
|
||||
<view class="list">
|
||||
<view v-for="(item, index) in records" :key="index" class="user-item" @click="goProfile(item.id)">
|
||||
<image class="avatar" :src="item.avatar" mode="aspectFill"></image>
|
||||
<view class="info">
|
||||
<view class="name-row">
|
||||
<text class="nickname">{{ item.nickname }}</text>
|
||||
<view v-if="item.is_mutual == 1" class="mutual-tag">
|
||||
<text class="mutual-txt">互相关注</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="time">{{ item.follow_time }}</text>
|
||||
</view>
|
||||
<view v-if="item.is_mutual == 0" class="follow-btn" @click.stop="follow(item)">
|
||||
<text class="follow-txt">回关</text>
|
||||
</view>
|
||||
<uni-icons v-else type="right" size="18" color="#ccc"></uni-icons>
|
||||
</view>
|
||||
|
||||
<view v-if="records.length == 0 && !loading" class="empty-state">
|
||||
<uni-icons type="staff" size="60" color="#ddd"></uni-icons>
|
||||
<text class="empty-text">还没有粉丝</text>
|
||||
</view>
|
||||
<view v-if="loading" class="loading-tip">
|
||||
<text class="loading-txt">加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import Api from '@/common/api-service.uts'
|
||||
import { getUserInfo } from '@/stores/user.uts'
|
||||
|
||||
type IFollowUser = {
|
||||
id : number
|
||||
nickname : string
|
||||
avatar : string
|
||||
is_mutual : number
|
||||
follow_time : string
|
||||
}
|
||||
|
||||
const records = reactive<IFollowUser[]>([])
|
||||
const loading = ref<boolean>(false)
|
||||
const refreshing = ref<boolean>(false)
|
||||
const page = ref<number>(1)
|
||||
const size = ref<number>(20)
|
||||
const total = ref<number>(0)
|
||||
const uid = ref<number>(0)
|
||||
|
||||
function loadData(reset : boolean) {
|
||||
if (loading.value) return
|
||||
if (uid.value <= 0) {
|
||||
const info = getUserInfo()
|
||||
uid.value = info?.uid ?? 0
|
||||
}
|
||||
if (uid.value <= 0) return
|
||||
loading.value = true
|
||||
if (reset) page.value = 1
|
||||
Api.follow.followerList({ uid: uid.value, page: page.value, size: size.value })
|
||||
.then((res : UTSJSONObject) => {
|
||||
total.value = (res.get('total') as number) ?? 0
|
||||
const list = (res.get('list') as Array<IFollowUser>) ?? []
|
||||
if (reset) records.splice(0, records.length)
|
||||
list.forEach((r : IFollowUser) => records.push(r))
|
||||
})
|
||||
.catch(() => {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
refreshing.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function onRefresh() {
|
||||
refreshing.value = true
|
||||
loadData(true)
|
||||
}
|
||||
|
||||
function onReachBottom() {
|
||||
if (records.length >= total.value) return
|
||||
page.value = page.value + 1
|
||||
loadData(false)
|
||||
}
|
||||
|
||||
function follow(item : IFollowUser) {
|
||||
if (uid.value <= 0 || item.id <= 0) return
|
||||
Api.follow.toggle(uid.value, item.id, 1)
|
||||
.then(() => {
|
||||
item.is_mutual = 1
|
||||
uni.showToast({ title: '已回关', icon: 'success' })
|
||||
})
|
||||
.catch(() => {
|
||||
uni.showToast({ title: '操作失败', icon: 'none' })
|
||||
})
|
||||
}
|
||||
|
||||
function goProfile(targetUid : number) {
|
||||
if (targetUid <= 0) return
|
||||
uni.navigateTo({ url: '/pages/user/profile?uid=' + targetUid })
|
||||
}
|
||||
|
||||
onMounted(() => { loadData(true) })
|
||||
onShow(() => { loadData(true) })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page {
|
||||
flex: 1;
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin: 24rpx 30rpx;
|
||||
}
|
||||
|
||||
.user-item {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
padding: 20rpx 24rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 44rpx;
|
||||
background-color: #eee;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.name-row {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nickname {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.mutual-tag {
|
||||
margin-left: 14rpx;
|
||||
padding: 2rpx 12rpx;
|
||||
border-radius: 16rpx;
|
||||
background-color: #fff0ec;
|
||||
}
|
||||
|
||||
.mutual-txt {
|
||||
font-size: 20rpx;
|
||||
color: #FF6B6B;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 22rpx;
|
||||
color: #bbb;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.follow-btn {
|
||||
padding: 10rpx 28rpx;
|
||||
border-radius: 30rpx;
|
||||
background: linear-gradient(135deg, #FF6B6B 0%, #FF8E53 100%);
|
||||
}
|
||||
|
||||
.follow-txt {
|
||||
font-size: 24rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 120rpx 0;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #bbb;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.loading-tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 30rpx 0;
|
||||
}
|
||||
|
||||
.loading-txt {
|
||||
font-size: 24rpx;
|
||||
color: #bbb;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<scroll-view direction="vertical" :show-scrollbar="false" style="flex:1;"
|
||||
:refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh"
|
||||
@scrolltolower="onReachBottom">
|
||||
<view class="list">
|
||||
<view v-for="(item, index) in records" :key="index" class="user-item" @click="goProfile(item.id)">
|
||||
<image class="avatar" :src="item.avatar" mode="aspectFill"></image>
|
||||
<view class="info">
|
||||
<view class="name-row">
|
||||
<text class="nickname">{{ item.nickname }}</text>
|
||||
<view v-if="item.is_mutual == 1" class="mutual-tag">
|
||||
<text class="mutual-txt">互相关注</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="time">{{ item.follow_time }}</text>
|
||||
</view>
|
||||
<uni-icons type="right" size="18" color="#ccc"></uni-icons>
|
||||
</view>
|
||||
|
||||
<view v-if="records.length == 0 && !loading" class="empty-state">
|
||||
<uni-icons type="staff" size="60" color="#ddd"></uni-icons>
|
||||
<text class="empty-text">还没有关注任何人</text>
|
||||
</view>
|
||||
<view v-if="loading" class="loading-tip">
|
||||
<text class="loading-txt">加载中...</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import Api from '@/common/api-service.uts'
|
||||
import { getUserInfo } from '@/stores/user.uts'
|
||||
|
||||
type IFollowUser = {
|
||||
id : number
|
||||
nickname : string
|
||||
avatar : string
|
||||
is_mutual : number
|
||||
follow_time : string
|
||||
}
|
||||
|
||||
const records = reactive<IFollowUser[]>([])
|
||||
const loading = ref<boolean>(false)
|
||||
const refreshing = ref<boolean>(false)
|
||||
const page = ref<number>(1)
|
||||
const size = ref<number>(20)
|
||||
const total = ref<number>(0)
|
||||
const uid = ref<number>(0)
|
||||
|
||||
function loadData(reset : boolean) {
|
||||
if (loading.value) return
|
||||
if (uid.value <= 0) {
|
||||
const info = getUserInfo()
|
||||
uid.value = info?.uid ?? 0
|
||||
}
|
||||
if (uid.value <= 0) return
|
||||
loading.value = true
|
||||
if (reset) page.value = 1
|
||||
Api.follow.followingList({ uid: uid.value, page: page.value, size: size.value })
|
||||
.then((res : UTSJSONObject) => {
|
||||
total.value = (res.get('total') as number) ?? 0
|
||||
const list = (res.get('list') as Array<IFollowUser>) ?? []
|
||||
if (reset) records.splice(0, records.length)
|
||||
list.forEach((r : IFollowUser) => records.push(r))
|
||||
})
|
||||
.catch(() => {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
refreshing.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function onRefresh() {
|
||||
refreshing.value = true
|
||||
loadData(true)
|
||||
}
|
||||
|
||||
function onReachBottom() {
|
||||
if (records.length >= total.value) return
|
||||
page.value = page.value + 1
|
||||
loadData(false)
|
||||
}
|
||||
|
||||
function goProfile(targetUid : number) {
|
||||
if (targetUid <= 0) return
|
||||
uni.navigateTo({ url: '/pages/user/profile?uid=' + targetUid })
|
||||
}
|
||||
|
||||
onMounted(() => { loadData(true) })
|
||||
onShow(() => { loadData(true) })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page {
|
||||
flex: 1;
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin: 24rpx 30rpx;
|
||||
}
|
||||
|
||||
.user-item {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
padding: 20rpx 24rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 44rpx;
|
||||
background-color: #eee;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.name-row {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nickname {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.mutual-tag {
|
||||
margin-left: 14rpx;
|
||||
padding: 2rpx 12rpx;
|
||||
border-radius: 16rpx;
|
||||
background-color: #fff0ec;
|
||||
}
|
||||
|
||||
.mutual-txt {
|
||||
font-size: 20rpx;
|
||||
color: #FF6B6B;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 22rpx;
|
||||
color: #bbb;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 120rpx 0;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #bbb;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.loading-tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 30rpx 0;
|
||||
}
|
||||
|
||||
.loading-txt {
|
||||
font-size: 24rpx;
|
||||
color: #bbb;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,233 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<scroll-view direction="vertical" :show-scrollbar="false" style="flex:1;"
|
||||
:refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
|
||||
<!-- 汇总卡片 -->
|
||||
<view class="summary-card">
|
||||
<view class="summary-item">
|
||||
<text class="summary-num">{{ totalCount }}</text>
|
||||
<text class="summary-label">收到礼物</text>
|
||||
</view>
|
||||
<view class="summary-divider"></view>
|
||||
<view class="summary-item">
|
||||
<text class="summary-num">{{ totalValue }}</text>
|
||||
<text class="summary-label">礼物价值(金币)</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 礼物记录 -->
|
||||
<view class="list">
|
||||
<view v-for="(item, index) in records" :key="index" class="gift-item">
|
||||
<view class="gift-icon">
|
||||
<uni-icons type="gift-filled" size="30" color="#FF6B6B"></uni-icons>
|
||||
</view>
|
||||
<view class="gift-info">
|
||||
<text class="gift-name">{{ item.gift_name }} x{{ item.count }}</text>
|
||||
<text v-if="item.message.length > 0" class="gift-msg">{{ item.message }}</text>
|
||||
<text class="gift-time">{{ formatTime(item.create_at) }}</text>
|
||||
</view>
|
||||
<view class="gift-value">
|
||||
<text class="value-num">+{{ item.total_price }}</text>
|
||||
<text class="value-label">金币</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="records.length === 0 && !loading" class="empty-state">
|
||||
<uni-icons type="gift" size="60" color="#ddd"></uni-icons>
|
||||
<text class="empty-text">还没有收到礼物哦</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import Api from '@/common/api-service.uts'
|
||||
|
||||
type IGiftRecord = {
|
||||
id : number
|
||||
sender_uid : number
|
||||
receiver_uid : number
|
||||
gift_id : number
|
||||
gift_name : string
|
||||
price : number
|
||||
count : number
|
||||
total_price : number
|
||||
message : string
|
||||
create_at : number
|
||||
}
|
||||
|
||||
const records = reactive<IGiftRecord[]>([])
|
||||
const loading = ref<boolean>(false)
|
||||
const refreshing = ref<boolean>(false)
|
||||
|
||||
const totalCount = computed<number>(() => {
|
||||
let c = 0
|
||||
records.forEach((r : IGiftRecord) => { c += r.count })
|
||||
return c
|
||||
})
|
||||
const totalValue = computed<number>(() => {
|
||||
let v = 0
|
||||
records.forEach((r : IGiftRecord) => { v += r.total_price })
|
||||
return v
|
||||
})
|
||||
|
||||
function loadData() {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
Api.gift.received({})
|
||||
.then((res : UTSJSONObject) => {
|
||||
const list = (res as Array<IGiftRecord>) ?? []
|
||||
records.splice(0, records.length)
|
||||
list.forEach((r : IGiftRecord) => records.push(r))
|
||||
})
|
||||
.catch(() => {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
refreshing.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function onRefresh() {
|
||||
refreshing.value = true
|
||||
loadData()
|
||||
}
|
||||
|
||||
function formatTime(ts : number) : string {
|
||||
if (ts <= 0) return ''
|
||||
const d = new Date(ts * 1000)
|
||||
const m = d.getMonth() + 1
|
||||
const day = d.getDate()
|
||||
const h = d.getHours()
|
||||
const min = d.getMinutes()
|
||||
const pad = (n : number) : string => n < 10 ? '0' + n : '' + n
|
||||
return `${m}-${pad(day)} ${pad(h)}:${pad(min)}`
|
||||
}
|
||||
|
||||
onMounted(() => { loadData() })
|
||||
onShow(() => { loadData() })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page {
|
||||
flex: 1;
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
margin: 24rpx 30rpx;
|
||||
padding: 40rpx 0;
|
||||
background: linear-gradient(135deg, #FF6B6B 0%, #FF8E53 100%);
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.summary-num {
|
||||
font-size: 44rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-size: 24rpx;
|
||||
color: #ffe;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.summary-divider {
|
||||
width: 2rpx;
|
||||
height: 60rpx;
|
||||
background-color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.list {
|
||||
margin: 0 30rpx;
|
||||
}
|
||||
|
||||
.gift-item {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
padding: 24rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.gift-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
background-color: #fff0ec;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.gift-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.gift-name {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.gift-msg {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
|
||||
.gift-time {
|
||||
font-size: 22rpx;
|
||||
color: #bbb;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
|
||||
.gift-value {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.value-num {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #FF6B6B;
|
||||
}
|
||||
|
||||
.value-label {
|
||||
font-size: 22rpx;
|
||||
color: #FF6B6B;
|
||||
margin-left: 4rpx;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 120rpx 0;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #bbb;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<scroll-view direction="vertical" :show-scrollbar="false" style="flex:1;">
|
||||
<view class="hero">
|
||||
<uni-icons type="help-filled" size="48" color="#FF6B6B"></uni-icons>
|
||||
<text class="hero-title">常见问题</text>
|
||||
</view>
|
||||
|
||||
<view class="qa" v-for="(item, idx) in qaList" :key="idx">
|
||||
<view class="q" @click="toggle(idx)">
|
||||
<text class="q-text">Q:{{ item.q }}</text>
|
||||
<uni-icons :type="item.open ? 'top' : 'right'" size="18" color="#999"></uni-icons>
|
||||
</view>
|
||||
<view v-if="item.open" class="a">
|
||||
<text class="a-text">{{ item.a }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="contact">
|
||||
<text class="contact-title">仍未解决?</text>
|
||||
<view class="contact-btn" @click="copyMail">
|
||||
<uni-icons type="email" size="20" color="#fff"></uni-icons>
|
||||
<text class="contact-btn-text">联系客服:support@xixingwl.cn</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
type QA = {
|
||||
q : string
|
||||
a : string
|
||||
open : boolean
|
||||
}
|
||||
|
||||
const qaList = reactive<QA[]>([
|
||||
{ q: '如何修改个人资料?', a: '在「我的」页面点击左上角头像区域,或在资料卡中点击编辑即可修改昵称、签名、择偶条件等信息。', open: true },
|
||||
{ q: '金币有什么用?', a: '金币可用于每日抽奖、赠送礼物给喜欢的用户,部分特权功能也需消耗金币。', open: false },
|
||||
{ q: '为什么看不到对方的在线状态?', a: '对方可能在隐私设置中隐藏了在线状态、年龄或距离,这是对方的个人选择,无法强制查看。', open: false },
|
||||
{ q: '动态被收藏/点赞会有提醒吗?', a: '当前版本点赞与收藏会累计到「我的」统计中,消息中心会同步相关互动通知。', open: false },
|
||||
{ q: '如何注销账号?', a: '进入「账号安全」页面,点击「注销账号」,按提示操作即可。注销后资料将被匿名化处理。', open: false },
|
||||
])
|
||||
|
||||
function toggle(idx : number) {
|
||||
qaList[idx].open = ! qaList[idx].open
|
||||
}
|
||||
|
||||
function copyMail() {
|
||||
uni.setClipboardData({
|
||||
data: 'support@xixingwl.cn',
|
||||
success: () => {
|
||||
uni.showToast({ title: '邮箱已复制', icon: 'success' })
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page {
|
||||
flex: 1;
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 50rpx 0 30rpx;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.qa {
|
||||
margin: 0 30rpx 16rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.q {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 28rpx 24rpx;
|
||||
}
|
||||
|
||||
.q-text {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
flex: 1;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.a {
|
||||
padding: 0 24rpx 28rpx;
|
||||
}
|
||||
|
||||
.a-text {
|
||||
font-size: 26rpx;
|
||||
color: #888;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.contact {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 40rpx 0 80rpx;
|
||||
}
|
||||
|
||||
.contact-title {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.contact-btn {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, #FF6B6B 0%, #FF8E53 100%);
|
||||
padding: 20rpx 36rpx;
|
||||
border-radius: 40rpx;
|
||||
}
|
||||
|
||||
.contact-btn-text {
|
||||
color: #fff;
|
||||
font-size: 26rpx;
|
||||
margin-left: 12rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,204 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<scroll-view direction="vertical" :show-scrollbar="false" style="flex:1;"
|
||||
:refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
|
||||
<view class="list">
|
||||
<view v-for="(m, idx) in list" :key="idx" class="moment-item" @click="openDetail(m)">
|
||||
<view class="m-head">
|
||||
<image class="m-avatar" :src="imgUrl(m.userAvatar)" mode="aspectFill"></image>
|
||||
<view class="m-meta">
|
||||
<text class="m-name">{{ m.userName }}</text>
|
||||
<text class="m-time">{{ formatTime(m.time) }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="m-content">{{ m.content }}</text>
|
||||
<view v-if="m.images && m.images.length > 0" class="m-images">
|
||||
<image v-for="(img, i) in m.images.slice(0,3)" :key="i" class="m-img"
|
||||
:src="imgUrl(img)" mode="aspectFill"></image>
|
||||
</view>
|
||||
<view class="m-foot">
|
||||
<view class="m-stat"><uni-icons type="heart" size="22" color="#FF2D55"></uni-icons>
|
||||
<text>{{ m.likes }}</text></view>
|
||||
<view class="m-stat"><uni-icons type="chatbubble" size="22" color="#999"></uni-icons>
|
||||
<text>{{ m.comments }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="list.length === 0 && !loading" class="empty-state">
|
||||
<uni-icons type="eye" size="60" color="#ddd"></uni-icons>
|
||||
<text class="empty-text">还没有浏览记录</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import Api from '@/common/api-service.uts'
|
||||
|
||||
const BASE = 'https://dev.xixingwl.cn'
|
||||
|
||||
type IMoment = {
|
||||
id : number
|
||||
uid : number
|
||||
userName : string
|
||||
userAvatar : string
|
||||
content : string
|
||||
images : string[]
|
||||
videoUrl : string
|
||||
type : string
|
||||
time : number
|
||||
likes : number
|
||||
comments : number
|
||||
shares : number
|
||||
}
|
||||
|
||||
const list = reactive<IMoment[]>([])
|
||||
const loading = ref<boolean>(false)
|
||||
const refreshing = ref<boolean>(false)
|
||||
|
||||
function imgUrl(src : string) : string {
|
||||
if (! src) return ''
|
||||
return src.startsWith('http') ? src : (BASE + src)
|
||||
}
|
||||
|
||||
function formatTime(ts : number) : string {
|
||||
if (! ts) return ''
|
||||
const d = new Date(ts)
|
||||
const pad = (n : number) : string => n < 10 ? '0' + n : '' + n
|
||||
return `${d.getMonth() + 1}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
}
|
||||
|
||||
function openDetail(m : IMoment) {
|
||||
uni.navigateTo({ url: '/pages/moment/details?id=' + m.id })
|
||||
}
|
||||
|
||||
function loadData() {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
Api.moment.historyList({ page: 1, page_size: 20 })
|
||||
.then((res : UTSJSONObject) => {
|
||||
const arr = (res.get('list') as Array<IMoment>) ?? []
|
||||
list.splice(0, list.length)
|
||||
arr.forEach((m : IMoment) => list.push(m))
|
||||
})
|
||||
.catch(() => {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
refreshing.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function onRefresh() {
|
||||
refreshing.value = true
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(() => { loadData() })
|
||||
onShow(() => { loadData() })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page {
|
||||
flex: 1;
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin: 24rpx 30rpx;
|
||||
}
|
||||
|
||||
.moment-item {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.m-head {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.m-avatar {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 36rpx;
|
||||
background-color: #f0f0f0;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.m-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.m-name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.m-time {
|
||||
font-size: 22rpx;
|
||||
color: #bbb;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.m-content {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.m-images {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.m-img {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
border-radius: 12rpx;
|
||||
margin: 0 12rpx 12rpx 0;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.m-foot {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.m-stat {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
margin-right: 40rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.m-stat text {
|
||||
margin-left: 6rpx;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 120rpx 0;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #bbb;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,188 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="-status-bar"></view>
|
||||
<view class="header">
|
||||
<uni-icons type="vip" size="44" color="#FFD700"></uni-icons>
|
||||
<text class="title">会员中心</text>
|
||||
<text class="sub">开通会员,尊享更多特权</text>
|
||||
</view>
|
||||
|
||||
<scroll-view direction="vertical" :show-scrollbar="false" style="flex:1;">
|
||||
<view class="plan-row">
|
||||
<view v-for="(p, idx) in plans" :key="p.id" class="plan-card" :class="{ active: selected == p.id }" @click="selected = p.id">
|
||||
<text class="plan-name">{{ p.name }}</text>
|
||||
<view class="plan-price">
|
||||
<text class="price-num">¥{{ p.price }}</text>
|
||||
<text class="price-unit">/{{ p.unit }}</text>
|
||||
</view>
|
||||
<text class="plan-tip">{{ p.tip }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="benefits">
|
||||
<text class="benefits-title">会员特权</text>
|
||||
<view class="benefit-item" v-for="(b, idx) in benefits" :key="idx">
|
||||
<uni-icons type="checkmark" size="18" color="#34C759"></uni-icons>
|
||||
<text class="benefit-text">{{ b }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="open-btn" @click="onOpen">
|
||||
<text class="open-text">立即开通</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
type IPlan = { id : number, name : string, price : number, unit : string, tip : string }
|
||||
|
||||
const selected = ref<number>(2)
|
||||
const plans = reactive<IPlan[]>([
|
||||
{ id: 1, name: '月度会员', price: 30, unit: '月', tip: '灵活体验' },
|
||||
{ id: 2, name: '季度会员', price: 78, unit: '季', tip: '省21元' },
|
||||
{ id: 3, name: '年度会员', price: 268, unit: '年', tip: '最划算' }
|
||||
])
|
||||
const benefits = reactive<string[]>([
|
||||
'无限畅聊,消息免打扰',
|
||||
'查看谁看过我 / 超级喜欢',
|
||||
'每日专属推荐位曝光',
|
||||
'专属动态置顶与标识',
|
||||
'会员专属装扮与客服优先'
|
||||
])
|
||||
|
||||
function onOpen() {
|
||||
uni.showToast({ title: '跳转支付(演示)', icon: 'none' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page {
|
||||
flex: 1;
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
.-status-bar {
|
||||
height: var(--status-bar-height);
|
||||
background: linear-gradient(135deg, #2C2C3A 0%, #1A1A24 100%);
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #2C2C3A 0%, #1A1A24 100%);
|
||||
padding: 50rpx 30rpx 60rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
color: #FFD700;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.sub {
|
||||
font-size: 24rpx;
|
||||
color: #bbb;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.plan-row {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
padding: 30rpx;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.plan-card {
|
||||
width: 31%;
|
||||
padding: 30rpx 0;
|
||||
background-color: #fff;
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
border: 2rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.plan-card.active {
|
||||
border-color: #FFD700;
|
||||
background: linear-gradient(180deg, #FFF8E1 0%, #FFFFFF 100%);
|
||||
}
|
||||
|
||||
.plan-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.plan-price {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: baseline;
|
||||
margin: 16rpx 0 8rpx;
|
||||
}
|
||||
|
||||
.price-num {
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
color: #FF9500;
|
||||
}
|
||||
|
||||
.price-unit {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-left: 4rpx;
|
||||
}
|
||||
|
||||
.plan-tip {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.benefits {
|
||||
margin: 10rpx 30rpx 0;
|
||||
background-color: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.benefits-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 20rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.benefit-item {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
padding: 16rpx 0;
|
||||
}
|
||||
|
||||
.benefit-text {
|
||||
font-size: 28rpx;
|
||||
color: #555;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.open-btn {
|
||||
margin: 40rpx 60rpx 60rpx;
|
||||
height: 92rpx;
|
||||
border-radius: 46rpx;
|
||||
background: linear-gradient(135deg, #FFD700 0%, #FFA000 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 10rpx 30rpx rgba(255, 168, 0, 0.3);
|
||||
}
|
||||
|
||||
.open-text {
|
||||
color: #5a3d00;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,815 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="-status-bar"></view>
|
||||
<scroll-view direction="vertical" :show-scrollbar="false" style="height: 100%;">
|
||||
<!-- 顶部背景 -->
|
||||
<view class="top-background"></view>
|
||||
<!-- 用户信息卡片 -->
|
||||
<view class="user-card">
|
||||
<view class="user-header">
|
||||
<view class="avatar-section">
|
||||
<image class="avatar" :src=" avatar" mode="aspectFill"></image>
|
||||
<!-- <text class="vip-badge" v-if="userInfo.vip">
|
||||
<uni-icons type="vip" size="16" color="#fff"></uni-icons>
|
||||
</text> -->
|
||||
<!-- <view class="online-status" v-if="userInfo.online"></view> -->
|
||||
</view>
|
||||
<view class="user-info">
|
||||
<view class="name-section">
|
||||
<text class="nickname">{{ userInfo?.nickname }}</text>
|
||||
<!-- <text class="level-badge">Lv.{{ userInfo.level }}</text> -->
|
||||
<!-- <view class="gender-badge" :class="userInfo.gender">
|
||||
<uni-icons :type="userInfo.gender === 'male' ? 'male' : 'female'" size="12"
|
||||
:color="userInfo.gender === 'male' ? '#007AFF' : '#FF2D55'"></uni-icons>
|
||||
</view> -->
|
||||
</view>
|
||||
|
||||
<text class="user-id">ID: {{ userInfo?.uid }}</text>
|
||||
<!-- <text class="signature">{{ userInfo.signature !=null ?userInfo.signature : '这家伙很懒,什么都没留下' }}</text> -->
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="stats-row">
|
||||
<view class="stat-item" @click="navigateTo('followers')">
|
||||
<text class="stat-value">{{ formatNumber(stats.friends) }}</text>
|
||||
<text class="stat-label">缘友</text>
|
||||
</view>
|
||||
<view class="stat-item" @click="navigateTo('followers')">
|
||||
<text class="stat-value">{{ formatNumber(stats.followers) }}</text>
|
||||
<text class="stat-label">粉丝</text>
|
||||
</view>
|
||||
<view class="stat-divider"></view>
|
||||
<view class="stat-item" @click="navigateTo('following')">
|
||||
<text class="stat-value">{{ formatNumber(stats.following) }}</text>
|
||||
<text class="stat-label">关注</text>
|
||||
</view>
|
||||
<view class="stat-divider"></view>
|
||||
<view class="stat-item" @click="navigateTo('likes')">
|
||||
<text class="stat-value">{{ formatNumber(stats.likes) }}</text>
|
||||
<text class="stat-label">获赞</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 钱包和收益区域 -->
|
||||
<view class="wallet-section">
|
||||
<view class="wallet-item" @click="navigateTo('wallet')">
|
||||
<uni-icons type="wallet" size="32" color="#FF9500"></uni-icons>
|
||||
<text class="wallet-label">我的钱包</text>
|
||||
<uni-icons type="right" size="16" color="#999"></uni-icons>
|
||||
</view>
|
||||
<view class="divider"></view>
|
||||
<view class="wallet-item" @click="navigateTo('income')">
|
||||
<uni-icons type="vip" size="32" color="#FF3B30"></uni-icons>
|
||||
<text class="wallet-label">会员中心</text>
|
||||
<uni-icons type="right" size="16" color="#999"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 功能区网格 -->
|
||||
<view class="function-grid">
|
||||
<view class="grid-row">
|
||||
<view class="grid-item" @click="navigateTo('dynamic')">
|
||||
<view class="grid-icon">
|
||||
<uni-icons type="pyq" size="32" color="#ffd790"></uni-icons>
|
||||
</view>
|
||||
<text class="grid-text">我的动态</text>
|
||||
<!-- <view v-if="userInfo.newDynamic" class="badge-dot"></view> -->
|
||||
</view>
|
||||
<view class="grid-item" @click="navigateTo('collect')">
|
||||
<view class="grid-icon">
|
||||
<uni-icons type="star" size="32" color="#FF9500"></uni-icons>
|
||||
</view>
|
||||
<text class="grid-text">我的收藏</text>
|
||||
</view>
|
||||
<view class="grid-item" @click="navigateTo('history')">
|
||||
<view class="grid-icon">
|
||||
<uni-icons type="heart" size="32" color="#FF2D55"></uni-icons>
|
||||
</view>
|
||||
<text class="grid-text">浏览记录</text>
|
||||
</view>
|
||||
<view class="grid-item" @click="navigateTo('gift')">
|
||||
<view class="grid-icon">
|
||||
<uni-icons type="gift" size="32" color="#5856D6"></uni-icons>
|
||||
</view>
|
||||
<text class="grid-text">我的礼物</text>
|
||||
<!-- <view v-if="userInfo.newGift" class="badge-dot"></view> -->
|
||||
</view>
|
||||
</view>
|
||||
<view class="grid-row">
|
||||
<view class="grid-item" @click="navigateTo('fans')">
|
||||
<view class="grid-icon">
|
||||
<uni-icons type="person" size="32" color="#007AFF"></uni-icons>
|
||||
</view>
|
||||
<text class="grid-text">粉丝团</text>
|
||||
</view>
|
||||
<view class="grid-item" @click="navigateTo('ranking')">
|
||||
<view class="grid-icon">
|
||||
<uni-icons type="list" size="32" color="#FFCC00"></uni-icons>
|
||||
</view>
|
||||
<text class="grid-text">排行榜</text>
|
||||
</view>
|
||||
<view class="grid-item" @click="navigateTo('task')">
|
||||
<view class="grid-icon">
|
||||
<uni-icons type="calendar" size="32" color="#5AC8FA"></uni-icons>
|
||||
</view>
|
||||
<text class="grid-text">每日任务</text>
|
||||
</view>
|
||||
<view class="grid-item" @click="navigateTo('lottery')">
|
||||
<view class="grid-icon">
|
||||
<uni-icons type="help" size="32" color="#AF52DE"></uni-icons>
|
||||
</view>
|
||||
<text class="grid-text">幸运抽奖</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 列表功能区 -->
|
||||
<view class="list-section">
|
||||
<text class="list-title">账号与安全</text>
|
||||
<view class="list-container">
|
||||
<view class="list-item" @click="navigateTo('security')">
|
||||
<view class="list-left">
|
||||
<uni-icons type="locked" size="20" color="#007AFF"></uni-icons>
|
||||
<text class="list-text">账号安全</text>
|
||||
</view>
|
||||
<uni-icons type="right" size="16" color="#999"></uni-icons>
|
||||
</view>
|
||||
<view class="list-item" @click="navigateTo('privacy')">
|
||||
<view class="list-left">
|
||||
<uni-icons type="eye" size="20" color="#34C759"></uni-icons>
|
||||
<text class="list-text">隐私设置</text>
|
||||
</view>
|
||||
<uni-icons type="right" size="16" color="#999"></uni-icons>
|
||||
</view>
|
||||
<view class="list-item" @click="navigateTo('notification')">
|
||||
<view class="list-left">
|
||||
<uni-icons type="sound" size="20" color="#FF9500"></uni-icons>
|
||||
<text class="list-text">消息通知</text>
|
||||
</view>
|
||||
<view class="list-right">
|
||||
<!-- <text class="list-desc">{{ userInfo.notification ? '已开启' : '已关闭' }}</text> -->
|
||||
<uni-icons type="right" size="16" color="#999"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<text class="list-title">服务与支持</text>
|
||||
<view class="list-container">
|
||||
<view class="list-item" @click="navigateTo('feedback')">
|
||||
<view class="list-left">
|
||||
<uni-icons type="chatbubble" size="20" color="#5856D6"></uni-icons>
|
||||
<text class="list-text">意见反馈</text>
|
||||
</view>
|
||||
<uni-icons type="right" size="16" color="#999"></uni-icons>
|
||||
</view>
|
||||
<view class="list-item" @click="navigateTo('help')">
|
||||
<view class="list-left">
|
||||
<uni-icons type="help" size="20" color="#FF3B30"></uni-icons>
|
||||
<text class="list-text">帮助中心</text>
|
||||
</view>
|
||||
<uni-icons type="right" size="16" color="#999"></uni-icons>
|
||||
</view>
|
||||
<view class="list-item" @click="navigateTo('about')">
|
||||
<view class="list-left">
|
||||
<uni-icons type="info" size="20" color="#5AC8FA"></uni-icons>
|
||||
<text class="list-text">关于我们</text>
|
||||
</view>
|
||||
<view class="list-right">
|
||||
<text class="list-desc">v{{ appVersion }}</text>
|
||||
<uni-icons type="right" size="16" color="#999"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
<view class="list-item" @click="showLogoutDialog">
|
||||
<view class="list-left">
|
||||
<uni-icons type="redo" size="20" color="#FF9500"></uni-icons>
|
||||
<text class="list-text logout-text">退出登录</text>
|
||||
</view>
|
||||
<uni-icons type="right" size="16" color="#999"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import userState from "@/stores/user.uts"
|
||||
import { IUserInfo } from "@/types/user.uts"
|
||||
import Api from "@/common/api-service.uts"
|
||||
|
||||
const userInfo = computed(() : IUserInfo | null => {
|
||||
return userState.info ?? null
|
||||
})
|
||||
|
||||
// 我的聚合统计(缘友/粉丝/关注/获赞/动态)
|
||||
const stats = reactive({
|
||||
friends: 0,
|
||||
followers: 0,
|
||||
following: 0,
|
||||
likes: 0,
|
||||
moments: 0
|
||||
})
|
||||
const loadStats = () => {
|
||||
Api.user.summary()
|
||||
.then((res : UTSJSONObject) => {
|
||||
stats.friends = (res.get('friends') as number) ?? 0
|
||||
stats.followers = (res.get('followers') as number) ?? 0
|
||||
stats.following = (res.get('following') as number) ?? 0
|
||||
stats.likes = (res.get('likes') as number) ?? 0
|
||||
stats.moments = (res.get('moments') as number) ?? 0
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
const avatar = computed(() => {
|
||||
return 'https://dev.xixingwl.cn' + userInfo.value?.avatar
|
||||
})
|
||||
// 响应式数据
|
||||
/* const userInfo = reactive<IUserInfo>({
|
||||
userId: '123456789',
|
||||
nickname: '心动玩家',
|
||||
avatar: "https://dev.xixingwl.cn/" + userState.user?.avatar,//'https://randomuser.me/api/portraits/men/32.jpg',
|
||||
signature: '阳光正好,微风不燥,遇见你真好~',
|
||||
gender: 'male',
|
||||
age: 25,
|
||||
level: 12,
|
||||
vip: true,
|
||||
online: true,
|
||||
balance: 888.88,
|
||||
todayIncome: 128.50,
|
||||
followers: 12560,
|
||||
following: 230,
|
||||
likes: 45620,
|
||||
newDynamic: true,
|
||||
newGift: false,
|
||||
notification: true
|
||||
} as IUserInfo) */
|
||||
|
||||
const appVersion = ref('2.5.0')
|
||||
// 加载用户信息
|
||||
const loadUserInfo = () => {
|
||||
console.log('加载用户信息...')
|
||||
// 模拟API调用
|
||||
setTimeout(() => {
|
||||
// 这里应该是从API获取数据
|
||||
console.log('用户信息加载完成')
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
// 页面加载
|
||||
onMounted(() => {
|
||||
console.log('我的页面加载完成')
|
||||
// 这里可以加载用户数据
|
||||
loadUserInfo()
|
||||
loadStats()
|
||||
})
|
||||
onShow(() => {
|
||||
loadStats()
|
||||
})
|
||||
|
||||
|
||||
// 格式化数字(千分位)
|
||||
const formatNumber = (num : number) : string => {
|
||||
if (num >= 10000) {
|
||||
return (num / 10000).toFixed(1) + 'w'
|
||||
} else if (num >= 1000) {
|
||||
return (num / 1000).toFixed(1) + 'k'
|
||||
}
|
||||
return num.toString()
|
||||
}
|
||||
|
||||
// 编辑资料
|
||||
const editProfile = () => {
|
||||
console.log('编辑资料')
|
||||
uni.navigateTo({
|
||||
url: '/pages/edit-profile/edit-profile'
|
||||
})
|
||||
}
|
||||
|
||||
// 开通VIP
|
||||
const openVip = () => {
|
||||
console.log('开通VIP')
|
||||
uni.showModal({
|
||||
title: '开通VIP会员',
|
||||
content: '开通VIP享受更多特权,包括去广告、专属装扮、优先匹配等',
|
||||
confirmText: '立即开通',
|
||||
cancelText: '再想想',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
uni.showToast({
|
||||
title: '跳转支付页面',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// VIP中心
|
||||
const vipCenter = () => {
|
||||
console.log('VIP中心')
|
||||
uni.navigateTo({
|
||||
url: '/pages/vip-center/vip-center'
|
||||
})
|
||||
}
|
||||
|
||||
// 页面跳转
|
||||
const navigateTo = (page : string) => {
|
||||
console.log('跳转到页面:', page)
|
||||
const pageUrl = ref<string>('')
|
||||
switch (page) {
|
||||
case 'wallet':
|
||||
pageUrl.value = '/pages/my/wallet';
|
||||
break;
|
||||
case 'income':
|
||||
pageUrl.value = '/pages/my/income';
|
||||
break;
|
||||
case 'dynamic':
|
||||
pageUrl.value = '/pages/my/dynamic';
|
||||
break;
|
||||
case 'collect':
|
||||
pageUrl.value = '/pages/my/collect';
|
||||
break;
|
||||
case 'history':
|
||||
pageUrl.value = '/pages/my/history';
|
||||
break;
|
||||
case 'gift':
|
||||
pageUrl.value = '/pages/my/gift';
|
||||
break;
|
||||
case 'fans':
|
||||
pageUrl.value = '/pages/my/fans';
|
||||
break;
|
||||
case 'ranking':
|
||||
pageUrl.value = '/pages/my/ranking';
|
||||
break;
|
||||
case 'task':
|
||||
pageUrl.value = '/pages/my/task';
|
||||
break;
|
||||
case 'lottery':
|
||||
pageUrl.value = '/pages/my/lottery';
|
||||
break;
|
||||
case 'security':
|
||||
pageUrl.value = '/pages/my/security';
|
||||
break;
|
||||
case 'privacy':
|
||||
pageUrl.value = '/pages/my/privacy';
|
||||
break;
|
||||
case 'notification':
|
||||
pageUrl.value = '/pages/notification/notification';
|
||||
break;
|
||||
case 'feedback':
|
||||
pageUrl.value = '/pages/my/feedback';
|
||||
break;
|
||||
case 'help':
|
||||
pageUrl.value = '/pages/my/help';
|
||||
break;
|
||||
case 'about':
|
||||
pageUrl.value = '/pages/my/about';
|
||||
break;
|
||||
case 'followers':
|
||||
pageUrl.value = '/pages/my/followers';
|
||||
break;
|
||||
case 'following':
|
||||
pageUrl.value = '/pages/my/following';
|
||||
break;
|
||||
case 'likes':
|
||||
pageUrl.value = '/pages/my/likes';
|
||||
break;
|
||||
}
|
||||
|
||||
if (pageUrl.value != '') {
|
||||
uni.navigateTo({
|
||||
url: pageUrl.value
|
||||
})
|
||||
} else {
|
||||
uni.showToast({
|
||||
title: '页面开发中',
|
||||
icon: 'none',
|
||||
duration: 1500
|
||||
})
|
||||
}
|
||||
}
|
||||
// 退出登录
|
||||
const logout = () => {
|
||||
console.log('退出登录')
|
||||
uni.showLoading({
|
||||
title: '退出中...',
|
||||
mask: true
|
||||
})
|
||||
|
||||
// 清除本地登录态(token / 用户信息)
|
||||
Api.auth.logout()
|
||||
|
||||
setTimeout(() => {
|
||||
uni.hideLoading()
|
||||
uni.showToast({
|
||||
title: '退出成功',
|
||||
icon: 'success',
|
||||
duration: 1500
|
||||
})
|
||||
|
||||
// 跳转到登录页面
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({
|
||||
url: '/pages/login/index'
|
||||
})
|
||||
}, 1500)
|
||||
}, 500)
|
||||
}
|
||||
// 显示退出登录对话框
|
||||
const showLogoutDialog = () => {
|
||||
uni.showModal({
|
||||
title: '退出登录',
|
||||
content: '确定要退出当前账号吗?',
|
||||
confirmText: '退出',
|
||||
confirmColor: '#FF3B30',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
logout()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* 顶部背景 */
|
||||
.top-background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 300rpx;
|
||||
background: linear-gradient(135deg, #FF6B6B 0%, #FF8E53 100%);
|
||||
border-bottom-left-radius: 40rpx;
|
||||
border-bottom-right-radius: 40rpx;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* 用户信息卡片 */
|
||||
.user-card {
|
||||
position: relative;
|
||||
margin: 40rpx 30rpx 0;
|
||||
padding: 40rpx 30rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 10rpx 30rpx rgba(255, 107, 107, 0.1);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.user-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: row;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.avatar-section {
|
||||
position: relative;
|
||||
margin-right: 30rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 140rpx;
|
||||
height: 140rpx;
|
||||
border-radius: 50%;
|
||||
border: 4rpx solid #fff;
|
||||
box-shadow: 0 10rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.vip-badge {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
|
||||
border-radius: 50%;
|
||||
text-align: center;
|
||||
border: 4rpx solid #fff;
|
||||
}
|
||||
|
||||
.online-status {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
background-color: #4CD964;
|
||||
border-radius: 50%;
|
||||
border: 4rpx solid #fff;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.name-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-flow: row nowrap;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.nickname {
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-right: 20rpx;
|
||||
max-width: 300rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.level-badge {
|
||||
background: linear-gradient(135deg, #007AFF 0%, #5856D6 100%);
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 20rpx;
|
||||
margin-right: 10rpx;
|
||||
}
|
||||
|
||||
.gender-badge {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.gender-badge.male {
|
||||
background-color: rgba(0, 122, 255, 0.1);
|
||||
}
|
||||
|
||||
.gender-badge.female {
|
||||
background-color: rgba(255, 45, 85, 0.1);
|
||||
}
|
||||
|
||||
.user-id {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
|
||||
.signature {
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 30rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
flex-flow: row nowrap;
|
||||
background-color: #f8f8f8;
|
||||
border-radius: 20rpx;
|
||||
padding: 20rpx 0;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-bottom: 5rpx;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.stat-divider {
|
||||
width: 2rpx;
|
||||
height: 40rpx;
|
||||
background-color: #e0e0e0;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.edit-btn,
|
||||
.vip-btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.edit-btn:active,
|
||||
.vip-btn:active {
|
||||
transform: scale(0.98);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
background-color: #f0f0f0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.vip-btn {
|
||||
background: linear-gradient(135deg, #FF6B6B 0%, #FF8E53 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.vip-btn.active {
|
||||
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 钱包区域 */
|
||||
.wallet-section {
|
||||
margin: 30rpx;
|
||||
padding: 30rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 5rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.wallet-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: row;
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
|
||||
.wallet-label {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
margin: 0 20rpx 0 15rpx;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.wallet-amount {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #FF9500;
|
||||
margin-right: 15rpx;
|
||||
}
|
||||
|
||||
.wallet-amount+.uni-icons {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 2rpx;
|
||||
height: 60rpx;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
/* 功能区网格 */
|
||||
.function-grid {
|
||||
margin: 0 30rpx 30rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 30rpx 0;
|
||||
box-shadow: 0 5rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.grid-row {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-around;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.grid-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.grid-icon {
|
||||
width: 100rpx;
|
||||
height: 100rpx;
|
||||
border-radius: 25rpx;
|
||||
background-color: #f8f8f8;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 15rpx;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.grid-item:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.grid-icon:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.grid-text {
|
||||
font-size: 24rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.badge-dot {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 20rpx;
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
background-color: #FF3B30;
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid #fff;
|
||||
}
|
||||
|
||||
/* 列表区域 */
|
||||
.list-section {
|
||||
margin: 0 30rpx;
|
||||
}
|
||||
|
||||
.list-title {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
margin: 20rpx 0 15rpx;
|
||||
padding-left: 10rpx;
|
||||
}
|
||||
|
||||
.list-container {
|
||||
background-color: #fff;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 5rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.list-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.list-item:active {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
.list-left {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.list-left .uni-icons {
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.list-text {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.logout-text {
|
||||
color: #FF3B30;
|
||||
}
|
||||
|
||||
.list-right {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.list-desc {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
margin-right: 15rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,260 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<scroll-view direction="vertical" :show-scrollbar="false" style="flex:1;"
|
||||
:refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
|
||||
<view class="summary-card">
|
||||
<text class="summary-num">{{ totalLikes }}</text>
|
||||
<text class="summary-label">共获得 {{ list.length }} 条动态的点赞</text>
|
||||
</view>
|
||||
|
||||
<view class="list">
|
||||
<view v-for="(m, idx) in sorted" :key="idx" class="moment-item" @click="openDetail(m)">
|
||||
<view class="m-head">
|
||||
<image class="m-avatar" :src="imgUrl(m.userAvatar)" mode="aspectFill"></image>
|
||||
<view class="m-meta">
|
||||
<text class="m-name">{{ m.userName }}</text>
|
||||
<text class="m-time">{{ formatTime(m.time) }}</text>
|
||||
</view>
|
||||
<view class="like-badge">
|
||||
<uni-icons type="heart-filled" size="22" color="#FF2D55"></uni-icons>
|
||||
<text class="like-num">{{ m.likes }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="m-content">{{ m.content }}</text>
|
||||
<view v-if="m.images && m.images.length > 0" class="m-images">
|
||||
<image v-for="(img, i) in m.images.slice(0,3)" :key="i" class="m-img"
|
||||
:src="imgUrl(img)" mode="aspectFill"></image>
|
||||
</view>
|
||||
<view class="m-foot">
|
||||
<view class="m-stat"><uni-icons type="chatbubble" size="22" color="#999"></uni-icons>
|
||||
<text>{{ m.comments }}</text></view>
|
||||
<view class="m-stat"><uni-icons type="redo" size="22" color="#999"></uni-icons>
|
||||
<text>{{ m.shares }}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="list.length === 0 && !loading" class="empty-state">
|
||||
<uni-icons type="heart" size="60" color="#ddd"></uni-icons>
|
||||
<text class="empty-text">还没有人给你点赞哦</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import Api from '@/common/api-service.uts'
|
||||
|
||||
const BASE = 'https://dev.xixingwl.cn'
|
||||
|
||||
type IMoment = {
|
||||
id : number
|
||||
uid : number
|
||||
userName : string
|
||||
userAvatar : string
|
||||
content : string
|
||||
images : string[]
|
||||
videoUrl : string
|
||||
type : string
|
||||
time : number
|
||||
likes : number
|
||||
comments : number
|
||||
shares : number
|
||||
}
|
||||
|
||||
const list = reactive<IMoment[]>([])
|
||||
const sorted = computed<IMoment[]>(() => {
|
||||
return list.slice().sort((a : IMoment, b : IMoment) => b.likes - a.likes)
|
||||
})
|
||||
const totalLikes = computed<number>(() => {
|
||||
let s = 0
|
||||
list.forEach((m : IMoment) => { s += m.likes })
|
||||
return s
|
||||
})
|
||||
const loading = ref<boolean>(false)
|
||||
const refreshing = ref<boolean>(false)
|
||||
|
||||
function imgUrl(src : string) : string {
|
||||
if (! src) return ''
|
||||
return src.startsWith('http') ? src : (BASE + src)
|
||||
}
|
||||
|
||||
function formatTime(ts : number) : string {
|
||||
if (! ts) return ''
|
||||
const d = new Date(ts)
|
||||
const pad = (n : number) : string => n < 10 ? '0' + n : '' + n
|
||||
return `${d.getMonth() + 1}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
}
|
||||
|
||||
function openDetail(m : IMoment) {
|
||||
uni.navigateTo({ url: '/pages/moment/details?id=' + m.id })
|
||||
}
|
||||
|
||||
function loadData() {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
Api.moment.mine({ page: 1, page_size: 50 })
|
||||
.then((res : UTSJSONObject) => {
|
||||
const arr = (res.get('list') as Array<IMoment>) ?? []
|
||||
list.splice(0, list.length)
|
||||
arr.forEach((m : IMoment) => list.push(m))
|
||||
})
|
||||
.catch(() => {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
refreshing.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function onRefresh() {
|
||||
refreshing.value = true
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(() => { loadData() })
|
||||
onShow(() => { loadData() })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page {
|
||||
flex: 1;
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 24rpx 30rpx;
|
||||
padding: 36rpx 0;
|
||||
background: linear-gradient(135deg, #FF2D55 0%, #FF6B6B 100%);
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.summary-num {
|
||||
font-size: 52rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-size: 24rpx;
|
||||
color: #ffe;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.list {
|
||||
margin: 0 30rpx 24rpx;
|
||||
}
|
||||
|
||||
.moment-item {
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.m-head {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.m-avatar {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 36rpx;
|
||||
background-color: #f0f0f0;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.m-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.m-name {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.m-time {
|
||||
font-size: 22rpx;
|
||||
color: #bbb;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.like-badge {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
background-color: rgba(255, 45, 85, 0.08);
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.like-num {
|
||||
font-size: 26rpx;
|
||||
color: #FF2D55;
|
||||
font-weight: bold;
|
||||
margin-left: 6rpx;
|
||||
}
|
||||
|
||||
.m-content {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.m-images {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.m-img {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
border-radius: 12rpx;
|
||||
margin: 0 12rpx 12rpx 0;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.m-foot {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.m-stat {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
margin-right: 40rpx;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.m-stat text {
|
||||
margin-left: 6rpx;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 120rpx 0;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #bbb;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,305 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<scroll-view direction="vertical" :show-scrollbar="false" style="flex:1;">
|
||||
<!-- 顶部金币信息 -->
|
||||
<view class="top-card">
|
||||
<view class="coin-row">
|
||||
<uni-icons type="wallet-filled" size="28" color="#FFD700"></uni-icons>
|
||||
<text class="coin-num">{{ coins }}</text>
|
||||
<text class="coin-label">我的金币</text>
|
||||
</view>
|
||||
<view class="free-tip">
|
||||
今日免费抽奖剩余 <text class="free-num">{{ freeLeft }}</text> 次(每次消耗 {{ cost }} 金币)
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 奖品展示 -->
|
||||
<view class="section-title">奖品池</view>
|
||||
<view class="prize-grid">
|
||||
<view v-for="(p, idx) in prizes" :key="idx" class="prize-item">
|
||||
<view class="prize-icon">
|
||||
<uni-icons :type="prizeIcon(p.type)" size="30"
|
||||
:color="p.type == 1 ? '#FF9500' : (p.type == 2 ? '#FF2D55' : '#999')"></uni-icons>
|
||||
</view>
|
||||
<text class="prize-name">{{ p.name }}</text>
|
||||
<text class="prize-sub" v-if="p.type == 1">{{ p.value }} 金币</text>
|
||||
<text class="prize-sub" v-else-if="p.type == 2">实物</text>
|
||||
<text class="prize-sub" v-else>谢谢参与</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 抽奖按钮 -->
|
||||
<view class="draw-btn" :class="{ disabled: drawing }" @click="onDraw">
|
||||
<text v-if="!drawing">{{ freeLeft > 0 ? '免费抽奖' : ('抽一次(' + cost + '金币)') }}</text>
|
||||
<text v-else>抽奖中...</text>
|
||||
</view>
|
||||
<text class="draw-hint">每日 {{ freeDaily }} 次免费机会,点击即抽</text>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 结果弹窗 -->
|
||||
<view v-if="showResult" class="mask" @click="showResult = false">
|
||||
<view class="result-card" @click.stop>
|
||||
<uni-icons :type="resultIcon" size="56"
|
||||
:color="resultType == 1 ? '#FF9500' : (resultType == 2 ? '#FF2D55' : '#bbb')"></uni-icons>
|
||||
<text class="result-title">{{ resultPrize }}</text>
|
||||
<text class="result-sub" v-if="resultType == 1">+{{ resultValue }} 金币已到账</text>
|
||||
<view class="result-btn" @click="showResult = false">
|
||||
<text>开心收下</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import Api from '@/common/api-service.uts'
|
||||
|
||||
const BASE = 'https://dev.xixingwl.cn'
|
||||
|
||||
type IPrize = {
|
||||
id : number
|
||||
name : string
|
||||
type : number
|
||||
value : number
|
||||
probability : number
|
||||
}
|
||||
|
||||
const coins = ref<number>(0)
|
||||
const cost = ref<number>(10)
|
||||
const freeDaily = ref<number>(1)
|
||||
const freeLeft = ref<number>(0)
|
||||
const prizes = reactive<IPrize[]>([])
|
||||
const drawing = ref<boolean>(false)
|
||||
|
||||
const showResult = ref<boolean>(false)
|
||||
const resultPrize = ref<string>('')
|
||||
const resultType = ref<number>(3)
|
||||
const resultValue = ref<number>(0)
|
||||
const resultIcon = computed<string>(() => {
|
||||
if (resultType.value == 1) return 'wallet-filled'
|
||||
if (resultType.value == 2) return 'gift-filled'
|
||||
return 'close'
|
||||
})
|
||||
|
||||
function prizeIcon(type : number) : string {
|
||||
if (type == 1) return 'wallet-filled'
|
||||
if (type == 2) return 'gift-filled'
|
||||
return 'more'
|
||||
}
|
||||
|
||||
function loadConfig() {
|
||||
Api.lottery.config()
|
||||
.then((res : UTSJSONObject) => {
|
||||
coins.value = (res.get('coins') as number) ?? 0
|
||||
cost.value = (res.get('cost') as number) ?? 10
|
||||
freeDaily.value = (res.get('free_daily') as number) ?? 1
|
||||
freeLeft.value = (res.get('free_left') as number) ?? 0
|
||||
const list = (res.get('prizes') as Array<IPrize>) ?? []
|
||||
prizes.splice(0, prizes.length)
|
||||
list.forEach((p : IPrize) => prizes.push(p))
|
||||
})
|
||||
.catch(() => {
|
||||
uni.showToast({ title: '配置加载失败', icon: 'none' })
|
||||
})
|
||||
}
|
||||
|
||||
function onDraw() {
|
||||
if (drawing.value) return
|
||||
if (freeLeft.value <= 0 && coins.value < cost.value) {
|
||||
uni.showToast({ title: '金币不足', icon: 'none' })
|
||||
return
|
||||
}
|
||||
drawing.value = true
|
||||
Api.lottery.draw()
|
||||
.then((res : UTSJSONObject) => {
|
||||
resultPrize.value = (res.get('prize') as string) ?? '谢谢参与'
|
||||
resultType.value = (res.get('prize_type') as number) ?? 3
|
||||
resultValue.value = (res.get('prize_value') as number) ?? 0
|
||||
coins.value = (res.get('coins_left') as number) ?? coins.value
|
||||
showResult.value = true
|
||||
loadConfig()
|
||||
})
|
||||
.catch((e : any) => {
|
||||
uni.showToast({ title: (e && e.message) ? e.message : '抽奖失败', icon: 'none' })
|
||||
})
|
||||
.finally(() => {
|
||||
drawing.value = false
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => { loadConfig() })
|
||||
onShow(() => { loadConfig() })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page {
|
||||
flex: 1;
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
.top-card {
|
||||
margin: 24rpx 30rpx;
|
||||
padding: 40rpx;
|
||||
background: linear-gradient(135deg, #6A5ACD 0%, #FF6B6B 100%);
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.coin-row {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.coin-num {
|
||||
font-size: 56rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin: 0 12rpx;
|
||||
}
|
||||
|
||||
.coin-label {
|
||||
font-size: 26rpx;
|
||||
color: #ffe;
|
||||
}
|
||||
|
||||
.free-tip {
|
||||
margin-top: 16rpx;
|
||||
font-size: 24rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.free-num {
|
||||
font-weight: bold;
|
||||
color: #FFD700;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
margin: 10rpx 30rpx 16rpx;
|
||||
}
|
||||
|
||||
.prize-grid {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
margin: 0 30rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 20rpx 0;
|
||||
}
|
||||
|
||||
.prize-item {
|
||||
width: 33.33%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20rpx 0;
|
||||
}
|
||||
|
||||
.prize-icon {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 48rpx;
|
||||
background-color: #f7f7f7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.prize-name {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.prize-sub {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.draw-btn {
|
||||
margin: 40rpx 60rpx 0;
|
||||
height: 96rpx;
|
||||
border-radius: 48rpx;
|
||||
background: linear-gradient(135deg, #FF6B6B 0%, #FF8E53 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 10rpx 30rpx rgba(255, 107, 107, 0.3);
|
||||
}
|
||||
|
||||
.draw-btn.disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.draw-btn text {
|
||||
color: #fff;
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.draw-hint {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 22rpx;
|
||||
color: #bbb;
|
||||
margin: 16rpx 0 60rpx;
|
||||
}
|
||||
|
||||
.mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.result-card {
|
||||
width: 560rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 24rpx;
|
||||
padding: 60rpx 40rpx 40rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.result-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
|
||||
.result-sub {
|
||||
font-size: 26rpx;
|
||||
color: #FF9500;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.result-btn {
|
||||
margin-top: 40rpx;
|
||||
width: 100%;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
background: linear-gradient(135deg, #FF6B6B 0%, #FF8E53 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.result-btn text {
|
||||
color: #fff;
|
||||
font-size: 30rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<scroll-view direction="vertical" :show-scrollbar="false" style="flex:1;">
|
||||
<view class="tip-bar">
|
||||
<uni-icons type="locked" size="20" color="#34C759"></uni-icons>
|
||||
<text class="tip-text">以下设置仅对你生效,管理他人查看你的方式</text>
|
||||
</view>
|
||||
|
||||
<view class="list-container">
|
||||
<view class="list-item" v-for="(item, idx) in items" :key="idx">
|
||||
<view class="item-left">
|
||||
<text class="item-title">{{ item.title }}</text>
|
||||
<text class="item-sub">{{ item.sub }}</text>
|
||||
</view>
|
||||
<switch :checked="item.value == 1" color="#34C759" @change="() => onToggle(item)" />
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import Api from '@/common/api-service.uts'
|
||||
|
||||
type SettingItem = {
|
||||
key : string
|
||||
title : string
|
||||
sub : string
|
||||
value : number
|
||||
}
|
||||
|
||||
const items = reactive<SettingItem[]>([
|
||||
{ key: 'hide_age', title: '隐藏年龄', sub: '他人将无法看到你的真实年龄', value: 0 },
|
||||
{ key: 'hide_online', title: '隐藏在线状态', sub: '他人看不到你是否在线', value: 0 },
|
||||
{ key: 'hide_distance', title: '隐藏距离', sub: '同城/附近不再显示与你的距离', value: 0 },
|
||||
{ key: 'hide_album', title: '隐藏相册', sub: '他人访问你的资料时看不到相册', value: 0 },
|
||||
{ key: 'danmaku_global', title: '弹幕总开关', sub: '关闭后全站不显示互动弹幕', value: 1 },
|
||||
])
|
||||
|
||||
function loadPrivacy() {
|
||||
Api.user.privacy()
|
||||
.then((res : UTSJSONObject) => {
|
||||
items.forEach((it : SettingItem) => {
|
||||
const v = res.get(it.key)
|
||||
if (v != null) {
|
||||
it.value = (v as number) == 1 ? 1 : 0
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
})
|
||||
}
|
||||
|
||||
function onToggle(item : SettingItem) {
|
||||
const next = item.value == 1 ? 0 : 1
|
||||
item.value = next
|
||||
Api.user.updatePrivacy({ [item.key]: next })
|
||||
.then(() => {
|
||||
uni.showToast({ title: '已更新', icon: 'success', duration: 1000 })
|
||||
})
|
||||
.catch((err : any) => {
|
||||
item.value = item.value == 1 ? 0 : 1
|
||||
uni.showToast({ title: (err && err.message) ? err.message : '更新失败', icon: 'none' })
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => { loadPrivacy() })
|
||||
onShow(() => { loadPrivacy() })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page {
|
||||
flex: 1;
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
.tip-bar {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
margin: 24rpx 30rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
background-color: rgba(52, 199, 89, 0.1);
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.tip-text {
|
||||
font-size: 24rpx;
|
||||
color: #34C759;
|
||||
margin-left: 12rpx;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.list-container {
|
||||
margin: 0 30rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.list-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.item-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.item-sub {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,215 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="tabs">
|
||||
<view class="tab" :class="{ active: type === 'charm' }" @click="switchTab('charm')">
|
||||
<text>魅力榜</text>
|
||||
</view>
|
||||
<view class="tab" :class="{ active: type === 'rich' }" @click="switchTab('rich')">
|
||||
<text>财富榜</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view direction="vertical" :show-scrollbar="false" style="flex:1;"
|
||||
:refresher-enabled="true" :refresher-triggered="refreshing" @refresherrefresh="onRefresh">
|
||||
<view class="list">
|
||||
<view v-for="(u, idx) in list" :key="idx" class="rank-item" @click="openUser(u)">
|
||||
<view class="rank-no" :class="'top' + u.rank">
|
||||
<text v-if="u.rank <= 3">{{ ['', '①', '②', '③'][u.rank] }}</text>
|
||||
<text v-else>{{ u.rank }}</text>
|
||||
</view>
|
||||
<image class="rank-avatar" :src="imgUrl(u.avatar)" mode="aspectFill"></image>
|
||||
<view class="rank-info">
|
||||
<text class="rank-name">{{ u.nickname }}</text>
|
||||
</view>
|
||||
<view class="rank-value">
|
||||
<text class="value-num">{{ u.value }}</text>
|
||||
<text class="value-unit">{{ type === 'rich' ? '金币' : '粉丝' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view v-if="list.length === 0 && !loading" class="empty-state">
|
||||
<uni-icons type="list" size="60" color="#ddd"></uni-icons>
|
||||
<text class="empty-text">暂无排行数据</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import Api from '@/common/api-service.uts'
|
||||
|
||||
const BASE = 'https://dev.xixingwl.cn'
|
||||
|
||||
type IRank = {
|
||||
rank : number
|
||||
uid : number
|
||||
nickname : string
|
||||
avatar : string
|
||||
value : number
|
||||
coins : number
|
||||
}
|
||||
|
||||
const type = ref<string>('charm')
|
||||
const list = reactive<IRank[]>([])
|
||||
const loading = ref<boolean>(false)
|
||||
const refreshing = ref<boolean>(false)
|
||||
|
||||
function imgUrl(src : string) : string {
|
||||
if (! src) return ''
|
||||
return src.startsWith('http') ? src : (BASE + src)
|
||||
}
|
||||
|
||||
function switchTab(t : string) {
|
||||
if (type.value === t) return
|
||||
type.value = t
|
||||
loadData()
|
||||
}
|
||||
|
||||
function openUser(u : IRank) {
|
||||
uni.navigateTo({ url: '/pages/user/profile?uid=' + u.uid })
|
||||
}
|
||||
|
||||
function loadData() {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
Api.user.ranking({ type: type.value })
|
||||
.then((res : UTSJSONObject) => {
|
||||
const arr = (res.get('list') as Array<IRank>) ?? []
|
||||
list.splice(0, list.length)
|
||||
arr.forEach((u : IRank) => list.push(u))
|
||||
})
|
||||
.catch(() => {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false
|
||||
refreshing.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function onRefresh() {
|
||||
refreshing.value = true
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(() => { loadData() })
|
||||
onShow(() => { loadData() })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page {
|
||||
flex: 1;
|
||||
background-color: #f7f7f7;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
background-color: #fff;
|
||||
padding: 16rpx 0;
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 30rpx;
|
||||
color: #999;
|
||||
padding: 12rpx 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: #FF6B6B;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tab.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
transform: translateX(-50%);
|
||||
width: 48rpx;
|
||||
height: 6rpx;
|
||||
border-radius: 3rpx;
|
||||
background: linear-gradient(135deg, #FF6B6B 0%, #FF8E53 100%);
|
||||
}
|
||||
|
||||
.list {
|
||||
margin: 24rpx 30rpx;
|
||||
}
|
||||
|
||||
.rank-item {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
background-color: #fff;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.rank-no {
|
||||
width: 56rpx;
|
||||
text-align: center;
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.rank-no.top1 { color: #FF9500; }
|
||||
.rank-no.top2 { color: #FF6B6B; }
|
||||
.rank-no.top3 { color: #AF52DE; }
|
||||
|
||||
.rank-avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 40rpx;
|
||||
background-color: #f0f0f0;
|
||||
margin: 0 20rpx;
|
||||
}
|
||||
|
||||
.rank-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.rank-name {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.rank-value {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.value-num {
|
||||
font-size: 34rpx;
|
||||
font-weight: bold;
|
||||
color: #FF6B6B;
|
||||
}
|
||||
|
||||
.value-unit {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-left: 4rpx;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 120rpx 0;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #bbb;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,178 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<scroll-view direction="vertical" :show-scrollbar="false" style="flex:1;">
|
||||
<view class="list-title">账号绑定</view>
|
||||
<view class="list-container">
|
||||
<view class="list-item" @click="toast('手机号已绑定')">
|
||||
<view class="item-left">
|
||||
<uni-icons type="phone" size="20" color="#007AFF"></uni-icons>
|
||||
<text class="list-text">手机号</text>
|
||||
</view>
|
||||
<view class="item-right">
|
||||
<text class="item-desc">{{ mobileMask }}</text>
|
||||
<uni-icons type="right" size="16" color="#999"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
<view class="list-item" @click="toast('修改密码功能开发中')">
|
||||
<view class="item-left">
|
||||
<uni-icons type="locked" size="20" color="#FF9500"></uni-icons>
|
||||
<text class="list-text">修改密码</text>
|
||||
</view>
|
||||
<uni-icons type="right" size="16" color="#999"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="list-title">隐私与安全</view>
|
||||
<view class="list-container">
|
||||
<view class="list-item" @click="goPage('/pages/my/privacy')">
|
||||
<view class="item-left">
|
||||
<uni-icons type="eye" size="20" color="#34C759"></uni-icons>
|
||||
<text class="list-text">隐私设置</text>
|
||||
</view>
|
||||
<uni-icons type="right" size="16" color="#999"></uni-icons>
|
||||
</view>
|
||||
<view class="list-item" @click="toast('登录设备管理开发中')">
|
||||
<view class="item-left">
|
||||
<uni-icons type="staff" size="20" color="#5AC8FA"></uni-icons>
|
||||
<text class="list-text">登录设备管理</text>
|
||||
</view>
|
||||
<uni-icons type="right" size="16" color="#999"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="list-title">其他</view>
|
||||
<view class="list-container">
|
||||
<view class="list-item danger" @click="onCancel">
|
||||
<view class="item-left">
|
||||
<uni-icons type="trash" size="20" color="#FF3B30"></uni-icons>
|
||||
<text class="list-text danger-text">注销账号</text>
|
||||
</view>
|
||||
<uni-icons type="right" size="16" color="#999"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text class="version">当前版本 v{{ appVersion }}</text>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import Api from '@/common/api-service.uts'
|
||||
import userState from '@/stores/user.uts'
|
||||
|
||||
const appVersion = ref('2.5.0')
|
||||
|
||||
const mobileMask = computed<string>(() => {
|
||||
const info = userState.info
|
||||
const m = info && info.mobile ? (info.mobile as string) : ''
|
||||
if (m.length >= 11) {
|
||||
return m.substring(0, 3) + '****' + m.substring(7)
|
||||
}
|
||||
return m || '未绑定'
|
||||
})
|
||||
|
||||
function toast(msg : string) {
|
||||
uni.showToast({ title: msg, icon: 'none' })
|
||||
}
|
||||
|
||||
function goPage(url : string) {
|
||||
uni.navigateTo({ url: url })
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
uni.showModal({
|
||||
title: '注销账号',
|
||||
content: '注销后你的资料将被匿名化处理,且无法恢复,确定继续吗?',
|
||||
confirmText: '注销',
|
||||
confirmColor: '#FF3B30',
|
||||
success: (res) => {
|
||||
if (! res.confirm) return
|
||||
Api.user.cancelAccount()
|
||||
.then(() => {
|
||||
Api.auth.logout()
|
||||
uni.reLaunch({ url: '/pages/login/index' })
|
||||
})
|
||||
.catch((e : any) => {
|
||||
uni.showToast({ title: (e && e.message) ? e.message : '操作失败', icon: 'none' })
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page {
|
||||
flex: 1;
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
.list-title {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
margin: 24rpx 30rpx 16rpx;
|
||||
}
|
||||
|
||||
.list-container {
|
||||
margin: 0 30rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 20rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.list-item {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 30rpx;
|
||||
border-bottom: 1rpx solid #f0f0f0;
|
||||
}
|
||||
|
||||
.list-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.list-item:active {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
.item-left {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.item-left .uni-icons {
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.list-text {
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.item-right {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.item-desc {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.danger-text {
|
||||
color: #FF3B30;
|
||||
}
|
||||
|
||||
.version {
|
||||
display: block;
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
color: #bbb;
|
||||
margin: 60rpx 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,270 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="-status-bar"></view>
|
||||
<view class="header">
|
||||
<view class="coins-card">
|
||||
<uni-icons type="wallet-filled" size="36" color="#FFB300"></uni-icons>
|
||||
<view class="coins-info">
|
||||
<text class="coins-num">{{ coins }}</text>
|
||||
<text class="coins-label">我的金币</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view direction="vertical" :show-scrollbar="false" style="flex:1;">
|
||||
<view class="task-item" v-for="(item, idx) in tasks" :key="item.id">
|
||||
<view class="task-icon">
|
||||
<uni-icons :type="item.icon" size="30" color="#fff"></uni-icons>
|
||||
</view>
|
||||
<view class="task-main">
|
||||
<text class="task-title">{{ item.title }}</text>
|
||||
<text class="task-desc">{{ item.desc }}</text>
|
||||
<view class="task-progress" v-if="item.status != 2">
|
||||
<view class="bar">
|
||||
<view class="bar-inner" :style="{ width: progressWidth(item) }"></view>
|
||||
</view>
|
||||
<text class="bar-text">{{ item.current }}/{{ item.target }}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="task-reward">+{{ item.reward }}</view>
|
||||
<button class="task-btn" :class="btnClass(item.status)" :disabled="item.status == 2"
|
||||
@click="onReceive(item)">
|
||||
{{ btnText(item.status) }}
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view v-if="tasks.length === 0 && !loading" class="empty">
|
||||
<text class="empty-text">暂无任务</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import Api from '@/common/api-service.uts'
|
||||
|
||||
type ITask = {
|
||||
id : number
|
||||
title : string
|
||||
desc : string
|
||||
icon : string
|
||||
reward : number
|
||||
target : number
|
||||
current : number
|
||||
status : number
|
||||
}
|
||||
|
||||
const coins = ref<number>(0)
|
||||
const loading = ref<boolean>(false)
|
||||
const tasks = reactive<ITask[]>([])
|
||||
|
||||
const btnText = (status : number) : string => {
|
||||
if (status == 2) return '已领取'
|
||||
if (status == 1) return '领取'
|
||||
return '去完成'
|
||||
}
|
||||
const btnClass = (status : number) : string => {
|
||||
if (status == 2) return 'done'
|
||||
if (status == 1) return 'ready'
|
||||
return 'todo'
|
||||
}
|
||||
const progressWidth = (item : ITask) : string => {
|
||||
const pct = item.target > 0 ? (item.current / item.target * 100) : 0
|
||||
return Math.min(100, pct) + '%'
|
||||
}
|
||||
|
||||
function loadData() {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
Api.task.list()
|
||||
.then((res : UTSJSONObject) => {
|
||||
coins.value = (res.get('coins') as number) ?? 0
|
||||
const list = (res.get('list') as Array<ITask>) ?? []
|
||||
tasks.splice(0, tasks.length)
|
||||
list.forEach((t : ITask) => tasks.push(t))
|
||||
})
|
||||
.catch(() => {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
})
|
||||
.finally(() => { loading.value = false })
|
||||
}
|
||||
|
||||
function onReceive(item : ITask) {
|
||||
if (item.status == 2) return
|
||||
if (item.status == 0) {
|
||||
uni.showToast({ title: '任务未完成', icon: 'none' })
|
||||
return
|
||||
}
|
||||
uni.showLoading({ title: '领取中...', mask: true })
|
||||
Api.task.receive(item.id)
|
||||
.then((res : UTSJSONObject) => {
|
||||
item.status = 2
|
||||
coins.value = (res.get('coins_left') as number) ?? coins.value
|
||||
uni.hideLoading()
|
||||
uni.showToast({ title: '领取成功 +' + item.reward + '金币', icon: 'none' })
|
||||
})
|
||||
.catch((e : any) => {
|
||||
uni.hideLoading()
|
||||
const msg = (e as UTSJSONObject)?.get('message') as string
|
||||
uni.showToast({ title: msg ?? '领取失败', icon: 'none' })
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => { loadData() })
|
||||
onShow(() => { loadData() })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page {
|
||||
flex: 1;
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
.-status-bar {
|
||||
height: var(--status-bar-height);
|
||||
background: linear-gradient(135deg, #FF6B6B 0%, #FF8E53 100%);
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #FF6B6B 0%, #FF8E53 100%);
|
||||
padding-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.coins-card {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
margin: 24rpx 30rpx 0;
|
||||
padding: 30rpx;
|
||||
background-color: rgba(255, 255, 255, 0.18);
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.coins-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 20rpx;
|
||||
}
|
||||
|
||||
.coins-num {
|
||||
font-size: 44rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.coins-label {
|
||||
font-size: 24rpx;
|
||||
color: #ffe;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.task-item {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
margin: 20rpx 30rpx 0;
|
||||
padding: 28rpx 24rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 18rpx;
|
||||
box-shadow: 0 6rpx 18rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.task-icon {
|
||||
width: 84rpx;
|
||||
height: 84rpx;
|
||||
border-radius: 20rpx;
|
||||
background: linear-gradient(135deg, #FF8E53 0%, #FF6B6B 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
|
||||
.task-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.task-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.task-desc {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.task-progress {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.bar {
|
||||
flex: 1;
|
||||
height: 12rpx;
|
||||
border-radius: 6rpx;
|
||||
background-color: #f0f0f0;
|
||||
overflow: hidden;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.bar-inner {
|
||||
height: 100%;
|
||||
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
.bar-text {
|
||||
font-size: 20rpx;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.task-reward {
|
||||
font-size: 26rpx;
|
||||
font-weight: bold;
|
||||
color: #FF9500;
|
||||
margin-right: 16rpx;
|
||||
}
|
||||
|
||||
.task-btn {
|
||||
height: 60rpx;
|
||||
padding: 0 28rpx;
|
||||
line-height: 60rpx;
|
||||
border-radius: 30rpx;
|
||||
font-size: 26rpx;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.task-btn.ready {
|
||||
background: linear-gradient(135deg, #FF6B6B 0%, #FF8E53 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.task-btn.todo {
|
||||
background-color: #f0f0f0;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.task-btn.done {
|
||||
background-color: #f0f0f0;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 0;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #bbb;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,231 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="-status-bar"></view>
|
||||
<view class="header">
|
||||
<view class="balance-card">
|
||||
<text class="balance-label">账户余额(元)</text>
|
||||
<text class="balance-num">{{ balance.toFixed(2) }}</text>
|
||||
<view class="coin-row">
|
||||
<uni-icons type="wallet-filled" size="22" color="#FFD700"></uni-icons>
|
||||
<text class="coin-num">{{ coins }}</text>
|
||||
<text class="coin-label">金币</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="summary-row">
|
||||
<view class="summary-item">
|
||||
<text class="s-num">{{ totalRecharge.toFixed(2) }}</text>
|
||||
<text class="s-label">累计充值</text>
|
||||
</view>
|
||||
<view class="summary-item">
|
||||
<text class="s-num">{{ totalConsume.toFixed(2) }}</text>
|
||||
<text class="s-label">累计消费</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<scroll-view direction="vertical" :show-scrollbar="false" style="flex:1;">
|
||||
<view class="block-title">明细记录</view>
|
||||
<view v-if="logs.length === 0 && !loading" class="empty">
|
||||
<text class="empty-text">暂无明细</text>
|
||||
</view>
|
||||
<view class="log-item" v-for="(log, idx) in logs" :key="log.id">
|
||||
<view class="log-left">
|
||||
<text class="log-type">{{ log.remark && log.remark != '' ? log.remark : log.type }}</text>
|
||||
<text class="log-time">{{ formatTime(log.create_at) }}</text>
|
||||
</view>
|
||||
<text class="log-num" :class="log.change_num >= 0 ? 'plus' : 'minus'">{{ log.change_num >= 0 ? '+' : '' }}{{ log.change_num }}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import Api from '@/common/api-service.uts'
|
||||
|
||||
type ILog = {
|
||||
id : number
|
||||
type : string
|
||||
change_num : number
|
||||
balance_after : number
|
||||
remark : string
|
||||
create_at : number
|
||||
}
|
||||
|
||||
const loading = ref<boolean>(false)
|
||||
const balance = ref<number>(0)
|
||||
const coins = ref<number>(0)
|
||||
const totalRecharge = ref<number>(0)
|
||||
const totalConsume = ref<number>(0)
|
||||
const logs = reactive<ILog[]>([])
|
||||
|
||||
function loadData() {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
Api.user.wallet()
|
||||
.then((res : UTSJSONObject) => {
|
||||
balance.value = (res.get('balance') as number) ?? 0
|
||||
coins.value = (res.get('coins') as number) ?? 0
|
||||
totalRecharge.value = (res.get('total_recharge') as number) ?? 0
|
||||
totalConsume.value = (res.get('total_consume') as number) ?? 0
|
||||
const arr = (res.get('logs') as Array<ILog>) ?? []
|
||||
logs.splice(0, logs.length)
|
||||
arr.forEach((l : ILog) => logs.push(l))
|
||||
})
|
||||
.catch(() => {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
})
|
||||
.finally(() => { loading.value = false })
|
||||
}
|
||||
|
||||
function formatTime(ts : number) : string {
|
||||
if (!ts) return ''
|
||||
const d = new Date(ts * 1000)
|
||||
const pad = (n : number) : string => { return n < 10 ? ('0' + n) : ('' + n) }
|
||||
return (d.getMonth() + 1) + '-' + d.getDate() + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes())
|
||||
}
|
||||
|
||||
onMounted(() => { loadData() })
|
||||
onShow(() => { loadData() })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page {
|
||||
flex: 1;
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
.-status-bar {
|
||||
height: var(--status-bar-height);
|
||||
background: linear-gradient(135deg, #FFB75E 0%, #ED8F03 100%);
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #FFB75E 0%, #ED8F03 100%);
|
||||
padding: 30rpx 30rpx 50rpx;
|
||||
}
|
||||
|
||||
.balance-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.balance-label {
|
||||
font-size: 26rpx;
|
||||
color: #fff;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.balance-num {
|
||||
font-size: 64rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin: 8rpx 0 20rpx;
|
||||
}
|
||||
|
||||
.coin-row {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.coin-num {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin: 0 8rpx 0 12rpx;
|
||||
}
|
||||
|
||||
.coin-label {
|
||||
font-size: 24rpx;
|
||||
color: #fff;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.summary-row {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
margin-top: 36rpx;
|
||||
}
|
||||
|
||||
.summary-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
border-right: 1rpx solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
.summary-item:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.s-num {
|
||||
font-size: 34rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.s-label {
|
||||
font-size: 22rpx;
|
||||
color: #fff;
|
||||
opacity: 0.9;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
|
||||
.block-title {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
margin: 30rpx 30rpx 16rpx;
|
||||
}
|
||||
|
||||
.empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 100rpx 0;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.log-item {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 28rpx 30rpx;
|
||||
background-color: #fff;
|
||||
border-bottom: 1rpx solid #f2f2f2;
|
||||
}
|
||||
|
||||
.log-left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.log-type {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
font-size: 22rpx;
|
||||
color: #bbb;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
|
||||
.log-num {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.log-num.plus {
|
||||
color: #34C759;
|
||||
}
|
||||
|
||||
.log-num.minus {
|
||||
color: #FF3B30;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,212 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="-status-bar"></view>
|
||||
<view class="header">
|
||||
<text class="title">消息通知</text>
|
||||
</view>
|
||||
<scroll-view direction="vertical" :show-scrollbar="false" style="flex:1;">
|
||||
<view v-if="list.length === 0 && !loading" class="empty">
|
||||
<uni-icons type="chat" size="64" color="#e0e0e0"></uni-icons>
|
||||
<text class="empty-text">暂无消息</text>
|
||||
</view>
|
||||
<view class="conv-item" v-for="(item, idx) in list" :key="item.peer_id" @click="openChat(item)">
|
||||
<view class="avatar-wrap">
|
||||
<image class="avatar" :src="avatarUrl(item.avatar)" mode="aspectFill"></image>
|
||||
<view v-if="item.online" class="online-dot"></view>
|
||||
<view v-if="item.unread_count > 0" class="badge">{{ item.unread_count > 99 ? '99+' : item.unread_count }}</view>
|
||||
</view>
|
||||
<view class="conv-main">
|
||||
<view class="conv-top">
|
||||
<text class="name">{{ item.nickname }}</text>
|
||||
<text class="time">{{ formatTime(item.last_time) }}</text>
|
||||
</view>
|
||||
<text class="last">{{ item.is_self ? '我: ' : '' }}{{ item.last_content }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import Api from '@/common/api-service.uts'
|
||||
|
||||
type IConv = {
|
||||
peer_id : number
|
||||
nickname : string
|
||||
avatar : string
|
||||
online : boolean
|
||||
last_content : string
|
||||
last_content_type : number
|
||||
last_time : number
|
||||
unread_count : number
|
||||
is_self : boolean
|
||||
}
|
||||
|
||||
const BASE = 'https://dev.xixingwl.cn'
|
||||
const loading = ref<boolean>(false)
|
||||
const list = reactive<IConv[]>([])
|
||||
|
||||
const avatarUrl = (avatar : string) : string => {
|
||||
if (avatar == null || avatar == '') return ''
|
||||
if (avatar.startsWith('http')) return avatar
|
||||
return BASE + avatar
|
||||
}
|
||||
|
||||
function loadData() {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
Api.message.conversations()
|
||||
.then((res : UTSJSONObject) => {
|
||||
const arr = (res.get('list') as Array<IConv>) ?? []
|
||||
list.splice(0, list.length)
|
||||
arr.forEach((c : IConv) => list.push(c))
|
||||
})
|
||||
.catch(() => {
|
||||
uni.showToast({ title: '加载失败', icon: 'none' })
|
||||
})
|
||||
.finally(() => { loading.value = false })
|
||||
}
|
||||
|
||||
function openChat(item : IConv) {
|
||||
uni.navigateTo({ url: `/pages/chat/chat?userId=${item.peer_id}&userName=${item.nickname}` })
|
||||
}
|
||||
|
||||
function formatTime(ts : number) : string {
|
||||
if (!ts) return ''
|
||||
const now = Math.floor(Date.now() / 1000)
|
||||
const diff = now - ts
|
||||
if (diff < 60) return '刚刚'
|
||||
if (diff < 3600) return Math.floor(diff / 60) + '分钟前'
|
||||
if (diff < 86400) return Math.floor(diff / 3600) + '小时前'
|
||||
if (diff < 7 * 86400) return Math.floor(diff / 86400) + '天前'
|
||||
const d = new Date(ts * 1000)
|
||||
return (d.getMonth() + 1) + '/' + d.getDate()
|
||||
}
|
||||
|
||||
onMounted(() => { loadData() })
|
||||
onShow(() => { loadData() })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.page {
|
||||
flex: 1;
|
||||
background-color: #f7f7f7;
|
||||
}
|
||||
|
||||
.-status-bar {
|
||||
height: var(--status-bar-height);
|
||||
background: linear-gradient(135deg, #FF6B6B 0%, #FF8E53 100%);
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, #FF6B6B 0%, #FF8E53 100%);
|
||||
padding: 24rpx 30rpx 36rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 140rpx 0;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #bbb;
|
||||
margin-top: 20rpx;
|
||||
}
|
||||
|
||||
.conv-item {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
padding: 28rpx 30rpx;
|
||||
background-color: #fff;
|
||||
border-bottom: 1rpx solid #f2f2f2;
|
||||
}
|
||||
|
||||
.conv-item:active {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
|
||||
.avatar-wrap {
|
||||
position: relative;
|
||||
margin-right: 24rpx;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.online-dot {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 22rpx;
|
||||
height: 22rpx;
|
||||
border-radius: 50%;
|
||||
background-color: #4CD964;
|
||||
border: 4rpx solid #fff;
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: -8rpx;
|
||||
right: -8rpx;
|
||||
min-width: 32rpx;
|
||||
height: 32rpx;
|
||||
padding: 0 6rpx;
|
||||
border-radius: 16rpx;
|
||||
background-color: #FF3B30;
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
line-height: 32rpx;
|
||||
text-align: center;
|
||||
border: 2rpx solid #fff;
|
||||
}
|
||||
|
||||
.conv-main {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.conv-top {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.time {
|
||||
font-size: 22rpx;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.last {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="nav-bar">
|
||||
<view class="nav-back" @click="goBack">
|
||||
<uni-icons type="left" size="22" color="#333"></uni-icons>
|
||||
</view>
|
||||
<text class="nav-title">神兽宠物</text>
|
||||
<view class="nav-right"></view>
|
||||
</view>
|
||||
|
||||
<view class="hero">
|
||||
<text class="hero-emoji">🐾</text>
|
||||
<text class="hero-title">神兽宠物社交</text>
|
||||
<text class="hero-sub">养一只专属神兽,结识同好铲屎官</text>
|
||||
</view>
|
||||
|
||||
<view class="grid">
|
||||
<view class="pet-card" v-for="(pet, i) in pets" :key="i" @click="selectPet(pet)">
|
||||
<text class="pet-emoji">{{ pet.emoji }}</text>
|
||||
<text class="pet-name">{{ pet.name }}</text>
|
||||
<text class="pet-desc">{{ pet.desc }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tip">
|
||||
<text>领养、互动、宠物社区功能即将上线~</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const pets = ref([
|
||||
{ emoji: '🐱', name: '招财猫', desc: '带来好运' },
|
||||
{ emoji: '🐶', name: '旺旺犬', desc: '守护缘分' },
|
||||
{ emoji: '🐰', name: '月亮兔', desc: '温柔陪伴' },
|
||||
{ emoji: '🐉', name: '祥云龙', desc: '稀有神兽' }
|
||||
] as UTSJSONObject[])
|
||||
|
||||
const selectPet = (pet : UTSJSONObject) => {
|
||||
uni.showToast({ title: '选中「' + (pet.getString('name') ?? '') + '」', icon: 'none' })
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #f6f6f6;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 20rpx 24rpx;
|
||||
padding-top: 60rpx;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
width: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 34rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.nav-right {
|
||||
width: 60rpx;
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 60rpx 0 40rpx;
|
||||
background: linear-gradient(to bottom right, #5AC8FA, #007AFF);
|
||||
}
|
||||
|
||||
.hero-emoji {
|
||||
font-size: 90rpx;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.hero-sub {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-top: 10rpx;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
padding: 24rpx;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.pet-card {
|
||||
width: 48%;
|
||||
background-color: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 36rpx 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.pet-emoji {
|
||||
font-size: 72rpx;
|
||||
}
|
||||
|
||||
.pet-name {
|
||||
font-size: 30rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.pet-desc {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.tip {
|
||||
text-align: center;
|
||||
padding: 30rpx;
|
||||
}
|
||||
|
||||
.tip text {
|
||||
font-size: 24rpx;
|
||||
color: #aaa;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,464 @@
|
||||
<template>
|
||||
<view class="page">
|
||||
<view class="nav-bar">
|
||||
<view class="nav-back" @click="goBack">
|
||||
<uni-icons type="left" size="22" color="#333"></uni-icons>
|
||||
</view>
|
||||
<text class="nav-title">真人匹配</text>
|
||||
<view class="nav-right">
|
||||
<uni-icons type="reload" size="22" color="#333" @click="reload"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="filter-bar">
|
||||
<text class="filter-label">缘分卡片</text>
|
||||
<text class="filter-sub">右滑喜欢 · 左滑跳过,遇到同频的人</text>
|
||||
</view>
|
||||
|
||||
<!-- 滑卡区 -->
|
||||
<view class="card-stack" v-if="displayCards.length > 0">
|
||||
<view v-for="(card, idx) in displayCards" :key="card.uid" class="card"
|
||||
:style="cardStyle(idx)">
|
||||
<!-- 顶部照片 -->
|
||||
<image class="card-photo" :src="card.avatar" mode="aspectFill"></image>
|
||||
<!-- 喜欢 / 跳过 印章 -->
|
||||
<view v-if="idx === 0" class="stamp like-stamp" :style="{ opacity: likeOpacity }">喜欢</view>
|
||||
<view v-if="idx === 0" class="stamp nope-stamp" :style="{ opacity: nopeOpacity }">跳过</view>
|
||||
<!-- 底部信息 -->
|
||||
<view class="card-info">
|
||||
<view class="card-name-row">
|
||||
<text class="card-name">{{ card.name }}</text>
|
||||
<text class="card-age" v-if="card.age > 0">{{ card.age }}岁</text>
|
||||
<text class="card-online" v-if="card.isOnline">在线</text>
|
||||
</view>
|
||||
<view class="card-meta" v-if="card.location || card.distance > 0">
|
||||
<text class="card-meta-i" v-if="card.location">{{ card.location }}</text>
|
||||
<text class="card-meta-i" v-if="card.distance > 0">{{ card.distance }}km</text>
|
||||
</view>
|
||||
<text class="card-bio" v-if="card.bio">{{ card.bio }}</text>
|
||||
</view>
|
||||
<!-- 仅顶层卡片绑定手势 -->
|
||||
<view v-if="idx === 0" class="card-gesture"
|
||||
@touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空态 -->
|
||||
<view v-else class="empty">
|
||||
<text class="empty-text">附近暂时没有更多缘分啦~</text>
|
||||
<button class="empty-btn" @click="reload">换一批</button>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="actions" v-if="displayCards.length > 0">
|
||||
<view class="act-btn skip" @click="skipTop">
|
||||
<uni-icons type="close" size="28" color="#FF6B6B"></uni-icons>
|
||||
</view>
|
||||
<view class="act-btn like" @click="likeTop">
|
||||
<uni-icons type="heart" size="30" color="#fff"></uni-icons>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tip">滑动卡片或点击按钮,喜欢即关注对方</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { Api } from '@/common/api-service.uts'
|
||||
import { getUserInfo } from '@/stores/user.uts'
|
||||
|
||||
const cards = ref<UTSJSONObject[]>([])
|
||||
const offsetX = ref(0)
|
||||
const rotate = ref(0)
|
||||
const likeOpacity = ref(0)
|
||||
const nopeOpacity = ref(0)
|
||||
const dragging = ref(false)
|
||||
let startX = 0
|
||||
let startY = 0
|
||||
let loading = false
|
||||
|
||||
const displayCards = computed<UTSJSONObject[]>(() => {
|
||||
return cards.value.slice(0, 3)
|
||||
})
|
||||
|
||||
const meUid = computed<number>(() => {
|
||||
return getUserInfo()?.uid ?? 0
|
||||
})
|
||||
|
||||
const mapCard = (it : UTSJSONObject) : UTSJSONObject => {
|
||||
const city = it.getString('residecity') ?? ''
|
||||
const province = it.getString('resideprovince') ?? ''
|
||||
return {
|
||||
uid: it.getNumber('uid') ?? 0,
|
||||
name: it.getString('nickname') ?? '匿名用户',
|
||||
avatar: it.getString('avatar') ?? '',
|
||||
age: it.getNumber('age') ?? 0,
|
||||
isOnline: it.getBoolean('is_online') ?? false,
|
||||
location: city.length > 0 ? city : province,
|
||||
distance: it.getNumber('distance') ?? 0,
|
||||
bio: it.getString('bio') ?? ''
|
||||
} as UTSJSONObject
|
||||
}
|
||||
|
||||
const loadUsers = async () => {
|
||||
if (loading) return
|
||||
loading = true
|
||||
try {
|
||||
let data : UTSJSONObject | null = null
|
||||
let list : any = null
|
||||
try {
|
||||
data = await Api.user.nearby({ limit: 30, radius: 100 })
|
||||
list = data.getJSONArray('list')
|
||||
} catch (e) {
|
||||
console.warn('nearby 失败,回退 local/online', e)
|
||||
}
|
||||
if (list == null || list.size() == 0) {
|
||||
try {
|
||||
data = await Api.user.local({})
|
||||
list = data.getJSONArray('list')
|
||||
} catch (e) { console.warn('local 失败', e) }
|
||||
}
|
||||
if (list == null || list.size() == 0) {
|
||||
try {
|
||||
data = await Api.user.online({})
|
||||
list = data.getJSONArray('list')
|
||||
} catch (e) { console.warn('online 失败', e) }
|
||||
}
|
||||
const arr : UTSJSONObject[] = []
|
||||
if (list != null) {
|
||||
for (let i = 0; i < list.size(); i++) {
|
||||
const it = list.get(i) as UTSJSONObject
|
||||
if (it != null) arr.push(mapCard(it))
|
||||
}
|
||||
}
|
||||
if (arr.length > 0) {
|
||||
cards.value = arr
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('加载匹配用户失败', e)
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
const reload = () => {
|
||||
offsetX.value = 0
|
||||
rotate.value = 0
|
||||
likeOpacity.value = 0
|
||||
nopeOpacity.value = 0
|
||||
loadUsers()
|
||||
}
|
||||
|
||||
const removeTop = () => {
|
||||
if (cards.value.length > 0) {
|
||||
cards.value.splice(0, 1)
|
||||
}
|
||||
offsetX.value = 0
|
||||
rotate.value = 0
|
||||
likeOpacity.value = 0
|
||||
nopeOpacity.value = 0
|
||||
if (cards.value.length === 0) {
|
||||
loadUsers()
|
||||
}
|
||||
}
|
||||
|
||||
const likeTop = () => {
|
||||
const top = cards.value[0]
|
||||
if (top != null) {
|
||||
const uid = top.getNumber('uid') ?? 0
|
||||
if (uid > 0 && meUid.value > 0) {
|
||||
Api.follow.toggle(meUid.value, uid, 1).then(() => {
|
||||
uni.showToast({ title: '已喜欢,已关注', icon: 'none' })
|
||||
}).catch((err : any) => {
|
||||
console.warn('关注失败', err)
|
||||
})
|
||||
}
|
||||
}
|
||||
removeTop()
|
||||
}
|
||||
|
||||
const skipTop = () => {
|
||||
removeTop()
|
||||
}
|
||||
|
||||
// ===== 手势 =====
|
||||
const onTouchStart = (e : any) => {
|
||||
dragging.value = true
|
||||
const t = e.touches[0]
|
||||
startX = t.clientX
|
||||
startY = t.clientY
|
||||
}
|
||||
const onTouchMove = (e : any) => {
|
||||
if (! dragging.value) return
|
||||
const t = e.touches[0]
|
||||
const dx = t.clientX - startX
|
||||
const dy = t.clientY - startY
|
||||
offsetX.value = dx
|
||||
rotate.value = dx / 20
|
||||
likeOpacity.value = dx > 0 ? Math.min(dx / 100, 1) : 0
|
||||
nopeOpacity.value = dx < 0 ? Math.min(-dx / 100, 1) : 0
|
||||
}
|
||||
const onTouchEnd = (e : any) => {
|
||||
if (! dragging.value) return
|
||||
dragging.value = false
|
||||
const dx = offsetX.value
|
||||
if (dx > 100) {
|
||||
likeTop()
|
||||
} else if (dx < -100) {
|
||||
skipTop()
|
||||
} else {
|
||||
offsetX.value = 0
|
||||
rotate.value = 0
|
||||
likeOpacity.value = 0
|
||||
nopeOpacity.value = 0
|
||||
}
|
||||
}
|
||||
|
||||
const cardStyle = (idx : number) : UTSJSONObject => {
|
||||
if (idx === 0) {
|
||||
return {
|
||||
transform: 'translateX(' + offsetX.value + 'px) rotate(' + rotate.value + 'deg)',
|
||||
zIndex: 10
|
||||
} as UTSJSONObject
|
||||
}
|
||||
return {
|
||||
transform: 'scale(' + (1 - idx * 0.05) + ') translateY(' + (idx * 16) + 'px)',
|
||||
zIndex: 10 - idx,
|
||||
opacity: 0.92
|
||||
} as UTSJSONObject
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
uni.navigateBack()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadUsers()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(180deg, #FFE9EC 0%, #f6f6f6 40%);
|
||||
min-height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 20rpx 24rpx;
|
||||
padding-top: 60rpx;
|
||||
}
|
||||
|
||||
.nav-back,
|
||||
.nav-right {
|
||||
width: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nav-right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 34rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10rpx 40rpx 20rpx;
|
||||
}
|
||||
|
||||
.filter-label {
|
||||
font-size: 38rpx;
|
||||
font-weight: bold;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.filter-sub {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.card-stack {
|
||||
position: relative;
|
||||
height: 880rpx;
|
||||
margin: 20rpx 50rpx;
|
||||
}
|
||||
|
||||
.card {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 880rpx;
|
||||
border-radius: 28rpx;
|
||||
overflow: hidden;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.card-photo {
|
||||
width: 100%;
|
||||
height: 700rpx;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.card-gesture {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.stamp {
|
||||
position: absolute;
|
||||
top: 60rpx;
|
||||
padding: 10rpx 30rpx;
|
||||
border: 6rpx solid;
|
||||
border-radius: 16rpx;
|
||||
font-size: 52rpx;
|
||||
font-weight: bold;
|
||||
transform: rotate(-18deg);
|
||||
}
|
||||
|
||||
.like-stamp {
|
||||
left: 40rpx;
|
||||
color: #4CD964;
|
||||
border-color: #4CD964;
|
||||
}
|
||||
|
||||
.nope-stamp {
|
||||
right: 40rpx;
|
||||
color: #FF3B30;
|
||||
border-color: #FF3B30;
|
||||
transform: rotate(18deg);
|
||||
}
|
||||
|
||||
.card-info {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 24rpx 30rpx 40rpx;
|
||||
background: linear-gradient(180deg, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 0.55) 100%);
|
||||
}
|
||||
|
||||
.card-name-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-name {
|
||||
font-size: 40rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.card-age {
|
||||
font-size: 32rpx;
|
||||
color: #fff;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.card-online {
|
||||
font-size: 22rpx;
|
||||
color: #fff;
|
||||
background: #4CD964;
|
||||
border-radius: 20rpx;
|
||||
padding: 4rpx 14rpx;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.card-meta {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
.card-meta-i {
|
||||
font-size: 24rpx;
|
||||
color: #fff;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
border-radius: 20rpx;
|
||||
padding: 4rpx 16rpx;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.card-bio {
|
||||
font-size: 24rpx;
|
||||
color: #f0f0f0;
|
||||
margin-top: 12rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin-top: 30rpx;
|
||||
}
|
||||
|
||||
.act-btn {
|
||||
width: 110rpx;
|
||||
height: 110rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 40rpx;
|
||||
background: #fff;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.act-btn.like {
|
||||
background: linear-gradient(135deg, #FF6B6B, #FF8E53);
|
||||
}
|
||||
|
||||
.tip {
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
margin-top: 30rpx;
|
||||
}
|
||||
|
||||
.empty {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 0;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #aaa;
|
||||
margin-bottom: 30rpx;
|
||||
}
|
||||
|
||||
.empty-btn {
|
||||
background: linear-gradient(135deg, #FF6B6B, #FF8E53);
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 40rpx;
|
||||
font-size: 26rpx;
|
||||
padding: 12rpx 50rpx;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
<!-- #ifdef APP -->
|
||||
<scroll-view style="flex:1">
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- #ifdef APP -->
|
||||
</scroll-view>
|
||||
<!-- #endif -->
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,613 @@
|
||||
<template>
|
||||
<scroll-view class="page" :show-scrollbar="false" style="padding-bottom: 140rpx;">
|
||||
<view class="-status-bar"></view>
|
||||
|
||||
<!-- 照片墙 -->
|
||||
<swiper class="photo-swiper" :indicator-dots="true" :circular="true" v-if="userInfo.albumThumbs.length > 0">
|
||||
<swiper-item v-for="(img, i) in userInfo.albumThumbs" :key="i">
|
||||
<image :src="img" mode="aspectFill" class="photo"></image>
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
<view v-else class="photo-swiper photo-empty">
|
||||
<uni-icons type="person" size="80" color="#ccc"></uni-icons>
|
||||
</view>
|
||||
|
||||
<!-- 昵称 / 年龄 / 在线 -->
|
||||
<view class="head">
|
||||
<text class="nickname">{{ userInfo.nickname }}</text>
|
||||
<text class="age" v-if="userInfo.age > 0">{{ userInfo.age }}岁</text>
|
||||
<text class="online" v-if="userInfo.isOnline">在线</text>
|
||||
</view>
|
||||
|
||||
<!-- 标签切换 -->
|
||||
<view class="tabsbox" style="justify-content: space-around;">
|
||||
<text class="tabsbox-item" style="flex:1"
|
||||
:class="currentTab == 'about' ? 'tabsbox-active tabsbox-active-txt' : ''"
|
||||
@click="switchTab('about')">关于TA</text>
|
||||
<text class="tabsbox-item" style="flex:1"
|
||||
:class="currentTab == 'moments' ? 'tabsbox-active tabsbox-active-txt' : ''"
|
||||
@click="switchTab('moments')">动态 ({{ userInfo.profile.momentCount }})</text>
|
||||
</view>
|
||||
|
||||
<!-- 关于TA -->
|
||||
<view v-if="currentTab == 'about'">
|
||||
<view class="panel">
|
||||
<text class="panel-title">个人资料</text>
|
||||
<view class="panel-content">
|
||||
<text class="panel-text" v-if="userInfo.profile.age > 0">{{ userInfo.profile.age }}岁</text>
|
||||
<text class="panel-text" v-if="userInfo.profile.height > 0">{{ userInfo.profile.height }}cm</text>
|
||||
<text class="panel-text" v-if="userInfo.profile.weight > 0">{{ userInfo.profile.weight }}kg</text>
|
||||
<text class="panel-text" v-if="userInfo.profile.live">{{ userInfo.profile.live }}</text>
|
||||
<text class="panel-text" v-if="userInfo.profile.hometown">{{ userInfo.profile.hometown }}</text>
|
||||
<text class="panel-text" v-if="userInfo.profile.education">{{ userInfo.profile.education }}</text>
|
||||
<text class="panel-text" v-if="userInfo.profile.profession">{{ userInfo.profile.profession }}</text>
|
||||
<text class="panel-text" v-if="userInfo.profile.constellation">{{ userInfo.profile.constellation }}</text>
|
||||
<text class="panel-text" v-if="userInfo.profile.annualIncome">{{ userInfo.profile.annualIncome }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="panel" v-if="userInfo.profile.bio">
|
||||
<text class="panel-title">个性签名</text>
|
||||
<view class="panel-content" style="flex-flow: column; align-items: flex-start;">
|
||||
<text class="bio">{{ userInfo.profile.bio }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="panel">
|
||||
<text class="panel-title">收到的礼物</text>
|
||||
<view class="panel-content" style="flex-flow: column; align-items: flex-start;">
|
||||
<text class="bio tip">在聊天中赠送礼物,表达你的心意~</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 动态 -->
|
||||
<view v-if="currentTab == 'moments'">
|
||||
<view class="panel">
|
||||
<view class="panel-content" style="flex-flow: column; align-items: center;">
|
||||
<text class="bio tip">该用户暂未公开动态</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部操作栏 -->
|
||||
<view class="tabbar-op">
|
||||
<view class="tabbar-item" @click="onFollowClick">
|
||||
<uni-icons :type="isFollowed ? 'star-filled' : 'star'" size="40"
|
||||
:color="isFollowed ? '#FFB400' : '#666'"></uni-icons>
|
||||
<text class="tabbar-text">{{ isFollowed ? '已关注' : '关注' }}</text>
|
||||
</view>
|
||||
<view class="tabbar-item" @click="onChatClick">
|
||||
<uni-icons type="chat" size="40" color="#666"></uni-icons>
|
||||
<text class="tabbar-text">私聊</text>
|
||||
</view>
|
||||
<view class="tabbar-item" @click="onGiftClick">
|
||||
<uni-icons type="gift" size="40" color="#666"></uni-icons>
|
||||
<text class="tabbar-text">礼物</text>
|
||||
</view>
|
||||
<view class="tabbar-item tabbar-hion" @click="onAccostClick">
|
||||
<uni-icons type="heart" size="40" color="#FF5C8A"></uni-icons>
|
||||
<text class="tabbar-text">搭讪</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 送礼弹窗 -->
|
||||
<view v-if="showGift" class="gift-mask" @click="closeGift">
|
||||
<view class="gift-sheet" @click.stop>
|
||||
<view class="gift-sheet-head">
|
||||
<text class="gift-sheet-title">赠送礼物</text>
|
||||
<uni-icons type="close" size="22" color="#999" @click="closeGift"></uni-icons>
|
||||
</view>
|
||||
<scroll-view class="gift-sheet-list" direction="vertical" :show-scrollbar="false">
|
||||
<view v-for="(g, i) in gifts" :key="g.id" class="gift-sheet-cell"
|
||||
:class="{ active: selectedGift != null && selectedGift.id == g.id }" @click="selectGift(g)">
|
||||
<text class="gift-sheet-name">{{ g.name }}</text>
|
||||
<text class="gift-sheet-price">{{ g.price }}金币</text>
|
||||
</view>
|
||||
<view v-if="gifts.length == 0" class="gift-sheet-empty">
|
||||
<text class="empty-text">暂无可赠送的礼物</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<view class="gift-sheet-count">
|
||||
<text class="count-label">数量</text>
|
||||
<view class="count-ctrl">
|
||||
<text class="count-btn" @click="giftCount > 1 ? giftCount = giftCount - 1 : null">-</text>
|
||||
<text class="count-num">{{ giftCount }}</text>
|
||||
<text class="count-btn" @click="giftCount = giftCount + 1">+</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="gift-sheet-send" @click="sendGift">
|
||||
<text class="send-text">赠送({{ giftTotal }}金币)</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
import { ref, reactive, computed, onLoad } from 'vue'
|
||||
import { Api } from '@/common/api-service.uts'
|
||||
import { getUserInfo } from '@/stores/user.uts'
|
||||
|
||||
type IViewProfile = {
|
||||
age: number
|
||||
height: number
|
||||
weight: number
|
||||
live: string
|
||||
hometown: string
|
||||
education: string
|
||||
profession: string
|
||||
constellation: string
|
||||
annualIncome: string
|
||||
bio: string
|
||||
momentCount: number
|
||||
uid: number
|
||||
}
|
||||
type IViewUser = {
|
||||
uid: number
|
||||
nickname: string
|
||||
avatar: string
|
||||
albumThumbs: string[]
|
||||
age: number
|
||||
isOnline: boolean
|
||||
profile: IViewProfile
|
||||
}
|
||||
|
||||
const userInfo = reactive<IViewUser>({
|
||||
uid: 0,
|
||||
nickname: '',
|
||||
avatar: '',
|
||||
albumThumbs: [],
|
||||
age: 0,
|
||||
isOnline: false,
|
||||
profile: {
|
||||
age: 0, height: 0, weight: 0, live: '', hometown: '', education: '',
|
||||
profession: '', constellation: '', annualIncome: '', bio: '', momentCount: 0, uid: 0
|
||||
}
|
||||
})
|
||||
|
||||
const currentTab = ref('about')
|
||||
const isFollowed = ref(false)
|
||||
const targetUid = ref(0)
|
||||
|
||||
const switchTab = (tab : string) => {
|
||||
currentTab.value = tab
|
||||
}
|
||||
|
||||
const loadDetail = async (uid : number) => {
|
||||
try {
|
||||
const data = await Api.user.detail(uid)
|
||||
const p = data
|
||||
const city = p.getString('residecity') ?? ''
|
||||
const province = p.getString('resideprovince') ?? ''
|
||||
const age = p.getNumber('age') ?? 0
|
||||
const photos = p.getJSONArray('photos')
|
||||
const album : string[] = []
|
||||
if (photos != null && photos.size() > 0) {
|
||||
for (let i = 0; i < photos.size(); i++) {
|
||||
const s = photos.get(i) as string
|
||||
if (s != null && s.length > 0) album.push(s)
|
||||
}
|
||||
}
|
||||
const avatar = p.getString('avatar') ?? ''
|
||||
if (album.length == 0 && avatar.length > 0) album.push(avatar)
|
||||
|
||||
userInfo.uid = uid
|
||||
userInfo.nickname = p.getString('nickname') ?? '匿名用户'
|
||||
userInfo.avatar = avatar
|
||||
userInfo.albumThumbs = album
|
||||
userInfo.age = age
|
||||
userInfo.isOnline = p.getBoolean('is_online') ?? false
|
||||
userInfo.profile = {
|
||||
age: age,
|
||||
height: p.getNumber('height') ?? 0,
|
||||
weight: p.getNumber('weight') ?? 0,
|
||||
live: city.length > 0 ? city : province,
|
||||
hometown: province,
|
||||
education: p.getString('education') ?? '',
|
||||
profession: p.getString('occupation') ?? '',
|
||||
constellation: p.getString('constellation') ?? '',
|
||||
annualIncome: p.getString('revenue') ?? '',
|
||||
bio: p.getString('bio') ?? '',
|
||||
momentCount: p.getNumber('moment_count') ?? 0,
|
||||
uid: uid
|
||||
}
|
||||
isFollowed.value = (p.getNumber('is_followed') ?? 0) == 1
|
||||
} catch (e : any) {
|
||||
uni.showToast({ title: e?.message ?? '加载失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
const meUid = () : number => {
|
||||
return getUserInfo()?.uid ?? 0
|
||||
}
|
||||
|
||||
const onFollowClick = () => {
|
||||
if (targetUid.value <= 0) return
|
||||
const action = isFollowed.value ? 0 : 1
|
||||
Api.follow.toggle(meUid(), targetUid.value, action).then(() => {
|
||||
isFollowed.value = ! isFollowed.value
|
||||
uni.showToast({ title: isFollowed.value ? '已关注' : '已取消关注', icon: 'none' })
|
||||
}).catch((err : any) => {
|
||||
uni.showToast({ title: err?.message ?? '操作失败', icon: 'none' })
|
||||
})
|
||||
}
|
||||
|
||||
const onChatClick = () => {
|
||||
if (targetUid.value <= 0) return
|
||||
uni.navigateTo({ url: `/pages/chat/chat?uid=${targetUid.value}` })
|
||||
}
|
||||
|
||||
const onGiftClick = () => {
|
||||
showGift.value = true
|
||||
loadGifts()
|
||||
}
|
||||
|
||||
// ===== 送礼弹窗 =====
|
||||
type IGift = {
|
||||
id : number
|
||||
name : string
|
||||
icon : string
|
||||
price : number
|
||||
}
|
||||
const showGift = ref<boolean>(false)
|
||||
const gifts = reactive<IGift[]>([])
|
||||
const selectedGift = ref<IGift | null>(null)
|
||||
const giftCount = ref<number>(1)
|
||||
const giftLoading = ref<boolean>(false)
|
||||
|
||||
const giftTotal = computed<number>(() => {
|
||||
const p = selectedGift.value?.price ?? 0
|
||||
return p * giftCount.value
|
||||
})
|
||||
|
||||
const loadGifts = () => {
|
||||
if (giftLoading.value) return
|
||||
giftLoading.value = true
|
||||
Api.gift.list()
|
||||
.then((res : any) => {
|
||||
const list = (res as Array<IGift>) ?? []
|
||||
gifts.splice(0, gifts.length)
|
||||
list.forEach((g : IGift) => gifts.push(g))
|
||||
if (gifts.length > 0 && selectedGift.value == null) {
|
||||
selectedGift.value = gifts[0]
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
uni.showToast({ title: '礼物加载失败', icon: 'none' })
|
||||
})
|
||||
.finally(() => { giftLoading.value = false })
|
||||
}
|
||||
|
||||
const selectGift = (g : IGift) => {
|
||||
selectedGift.value = g
|
||||
}
|
||||
|
||||
const sendGift = () => {
|
||||
if (selectedGift.value == null) {
|
||||
uni.showToast({ title: '请选择礼物', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (targetUid.value <= 0) return
|
||||
uni.showLoading({ title: '赠送中...', mask: true })
|
||||
Api.gift.send({
|
||||
to_uid: targetUid.value,
|
||||
gift_id: selectedGift.value.id,
|
||||
count: giftCount.value,
|
||||
message: ''
|
||||
})
|
||||
.then(() => {
|
||||
uni.hideLoading()
|
||||
showGift.value = false
|
||||
uni.showToast({ title: '赠送成功', icon: 'success' })
|
||||
})
|
||||
.catch((e : any) => {
|
||||
uni.hideLoading()
|
||||
const msg = (e as UTSJSONObject)?.get('message') as string
|
||||
uni.showToast({ title: msg ?? '赠送失败', icon: 'none' })
|
||||
})
|
||||
}
|
||||
|
||||
const closeGift = () => {
|
||||
showGift.value = false
|
||||
}
|
||||
|
||||
const onAccostClick = () => {
|
||||
if (targetUid.value <= 0) return
|
||||
Api.message.send({
|
||||
receiver_id: targetUid.value,
|
||||
receiver_type: 0,
|
||||
content: '你好,很高兴认识你~'
|
||||
}).then(() => {
|
||||
uni.showToast({ title: '搭讪已送达', icon: 'success' })
|
||||
}).catch((err : any) => {
|
||||
uni.showToast({ title: err?.message ?? '搭讪失败', icon: 'none' })
|
||||
})
|
||||
}
|
||||
|
||||
onLoad((options : any) => {
|
||||
const uid = Number(options?.uid ?? options?.id ?? 0)
|
||||
targetUid.value = uid
|
||||
if (uid > 0) {
|
||||
loadDetail(uid)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #f6f6f6;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.photo-swiper {
|
||||
width: 100%;
|
||||
height: 760rpx;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.photo-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.photo {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.head {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
padding: 24rpx 30rpx;
|
||||
}
|
||||
|
||||
.nickname {
|
||||
font-size: 38rpx;
|
||||
font-weight: bold;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
.age {
|
||||
font-size: 30rpx;
|
||||
color: #666;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.online {
|
||||
font-size: 22rpx;
|
||||
color: #fff;
|
||||
background: #4CD964;
|
||||
border-radius: 20rpx;
|
||||
padding: 4rpx 14rpx;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.tabsbox {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
background: #fff;
|
||||
margin-top: 2rpx;
|
||||
padding: 20rpx 0;
|
||||
}
|
||||
|
||||
.tabsbox-item {
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.tabsbox-active {
|
||||
font-weight: bold;
|
||||
color: #FF5C8A;
|
||||
}
|
||||
|
||||
.panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #fff;
|
||||
margin-top: 16rpx;
|
||||
padding: 20rpx 30rpx;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: bold;
|
||||
padding: 10rpx 0;
|
||||
border-bottom: 1rpx solid #eee;
|
||||
}
|
||||
|
||||
.panel-content {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
padding: 24rpx 0;
|
||||
}
|
||||
|
||||
.panel-text {
|
||||
font-size: 24rpx;
|
||||
border-radius: 25rpx;
|
||||
padding: 6rpx 18rpx;
|
||||
margin: 6rpx;
|
||||
background: #f3f3f3;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.bio {
|
||||
font-size: 26rpx;
|
||||
color: #444;
|
||||
line-height: 40rpx;
|
||||
}
|
||||
|
||||
.tip {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.tabbar-op {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
bottom: var(--window-bottom);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
padding: 16rpx 20rpx;
|
||||
background-color: #fff;
|
||||
border-top: 1rpx solid #e2e2e2;
|
||||
}
|
||||
|
||||
.tabbar-item {
|
||||
width: 16%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-flow: column nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tabbar-text {
|
||||
font-size: 22rpx;
|
||||
color: #666;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.tabbar-hion {
|
||||
width: 40%;
|
||||
flex-flow: row nowrap;
|
||||
background-image: linear-gradient(to bottom, #ffd2fb, #ffaaff);
|
||||
border-radius: 40rpx;
|
||||
padding: 16rpx 0;
|
||||
}
|
||||
|
||||
.gift-mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.gift-sheet {
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
border-top-left-radius: 28rpx;
|
||||
border-top-right-radius: 28rpx;
|
||||
padding: 24rpx 30rpx calc(30rpx + var(--uni-safe-area-inset-bottom));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.gift-sheet-head {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.gift-sheet-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.gift-sheet-list {
|
||||
max-height: 420rpx;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
|
||||
.gift-sheet-cell {
|
||||
width: 22%;
|
||||
margin: 10rpx 1.5%;
|
||||
padding: 20rpx 0;
|
||||
border-radius: 16rpx;
|
||||
background-color: #f7f7f7;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.gift-sheet-cell.active {
|
||||
background-color: #fff0ec;
|
||||
border: 2rpx solid #FF6B6B;
|
||||
}
|
||||
|
||||
.gift-sheet-name {
|
||||
font-size: 26rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.gift-sheet-price {
|
||||
font-size: 22rpx;
|
||||
color: #FF6B6B;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
|
||||
.gift-sheet-empty {
|
||||
width: 100%;
|
||||
padding: 60rpx 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.gift-sheet-count {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 20rpx 0;
|
||||
}
|
||||
|
||||
.count-label {
|
||||
font-size: 28rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.count-ctrl {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.count-btn {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
border-radius: 30rpx;
|
||||
background-color: #f0f0f0;
|
||||
text-align: center;
|
||||
line-height: 60rpx;
|
||||
font-size: 36rpx;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.count-num {
|
||||
min-width: 60rpx;
|
||||
text-align: center;
|
||||
font-size: 30rpx;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.gift-sheet-send {
|
||||
height: 92rpx;
|
||||
border-radius: 46rpx;
|
||||
background: linear-gradient(135deg, #FF6B6B 0%, #FF8E53 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.send-text {
|
||||
font-size: 32rpx;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,451 @@
|
||||
// message-service.uts
|
||||
// 实时消息中枢:统一接管 WebSocket 连接、消息分发与会话管理。
|
||||
//
|
||||
// 职责:
|
||||
// 1. 在用户已登录且已拿到后端 appConf.socketUrl 后,自动注入 token 建立连接;
|
||||
// 2. 监听 wsManager 的 open/message/close/error/reconnect 事件;
|
||||
// 3. 将服务器下发的消息按 header.type 分发为应用级事件(uni.$emit),
|
||||
// 各页面(聊天/通知/系统)只需监听对应事件即可,无需关心底层 WS;
|
||||
// 4. 维护内存中的会话列表与消息列表,提供排序、未读、置顶、免打扰等会话管理;
|
||||
// 5. 提供 sendChat() 便捷发送单聊/群聊消息。
|
||||
//
|
||||
// 注意:连接鉴权采用与后端约定一致的 Authorization 头(Bearer token),
|
||||
// 由 wsManager.connect 透传到 uni.connectSocket 的 header 中。
|
||||
|
||||
import { wsManager } from './websocket.uts'
|
||||
import {
|
||||
IMessageBase,
|
||||
IMessageHeader,
|
||||
IMessagePayload,
|
||||
IMessagePreview,
|
||||
IMessageSession,
|
||||
IMessageServiceCallbacks,
|
||||
IChatMessage,
|
||||
IConnectOptions
|
||||
} from './socket-types.uts'
|
||||
import { getUserToken } from '@/stores/user.uts'
|
||||
import userState from '@/stores/user.uts'
|
||||
|
||||
class MessageService {
|
||||
private initialized = false
|
||||
private connected = false
|
||||
private token = ''
|
||||
|
||||
// 会话管理状态
|
||||
private callbacks : IMessageServiceCallbacks = {}
|
||||
private sessions : Map<string, IMessageSession> = new Map()
|
||||
private messages : Map<string, IMessageBase[]> = new Map() // session_id -> messages[]
|
||||
private currentUserId = userState.uid
|
||||
|
||||
// 注册 WS 事件监听(幂等,只执行一次)
|
||||
init() : void {
|
||||
if (this.initialized == true) {
|
||||
return
|
||||
}
|
||||
this.initialized = true
|
||||
wsManager.on({
|
||||
onOpen: (res : any) => {
|
||||
this.connected = true
|
||||
console.log('[MessageService] WS 已连接')
|
||||
uni.$emit('onWsConnected', res)
|
||||
},
|
||||
onMessage: (msg : IMessageBase) => {
|
||||
// 1) 维护会话与消息列表
|
||||
this.handleWebSocketMessage(msg)
|
||||
// 2) 应用级事件分发
|
||||
this.dispatch(msg)
|
||||
},
|
||||
onClose: (res : any) => {
|
||||
this.connected = false
|
||||
console.log('[MessageService] WS 已关闭')
|
||||
uni.$emit('onWsDisconnected', res)
|
||||
},
|
||||
onError: (err : any) => {
|
||||
console.error('[MessageService] WS 错误', err)
|
||||
uni.$emit('onWsError', err)
|
||||
},
|
||||
onReconnect: (res : any) => {
|
||||
console.log('[MessageService] WS 重连中', res)
|
||||
uni.$emit('onWsReconnect', res)
|
||||
},
|
||||
onReconnectFailed: (res : any) => {
|
||||
console.error('[MessageService] WS 重连失败', res)
|
||||
uni.$emit('onWsReconnectFailed', res)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 已登录时按需建立 WS 连接(幂等)。
|
||||
* @param socketUrl 后端 appConf.socketUrl,为空则跳过
|
||||
*/
|
||||
connectIfNeeded(socketUrl : string | null) : void {
|
||||
this.init()
|
||||
if (socketUrl == null || socketUrl.length == 0) {
|
||||
console.warn('[MessageService] 未配置 socketUrl,跳过 WS 连接')
|
||||
return
|
||||
}
|
||||
const tokenResult = getUserToken()
|
||||
if (tokenResult == null || tokenResult.accessToken == null || tokenResult.accessToken.length == 0) {
|
||||
console.warn('[MessageService] 未登录或 token 为空,跳过 WS 连接')
|
||||
return
|
||||
}
|
||||
if (wsManager.isConnected() == true) {
|
||||
return
|
||||
}
|
||||
this.token = tokenResult.accessToken!
|
||||
const options : IConnectOptions = {
|
||||
reconnectInterval: 3000,
|
||||
heartbeatInterval: 20000,
|
||||
maxReconnectCount: 5,
|
||||
header: {
|
||||
'Authorization': `Bearer ${this.token}`
|
||||
}
|
||||
}
|
||||
wsManager.connect(socketUrl, options)
|
||||
}
|
||||
|
||||
// 主动断开
|
||||
disconnect() : void {
|
||||
wsManager.close(1000, 'user logout')
|
||||
this.connected = false
|
||||
}
|
||||
|
||||
isConnected() : boolean {
|
||||
return this.connected
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送聊天消息(单聊/群聊)。
|
||||
* 真实送达以服务器回执为准,本方法仅保证消息已提交至 WS。
|
||||
*/
|
||||
sendChat(receiver : number, sessionId : string, contentType : string, content : any) : void {
|
||||
const header : IMessageHeader = {
|
||||
id: `c_${Date.now()}_${Math.floor(Math.random() * 1000)}`,
|
||||
type: 'chat',
|
||||
target: 'single',
|
||||
seqno: 1,
|
||||
timestamp: Date.now(),
|
||||
source: 'client',
|
||||
version: '1.0.0',
|
||||
token: this.token
|
||||
}
|
||||
const msg : IMessageBase = {
|
||||
header: header,
|
||||
payload: {
|
||||
uid: userState.uid,
|
||||
receiver: receiver,
|
||||
session_id: sessionId,
|
||||
type: contentType,
|
||||
status: 'sending',
|
||||
content: content
|
||||
} as any
|
||||
}
|
||||
wsManager.send(msg)
|
||||
}
|
||||
|
||||
// 按消息类型分发为应用级事件
|
||||
private dispatch(msg : IMessageBase) : void {
|
||||
const type = msg.header.type
|
||||
switch (type) {
|
||||
case 'chat':
|
||||
uni.$emit('onChatMessage', msg)
|
||||
break
|
||||
case 'notify':
|
||||
uni.$emit('onWsNotify', msg)
|
||||
break
|
||||
case 'status':
|
||||
uni.$emit('onWsStatus', msg)
|
||||
break
|
||||
case 'system':
|
||||
uni.$emit('onWsSystem', msg)
|
||||
break
|
||||
case 'pong':
|
||||
// 心跳回包,wsManager 内部已处理,无需外发
|
||||
break
|
||||
default:
|
||||
uni.$emit('onWsMessage', msg)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 WebSocket 消息,维护会话与消息列表
|
||||
*/
|
||||
private handleWebSocketMessage(message : IMessageBase) : void {
|
||||
if (message == null) {
|
||||
return
|
||||
}
|
||||
const { header, payload } = message
|
||||
const session_id = this.getSessionId(header.target, payload)
|
||||
// 1. 更新会话信息
|
||||
this.updateSession(session_id, message)
|
||||
// 2. 添加到消息列表
|
||||
this.addMessageToSession(session_id, message)
|
||||
// 3. 触发 UI 更新
|
||||
uni.$emit('session_updated', session_id)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话ID
|
||||
*/
|
||||
private getSessionId(target : string, payload : IMessagePayload) : string {
|
||||
const sessionPayload = payload as IChatMessage
|
||||
const sessionId = sessionPayload.session_id ?? ''
|
||||
switch (target) {
|
||||
case 'single':
|
||||
return 'single' + sessionId
|
||||
case 'group':
|
||||
return 'group' + sessionId
|
||||
case 'room':
|
||||
return 'room' + sessionId
|
||||
case 'system':
|
||||
default:
|
||||
return 'system' + sessionId
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话名称
|
||||
*/
|
||||
private getSessionName(payload : IMessagePayload, type : string) : string {
|
||||
const chatPayload = payload as IChatMessage
|
||||
switch (type) {
|
||||
case 'single':
|
||||
return chatPayload.nickname ?? '未知用户'
|
||||
case 'group':
|
||||
return '群聊'
|
||||
case 'room':
|
||||
return chatPayload.nickname ?? '房间'
|
||||
case 'system':
|
||||
return '系统消息'
|
||||
default:
|
||||
return '会话'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建会话
|
||||
*/
|
||||
private createSession(session_id : string, message : IMessageBase) : IMessageSession {
|
||||
const { header, payload } = message
|
||||
const session_type = header.target
|
||||
const chatPayload = payload as IChatMessage
|
||||
return {
|
||||
session_id,
|
||||
session_type,
|
||||
session_name: this.getSessionName(payload, session_type),
|
||||
session_avatar: chatPayload.avatar ?? 'https://cdn.example.com/default_avatar.png',
|
||||
last_message: this.createMessagePreview(message),
|
||||
unread_count: 1,
|
||||
is_pinned: false,
|
||||
is_muted: false,
|
||||
last_active_time: message.header.timestamp,
|
||||
created_time: message.header.timestamp,
|
||||
status: 'normal',
|
||||
ext: {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建消息预览
|
||||
*/
|
||||
private createMessagePreview(message : IMessageBase) : IMessagePreview {
|
||||
const { header, payload } = message
|
||||
const chatPayload = payload as IChatMessage
|
||||
return {
|
||||
id: header.id,
|
||||
type: chatPayload.type ?? 'text',
|
||||
content_preview: this.formatContentPreview(payload),
|
||||
timestamp: header.timestamp,
|
||||
msg_status: chatPayload.status ?? 'delivered',
|
||||
is_mentioned: this.isMentioned(payload),
|
||||
is_important: this.isImportant(payload),
|
||||
ext: chatPayload.ext ?? {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否@我
|
||||
*/
|
||||
private isMentioned(payload : IMessagePayload) : boolean {
|
||||
const mentionPayload = payload as IChatMessage
|
||||
if (mentionPayload.at_user == null)
|
||||
return false
|
||||
else if (mentionPayload.at_user == 'all')
|
||||
return true
|
||||
else if ((mentionPayload.at_user as number[]).includes(this.currentUserId))
|
||||
return true
|
||||
else
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否重要消息
|
||||
*/
|
||||
private isImportant(payload : IMessagePayload) : boolean {
|
||||
const chatPayload = payload as IChatMessage
|
||||
// 红包、转账、重要通知等
|
||||
const importantTypes = ['red_packet', 'transfer', 'system_notify']
|
||||
return importantTypes.includes(chatPayload.type ?? '')
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化消息预览内容
|
||||
*/
|
||||
private formatContentPreview(payload : IMessagePayload) : string {
|
||||
const chatPayload = payload as IChatMessage
|
||||
if (chatPayload.content == null) return '[消息]'
|
||||
const type = chatPayload.type ?? 'text'
|
||||
switch (type) {
|
||||
case 'text':
|
||||
return '[文本消息]'
|
||||
case 'image':
|
||||
return '[图片]'
|
||||
case 'audio':
|
||||
return '[语音]'
|
||||
case 'video':
|
||||
return '[视频]'
|
||||
case 'file':
|
||||
return '[文件]'
|
||||
case 'location':
|
||||
return '[位置]'
|
||||
case 'emoji':
|
||||
return '[表情]'
|
||||
case 'sticker':
|
||||
return '[贴纸]'
|
||||
case 'red_packet':
|
||||
return '[红包]'
|
||||
case 'recall':
|
||||
return '[消息已撤回]'
|
||||
case 'system':
|
||||
return '[系统消息]'
|
||||
default:
|
||||
return '[消息]'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话信息
|
||||
*/
|
||||
private updateSession(session_id : string, message : IMessageBase) : void {
|
||||
const { header, payload } = message
|
||||
const chatPayload = payload as IChatMessage
|
||||
// 获取或创建会话
|
||||
let session = this.sessions.get(session_id)
|
||||
if (session == null) {
|
||||
session = this.createSession(session_id, message)
|
||||
}
|
||||
// 更新最后消息
|
||||
session.last_message = this.createMessagePreview(message)
|
||||
session.last_active_time = header.timestamp
|
||||
// 增加未读数(如果不是自己发送的消息)
|
||||
if (chatPayload.uid != this.currentUserId) {
|
||||
session.unread_count++
|
||||
}
|
||||
this.sessions.set(session_id, session)
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加消息到会话
|
||||
*/
|
||||
private addMessageToSession(session_id : string, message : IMessageBase) : void {
|
||||
if (!this.messages.has(session_id)) {
|
||||
this.messages.set(session_id, [])
|
||||
}
|
||||
const sessionMessages = this.messages.get(session_id)!
|
||||
sessionMessages.push(message)
|
||||
// 保持消息按时间排序
|
||||
sessionMessages.sort((a, b) => a.header.timestamp - b.header.timestamp)
|
||||
// 限制每会话最多保存100条消息
|
||||
if (sessionMessages.length > 100) {
|
||||
sessionMessages.splice(0, sessionMessages.length - 100)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取排序后的会话列表
|
||||
*/
|
||||
getSortedSessions() : IMessageSession[] {
|
||||
const sessions : IMessageSession[] = []
|
||||
this.sessions.forEach((value, key) => {
|
||||
sessions.push(value)
|
||||
})
|
||||
// 排序规则:
|
||||
// 1. 置顶的在前
|
||||
// 2. 未读消息数多的在前
|
||||
// 3. 未读且被@的在前
|
||||
// 4. 最后活跃时间新的在前
|
||||
sessions.sort((a, b) => {
|
||||
// 置顶排序
|
||||
if (a.is_pinned != b.is_pinned) {
|
||||
return a.is_pinned ? -1 : 1
|
||||
}
|
||||
// 未读数排序
|
||||
if (a.unread_count != b.unread_count) {
|
||||
return b.unread_count - a.unread_count
|
||||
}
|
||||
// @消息排序
|
||||
const aHasMention = a.last_message?.is_mentioned ?? false
|
||||
const bHasMention = b.last_message?.is_mentioned ?? false
|
||||
if (aHasMention != bHasMention) {
|
||||
return aHasMention ? -1 : 1
|
||||
}
|
||||
// 最后活跃时间排序
|
||||
return b.last_active_time - a.last_active_time
|
||||
})
|
||||
return sessions
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话消息列表
|
||||
*/
|
||||
getSessionMessages(session_id : string) : any[] {
|
||||
return (this.messages.get(session_id) as any[] | null) ?? []
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空会话未读数
|
||||
*/
|
||||
clearUnread(session_id : string) : void {
|
||||
const session = this.sessions.get(session_id)
|
||||
if (session != null) {
|
||||
session.unread_count = 0
|
||||
uni.$emit('session_updated', session_id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 置顶/取消置顶
|
||||
*/
|
||||
togglePin(session_id : string, is_pinned : boolean = true) : void {
|
||||
const session = this.sessions.get(session_id)
|
||||
if (session != null) {
|
||||
session.is_pinned = is_pinned
|
||||
uni.$emit('session_updated', session_id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 免打扰/取消免打扰
|
||||
*/
|
||||
toggleMute(session_id : string, is_muted : boolean = true) : void {
|
||||
const session = this.sessions.get(session_id)
|
||||
if (session != null) {
|
||||
session.is_muted = is_muted
|
||||
uni.$emit('session_updated', session_id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册消息回调
|
||||
*/
|
||||
on(callbacks : IMessageServiceCallbacks) : void {
|
||||
if (callbacks.onNewMessage != null) this.callbacks.onNewMessage = callbacks.onNewMessage
|
||||
if (callbacks.onMessageStatusChange != null) this.callbacks.onMessageStatusChange = callbacks.onMessageStatusChange
|
||||
}
|
||||
}
|
||||
|
||||
export { IMessageSession }
|
||||
export const messageService = new MessageService()
|
||||
export const messageManager = messageService
|
||||
export default messageService
|
||||
@@ -0,0 +1,453 @@
|
||||
|
||||
export type ContentType =
|
||||
| 'text' // 文本
|
||||
| 'image' // 图片
|
||||
| 'audio' // 音频
|
||||
| 'video' // 视频
|
||||
| 'file' // 文件
|
||||
| 'location' // 位置
|
||||
| 'emoji' // 表情
|
||||
| 'sticker' // 贴纸
|
||||
| 'card' // 卡片
|
||||
| 'system' // 系统消息
|
||||
| 'notify' // 通知消息
|
||||
| 'status' // 状态消息
|
||||
| 'game' // 游戏消息
|
||||
| 'room' // 房间消息
|
||||
| 'user' // 用户消息
|
||||
| 'error' // 错误消息
|
||||
| 'red_packet' // 红包
|
||||
| 'transfer' // 转账
|
||||
| 'recall' // 撤回
|
||||
|
||||
export type MessageStatus =
|
||||
| 'sending' // 发送中
|
||||
| 'sent' // 已发送
|
||||
| 'delivereadBase' //已送达
|
||||
| 'read' // 已读
|
||||
| 'failed' // 失败
|
||||
| 'recalled'; // 已撤回
|
||||
|
||||
// ==================== 1. 文本消息 ====================
|
||||
export type IHeartbeatMessage = {
|
||||
uid : number; // 用户ID
|
||||
nickname ?: string; // 昵称(可选,用于显示)
|
||||
avatar ?: string; // 头像URL(可选,用于显示)
|
||||
session_id ?: string; // 会话ID uid | group_id | room _id
|
||||
status : MessageStatus;
|
||||
rtt ?: number; // 往返延迟(毫秒,响应时返回)
|
||||
type ?: 'text';
|
||||
content ?: string | IMessageContent
|
||||
at_all ?: boolean;
|
||||
at_user ?: null | 'all' | number[];
|
||||
ext ?: any; // 扩展字段
|
||||
}
|
||||
|
||||
// ==================== 2. 图片消息 ====================
|
||||
type IImageField = {
|
||||
// 图片消息特有字段
|
||||
file_url : string;
|
||||
file_name ?: string;
|
||||
file_size ?: number;
|
||||
file_mime ?: string;
|
||||
file_width ?: number;
|
||||
file_height ?: number;
|
||||
thumbnail_url ?: string;
|
||||
thumbnail_size ?: number;
|
||||
at_user ?: number[];
|
||||
}
|
||||
|
||||
// ==================== 3. 音频消息 ====================
|
||||
type IVoiceField = {
|
||||
// 音频消息特有字段
|
||||
file_url : string;
|
||||
file_name ?: string;
|
||||
file_size ?: number;
|
||||
file_mime ?: string;
|
||||
file_duration ?: number;
|
||||
thumbnail_url ?: string;
|
||||
}
|
||||
|
||||
// ==================== 4. 视频消息 ====================
|
||||
type IVideoField = {
|
||||
// 音频消息特有字段
|
||||
file_url : string;
|
||||
file_name ?: string;
|
||||
file_size ?: number;
|
||||
file_mime ?: string;
|
||||
file_duration ?: number;
|
||||
thumbnail_url ?: string;
|
||||
}
|
||||
|
||||
// ==================== 5. 文件消息 ====================
|
||||
type IFileField = {
|
||||
// 文件消息特有字段
|
||||
file_url : string;
|
||||
file_name : string;
|
||||
file_size : number;
|
||||
file_mime ?: string;
|
||||
}
|
||||
|
||||
// ==================== 6. 位置消息 ====================
|
||||
type ILocationField = {
|
||||
// 位置消息特有字段
|
||||
latitude : number;
|
||||
longitude : number;
|
||||
address ?: string;
|
||||
}
|
||||
|
||||
// ==================== 7. 表情消息 ====================
|
||||
type IEmojiField = {
|
||||
|
||||
// 表情消息特有字段
|
||||
emoji_code : string;
|
||||
}
|
||||
|
||||
// ==================== 8. 贴纸消息 ====================
|
||||
type IStickerField = {
|
||||
// 贴纸消息特有字段
|
||||
sticker_id : string;
|
||||
sticker_url : string;
|
||||
}
|
||||
|
||||
// ==================== 9. 卡片消息 ====================
|
||||
type ICardField = {
|
||||
// 卡片消息特有字段
|
||||
card_title : string;
|
||||
card_desc ?: string;
|
||||
card_image ?: string;
|
||||
card_url ?: string;
|
||||
card_data ?: any;
|
||||
}
|
||||
|
||||
// ==================== 10. 系统消息 ====================
|
||||
type ISystemField = {
|
||||
// 系统消息特有字段
|
||||
content : string;
|
||||
sub_type ?: string;
|
||||
error_code ?: number;
|
||||
error_msg ?: string;
|
||||
error_detail ?: string;
|
||||
}
|
||||
|
||||
export type IMessageContent =
|
||||
string
|
||||
| IImageField
|
||||
| IVoiceField
|
||||
| IVideoField
|
||||
| IFileField
|
||||
| ILocationField
|
||||
| IEmojiField
|
||||
| IStickerField
|
||||
| ICardField
|
||||
| ISystemField
|
||||
|
||||
export interface IChatMessage {
|
||||
uid : number;
|
||||
nickname ?: string; // 昵称(可选,用于显示)
|
||||
avatar ?: string; // 头像URL(可选,用于显示)
|
||||
|
||||
session_id ?: string; // 会话ID uid | group_id | room _id
|
||||
receiver : number // 接收用户UID
|
||||
|
||||
type : ContentType;
|
||||
status : MessageStatus;
|
||||
msg_id ?: string;
|
||||
ext ?: any;
|
||||
// 文本消息特有字段
|
||||
content : IMessageContent;
|
||||
at_user ?: 'all' | number[];
|
||||
}
|
||||
|
||||
// ==================== 11. 通知消息 ====================
|
||||
export type NotifyType = 'system' | 'friend' | 'group' | 'game';
|
||||
export type ActionType = 'accept' | 'reject' | 'ignore';
|
||||
|
||||
export interface INotifyMessage {
|
||||
// 基础字段
|
||||
uid : number;
|
||||
nickname ?: string; // 昵称(可选,用于显示)
|
||||
avatar ?: string; // 头像URL(可选,用于显示)
|
||||
type : 'notify';
|
||||
status : MessageStatus;
|
||||
msg_id ?: string;
|
||||
receiver : number
|
||||
at_user ?: number[];
|
||||
|
||||
session_id ?: string;
|
||||
ext ?: any;
|
||||
|
||||
content ?: string | IMessageContent
|
||||
// 通知消息特有字段
|
||||
notify_type : NotifyType;
|
||||
notify_title ?: string;
|
||||
notify_content : string;
|
||||
notify_icon ?: string;
|
||||
need_confirm ?: boolean;
|
||||
action ?: ActionType;
|
||||
}
|
||||
|
||||
// ==================== 12. 状态消息 ====================
|
||||
export interface IStatusMessage {
|
||||
uid : number;
|
||||
nickname ?: string; // 昵称(可选,用于显示)
|
||||
avatar ?: string; // 头像URL(可选,用于显示)
|
||||
|
||||
session_id ?: string; // 会话ID uid | group_id | room _id
|
||||
receiver : number // 接收用户UID
|
||||
|
||||
content ?: string | IMessageContent
|
||||
type : 'status';
|
||||
status : MessageStatus;
|
||||
msg_id ?: string;
|
||||
at_user ?: number[];
|
||||
ext ?: any;
|
||||
|
||||
// 状态消息特有字段
|
||||
status_type : 'online' | 'offline' | 'typing' | 'recording';
|
||||
status_data ?: any;
|
||||
}
|
||||
|
||||
// ==================== 13. 游戏消息 ====================
|
||||
export type GameType = 'chess' | 'poker' | 'mahjong' | 'custom';
|
||||
export type GameAction = 'start' | 'move' | 'end' | 'ready' | 'leave';
|
||||
export type GameState = 'waiting' | 'playing' | 'finished';
|
||||
|
||||
export interface IGameMessage {
|
||||
uid : number;
|
||||
nickname ?: string; // 昵称(可选,用于显示)
|
||||
avatar ?: string; // 头像URL(可选,用于显示)
|
||||
|
||||
session_id ?: string; // 会话ID uid | group_id | room _id
|
||||
receiver : number // 接收用户UID
|
||||
|
||||
content ?: string | IMessageContent
|
||||
type : 'game';
|
||||
status : MessageStatus;
|
||||
msg_id ?: string;
|
||||
at_user ?: number[];
|
||||
ext ?: any;
|
||||
|
||||
// 游戏消息特有字段
|
||||
game_type : GameType;
|
||||
game_action : GameAction;
|
||||
game_data ?: any;
|
||||
game_status ?: GameState;
|
||||
current_player ?: string;
|
||||
game_score ?: number;
|
||||
}
|
||||
|
||||
// ==================== 14. 房间消息 ====================
|
||||
export type RoomType = 'game' | 'chat';
|
||||
export type RoomStatus = 'waiting' | 'playing' | 'finished' | 'closed';
|
||||
|
||||
export interface IRoomMember {
|
||||
member_id : string;
|
||||
nickname ?: string;
|
||||
avatar ?: string;
|
||||
role ?: 'owner' | 'admin' | 'member';
|
||||
join_time ?: number;
|
||||
online ?: boolean;
|
||||
}
|
||||
|
||||
export interface IRoomMessage {
|
||||
uid : number;
|
||||
nickname ?: string; // 昵称(可选,用于显示)
|
||||
avatar ?: string; // 头像URL(可选,用于显示)
|
||||
|
||||
session_id ?: string; // 会话ID uid | group_id | room _id
|
||||
receiver : number // 接收用户UID
|
||||
|
||||
content ?: string | IMessageContent
|
||||
type : 'room';
|
||||
status : MessageStatus;
|
||||
msg_id ?: string;
|
||||
at_user ?: number[];
|
||||
ext ?: any;
|
||||
|
||||
// 房间消息特有字段
|
||||
room_name ?: string;
|
||||
room_type : RoomType;
|
||||
room_status ?: RoomStatus;
|
||||
room_members ?: IRoomMember[];
|
||||
room_max_members ?: number;
|
||||
room_description ?: string;
|
||||
room_created_time ?: number;
|
||||
}
|
||||
|
||||
// ==================== 15. 用户消息 ====================
|
||||
export type UserStatus = 'online' | 'offline' | 'away' | 'busy';
|
||||
|
||||
export interface IUserMessage {
|
||||
uid : number;
|
||||
nickname ?: string; // 昵称(可选,用于显示)
|
||||
avatar ?: string; // 头像URL(可选,用于显示)
|
||||
|
||||
session_id ?: string; // 会话ID uid | group_id | room _id
|
||||
receiver : number // 接收用户UID
|
||||
|
||||
content ?: string | IMessageContent
|
||||
type : 'user';
|
||||
status : MessageStatus;
|
||||
msg_id ?: string;
|
||||
at_user ?: number[];
|
||||
ext ?: any;
|
||||
|
||||
user_status ?: UserStatus;
|
||||
signature ?: string;
|
||||
user_data ?: any;
|
||||
}
|
||||
|
||||
// ==================== 16. 错误消息 ====================
|
||||
export interface IErrorMessage {
|
||||
uid : number;
|
||||
nickname ?: string; // 昵称(可选,用于显示)
|
||||
avatar ?: string; // 头像URL(可选,用于显示)
|
||||
|
||||
session_id ?: string; // 会话ID uid | group_id | room _id
|
||||
receiver : number; // 接收用户UID
|
||||
|
||||
type? : 'error';
|
||||
content ?: string | IMessageContent
|
||||
status? : MessageStatus;
|
||||
msg_id ?: string;
|
||||
at_user ?: number[];
|
||||
ext ?: any;
|
||||
|
||||
// 错误消息特有字段
|
||||
error_code ?: number;
|
||||
error_msg ?: string;
|
||||
error_detail ?: string;
|
||||
error_data ?: any;
|
||||
}
|
||||
|
||||
// ==================== 17. 联合类型组织 ====================
|
||||
// 聊天消息(用户之间的消息)
|
||||
|
||||
|
||||
// 完整的消息载荷类型
|
||||
export type IMessagePayload =
|
||||
IHeartbeatMessage
|
||||
| IChatMessage // 聊天消息
|
||||
| INotifyMessage // 通知消息
|
||||
| IStatusMessage // 状态消息
|
||||
| IGameMessage // 游戏消息
|
||||
| IRoomMessage // 房间消息
|
||||
| IUserMessage // 用户消息
|
||||
| IErrorMessage; // 错误消息
|
||||
|
||||
|
||||
|
||||
|
||||
export type IMessageHeader = {
|
||||
// 消息唯一ID(雪花算法生成,必填)
|
||||
id : string;
|
||||
// 消息类型(必填)
|
||||
type : 'ping' | 'pong' | 'chat' | 'notify' | 'status' | 'game' | 'system';
|
||||
// 目标会话类型(single/group/room/system,选填)
|
||||
target : "single" | "group" | "room" | "system";
|
||||
// 客户端序列号(用于去重和顺序保证,必填)
|
||||
seqno : number;
|
||||
// 时间戳(毫秒,必填)
|
||||
timestamp : number;
|
||||
// 消息来源(client/server,选填)
|
||||
source ?: string;
|
||||
version : "1.0.0" | string
|
||||
token ?: string
|
||||
platform ?: 'ios' | 'android' | 'web'; // 平台类型
|
||||
device_id ?: string; // 设备ID
|
||||
network_type ?: 'wifi' | '4g' | '5g' | 'unknown'; // 网络类型
|
||||
|
||||
}
|
||||
|
||||
export type IMessageBase = {
|
||||
// 消息头
|
||||
header : IMessageHeader;
|
||||
// 消息体
|
||||
payload : IMessagePayload;
|
||||
}
|
||||
|
||||
export type IMessageSession = {
|
||||
// 会话唯一标识(user_id/group_id/room_id/system)
|
||||
session_id : string;
|
||||
// 会话类型(single/group/room/system)
|
||||
session_type : 'single' | 'group' | 'room' | 'system';
|
||||
// 会话名称
|
||||
session_name : string;
|
||||
// 会话头像/封面
|
||||
session_avatar : string;
|
||||
// 最后一条消息
|
||||
last_message : IMessagePreview | null;
|
||||
// 未读消息数
|
||||
unread_count : number;
|
||||
// 是否置顶
|
||||
is_pinned : boolean;
|
||||
// 是否免打扰
|
||||
is_muted : boolean;
|
||||
// 最后活跃时间(毫秒时间戳)
|
||||
last_active_time : number;
|
||||
// 创建时间
|
||||
created_time : number;
|
||||
// 会话状态(normal/archived/deleted)
|
||||
status : 'normal' | 'archived' | 'deleted';
|
||||
// 扩展字段
|
||||
ext ?: any;
|
||||
}
|
||||
|
||||
export type IMessagePreview = {
|
||||
// 消息ID
|
||||
id : string;
|
||||
// 消息类型
|
||||
type : string;
|
||||
// 消息内容预览
|
||||
content_preview : string;
|
||||
// 发送者(单聊时显示对方昵称)
|
||||
sender_name ?: string;
|
||||
// 发送者头像
|
||||
sender_avatar ?: string;
|
||||
// 消息时间
|
||||
timestamp : number;
|
||||
// 消息状态
|
||||
msg_status : string;
|
||||
// 是否@我
|
||||
is_mentioned : boolean;
|
||||
// 是否为重要消息
|
||||
is_important : boolean;
|
||||
// 消息扩展信息
|
||||
ext ?: any;
|
||||
}
|
||||
|
||||
export type IWebSocketPayload = string | UTSJSONObject | ArrayBuffer | IMessageBase
|
||||
|
||||
export type IConnectOptions = {
|
||||
reconnectInterval ?: number
|
||||
heartbeatInterval ?: number
|
||||
maxReconnectCount ?: number
|
||||
protocols ?: Array<string> | null
|
||||
header ?: UTSJSONObject | null
|
||||
heartbeatTimeout ?: number
|
||||
connectTimeout ?: number
|
||||
debug ?: boolean
|
||||
}
|
||||
|
||||
export type IWebSocketCallbacks = {
|
||||
onOpen ?: ((res : any) => void) | null
|
||||
onMessage ?: ((res : IMessageBase) => void) | null
|
||||
onError ?: ((res : any) => void) | null
|
||||
onClose ?: ((res : any) => void) | null
|
||||
onReconnect ?: ((res : any) => void) | null
|
||||
onReconnectFailed ?: ((res : any) => void) | null
|
||||
onHeartbeatTimeout ?: ((res : any) => void) | null
|
||||
}
|
||||
|
||||
export type IMessageQueueItem = {
|
||||
data : IMessageBase | IWebSocketPayload
|
||||
success : (() => void) | null
|
||||
fail : ((err : any) => void) | null
|
||||
}
|
||||
|
||||
export type IMessageServiceCallbacks = {
|
||||
onNewMessage ?: ((msg : IMessageBase) => void) | null
|
||||
//onConversationUpdate ?: ((conv : IConversation) => void) | null
|
||||
onMessageStatusChange ?: ((msgId : string, status : string) => void) | null
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
|
||||
|
||||
|
||||
// 消息类型 文本 图片 音频 视频 文件 // 位置 表情 // 贴纸// 卡片 系统消息
|
||||
export type ContentType = 'text' | 'image' | 'audio' | 'video' | 'file' | 'location' | 'emoji' | 'sticker' | 'card' | 'system'
|
||||
|
||||
// 消息状态 发送中 已发送 已送达 已读 失败 已撤回
|
||||
export type MessageStatus = 'sending' | 'sent' | 'delivered' | 'read' | 'failed' | 'recalled'
|
||||
|
||||
|
||||
export interface IPayloadBase {
|
||||
msg_id : string; // 消息ID
|
||||
seq_no : number; // 序列号
|
||||
timestamp : number; // 时间戳
|
||||
uid : number; // 发送者ID
|
||||
msg_status : MessageStatus; // 消息状态
|
||||
target_msg_id ?: string; // 目标消息ID
|
||||
to_user_id ?: string; // 会话路由
|
||||
group_id ?: string;
|
||||
room_id ?: string;
|
||||
session_id ?: string;
|
||||
ext ?: any; // 扩展字段
|
||||
}
|
||||
export interface IHeartbeatMessage extends IPayloadBase {
|
||||
msg_id : string; // 消息ID
|
||||
seq_no : number; // 序列号
|
||||
timestamp : number; // 时间戳
|
||||
uid : number; // 发送者ID
|
||||
}
|
||||
// ==================== 3. 聊天消息类型 ====================
|
||||
|
||||
// 文本消息
|
||||
export interface ITextMessage extends IPayloadBase {
|
||||
content_type : ContentType
|
||||
content : string;
|
||||
at_all ?: boolean;
|
||||
at_users ?: number[];
|
||||
sub_type ?: string;
|
||||
}
|
||||
|
||||
// 图片消息
|
||||
export interface IImageMessage extends IPayloadBase {
|
||||
content_type : ContentType
|
||||
file_url : string;
|
||||
file_name ?: string;
|
||||
file_size ?: number;
|
||||
file_mime ?: string;
|
||||
file_width ?: number;
|
||||
file_height ?: number;
|
||||
thumbnail_url ?: string;
|
||||
thumbnail_size ?: number;
|
||||
at_all ?: boolean;
|
||||
at_users ?: number[];
|
||||
}
|
||||
|
||||
// 音频消息
|
||||
export interface IAudioMessage extends IPayloadBase {
|
||||
content_type : ContentType
|
||||
file_url : string;
|
||||
file_name ?: string;
|
||||
file_size ?: number;
|
||||
file_mime ?: string;
|
||||
file_duration ?: number;
|
||||
thumbnail_url ?: string;
|
||||
}
|
||||
|
||||
// 视频消息
|
||||
export interface IVideoMessage extends IPayloadBase {
|
||||
content_type : ContentType
|
||||
file_url : string;
|
||||
file_name ?: string;
|
||||
file_size ?: number;
|
||||
file_mime ?: string;
|
||||
file_duration ?: number;
|
||||
file_width ?: number;
|
||||
file_height ?: number;
|
||||
thumbnail_url ?: string;
|
||||
thumbnail_size ?: number;
|
||||
}
|
||||
|
||||
// 文件消息
|
||||
export interface IFileMessage extends IPayloadBase {
|
||||
content_type : ContentType
|
||||
file_url : string;
|
||||
file_name : string;
|
||||
file_size : number;
|
||||
file_mime ?: string;
|
||||
}
|
||||
|
||||
// 位置消息
|
||||
export interface ILocationMessage extends IPayloadBase {
|
||||
content_type : ContentType
|
||||
latitude : number;
|
||||
longitude : number;
|
||||
address ?: string;
|
||||
}
|
||||
|
||||
// 表情消息
|
||||
export interface IEmojiMessage extends IPayloadBase {
|
||||
content_type : ContentType
|
||||
emoji_code : string;
|
||||
}
|
||||
|
||||
// 贴纸消息
|
||||
export interface IStickerMessage extends IPayloadBase {
|
||||
content_type : ContentType
|
||||
sticker_id : string;
|
||||
sticker_url : string;
|
||||
}
|
||||
|
||||
// 卡片消息
|
||||
export interface ICardMessage extends IPayloadBase {
|
||||
content_type : ContentType
|
||||
card_title : string;
|
||||
card_desc ?: string;
|
||||
card_image ?: string;
|
||||
card_url ?: string;
|
||||
card_data ?: any;
|
||||
}
|
||||
|
||||
// 系统消息
|
||||
export interface ISystemMessage extends IPayloadBase {
|
||||
content_type : ContentType
|
||||
content : string;
|
||||
sub_type ?: string;
|
||||
error_code ?: number;
|
||||
error_msg ?: string;
|
||||
error_detail ?: string;
|
||||
}
|
||||
|
||||
|
||||
// ==================== 4. 通知消息类型 ====================
|
||||
|
||||
export type NotifyType = 'system' | 'friend' | 'group' | 'game';
|
||||
export type ActionType = 'accept' | 'reject' | 'ignore';
|
||||
|
||||
|
||||
export interface INotifyMessage extends IPayloadBase {
|
||||
notify_type : NotifyType;
|
||||
notify_title ?: string;
|
||||
notify_content : string;
|
||||
notify_icon ?: string;
|
||||
need_confirm ?: boolean;
|
||||
action ?: ActionType;
|
||||
}
|
||||
|
||||
// ==================== 5. 状态消息类型 ====================
|
||||
|
||||
export interface IStatusMessage extends IPayloadBase {
|
||||
|
||||
status_type : 'online' | 'offline' | 'typing' | 'recording';
|
||||
status_data ?: any;
|
||||
}
|
||||
|
||||
|
||||
// ==================== 6. 游戏消息类型 ====================
|
||||
|
||||
export type GameType = 'chess' | 'poker' | 'mahjong' | 'custom';
|
||||
export type GameAction = 'start' | 'move' | 'end' | 'ready' | 'leave';
|
||||
export type GameState = 'waiting' | 'playing' | 'finished';
|
||||
|
||||
export interface IGameMessage extends IPayloadBase {
|
||||
|
||||
game_type : GameType;
|
||||
game_action : GameAction;
|
||||
game_data ?: any;
|
||||
game_status ?: GameState;
|
||||
current_player ?: string;
|
||||
game_score ?: number;
|
||||
}
|
||||
|
||||
// ==================== 7. 房间消息类型 ====================
|
||||
|
||||
export type RoomType = 'game' | 'chat';
|
||||
export type RoomStatus = 'waiting' | 'playing' | 'finished' | 'closed';
|
||||
|
||||
export interface IRoomMember {
|
||||
member_id : string;
|
||||
nickname ?: string;
|
||||
avatar ?: string;
|
||||
role ?: 'owner' | 'admin' | 'member';
|
||||
join_time ?: number;
|
||||
online ?: boolean;
|
||||
}
|
||||
|
||||
export interface IRoomMessage extends IPayloadBase {
|
||||
|
||||
room_name ?: string;
|
||||
room_type : RoomType;
|
||||
room_status ?: RoomStatus;
|
||||
room_members ?: IRoomMember[];
|
||||
room_max_members ?: number;
|
||||
room_description ?: string;
|
||||
room_created_time ?: number;
|
||||
}
|
||||
// ==================== 8. 用户消息类型 ====================
|
||||
|
||||
export type UserStatus = 'online' | 'offline' | 'away' | 'busy';
|
||||
|
||||
export interface IUserMessage extends IPayloadBase {
|
||||
|
||||
nickname ?: string;
|
||||
avatar ?: string;
|
||||
user_status ?: UserStatus;
|
||||
signature ?: string;
|
||||
user_data ?: any;
|
||||
}
|
||||
|
||||
// ==================== 9. 错误消息类型 ====================
|
||||
|
||||
export interface IErrorMessage extends IPayloadBase {
|
||||
|
||||
error_code : number;
|
||||
error_msg : string;
|
||||
error_detail ?: string;
|
||||
error_data ?: any;
|
||||
}
|
||||
|
||||
// ==================== 10. 联合类型组织 ====================
|
||||
|
||||
// 聊天消息(用户之间的消息)
|
||||
export type IChatMessage = ITextMessage | IImageMessage | IAudioMessage | IVideoMessage | IFileMessage | ILocationMessage | IEmojiMessage | IStickerMessage | ICardMessage | ISystemMessage;
|
||||
|
||||
// 通知消息
|
||||
export type INotifyMessageUnion = INotifyMessage;
|
||||
|
||||
// 系统状态消息
|
||||
export type IStatusMessageUnion = IStatusMessage;
|
||||
|
||||
// 游戏消息
|
||||
export type IGameMessageUnion = IGameMessage;
|
||||
|
||||
// 房间消息
|
||||
export type IRoomMessageUnion = IRoomMessage;
|
||||
|
||||
// 用户消息
|
||||
export type IUserMessageUnion = IUserMessage;
|
||||
|
||||
// 错误消息
|
||||
export type IErrorMessageUnion = IErrorMessage;
|
||||
|
||||
// 完整的消息载荷类型 // 聊天消息 通知消息 状态消息 游戏消息 房间消息 用户消息 错误消息
|
||||
type IMessagePayload = IHeartbeatMessage | IChatMessage | INotifyMessage | IStatusMessage | IGameMessage | IRoomMessage | IUserMessage | IErrorMessage;
|
||||
export default IMessagePayload;
|
||||
@@ -0,0 +1,424 @@
|
||||
|
||||
import {
|
||||
IMessageBase,
|
||||
IConnectOptions,
|
||||
IWebSocketCallbacks,
|
||||
IMessageQueueItem,
|
||||
IChatMessage,
|
||||
IHeartbeatMessage,
|
||||
IWebSocketPayload
|
||||
} from './socket-types.uts'
|
||||
|
||||
import userState from '@/stores/user.uts'
|
||||
|
||||
class WebSocketManager {
|
||||
private static instance : WebSocketManager | null = null
|
||||
private socketTask : SocketTask | null = null
|
||||
private url = ''
|
||||
private protocols : Array<string> = []
|
||||
private header : UTSJSONObject | null = null
|
||||
private connected = false
|
||||
private connecting = false
|
||||
private manualClose = false
|
||||
private reconnectTimer : number | null = null
|
||||
private heartbeatTimer : number | null = null
|
||||
private lastHeartbeatAt = 0
|
||||
private waitingHeartbeat = false
|
||||
private reconnectCount = 0
|
||||
private reconnectInterval = 3000
|
||||
private heartbeatInterval = 20000
|
||||
private heartbeatTimeout = 10000
|
||||
private connectTimeout = 10000
|
||||
private maxReconnectCount = 5
|
||||
private debug = false
|
||||
private callbacks : IWebSocketCallbacks = {}
|
||||
private messageQueue : Array<IMessageQueueItem> = []
|
||||
|
||||
static getInstance() : WebSocketManager {
|
||||
if (WebSocketManager.instance == null) {
|
||||
WebSocketManager.instance = new WebSocketManager()
|
||||
}
|
||||
return WebSocketManager.instance
|
||||
}
|
||||
|
||||
connect(url : string, options : IConnectOptions | null = null) : void {
|
||||
this.log('WebSocket connect')
|
||||
if (url.length == 0) {
|
||||
this.log('WebSocket url 不能为空')
|
||||
return
|
||||
}
|
||||
const urlChanged = this.url.length > 0 && this.url != url
|
||||
this.url = url
|
||||
this.applyOptions(options)
|
||||
if (urlChanged == true) {
|
||||
this.releaseSocket()
|
||||
}
|
||||
if (this.connected == true || this.connecting == true) {
|
||||
return
|
||||
}
|
||||
this.manualClose = false
|
||||
this.connecting = true
|
||||
this.clearReconnectTimer()
|
||||
this.createSocketTask()
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用配置
|
||||
*/
|
||||
private applyOptions(options : IConnectOptions | null) : void {
|
||||
if (options == null) {
|
||||
return
|
||||
}
|
||||
if (options.reconnectInterval != null) this.reconnectInterval = options.reconnectInterval
|
||||
if (options.heartbeatInterval != null) this.heartbeatInterval = options.heartbeatInterval
|
||||
if (options.maxReconnectCount != null) this.maxReconnectCount = options.maxReconnectCount
|
||||
if (options.protocols != null) this.protocols = options.protocols
|
||||
if (options.header != null) this.header = options.header
|
||||
if (options.heartbeatTimeout != null) this.heartbeatTimeout = options.heartbeatTimeout
|
||||
if (options.connectTimeout != null) this.connectTimeout = options.connectTimeout
|
||||
if (options.debug != null) this.debug = options.debug
|
||||
}
|
||||
|
||||
private createSocketTask() : void {
|
||||
this.releaseSocket()
|
||||
let finished = false
|
||||
const finishConnect = () : void => {
|
||||
if (finished == true) {
|
||||
return
|
||||
}
|
||||
finished = true
|
||||
this.connecting = false
|
||||
}
|
||||
|
||||
const connectTimeoutTimer = setTimeout(() => {
|
||||
finishConnect()
|
||||
this.handleError({ errMsg: 'WebSocket 连接超时' })
|
||||
this.scheduleReconnect('connect timeout')
|
||||
}, this.connectTimeout) as number
|
||||
|
||||
try {
|
||||
this.socketTask = uni.connectSocket({
|
||||
url: this.url,
|
||||
header: this.header,
|
||||
protocols: this.protocols
|
||||
})
|
||||
|
||||
const currentTask = this.socketTask
|
||||
if (currentTask == null) {
|
||||
clearTimeout(connectTimeoutTimer)
|
||||
finishConnect()
|
||||
return
|
||||
}
|
||||
|
||||
currentTask.onOpen((res : any) => {
|
||||
clearTimeout(connectTimeoutTimer)
|
||||
finishConnect()
|
||||
this.connected = true
|
||||
this.reconnectCount = 0
|
||||
this.waitingHeartbeat = false
|
||||
this.startHeartbeat()
|
||||
this.flushMessageQueue()
|
||||
if (this.callbacks.onOpen != null) {
|
||||
this.callbacks.onOpen(res)
|
||||
}
|
||||
uni.$emit('onWsOpen', res)
|
||||
})
|
||||
|
||||
currentTask.onMessage((res : OnSocketMessageCallbackResult) => {
|
||||
console.log(res)
|
||||
this.handleMessage(res)
|
||||
})
|
||||
|
||||
currentTask.onError((err : any) => {
|
||||
clearTimeout(connectTimeoutTimer)
|
||||
finishConnect()
|
||||
uni.$emit('onWsError', err)
|
||||
this.handleError(err)
|
||||
})
|
||||
|
||||
currentTask.onClose((res : any) => {
|
||||
clearTimeout(connectTimeoutTimer)
|
||||
finishConnect()
|
||||
uni.$emit('onWsClose', res)
|
||||
this.handleClose(res)
|
||||
})
|
||||
} catch (error) {
|
||||
clearTimeout(connectTimeoutTimer)
|
||||
finishConnect()
|
||||
this.handleError(error)
|
||||
}
|
||||
}
|
||||
|
||||
private handleMessage(res : OnSocketMessageCallbackResult) : void {
|
||||
const dataStr = res.data as string
|
||||
// #ifndef APP
|
||||
const rawData = JSON.parse(dataStr) as IMessageBase
|
||||
// #endif
|
||||
// #ifdef APP
|
||||
const rawJson = JSON.parseObject(dataStr)
|
||||
console.log("OnMessage:", rawJson)
|
||||
const rawData = rawJson?.parse<IMessageBase>()
|
||||
// #endif
|
||||
//if (rawData.type == "ping")
|
||||
console.log("OnMessage:", rawData)
|
||||
if (rawData != null && rawData.header.type == "pong") {
|
||||
this.waitingHeartbeat = false
|
||||
this.lastHeartbeatAt = Date.now()
|
||||
return
|
||||
}
|
||||
if (rawData != null && this.callbacks.onMessage != null) {
|
||||
uni.$emit('onWsMessage', rawData)
|
||||
this.callbacks.onMessage(rawData)
|
||||
}
|
||||
}
|
||||
|
||||
send(data : IWebSocketPayload, success : (() => void) | null = null, fail : ((err : any) => void) | null = null) : void {
|
||||
let payload : string | IMessageBase | UTSJSONObject | null = null
|
||||
try {
|
||||
payload = JSON.stringify(data)
|
||||
} catch (error) {
|
||||
payload = null
|
||||
}
|
||||
if (payload == null) {
|
||||
if (fail != null) {
|
||||
fail({ errMsg: 'WebSocket 消息序列化失败' })
|
||||
}
|
||||
return
|
||||
}
|
||||
if (this.socketTask == null || this.connected == false) {
|
||||
this.enqueueMessage(data, success, fail)
|
||||
return
|
||||
}
|
||||
this.socketTask.send({
|
||||
data: payload,
|
||||
success: () => {
|
||||
if (success != null) {
|
||||
success()
|
||||
}
|
||||
},
|
||||
fail: (err : any) => {
|
||||
this.enqueueMessage(data, success, fail)
|
||||
if (fail != null) {
|
||||
fail(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
close(code : number = 1000, reason : string = 'manual close') : void {
|
||||
this.manualClose = true
|
||||
this.stopHeartbeat()
|
||||
this.clearReconnectTimer()
|
||||
this.connected = false
|
||||
this.connecting = false
|
||||
if (this.socketTask != null) {
|
||||
try {
|
||||
this.socketTask.close({ code, reason })
|
||||
} catch (error) {
|
||||
this.log('关闭 WebSocket 失败', error)
|
||||
}
|
||||
}
|
||||
this.releaseSocket()
|
||||
}
|
||||
|
||||
on(callbacks : IWebSocketCallbacks) : void {
|
||||
if (callbacks.onOpen != null) this.callbacks.onOpen = callbacks.onOpen
|
||||
if (callbacks.onMessage != null) this.callbacks.onMessage = callbacks.onMessage
|
||||
if (callbacks.onError != null) this.callbacks.onError = callbacks.onError
|
||||
if (callbacks.onClose != null) this.callbacks.onClose = callbacks.onClose
|
||||
if (callbacks.onReconnect != null) this.callbacks.onReconnect = callbacks.onReconnect
|
||||
if (callbacks.onReconnectFailed != null) this.callbacks.onReconnectFailed = callbacks.onReconnectFailed
|
||||
if (callbacks.onHeartbeatTimeout != null) this.callbacks.onHeartbeatTimeout = callbacks.onHeartbeatTimeout
|
||||
}
|
||||
|
||||
isConnected() : boolean {
|
||||
return this.connected
|
||||
}
|
||||
|
||||
getReconnectCount() : number {
|
||||
return this.reconnectCount
|
||||
}
|
||||
|
||||
getStats() : UTSJSONObject {
|
||||
return {
|
||||
url: this.url,
|
||||
connected: this.connected,
|
||||
connecting: this.connecting,
|
||||
reconnectCount: this.reconnectCount,
|
||||
queueSize: this.messageQueue.length,
|
||||
lastHeartbeatAt: this.lastHeartbeatAt
|
||||
} as UTSJSONObject
|
||||
}
|
||||
|
||||
removeAllListeners() : void {
|
||||
this.callbacks = {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始心跳
|
||||
*/
|
||||
private startHeartbeat() : void {
|
||||
this.stopHeartbeat()
|
||||
if (this.heartbeatInterval <= 0) return
|
||||
this.heartbeatTimer = setInterval(() => {
|
||||
if (this.connected == false) {
|
||||
this.stopHeartbeat()
|
||||
return
|
||||
}
|
||||
if (this.waitingHeartbeat == true) {
|
||||
const expired = Date.now() - this.lastHeartbeatAt > this.heartbeatTimeout
|
||||
if (expired == true) {
|
||||
if (this.callbacks.onHeartbeatTimeout != null) {
|
||||
this.callbacks.onHeartbeatTimeout({ errMsg: 'WebSocket 心跳超时' })
|
||||
}
|
||||
this.scheduleReconnect('heartbeat timeout')
|
||||
return
|
||||
}
|
||||
}
|
||||
this.waitingHeartbeat = true
|
||||
this.lastHeartbeatAt = Date.now()
|
||||
const heartbeatUserId = userState.uid
|
||||
const heartData : IMessageBase = {
|
||||
header: {
|
||||
id: "heartbeat_1700000000000",
|
||||
type: "ping",
|
||||
seqno: 1,
|
||||
timestamp: Date.now(),
|
||||
source: "client",
|
||||
target: "system",
|
||||
version: "1.0.0",
|
||||
platform: 'android',
|
||||
device_id: "device_id",
|
||||
network_type: 'wifi',
|
||||
},
|
||||
payload: {
|
||||
uid: heartbeatUserId,
|
||||
status: 'sent',
|
||||
type: 'text',
|
||||
content: 'ping'
|
||||
}
|
||||
}
|
||||
this.send(heartData)
|
||||
}, this.heartbeatInterval) as number
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止心跳
|
||||
*/
|
||||
private stopHeartbeat() : void {
|
||||
const timer = this.heartbeatTimer
|
||||
if (timer != null) {
|
||||
clearInterval(timer)
|
||||
this.heartbeatTimer = null
|
||||
}
|
||||
this.waitingHeartbeat = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 将消息入队
|
||||
* @param data IWebSocketPayload
|
||||
* @param success (() => void) | null
|
||||
* @param fail fail : ((err : any) => void) | null
|
||||
* @return { void }
|
||||
*/
|
||||
private enqueueMessage(data : IWebSocketPayload, success : (() => void) | null, fail : ((err : any) => void) | null) : void {
|
||||
this.messageQueue.push({ data, success, fail })
|
||||
if (this.messageQueue.length > 100) {
|
||||
this.messageQueue.splice(0, this.messageQueue.length - 100)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新消息队列
|
||||
* @return {void}
|
||||
*/
|
||||
private flushMessageQueue() : void {
|
||||
if (this.messageQueue.length == 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const queue = this.messageQueue.slice()
|
||||
this.messageQueue = []
|
||||
for (let i = 0; i < queue.length; i++) {
|
||||
const item = queue[i]
|
||||
this.send(item.data, item.success, item.fail)
|
||||
}
|
||||
}
|
||||
|
||||
private handleError(err : any) : void {
|
||||
if (this.callbacks.onError != null) {
|
||||
this.callbacks.onError(err)
|
||||
}
|
||||
this.scheduleReconnect('socket error')
|
||||
}
|
||||
|
||||
private handleClose(res : any) : void {
|
||||
this.connected = false
|
||||
this.connecting = false
|
||||
this.stopHeartbeat()
|
||||
this.releaseSocket()
|
||||
if (this.callbacks.onClose != null) {
|
||||
this.callbacks.onClose(res)
|
||||
}
|
||||
if (this.manualClose == false) {
|
||||
this.scheduleReconnect('socket close')
|
||||
}
|
||||
}
|
||||
|
||||
private scheduleReconnect(reason : string) : void {
|
||||
if (this.manualClose == true || this.url.length == 0) {
|
||||
return
|
||||
}
|
||||
if (this.reconnectTimer != null) {
|
||||
return
|
||||
}
|
||||
if (this.reconnectCount >= this.maxReconnectCount) {
|
||||
if (this.callbacks.onReconnectFailed != null) {
|
||||
this.callbacks.onReconnectFailed({ errMsg: reason, reconnectCount: this.reconnectCount })
|
||||
}
|
||||
return
|
||||
}
|
||||
this.connected = false
|
||||
this.connecting = false
|
||||
this.stopHeartbeat()
|
||||
this.releaseSocket()
|
||||
this.reconnectCount = this.reconnectCount + 1
|
||||
if (this.callbacks.onReconnect != null) {
|
||||
this.callbacks.onReconnect({ reason, reconnectCount: this.reconnectCount })
|
||||
}
|
||||
const delay = this.reconnectInterval * this.reconnectCount
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
this.reconnectTimer = null
|
||||
this.connect(this.url)
|
||||
}, delay) as number
|
||||
}
|
||||
|
||||
private clearReconnectTimer() : void {
|
||||
const timer = this.reconnectTimer
|
||||
if (timer != null) {
|
||||
clearTimeout(timer)
|
||||
this.reconnectTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
private releaseSocket() : void {
|
||||
this.socketTask = null
|
||||
}
|
||||
|
||||
private log(message : string, detail : any | null = null) : void {
|
||||
if (this.debug == false) {
|
||||
return
|
||||
}
|
||||
|
||||
if (detail != null) {
|
||||
console.log('[WebSocketManager]', message, detail)
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[WebSocketManager]', message)
|
||||
}
|
||||
}
|
||||
|
||||
export type { IConnectOptions, IWebSocketCallbacks }
|
||||
export const wsManager = WebSocketManager.getInstance()
|
||||
export default WebSocketManager
|
||||
@@ -0,0 +1,246 @@
|
||||
.page {
|
||||
flex: 1;
|
||||
/* #ifdef WEB */
|
||||
min-height: 100vh;
|
||||
padding-bottom: 2rpx;
|
||||
/* #endif */
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(to top right, #ffcbef 0%, #dd98ff 66%);
|
||||
}
|
||||
.-status-bar{
|
||||
top: var(--uni-safe-area-inset-top);
|
||||
height: var(--status-bar-height);
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.page-nav{
|
||||
height: 96rpx;
|
||||
position: fixed;
|
||||
/* #ifdef APP */
|
||||
top: var(--status-bar-height);
|
||||
/* #endif */
|
||||
/* #ifndef APP */
|
||||
top: 0;
|
||||
/* #endif */
|
||||
width: 100%; /* 确保宽度占满整个视口 */
|
||||
z-index: 100; /* 确保它在其他内容之上 */
|
||||
border-radius: 10rpx;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 5rpx 20rpx rgba(0, 0, 0, 0.1);
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 5rpx;
|
||||
}
|
||||
|
||||
.page-nav-left{
|
||||
width: auto;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.page-nav-center{
|
||||
width: auto;
|
||||
flex:1;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.page-nav-right{
|
||||
width: auto;
|
||||
padding: 3rpx 6rpx;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.page-body{
|
||||
flex: 1;
|
||||
top:96rpx;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
|
||||
.avatar-square {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 20rpx;
|
||||
margin-right: 20rpx;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
.avatar-circle {
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
border-radius: 64rpx;
|
||||
margin-right: 30rpx;
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
/* ------------------ 聊天列表样式 ------------------ */
|
||||
.chat-item{
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
margin: 5rpx;
|
||||
padding: 5rpx 5rpx;
|
||||
border-radius: 15rpx;
|
||||
margin-bottom: 20rpx;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.chat-item-left{
|
||||
display: flex;
|
||||
padding: 5rpx;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chat-item-center{
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
flex-grow: 1;
|
||||
padding: 3rpx;
|
||||
padding: 5rpx;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.chat-item-right{
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
align-items: center;
|
||||
padding: 5rpx;
|
||||
width: 96rpx;
|
||||
}
|
||||
/* ------------------ 卡片面板样式 ------------------ */
|
||||
.card-panel{
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
margin: 3rpx;
|
||||
background-color: #fff;
|
||||
border-radius: 20rpx;
|
||||
padding: 5rpx;
|
||||
margin-bottom: 20rpx;
|
||||
box-shadow: 0 5rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.card-panel-header{
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
padding: 10rpx;
|
||||
border-bottom: 1rpx solid #d4d4d45c;
|
||||
}
|
||||
|
||||
.card-panel-body{
|
||||
flex:1;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
padding: 10rpx;
|
||||
}
|
||||
|
||||
.card-panel-footer{
|
||||
border-top: 1rpx solid #d4d4d45c;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
padding: 10rpx;
|
||||
}
|
||||
|
||||
.grid-container{
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
padding: 5rpx;
|
||||
}
|
||||
|
||||
.grid-container-item{
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.tabsbox {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
margin: 0 10rpx;
|
||||
margin-bottom: 10rpx;
|
||||
border-radius: 50rpx;
|
||||
padding: 10rpx;
|
||||
}
|
||||
|
||||
.tabsbox-item{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
margin: 0 3rpx;
|
||||
padding:5rpx 3rpx;
|
||||
border-radius: 10rpx;
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.tabsbox-txt{
|
||||
text-align: center;
|
||||
font-size: 26rpx;
|
||||
color: #464646;
|
||||
margin: 0 3rpx;
|
||||
padding: 3rpx;
|
||||
border-radius: 10rpx;
|
||||
background-color: rgba(255, 255, 255, 0.005);
|
||||
}
|
||||
.tabsbox-badge{
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
min-width: 32rpx;
|
||||
height: 32rpx;
|
||||
line-height: 32rpx;
|
||||
text-align: center;
|
||||
background-color: #FF6B6B;
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
border-radius: 16rpx;
|
||||
padding: 0 3rpx;
|
||||
transform: translate(30%, -30%);
|
||||
}
|
||||
.tabsbox-active{
|
||||
/*background-color: rgba(255, 107, 107, 0.8);*/
|
||||
border-radius: 5rpx;
|
||||
border-bottom: 5rpx solid rgba(255, 107, 107, 0.8);
|
||||
}
|
||||
.tabsbox-active-txt{
|
||||
color: #FF6B6B;
|
||||
font-weight: bold;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.icon-btn{
|
||||
position: relative;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
.icon-badge{
|
||||
position: absolute;
|
||||
top: -5rpx;
|
||||
right: -5rpx;
|
||||
min-width: 32rpx;
|
||||
height: 32rpx;
|
||||
line-height: 32rpx;
|
||||
text-align: center;
|
||||
background-color: #FF3B30;
|
||||
color: #fff;
|
||||
font-size: 20rpx;
|
||||
border-radius: 16rpx;
|
||||
padding: 0 8rpx;
|
||||
}
|
||||
|
||||
/********************** 首页样式**********************/
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 587 B |
|
After Width: | Height: | Size: 225 B |
|
After Width: | Height: | Size: 586 B |
|
After Width: | Height: | Size: 275 KiB |
|
After Width: | Height: | Size: 412 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 176 KiB |
@@ -0,0 +1,460 @@
|
||||
import {
|
||||
IAppState, type ISystemConfig, type ILaunchData,
|
||||
type IBaseData, type IRecommendData,
|
||||
IUnread
|
||||
} from '@/types/app.uts'
|
||||
|
||||
import userState, { isLoggedIn } from './user.uts'
|
||||
import Api from '@/common/api-service.uts'
|
||||
import messageService from '@/services/websocket/message-service.uts'
|
||||
import httpApi from '@/utlis/http-api.uts'
|
||||
|
||||
const initialAppState : IAppState = {
|
||||
baseURL: '',
|
||||
timeout: 15000,
|
||||
enableCache: true,
|
||||
cacheExpire: 5 * 60 * 1000,
|
||||
enableLog: true,
|
||||
systemConf: null,
|
||||
launchData: null,
|
||||
baseData: null,
|
||||
recommendData: null,
|
||||
unread: null,
|
||||
initialized: false,
|
||||
// 时间信息
|
||||
startTime: 0,
|
||||
endTime: 0,
|
||||
lastInitTime: 0,
|
||||
// 网络状态
|
||||
networkType: 'none',
|
||||
isConnected: false
|
||||
}
|
||||
|
||||
const appState = reactive<IAppState>(initialAppState)
|
||||
|
||||
|
||||
/**
|
||||
* 初始化应用
|
||||
* 注意:系统配置/启动数据/基础数据每次启动都需加载(提供 socketUrl、功能开关、cdn 等),
|
||||
* 不再依赖 isInitialUse 闸门,避免配置永远不加载。
|
||||
*/
|
||||
export const initApp = async function () : Promise<boolean> {
|
||||
try {
|
||||
appState.startTime = Date.now()
|
||||
startNetworkListen()
|
||||
|
||||
// 1. 系统配置(socketUrl / 功能开关 / cdn 等)
|
||||
await loadSystemConfig()
|
||||
// 2. 启动数据(广告/轮播/快捷入口/新手引导)
|
||||
await loadLaunchData()
|
||||
// 3. 基础数据(城市/标签/礼物/等级等)
|
||||
await loadBaseData()
|
||||
|
||||
// 已登录:连接实时消息通道并加载用户态数据
|
||||
if (isLoggedIn.value) {
|
||||
connectWebSocket()
|
||||
await loadRecommendData()
|
||||
await syncUnreadCounts()
|
||||
}
|
||||
|
||||
appState.initialized = true
|
||||
appState.endTime = Date.now()
|
||||
appState.lastInitTime = Date.now()
|
||||
return true
|
||||
} catch (error : any) {
|
||||
console.error('应用初始化失败:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 已登录时按需连接 WebSocket 实时消息通道
|
||||
*/
|
||||
export const connectWebSocket = function () : void {
|
||||
messageService.connectIfNeeded(appState.systemConf?.socketUrl ?? null)
|
||||
}
|
||||
|
||||
|
||||
export const startNetworkListen = async function () {
|
||||
uni.onNetworkStatusChange((result) => {
|
||||
console.log('网络状态变化:', result)
|
||||
appState.networkType = result.networkType //网络类型
|
||||
appState.isConnected = result.isConnected //是否已连接
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载系统配置
|
||||
*/
|
||||
export const loadSystemConfig = async function () : Promise<void> {
|
||||
try {
|
||||
const data = await Api.app.appConf()
|
||||
const result = data.parse<ISystemConfig>()
|
||||
if (result != null) {
|
||||
appState.systemConf = result
|
||||
setCacheData('system_config', data)
|
||||
} else {
|
||||
appState.systemConf = getAppDefaultConf()
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('加载系统配置失败,使用缓存或默认配置', error)
|
||||
const cachedConfig = getCachedSystemConfig('system_config')
|
||||
appState.systemConf = cachedConfig != null ? cachedConfig : getAppDefaultConf()
|
||||
}
|
||||
// 将后端下发的 apiUrl 同步为 HTTP 客户端基础域名(接口 URL 已含 /wxchat/api 前缀)
|
||||
applyApiBaseURL(appState.systemConf?.apiUrl)
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 HTTP 客户端基础域名(去除末尾斜杠,避免拼接出 //wxchat)
|
||||
*/
|
||||
export const applyApiBaseURL = function (apiUrl : string | null | undefined) : void {
|
||||
if (apiUrl == null || apiUrl.length == 0) {
|
||||
return
|
||||
}
|
||||
const base = apiUrl.replace(/\/+$/, '')
|
||||
if (base.length > 0) {
|
||||
httpApi.setBaseURL(base)
|
||||
console.log('[app] HTTP baseURL 已设置为:', base)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载启动数据
|
||||
*/
|
||||
const loadLaunchData = async function () : Promise<void> {
|
||||
try {
|
||||
const data = await Api.app.launchData()
|
||||
const result = data.parse<ILaunchData>()
|
||||
if (result != null) {
|
||||
appState.launchData = result
|
||||
setCacheData('launch_data', data)
|
||||
} else {
|
||||
appState.launchData = getDefaultLaunchData()
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('加载启动数据失败,使用默认启动数据 :', error)
|
||||
const cachedData = getCachedLaunchData('launch_data')
|
||||
appState.launchData = cachedData != null ? cachedData : getDefaultLaunchData()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载基础数据
|
||||
*/
|
||||
const loadBaseData = async function () : Promise<void> {
|
||||
try {
|
||||
const data = await Api.app.baseData()
|
||||
const result = data.parse<IBaseData>()
|
||||
if (result != null) {
|
||||
appState.baseData = result
|
||||
setCacheData('base_data', data)
|
||||
console.log('基础数据加载成功')
|
||||
} else {
|
||||
appState.baseData = getDefaultBaseData()
|
||||
console.log('基础数据为空,使用默认基础数据')
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('加载基础数据失败,使用缓存或默认基础数据', error)
|
||||
const cachedData = getCachedBaseData('base_data')
|
||||
appState.baseData = cachedData != null ? cachedData : getDefaultBaseData()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载推荐数据
|
||||
*/
|
||||
const loadRecommendData = async function () : Promise<void> {
|
||||
if (isLoggedIn.value == false) {
|
||||
appState.recommendData = null
|
||||
console.log('当前未登录,跳过推荐数据加载')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const data = await Api.recommend.index()
|
||||
const result = data.parse<IRecommendData>()
|
||||
if (result != null) {
|
||||
appState.recommendData = result
|
||||
setCacheData('recommend_data', data)
|
||||
console.log('推荐数据加载成功')
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('加载推荐数据失败,尝试使用缓存数据', error)
|
||||
const cachedData = getCachedRecommendData('recommend_data')
|
||||
if (cachedData != null) {
|
||||
appState.recommendData = cachedData
|
||||
console.log('使用缓存的推荐数据')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步未读消息
|
||||
*/
|
||||
const syncUnreadCounts = async function () : Promise<void> {
|
||||
try {
|
||||
const data = await Api.message.unread()
|
||||
const result = data.parse<IUnread>()
|
||||
if (result != null) {
|
||||
appState.unread = result
|
||||
console.log('未读消息同步成功')
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('同步未读消息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 获取缓存数据
|
||||
*/
|
||||
export const getCachedData = function (key : string) : UTSJSONObject | null {
|
||||
try {
|
||||
const cached = uni.getStorageSync(key) as string | null
|
||||
const cachedTime = uni.getStorageSync(`${key}_time`) as string | null
|
||||
if (cached == null || cachedTime == null) {
|
||||
return null
|
||||
}
|
||||
const cacheAge = Date.now() - parseInt(cachedTime)
|
||||
if (cacheAge < appState.cacheExpire) {
|
||||
const parsed = JSON.parse(cached) as UTSJSONObject
|
||||
return parsed
|
||||
}
|
||||
return null
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export const getCachedSystemConfig = function (key : string) : ISystemConfig | null {
|
||||
const cached = getCachedData(key)
|
||||
if (cached == null) {
|
||||
return null
|
||||
}
|
||||
return cached.parse<ISystemConfig>()
|
||||
}
|
||||
|
||||
export const getCachedLaunchData = function (key : string) : ILaunchData | null {
|
||||
const cached = getCachedData(key)
|
||||
if (cached == null) {
|
||||
return null
|
||||
}
|
||||
return cached.parse<ILaunchData>()
|
||||
}
|
||||
|
||||
export const getCachedBaseData = function (key : string) : IBaseData | null {
|
||||
const cached = getCachedData(key)
|
||||
if (cached == null) {
|
||||
return null
|
||||
}
|
||||
return cached.parse<IBaseData>()
|
||||
}
|
||||
|
||||
export const getCachedRecommendData = function (key : string) : IRecommendData | null {
|
||||
const cached = getCachedData(key)
|
||||
if (cached == null) {
|
||||
return null
|
||||
}
|
||||
return cached.parse<IRecommendData>()
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存数据
|
||||
*/
|
||||
export const setCacheData = function (key : string, data : any) : void {
|
||||
try {
|
||||
uni.setStorageSync(key, JSON.stringify(data))
|
||||
uni.setStorageSync(`${key}_time`, Date.now().toString())
|
||||
} catch (error) {
|
||||
console.error('缓存数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认加载数据
|
||||
*/
|
||||
export const getDefaultLaunchData = () : ILaunchData{
|
||||
const launchData : ILaunchData = {
|
||||
splashAds: [],
|
||||
banners: [
|
||||
{
|
||||
id: 1,
|
||||
image: 'https://example.com/banner1.jpg',
|
||||
title: '欢迎使用',
|
||||
link: '',
|
||||
type: 'page',
|
||||
order: 1
|
||||
}
|
||||
],
|
||||
// announcements: [],
|
||||
activities: [],
|
||||
quickActions: [
|
||||
{
|
||||
id: 1,
|
||||
name: '匹配',
|
||||
icon: 'heart',
|
||||
color: '#FF6B6B',
|
||||
route: '/pages/match/match',
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '聊天',
|
||||
icon: 'message',
|
||||
color: '#007AFF',
|
||||
route: '/pages/chat/chat',
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '动态',
|
||||
icon: 'image',
|
||||
color: '#34C759',
|
||||
route: '/pages/moment/moment',
|
||||
visible: true
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '房间',
|
||||
icon: 'mic',
|
||||
color: '#5856D6',
|
||||
route: '/pages/room/room',
|
||||
visible: true
|
||||
}
|
||||
],
|
||||
newbieGuide: {
|
||||
enabled: true,
|
||||
steps: [
|
||||
{
|
||||
id: 1,
|
||||
title: '欢迎来到心遇',
|
||||
content: '遇见美好,遇见你',
|
||||
image: 'https://example.com/guide1.png'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '智能匹配',
|
||||
content: '根据你的兴趣和喜好,智能推荐合适的伙伴',
|
||||
image: 'https://example.com/guide2.png'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '实时聊天',
|
||||
content: '文字、语音、视频,多种方式畅聊无阻',
|
||||
image: 'https://example.com/guide3.png'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
return launchData
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认系统配置
|
||||
*/
|
||||
export const getAppDefaultConf = () : ISystemConfig {
|
||||
const defaultConf : ISystemConfig = {
|
||||
|
||||
dailyMatchLimit: 50,
|
||||
dailyMessageLimit: 100,
|
||||
maxFriends: 500,
|
||||
maxMomentImages: 9,
|
||||
maxChatImageSize: 10 * 1024 * 1024, // 10MB
|
||||
maxVideoDuration: 300, // 5分钟
|
||||
maxVoiceDuration: 60 // 1分钟
|
||||
|
||||
enableAd: false,
|
||||
splashAdId: '',
|
||||
bannerAdId: '',
|
||||
interstitialAdId: '',
|
||||
rewardAdId: '',
|
||||
adFrequency: 3
|
||||
|
||||
uploadUrl: 'https://upload.example.com',
|
||||
cdnUrl: 'https://cdn.example.com',
|
||||
socketUrl: 'wss://socket.example.com',
|
||||
apiVersion: 'v1'
|
||||
|
||||
wechatAppId: '',
|
||||
qqAppId: '',
|
||||
appleServiceId: '',
|
||||
pushService: '',
|
||||
mapService: 'amap'
|
||||
|
||||
minIosVersion: '11.0',
|
||||
minAndroidVersion: '5.0',
|
||||
supportedPlatforms: ['ios', 'android', 'web']
|
||||
|
||||
}
|
||||
return defaultConf
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认基础数据
|
||||
*/
|
||||
export const getDefaultBaseData = () : IBaseData {
|
||||
const baseData : IBaseData = {
|
||||
cities: [
|
||||
{ id: 1, name: '北京', code: '110000', parentId: 0, level: 1, pinyin: 'beijing', hot: true },
|
||||
{ id: 2, name: '上海', code: '310000', parentId: 0, level: 1, pinyin: 'shanghai', hot: true },
|
||||
{ id: 3, name: '广州', code: '440100', parentId: 0, level: 1, pinyin: 'guangzhou', hot: true },
|
||||
{ id: 4, name: '深圳', code: '440300', parentId: 0, level: 1, pinyin: 'shenzhen', hot: true },
|
||||
{ id: 5, name: '成都', code: '510100', parentId: 0, level: 1, pinyin: 'chengdu', hot: true }
|
||||
],
|
||||
tags: [
|
||||
{ id: 1, name: '旅行', type: 'hobby', hot: true, count: 1000 },
|
||||
{ id: 2, name: '美食', type: 'hobby', hot: true, count: 1500 },
|
||||
{ id: 3, name: '音乐', type: 'hobby', hot: false, count: 800 },
|
||||
{ id: 4, name: '运动', type: 'hobby', hot: true, count: 1200 },
|
||||
{ id: 5, name: '游戏', type: 'hobby', hot: true, count: 2000 }
|
||||
],
|
||||
gifts: [
|
||||
{ id: 1, name: '玫瑰花', description: '代表爱意', image: '🌹', price: 10, coinType: 'coin', category: 'love', isHot: true, isNew: false, isVipOnly: false, isLimited: false, sortOrder: 1 },
|
||||
{ id: 2, name: '棒棒糖', description: '甜甜蜜蜜', image: '🍭', price: 20, coinType: 'coin', category: 'sweet', isHot: true, isNew: false, isVipOnly: false, isLimited: false, sortOrder: 2 },
|
||||
{ id: 3, name: '爱心', description: '表达心意', image: '❤️', price: 50, coinType: 'coin', category: 'love', isHot: true, isNew: true, isVipOnly: false, isLimited: false, sortOrder: 3 },
|
||||
{ id: 4, name: '钻戒', description: '永恒的爱', image: '💍', price: 999, coinType: 'diamond', category: 'luxury', isHot: false, isNew: false, isVipOnly: true, isLimited: false, sortOrder: 4 }
|
||||
],
|
||||
levels: [
|
||||
{ level: 1, minExp: 0, maxExp: 100, name: '新手', icon: '🌟', color: '#999999', privileges: ['基础功能'] },
|
||||
{ level: 2, minExp: 100, maxExp: 300, name: '入门', icon: '🌟', color: '#666666', privileges: ['每日匹配+5'] },
|
||||
{ level: 3, minExp: 300, maxExp: 600, name: '活跃', icon: '🌟', color: '#FF9500', privileges: ['去广告', '高级筛选'] },
|
||||
{ level: 4, minExp: 600, maxExp: 1000, name: '资深', icon: '🌟', color: '#FF3B30', privileges: ['VIP标识', '优先展示'] },
|
||||
{ level: 5, minExp: 1000, maxExp: 2000, name: '大神', icon: '🌟', color: '#5856D6', privileges: ['专属客服', '特权标识'] }
|
||||
],
|
||||
vipPackages: [
|
||||
{ id: 1, name: '月卡', duration: 30, durationUnit: 'day', price: 30, benefits: ['去广告', '每日匹配+20', '聊天背景+5'], isHot: true, isRecommend: true },
|
||||
{ id: 2, name: '季卡', duration: 90, durationUnit: 'day', price: 80, originalPrice: 90, benefits: ['月卡所有特权', '专属标识', '优先匹配'], isHot: false, isRecommend: true },
|
||||
{ id: 3, name: '年卡', duration: 365, durationUnit: 'day', price: 288, originalPrice: 360, benefits: ['季卡所有特权', '专属客服', '无限匹配'], isHot: true, isRecommend: false }
|
||||
],
|
||||
rechargePackages: [
|
||||
{ id: 1, name: '小试牛刀', coinAmount: 60, diamondAmount: 6, price: 6, currency: 'CNY', discount: 0, isHot: false, isRecommend: false },
|
||||
{ id: 2, name: '日常所需', coinAmount: 300, diamondAmount: 30, price: 30, currency: 'CNY', discount: 0, isHot: true, isRecommend: true },
|
||||
{ id: 3, name: '土豪专享', coinAmount: 980, diamondAmount: 98, price: 98, currency: 'CNY', discount: 10, isHot: true, isRecommend: false },
|
||||
{ id: 4, name: '至尊VIP', coinAmount: 2980, diamondAmount: 298, price: 298, currency: 'CNY', discount: 20, isHot: false, isRecommend: true }
|
||||
],
|
||||
verifications: [
|
||||
{ id: 1, name: '实名认证', description: '提高信任度', icon: '✅', price: 0, benefits: ['认证标识', '提高匹配率'] },
|
||||
{ id: 2, name: '学历认证', description: '展示教育背景', icon: '🎓', price: 9.9, benefits: ['学历标识', '优先展示'] },
|
||||
{ id: 3, name: '职业认证', description: '验证职业信息', icon: '💼', price: 9.9, benefits: ['职业标识', '增加可信度'] }
|
||||
],
|
||||
chatBackgrounds: [
|
||||
{ id: 1, name: '默认背景', image: 'https://example.com/bg1.jpg', price: 0, isFree: true },
|
||||
{ id: 2, name: '星空', image: 'https://example.com/bg2.jpg', price: 10, isFree: false },
|
||||
{ id: 3, name: '海洋', image: 'https://example.com/bg3.jpg', price: 10, isFree: false },
|
||||
{ id: 4, name: '森林', image: 'https://example.com/bg4.jpg', price: 20, isFree: false }
|
||||
],
|
||||
emojis: [
|
||||
{ id: 1, name: '微笑', emoji: '😊', category: '表情' },
|
||||
{ id: 2, name: '爱心', emoji: '❤️', category: '表情' },
|
||||
{ id: 3, name: '笑哭', emoji: '😂', category: '表情' },
|
||||
{ id: 4, name: '点赞', emoji: '👍', category: '手势' },
|
||||
{ id: 5, name: '加油', emoji: '💪', category: '手势' }
|
||||
]
|
||||
}
|
||||
return baseData
|
||||
}
|
||||
|
||||
|
||||
export { appState }
|
||||
export default appState
|
||||
@@ -0,0 +1,114 @@
|
||||
//定义一个大写的State类型
|
||||
import { IUser, IUserInfo } from "@/types/user.uts"
|
||||
export type State = {
|
||||
uid : number
|
||||
tokens : ITokens | null
|
||||
info : IUserInfo | null
|
||||
profile : UTSJSONObject | null
|
||||
// 如有需要,可增加更多属性
|
||||
}
|
||||
|
||||
export type ITokens = {
|
||||
accessToken : string | null
|
||||
accessExpired : number | null
|
||||
refreshToken : string | null
|
||||
refreshExpired : number | null
|
||||
}
|
||||
|
||||
// 实例化为state
|
||||
const userState = reactive({
|
||||
uid: 0,
|
||||
tokens: null,
|
||||
info: null,
|
||||
profile: null
|
||||
} as State)
|
||||
|
||||
export const isLoggedIn = computed(() => {
|
||||
const tokens = getUserToken()
|
||||
if (tokens == null) return false
|
||||
const { accessToken, accessExpired } = tokens
|
||||
return accessToken != null && accessExpired != null && accessExpired > Date.now()
|
||||
})
|
||||
|
||||
export const setUserToken = (accessToken : string, accessExpired : number | null, refreshToken : string | null, refreshExpired : number | null) : void => {
|
||||
userState.tokens = {
|
||||
accessToken: accessToken,
|
||||
accessExpired: accessExpired ?? 0,
|
||||
refreshToken: refreshToken ?? userState.tokens?.refreshToken ?? null,
|
||||
refreshExpired: refreshExpired ?? userState.tokens?.refreshExpired ?? 0
|
||||
} as ITokens
|
||||
|
||||
uni.setStorageSync('user_tokens', JSON.stringify(userState.tokens))
|
||||
}
|
||||
export const getUserToken = function () : ITokens | null {
|
||||
try {
|
||||
if (userState.tokens == null) {
|
||||
const storageValue = uni.getStorageSync('user_tokens')
|
||||
// 增加空字符串判断
|
||||
if (storageValue != null && storageValue.toString().length > 0) {
|
||||
// #ifndef APP
|
||||
const localTokens : string = storageValue.toString()
|
||||
const objcet = JSON.parse(localTokens) as ITokens
|
||||
// #endif
|
||||
// #ifdef APP
|
||||
const localToken = JSON.parseObject(storageValue.toString())
|
||||
const objcet = localToken?.parse<ITokens>()
|
||||
// #endif
|
||||
userState.tokens = objcet
|
||||
}
|
||||
}
|
||||
if (userState.tokens != null && userState.tokens.accessToken != null) {
|
||||
return userState.tokens
|
||||
}
|
||||
} catch (e) {
|
||||
// 建议清除无效的存储,避免下次继续报错
|
||||
uni.removeStorageSync('user_tokens')
|
||||
return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
export const clearToken = () => {
|
||||
userState.tokens = null;
|
||||
uni.removeStorageSync('user_tokens')
|
||||
}
|
||||
|
||||
export const getUserInfo = function () : IUserInfo | null {
|
||||
if (userState.info == null) {
|
||||
const storageValue = uni.getStorageSync('user_info')
|
||||
const localUserInfo : string = storageValue != null ? storageValue.toString() : ""
|
||||
if (localUserInfo.length > 0) {
|
||||
const objcet = JSON.parseObject(localUserInfo)
|
||||
userState.info = objcet?.parse<IUserInfo>()
|
||||
userState.uid = userState.info?.uid ?? 0
|
||||
}
|
||||
}
|
||||
if (userState.info != null) {
|
||||
return userState.info
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export const setUserInfo = (info : IUserInfo | null) => {
|
||||
userState.info = info
|
||||
if (info != null) {
|
||||
uni.setStorageSync('user_info', JSON.stringify(info))
|
||||
} else {
|
||||
uni.removeStorageSync('user_info')
|
||||
}
|
||||
}
|
||||
|
||||
export const initStore = () => {
|
||||
try {
|
||||
const tokens = getUserToken()
|
||||
} catch (e) {
|
||||
console.error('解析用户Token失败', e)
|
||||
}
|
||||
try {
|
||||
const info = getUserInfo()
|
||||
} catch (e) {
|
||||
console.error('解析用户信息失败', e)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default userState
|
||||
@@ -0,0 +1,495 @@
|
||||
export type IAppState = {
|
||||
baseURL : string
|
||||
timeout : number
|
||||
enableCache : boolean
|
||||
cacheExpire : number
|
||||
enableLog : boolean
|
||||
systemConf : ISystemConfig | null
|
||||
launchData : ILaunchData | null
|
||||
baseData : IBaseData | null
|
||||
recommendData : IRecommendData | null
|
||||
unread : IUnread | null
|
||||
initialized : boolean
|
||||
// 时间信息
|
||||
startTime : number
|
||||
endTime : number
|
||||
lastInitTime : number
|
||||
// 网络状态
|
||||
networkType : string
|
||||
isConnected : boolean
|
||||
}
|
||||
|
||||
|
||||
export type IUnread = {
|
||||
unreadMessages : number
|
||||
unreadNotifications : number
|
||||
|
||||
}
|
||||
|
||||
|
||||
export type IFeatures = {
|
||||
chatEnabled : boolean //聊天功能启用:布尔值
|
||||
videoCallEnabled : boolean //视频通话功能启用:布尔值
|
||||
voiceRoomEnabled : boolean //语音聊天室功能启用:布尔值
|
||||
giftShopEnabled : boolean //礼物商店功能启用:布尔值
|
||||
rechargeEnabled : boolean //充值功能启用:布尔值
|
||||
vipEnabled : boolean //VIP功能启用:布尔值
|
||||
matchEnabled : boolean //匹配功能启用:布尔值
|
||||
momentEnabled : boolean //动态功能启用:布尔值
|
||||
}
|
||||
export type ILimits = {
|
||||
dailyMatchLimit : number //每日匹配次数限制:数字
|
||||
dailyMessageLimit : number //每日消息发送次数限制:数字
|
||||
maxFriends : number //好友数量上限:数字
|
||||
maxMomentImages : number //动态图片数量上限:数字
|
||||
maxChatImageSize : number //聊天图片大小上限:数字
|
||||
maxVideoDuration : number //视频时长上限:数字
|
||||
maxVoiceDuration : number //音时长上限:数字
|
||||
}
|
||||
export type IAdConfig = {
|
||||
enable : boolean //启用
|
||||
splashAdId : string //启动广告ID:字符串
|
||||
bannerAdId : string //横幅广告ID:字符串
|
||||
interstitialAdId : string //间隙广告ID:字符串
|
||||
rewardAdId : string //奖励广告ID:字符串
|
||||
adFrequency : number //广告频率:数字
|
||||
}
|
||||
export type IServerConf = {
|
||||
socket : string
|
||||
upload : string
|
||||
cdnurl : string
|
||||
}
|
||||
/**
|
||||
* 第三方
|
||||
*/
|
||||
export type IThirdParty = {
|
||||
wechatAppId : string //微信AppId:字符串
|
||||
qqAppId : string //qq AppId:字符串
|
||||
appleServiceId : string //苹果 服务ID:字符串
|
||||
pushService : string //推送服务:字符串
|
||||
mapService : 'amap' | 'qqmap' | 'baidumap' //地图服务
|
||||
}
|
||||
export type ICustomerService = {
|
||||
qq : string
|
||||
wechat : string
|
||||
phone : string
|
||||
email : string
|
||||
onlineTime : string
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容性
|
||||
*/
|
||||
export type ICompatibility = {
|
||||
minIosVersion : string //最小iOS版本:字符串
|
||||
minAndroidVersion : string //最小Android版本:字符串
|
||||
supportedPlatforms : string[] //支持平台:字符串数组
|
||||
}
|
||||
|
||||
/**
|
||||
* 系统配置接口
|
||||
*/
|
||||
export type ISystemConfig = {
|
||||
// 系统功能开关
|
||||
chatEnabled ?: boolean //聊天功能启用:布尔值
|
||||
videoCallEnabled ?: boolean //视频通话功能启用:布尔值
|
||||
voiceRoomEnabled ?: boolean //语音聊天室功能启用:布尔值
|
||||
giftShopEnabled ?: boolean //礼物商店功能启用:布尔值
|
||||
rechargeEnabled ?: boolean //充值功能启用:布尔值
|
||||
vipEnabled ?: boolean //VIP功能启用:布尔值
|
||||
matchEnabled ?: boolean //匹配功能启用:布尔值
|
||||
momentEnabled ?: boolean //动态功能启用:布尔值
|
||||
|
||||
// 业务限制配置
|
||||
dailyMatchLimit ?: number //每日匹配次数限制:数字
|
||||
dailyMessageLimit ?: number //每日消息发送次数限制:数字
|
||||
maxFriends ?: number //好友数量上限:数字
|
||||
maxMomentImages ?: number //动态图片数量上限:数字
|
||||
maxChatImageSize ?: number //聊天图片大小上限:数字
|
||||
maxVideoDuration ?: number //视频时长上限:数字
|
||||
maxVoiceDuration ?: number //音时长上限:数字
|
||||
// 广告配置
|
||||
enableAd ?: boolean //启用
|
||||
splashAdId ?: string //启动广告ID:字符串
|
||||
bannerAdId ?: string //横幅广告ID:字符串
|
||||
interstitialAdId ?: string //间隙广告ID:字符串
|
||||
rewardAdId ?: string //奖励广告ID:字符串
|
||||
adFrequency ?: number //广告频率:数字
|
||||
// 服务器配置
|
||||
socketUrl ?: string
|
||||
uploadUrl ?: string
|
||||
cdnUrl ?: string
|
||||
apiUrl ?: string
|
||||
apiVersion ?: string
|
||||
// 第三方服务配置
|
||||
wechatAppId ?: string //微信AppId:字符串
|
||||
qqAppId ?: string //qq AppId:字符串
|
||||
appleServiceId ?: string //苹果 服务ID:字符串
|
||||
pushService ?: string //推送服务:字符串
|
||||
mapService ?: 'amap' | 'qqmap' | 'baidumap' //地图服务
|
||||
// 客服配置
|
||||
//customerService ?: ICustomerService
|
||||
// 版本兼容性
|
||||
minIosVersion ?: string //最小iOS版本:字符串
|
||||
minAndroidVersion ?: string //最小Android版本:字符串
|
||||
supportedPlatforms ?: string[] //支持平台:字符串数组
|
||||
}
|
||||
|
||||
export type IsplashAd = {
|
||||
id : number
|
||||
image : string
|
||||
title : string
|
||||
link : string
|
||||
duration : number
|
||||
skipable : boolean
|
||||
showCount : number
|
||||
}
|
||||
export type IBanner = {
|
||||
id : number
|
||||
image : string
|
||||
title : string
|
||||
link : string
|
||||
type : 'url' | 'page' | 'activity'
|
||||
order : number
|
||||
}
|
||||
export type IActivitie = {
|
||||
id : number
|
||||
title : string
|
||||
description : string
|
||||
icon : string
|
||||
badge ?: string
|
||||
link : string
|
||||
startTime : number
|
||||
endTime : number
|
||||
status : 'ongoing' | 'upcoming' | 'ended'
|
||||
}
|
||||
export type IQuickAction = {
|
||||
id : number
|
||||
name : string
|
||||
icon : string
|
||||
color : string
|
||||
route : string
|
||||
badge ?: number
|
||||
visible : boolean
|
||||
}
|
||||
export type IStep = {
|
||||
id : number
|
||||
title : string
|
||||
content : string
|
||||
image : string
|
||||
action ?: string
|
||||
}
|
||||
export type INewbieGuide = {
|
||||
|
||||
enabled : boolean
|
||||
steps : IStep[]
|
||||
}
|
||||
|
||||
// 启动数据接口
|
||||
export type ILaunchData = {
|
||||
// 启动页广告
|
||||
splashAds : IsplashAd[]
|
||||
// 首页轮播图
|
||||
banners : IBanner[]
|
||||
|
||||
// 运营活动
|
||||
activities : IActivitie[]
|
||||
|
||||
// 快捷入口
|
||||
quickActions : IQuickAction[]
|
||||
// 新手引导
|
||||
newbieGuide : INewbieGuide
|
||||
}
|
||||
|
||||
export type ICity = {
|
||||
id : number
|
||||
name : string
|
||||
code : string
|
||||
parentId : number
|
||||
level : number
|
||||
pinyin : string
|
||||
hot : boolean
|
||||
latitude ?: number
|
||||
longitude ?: number
|
||||
}
|
||||
export type ITag = {
|
||||
id : number
|
||||
name : string
|
||||
type : 'interest' | 'personality' | 'hobby' | 'profession'
|
||||
icon ?: string
|
||||
color ?: string
|
||||
hot : boolean
|
||||
count : number
|
||||
}
|
||||
export type IGift = {
|
||||
id : number
|
||||
name : string
|
||||
description : string
|
||||
image : string
|
||||
animation ?: string
|
||||
price : number
|
||||
coinType : 'coin' | 'diamond'
|
||||
category : string
|
||||
isHot : boolean
|
||||
isNew : boolean
|
||||
isVipOnly : boolean
|
||||
isLimited : boolean
|
||||
stock ?: number
|
||||
sortOrder : number
|
||||
}
|
||||
export type ILevel = {
|
||||
level : number
|
||||
minExp : number
|
||||
maxExp : number
|
||||
name : string
|
||||
icon : string
|
||||
color : string
|
||||
privileges : string[]
|
||||
}
|
||||
export type IVipPackage = {
|
||||
id : number
|
||||
name : string
|
||||
duration : number
|
||||
durationUnit : 'day' | 'month' | 'year'
|
||||
price : number
|
||||
originalPrice ?: number
|
||||
benefits : string[]
|
||||
isHot : boolean
|
||||
isRecommend : boolean
|
||||
}
|
||||
export type IRechargePackage = {
|
||||
id : number
|
||||
name : string
|
||||
coinAmount : number
|
||||
diamondAmount : number
|
||||
price : number
|
||||
currency : 'CNY' | 'USD'
|
||||
discount ?: number
|
||||
isHot : boolean
|
||||
isRecommend : boolean
|
||||
}
|
||||
export type IVerification = {
|
||||
id : number
|
||||
name : string
|
||||
description : string
|
||||
icon : string
|
||||
price : number
|
||||
benefits : string[]
|
||||
}
|
||||
export type IChatBackground = {
|
||||
id : number
|
||||
name : string
|
||||
image : string
|
||||
price : number
|
||||
isFree : boolean
|
||||
}
|
||||
export type IEmoji = {
|
||||
id : number
|
||||
name : string
|
||||
emoji : string
|
||||
category : string
|
||||
}
|
||||
// 基础数据接口
|
||||
export type IBaseData = {
|
||||
// 城市地区数据
|
||||
cities : ICity[]
|
||||
// 用户兴趣标签
|
||||
tags : ITag[]
|
||||
// 礼物商城数据
|
||||
gifts : IGift[]
|
||||
// 用户等级体系
|
||||
levels : ILevel[]
|
||||
// VIP套餐
|
||||
vipPackages : IVipPackage[]
|
||||
// 充值套餐
|
||||
rechargePackages : IRechargePackage[]
|
||||
// 认证类型
|
||||
verifications : IVerification[]
|
||||
// 聊天背景
|
||||
chatBackgrounds : IChatBackground[]
|
||||
// 表情包
|
||||
emojis : IEmoji[]
|
||||
}
|
||||
|
||||
export type IState = {
|
||||
todayMatches : number
|
||||
todayMessages : number
|
||||
unreadMessages : number
|
||||
unreadNotifications : number
|
||||
unreadSystemMsg : number
|
||||
newMatches : number
|
||||
newLikes : number
|
||||
newVisitors : number
|
||||
newFollowers : number
|
||||
}
|
||||
export type IWallet = {
|
||||
coinBalance : number
|
||||
diamondBalance : number
|
||||
todayEarnings : number
|
||||
todaySpent : number
|
||||
totalRecharge : number
|
||||
totalWithdraw : number
|
||||
}
|
||||
export type IDailyTask = {
|
||||
id : number
|
||||
name : string
|
||||
description : string
|
||||
progress : number
|
||||
target : number
|
||||
reward : number
|
||||
completed : boolean
|
||||
}
|
||||
export type ITask = {
|
||||
dailyTasks : IDailyTask[]
|
||||
achievementPoints : number
|
||||
}
|
||||
export type IDeviceInfo = {
|
||||
platform : string
|
||||
version : string
|
||||
}
|
||||
export type IOnline = {
|
||||
isOnline : boolean
|
||||
lastOnlineTime : number
|
||||
deviceInfo : IDeviceInfo
|
||||
}
|
||||
|
||||
export type IPrivacy = {
|
||||
showOnline : boolean
|
||||
showDistance : boolean
|
||||
showLastActive : boolean
|
||||
allowStrangerChat : boolean
|
||||
allowRecommend : boolean
|
||||
}
|
||||
|
||||
export type INotification = {
|
||||
newMessage : boolean
|
||||
newMatch : boolean
|
||||
newLike : boolean
|
||||
sound : boolean
|
||||
vibration : boolean
|
||||
}
|
||||
|
||||
export type ISetting = {
|
||||
privacy : IPrivacy
|
||||
notification : INotification
|
||||
}
|
||||
|
||||
export type IUserSummary = {
|
||||
// 用户基本信息
|
||||
userId : number
|
||||
nickname : string
|
||||
avatar : string
|
||||
level : number
|
||||
vipLevel : number
|
||||
vipExpireTime ?: number
|
||||
// 统计数据
|
||||
stats : IState
|
||||
// 钱包信息
|
||||
wallet : IWallet
|
||||
// 任务和成就
|
||||
tasks : ITask
|
||||
// 在线状态
|
||||
online : IOnline
|
||||
// 用户设置
|
||||
settings : ISetting
|
||||
}
|
||||
|
||||
export type IRecommendedUser = {
|
||||
id : number
|
||||
avatar : string
|
||||
nickname : string
|
||||
age : number
|
||||
gender : 'male' | 'female'
|
||||
distance : number
|
||||
online : boolean
|
||||
tags : string[]
|
||||
matchScore : number
|
||||
}
|
||||
export type IHotMomentUser = {
|
||||
id : number
|
||||
nickname : string
|
||||
avatar : string
|
||||
}
|
||||
|
||||
export type IHotMoment = {
|
||||
id : number
|
||||
userId : number
|
||||
content : string
|
||||
image ?: string
|
||||
video ?: string
|
||||
likeCount : number
|
||||
commentCount : number
|
||||
shareCount : number
|
||||
user : IHotMomentUser
|
||||
}
|
||||
|
||||
export type IVoiceRoomOwer = {
|
||||
id : number
|
||||
nickname : string
|
||||
avatar : string
|
||||
}
|
||||
export type IVoiceRoom = {
|
||||
id : number
|
||||
title : string
|
||||
onlineCount : number
|
||||
maxUsers : number
|
||||
cover : string
|
||||
tags : string[]
|
||||
owner : IVoiceRoomOwer
|
||||
}
|
||||
export type IVideoRoom = {
|
||||
id : number
|
||||
title : string
|
||||
onlineCount : number
|
||||
cover : string
|
||||
category : string
|
||||
hotValue : number
|
||||
}
|
||||
export type IMiniGame = {
|
||||
id : number
|
||||
name : string
|
||||
icon : string
|
||||
description : string
|
||||
onlineCount : number
|
||||
link : string
|
||||
}
|
||||
export type IInterestCircle = {
|
||||
id : number
|
||||
name : string
|
||||
icon : string
|
||||
memberCount : number
|
||||
postCount : number
|
||||
description : string
|
||||
}
|
||||
|
||||
|
||||
// 推荐数据接口
|
||||
export type IRecommendData = {
|
||||
// 推荐用户
|
||||
recommendedUsers : IRecommendedUser[]
|
||||
|
||||
// 热门动态
|
||||
hotMoments : IHotMoment[]
|
||||
|
||||
// 语音房间
|
||||
voiceRooms : IVoiceRoom[]
|
||||
|
||||
// 视频房间
|
||||
videoRooms : IVideoRoom[]
|
||||
|
||||
// 小游戏
|
||||
miniGames : IMiniGame[]
|
||||
|
||||
// 兴趣圈子
|
||||
interestCircles : IInterestCircle[]
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export type INetworkConf = {
|
||||
AttachURL : string
|
||||
SocketURL : string
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// 定义用户类型
|
||||
export type ISender = {
|
||||
id : number
|
||||
nickname : string
|
||||
avatar : string
|
||||
online : boolean
|
||||
vip : boolean
|
||||
}
|
||||
|
||||
// 定义消息类型
|
||||
export type IMessage = {
|
||||
id : number
|
||||
sender : ISender
|
||||
content : string
|
||||
timestamp : number
|
||||
unreadCount : number
|
||||
pinned : boolean
|
||||
type : 'text' | 'image' | 'voice' | 'video'
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
|
||||
|
||||
export type ITab = {
|
||||
name : string
|
||||
badge : number
|
||||
}
|
||||
|
||||
export type IMoment = {
|
||||
id : number
|
||||
userId : number
|
||||
userName : string
|
||||
userAvatar : string
|
||||
userVip : boolean
|
||||
content : string
|
||||
images : string[]
|
||||
videoUrl : string
|
||||
type : 'text' | 'image' | 'video'
|
||||
location : string
|
||||
time : number
|
||||
likes : number
|
||||
comments : number
|
||||
shares : number
|
||||
isLiked : boolean
|
||||
isPinned : boolean
|
||||
tags : string[]
|
||||
hot : boolean
|
||||
distance : number
|
||||
// 详情扩展字段
|
||||
uid? : number
|
||||
media? : Array<{ type : 'image' | 'video', url : string, thumbnail? : string }>
|
||||
hashtags? : Array<{ id : number, name : string }>
|
||||
stats? : {
|
||||
like_count : number
|
||||
comment_count : number
|
||||
share_count : number
|
||||
collect_count : number
|
||||
view_count : number
|
||||
}
|
||||
interaction? : { liked : boolean, collected : boolean, followed : boolean }
|
||||
view? : number
|
||||
collected? : boolean
|
||||
followed? : boolean
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
|
||||
|
||||
export type IUser = {}
|
||||
|
||||
// 语音房接口
|
||||
export type IVoiceRoom = {
|
||||
id : number
|
||||
title : string
|
||||
cover : string
|
||||
onlineCount : number
|
||||
maxCount : number
|
||||
hot : number
|
||||
tags : string[]
|
||||
users : usera[]
|
||||
}
|
||||
export type usera = {
|
||||
id : number;
|
||||
avatar : string
|
||||
}
|
||||
// 游戏接口
|
||||
export type IGame = {
|
||||
id : number
|
||||
title : string
|
||||
image : string
|
||||
description : string
|
||||
playing : number
|
||||
reward : number
|
||||
tags : string[]
|
||||
}
|
||||
|
||||
export type IMenu = {
|
||||
id : number
|
||||
icon : string
|
||||
iconColor ?: string
|
||||
text : string
|
||||
bgColor : string
|
||||
url : string
|
||||
hot ?: boolean
|
||||
}
|
||||
|
||||
export type IBanner = {
|
||||
id : number
|
||||
image : string
|
||||
tag : string
|
||||
url : string
|
||||
}
|
||||
|
||||
export type IFilter = {
|
||||
id : number
|
||||
text : string
|
||||
count : number
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
|
||||
type IRequestMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS' | 'HEAD'
|
||||
/**
|
||||
* 请求配置接口 - 兼容 uni-app x 各平台
|
||||
* 使用 UTSJSONObject 替代具体对象类型,避免类型推断问题
|
||||
*/
|
||||
type IRequestOption = {
|
||||
/** 请求地址 */
|
||||
url : string
|
||||
/** 请求方法 */
|
||||
method ?: IRequestMethod
|
||||
/** 请求数据 - 使用 UTSJSONObject 兼容任意对象 */
|
||||
data ?: UTSJSONObject | null
|
||||
/** 请求头 - 使用 UTSJSONObject 避免 Any? 类型错误 */
|
||||
header ?: UTSJSONObject | null
|
||||
/** 超时时间(毫秒) */
|
||||
timeout ?: number
|
||||
/** 返回数据类型 */
|
||||
dataType ?: 'json' | 'text' | 'arraybuffer'
|
||||
/** 响应数据编码 */
|
||||
responseType ?: 'text' | 'arraybuffer'
|
||||
/** 是否显示加载提示 */
|
||||
showLoading ?: boolean
|
||||
/** 加载提示文字 */
|
||||
loadingText ?: string
|
||||
/** 是否显示错误提示 */
|
||||
showError ?: boolean
|
||||
/** 是否需要 token */
|
||||
needToken ?: boolean
|
||||
/** 重试次数 */
|
||||
retryCount ?: number
|
||||
/** 基础地址(用于覆盖全局 baseURL) */
|
||||
baseURL ?: string
|
||||
/** 内容类型 */
|
||||
contentType ?: 'json' | 'form' | 'multipart'
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求成功响应 - 泛型提升到接口级别
|
||||
*/
|
||||
type IResponse = {
|
||||
code : number
|
||||
message : string
|
||||
data ?: UTSJSONObject
|
||||
timestamp : number
|
||||
access_token ?: string
|
||||
access_expired ?: number
|
||||
refresh_token ?: string
|
||||
refresh_expired ?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求取消错误
|
||||
*/
|
||||
class ICancelError extends Error {
|
||||
public code : string = 'ERR_CANCELED'
|
||||
public isCancelError : boolean = true
|
||||
|
||||
constructor(message : string = 'Request cancelled') {
|
||||
super(message)
|
||||
this.name = 'CancelError'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 超时错误
|
||||
*/
|
||||
class ITimeoutError extends Error {
|
||||
public code : string = 'ECONNABORTED'
|
||||
public isTimeoutError : boolean = true
|
||||
|
||||
constructor(message : string = 'Request timeout') {
|
||||
super(message)
|
||||
this.name = 'TimeoutError'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络错误
|
||||
*/
|
||||
class INetworkError extends Error {
|
||||
public code : string = 'NETWORK_ERROR'
|
||||
public isNetworkError : boolean = true
|
||||
|
||||
constructor(message : string = 'Network error') {
|
||||
super(message)
|
||||
this.name = 'NetworkError'
|
||||
}
|
||||
}
|
||||
|
||||
type IHttpError = {
|
||||
errCode : number
|
||||
errMsg ?: string
|
||||
cause ?: Error
|
||||
options ?: IRequestOption | IUploadOption
|
||||
data ?: any
|
||||
}
|
||||
|
||||
type IUploadOption = {
|
||||
url : string
|
||||
filePath ?: string
|
||||
name ?: string
|
||||
timeout ?: number
|
||||
formData ?: UTSJSONObject
|
||||
header ?: UTSJSONObject
|
||||
}
|
||||
|
||||
type IUploadInfo = {
|
||||
fid : number
|
||||
url : string,
|
||||
name : string
|
||||
}
|
||||
|
||||
// 文件信息接口
|
||||
type IFileInfo = {
|
||||
index ?: number // 文件索引
|
||||
fid ?: number
|
||||
path : string // 本地文件路径
|
||||
name ?: string // 自定义文件名(可选)
|
||||
}
|
||||
|
||||
|
||||
// 上传结果接口
|
||||
type IUploadResult = {
|
||||
index : number // 文件索引
|
||||
success : boolean // 是否成功
|
||||
data ?: IResponse // 成功时的响应数据
|
||||
error ?: any // 失败时的错误信息
|
||||
}
|
||||
|
||||
|
||||
// 请求方法类型
|
||||
type IDownRespone = {
|
||||
tempFilePath : string,
|
||||
statusCode : number
|
||||
}
|
||||
|
||||
// 请求配置接口
|
||||
type IResponseToken = {
|
||||
access_token : string
|
||||
refresh_token : string
|
||||
expires_in : number
|
||||
}
|
||||
// Token 信息接口
|
||||
type ITokenInfo = {
|
||||
accessToken : string
|
||||
refreshToken : string
|
||||
expireTime : number
|
||||
tokenType ?: string
|
||||
}
|
||||
|
||||
// 网络状态类型
|
||||
interface INetworkInfo {
|
||||
isConnected : boolean
|
||||
networkType : string
|
||||
isWifi : boolean
|
||||
isCellular : boolean
|
||||
}
|
||||
|
||||
|
||||
// 创建错误对象
|
||||
/**
|
||||
*
|
||||
* ECONNABORTED ERR_CANCELED NETWORK_ERROR UNKNOWN_ERROR
|
||||
*/
|
||||
function HttpError(options : IHttpError) : Error {
|
||||
const errMsg = options.errMsg ?? `HTTP Error: ${options.errCode}`
|
||||
|
||||
// #ifdef APP
|
||||
const error = new UniError()
|
||||
error.errSubject = 'http-request'
|
||||
error.errCode = options.errCode
|
||||
error.errMsg = errMsg
|
||||
if (options.cause != null) error.cause = options.cause
|
||||
if (options.data != null) error.data = options.data
|
||||
return error
|
||||
// #endif
|
||||
|
||||
// #ifndef APP
|
||||
const error = new Error(errMsg);
|
||||
(error as any).errSubject = 'http-request';
|
||||
(error as any).errCode = options.errCode;
|
||||
(error as any).errMsg = errMsg
|
||||
if (options.cause != null) (error as any).cause = options.cause
|
||||
if (options.data != null) (error as any).data = options.data
|
||||
return error
|
||||
// #endif
|
||||
}
|
||||
export default HttpError
|
||||
export {
|
||||
IRequestMethod, IRequestOption, IResponse,
|
||||
ITokenInfo, INetworkInfo,
|
||||
IUploadOption, IUploadResult, IFileInfo, IUploadInfo,
|
||||
IDownRespone, IResponseToken,
|
||||
IHttpError, INetworkError, ITimeoutError, ICancelError
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
|
||||
|
||||
export type IGrid = {
|
||||
name : string
|
||||
desc : string
|
||||
bgColor : string
|
||||
icon : string
|
||||
path : string
|
||||
}
|
||||
|
||||
|
||||
export type IUser = {
|
||||
id: number,
|
||||
nickname: string,
|
||||
realname: boolean,
|
||||
location: string,
|
||||
age: number,
|
||||
height: number,
|
||||
profession: string,
|
||||
signature: string,
|
||||
avatar: string,
|
||||
vip: boolean,
|
||||
verified: boolean,
|
||||
live: boolean
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
|
||||
|
||||
export type IMoment ={
|
||||
id : number
|
||||
uid : number
|
||||
nickname : string
|
||||
avatar : string
|
||||
location : string
|
||||
content : string
|
||||
media : IMedia[]
|
||||
createTime : number
|
||||
like : number
|
||||
likede : boolean
|
||||
comment : number
|
||||
share:number
|
||||
view?: number
|
||||
expanded? :boolean
|
||||
}
|
||||
export type IComment ={
|
||||
id:number
|
||||
mid:number
|
||||
uid:number
|
||||
nickname:string
|
||||
avatar:string
|
||||
content:string
|
||||
createTime:number
|
||||
like:number
|
||||
liked:boolean
|
||||
replyCount:number
|
||||
replies:UTSJSONObject[]
|
||||
|
||||
}
|
||||
|
||||
export type IMedia = {
|
||||
type : 'image' | 'video'
|
||||
url : string
|
||||
thumbnail ?: boolean
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
|
||||
|
||||
|
||||
export type TProfile = {
|
||||
uid : number // 用户id
|
||||
age : string // 年龄
|
||||
height : string // 身高
|
||||
weight : string // 体重
|
||||
live : string // 居住地
|
||||
hometown : string // 家乡
|
||||
education : string // 学历
|
||||
profession : string // 工作
|
||||
annualIncome : string // 收入
|
||||
constellation : string // 星座
|
||||
}
|
||||
|
||||
// export interface ISender { }
|
||||
|
||||
|
||||
// 用户信息接口
|
||||
export type IVoiceMessages = {
|
||||
id : number
|
||||
senderId : number
|
||||
receiverId : number
|
||||
duration : number
|
||||
timestamp : number
|
||||
type : string
|
||||
playing : boolean
|
||||
|
||||
}
|
||||
|
||||
// 用户信息接口
|
||||
export type IImageMessages = {
|
||||
id : number
|
||||
senderId : number
|
||||
receiverId : number
|
||||
url : string
|
||||
timestamp : number
|
||||
type : string
|
||||
|
||||
}
|
||||
|
||||
// 用户信息接口
|
||||
export type IUser = {
|
||||
id : number
|
||||
nickname : string
|
||||
avatar : string
|
||||
age ?: number
|
||||
gender ?: 'male' | 'female'
|
||||
signature ?: string
|
||||
online : boolean
|
||||
vip : boolean
|
||||
distance ?: number
|
||||
tags ?: string[]
|
||||
likes ?: number
|
||||
views ?: number
|
||||
}
|
||||
export type IUserInfo = {
|
||||
uid : number
|
||||
account : string
|
||||
nickname : string
|
||||
create_ip : string
|
||||
update_time : string
|
||||
update_ip : string
|
||||
avatar : string
|
||||
email : string
|
||||
mobile : string
|
||||
gid : number
|
||||
status : number
|
||||
email_verified : number
|
||||
create_time : string
|
||||
status_text : string
|
||||
last_login : string
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* 这里是uni-app内置的常用样式变量
|
||||
*
|
||||
* uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
|
||||
* 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
|
||||
*
|
||||
* 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
|
||||
*/
|
||||
|
||||
/* 颜色变量 */
|
||||
|
||||
/* 行为相关颜色 */
|
||||
$uni-color-primary: #007aff;
|
||||
$uni-color-success: #4cd964;
|
||||
$uni-color-warning: #f0ad4e;
|
||||
$uni-color-error: #dd524d;
|
||||
|
||||
/* 文字基本颜色 */
|
||||
$uni-text-color:#333;//基本色
|
||||
$uni-text-color-inverse:#fff;//反色
|
||||
$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
|
||||
$uni-text-color-placeholder: #808080;
|
||||
$uni-text-color-disable:#c0c0c0;
|
||||
|
||||
/* 背景颜色 */
|
||||
$uni-bg-color:#ffffff;
|
||||
$uni-bg-color-grey:#f8f8f8;
|
||||
$uni-bg-color-hover:#f1f1f1;//点击状态颜色
|
||||
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
|
||||
|
||||
/* 边框颜色 */
|
||||
$uni-border-color:#c8c7cc;
|
||||
|
||||
/* 尺寸变量 */
|
||||
|
||||
/* 文字尺寸 */
|
||||
$uni-font-size-sm:12px;
|
||||
$uni-font-size-base:14px;
|
||||
$uni-font-size-lg:16px;
|
||||
|
||||
/* 图片尺寸 */
|
||||
$uni-img-size-sm:20px;
|
||||
$uni-img-size-base:26px;
|
||||
$uni-img-size-lg:40px;
|
||||
|
||||
/* Border Radius */
|
||||
$uni-border-radius-sm: 2px;
|
||||
$uni-border-radius-base: 3px;
|
||||
$uni-border-radius-lg: 6px;
|
||||
$uni-border-radius-circle: 50%;
|
||||
|
||||
/* 水平间距 */
|
||||
$uni-spacing-row-sm: 5px;
|
||||
$uni-spacing-row-base: 10px;
|
||||
$uni-spacing-row-lg: 15px;
|
||||
|
||||
/* 垂直间距 */
|
||||
$uni-spacing-col-sm: 4px;
|
||||
$uni-spacing-col-base: 8px;
|
||||
$uni-spacing-col-lg: 12px;
|
||||
|
||||
/* 透明度 */
|
||||
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
|
||||
|
||||
/* 文章场景相关 */
|
||||
$uni-color-title: #2C405A; // 文章标题颜色
|
||||
$uni-font-size-title:20px;
|
||||
$uni-color-subtitle: #555555; // 二级标题颜色
|
||||
$uni-font-size-subtitle:26px;
|
||||
$uni-color-paragraph: #3F536E; // 文章段落颜色
|
||||
$uni-font-size-paragraph:15px;
|
||||
@@ -0,0 +1,44 @@
|
||||
## 2.0.12(2025-08-26)
|
||||
- 优化 uni-app x 下 size 类型问题
|
||||
## 2.0.11(2025-08-18)
|
||||
- 修复 图标点击事件返回
|
||||
## 2.0.9(2024-01-12)
|
||||
fix: 修复图标大小默认值错误的问题
|
||||
## 2.0.8(2023-12-14)
|
||||
- 修复 项目未使用 ts 情况下,打包报错的bug
|
||||
## 2.0.7(2023-12-14)
|
||||
- 修复 size 属性为 string 时,不加单位导致尺寸异常的bug
|
||||
## 2.0.6(2023-12-11)
|
||||
- 优化 兼容老版本icon类型,如 top ,bottom 等
|
||||
## 2.0.5(2023-12-11)
|
||||
- 优化 兼容老版本icon类型,如 top ,bottom 等
|
||||
## 2.0.4(2023-12-06)
|
||||
- 优化 uni-app x 下示例项目图标排序
|
||||
## 2.0.3(2023-12-06)
|
||||
- 修复 nvue下引入组件报错的bug
|
||||
## 2.0.2(2023-12-05)
|
||||
-优化 size 属性支持单位
|
||||
## 2.0.1(2023-12-05)
|
||||
- 新增 uni-app x 支持定义图标
|
||||
## 1.3.5(2022-01-24)
|
||||
- 优化 size 属性可以传入不带单位的字符串数值
|
||||
## 1.3.4(2022-01-24)
|
||||
- 优化 size 支持其他单位
|
||||
## 1.3.3(2022-01-17)
|
||||
- 修复 nvue 有些图标不显示的bug,兼容老版本图标
|
||||
## 1.3.2(2021-12-01)
|
||||
- 优化 示例可复制图标名称
|
||||
## 1.3.1(2021-11-23)
|
||||
- 优化 兼容旧组件 type 值
|
||||
## 1.3.0(2021-11-19)
|
||||
- 新增 更多图标
|
||||
- 优化 自定义图标使用方式
|
||||
- 优化 组件UI,并提供设计资源,详见:[https://uniapp.dcloud.io/component/uniui/resource](https://uniapp.dcloud.io/component/uniui/resource)
|
||||
- 文档迁移,详见:[https://uniapp.dcloud.io/component/uniui/uni-icons](https://uniapp.dcloud.io/component/uniui/uni-icons)
|
||||
## 1.1.7(2021-11-08)
|
||||
## 1.2.0(2021-07-30)
|
||||
- 组件兼容 vue3,如何创建vue3项目,详见 [uni-app 项目支持 vue3 介绍](https://ask.dcloud.net.cn/article/37834)
|
||||
## 1.1.5(2021-05-12)
|
||||
- 新增 组件示例地址
|
||||
## 1.1.4(2021-02-05)
|
||||
- 调整为uni_modules目录规范
|
||||
@@ -0,0 +1,91 @@
|
||||
<template>
|
||||
<text class="uni-icons" :style="styleObj">
|
||||
<slot>{{unicode}}</slot>
|
||||
</text>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { fontData, IconsDataItem } from './uniicons_file'
|
||||
|
||||
/**
|
||||
* Icons 图标
|
||||
* @description 用于展示 icon 图标
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?id=28
|
||||
* @property {Number} size 图标大小
|
||||
* @property {String} type 图标图案,参考示例
|
||||
* @property {String} color 图标颜色
|
||||
* @property {String} customPrefix 自定义图标
|
||||
* @event {Function} click 点击 Icon 触发事件
|
||||
*/
|
||||
export default {
|
||||
name: "uni-icons",
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '#333333'
|
||||
},
|
||||
size: {
|
||||
type: [Number, String],
|
||||
default: 16
|
||||
},
|
||||
fontFamily: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
unicode() : string {
|
||||
let codes = fontData.find((item : IconsDataItem) : boolean => { return item.font_class == this.type })
|
||||
if (codes !== null) {
|
||||
return codes.unicode
|
||||
}
|
||||
return ''
|
||||
},
|
||||
iconSize() : string {
|
||||
const size = this.size
|
||||
if (typeof size == 'string') {
|
||||
const reg = /^[0-9]*$/g
|
||||
return reg.test(size as string) ? '' + size + 'px' : '' + size;
|
||||
// return '' + this.size
|
||||
}
|
||||
return this.getFontSize(size as number)
|
||||
},
|
||||
styleObj() : UTSJSONObject {
|
||||
if (this.fontFamily !== '') {
|
||||
return { color: this.color, fontSize: this.iconSize, fontFamily: this.fontFamily }
|
||||
}
|
||||
return { color: this.color, fontSize: this.iconSize }
|
||||
}
|
||||
},
|
||||
created() { },
|
||||
methods: {
|
||||
/**
|
||||
* 字体大小
|
||||
*/
|
||||
getFontSize(size : number) : string {
|
||||
return size + 'px';
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@font-face {
|
||||
font-family: UniIconsFontFamily;
|
||||
src: url('./uniicons.ttf');
|
||||
}
|
||||
|
||||
.uni-icons {
|
||||
font-family: UniIconsFontFamily;
|
||||
font-size: 18px;
|
||||
font-style: normal;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<!-- #ifdef APP-NVUE -->
|
||||
<text :style="styleObj" class="uni-icons" @click="_onClick">{{unicode}}</text>
|
||||
<!-- #endif -->
|
||||
<!-- #ifndef APP-NVUE -->
|
||||
<text :style="styleObj" class="uni-icons" :class="['uniui-'+type,customPrefix,customPrefix?type:'']" @click="_onClick">
|
||||
<slot></slot>
|
||||
</text>
|
||||
<!-- #endif -->
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { fontData } from './uniicons_file_vue.js';
|
||||
|
||||
const getVal = (val) => {
|
||||
const reg = /^[0-9]*$/g
|
||||
return (typeof val === 'number' || reg.test(val)) ? val + 'px' : val;
|
||||
}
|
||||
|
||||
// #ifdef APP-NVUE
|
||||
var domModule = weex.requireModule('dom');
|
||||
import iconUrl from './uniicons.ttf'
|
||||
domModule.addRule('fontFace', {
|
||||
'fontFamily': "uniicons",
|
||||
'src': "url('" + iconUrl + "')"
|
||||
});
|
||||
// #endif
|
||||
|
||||
/**
|
||||
* Icons 图标
|
||||
* @description 用于展示 icons 图标
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?id=28
|
||||
* @property {Number} size 图标大小
|
||||
* @property {String} type 图标图案,参考示例
|
||||
* @property {String} color 图标颜色
|
||||
* @property {String} customPrefix 自定义图标
|
||||
* @event {Function} click 点击 Icon 触发事件
|
||||
*/
|
||||
export default {
|
||||
name: 'UniIcons',
|
||||
emits: ['click'],
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '#333333'
|
||||
},
|
||||
size: {
|
||||
type: [Number, String],
|
||||
default: 16
|
||||
},
|
||||
customPrefix: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
fontFamily: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
icons: fontData
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
unicode() {
|
||||
let code = this.icons.find(v => v.font_class === this.type)
|
||||
if (code) {
|
||||
return code.unicode
|
||||
}
|
||||
return ''
|
||||
},
|
||||
iconSize() {
|
||||
return getVal(this.size)
|
||||
},
|
||||
styleObj() {
|
||||
if (this.fontFamily !== '') {
|
||||
return `color: ${this.color}; font-size: ${this.iconSize}; font-family: ${this.fontFamily};`
|
||||
}
|
||||
return `color: ${this.color}; font-size: ${this.iconSize};`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
_onClick(e) {
|
||||
this.$emit('click', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
/* #ifndef APP-NVUE */
|
||||
@import './uniicons.css';
|
||||
|
||||
@font-face {
|
||||
font-family: uniicons;
|
||||
src: url('./uniicons.ttf');
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
.uni-icons {
|
||||
font-family: uniicons;
|
||||
text-decoration: none;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,664 @@
|
||||
|
||||
.uniui-cart-filled:before {
|
||||
content: "\e6d0";
|
||||
}
|
||||
|
||||
.uniui-gift-filled:before {
|
||||
content: "\e6c4";
|
||||
}
|
||||
|
||||
.uniui-color:before {
|
||||
content: "\e6cf";
|
||||
}
|
||||
|
||||
.uniui-wallet:before {
|
||||
content: "\e6b1";
|
||||
}
|
||||
|
||||
.uniui-settings-filled:before {
|
||||
content: "\e6ce";
|
||||
}
|
||||
|
||||
.uniui-auth-filled:before {
|
||||
content: "\e6cc";
|
||||
}
|
||||
|
||||
.uniui-shop-filled:before {
|
||||
content: "\e6cd";
|
||||
}
|
||||
|
||||
.uniui-staff-filled:before {
|
||||
content: "\e6cb";
|
||||
}
|
||||
|
||||
.uniui-vip-filled:before {
|
||||
content: "\e6c6";
|
||||
}
|
||||
|
||||
.uniui-plus-filled:before {
|
||||
content: "\e6c7";
|
||||
}
|
||||
|
||||
.uniui-folder-add-filled:before {
|
||||
content: "\e6c8";
|
||||
}
|
||||
|
||||
.uniui-color-filled:before {
|
||||
content: "\e6c9";
|
||||
}
|
||||
|
||||
.uniui-tune-filled:before {
|
||||
content: "\e6ca";
|
||||
}
|
||||
|
||||
.uniui-calendar-filled:before {
|
||||
content: "\e6c0";
|
||||
}
|
||||
|
||||
.uniui-notification-filled:before {
|
||||
content: "\e6c1";
|
||||
}
|
||||
|
||||
.uniui-wallet-filled:before {
|
||||
content: "\e6c2";
|
||||
}
|
||||
|
||||
.uniui-medal-filled:before {
|
||||
content: "\e6c3";
|
||||
}
|
||||
|
||||
.uniui-fire-filled:before {
|
||||
content: "\e6c5";
|
||||
}
|
||||
|
||||
.uniui-refreshempty:before {
|
||||
content: "\e6bf";
|
||||
}
|
||||
|
||||
.uniui-location-filled:before {
|
||||
content: "\e6af";
|
||||
}
|
||||
|
||||
.uniui-person-filled:before {
|
||||
content: "\e69d";
|
||||
}
|
||||
|
||||
.uniui-personadd-filled:before {
|
||||
content: "\e698";
|
||||
}
|
||||
|
||||
.uniui-arrowthinleft:before {
|
||||
content: "\e6d2";
|
||||
}
|
||||
|
||||
.uniui-arrowthinup:before {
|
||||
content: "\e6d3";
|
||||
}
|
||||
|
||||
.uniui-arrowthindown:before {
|
||||
content: "\e6d4";
|
||||
}
|
||||
|
||||
.uniui-back:before {
|
||||
content: "\e6b9";
|
||||
}
|
||||
|
||||
.uniui-forward:before {
|
||||
content: "\e6ba";
|
||||
}
|
||||
|
||||
.uniui-arrow-right:before {
|
||||
content: "\e6bb";
|
||||
}
|
||||
|
||||
.uniui-arrow-left:before {
|
||||
content: "\e6bc";
|
||||
}
|
||||
|
||||
.uniui-arrow-up:before {
|
||||
content: "\e6bd";
|
||||
}
|
||||
|
||||
.uniui-arrow-down:before {
|
||||
content: "\e6be";
|
||||
}
|
||||
|
||||
.uniui-arrowthinright:before {
|
||||
content: "\e6d1";
|
||||
}
|
||||
|
||||
.uniui-down:before {
|
||||
content: "\e6b8";
|
||||
}
|
||||
|
||||
.uniui-bottom:before {
|
||||
content: "\e6b8";
|
||||
}
|
||||
|
||||
.uniui-arrowright:before {
|
||||
content: "\e6d5";
|
||||
}
|
||||
|
||||
.uniui-right:before {
|
||||
content: "\e6b5";
|
||||
}
|
||||
|
||||
.uniui-up:before {
|
||||
content: "\e6b6";
|
||||
}
|
||||
|
||||
.uniui-top:before {
|
||||
content: "\e6b6";
|
||||
}
|
||||
|
||||
.uniui-left:before {
|
||||
content: "\e6b7";
|
||||
}
|
||||
|
||||
.uniui-arrowup:before {
|
||||
content: "\e6d6";
|
||||
}
|
||||
|
||||
.uniui-eye:before {
|
||||
content: "\e651";
|
||||
}
|
||||
|
||||
.uniui-eye-filled:before {
|
||||
content: "\e66a";
|
||||
}
|
||||
|
||||
.uniui-eye-slash:before {
|
||||
content: "\e6b3";
|
||||
}
|
||||
|
||||
.uniui-eye-slash-filled:before {
|
||||
content: "\e6b4";
|
||||
}
|
||||
|
||||
.uniui-info-filled:before {
|
||||
content: "\e649";
|
||||
}
|
||||
|
||||
.uniui-reload:before {
|
||||
content: "\e6b2";
|
||||
}
|
||||
|
||||
.uniui-micoff-filled:before {
|
||||
content: "\e6b0";
|
||||
}
|
||||
|
||||
.uniui-map-pin-ellipse:before {
|
||||
content: "\e6ac";
|
||||
}
|
||||
|
||||
.uniui-map-pin:before {
|
||||
content: "\e6ad";
|
||||
}
|
||||
|
||||
.uniui-location:before {
|
||||
content: "\e6ae";
|
||||
}
|
||||
|
||||
.uniui-starhalf:before {
|
||||
content: "\e683";
|
||||
}
|
||||
|
||||
.uniui-star:before {
|
||||
content: "\e688";
|
||||
}
|
||||
|
||||
.uniui-star-filled:before {
|
||||
content: "\e68f";
|
||||
}
|
||||
|
||||
.uniui-calendar:before {
|
||||
content: "\e6a0";
|
||||
}
|
||||
|
||||
.uniui-fire:before {
|
||||
content: "\e6a1";
|
||||
}
|
||||
|
||||
.uniui-medal:before {
|
||||
content: "\e6a2";
|
||||
}
|
||||
|
||||
.uniui-font:before {
|
||||
content: "\e6a3";
|
||||
}
|
||||
|
||||
.uniui-gift:before {
|
||||
content: "\e6a4";
|
||||
}
|
||||
|
||||
.uniui-link:before {
|
||||
content: "\e6a5";
|
||||
}
|
||||
|
||||
.uniui-notification:before {
|
||||
content: "\e6a6";
|
||||
}
|
||||
|
||||
.uniui-staff:before {
|
||||
content: "\e6a7";
|
||||
}
|
||||
|
||||
.uniui-vip:before {
|
||||
content: "\e6a8";
|
||||
}
|
||||
|
||||
.uniui-folder-add:before {
|
||||
content: "\e6a9";
|
||||
}
|
||||
|
||||
.uniui-tune:before {
|
||||
content: "\e6aa";
|
||||
}
|
||||
|
||||
.uniui-auth:before {
|
||||
content: "\e6ab";
|
||||
}
|
||||
|
||||
.uniui-person:before {
|
||||
content: "\e699";
|
||||
}
|
||||
|
||||
.uniui-email-filled:before {
|
||||
content: "\e69a";
|
||||
}
|
||||
|
||||
.uniui-phone-filled:before {
|
||||
content: "\e69b";
|
||||
}
|
||||
|
||||
.uniui-phone:before {
|
||||
content: "\e69c";
|
||||
}
|
||||
|
||||
.uniui-email:before {
|
||||
content: "\e69e";
|
||||
}
|
||||
|
||||
.uniui-personadd:before {
|
||||
content: "\e69f";
|
||||
}
|
||||
|
||||
.uniui-chatboxes-filled:before {
|
||||
content: "\e692";
|
||||
}
|
||||
|
||||
.uniui-contact:before {
|
||||
content: "\e693";
|
||||
}
|
||||
|
||||
.uniui-chatbubble-filled:before {
|
||||
content: "\e694";
|
||||
}
|
||||
|
||||
.uniui-contact-filled:before {
|
||||
content: "\e695";
|
||||
}
|
||||
|
||||
.uniui-chatboxes:before {
|
||||
content: "\e696";
|
||||
}
|
||||
|
||||
.uniui-chatbubble:before {
|
||||
content: "\e697";
|
||||
}
|
||||
|
||||
.uniui-upload-filled:before {
|
||||
content: "\e68e";
|
||||
}
|
||||
|
||||
.uniui-upload:before {
|
||||
content: "\e690";
|
||||
}
|
||||
|
||||
.uniui-weixin:before {
|
||||
content: "\e691";
|
||||
}
|
||||
|
||||
.uniui-compose:before {
|
||||
content: "\e67f";
|
||||
}
|
||||
|
||||
.uniui-qq:before {
|
||||
content: "\e680";
|
||||
}
|
||||
|
||||
.uniui-download-filled:before {
|
||||
content: "\e681";
|
||||
}
|
||||
|
||||
.uniui-pyq:before {
|
||||
content: "\e682";
|
||||
}
|
||||
|
||||
.uniui-sound:before {
|
||||
content: "\e684";
|
||||
}
|
||||
|
||||
.uniui-trash-filled:before {
|
||||
content: "\e685";
|
||||
}
|
||||
|
||||
.uniui-sound-filled:before {
|
||||
content: "\e686";
|
||||
}
|
||||
|
||||
.uniui-trash:before {
|
||||
content: "\e687";
|
||||
}
|
||||
|
||||
.uniui-videocam-filled:before {
|
||||
content: "\e689";
|
||||
}
|
||||
|
||||
.uniui-spinner-cycle:before {
|
||||
content: "\e68a";
|
||||
}
|
||||
|
||||
.uniui-weibo:before {
|
||||
content: "\e68b";
|
||||
}
|
||||
|
||||
.uniui-videocam:before {
|
||||
content: "\e68c";
|
||||
}
|
||||
|
||||
.uniui-download:before {
|
||||
content: "\e68d";
|
||||
}
|
||||
|
||||
.uniui-help:before {
|
||||
content: "\e679";
|
||||
}
|
||||
|
||||
.uniui-navigate-filled:before {
|
||||
content: "\e67a";
|
||||
}
|
||||
|
||||
.uniui-plusempty:before {
|
||||
content: "\e67b";
|
||||
}
|
||||
|
||||
.uniui-smallcircle:before {
|
||||
content: "\e67c";
|
||||
}
|
||||
|
||||
.uniui-minus-filled:before {
|
||||
content: "\e67d";
|
||||
}
|
||||
|
||||
.uniui-micoff:before {
|
||||
content: "\e67e";
|
||||
}
|
||||
|
||||
.uniui-closeempty:before {
|
||||
content: "\e66c";
|
||||
}
|
||||
|
||||
.uniui-clear:before {
|
||||
content: "\e66d";
|
||||
}
|
||||
|
||||
.uniui-navigate:before {
|
||||
content: "\e66e";
|
||||
}
|
||||
|
||||
.uniui-minus:before {
|
||||
content: "\e66f";
|
||||
}
|
||||
|
||||
.uniui-image:before {
|
||||
content: "\e670";
|
||||
}
|
||||
|
||||
.uniui-mic:before {
|
||||
content: "\e671";
|
||||
}
|
||||
|
||||
.uniui-paperplane:before {
|
||||
content: "\e672";
|
||||
}
|
||||
|
||||
.uniui-close:before {
|
||||
content: "\e673";
|
||||
}
|
||||
|
||||
.uniui-help-filled:before {
|
||||
content: "\e674";
|
||||
}
|
||||
|
||||
.uniui-paperplane-filled:before {
|
||||
content: "\e675";
|
||||
}
|
||||
|
||||
.uniui-plus:before {
|
||||
content: "\e676";
|
||||
}
|
||||
|
||||
.uniui-mic-filled:before {
|
||||
content: "\e677";
|
||||
}
|
||||
|
||||
.uniui-image-filled:before {
|
||||
content: "\e678";
|
||||
}
|
||||
|
||||
.uniui-locked-filled:before {
|
||||
content: "\e668";
|
||||
}
|
||||
|
||||
.uniui-info:before {
|
||||
content: "\e669";
|
||||
}
|
||||
|
||||
.uniui-locked:before {
|
||||
content: "\e66b";
|
||||
}
|
||||
|
||||
.uniui-camera-filled:before {
|
||||
content: "\e658";
|
||||
}
|
||||
|
||||
.uniui-chat-filled:before {
|
||||
content: "\e659";
|
||||
}
|
||||
|
||||
.uniui-camera:before {
|
||||
content: "\e65a";
|
||||
}
|
||||
|
||||
.uniui-circle:before {
|
||||
content: "\e65b";
|
||||
}
|
||||
|
||||
.uniui-checkmarkempty:before {
|
||||
content: "\e65c";
|
||||
}
|
||||
|
||||
.uniui-chat:before {
|
||||
content: "\e65d";
|
||||
}
|
||||
|
||||
.uniui-circle-filled:before {
|
||||
content: "\e65e";
|
||||
}
|
||||
|
||||
.uniui-flag:before {
|
||||
content: "\e65f";
|
||||
}
|
||||
|
||||
.uniui-flag-filled:before {
|
||||
content: "\e660";
|
||||
}
|
||||
|
||||
.uniui-gear-filled:before {
|
||||
content: "\e661";
|
||||
}
|
||||
|
||||
.uniui-home:before {
|
||||
content: "\e662";
|
||||
}
|
||||
|
||||
.uniui-home-filled:before {
|
||||
content: "\e663";
|
||||
}
|
||||
|
||||
.uniui-gear:before {
|
||||
content: "\e664";
|
||||
}
|
||||
|
||||
.uniui-smallcircle-filled:before {
|
||||
content: "\e665";
|
||||
}
|
||||
|
||||
.uniui-map-filled:before {
|
||||
content: "\e666";
|
||||
}
|
||||
|
||||
.uniui-map:before {
|
||||
content: "\e667";
|
||||
}
|
||||
|
||||
.uniui-refresh-filled:before {
|
||||
content: "\e656";
|
||||
}
|
||||
|
||||
.uniui-refresh:before {
|
||||
content: "\e657";
|
||||
}
|
||||
|
||||
.uniui-cloud-upload:before {
|
||||
content: "\e645";
|
||||
}
|
||||
|
||||
.uniui-cloud-download-filled:before {
|
||||
content: "\e646";
|
||||
}
|
||||
|
||||
.uniui-cloud-download:before {
|
||||
content: "\e647";
|
||||
}
|
||||
|
||||
.uniui-cloud-upload-filled:before {
|
||||
content: "\e648";
|
||||
}
|
||||
|
||||
.uniui-redo:before {
|
||||
content: "\e64a";
|
||||
}
|
||||
|
||||
.uniui-images-filled:before {
|
||||
content: "\e64b";
|
||||
}
|
||||
|
||||
.uniui-undo-filled:before {
|
||||
content: "\e64c";
|
||||
}
|
||||
|
||||
.uniui-more:before {
|
||||
content: "\e64d";
|
||||
}
|
||||
|
||||
.uniui-more-filled:before {
|
||||
content: "\e64e";
|
||||
}
|
||||
|
||||
.uniui-undo:before {
|
||||
content: "\e64f";
|
||||
}
|
||||
|
||||
.uniui-images:before {
|
||||
content: "\e650";
|
||||
}
|
||||
|
||||
.uniui-paperclip:before {
|
||||
content: "\e652";
|
||||
}
|
||||
|
||||
.uniui-settings:before {
|
||||
content: "\e653";
|
||||
}
|
||||
|
||||
.uniui-search:before {
|
||||
content: "\e654";
|
||||
}
|
||||
|
||||
.uniui-redo-filled:before {
|
||||
content: "\e655";
|
||||
}
|
||||
|
||||
.uniui-list:before {
|
||||
content: "\e644";
|
||||
}
|
||||
|
||||
.uniui-mail-open-filled:before {
|
||||
content: "\e63a";
|
||||
}
|
||||
|
||||
.uniui-hand-down-filled:before {
|
||||
content: "\e63c";
|
||||
}
|
||||
|
||||
.uniui-hand-down:before {
|
||||
content: "\e63d";
|
||||
}
|
||||
|
||||
.uniui-hand-up-filled:before {
|
||||
content: "\e63e";
|
||||
}
|
||||
|
||||
.uniui-hand-up:before {
|
||||
content: "\e63f";
|
||||
}
|
||||
|
||||
.uniui-heart-filled:before {
|
||||
content: "\e641";
|
||||
}
|
||||
|
||||
.uniui-mail-open:before {
|
||||
content: "\e643";
|
||||
}
|
||||
|
||||
.uniui-heart:before {
|
||||
content: "\e639";
|
||||
}
|
||||
|
||||
.uniui-loop:before {
|
||||
content: "\e633";
|
||||
}
|
||||
|
||||
.uniui-pulldown:before {
|
||||
content: "\e632";
|
||||
}
|
||||
|
||||
.uniui-scan:before {
|
||||
content: "\e62a";
|
||||
}
|
||||
|
||||
.uniui-bars:before {
|
||||
content: "\e627";
|
||||
}
|
||||
|
||||
.uniui-checkbox:before {
|
||||
content: "\e62b";
|
||||
}
|
||||
|
||||
.uniui-checkbox-filled:before {
|
||||
content: "\e62c";
|
||||
}
|
||||
|
||||
.uniui-shop:before {
|
||||
content: "\e62f";
|
||||
}
|
||||
|
||||
.uniui-headphones:before {
|
||||
content: "\e630";
|
||||
}
|
||||
|
||||
.uniui-cart:before {
|
||||
content: "\e631";
|
||||
}
|
||||
@@ -0,0 +1,664 @@
|
||||
|
||||
export type IconsData = {
|
||||
id : string
|
||||
name : string
|
||||
font_family : string
|
||||
css_prefix_text : string
|
||||
description : string
|
||||
glyphs : Array<IconsDataItem>
|
||||
}
|
||||
|
||||
export type IconsDataItem = {
|
||||
font_class : string
|
||||
unicode : string
|
||||
}
|
||||
|
||||
|
||||
export const fontData = [
|
||||
{
|
||||
"font_class": "arrow-down",
|
||||
"unicode": "\ue6be"
|
||||
},
|
||||
{
|
||||
"font_class": "arrow-left",
|
||||
"unicode": "\ue6bc"
|
||||
},
|
||||
{
|
||||
"font_class": "arrow-right",
|
||||
"unicode": "\ue6bb"
|
||||
},
|
||||
{
|
||||
"font_class": "arrow-up",
|
||||
"unicode": "\ue6bd"
|
||||
},
|
||||
{
|
||||
"font_class": "auth",
|
||||
"unicode": "\ue6ab"
|
||||
},
|
||||
{
|
||||
"font_class": "auth-filled",
|
||||
"unicode": "\ue6cc"
|
||||
},
|
||||
{
|
||||
"font_class": "back",
|
||||
"unicode": "\ue6b9"
|
||||
},
|
||||
{
|
||||
"font_class": "bars",
|
||||
"unicode": "\ue627"
|
||||
},
|
||||
{
|
||||
"font_class": "calendar",
|
||||
"unicode": "\ue6a0"
|
||||
},
|
||||
{
|
||||
"font_class": "calendar-filled",
|
||||
"unicode": "\ue6c0"
|
||||
},
|
||||
{
|
||||
"font_class": "camera",
|
||||
"unicode": "\ue65a"
|
||||
},
|
||||
{
|
||||
"font_class": "camera-filled",
|
||||
"unicode": "\ue658"
|
||||
},
|
||||
{
|
||||
"font_class": "cart",
|
||||
"unicode": "\ue631"
|
||||
},
|
||||
{
|
||||
"font_class": "cart-filled",
|
||||
"unicode": "\ue6d0"
|
||||
},
|
||||
{
|
||||
"font_class": "chat",
|
||||
"unicode": "\ue65d"
|
||||
},
|
||||
{
|
||||
"font_class": "chat-filled",
|
||||
"unicode": "\ue659"
|
||||
},
|
||||
{
|
||||
"font_class": "chatboxes",
|
||||
"unicode": "\ue696"
|
||||
},
|
||||
{
|
||||
"font_class": "chatboxes-filled",
|
||||
"unicode": "\ue692"
|
||||
},
|
||||
{
|
||||
"font_class": "chatbubble",
|
||||
"unicode": "\ue697"
|
||||
},
|
||||
{
|
||||
"font_class": "chatbubble-filled",
|
||||
"unicode": "\ue694"
|
||||
},
|
||||
{
|
||||
"font_class": "checkbox",
|
||||
"unicode": "\ue62b"
|
||||
},
|
||||
{
|
||||
"font_class": "checkbox-filled",
|
||||
"unicode": "\ue62c"
|
||||
},
|
||||
{
|
||||
"font_class": "checkmarkempty",
|
||||
"unicode": "\ue65c"
|
||||
},
|
||||
{
|
||||
"font_class": "circle",
|
||||
"unicode": "\ue65b"
|
||||
},
|
||||
{
|
||||
"font_class": "circle-filled",
|
||||
"unicode": "\ue65e"
|
||||
},
|
||||
{
|
||||
"font_class": "clear",
|
||||
"unicode": "\ue66d"
|
||||
},
|
||||
{
|
||||
"font_class": "close",
|
||||
"unicode": "\ue673"
|
||||
},
|
||||
{
|
||||
"font_class": "closeempty",
|
||||
"unicode": "\ue66c"
|
||||
},
|
||||
{
|
||||
"font_class": "cloud-download",
|
||||
"unicode": "\ue647"
|
||||
},
|
||||
{
|
||||
"font_class": "cloud-download-filled",
|
||||
"unicode": "\ue646"
|
||||
},
|
||||
{
|
||||
"font_class": "cloud-upload",
|
||||
"unicode": "\ue645"
|
||||
},
|
||||
{
|
||||
"font_class": "cloud-upload-filled",
|
||||
"unicode": "\ue648"
|
||||
},
|
||||
{
|
||||
"font_class": "color",
|
||||
"unicode": "\ue6cf"
|
||||
},
|
||||
{
|
||||
"font_class": "color-filled",
|
||||
"unicode": "\ue6c9"
|
||||
},
|
||||
{
|
||||
"font_class": "compose",
|
||||
"unicode": "\ue67f"
|
||||
},
|
||||
{
|
||||
"font_class": "contact",
|
||||
"unicode": "\ue693"
|
||||
},
|
||||
{
|
||||
"font_class": "contact-filled",
|
||||
"unicode": "\ue695"
|
||||
},
|
||||
{
|
||||
"font_class": "down",
|
||||
"unicode": "\ue6b8"
|
||||
},
|
||||
{
|
||||
"font_class": "bottom",
|
||||
"unicode": "\ue6b8"
|
||||
},
|
||||
{
|
||||
"font_class": "download",
|
||||
"unicode": "\ue68d"
|
||||
},
|
||||
{
|
||||
"font_class": "download-filled",
|
||||
"unicode": "\ue681"
|
||||
},
|
||||
{
|
||||
"font_class": "email",
|
||||
"unicode": "\ue69e"
|
||||
},
|
||||
{
|
||||
"font_class": "email-filled",
|
||||
"unicode": "\ue69a"
|
||||
},
|
||||
{
|
||||
"font_class": "eye",
|
||||
"unicode": "\ue651"
|
||||
},
|
||||
{
|
||||
"font_class": "eye-filled",
|
||||
"unicode": "\ue66a"
|
||||
},
|
||||
{
|
||||
"font_class": "eye-slash",
|
||||
"unicode": "\ue6b3"
|
||||
},
|
||||
{
|
||||
"font_class": "eye-slash-filled",
|
||||
"unicode": "\ue6b4"
|
||||
},
|
||||
{
|
||||
"font_class": "fire",
|
||||
"unicode": "\ue6a1"
|
||||
},
|
||||
{
|
||||
"font_class": "fire-filled",
|
||||
"unicode": "\ue6c5"
|
||||
},
|
||||
{
|
||||
"font_class": "flag",
|
||||
"unicode": "\ue65f"
|
||||
},
|
||||
{
|
||||
"font_class": "flag-filled",
|
||||
"unicode": "\ue660"
|
||||
},
|
||||
{
|
||||
"font_class": "folder-add",
|
||||
"unicode": "\ue6a9"
|
||||
},
|
||||
{
|
||||
"font_class": "folder-add-filled",
|
||||
"unicode": "\ue6c8"
|
||||
},
|
||||
{
|
||||
"font_class": "font",
|
||||
"unicode": "\ue6a3"
|
||||
},
|
||||
{
|
||||
"font_class": "forward",
|
||||
"unicode": "\ue6ba"
|
||||
},
|
||||
{
|
||||
"font_class": "gear",
|
||||
"unicode": "\ue664"
|
||||
},
|
||||
{
|
||||
"font_class": "gear-filled",
|
||||
"unicode": "\ue661"
|
||||
},
|
||||
{
|
||||
"font_class": "gift",
|
||||
"unicode": "\ue6a4"
|
||||
},
|
||||
{
|
||||
"font_class": "gift-filled",
|
||||
"unicode": "\ue6c4"
|
||||
},
|
||||
{
|
||||
"font_class": "hand-down",
|
||||
"unicode": "\ue63d"
|
||||
},
|
||||
{
|
||||
"font_class": "hand-down-filled",
|
||||
"unicode": "\ue63c"
|
||||
},
|
||||
{
|
||||
"font_class": "hand-up",
|
||||
"unicode": "\ue63f"
|
||||
},
|
||||
{
|
||||
"font_class": "hand-up-filled",
|
||||
"unicode": "\ue63e"
|
||||
},
|
||||
{
|
||||
"font_class": "headphones",
|
||||
"unicode": "\ue630"
|
||||
},
|
||||
{
|
||||
"font_class": "heart",
|
||||
"unicode": "\ue639"
|
||||
},
|
||||
{
|
||||
"font_class": "heart-filled",
|
||||
"unicode": "\ue641"
|
||||
},
|
||||
{
|
||||
"font_class": "help",
|
||||
"unicode": "\ue679"
|
||||
},
|
||||
{
|
||||
"font_class": "help-filled",
|
||||
"unicode": "\ue674"
|
||||
},
|
||||
{
|
||||
"font_class": "home",
|
||||
"unicode": "\ue662"
|
||||
},
|
||||
{
|
||||
"font_class": "home-filled",
|
||||
"unicode": "\ue663"
|
||||
},
|
||||
{
|
||||
"font_class": "image",
|
||||
"unicode": "\ue670"
|
||||
},
|
||||
{
|
||||
"font_class": "image-filled",
|
||||
"unicode": "\ue678"
|
||||
},
|
||||
{
|
||||
"font_class": "images",
|
||||
"unicode": "\ue650"
|
||||
},
|
||||
{
|
||||
"font_class": "images-filled",
|
||||
"unicode": "\ue64b"
|
||||
},
|
||||
{
|
||||
"font_class": "info",
|
||||
"unicode": "\ue669"
|
||||
},
|
||||
{
|
||||
"font_class": "info-filled",
|
||||
"unicode": "\ue649"
|
||||
},
|
||||
{
|
||||
"font_class": "left",
|
||||
"unicode": "\ue6b7"
|
||||
},
|
||||
{
|
||||
"font_class": "link",
|
||||
"unicode": "\ue6a5"
|
||||
},
|
||||
{
|
||||
"font_class": "list",
|
||||
"unicode": "\ue644"
|
||||
},
|
||||
{
|
||||
"font_class": "location",
|
||||
"unicode": "\ue6ae"
|
||||
},
|
||||
{
|
||||
"font_class": "location-filled",
|
||||
"unicode": "\ue6af"
|
||||
},
|
||||
{
|
||||
"font_class": "locked",
|
||||
"unicode": "\ue66b"
|
||||
},
|
||||
{
|
||||
"font_class": "locked-filled",
|
||||
"unicode": "\ue668"
|
||||
},
|
||||
{
|
||||
"font_class": "loop",
|
||||
"unicode": "\ue633"
|
||||
},
|
||||
{
|
||||
"font_class": "mail-open",
|
||||
"unicode": "\ue643"
|
||||
},
|
||||
{
|
||||
"font_class": "mail-open-filled",
|
||||
"unicode": "\ue63a"
|
||||
},
|
||||
{
|
||||
"font_class": "map",
|
||||
"unicode": "\ue667"
|
||||
},
|
||||
{
|
||||
"font_class": "map-filled",
|
||||
"unicode": "\ue666"
|
||||
},
|
||||
{
|
||||
"font_class": "map-pin",
|
||||
"unicode": "\ue6ad"
|
||||
},
|
||||
{
|
||||
"font_class": "map-pin-ellipse",
|
||||
"unicode": "\ue6ac"
|
||||
},
|
||||
{
|
||||
"font_class": "medal",
|
||||
"unicode": "\ue6a2"
|
||||
},
|
||||
{
|
||||
"font_class": "medal-filled",
|
||||
"unicode": "\ue6c3"
|
||||
},
|
||||
{
|
||||
"font_class": "mic",
|
||||
"unicode": "\ue671"
|
||||
},
|
||||
{
|
||||
"font_class": "mic-filled",
|
||||
"unicode": "\ue677"
|
||||
},
|
||||
{
|
||||
"font_class": "micoff",
|
||||
"unicode": "\ue67e"
|
||||
},
|
||||
{
|
||||
"font_class": "micoff-filled",
|
||||
"unicode": "\ue6b0"
|
||||
},
|
||||
{
|
||||
"font_class": "minus",
|
||||
"unicode": "\ue66f"
|
||||
},
|
||||
{
|
||||
"font_class": "minus-filled",
|
||||
"unicode": "\ue67d"
|
||||
},
|
||||
{
|
||||
"font_class": "more",
|
||||
"unicode": "\ue64d"
|
||||
},
|
||||
{
|
||||
"font_class": "more-filled",
|
||||
"unicode": "\ue64e"
|
||||
},
|
||||
{
|
||||
"font_class": "navigate",
|
||||
"unicode": "\ue66e"
|
||||
},
|
||||
{
|
||||
"font_class": "navigate-filled",
|
||||
"unicode": "\ue67a"
|
||||
},
|
||||
{
|
||||
"font_class": "notification",
|
||||
"unicode": "\ue6a6"
|
||||
},
|
||||
{
|
||||
"font_class": "notification-filled",
|
||||
"unicode": "\ue6c1"
|
||||
},
|
||||
{
|
||||
"font_class": "paperclip",
|
||||
"unicode": "\ue652"
|
||||
},
|
||||
{
|
||||
"font_class": "paperplane",
|
||||
"unicode": "\ue672"
|
||||
},
|
||||
{
|
||||
"font_class": "paperplane-filled",
|
||||
"unicode": "\ue675"
|
||||
},
|
||||
{
|
||||
"font_class": "person",
|
||||
"unicode": "\ue699"
|
||||
},
|
||||
{
|
||||
"font_class": "person-filled",
|
||||
"unicode": "\ue69d"
|
||||
},
|
||||
{
|
||||
"font_class": "personadd",
|
||||
"unicode": "\ue69f"
|
||||
},
|
||||
{
|
||||
"font_class": "personadd-filled",
|
||||
"unicode": "\ue698"
|
||||
},
|
||||
{
|
||||
"font_class": "personadd-filled-copy",
|
||||
"unicode": "\ue6d1"
|
||||
},
|
||||
{
|
||||
"font_class": "phone",
|
||||
"unicode": "\ue69c"
|
||||
},
|
||||
{
|
||||
"font_class": "phone-filled",
|
||||
"unicode": "\ue69b"
|
||||
},
|
||||
{
|
||||
"font_class": "plus",
|
||||
"unicode": "\ue676"
|
||||
},
|
||||
{
|
||||
"font_class": "plus-filled",
|
||||
"unicode": "\ue6c7"
|
||||
},
|
||||
{
|
||||
"font_class": "plusempty",
|
||||
"unicode": "\ue67b"
|
||||
},
|
||||
{
|
||||
"font_class": "pulldown",
|
||||
"unicode": "\ue632"
|
||||
},
|
||||
{
|
||||
"font_class": "pyq",
|
||||
"unicode": "\ue682"
|
||||
},
|
||||
{
|
||||
"font_class": "qq",
|
||||
"unicode": "\ue680"
|
||||
},
|
||||
{
|
||||
"font_class": "redo",
|
||||
"unicode": "\ue64a"
|
||||
},
|
||||
{
|
||||
"font_class": "redo-filled",
|
||||
"unicode": "\ue655"
|
||||
},
|
||||
{
|
||||
"font_class": "refresh",
|
||||
"unicode": "\ue657"
|
||||
},
|
||||
{
|
||||
"font_class": "refresh-filled",
|
||||
"unicode": "\ue656"
|
||||
},
|
||||
{
|
||||
"font_class": "refreshempty",
|
||||
"unicode": "\ue6bf"
|
||||
},
|
||||
{
|
||||
"font_class": "reload",
|
||||
"unicode": "\ue6b2"
|
||||
},
|
||||
{
|
||||
"font_class": "right",
|
||||
"unicode": "\ue6b5"
|
||||
},
|
||||
{
|
||||
"font_class": "scan",
|
||||
"unicode": "\ue62a"
|
||||
},
|
||||
{
|
||||
"font_class": "search",
|
||||
"unicode": "\ue654"
|
||||
},
|
||||
{
|
||||
"font_class": "settings",
|
||||
"unicode": "\ue653"
|
||||
},
|
||||
{
|
||||
"font_class": "settings-filled",
|
||||
"unicode": "\ue6ce"
|
||||
},
|
||||
{
|
||||
"font_class": "shop",
|
||||
"unicode": "\ue62f"
|
||||
},
|
||||
{
|
||||
"font_class": "shop-filled",
|
||||
"unicode": "\ue6cd"
|
||||
},
|
||||
{
|
||||
"font_class": "smallcircle",
|
||||
"unicode": "\ue67c"
|
||||
},
|
||||
{
|
||||
"font_class": "smallcircle-filled",
|
||||
"unicode": "\ue665"
|
||||
},
|
||||
{
|
||||
"font_class": "sound",
|
||||
"unicode": "\ue684"
|
||||
},
|
||||
{
|
||||
"font_class": "sound-filled",
|
||||
"unicode": "\ue686"
|
||||
},
|
||||
{
|
||||
"font_class": "spinner-cycle",
|
||||
"unicode": "\ue68a"
|
||||
},
|
||||
{
|
||||
"font_class": "staff",
|
||||
"unicode": "\ue6a7"
|
||||
},
|
||||
{
|
||||
"font_class": "staff-filled",
|
||||
"unicode": "\ue6cb"
|
||||
},
|
||||
{
|
||||
"font_class": "star",
|
||||
"unicode": "\ue688"
|
||||
},
|
||||
{
|
||||
"font_class": "star-filled",
|
||||
"unicode": "\ue68f"
|
||||
},
|
||||
{
|
||||
"font_class": "starhalf",
|
||||
"unicode": "\ue683"
|
||||
},
|
||||
{
|
||||
"font_class": "trash",
|
||||
"unicode": "\ue687"
|
||||
},
|
||||
{
|
||||
"font_class": "trash-filled",
|
||||
"unicode": "\ue685"
|
||||
},
|
||||
{
|
||||
"font_class": "tune",
|
||||
"unicode": "\ue6aa"
|
||||
},
|
||||
{
|
||||
"font_class": "tune-filled",
|
||||
"unicode": "\ue6ca"
|
||||
},
|
||||
{
|
||||
"font_class": "undo",
|
||||
"unicode": "\ue64f"
|
||||
},
|
||||
{
|
||||
"font_class": "undo-filled",
|
||||
"unicode": "\ue64c"
|
||||
},
|
||||
{
|
||||
"font_class": "up",
|
||||
"unicode": "\ue6b6"
|
||||
},
|
||||
{
|
||||
"font_class": "top",
|
||||
"unicode": "\ue6b6"
|
||||
},
|
||||
{
|
||||
"font_class": "upload",
|
||||
"unicode": "\ue690"
|
||||
},
|
||||
{
|
||||
"font_class": "upload-filled",
|
||||
"unicode": "\ue68e"
|
||||
},
|
||||
{
|
||||
"font_class": "videocam",
|
||||
"unicode": "\ue68c"
|
||||
},
|
||||
{
|
||||
"font_class": "videocam-filled",
|
||||
"unicode": "\ue689"
|
||||
},
|
||||
{
|
||||
"font_class": "vip",
|
||||
"unicode": "\ue6a8"
|
||||
},
|
||||
{
|
||||
"font_class": "vip-filled",
|
||||
"unicode": "\ue6c6"
|
||||
},
|
||||
{
|
||||
"font_class": "wallet",
|
||||
"unicode": "\ue6b1"
|
||||
},
|
||||
{
|
||||
"font_class": "wallet-filled",
|
||||
"unicode": "\ue6c2"
|
||||
},
|
||||
{
|
||||
"font_class": "weibo",
|
||||
"unicode": "\ue68b"
|
||||
},
|
||||
{
|
||||
"font_class": "weixin",
|
||||
"unicode": "\ue691"
|
||||
}
|
||||
] as IconsDataItem[]
|
||||
|
||||
// export const fontData = JSON.parse<IconsDataItem>(fontDataJson)
|
||||
@@ -0,0 +1,649 @@
|
||||
|
||||
export const fontData = [
|
||||
{
|
||||
"font_class": "arrow-down",
|
||||
"unicode": "\ue6be"
|
||||
},
|
||||
{
|
||||
"font_class": "arrow-left",
|
||||
"unicode": "\ue6bc"
|
||||
},
|
||||
{
|
||||
"font_class": "arrow-right",
|
||||
"unicode": "\ue6bb"
|
||||
},
|
||||
{
|
||||
"font_class": "arrow-up",
|
||||
"unicode": "\ue6bd"
|
||||
},
|
||||
{
|
||||
"font_class": "auth",
|
||||
"unicode": "\ue6ab"
|
||||
},
|
||||
{
|
||||
"font_class": "auth-filled",
|
||||
"unicode": "\ue6cc"
|
||||
},
|
||||
{
|
||||
"font_class": "back",
|
||||
"unicode": "\ue6b9"
|
||||
},
|
||||
{
|
||||
"font_class": "bars",
|
||||
"unicode": "\ue627"
|
||||
},
|
||||
{
|
||||
"font_class": "calendar",
|
||||
"unicode": "\ue6a0"
|
||||
},
|
||||
{
|
||||
"font_class": "calendar-filled",
|
||||
"unicode": "\ue6c0"
|
||||
},
|
||||
{
|
||||
"font_class": "camera",
|
||||
"unicode": "\ue65a"
|
||||
},
|
||||
{
|
||||
"font_class": "camera-filled",
|
||||
"unicode": "\ue658"
|
||||
},
|
||||
{
|
||||
"font_class": "cart",
|
||||
"unicode": "\ue631"
|
||||
},
|
||||
{
|
||||
"font_class": "cart-filled",
|
||||
"unicode": "\ue6d0"
|
||||
},
|
||||
{
|
||||
"font_class": "chat",
|
||||
"unicode": "\ue65d"
|
||||
},
|
||||
{
|
||||
"font_class": "chat-filled",
|
||||
"unicode": "\ue659"
|
||||
},
|
||||
{
|
||||
"font_class": "chatboxes",
|
||||
"unicode": "\ue696"
|
||||
},
|
||||
{
|
||||
"font_class": "chatboxes-filled",
|
||||
"unicode": "\ue692"
|
||||
},
|
||||
{
|
||||
"font_class": "chatbubble",
|
||||
"unicode": "\ue697"
|
||||
},
|
||||
{
|
||||
"font_class": "chatbubble-filled",
|
||||
"unicode": "\ue694"
|
||||
},
|
||||
{
|
||||
"font_class": "checkbox",
|
||||
"unicode": "\ue62b"
|
||||
},
|
||||
{
|
||||
"font_class": "checkbox-filled",
|
||||
"unicode": "\ue62c"
|
||||
},
|
||||
{
|
||||
"font_class": "checkmarkempty",
|
||||
"unicode": "\ue65c"
|
||||
},
|
||||
{
|
||||
"font_class": "circle",
|
||||
"unicode": "\ue65b"
|
||||
},
|
||||
{
|
||||
"font_class": "circle-filled",
|
||||
"unicode": "\ue65e"
|
||||
},
|
||||
{
|
||||
"font_class": "clear",
|
||||
"unicode": "\ue66d"
|
||||
},
|
||||
{
|
||||
"font_class": "close",
|
||||
"unicode": "\ue673"
|
||||
},
|
||||
{
|
||||
"font_class": "closeempty",
|
||||
"unicode": "\ue66c"
|
||||
},
|
||||
{
|
||||
"font_class": "cloud-download",
|
||||
"unicode": "\ue647"
|
||||
},
|
||||
{
|
||||
"font_class": "cloud-download-filled",
|
||||
"unicode": "\ue646"
|
||||
},
|
||||
{
|
||||
"font_class": "cloud-upload",
|
||||
"unicode": "\ue645"
|
||||
},
|
||||
{
|
||||
"font_class": "cloud-upload-filled",
|
||||
"unicode": "\ue648"
|
||||
},
|
||||
{
|
||||
"font_class": "color",
|
||||
"unicode": "\ue6cf"
|
||||
},
|
||||
{
|
||||
"font_class": "color-filled",
|
||||
"unicode": "\ue6c9"
|
||||
},
|
||||
{
|
||||
"font_class": "compose",
|
||||
"unicode": "\ue67f"
|
||||
},
|
||||
{
|
||||
"font_class": "contact",
|
||||
"unicode": "\ue693"
|
||||
},
|
||||
{
|
||||
"font_class": "contact-filled",
|
||||
"unicode": "\ue695"
|
||||
},
|
||||
{
|
||||
"font_class": "down",
|
||||
"unicode": "\ue6b8"
|
||||
},
|
||||
{
|
||||
"font_class": "bottom",
|
||||
"unicode": "\ue6b8"
|
||||
},
|
||||
{
|
||||
"font_class": "download",
|
||||
"unicode": "\ue68d"
|
||||
},
|
||||
{
|
||||
"font_class": "download-filled",
|
||||
"unicode": "\ue681"
|
||||
},
|
||||
{
|
||||
"font_class": "email",
|
||||
"unicode": "\ue69e"
|
||||
},
|
||||
{
|
||||
"font_class": "email-filled",
|
||||
"unicode": "\ue69a"
|
||||
},
|
||||
{
|
||||
"font_class": "eye",
|
||||
"unicode": "\ue651"
|
||||
},
|
||||
{
|
||||
"font_class": "eye-filled",
|
||||
"unicode": "\ue66a"
|
||||
},
|
||||
{
|
||||
"font_class": "eye-slash",
|
||||
"unicode": "\ue6b3"
|
||||
},
|
||||
{
|
||||
"font_class": "eye-slash-filled",
|
||||
"unicode": "\ue6b4"
|
||||
},
|
||||
{
|
||||
"font_class": "fire",
|
||||
"unicode": "\ue6a1"
|
||||
},
|
||||
{
|
||||
"font_class": "fire-filled",
|
||||
"unicode": "\ue6c5"
|
||||
},
|
||||
{
|
||||
"font_class": "flag",
|
||||
"unicode": "\ue65f"
|
||||
},
|
||||
{
|
||||
"font_class": "flag-filled",
|
||||
"unicode": "\ue660"
|
||||
},
|
||||
{
|
||||
"font_class": "folder-add",
|
||||
"unicode": "\ue6a9"
|
||||
},
|
||||
{
|
||||
"font_class": "folder-add-filled",
|
||||
"unicode": "\ue6c8"
|
||||
},
|
||||
{
|
||||
"font_class": "font",
|
||||
"unicode": "\ue6a3"
|
||||
},
|
||||
{
|
||||
"font_class": "forward",
|
||||
"unicode": "\ue6ba"
|
||||
},
|
||||
{
|
||||
"font_class": "gear",
|
||||
"unicode": "\ue664"
|
||||
},
|
||||
{
|
||||
"font_class": "gear-filled",
|
||||
"unicode": "\ue661"
|
||||
},
|
||||
{
|
||||
"font_class": "gift",
|
||||
"unicode": "\ue6a4"
|
||||
},
|
||||
{
|
||||
"font_class": "gift-filled",
|
||||
"unicode": "\ue6c4"
|
||||
},
|
||||
{
|
||||
"font_class": "hand-down",
|
||||
"unicode": "\ue63d"
|
||||
},
|
||||
{
|
||||
"font_class": "hand-down-filled",
|
||||
"unicode": "\ue63c"
|
||||
},
|
||||
{
|
||||
"font_class": "hand-up",
|
||||
"unicode": "\ue63f"
|
||||
},
|
||||
{
|
||||
"font_class": "hand-up-filled",
|
||||
"unicode": "\ue63e"
|
||||
},
|
||||
{
|
||||
"font_class": "headphones",
|
||||
"unicode": "\ue630"
|
||||
},
|
||||
{
|
||||
"font_class": "heart",
|
||||
"unicode": "\ue639"
|
||||
},
|
||||
{
|
||||
"font_class": "heart-filled",
|
||||
"unicode": "\ue641"
|
||||
},
|
||||
{
|
||||
"font_class": "help",
|
||||
"unicode": "\ue679"
|
||||
},
|
||||
{
|
||||
"font_class": "help-filled",
|
||||
"unicode": "\ue674"
|
||||
},
|
||||
{
|
||||
"font_class": "home",
|
||||
"unicode": "\ue662"
|
||||
},
|
||||
{
|
||||
"font_class": "home-filled",
|
||||
"unicode": "\ue663"
|
||||
},
|
||||
{
|
||||
"font_class": "image",
|
||||
"unicode": "\ue670"
|
||||
},
|
||||
{
|
||||
"font_class": "image-filled",
|
||||
"unicode": "\ue678"
|
||||
},
|
||||
{
|
||||
"font_class": "images",
|
||||
"unicode": "\ue650"
|
||||
},
|
||||
{
|
||||
"font_class": "images-filled",
|
||||
"unicode": "\ue64b"
|
||||
},
|
||||
{
|
||||
"font_class": "info",
|
||||
"unicode": "\ue669"
|
||||
},
|
||||
{
|
||||
"font_class": "info-filled",
|
||||
"unicode": "\ue649"
|
||||
},
|
||||
{
|
||||
"font_class": "left",
|
||||
"unicode": "\ue6b7"
|
||||
},
|
||||
{
|
||||
"font_class": "link",
|
||||
"unicode": "\ue6a5"
|
||||
},
|
||||
{
|
||||
"font_class": "list",
|
||||
"unicode": "\ue644"
|
||||
},
|
||||
{
|
||||
"font_class": "location",
|
||||
"unicode": "\ue6ae"
|
||||
},
|
||||
{
|
||||
"font_class": "location-filled",
|
||||
"unicode": "\ue6af"
|
||||
},
|
||||
{
|
||||
"font_class": "locked",
|
||||
"unicode": "\ue66b"
|
||||
},
|
||||
{
|
||||
"font_class": "locked-filled",
|
||||
"unicode": "\ue668"
|
||||
},
|
||||
{
|
||||
"font_class": "loop",
|
||||
"unicode": "\ue633"
|
||||
},
|
||||
{
|
||||
"font_class": "mail-open",
|
||||
"unicode": "\ue643"
|
||||
},
|
||||
{
|
||||
"font_class": "mail-open-filled",
|
||||
"unicode": "\ue63a"
|
||||
},
|
||||
{
|
||||
"font_class": "map",
|
||||
"unicode": "\ue667"
|
||||
},
|
||||
{
|
||||
"font_class": "map-filled",
|
||||
"unicode": "\ue666"
|
||||
},
|
||||
{
|
||||
"font_class": "map-pin",
|
||||
"unicode": "\ue6ad"
|
||||
},
|
||||
{
|
||||
"font_class": "map-pin-ellipse",
|
||||
"unicode": "\ue6ac"
|
||||
},
|
||||
{
|
||||
"font_class": "medal",
|
||||
"unicode": "\ue6a2"
|
||||
},
|
||||
{
|
||||
"font_class": "medal-filled",
|
||||
"unicode": "\ue6c3"
|
||||
},
|
||||
{
|
||||
"font_class": "mic",
|
||||
"unicode": "\ue671"
|
||||
},
|
||||
{
|
||||
"font_class": "mic-filled",
|
||||
"unicode": "\ue677"
|
||||
},
|
||||
{
|
||||
"font_class": "micoff",
|
||||
"unicode": "\ue67e"
|
||||
},
|
||||
{
|
||||
"font_class": "micoff-filled",
|
||||
"unicode": "\ue6b0"
|
||||
},
|
||||
{
|
||||
"font_class": "minus",
|
||||
"unicode": "\ue66f"
|
||||
},
|
||||
{
|
||||
"font_class": "minus-filled",
|
||||
"unicode": "\ue67d"
|
||||
},
|
||||
{
|
||||
"font_class": "more",
|
||||
"unicode": "\ue64d"
|
||||
},
|
||||
{
|
||||
"font_class": "more-filled",
|
||||
"unicode": "\ue64e"
|
||||
},
|
||||
{
|
||||
"font_class": "navigate",
|
||||
"unicode": "\ue66e"
|
||||
},
|
||||
{
|
||||
"font_class": "navigate-filled",
|
||||
"unicode": "\ue67a"
|
||||
},
|
||||
{
|
||||
"font_class": "notification",
|
||||
"unicode": "\ue6a6"
|
||||
},
|
||||
{
|
||||
"font_class": "notification-filled",
|
||||
"unicode": "\ue6c1"
|
||||
},
|
||||
{
|
||||
"font_class": "paperclip",
|
||||
"unicode": "\ue652"
|
||||
},
|
||||
{
|
||||
"font_class": "paperplane",
|
||||
"unicode": "\ue672"
|
||||
},
|
||||
{
|
||||
"font_class": "paperplane-filled",
|
||||
"unicode": "\ue675"
|
||||
},
|
||||
{
|
||||
"font_class": "person",
|
||||
"unicode": "\ue699"
|
||||
},
|
||||
{
|
||||
"font_class": "person-filled",
|
||||
"unicode": "\ue69d"
|
||||
},
|
||||
{
|
||||
"font_class": "personadd",
|
||||
"unicode": "\ue69f"
|
||||
},
|
||||
{
|
||||
"font_class": "personadd-filled",
|
||||
"unicode": "\ue698"
|
||||
},
|
||||
{
|
||||
"font_class": "personadd-filled-copy",
|
||||
"unicode": "\ue6d1"
|
||||
},
|
||||
{
|
||||
"font_class": "phone",
|
||||
"unicode": "\ue69c"
|
||||
},
|
||||
{
|
||||
"font_class": "phone-filled",
|
||||
"unicode": "\ue69b"
|
||||
},
|
||||
{
|
||||
"font_class": "plus",
|
||||
"unicode": "\ue676"
|
||||
},
|
||||
{
|
||||
"font_class": "plus-filled",
|
||||
"unicode": "\ue6c7"
|
||||
},
|
||||
{
|
||||
"font_class": "plusempty",
|
||||
"unicode": "\ue67b"
|
||||
},
|
||||
{
|
||||
"font_class": "pulldown",
|
||||
"unicode": "\ue632"
|
||||
},
|
||||
{
|
||||
"font_class": "pyq",
|
||||
"unicode": "\ue682"
|
||||
},
|
||||
{
|
||||
"font_class": "qq",
|
||||
"unicode": "\ue680"
|
||||
},
|
||||
{
|
||||
"font_class": "redo",
|
||||
"unicode": "\ue64a"
|
||||
},
|
||||
{
|
||||
"font_class": "redo-filled",
|
||||
"unicode": "\ue655"
|
||||
},
|
||||
{
|
||||
"font_class": "refresh",
|
||||
"unicode": "\ue657"
|
||||
},
|
||||
{
|
||||
"font_class": "refresh-filled",
|
||||
"unicode": "\ue656"
|
||||
},
|
||||
{
|
||||
"font_class": "refreshempty",
|
||||
"unicode": "\ue6bf"
|
||||
},
|
||||
{
|
||||
"font_class": "reload",
|
||||
"unicode": "\ue6b2"
|
||||
},
|
||||
{
|
||||
"font_class": "right",
|
||||
"unicode": "\ue6b5"
|
||||
},
|
||||
{
|
||||
"font_class": "scan",
|
||||
"unicode": "\ue62a"
|
||||
},
|
||||
{
|
||||
"font_class": "search",
|
||||
"unicode": "\ue654"
|
||||
},
|
||||
{
|
||||
"font_class": "settings",
|
||||
"unicode": "\ue653"
|
||||
},
|
||||
{
|
||||
"font_class": "settings-filled",
|
||||
"unicode": "\ue6ce"
|
||||
},
|
||||
{
|
||||
"font_class": "shop",
|
||||
"unicode": "\ue62f"
|
||||
},
|
||||
{
|
||||
"font_class": "shop-filled",
|
||||
"unicode": "\ue6cd"
|
||||
},
|
||||
{
|
||||
"font_class": "smallcircle",
|
||||
"unicode": "\ue67c"
|
||||
},
|
||||
{
|
||||
"font_class": "smallcircle-filled",
|
||||
"unicode": "\ue665"
|
||||
},
|
||||
{
|
||||
"font_class": "sound",
|
||||
"unicode": "\ue684"
|
||||
},
|
||||
{
|
||||
"font_class": "sound-filled",
|
||||
"unicode": "\ue686"
|
||||
},
|
||||
{
|
||||
"font_class": "spinner-cycle",
|
||||
"unicode": "\ue68a"
|
||||
},
|
||||
{
|
||||
"font_class": "staff",
|
||||
"unicode": "\ue6a7"
|
||||
},
|
||||
{
|
||||
"font_class": "staff-filled",
|
||||
"unicode": "\ue6cb"
|
||||
},
|
||||
{
|
||||
"font_class": "star",
|
||||
"unicode": "\ue688"
|
||||
},
|
||||
{
|
||||
"font_class": "star-filled",
|
||||
"unicode": "\ue68f"
|
||||
},
|
||||
{
|
||||
"font_class": "starhalf",
|
||||
"unicode": "\ue683"
|
||||
},
|
||||
{
|
||||
"font_class": "trash",
|
||||
"unicode": "\ue687"
|
||||
},
|
||||
{
|
||||
"font_class": "trash-filled",
|
||||
"unicode": "\ue685"
|
||||
},
|
||||
{
|
||||
"font_class": "tune",
|
||||
"unicode": "\ue6aa"
|
||||
},
|
||||
{
|
||||
"font_class": "tune-filled",
|
||||
"unicode": "\ue6ca"
|
||||
},
|
||||
{
|
||||
"font_class": "undo",
|
||||
"unicode": "\ue64f"
|
||||
},
|
||||
{
|
||||
"font_class": "undo-filled",
|
||||
"unicode": "\ue64c"
|
||||
},
|
||||
{
|
||||
"font_class": "up",
|
||||
"unicode": "\ue6b6"
|
||||
},
|
||||
{
|
||||
"font_class": "top",
|
||||
"unicode": "\ue6b6"
|
||||
},
|
||||
{
|
||||
"font_class": "upload",
|
||||
"unicode": "\ue690"
|
||||
},
|
||||
{
|
||||
"font_class": "upload-filled",
|
||||
"unicode": "\ue68e"
|
||||
},
|
||||
{
|
||||
"font_class": "videocam",
|
||||
"unicode": "\ue68c"
|
||||
},
|
||||
{
|
||||
"font_class": "videocam-filled",
|
||||
"unicode": "\ue689"
|
||||
},
|
||||
{
|
||||
"font_class": "vip",
|
||||
"unicode": "\ue6a8"
|
||||
},
|
||||
{
|
||||
"font_class": "vip-filled",
|
||||
"unicode": "\ue6c6"
|
||||
},
|
||||
{
|
||||
"font_class": "wallet",
|
||||
"unicode": "\ue6b1"
|
||||
},
|
||||
{
|
||||
"font_class": "wallet-filled",
|
||||
"unicode": "\ue6c2"
|
||||
},
|
||||
{
|
||||
"font_class": "weibo",
|
||||
"unicode": "\ue68b"
|
||||
},
|
||||
{
|
||||
"font_class": "weixin",
|
||||
"unicode": "\ue691"
|
||||
}
|
||||
]
|
||||
|
||||
// export const fontData = JSON.parse<IconsDataItem>(fontDataJson)
|
||||
@@ -0,0 +1,111 @@
|
||||
{
|
||||
"id": "uni-icons",
|
||||
"displayName": "uni-icons 图标",
|
||||
"version": "2.0.12",
|
||||
"description": "图标组件,用于展示移动端常见的图标,可自定义颜色、大小。",
|
||||
"keywords": [
|
||||
"uni-ui",
|
||||
"uniui",
|
||||
"icon",
|
||||
"图标"
|
||||
],
|
||||
"repository": "https://github.com/dcloudio/uni-ui",
|
||||
"engines": {
|
||||
"HBuilderX": "^3.2.14",
|
||||
"uni-app": "^4.08",
|
||||
"uni-app-x": "^4.61"
|
||||
},
|
||||
"directories": {
|
||||
"example": "../../temps/example_temps"
|
||||
},
|
||||
"dcloudext": {
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui",
|
||||
"type": "component-vue",
|
||||
"darkmode": "x",
|
||||
"i18n": "x",
|
||||
"widescreen": "x"
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [
|
||||
"uni-scss"
|
||||
],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "x",
|
||||
"aliyun": "x",
|
||||
"alipay": "x"
|
||||
},
|
||||
"client": {
|
||||
"uni-app": {
|
||||
"vue": {
|
||||
"vue2": "√",
|
||||
"vue3": "√"
|
||||
},
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"vue": "√",
|
||||
"nvue": "-",
|
||||
"android": {
|
||||
"extVersion": "",
|
||||
"minVersion": "29"
|
||||
},
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√",
|
||||
"alipay": "√",
|
||||
"toutiao": "√",
|
||||
"baidu": "√",
|
||||
"kuaishou": "-",
|
||||
"jd": "-",
|
||||
"harmony": "-",
|
||||
"qq": "√",
|
||||
"lark": "-"
|
||||
},
|
||||
"quickapp": {
|
||||
"huawei": "√",
|
||||
"union": "√"
|
||||
}
|
||||
},
|
||||
"uni-app-x": {
|
||||
"web": {
|
||||
"safari": "√",
|
||||
"chrome": "√"
|
||||
},
|
||||
"app": {
|
||||
"android": {
|
||||
"extVersion": "",
|
||||
"minVersion": "29"
|
||||
},
|
||||
"ios": "√",
|
||||
"harmony": "√"
|
||||
},
|
||||
"mp": {
|
||||
"weixin": "√"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
## Icons 图标
|
||||
> **组件名:uni-icons**
|
||||
> 代码块: `uIcons`
|
||||
|
||||
用于展示 icons 图标 。
|
||||
|
||||
### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-icons)
|
||||
#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839
|
||||
@@ -0,0 +1,8 @@
|
||||
## 1.0.3(2022-01-21)
|
||||
- 优化 组件示例
|
||||
## 1.0.2(2021-11-22)
|
||||
- 修复 / 符号在 vue 不同版本兼容问题引起的报错问题
|
||||
## 1.0.1(2021-11-22)
|
||||
- 修复 vue3中scss语法兼容问题
|
||||
## 1.0.0(2021-11-18)
|
||||
- init
|
||||
@@ -0,0 +1 @@
|
||||
@import './styles/index.scss';
|
||||
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"id": "uni-scss",
|
||||
"displayName": "uni-scss 辅助样式",
|
||||
"version": "1.0.3",
|
||||
"description": "uni-sass是uni-ui提供的一套全局样式 ,通过一些简单的类名和sass变量,实现简单的页面布局操作,比如颜色、边距、圆角等。",
|
||||
"keywords": [
|
||||
"uni-scss",
|
||||
"uni-ui",
|
||||
"辅助样式"
|
||||
],
|
||||
"repository": "https://github.com/dcloudio/uni-ui",
|
||||
"engines": {
|
||||
"HBuilderX": "^3.1.0"
|
||||
},
|
||||
"dcloudext": {
|
||||
"category": [
|
||||
"JS SDK",
|
||||
"通用 SDK"
|
||||
],
|
||||
"sale": {
|
||||
"regular": {
|
||||
"price": "0.00"
|
||||
},
|
||||
"sourcecode": {
|
||||
"price": "0.00"
|
||||
}
|
||||
},
|
||||
"contact": {
|
||||
"qq": ""
|
||||
},
|
||||
"declaration": {
|
||||
"ads": "无",
|
||||
"data": "无",
|
||||
"permissions": "无"
|
||||
},
|
||||
"npmurl": "https://www.npmjs.com/package/@dcloudio/uni-ui"
|
||||
},
|
||||
"uni_modules": {
|
||||
"dependencies": [],
|
||||
"encrypt": [],
|
||||
"platforms": {
|
||||
"cloud": {
|
||||
"tcb": "y",
|
||||
"aliyun": "y"
|
||||
},
|
||||
"client": {
|
||||
"App": {
|
||||
"app-vue": "y",
|
||||
"app-nvue": "u"
|
||||
},
|
||||
"H5-mobile": {
|
||||
"Safari": "y",
|
||||
"Android Browser": "y",
|
||||
"微信浏览器(Android)": "y",
|
||||
"QQ浏览器(Android)": "y"
|
||||
},
|
||||
"H5-pc": {
|
||||
"Chrome": "y",
|
||||
"IE": "y",
|
||||
"Edge": "y",
|
||||
"Firefox": "y",
|
||||
"Safari": "y"
|
||||
},
|
||||
"小程序": {
|
||||
"微信": "y",
|
||||
"阿里": "y",
|
||||
"百度": "y",
|
||||
"字节跳动": "y",
|
||||
"QQ": "y"
|
||||
},
|
||||
"快应用": {
|
||||
"华为": "n",
|
||||
"联盟": "n"
|
||||
},
|
||||
"Vue": {
|
||||
"vue2": "y",
|
||||
"vue3": "y"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
`uni-sass` 是 `uni-ui`提供的一套全局样式 ,通过一些简单的类名和`sass`变量,实现简单的页面布局操作,比如颜色、边距、圆角等。
|
||||
|
||||
### [查看文档](https://uniapp.dcloud.io/component/uniui/uni-sass)
|
||||
#### 如使用过程中有任何问题,或者您对uni-ui有一些好的建议,欢迎加入 uni-ui 交流群:871950839
|
||||
@@ -0,0 +1,7 @@
|
||||
@import './setting/_variables.scss';
|
||||
@import './setting/_border.scss';
|
||||
@import './setting/_color.scss';
|
||||
@import './setting/_space.scss';
|
||||
@import './setting/_radius.scss';
|
||||
@import './setting/_text.scss';
|
||||
@import './setting/_styles.scss';
|
||||
@@ -0,0 +1,3 @@
|
||||
.uni-border {
|
||||
border: 1px $uni-border-1 solid;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
|
||||
// TODO 暂时不需要 class ,需要用户使用变量实现 ,如果使用类名其实并不推荐
|
||||
// @mixin get-styles($k,$c) {
|
||||
// @if $k == size or $k == weight{
|
||||
// font-#{$k}:#{$c}
|
||||
// }@else{
|
||||
// #{$k}:#{$c}
|
||||
// }
|
||||
// }
|
||||
$uni-ui-color:(
|
||||
// 主色
|
||||
primary: $uni-primary,
|
||||
primary-disable: $uni-primary-disable,
|
||||
primary-light: $uni-primary-light,
|
||||
// 辅助色
|
||||
success: $uni-success,
|
||||
success-disable: $uni-success-disable,
|
||||
success-light: $uni-success-light,
|
||||
warning: $uni-warning,
|
||||
warning-disable: $uni-warning-disable,
|
||||
warning-light: $uni-warning-light,
|
||||
error: $uni-error,
|
||||
error-disable: $uni-error-disable,
|
||||
error-light: $uni-error-light,
|
||||
info: $uni-info,
|
||||
info-disable: $uni-info-disable,
|
||||
info-light: $uni-info-light,
|
||||
// 中性色
|
||||
main-color: $uni-main-color,
|
||||
base-color: $uni-base-color,
|
||||
secondary-color: $uni-secondary-color,
|
||||
extra-color: $uni-extra-color,
|
||||
// 背景色
|
||||
bg-color: $uni-bg-color,
|
||||
// 边框颜色
|
||||
border-1: $uni-border-1,
|
||||
border-2: $uni-border-2,
|
||||
border-3: $uni-border-3,
|
||||
border-4: $uni-border-4,
|
||||
// 黑色
|
||||
black:$uni-black,
|
||||
// 白色
|
||||
white:$uni-white,
|
||||
// 透明
|
||||
transparent:$uni-transparent
|
||||
) !default;
|
||||
@each $key, $child in $uni-ui-color {
|
||||
.uni-#{"" + $key} {
|
||||
color: $child;
|
||||
}
|
||||
.uni-#{"" + $key}-bg {
|
||||
background-color: $child;
|
||||
}
|
||||
}
|
||||
.uni-shadow-sm {
|
||||
box-shadow: $uni-shadow-sm;
|
||||
}
|
||||
.uni-shadow-base {
|
||||
box-shadow: $uni-shadow-base;
|
||||
}
|
||||
.uni-shadow-lg {
|
||||
box-shadow: $uni-shadow-lg;
|
||||
}
|
||||
.uni-mask {
|
||||
background-color:$uni-mask;
|
||||
}
|
||||