需要开发 IM

This commit is contained in:
cbb
2025-12-30 17:52:19 +08:00
parent 8fe2079446
commit d0cf491201
23 changed files with 515 additions and 61 deletions

View File

@@ -17,4 +17,11 @@ unaipp多端im+会议+积分商城
### TUILiveKit 文档地址
```https://trtc.io/zh/document/60034?platform=ios&product=live```
```https://trtc.io/zh/document/60034?platform=ios&product=live```
### 直播与语音 文档
```https://cloud.tencent.com/document/product/647/111859```
### 聊天列表
```https://cloud.tencent.com/document/product/269/105582```

View File

@@ -95,3 +95,36 @@ export const getUserIntegralRank = data => {
data
})
}
/** 签到列表 */
export const getSignList = data => {
return http({
url: '/api/system/signRecord/list',
method: 'get',
data
})
}
/** 签到 */
export const signIn = () => {
return http({
url: '/api/system/signRecord',
method: 'post'
})
}
/** 根据id获取文章详细信息 */
export const getArticleDetail = id => {
return http({
url: `/api/service/articleContent/type/${id}`,
method: 'get'
})
}
/** 获取腾讯usersig */
export const getTencentUserSig = () => {
return http({
url: '/api/user/usersig',
method: 'get'
})
}

52
api/my-index.js Normal file
View File

@@ -0,0 +1,52 @@
import http from '@/utils/request'
/** 添加银行卡 */
export const addUserAddress = data => {
return http({
url: '/api/service/userCard',
method: 'post',
data
})
}
/** 修改银行卡 */
export const updateUserAddress = data => {
return http({
url: '/api/service/userCard',
method: 'put',
data
})
}
/** 获取用户银行卡列表 */
export const getUserAddress = data => {
return http({
url: '/api/service/userCard/list',
method: 'get',
data
})
}
/** 删除银行卡 */
export const deleteUserAddress = id => {
return http({
url: `/api/service/userCard/${id}`,
method: 'delete'
})
}
/** 获取用户银行卡详情 */
export const getUserAddressDetail = id => {
return http({
url: `/api/service/userCard/details/${id}`,
method: 'get'
})
}
/** 获取用户支付密码详细信息 */
export const getUserPayPwd = () => {
return http({
url: '/api/service/userPassword/details',
method: 'get'
})
}

View File

