Merge branch 'feat-scene' into test

This commit is contained in:
2026-08-20 14:37:47 +08:00
38 changed files with 1786 additions and 37 deletions
+56 -2
View File
@@ -26,8 +26,9 @@ try {
} }
const ADDONS_BASE_URL = BASE_URL.replace(/\/api\/shoot$/, "/api/shoot"); const ADDONS_BASE_URL = BASE_URL.replace(/\/api\/shoot$/, "/api/shoot");
const API_ROOT_URL = BASE_URL.replace(/\/api\/shoot$/, "");
// 统一处理业务接口请求,包含登录态、业务错误和特定接口空响应兼容。 // 统一处理业务接口请求,包含登录态、业务错误和特定接口空响应兼容。
function request(method, url, data = {}, baseUrl = BASE_URL) { function request(method, url, data = {}, baseUrl = BASE_URL, successCodes = [0]) {
const token = uni.getStorageSync( const token = uni.getStorageSync(
`${uni.getAccountInfoSync().miniProgram.envVersion}_token` `${uni.getAccountInfoSync().miniProgram.envVersion}_token`
); );
@@ -51,7 +52,7 @@ function request(method, url, data = {}, baseUrl = BASE_URL) {
} }
if (res.data) { if (res.data) {
const {code, data, message} = res.data; const {code, data, message} = res.data;
if (code === 0) resolve(data); if (successCodes.includes(code)) resolve(data);
else if (message) { else if (message) {
const error = {code, data, message}; const error = {code, data, message};
if (message.indexOf("登录身份已失效") !== -1) { if (message.indexOf("登录身份已失效") !== -1) {
@@ -698,3 +699,56 @@ export const getMyTenRingRank = (seasonId) => {
if (seasonId !== undefined && seasonId !== null) data.seasonId = seasonId; if (seasonId !== undefined && seasonId !== null) data.seasonId = seasonId;
return request("GET", "/index/myTenRingRank", data); return request("GET", "/index/myTenRingRank", data);
}; };
// 获取当前用户的金币统计,可按门店查询。
export const getMyGoldAPI = (storeId) => {
const data = {};
if (storeId !== undefined && storeId !== null) data.storeId = storeId;
return request("GET", "/index/gold/my", data);
};
// 分页获取当前用户的金币流水,type:1=获得,2=兑换。
export const getGoldLogAPI = ({page = 1, pageSize = 20, type, storeId} = {}) => {
const data = {page, pageSize};
if (type !== undefined && type !== null) data.type = type;
if (storeId !== undefined && storeId !== null) data.storeId = storeId;
return request("GET", "/index/gold/log", data);
};
// 前台礼品接口位于站点根路径,并使用 code=200 表示成功。
export const getGiftListAPI = ({page = 1, pageSize = 20, sort = "coin_desc"} = {}) => {
return request(
"GET",
"/gin/api/v1/gift/list",
{page, page_size: pageSize, sort},
API_ROOT_URL,
[0, 200]
);
};
export const getGiftDetailAPI = (id) => {
return request(
"GET",
`/gin/api/v1/gift/${id}`,
{},
API_ROOT_URL,
[0, 200]
);
};
// 根据用户定位分页获取附近门店。
export const getNearbyStoresAPI = ({
longitude,
latitude,
radius = 65535,
page = 1,
pageSize = 20,
} = {}) => {
return request("GET", "/store/nearby", {
longitude,
latitude,
radius,
page,
pageSize,
});
};
+6 -2
View File
@@ -9,6 +9,7 @@ import DeviceChargingDialog from "@/components/DeviceChargingDialog.vue";
import {laserAimAPI, getBattleAPI, matchGameAPI} from "@/apis"; import {laserAimAPI, getBattleAPI, matchGameAPI} from "@/apis";
import { capsuleHeight, debounce } from "@/util"; import { capsuleHeight, debounce } from "@/util";
import { returnToBattle } from "@/utils/matchReturn"; import { returnToBattle } from "@/utils/matchReturn";
const emit = defineEmits(["scrolltolower"]);
const props = defineProps({ const props = defineProps({
title: { title: {
type: String, type: String,
@@ -131,8 +132,9 @@ const goCalibration = async () => {
<template> <template>
<view :style="{ paddingTop: capsuleHeight + 'px' }"> <view :style="{ paddingTop: capsuleHeight + 'px' }">
<AppBackground :type="bgType" :bgColor="bgColor" /> <AppBackground :type="bgType" :bgColor="bgColor" />
<slot v-if="$slots.header" name="header"></slot>
<Header <Header
v-if="!isHome" v-else-if="!isHome"
:class="headerClass" :class="headerClass"
:title="title" :title="title"
:onBack="onBack" :onBack="onBack"
@@ -158,8 +160,10 @@ const goCalibration = async () => {
:enhanced="true" :enhanced="true"
:bounces="false" :bounces="false"
:show-scrollbar="false" :show-scrollbar="false"
:lower-threshold="120"
@scrolltolower="emit('scrolltolower')"
:style="{ :style="{
height: `calc(100vh - ${capsuleHeight + (isHome ? 0 : 50)}px - ${ height: `calc(100vh - ${capsuleHeight + (($slots.header || !isHome) ? 50 : 0)}px - ${
$slots.bottom && showBottom ? (isIOS ? '75px' : '65px') : '0px' $slots.bottom && showBottom ? (isIOS ? '75px' : '65px') : '0px'
})`, })`,
}" }"
+3
View File
@@ -125,6 +125,8 @@ onBeforeUnmount(() => {
<view <view
:style="[{ color: whiteBackArrow ? '#fff' : '#000' }, titleStyle]" :style="[{ color: whiteBackArrow ? '#fff' : '#000' }, titleStyle]"
> >
<slot v-if="$slots.title" name="title"></slot>
<template v-else>
<view <view
v-if="currentPage === 'pages/point-book'" v-if="currentPage === 'pages/point-book'"
class="user-header" class="user-header"
@@ -190,6 +192,7 @@ onBeforeUnmount(() => {
> >
</view> </view>
</block> </block>
</template>
</view> </view>
<view v-if="pointBook" class="point-book-info"> <view v-if="pointBook" class="point-book-info">
<text>{{ pointBook.bowType.name }}</text> <text>{{ pointBook.bowType.name }}</text>
+23
View File
@@ -133,6 +133,29 @@
} }
}, },
"subPackages": [ "subPackages": [
{
"root": "pages/coin",
"pages": [
{
"path": "index"
},
{
"path": "rules"
},
{
"path": "earning-records"
},
{
"path": "exchange-records"
},
{
"path": "nearby-stores"
},
{
"path": "product-detail"
}
]
},
{ {
"root": "pages/device", "root": "pages/device",
"pages": [ "pages": [
@@ -0,0 +1,76 @@
<script setup>
defineProps({
cumulative: {
type: [Number, String],
default: 0,
},
available: {
type: [Number, String],
default: 0,
},
});
</script>
<template>
<view class="balance-panel">
<view class="balance-list">
<view class="balance-item">
<text class="balance-item__label">累计金币</text>
<text class="balance-item__value">{{ cumulative }}</text>
</view>
<view class="balance-list__line" />
<view class="balance-item">
<text class="balance-item__label">可兑换金币</text>
<text class="balance-item__value">{{ available }}</text>
</view>
</view>
</view>
</template>
<style scoped>
.balance-panel {
width: 100%;
padding: 8rpx 28rpx 0;
box-sizing: border-box;
}
.balance-list {
display: flex;
align-items: center;
justify-content: center;
width: 706rpx;
height: 60rpx;
margin-top: 0;
border: 2rpx solid rgba(255, 217, 71, 0.25);
border-radius: 12rpx;
box-sizing: border-box;
background-color: rgba(255, 217, 71, 0.06);
}
.balance-item {
width: auto;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
.balance-item__label {
color: #ffffff;
font-size: 24rpx;
line-height: 34rpx;
}
.balance-item__value {
color: #ffd947;
font-size: 30rpx;
line-height: 42rpx;
}
.balance-list__line {
width: 2rpx;
height: 28rpx;
background-color: rgba(255, 255, 255, 0.5);
margin: 0 22rpx;
}
</style>
@@ -0,0 +1,34 @@
<script setup>
defineProps({
text: {
type: String,
default: "暂无金币获取记录。",
},
});
</script>
<template>
<view class="empty-state">
<image src="/static/coin/empty-coin-record.png" mode="aspectFit" />
<text>{{ text }}</text>
</view>
</template>
<style scoped>
.empty-state {
width: 100%;
padding-top: 280rpx;
display: flex;
flex-direction: column;
align-items: center;
color: #ffffff;
font-size: 26rpx;
line-height: 36rpx;
}
.empty-state > image {
width: 162rpx;
height: 190rpx;
margin-bottom: 26rpx;
}
</style>
+86
View File
@@ -0,0 +1,86 @@
<script setup>
import Header from "@/components/Header.vue";
const props = defineProps({
title: {
type: String,
default: "",
},
subtitle: {
type: String,
default: "",
},
onBack: {
type: Function,
default: null,
},
});
</script>
<template>
<view class="coin-header">
<Header title="" :onBack="onBack">
<template #title>
<view class="coin-header__title-group">
<text
:class="[
'coin-header__title',
subtitle ? 'coin-header__title--with-subtitle' : '',
]"
>
{{ title }}
</text>
<text v-if="subtitle" class="coin-header__subtitle">
{{ subtitle }}
</text>
</view>
</template>
</Header>
</view>
</template>
<style scoped>
.coin-header {
position: sticky;
top: 0;
z-index: 20;
width: 100%;
height: 50px;
display: flex;
align-items: center;
flex-shrink: 0;
}
.coin-header__title-group {
position: absolute;
top: 50%;
left: 50%;
width: 430rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
transform: translate(-50%, -50%);
text-align: center;
}
.coin-header__title {
color: #e7ba80;
font-size: 30rpx;
line-height: 42rpx;
font-weight: 500;
white-space: nowrap;
}
.coin-header__title--with-subtitle {
font-size: 28rpx;
}
.coin-header__subtitle {
color: #ffffff;
font-size: 20rpx;
line-height: 28rpx;
font-weight: 400;
white-space: nowrap;
}
</style>
@@ -0,0 +1,50 @@
<script setup>
defineProps({
menus: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(["select"]);
</script>
<template>
<view class="quick-menu">
<view
v-for="item in menus"
:key="item.key"
class="quick-menu__item"
@click="emit('select', item)"
>
<image class="quick-menu__icon" :src="item.icon" mode="aspectFit" />
<text>{{ item.label }}</text>
</view>
</view>
</template>
<style scoped>
.quick-menu {
display: flex;
align-items: flex-start;
justify-content: space-between;
width: 100%;
padding: 26rpx 46rpx 30rpx;
box-sizing: border-box;
}
.quick-menu__item {
width: 142rpx;
display: flex;
flex-direction: column;
align-items: center;
color: #fae6bc;
font-size: 24rpx;
line-height: 34rpx;
}
.quick-menu__icon {
width: 116rpx;
height: 116rpx;
}
</style>
@@ -0,0 +1,188 @@
<script setup>
defineProps({
records: {
type: Array,
default: () => [],
},
mode: {
type: String,
default: "earning",
},
});
</script>
<template>
<view
class="record-table"
:class="{ 'record-table--exchange': mode === 'exchange' }"
>
<view class="record-table__header">
<template v-if="mode === 'exchange'">
<text>兑换内容</text>
<text>兑换类型</text>
<text>时间</text>
<text>金币使用</text>
</template>
<template v-else>
<text>时间</text>
<text>类型</text>
<text>金币获取</text>
</template>
</view>
<view
v-for="item in records"
:key="item.id"
class="record-table__row"
>
<template v-if="mode === 'exchange'">
<text class="record-table__exchange-content">{{ item.content }}</text>
<text class="record-table__exchange-type">{{ item.type }}</text>
<view class="record-table__time record-table__exchange-time">
<text>{{ item.date }}</text>
<text>{{ item.time }}</text>
</view>
<text class="record-table__amount record-table__exchange-amount">
{{ item.amount }}
</text>
</template>
<template v-else>
<view class="record-table__time">
<text>{{ item.date }}</text>
<text>{{ item.time }}</text>
</view>
<text class="record-table__type">{{ item.type }}</text>
<text class="record-table__amount">{{ item.amount }}</text>
</template>
</view>
</view>
</template>
<style scoped>
.record-table {
width: 670rpx;
margin: 20rpx auto 0;
box-sizing: border-box;
border: 2rpx solid rgba(255, 255, 255, 0.35);
}
.record-table__header,
.record-table__row {
display: flex;
align-items: center;
}
.record-table__header {
height: 66rpx;
color: #ffffff;
font-size: 24rpx;
line-height: 34rpx;
}
.record-table__header > text {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
border-right: 2rpx solid rgba(255, 255, 255, 0.35);
}
.record-table__header > text:nth-child(1),
.record-table__time {
width: 182rpx;
flex: 0 0 182rpx;
}
.record-table__header > text:nth-child(2),
.record-table__type {
width: 304rpx;
flex: 0 0 304rpx;
}
.record-table__header > text:nth-child(3),
.record-table__amount {
width: 180rpx;
flex: 0 0 180rpx;
border-right: none;
}
.record-table__row {
height: 86rpx;
border-top: 2rpx solid rgba(255, 255, 255, 0.35);
color: #ffffff;
font-size: 24rpx;
line-height: 34rpx;
}
.record-table__row > view,
.record-table__row > text {
height: 100%;
box-sizing: border-box;
border-right: 2rpx solid rgba(255, 255, 255, 0.35);
}
.record-table__time {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.record-table__time > text:last-child {
color: #ffffff;
font-size: 24rpx;
line-height: 34rpx;
}
.record-table__type {
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
}
.record-table__row > .record-table__amount {
display: flex;
align-items: center;
justify-content: center;
color: #ffffff;
text-align: center;
font-size: 24rpx;
border-right: none;
}
.record-table__exchange-content,
.record-table__exchange-type {
display: flex;
align-items: center;
justify-content: center;
padding: 0 8rpx;
color: #ffffff;
text-align: center;
}
.record-table--exchange .record-table__header > text:nth-child(1),
.record-table__exchange-content {
width: 286rpx;
flex: 0 0 286rpx;
}
.record-table--exchange .record-table__header > text:nth-child(2),
.record-table__exchange-type {
width: 110rpx;
flex: 0 0 110rpx;
}
.record-table--exchange .record-table__header > text:nth-child(3),
.record-table__exchange-time {
width: 160rpx;
flex: 0 0 160rpx;
}
.record-table--exchange .record-table__header > text:nth-child(4),
.record-table__exchange-amount {
width: 110rpx;
flex: 0 0 110rpx;
border-right: none;
}
</style>
@@ -0,0 +1,51 @@
<script setup>
const emit = defineEmits(["authorize"]);
</script>
<template>
<view class="location-state">
<image src="/static/coin/location-permission.png" mode="aspectFit" />
<text>授权获取你的定位以便推荐附近门店</text>
<button hover-class="none" @click="emit('authorize')">立即授权</button>
</view>
</template>
<style scoped>
.location-state {
width: 100%;
padding-top: 190rpx;
display: flex;
flex-direction: column;
align-items: center;
color: #ffffff;
font-size: 32rpx;
line-height: 44rpx;
font-weight: 600;
}
.location-state > image {
width: 168rpx;
height: 190rpx;
margin-bottom: 52rpx;
}
.location-state > button {
width: 360rpx;
height: 72rpx;
margin-top: 44rpx;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
border-radius: 36rpx;
background-color: #ffd947;
color: #22222e;
font-size: 26rpx;
line-height: 72rpx;
font-weight: 500;
}
.location-state > button::after {
border: none;
}
</style>
@@ -0,0 +1,53 @@
<script setup>
defineProps({
variant: {
type: String,
default: "gold",
},
});
</script>
<template>
<view class="exchange-notice" :class="`exchange-notice--${variant}`">
<image src="/static/coin/icon-notice.png" mode="aspectFit" />
<text>暂不支持线上兑换请前往线下门店进行兑换</text>
</view>
</template>
<style scoped>
.exchange-notice {
display: flex;
align-items: center;
justify-content: center;
width: 504rpx;
height: 40rpx;
margin: 0 auto;
padding: 0 16rpx;
box-sizing: border-box;
color: rgba(255, 255, 255, 0.65);
font-size: 20rpx;
line-height: 28rpx;
border-radius: 24rpx;
}
.exchange-notice--gold {
border: 2rpx solid rgba(255, 217, 71, 0.28);
background-color: rgba(255, 217, 71, 0.05);
}
.exchange-notice--red {
width: 492rpx;
height: 44rpx;
margin: 0 0 0 40rpx;
border: none;
background-color: rgba(255, 96, 96, 0.3);
color: #ffffff;
}
.exchange-notice > image {
width: 24rpx;
height: 24rpx;
margin-right: 8rpx;
flex-shrink: 0;
}
</style>
@@ -0,0 +1,89 @@
<script setup>
import { computed, ref } from "vue";
const DEFAULT_PRODUCT_IMAGE = "/static/coin/product-hero-item.png";
const props = defineProps({
images: {
type: Array,
default: () => [],
},
});
const current = ref(0);
const slideImages = computed(() => {
const images = props.images.filter(
(image) => typeof image === "string" && image.trim()
);
return images.length ? images : [DEFAULT_PRODUCT_IMAGE];
});
const onChange = (event) => {
current.value = event.detail.current;
};
</script>
<template>
<view class="hero-swiper">
<swiper class="hero-swiper__body" :duration="260" @change="onChange">
<swiper-item v-for="(image, index) in slideImages" :key="index">
<view class="hero-swiper__item">
<image
class="hero-swiper__background"
src="/static/coin/product-hero-bg.png"
mode="scaleToFill"
/>
<image class="hero-swiper__product" :src="image" mode="aspectFit" />
</view>
</swiper-item>
</swiper>
<view class="hero-swiper__dots">
<view
v-for="(_, index) in slideImages"
:key="index"
class="hero-swiper__dot"
:class="{ 'hero-swiper__dot--active': current === index }"
/>
</view>
</view>
</template>
<style scoped>
.hero-swiper,
.hero-swiper__body,
.hero-swiper__item {
position: relative;
width: 750rpx;
height: 750rpx;
}
.hero-swiper__background,
.hero-swiper__product {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
}
.hero-swiper__dots {
position: absolute;
left: 0;
bottom: 20rpx;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.hero-swiper__dot {
width: 12rpx;
height: 12rpx;
margin: 0 6rpx;
border-radius: 50%;
background-color: rgba(34, 34, 46, 0.55);
}
.hero-swiper__dot--active {
background-color: #ffd947;
}
</style>
@@ -0,0 +1,96 @@
<script setup>
import { computed } from "vue";
const DEFAULT_PRODUCT_IMAGE = "/static/coin/product-card-item.png";
const props = defineProps({
product: {
type: Object,
required: true,
},
});
const productImage = computed(() => {
const image = props.product?.image;
return typeof image === "string" && image.trim()
? image
: DEFAULT_PRODUCT_IMAGE;
});
</script>
<template>
<view class="product-card">
<view class="product-card__image-wrap">
<image class="product-card__image" :src="productImage" mode="aspectFit" />
</view>
<text class="product-card__name">{{ product.name }}</text>
<view class="product-card__footer">
<text>{{ product.cost }}金币</text>
<text class="product-card__divider">|</text>
<text>剩余{{ product.stock }}</text>
</view>
</view>
</template>
<style scoped>
.product-card {
width: 336rpx;
height: 418rpx;
padding: 10rpx 10rpx 14rpx;
box-sizing: border-box;
border: 2rpx solid rgba(255, 217, 71, 0.1);
border-radius: 12rpx;
background-color: rgba(84, 67, 29, 0.2);
overflow: hidden;
}
.product-card__image-wrap {
position: relative;
width: 312rpx;
height: 312rpx;
overflow: hidden;
border-radius: 12rpx;
}
.product-card__image {
display: block;
width: 100%;
height: 100%;
}
.product-card__name {
display: block;
margin-top: 12rpx;
color: #fff0c9;
font-size: 26rpx;
line-height: 36rpx;
font-weight: 600;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.product-card__footer {
display: flex;
align-items: center;
justify-content: center;
margin-top: 4rpx;
color: rgba(255, 255, 255, 0.85);
font-size: 22rpx;
line-height: 32rpx;
}
.product-card__footer > text:first-child {
color: #ffffff;
}
.product-card__footer > text:last-child {
color: rgba(255, 255, 255, 0.78);
}
.product-card__divider {
margin: 0 10rpx;
color: rgba(255, 255, 255, 0.45);
}
</style>
+63
View File
@@ -0,0 +1,63 @@
<script setup>
defineProps({
store: {
type: Object,
required: true,
},
});
</script>
<template>
<view class="store-card">
<image class="store-card__photo" :src="store.image" mode="aspectFill" />
<text class="store-card__name">{{ store.name }}</text>
<view class="store-card__info">
<view class="store-card__row">
<text>地址{{ store.address }}</text>
</view>
<view class="store-card__row">
<text>电话{{ store.phone }}</text>
</view>
<view class="store-card__row">
<text>营业时间{{ store.hours }}</text>
</view>
</view>
</view>
</template>
<style scoped>
.store-card {
width: 650rpx;
margin: 22rpx auto 0;
box-sizing: border-box;
}
.store-card__photo {
width: 650rpx;
height: 324rpx;
border-radius: 20rpx;
}
.store-card__name {
display: block;
margin-top: 24rpx;
margin-left: 10rpx;
color: #ffffff;
font-size: 32rpx;
line-height: 44rpx;
font-weight: 500;
}
.store-card__info {
width: 584rpx;
margin-top: 12rpx;
margin-left: 10rpx;
}
.store-card__row {
margin-top: 0;
color: #ffffff;
font-size: 24rpx;
line-height: 40rpx;
}
</style>
+30
View File
@@ -0,0 +1,30 @@
export const quickMenus = [
{
key: "rules",
label: "金币规则",
icon: "/static/coin/icon-rules.png",
},
{
key: "earningRecords",
label: "获取明细",
icon: "/static/coin/icon-earning-records.png",
},
{
key: "exchangeRecords",
label: "兑换记录",
icon: "/static/coin/icon-exchange-records.png",
},
{
key: "nearbyStores",
label: "附近门店",
icon: "/static/coin/icon-nearby-store.png",
},
];
export const coinRules = [
"金币获取规则:登录个人账号后,使用射灵智能弓体验个人训练、好友约战、排位赛三种模式,每射出一箭并判为有效,即可根据该箭的实际环数,获得等额金币。如:用户在个人训练中,射出一箭并获得9环,即获得9金币。",
"累计金币:累计金币为用户历史获得的所有金币,不会过期、不受兑换影响。",
"可兑换金币:可兑换金币为用户可用于兑换奖品的金币,用户使用金币兑换相应礼品后,将扣除相应金币。同时,可兑换金币有1年有效期,自金币获得之日起开始计算。",
"射灵星球官方可能会不定期举办各类活动,有机会额外获得更多机会,具体以实际公告为准。",
"金币以每个门店/俱乐部为单位独立计算与兑换核销,暂不支持跨门店、跨俱乐部计算,具体以各门店实际公告为准。",
];
+117
View File
@@ -0,0 +1,117 @@
<script setup>
import { computed, ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import CoinHeader from "./components/CoinHeader.vue";
import CoinRecordList from "./components/CoinRecordList.vue";
import CoinEmptyState from "./components/CoinEmptyState.vue";
import { getGoldLogAPI } from "@/apis";
const PAGE_SIZE = 20;
const earningRecords = ref([]);
const page = ref(0);
const total = ref(0);
const loading = ref(false);
const noMore = ref(false);
const loaded = ref(false);
const showEmpty = computed(() => loaded.value && !earningRecords.value.length);
const formatRecord = (item = {}) => {
const [date = "", time = ""] = String(item.createdAt || "").split(" ");
const amount = Number(item.amount) || 0;
return {
id: item.id,
date,
time,
type: item.remark || item.typeDesc || "-",
amount: amount > 0 ? `+${amount}` : String(amount),
};
};
// 实际接口返回 totalCount/pageCount/pageSize,同时兼容接口文档中的旧字段。
const updatePaginationState = (result, list, nextPage) => {
const currentPage = Number(result?.page) || nextPage;
const pageCount = Number(result?.pageCount);
const totalCount = Number(result?.totalCount ?? result?.total);
const responsePageSize = Number(result?.pageSize ?? result?.perPage) || PAGE_SIZE;
page.value = currentPage;
total.value = Number.isFinite(totalCount) ? totalCount : 0;
if (Number.isFinite(pageCount) && pageCount >= 0) {
noMore.value = currentPage >= pageCount;
return;
}
if (Number.isFinite(totalCount) && totalCount >= 0) {
noMore.value = earningRecords.value.length >= totalCount;
return;
}
noMore.value = list.length < responsePageSize;
};
const loadRecords = async ({ reset = false } = {}) => {
if (loading.value || (!reset && noMore.value)) return;
const nextPage = reset ? 1 : page.value + 1;
loading.value = true;
if (reset) noMore.value = false;
try {
const result = await getGoldLogAPI({
page: nextPage,
pageSize: PAGE_SIZE,
type: 1,
});
const list = Array.isArray(result?.list) ? result.list : [];
const mappedList = list.map(formatRecord);
earningRecords.value = reset
? mappedList
: earningRecords.value.concat(mappedList);
updatePaginationState(result, list, nextPage);
} catch (error) {
if (reset) {
earningRecords.value = [];
page.value = 0;
}
console.error("加载金币获取明细失败", error);
} finally {
loading.value = false;
loaded.value = true;
}
};
onLoad(() => loadRecords({ reset: true }));
</script>
<template>
<Container :bgType="6" :isHome="true" @scrolltolower="loadRecords">
<template #header>
<CoinHeader title="金币获取明细" />
</template>
<view class="records-page">
<CoinEmptyState v-if="showEmpty" />
<CoinRecordList v-else :records="earningRecords" />
<view
v-if="loading || (noMore && earningRecords.length)"
class="records-page__status"
>
<text>{{ loading ? "加载中..." : "没有更多了" }}</text>
</view>
</view>
</Container>
</template>
<style scoped>
.records-page {
width: 100%;
min-height: 100%;
}
.records-page__status {
padding: 24rpx 0 32rpx;
color: rgba(255, 255, 255, 0.6);
font-size: 24rpx;
line-height: 34rpx;
text-align: center;
}
</style>
+117
View File
@@ -0,0 +1,117 @@
<script setup>
import { computed, ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import CoinHeader from "./components/CoinHeader.vue";
import CoinRecordList from "./components/CoinRecordList.vue";
import CoinEmptyState from "./components/CoinEmptyState.vue";
import { getGoldLogAPI } from "@/apis";
const PAGE_SIZE = 20;
const exchangeRecords = ref([]);
const page = ref(0);
const total = ref(0);
const loading = ref(false);
const noMore = ref(false);
const loaded = ref(false);
const showEmpty = computed(() => loaded.value && !exchangeRecords.value.length);
const formatRecord = (item = {}) => {
const [date = "", time = ""] = String(item.createdAt || "").split(" ");
return {
id: item.id,
content: item.remark || "-",
type: item.typeDesc || "-",
date,
time,
amount: String(Number(item.amount) || 0),
};
};
// 实际接口返回 totalCount/pageCount/pageSize,同时兼容接口文档中的旧字段。
const updatePaginationState = (result, list, nextPage) => {
const currentPage = Number(result?.page) || nextPage;
const pageCount = Number(result?.pageCount);
const totalCount = Number(result?.totalCount ?? result?.total);
const responsePageSize = Number(result?.pageSize ?? result?.perPage) || PAGE_SIZE;
page.value = currentPage;
total.value = Number.isFinite(totalCount) ? totalCount : 0;
if (Number.isFinite(pageCount) && pageCount >= 0) {
noMore.value = currentPage >= pageCount;
return;
}
if (Number.isFinite(totalCount) && totalCount >= 0) {
noMore.value = exchangeRecords.value.length >= totalCount;
return;
}
noMore.value = list.length < responsePageSize;
};
const loadRecords = async ({ reset = false } = {}) => {
if (loading.value || (!reset && noMore.value)) return;
const nextPage = reset ? 1 : page.value + 1;
loading.value = true;
if (reset) noMore.value = false;
try {
const result = await getGoldLogAPI({
page: nextPage,
pageSize: PAGE_SIZE,
type: 2,
});
const list = Array.isArray(result?.list) ? result.list : [];
const mappedList = list.map(formatRecord);
exchangeRecords.value = reset
? mappedList
: exchangeRecords.value.concat(mappedList);
updatePaginationState(result, list, nextPage);
} catch (error) {
if (reset) {
exchangeRecords.value = [];
page.value = 0;
}
console.error("加载金币兑换记录失败", error);
} finally {
loading.value = false;
loaded.value = true;
}
};
onLoad(() => loadRecords({ reset: true }));
</script>
<template>
<Container :bgType="6" :isHome="true" @scrolltolower="loadRecords">
<template #header>
<CoinHeader title="金币兑换记录" />
</template>
<view class="records-page">
<CoinEmptyState v-if="showEmpty" text="暂无金币兑换记录。" />
<CoinRecordList v-else mode="exchange" :records="exchangeRecords" />
<view
v-if="loading || (noMore && exchangeRecords.length)"
class="records-page__status"
>
<text>{{ loading ? "加载中..." : "没有更多了" }}</text>
</view>
</view>
</Container>
</template>
<style scoped>
.records-page {
width: 100%;
min-height: 100%;
}
.records-page__status {
padding: 24rpx 0 32rpx;
color: rgba(255, 255, 255, 0.6);
font-size: 24rpx;
line-height: 34rpx;
text-align: center;
}
</style>
+171
View File
@@ -0,0 +1,171 @@
<script setup>
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import CoinHeader from "./components/CoinHeader.vue";
import CoinBalancePanel from "./components/CoinBalancePanel.vue";
import CoinQuickMenu from "./components/CoinQuickMenu.vue";
import OfflineExchangeNotice from "./components/OfflineExchangeNotice.vue";
import RewardProductCard from "./components/RewardProductCard.vue";
import { getGiftListAPI, getMyGoldAPI } from "@/apis";
import { quickMenus } from "./data";
const PAGE_SIZE = 20;
const coinSummary = ref({
cumulative: 0,
available: 0,
});
const rewardProducts = ref([]);
const productPage = ref(0);
const productTotal = ref(0);
const productLoading = ref(false);
const productNoMore = ref(false);
const loadCoinSummary = async () => {
try {
const result = await getMyGoldAPI();
coinSummary.value = {
cumulative: Number(result?.totalGold) || 0,
available: Number(result?.usableGold) || 0,
};
} catch (error) {
console.error("加载金币统计失败", error);
}
};
const loadProducts = async ({ reset = false } = {}) => {
if (productLoading.value || (!reset && productNoMore.value)) return;
const nextPage = reset ? 1 : productPage.value + 1;
productLoading.value = true;
if (reset) productNoMore.value = false;
try {
const result = await getGiftListAPI({
page: nextPage,
pageSize: PAGE_SIZE,
});
const list = Array.isArray(result?.list) ? result.list : [];
const mappedList = list.map((item) => ({
id: item.id,
name: item.name || "",
cost: Number(item.coin_price) || 0,
stock: Number(item.stock) || 0,
image: item.cover_image || "",
}));
rewardProducts.value = reset
? mappedList
: rewardProducts.value.concat(mappedList);
productPage.value = Number(result?.page) || nextPage;
const pageCount = Number(result?.pageCount ?? result?.page_count);
const totalCount = Number(result?.totalCount ?? result?.total);
const responsePageSize =
Number(result?.pageSize ?? result?.page_size) || PAGE_SIZE;
productTotal.value = Number.isFinite(totalCount) ? totalCount : 0;
if (Number.isFinite(pageCount) && pageCount >= 0) {
productNoMore.value = productPage.value >= pageCount;
} else if (Number.isFinite(totalCount) && totalCount >= 0) {
productNoMore.value = rewardProducts.value.length >= totalCount;
} else {
productNoMore.value = list.length < responsePageSize;
}
} catch (error) {
if (reset) {
rewardProducts.value = [];
productPage.value = 0;
}
console.error("加载礼品列表失败", error);
} finally {
productLoading.value = false;
}
};
const menuRoutes = {
rules: "/pages/coin/rules",
earningRecords: "/pages/coin/earning-records",
exchangeRecords: "/pages/coin/exchange-records",
nearbyStores: "/pages/coin/nearby-stores",
};
const onMenuSelect = (menu) => {
const url = menuRoutes[menu.key];
if (!url) {
uni.showToast({
title: "功能开发中",
icon: "none",
});
return;
}
uni.navigateTo({ url });
};
const toProductDetail = (product) => {
uni.navigateTo({
url: `/pages/coin/product-detail?id=${product.id}`,
});
};
onLoad(() => {
loadCoinSummary();
loadProducts({ reset: true });
});
</script>
<template>
<Container :bgType="6" :isHome="true" @scrolltolower="loadProducts">
<template #header>
<CoinHeader title="我的金币" />
</template>
<view class="coin-page">
<CoinBalancePanel
:cumulative="coinSummary.cumulative"
:available="coinSummary.available"
/>
<CoinQuickMenu :menus="quickMenus" @select="onMenuSelect" />
<OfflineExchangeNotice />
<view class="reward-section">
<view class="reward-grid">
<view
v-for="product in rewardProducts"
:key="product.id"
class="reward-grid__item"
@click="toProductDetail(product)"
>
<RewardProductCard :product="product" />
</view>
</view>
</view>
</view>
</Container>
</template>
<style scoped>
.coin-page {
width: 100%;
min-height: 100%;
padding-bottom: 40rpx;
box-sizing: border-box;
}
.reward-section {
padding: 20rpx 28rpx 40rpx;
box-sizing: border-box;
}
.reward-grid {
display: flex;
flex-wrap: wrap;
}
.reward-grid__item {
width: 336rpx;
margin-right: 22rpx;
margin-bottom: 20rpx;
}
.reward-grid__item:nth-child(2n) {
margin-right: 0;
}
</style>
+144
View File
@@ -0,0 +1,144 @@
<script setup>
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import CoinHeader from "./components/CoinHeader.vue";
import LocationPermissionState from "./components/LocationPermissionState.vue";
import StoreCard from "./components/StoreCard.vue";
import { getNearbyStoresAPI } from "@/apis";
const PAGE_SIZE = 20;
const DEFAULT_STORE_IMAGE = "/static/coin/store-photo.png";
const showPermissionState = ref(false);
const stores = ref([]);
const location = ref(null);
const page = ref(0);
const total = ref(0);
const loading = ref(false);
const noMore = ref(false);
const loaded = ref(false);
const getCurrentLocation = () =>
new Promise((resolve, reject) => {
uni.getLocation({
type: "gcj02",
success: resolve,
fail: reject,
});
});
const mapStore = (item = {}) => ({
id: item.id,
name: item.name || "",
address: item.address || "",
phone: item.phone || "",
hours: item.businessHours || "",
image: item.coverImage || DEFAULT_STORE_IMAGE,
});
const loadStores = async ({ reset = false } = {}) => {
if (!location.value || loading.value || (!reset && noMore.value)) return;
const nextPage = reset ? 1 : page.value + 1;
loading.value = true;
if (reset) noMore.value = false;
try {
const result = await getNearbyStoresAPI({
...location.value,
page: nextPage,
pageSize: PAGE_SIZE,
});
const list = Array.isArray(result?.list) ? result.list : [];
const mappedList = list.map(mapStore);
stores.value = reset ? mappedList : stores.value.concat(mappedList);
page.value = Number(result?.page) || nextPage;
const pageCount = Number(result?.pageCount);
const totalCount = Number(result?.totalCount ?? result?.total);
const responsePageSize = Number(result?.pageSize) || PAGE_SIZE;
total.value = Number.isFinite(totalCount) ? totalCount : 0;
if (Number.isFinite(pageCount) && pageCount >= 0) {
noMore.value = page.value >= pageCount;
} else if (Number.isFinite(totalCount) && totalCount >= 0) {
noMore.value = stores.value.length >= totalCount;
} else {
noMore.value = list.length < responsePageSize;
}
} catch (error) {
if (reset) {
stores.value = [];
page.value = 0;
}
console.error("加载附近门店失败", error);
} finally {
loading.value = false;
loaded.value = true;
}
};
const locateAndLoadStores = async () => {
try {
const position = await getCurrentLocation();
location.value = {
longitude: position.longitude,
latitude: position.latitude,
};
showPermissionState.value = false;
await loadStores({ reset: true });
} catch (error) {
showPermissionState.value = true;
loaded.value = true;
console.error("获取定位失败", error);
}
};
const authorizeLocation = () => {
uni.openSetting({
success: locateAndLoadStores,
fail: locateAndLoadStores,
});
};
onLoad(locateAndLoadStores);
</script>
<template>
<Container :bgType="6" :isHome="true" @scrolltolower="loadStores">
<template #header>
<CoinHeader title="附近门店" />
</template>
<view class="stores-page">
<LocationPermissionState
v-if="showPermissionState"
@authorize="authorizeLocation"
/>
<template v-else>
<StoreCard v-for="store in stores" :key="store.id" :store="store" />
<text v-if="loaded && !stores.length" class="stores-page__more">
附近暂无门店~
</text>
<text v-else-if="noMore" class="stores-page__more">没有更多门店了~</text>
</template>
</view>
</Container>
</template>
<style scoped>
.stores-page {
width: 100%;
min-height: 100%;
padding-bottom: 54rpx;
box-sizing: border-box;
}
.stores-page__more {
display: block;
width: 650rpx;
margin: 26rpx auto 0;
color: rgba(255, 255, 255, 0.72);
font-size: 26rpx;
line-height: 36rpx;
text-align: left;
}
</style>
+144
View File
@@ -0,0 +1,144 @@
<script setup>
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import Container from "@/components/Container.vue";
import CoinHeader from "./components/CoinHeader.vue";
import ProductHeroSwiper from "./components/ProductHeroSwiper.vue";
import OfflineExchangeNotice from "./components/OfflineExchangeNotice.vue";
import { getGiftDetailAPI } from "@/apis";
const productDetail = ref({
name: "",
coinPrice: 0,
stock: 0,
images: [],
descriptions: [],
});
const loadProductDetail = async (id) => {
try {
const result = await getGiftDetailAPI(id);
const images = Array.isArray(result?.images)
? result.images
.slice()
.sort((first, second) =>
(Number(first?.sort_order) || 0) - (Number(second?.sort_order) || 0)
)
.map((item) => item?.image_url)
.filter(Boolean)
: [];
productDetail.value = {
...productDetail.value,
name: result?.name || "",
coinPrice: Number(result?.coin_price) || 0,
stock: Number(result?.stock) || 0,
images,
descriptions: result?.description
? String(result.description).split(/\r?\n/).filter(Boolean)
: [],
};
} catch (error) {
console.error("加载礼品详情失败", error);
}
};
onLoad((options = {}) => {
const id = Number(options.id);
if (!Number.isInteger(id) || id <= 0) {
uni.showToast({
title: "商品参数无效",
icon: "none",
});
return;
}
loadProductDetail(id);
});
</script>
<template>
<Container :bgType="6" :isHome="true">
<template #header>
<CoinHeader title="商品详情" />
</template>
<view class="product-detail">
<ProductHeroSwiper
:images="productDetail.images"
/>
<view class="product-detail__summary">
<text class="product-detail__name">{{ productDetail.name }}</text>
<view class="product-detail__balance">
<text>金币</text>
<text class="product-detail__amount">{{ productDetail.coinPrice }}</text>
<text>剩余{{ productDetail.stock }}</text>
</view>
</view>
<OfflineExchangeNotice variant="red" />
<view class="product-detail__content">
<text
v-for="(description, index) in productDetail.descriptions"
:key="index"
class="product-detail__paragraph"
>
{{ description }}
</text>
</view>
</view>
</Container>
</template>
<style scoped>
.product-detail {
width: 100%;
min-height: 100%;
padding-bottom: 60rpx;
box-sizing: border-box;
background-color: #22222e;
}
.product-detail__summary {
padding: 30rpx 40rpx 20rpx;
display: flex;
flex-direction: column;
}
.product-detail__name {
color: #ffd947;
font-size: 52rpx;
line-height: 74rpx;
font-weight: 500;
}
.product-detail__balance {
display: flex;
align-items: baseline;
margin-top: 10rpx;
color: rgba(255, 255, 255, 0.75);
font-size: 24rpx;
line-height: 34rpx;
}
.product-detail__amount {
margin-right: 6rpx;
color: #ffffff;
font-size: 36rpx;
line-height: 50rpx;
font-weight: 500;
}
.product-detail__content {
padding: 38rpx 40rpx 60rpx;
box-sizing: border-box;
border-bottom: 2rpx solid rgba(255, 217, 71, 0.05);
}
.product-detail__paragraph {
display: block;
margin-bottom: 20rpx;
color: rgba(255, 255, 255, 0.75);
font-size: 26rpx;
line-height: 40rpx;
text-align: justify;
}
</style>
+38
View File
@@ -0,0 +1,38 @@
<script setup>
import Container from "@/components/Container.vue";
import CoinHeader from "./components/CoinHeader.vue";
import { coinRules } from "./data";
</script>
<template>
<Container :bgType="6" :isHome="true">
<template #header>
<CoinHeader title="金币规则" />
</template>
<view class="rules-page">
<view
v-for="(rule, index) in coinRules"
:key="index"
class="rules-page__item"
>
<text>{{ index + 1 }}{{ rule }}</text>
</view>
</view>
</Container>
</template>
<style scoped>
.rules-page {
width: 100%;
padding: 28rpx 38rpx 60rpx;
box-sizing: border-box;
}
.rules-page__item {
margin-bottom: 26rpx;
color: rgba(255, 255, 255, 0.88);
font-size: 28rpx;
line-height: 52rpx;
text-align: justify;
}
</style>
+144 -32
View File
@@ -1,16 +1,35 @@
<script setup> <script setup>
import { ref } from "vue"; import { computed, ref } from "vue";
import { onLoad } from "@dcloudio/uni-app"; import { onLoad } from "@dcloudio/uni-app";
import Signin from "@/components/Signin.vue"; import Signin from "@/components/Signin.vue";
import SButton from "@/components/SButton.vue"; import SButton from "@/components/SButton.vue";
import Avatar from "@/components/Avatar.vue";
import AppBackground from "@/components/AppBackground.vue";
import DeviceChargingDialog from "@/components/DeviceChargingDialog.vue"; import DeviceChargingDialog from "@/components/DeviceChargingDialog.vue";
import { tempBindOrgAPI } from "@/apis"; import { tempBindOrgAPI } from "@/apis";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user } = storeToRefs(store);
const scene = ref(""); const scene = ref("");
const status = ref("idle"); const status = ref("idle");
const errorMessage = ref(""); const errorMessage = ref("");
const showSignin = ref(false); const showSignin = ref(false);
const binding = ref(false); const binding = ref(false);
const bindInfo = ref({});
const successInfo = computed(() => {
const result = bindInfo.value || {};
return {
avatar: user.value.avatar,
nickName: user.value.nickName || "--",
mobile: result.mobile || "--",
storeName: result.storeName || "--",
};
});
const getToken = () => { const getToken = () => {
try { try {
@@ -40,9 +59,17 @@ const bindOrg = async () => {
binding.value = true; binding.value = true;
status.value = "binding"; status.value = "binding";
errorMessage.value = ""; errorMessage.value = "";
bindInfo.value = {};
try { try {
await tempBindOrgAPI(scene.value); const result = (await tempBindOrgAPI(scene.value)) || {};
if (result.success !== true) {
status.value = "failed";
errorMessage.value = result.msg || "授权登录失败,请稍后重试。";
return;
}
bindInfo.value = result;
status.value = "success"; status.value = "success";
} catch (error) { } catch (error) {
if (error?.type === "AUTH_INVALID") { if (error?.type === "AUTH_INVALID") {
@@ -83,45 +110,69 @@ onLoad((options) => {
<template> <template>
<view class="page"> <view class="page">
<AppBackground :type="6" bgColor="#20202c" />
<view class="content"> <view class="content">
<text class="title">机构设备绑定</text> <text class="title">登录门店Pad端</text>
<view v-if="status === 'idle' || status === 'binding'" class="state"> <view
<text class="state-title">正在绑定</text> v-if="status === 'idle' || status === 'binding'"
<text class="description">正在绑定机构设备请稍候</text> class="state message-state"
>
<text class="state-title">正在授权登录</text>
<text class="description">正在登录门店Pad端请稍候</text>
</view> </view>
<view v-else-if="status === 'login'" class="state"> <view v-else-if="status === 'login'" class="state login-state">
<text class="state-title">请先登录</text> <image
<text class="description"> class="login-icon"
{{ src="../static/org-bind/login-icon.png"
errorMessage || mode="aspectFit"
"登录后即可绑定当前机构设备。关闭登录窗口后,也可以再次点击下方按钮继续。" />
}} <text class="login-title">{{
</text> errorMessage || "请先登录小程序"
<SButton width="560rpx" :rounded="20" :onClick="openSignin"> }}</text>
<text>立即登录</text> <SButton width="600rpx" :rounded="22" :onClick="openSignin">
<text>微信授权登录</text>
</SButton> </SButton>
</view> </view>
<view v-else-if="status === 'success'" class="state"> <view v-else-if="status === 'success'" class="state success-state">
<text class="state-title">绑定成功</text> <view class="avatar-wrap">
<text class="description"> <Avatar
您的账号已成功绑定机构设备请返回 iPad 继续操作 :src="successInfo.avatar"
:size="176"
sizeUnit="rpx"
imageMode="aspectFill"
/>
<image
class="success-badge"
src="../static/org-bind/green-gou.png"
mode="aspectFit"
/>
</view>
<text class="success-title">已成功授权登录射灵星球门店Pad端</text>
<view class="account-info">
<text>登录账号{{ successInfo.nickName }}</text>
<text>手机号{{ successInfo.mobile }}</text>
<text>登录门店{{ successInfo.storeName }}</text>
<text class="warm-tip">
温馨提示离开门店时候记得在Pad退出登录哦
</text> </text>
</view> </view>
</view>
<view v-else-if="status === 'failed'" class="state"> <view v-else-if="status === 'failed'" class="state message-state">
<text class="state-title">绑定失败</text> <text class="state-title">授权登录失败</text>
<text class="description">{{ errorMessage }}</text> <text class="description">{{ errorMessage }}</text>
<SButton width="560rpx" :rounded="20" :onClick="bindOrg"> <SButton width="600rpx" :rounded="22" :onClick="bindOrg">
<text>重新绑定</text> <text>重新登录</text>
</SButton> </SButton>
</view> </view>
<view v-else class="state"> <view v-else class="state message-state">
<text class="state-title">二维码无效</text> <text class="state-title">二维码无效</text>
<text class="description">请重新扫描机构设备上的小程序码</text> <text class="description">请重新扫描门店Pad端上的小程序码</text>
</view> </view>
</view> </view>
@@ -139,32 +190,93 @@ onLoad((options) => {
box-sizing: border-box; box-sizing: border-box;
width: 100%; width: 100%;
min-height: 100vh; min-height: 100vh;
padding: calc(var(--status-bar-height) + 120rpx) 48rpx 80rpx; padding: calc(var(--status-bar-height) + 64rpx) 48rpx 80rpx;
background-color: #000;
color: #fff; color: #fff;
} }
.content { .content {
position: relative;
z-index: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
} }
.title { .title {
font-size: 44rpx; color: #d8ad69;
font-weight: 600; font-size: 30rpx;
font-weight: 500;
} }
.state { .state {
width: 100%; width: 100%;
margin-top: 120rpx;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
} }
.login-state,
.success-state {
margin-top: 320rpx;
}
.message-state {
margin-top: 240rpx;
}
.login-icon {
width: 176rpx;
height: 176rpx;
border-radius: 50%;
}
.login-title {
margin: 28rpx 0 60rpx;
font-size: 40rpx;
font-weight: 500;
}
.avatar-wrap {
position: relative;
}
.success-badge {
position: absolute;
right: -2rpx;
bottom: 4rpx;
width: 40rpx;
height: 40rpx;
}
.success-title {
width: 572rpx;
margin-top: 28rpx;
color: #fed847;
font-size: 34rpx;
font-weight: 500;
line-height: 48rpx;
}
.account-info {
box-sizing: border-box;
width: 572rpx;
margin-top: 20rpx;
display: flex;
flex-direction: column;
color: #FFFFFF;
font-size: 28rpx;
line-height: 52rpx;
}
.warm-tip {
margin-top: 40rpx;
color: #b8b8bd;
font-size: 24rpx;
line-height: 40rpx;
}
.state-title { .state-title {
font-size: 36rpx; font-size: 38rpx;
font-weight: 600; font-weight: 600;
} }
+6
View File
@@ -24,6 +24,11 @@ const toFristTryPage = async () => {
}); });
} }
}; };
const toCoinPage = () => {
uni.navigateTo({
url: "/pages/coin/index",
});
};
const toBeVipPage = () => { const toBeVipPage = () => {
uni.navigateTo({ uni.navigateTo({
url: "/pages/member/be-vip", url: "/pages/member/be-vip",
@@ -108,6 +113,7 @@ const buildVersion = typeof __BUILD_TIME__ !== 'undefined' ? __BUILD_TIME__ : ''
<text v-if="user.trio > 0" :style="{ color: '#259249' }">已完成</text> <text v-if="user.trio > 0" :style="{ color: '#259249' }">已完成</text>
<text v-else :style="{ color: '#CC311F' }">未完成</text> <text v-else :style="{ color: '#CC311F' }">未完成</text>
</UserItem> </UserItem>
<UserItem title="我的金币" :onClick="toCoinPage" />
<UserItem title="会员" :onClick="toBeVipPage"> <UserItem title="会员" :onClick="toBeVipPage">
<view <view
v-if="user.sVip === true" v-if="user.sVip === true"
Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 742 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB