chore: 重写初始提交(清空历史,整理后全量提交)

This commit is contained in:
ywxapp
2026-08-16 16:54:14 +08:00
commit 6c1a106bc1
1808 changed files with 238144 additions and 0 deletions
@@ -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>
File diff suppressed because it is too large Load Diff
@@ -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>