@@ -1,4 +1,6 @@
<script setup>
import { navigateTo } from '@/utils/router'
const isShow = defineModel({
type: Boolean,
default: false
@@ -20,9 +22,13 @@
@click="isShow = false"
></image>
<text class="text">我已阅读并同意</text>
<text class="on">用户权益</text>
<text class="on" @click="navigateTo('/pages/login/protocol')">
用户权益
</text>
<text class="text"></text>
<text class="on">隐私政策</text>
<text class="on" @click="navigateTo('/pages/login/regime')">
隐私政策
</text>
</view>
</template>

View File

@@ -0,0 +1,38 @@
<script setup>
import { getArticleDetail } from '@/api'
import { onMounted, ref } from 'vue'
const props = defineProps({
/** 文章类型
* user_rights: 用户权益
* privacy_policy: 隐私政策
* company_info: 公司介绍
*/
type: {
type: String,
default: 'user_rights'
}
})
const articleDetails = ref('')
const getData = async () => {
const res = await getArticleDetail(props.type)
articleDetails.value = res.data.articleDetails
}
onMounted(() => {
getData()
})
</script>
<template>
<div class="app-article-detail">
<mp-html :content="articleDetails"></mp-html>
</div>
</template>
<style lang="scss" scoped>
.app-article-detail {
padding: 32rpx 24rpx;
}
</style>

View File

@@ -1,5 +1,10 @@
<script setup>
const props = defineProps()
const props = defineProps({
placeholder: {
type: String,
default: '请输入内容'
}
})
const placeholderStyle = `font-family: PingFang SC, PingFang SC; font-weight: 500; color: #666666; font-size: 24rpx; font-style: normal; text-transform: none;`
@@ -19,7 +24,7 @@
<input
v-model="name"
:placeholder-style="placeholderStyle"
placeholder="请输入内容"
:placeholder="props.placeholder"
class="search-box"
/>
<button class="search-btn">搜索</button>

View File

@@ -9,6 +9,11 @@
type: String,
default: ''
},
/** 是否显示左侧返回按钮 */
showBack: {
type: Boolean,
default: true
},
/** 导航栏颜色 */
targetColor: {
type: String,
@@ -39,7 +44,7 @@
const emits = defineEmits(['onBack'])
// 判断是否传入了名为 "back" 的插槽
const hasBackSlot = !!slots.back
const hasBackSlot = computed(() => !!slots.back)
/** 全局存储当前滚动位置(供 computed 使用) */
const currentScrollTop = ref(0)
/** 缓存 rpx 转 px 的比例 */
@@ -118,9 +123,10 @@
</view>
<view class="nav-bar-box">
<!-- 左侧插槽 -->
<view @click="onBack">
<view @click="props.showBack && onBack()">
<!-- -->
<image
v-if="!hasBackSlot"
v-if="props.showBack && !hasBackSlot"
src="/static/images/login/back.png"
mode="heightFix"
class="top_left-icon"

View File

@@ -10,11 +10,12 @@ export const useAuthUser = () => {
const tokenStore = useTokenStore()
// 响应式状态state & getters
const { userInfo } = storeToRefs(userStore)
const { userInfo, tencentUserSig } = storeToRefs(userStore)
const { token } = storeToRefs(tokenStore)
return {
userInfo,
tencentUserSig,
token
}
}

View File

@@ -1,4 +1,5 @@
export const STORAGE_KEYS = {
TOKEN: 'token',
USER: 'userInfo'
USER: 'userInfo',
TENCENT_USER_SIG: 'tencentUserSig' // 腾讯 IM 签名
}

View File

@@ -14,6 +14,18 @@
"navigationStyle": "custom"
}
},
{
"path": "pages/login/protocol",
"style": {
"navigationBarTitleText": "用户权益"
}
},
{
"path": "pages/login/regime",
"style": {
"navigationBarTitleText": "隐私政策"
}
},
{
"path": "pages/login/phone-register/phone-register",
"style": {
@@ -38,7 +50,8 @@
{
"path": "pages/news-list/news-list",
"style": {
"navigationBarTitleText": "消息"
"navigationBarTitleText": "消息",
"navigationStyle": "custom"
}
},
{
@@ -230,7 +243,8 @@
{
"path": "pages/discover/company",
"style": {
"navigationBarTitleText": "公司介绍"
"navigationBarTitleText": "公司介绍",
"navigationBarBackgroundColor": "#ffffff"
}
}
],
@@ -273,4 +287,4 @@
}
]
}
}
}

View File

@@ -1,18 +1,34 @@
<script setup></script>
<script setup>
import { getArticleDetail } from '@/api'
import { onLoad } from '@dcloudio/uni-app'
import { ref } from 'vue'
const formData = ref({})
const getData = async () => {
const res = await getArticleDetail('company_info')
formData.value = res.data
}
onLoad(() => {
getData()
})
</script>
<template>
<view class="company">
<view class="top-log">
<image
src="https://qcloud.dpfile.com/pc/TrdZpLN1zkXDV4oN2FH98LdVnvHj694NKQu0_KA3ul4eYxZWRPQ7CJuw-PqyZBS4.jpg"
:src="formData.articleImg"
mode="aspectFill"
class="img"
></image>
<text>名字</text>
<text>{{ formData.articleTitle }}</text>
</view>
<view class="consten">
<view class="item">富文本</view>
<view class="item">
<mp-html :content="formData.articleDetails"></mp-html>
</view>
</view>
</view>
</template>

View File

@@ -23,7 +23,9 @@ text-transform: none;`
<view class="release-box">
<nav-bar isPlaceholder>
<template #right>
<text class="right-btn" @click="onUpData">发布</text>
<text class="public-navbar__right-btn" @click="onUpData">
发布
</text>
</template>
</nav-bar>
@@ -46,18 +48,6 @@ text-transform: none;`
</template>
<style lang="scss" scoped>
.right-btn {
font-family: PingFang SC, PingFang SC;
font-weight: 500;
font-size: 28rpx;
color: #ffffff;
font-style: normal;
text-transform: none;
background: linear-gradient(0deg, #00d993 0%, #00d9c5 100%);
padding: 12rpx 36rpx;
border-radius: 8rpx;
}
.content {
padding: 32rpx 58rpx;
.textarea {

View File

@@ -1,16 +1,164 @@
<script setup>
import { ref } from 'vue'
import { formatMonthDay, formatDate } from '@/utils/dateUtils'
import { onLoad } from '@dcloudio/uni-app'
import { getSignList, signIn } from '@/api'
import { useUI } from '@/utils/use-ui'
import { useAuthUser } from '@/composables/useAuthUser'
import { useUserStore } from '@/stores/user'
const weekList = ['一', '二', '三', '四', '五', '六', '日']
const { showToast } = useUI()
const { userInfo } = useAuthUser()
const { refreshUserInfo } = useUserStore()
const indexData = ref([1, 2, 3, 4])
const weekList = [
{ name: '一', value: 1 },
{ name: '二', value: 2 },
{ name: '三', value: 3 },
{ name: '四', value: 4 },
{ name: '五', value: 5 },
{ name: '六', value: 6 },
{ name: '日', value: 7 }
]
const indexData = ref([])
const onItem = item => {
if (indexData.value.includes(item)) {
return
const currentYear = ref(2025)
const currentMonth = ref(12)
/** 日期列表 */
const dateList = ref([])
/** 连续天数 */
const continuousDays = ref(0)
const getData = async msg => {
const res = await getSignList({
signMonth: `${currentYear.value}-${currentMonth.value}`
})
dateList.value = generateCalendar(
currentYear.value,
currentMonth.value
)
if (msg) {
await refreshUserInfo()
showToast(msg)
}
indexData.value.push(item)
continuousDays.value = res.data.continuousDays
indexData.value = res.data.list.map(v => v.signDate)
}
/**
* 生成一个 5x7 的日历数组(从周一到周日)
* @param {number} year - 当前年份
* @param {number} month - 当前月份1-12
* @returns {Array<{ date: Date, type: 'prev' | 'current' | 'next' }>}
*/
const generateCalendar = (year, month) => {
const today = new Date()
const todayYear = today.getFullYear()
const todayMonth = today.getMonth()
const todayDate = today.getDate()
// 辅助函数:判断两个 Date 是否是同一天(忽略时分秒)
const isSameDay = (d1, d2) => {
return (
d1.getFullYear() === d2.getFullYear() &&
d1.getMonth() === d2.getMonth() &&
d1.getDate() === d2.getDate()
)
}
// 创建当前月的第一天
const firstDay = new Date(year, month - 1, 1)
// 创建当前月的最后一天
const lastDay = new Date(year, month, 0)
// 获取当前月第一天是星期几
let startDayOfWeek = firstDay.getDay()
// 转换为以周一为 0如果 getDay() 是 0周日则视为 6即一周最后一天
if (startDayOfWeek === 0) startDayOfWeek = 7
const offsetFromMonday = startDayOfWeek - 1 // 周一为0
// 上个月的最后一天
const prevMonthLastDay = new Date(year, month - 1, 0).getDate()
// 当前月总天数
const currentMonthDays = lastDay.getDate()
const calendar = []
// 仅用于比较
const todayRef = new Date(todayYear, todayMonth, todayDate)
// 填充上个月的尾几天
for (let i = 0; i < offsetFromMonday; i++) {
const day = prevMonthLastDay - offsetFromMonday + 1 + i
const date = new Date(year, month - 2, day) // month - 2 表示上个月
calendar.push({
date: formatMonthDay(date),
signDate: formatDate(date),
type: 'prev',
isToday: isSameDay(date, todayRef)
})
}
// 填充当前月
for (let i = 0; i < currentMonthDays; i++) {
const date = new Date(year, month - 1, i + 1)
calendar.push({
date: formatMonthDay(date),
signDate: formatDate(date),
type: 'current',
isToday: isSameDay(date, todayRef)
})
}
// 填充下个月的前几天,使总数达到 355*7
const remaining = 35 - calendar.length
for (let i = 1; i <= remaining; i++) {
const date = new Date(year, month, i) // month 是下个月(因为 JS 月份从 0 开始)
calendar.push({
date: formatMonthDay(date),
signDate: formatDate(date),
type: 'next',
isToday: isSameDay(date, todayRef)
})
}
return calendar
}
/** 点击签到 */
const onItem = async item => {
if (!item.isToday) return
const res = await signIn()
await getData(res.data.message)
}
/** 日期切换 */
const dateChange = e => {
if (e === 0) {
if (currentMonth.value === 1) {
currentYear.value--
currentMonth.value = 12
} else {
currentMonth.value--
}
} else {
if (currentMonth.value === 12) {
currentYear.value++
currentMonth.value = 1
} else {
currentMonth.value++
}
}
getData()
}
onLoad(() => {
getData()
})
</script>
<template>
@@ -27,7 +175,7 @@
<view class="public-header—box">
<view class="integral-box">
<text>我的积分</text>
<text>410</text>
<text>{{ userInfo.totalPoints }}</text>
</view>
<image
src="/static/images/discover/calendar.png"
@@ -41,28 +189,40 @@
<text class="title">每日签到领积分</text>
<view class="right-box">
<text>已连续签到</text>
<text>05</text>
<text>{{ continuousDays }}</text>
<text></text>
</view>
</view>
<!-- 切换 -->
<view class="switch-box">
<text class="date">2025年12</text>
<text class="date">{{ currentYear }}{{ currentMonth }}</text>
<view class="btn">
<uni-icons type="left" size="22" color="#666666"></uni-icons>
<uni-icons type="right" size="22" color="#666666"></uni-icons>
<uni-icons
type="left"
size="22"
color="#666666"
@click="dateChange(0)"
></uni-icons>
<uni-icons
type="right"
size="22"
color="#666666"
@click="dateChange(1)"
></uni-icons>
</view>
</view>
<!-- 签到列表 -->
<view class="list-box">
<!-- 星期列表 -->
<view v-for="(item, index) in weekList" :key="index" class="item">
<text class="bottom-name">{{ item }}</text>
<text class="bottom-name">{{ item.name }}</text>
</view>
<!-- 签到 -->
<view
v-for="item in 30"
:key="item"
:class="{ active: indexData.includes(item) }"
v-for="(item, index) in dateList"
:key="index"
:class="{ active: indexData.includes(item.signDate) }"
class="item"
@click="onItem(item)"
>
@@ -72,9 +232,14 @@
mode="heightFix"
class="icon"
></image>
<text>+10</text>
<!-- <text>+10</text> -->
</view>
<text class="bottom-name">今天</text>
<text
:style="{ color: item.isToday ? '#00d993' : '#999999' }"
class="bottom-name"
>
{{ item.isToday ? '今日' : item.date }}
</text>
</view>
</view>
</view>

9
pages/login/protocol.vue Normal file
View File

@@ -0,0 +1,9 @@
<script setup></script>
<template>
<view class="protocol-box">
<app-article-detail type="user_rights"></app-article-detail>
</view>
</template>
<style lang="scss" scoped></style>

9
pages/login/regime.vue Normal file
View File

@@ -0,0 +1,9 @@
<script setup></script>
<template>
<view class="regime-box">
<app-article-detail type="privacy_policy"></app-article-detail>
</view>
</template>
<style lang="scss" scoped></style>

View File

@@ -138,7 +138,10 @@
<!-- 商品详情 -->
<view class="detail-content">
<text class="title">商品详情</text>
<mp-html :content="viewData.description" class="rich-box" />
<mp-html
:content="viewData.description"
class="rich-box"
></mp-html>
</view>
</view>

View File

@@ -20,6 +20,16 @@
// 图片链接
img: ''
})
const onAdd = () => {
console.log(formData)
const data = {
bankName: formData.name,
cardNumber: formData.cardNum,
bankName: formData.khName
}
}
onLoad(e => {
const titltData = {
0: '添加银行卡',
@@ -33,7 +43,11 @@
<template>
<view>
<nav-bar isTopBg isPlaceholder :title="stateData.title"></nav-bar>
<nav-bar isTopBg isPlaceholder :title="stateData.title">
<template #right>
<text class="public-navbar__right-btn" @click="onAdd">添加</text>
</template>
</nav-bar>
<view
v-if="!['101', '102'].includes(stateData.state)"

View File

@@ -1,5 +1,7 @@
<script setup>
import { navigateTo } from '@/utils/router'
import { onShow } from '@dcloudio/uni-app'
import { getUserPayPwd } from '@/api/my-index'
const itemList = [
{
@@ -15,6 +17,15 @@
},
{ title: '实名认证', key: '4', url: '/pages/my-index/wallet/real-id' }
]
const getData = async () => {
const res = await getUserPayPwd()
console.log(res, '===')
}
onShow(() => {
getData()
})
</script>
<template>

View File

@@ -1,13 +1,21 @@
<script setup>
import { onLoad } from '@dcloudio/uni-app'
import { useAuthUser } from '@/composables/useAuthUser'
const { userInfo } = useAuthUser()
console.log(userInfo.value, '====use22rInfo===')
const { tencentUserSig } = useAuthUser()
onLoad(() => {
console.log(tencentUserSig.value, '===222===')
})
</script>
<template>
<view class="news-list">消息列表</view>
<view class="news-list">
<nav-bar isTopBg isPlaceholder :showBack="false" title="消息(99+)">
<template #right>右侧按钮</template>
</nav-bar>
<cb-search placeholder="搜索"></cb-search>
消息列表
</view>
</template>
<style lang="scss" scoped></style>

View File

@@ -3,13 +3,17 @@ import {
getToken,
getUserInfoData,
setUserInfoData,
removeUserInfoData
removeUserInfoData,
getSig,
setSig,
removeSig
} from '@/utils/storage'
import { useTokenStore } from './token'
import { getUserData, userLogout } from '@/api'
import { ref } from 'vue'
import { useUI } from '@/utils/use-ui'
import { reLaunch } from '@/utils/router'
import { getTencentUserSig } from '@/api'
export const useUserStore = defineStore('user', () => {
const { clearToken } = useTokenStore()
@@ -18,8 +22,8 @@ export const useUserStore = defineStore('user', () => {
const userInfo = ref(
getUserInfoData() ? JSON?.parse(getUserInfoData()) : {}
)
/** 用户信息对象 */
/** 腾讯 IM 存储数据 */
const tencentUserSig = ref(getSig() ? JSON?.parse(getSig()) : {})
/**
* 获取用户信息(可从缓存或接口)
@@ -28,23 +32,27 @@ export const useUserStore = defineStore('user', () => {
// 尝试从本地缓存读取
const cachedToken = getToken()
const cachedUserInfo = getUserInfoData()
const cachedSig = getSig()
if (cachedToken && cachedUserInfo) {
userInfo.value = JSON.parse(cachedUserInfo)
tencentUserSig.value = JSON.parse(cachedSig)
return
}
const res = await getUserData()
setUserInfo(res.data)
await setUserInfo(res.data)
return
}
/**
* 设置用户信息
*/
const setUserInfo = data => {
const setUserInfo = async data => {
const res = await getTencentUserSig()
tencentUserSig.value = res.data
userInfo.value = data
// 同步到本地存储
setUserInfoData(data)
setSig(res.data)
}
/**
@@ -57,6 +65,7 @@ export const useUserStore = defineStore('user', () => {
userInfo.value = null
clearToken()
removeUserInfoData()
removeSig()
reLaunch('/pages/login/login')
}
}
@@ -64,7 +73,7 @@ export const useUserStore = defineStore('user', () => {
/** 刷新用户信息(如用户信息被修改) */
const refreshUserInfo = async () => {
const res = await getUserData()
setUserInfo(res.data)
await setUserInfo(res.data)
}
/**
@@ -78,6 +87,7 @@ export const useUserStore = defineStore('user', () => {
return {
userInfo,
tencentUserSig,
refreshUserInfo,
fetchUserInfo,
setUserInfo,

View File

@@ -2,3 +2,16 @@
page {
background-color: #fff;
}
/** 顶部导航栏右侧按钮样式 */
.public-navbar__right-btn {
font-family: PingFang SC, PingFang SC;
font-weight: 500;
font-size: 28rpx;
color: #ffffff;
font-style: normal;
text-transform: none;
background: linear-gradient(0deg, #00d993 0%, #00d9c5 100%);
padding: 12rpx 36rpx;
border-radius: 8rpx;
}

25
utils/dateUtils.js Normal file
View File

@@ -0,0 +1,25 @@
/** 日期格式化 YYYY-MM-DD */
export const formatDate = date => {
const d = new Date(date)
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0') // 月份从0开始
const day = String(d.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
/** 日期格式化:月.日 */
export const formatMonthDay = date => {
// 统一处理为 Date 对象
const d = new Date(date)
// 检查是否是有效日期
if (isNaN(d.getTime())) {
console.error('Invalid date:', date)
return '--.--'
}
const month = String(d.getMonth() + 1).padStart(2, '0') // getMonth() 是 0-11
const day = String(d.getDate()).padStart(2, '0')
return `${month}.${day}`
}

View File

@@ -17,7 +17,7 @@ export const removeToken = () => {
/** 保存用户信息 */
export const setUserInfoData = v => {
return uni.setStorageSync(STORAGE_KEYS.USER, JSON.stringify(v))
return uni.setStorageSync(STORAGE_KEYS.USER, JSON.stringify(v))
}
/** 获取用户信息 */
@@ -29,3 +29,21 @@ export const getUserInfoData = () => {
export const removeUserInfoData = () => {
return uni.removeStorageSync(STORAGE_KEYS.USER)
}
/** 保存腾讯 IM */
export const setSig = v => {
return uni.setStorageSync(
STORAGE_KEYS.TENCENT_USER_SIG,
JSON.stringify(v)
)
}
/** 获取腾讯 IM */
export const getSig = () => {
return uni.getStorageSync(STORAGE_KEYS.TENCENT_USER_SIG) || ''
}
/** 删除腾讯 IM */
export const removeSig = () => {
return uni.removeStorageSync(STORAGE_KEYS.TENCENT_USER_SIG)
}