次卡+道具卡 道具卡不生效
0
.gitignore
vendored
Normal file → Executable file
@ -1,46 +0,0 @@
|
|||||||
## 接口梳理(App 用户相关)
|
|
||||||
- 登录与绑定:
|
|
||||||
- `POST /api/app/users/weixin/login`,请求含 `code`、`invite_code`;响应含 `token`、`user_id`、`avatar`、`nickname`、`invite_code`(.trae/documents/基于 Swagger 的 App 用户 API 汇总与 Uni-App 微信登录页面实现方案.md:5-10)
|
|
||||||
- `POST /api/app/users/{user_id}/phone/bind`,请求含微信手机号 `code`;响应为标准成功(.trae/documents/基于 Swagger 的 App 用户 API 汇总与 Uni-App 微信登录页面实现方案.md:11-16)
|
|
||||||
- 用户资料与地址:
|
|
||||||
- `PUT /api/app/users/{user_id}`(修改头像/昵称,`avatar`、`nickname`)(:19-22)
|
|
||||||
- 地址列表/新增/删除/设默认(`:23-27`),新增请求含基本地址字段(`:25-27`)
|
|
||||||
- 积分与统计:
|
|
||||||
- `GET /api/app/users/{user_id}/points/balance` 响应 `balance`(`:31-34`)
|
|
||||||
- `GET /api/app/users/{user_id}/stats` 响应 `coupon_count`、`item_card_count`、`points_balance`(`:35-37`)
|
|
||||||
- 订单与卡券/道具:
|
|
||||||
- 订单列表、优惠券、邀请、道具卡与使用记录(`:40-48`)
|
|
||||||
|
|
||||||
## 技术方案
|
|
||||||
- 网络层:
|
|
||||||
- 方案A(推荐)复用 `alova` 客户端与生成器,统一 `Authorization` 与错误处理(`:51`)
|
|
||||||
- 方案B 以 `uni.request` 封装最小所需接口(登录/绑定/统计),在请求头注入 `Bearer` token(`:71-72`)
|
|
||||||
- 平台与配置:
|
|
||||||
- `baseURL` 指向后端 `http://127.0.0.1:9991`;在微信小程序后台配置合法域名与 HTTPS 证书(`:75`)
|
|
||||||
- 状态与路由:
|
|
||||||
- `pages.json` 添加 `pages/login/index`;登录成功后 `uni.reLaunch` 到首页;用 `pinia` 管理 `isLogin`、`userInfo`、`points`(`:79`)
|
|
||||||
- 错误处理:
|
|
||||||
- 按既有分类提示与重试策略,覆盖网络错误、超时、404/500、参数错误(`:83`)
|
|
||||||
|
|
||||||
## 页面实现(Uni-App Vue3)
|
|
||||||
- 结构:Logo/说明、`「微信登录」`按钮、`open-type="getPhoneNumber"` 授权按钮、加载与错误提示(`:57-58`)
|
|
||||||
- 流程:
|
|
||||||
- 触发 `uni.login({ provider: 'weixin' })` 获取 `code` → 调用 `POST /api/app/users/weixin/login` → 持久化 `token`、`user_id`(`:61-62`)
|
|
||||||
- 可选手机号绑定:`onGetPhoneNumber` 取 `code` → `POST /api/app/users/{user_id}/phone/bind`(`:63-64`)
|
|
||||||
- 登录后拉取统计与积分余额更新首页(`:65-66`)
|
|
||||||
|
|
||||||
## 交付内容
|
|
||||||
- 新增 `pages/login/index.vue`(Composition API)含完整登录/绑定流程与错误提示(`:91-94`)
|
|
||||||
- 接入并配置网络层(复用 `alova` 或最小 `uni.request` 封装)
|
|
||||||
- 路由与 `pinia` 状态的最小接入
|
|
||||||
|
|
||||||
## 验证方法
|
|
||||||
- 在开发者工具/真机验证:`code` 获取、接口返回、`token/user_id` 存储、后续接口成功(`:87-88`)
|
|
||||||
- 输出必要的调试日志(不含敏感信息),观察错误分支与重试入口
|
|
||||||
|
|
||||||
## 执行步骤
|
|
||||||
1. 接入 Swagger JSON(`http://127.0.0.1:9991/swagger/v1/swagger.json`)同步生成或确认接口(`:99-101`)
|
|
||||||
2. 选定网络层方案并落地调用
|
|
||||||
3. 新增登录页面与交互逻辑
|
|
||||||
4. 调整路由与状态管理
|
|
||||||
5. 自测与联调,完成交付
|
|
||||||
@ -1,102 +0,0 @@
|
|||||||
## API 文档汇总(App 用户相关)
|
|
||||||
|
|
||||||
* 登录与绑定:
|
|
||||||
|
|
||||||
* `POST /api/app/users/weixin/login`(miniapp/src/api/apis/apiDefinitions.js:106)
|
|
||||||
|
|
||||||
* 请求: `App_weixin_login_request`(code、invite\_code,可选)
|
|
||||||
|
|
||||||
* 响应: `App_weixin_login_response`(token、user\_id、avatar、nickname、invite\_code)(miniapp/src/api/apis/globals.d.ts:760)
|
|
||||||
|
|
||||||
* `POST /api/app/users/{user_id}/phone/bind`(apiDefinitions.js:123)
|
|
||||||
|
|
||||||
* 请求: `App_bind_phone_request`(code,来源于微信手机号授权)(globals.d.ts:376)
|
|
||||||
|
|
||||||
* 响应: 成功布尔或标准成功结构(项目统一在 `responded` 钩子返回 `response.data` 或 `response.data.data`,miniapp/src/api/apis/index.js:59)
|
|
||||||
|
|
||||||
* 用户资料与地址:
|
|
||||||
|
|
||||||
* `PUT /api/app/users/{user_id}` 修改头像/昵称(apiDefinitions.js:107)
|
|
||||||
|
|
||||||
* 请求: `App_modify_user_request`(avatar、nickname,可选)(globals.d.ts:363)
|
|
||||||
|
|
||||||
* `GET /api/app/users/{user_id}/addresses` 列表(apiDefinitions.js:108)/ `POST` 新增(apiDefinitions.js:109)/ `DELETE` 删除(apiDefinitions.js:110)/ `PUT .../default` 设默认(apiDefinitions.js:114)
|
|
||||||
|
|
||||||
* 新增请求: `App_add_address_request`(姓名、手机号、省市区、详细地址、是否默认)(globals.d.ts:367)
|
|
||||||
|
|
||||||
* 响应: 列表返回数组,新增/删除/设默认返回标准成功结构(项目统一 `responded` 处理)
|
|
||||||
|
|
||||||
* 积分与统计:
|
|
||||||
|
|
||||||
* `GET /api/app/users/{user_id}/points`(apiDefinitions.js:124)/ `GET .../points/balance`(apiDefinitions.js:125)
|
|
||||||
|
|
||||||
* 响应: `App_points_balance_response`(balance)(globals.d.ts:773)
|
|
||||||
|
|
||||||
* `GET /api/app/users/{user_id}/stats`(apiDefinitions.js:126)
|
|
||||||
|
|
||||||
* 响应: `App_user_stats_response`(coupon\_count、item\_card\_count、points\_balance)(globals.d.ts:776)
|
|
||||||
|
|
||||||
* 订单与卡券/道具:
|
|
||||||
|
|
||||||
* `GET /api/app/users/{user_id}/orders`(apiDefinitions.js:122)→ 订单列表(类型包含 `Model_order_items` 等)
|
|
||||||
|
|
||||||
* `GET /api/app/users/{user_id}/coupons`(apiDefinitions.js:118)/ `GET .../invites`(apiDefinitions.js:119)
|
|
||||||
|
|
||||||
* `GET /api/app/users/{user_id}/item_cards`(apiDefinitions.js:120)/ `GET .../item_cards/uses`(apiDefinitions.js:121)
|
|
||||||
|
|
||||||
* 响应: `User_item_card_with_template[]`(globals.d.ts:978)
|
|
||||||
|
|
||||||
## 现有代码要点(可复用)
|
|
||||||
|
|
||||||
* API 客户端:`alova` + 生成器(miniapp/src/api/apis/index.js:35、112;miniapp/alova.config.js:6),已封装 `Authorization`、401 刷新登录(index.js:9-33)。
|
|
||||||
|
|
||||||
* 登录页(Taro版):`miniapp/src/pages/login/index.vue`,逻辑封装在 `Apis.login.WechatAppLogin`(index.js:122-421)。
|
|
||||||
|
|
||||||
## 登录页面实现(Uni-App Vue3)
|
|
||||||
|
|
||||||
* 页面结构:Logo/说明文案、按钮`「微信登录」`与`open-type="getPhoneNumber"`的手机号授权按钮,加载与错误提示。
|
|
||||||
|
|
||||||
* 流程:
|
|
||||||
|
|
||||||
* `uni.login({ provider: 'weixin' })` 获取 `code` → 调用 `POST /api/app/users/weixin/login` → 存储 `token`、`user_id` 到 `uni.setStorageSync`。
|
|
||||||
|
|
||||||
* 可选:用户点击手机号授权后触发 `onGetPhoneNumber`,拿到 `code` 调用 `POST /api/app/users/{user_id}/phone/bind` 绑定手机号。
|
|
||||||
|
|
||||||
* 登录完成后拉取 `GET /api/app/users/{user_id}/stats` 与 `.../points/balance` 更新首页状态。
|
|
||||||
|
|
||||||
* 网络层:
|
|
||||||
|
|
||||||
* 方案A(推荐,复用现有):在 Uni-App 中引入与复用 `alova` 生成的 `Apis`(保持统一的拦截器与基址、响应处理)。
|
|
||||||
|
|
||||||
* 方案B(轻量):使用 `uni.request` 封装最小调用(登录/绑定/统计),按现有 `Authorization: Bearer <token>` 规则注入。
|
|
||||||
|
|
||||||
* 配置与安全:
|
|
||||||
|
|
||||||
* `baseURL` 指向后端地址(如 `http://127.0.0.1:9991`),并在微信小程序后台配置合法域名/证书;避免在日志中输出明文 token/手机号等敏感信息。
|
|
||||||
|
|
||||||
* 路由与状态:
|
|
||||||
|
|
||||||
* 在 `pages.json` 新增 `pages/login/index`,登录成功后 `uni.reLaunch` 到首页;使用 `pinia` 存储 `isLogin`、`userInfo`、`points` 等(参考 miniapp/src/store/index.js)。
|
|
||||||
|
|
||||||
* 错误处理:
|
|
||||||
|
|
||||||
* 按当前项目的分类提示(连接被拒绝、超时、域名未配置、SSL 错误、404、500、参数错误)进行用户级文案与重试入口(参考 index.js:221-253)。
|
|
||||||
|
|
||||||
## 验证与交付
|
|
||||||
|
|
||||||
* 验证:真机或开发者工具下,观察 `code` 获取、接口返回、`token/user_id` 存储与后续接口成功;埋点或日志控制台输出关键步骤。
|
|
||||||
|
|
||||||
* 交付:
|
|
||||||
|
|
||||||
* 新增 `pages/login/index.vue`(Uni-App Vue3 Composition API 实现)。
|
|
||||||
|
|
||||||
* 复用或新增 API 封装(A/B 二选一)。
|
|
||||||
|
|
||||||
* 配置/路由调整与最小 `pinia` 状态接入。
|
|
||||||
|
|
||||||
## 后续执行步骤
|
|
||||||
|
|
||||||
* 接入 Swagger 源:将生成器输入指向 `http://127.0.0.1:9991/swagger/v1/swagger.json`(或项目后端的 Swagger JSON),生成/更新 `Apis` 并对齐 `baseURL`。
|
|
||||||
|
|
||||||
* 按上述方案完成页面与调用接入,并保持与现有 `alova` 响应处理一致性。
|
|
||||||
|
|
||||||
@ -1,35 +0,0 @@
|
|||||||
## 目标
|
|
||||||
- 首页 UI 始终可见:轮播图在无数据时也显示占位滑块;通知始终滚动显示(Marquee)。
|
|
||||||
- 接口路径统一为 `/api/app/*`,兼容返回 `{list: [...]}` 与字段 `snake_case`(如 `image_url`)。
|
|
||||||
|
|
||||||
## 变更范围
|
|
||||||
- 文件:`pages/index/index.vue`
|
|
||||||
- 保留现有登录弹窗,但不阻断首页数据加载;完善数据清洗与空态展示。
|
|
||||||
|
|
||||||
## 轮播图(无数据也展示)
|
|
||||||
- 始终渲染 `swiper.banner-swiper`,不再用 `v-if` 隐藏容器。
|
|
||||||
- 数据存在:按 `banners` 渲染;字段映射 `id`、`image_url|imageUrl|image`、`link_url|linkUrl|link|url`。
|
|
||||||
- 数据为空:渲染 3 个占位滑块(`swiper-item` 内用 `<view>` 纯色/渐变背景 + 文案“敬请期待”),避免依赖静态图片资源。
|
|
||||||
- URL 清洗:移除反引号与空格,保证 `image_url` 可用。
|
|
||||||
|
|
||||||
## 通知滚动(Marquee)
|
|
||||||
- 始终渲染通知条。
|
|
||||||
- 有数据:横向无缝滚动(CSS `@keyframes` + `transform: translateX`),将所有通知拼接为一条长文本,重复一份以实现循环滚动。
|
|
||||||
- 无数据:使用默认文案(如“欢迎光临”“最新活动敬请期待”)参与滚动,保证始终有动效。
|
|
||||||
|
|
||||||
## 活动区
|
|
||||||
- 保持当前两列栅格布局;无数据时显示“暂无活动”占位文案;点击仅在 `link` 为内部路径时跳转。
|
|
||||||
|
|
||||||
## 数据与接口
|
|
||||||
- 请求入口统一:`/api/app/notices`、`/api/app/banners`、`/api/app/activities`。
|
|
||||||
- 解包:支持 `list|items|data`。
|
|
||||||
- 映射:通知 `content|text|title`;轮播图 `image_url|imageUrl|image|img|pic`、`link_url|linkUrl|link|url`;活动 `cover_url|coverUrl|image|img|pic`、`title|name`、`sub_title|subTitle|subtitle|desc|description`。
|
|
||||||
|
|
||||||
## 交互与空态
|
|
||||||
- 未登录/未绑定:弹窗提醒,但首页照常加载并显示占位内容。
|
|
||||||
- 点击跳转:内部路径以 `/` 开头才触发 `navigateTo`;避免空链接导致错误。
|
|
||||||
|
|
||||||
## 验证
|
|
||||||
- 模拟后端返回 `{list:[...]}` 含反引号的 `image_url`,确认轮播图正常显示。
|
|
||||||
- 清空 `banners` 与 `notices`,确认占位滑块与默认滚动文案显示。
|
|
||||||
- 在微信小程序/浏览器预览,验证滚动流畅度与样式适配。
|
|
||||||
0
api/appUser.js
Normal file → Executable file
0
components/BoxReveal.vue
Normal file → Executable file
0
components/ClayButton.vue
Normal file → Executable file
0
components/ClayCard.vue
Normal file → Executable file
0
components/ClayInput.vue
Normal file → Executable file
0
components/ElCard.vue
Normal file → Executable file
0
components/FlipGrid.vue
Normal file → Executable file
0
components/GamePassPurchasePopup.vue
Normal file → Executable file
0
components/GamePassPurchasePopup.vue.backup
Normal file → Executable file
0
components/MatchingGame.vue
Normal file → Executable file
2
components/PaymentPopup.vue
Normal file → Executable file
@ -304,7 +304,7 @@ function handleClose() {
|
|||||||
function handleConfirm() {
|
function handleConfirm() {
|
||||||
emit('confirm', {
|
emit('confirm', {
|
||||||
coupon: useGamePass.value ? null : selectedCoupon.value,
|
coupon: useGamePass.value ? null : selectedCoupon.value,
|
||||||
card: (props.showCards && !useGamePass.value) ? selectedCard.value : null,
|
card: props.showCards ? selectedCard.value : null, // 次卡使用时也保留道具卡(后端只限制次卡与优惠券互斥,不限制道具卡)
|
||||||
useGamePass: useGamePass.value
|
useGamePass: useGamePass.value
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
0
components/SplashScreen.vue
Normal file → Executable file
0
components/YifanSelector.vue
Normal file → Executable file
0
components/activity/ActivityHeader.vue
Normal file → Executable file
0
components/activity/ActivityPageLayout.vue
Normal file → Executable file
0
components/activity/ActivityTabs.vue
Normal file → Executable file
0
components/activity/CabinetPreviewPopup.vue
Normal file → Executable file
0
components/activity/DrawLoadingPopup.vue
Normal file → Executable file
0
components/activity/LotteryResultPopup.vue
Normal file → Executable file
0
components/activity/RecordsList.vue
Normal file → Executable file
0
components/activity/RewardsPopup.vue
Normal file → Executable file
0
components/activity/RewardsPreview.vue
Normal file → Executable file
0
components/activity/RulesPopup.vue
Normal file → Executable file
0
components/activity/index.js
Normal file → Executable file
0
components/app-tab-bar-toutiao.vue
Normal file → Executable file
0
components/app-tab-bar.vue
Normal file → Executable file
0
components/clay-components.js
Normal file → Executable file
0
docs/CLAYMORPHISM.md
Normal file → Executable file
0
docs/代码重构分析/ALIGNMENT_代码冗余分析.md
Normal file → Executable file
0
docs/代码重构分析/DESIGN_组件化重构.md
Normal file → Executable file
0
index.html
Normal file → Executable file
0
manifest.json
Normal file → Executable file
0
pages-activity/activity/duiduipeng/index.vue
Normal file → Executable file
0
pages-activity/activity/list/index.vue
Normal file → Executable file
0
pages-activity/activity/pata/index.vue
Normal file → Executable file
0
pages-activity/activity/wuxianshang/index.vue
Normal file → Executable file
0
pages-activity/activity/yifanshang/index.vue
Normal file → Executable file
0
pages-activity/composables/index.js
Normal file → Executable file
0
pages-activity/composables/useActivity.js
Normal file → Executable file
0
pages-activity/composables/useIssues.js
Normal file → Executable file
0
pages-activity/composables/useRecords.js
Normal file → Executable file
0
pages-activity/composables/useRewards.js
Normal file → Executable file
0
pages-game/game/minesweeper/index.vue
Normal file → Executable file
0
pages-game/game/minesweeper/play.scss
Normal file → Executable file
0
pages-game/game/minesweeper/play.vue
Normal file → Executable file
0
pages-game/game/minesweeper/room-list.vue
Normal file → Executable file
0
pages-game/game/webview.vue
Normal file → Executable file
0
pages-shop/shop/detail.vue
Normal file → Executable file
0
pages-user/address/edit.vue
Normal file → Executable file
0
pages-user/address/index.vue
Normal file → Executable file
0
pages-user/address/submit.vue
Normal file → Executable file
0
pages-user/agreement/purchase.vue
Normal file → Executable file
0
pages-user/agreement/user.vue
Normal file → Executable file
0
pages-user/coupons/index.vue
Normal file → Executable file
0
pages-user/help/index.vue
Normal file → Executable file
0
pages-user/invite/landing.vue
Normal file → Executable file
0
pages-user/invites/index.vue
Normal file → Executable file
0
pages-user/item-cards/index.vue
Normal file → Executable file
0
pages-user/orders/detail.vue
Normal file → Executable file
0
pages-user/orders/index.vue
Normal file → Executable file
0
pages-user/points/index.vue
Normal file → Executable file
0
pages-user/settings/index.vue
Normal file → Executable file
167
pages-user/tasks/index.vue
Normal file → Executable file
@ -74,14 +74,14 @@
|
|||||||
<view class="sub-progress-list" v-if="taskProgress[task.id]?.subProgress?.length > 0">
|
<view class="sub-progress-list" v-if="taskProgress[task.id]?.subProgress?.length > 0">
|
||||||
<view
|
<view
|
||||||
v-for="sub in taskProgress[task.id].subProgress"
|
v-for="sub in taskProgress[task.id].subProgress"
|
||||||
:key="sub.activity_id"
|
:key="sub.activity_id ?? sub.label"
|
||||||
class="sub-progress-item"
|
class="sub-progress-item"
|
||||||
>
|
>
|
||||||
<text class="sub-label">活动 {{ sub.activity_id }}</text>
|
<text class="sub-label">{{ getSubProgressLabel(sub) }}</text>
|
||||||
<view class="sub-bar-bg">
|
<view class="sub-bar-bg">
|
||||||
<view class="sub-bar-fill" :style="{ width: getSubProgressWidth(sub, task) }"></view>
|
<view class="sub-bar-fill" :style="{ width: getSubProgressWidth(sub, task) }"></view>
|
||||||
</view>
|
</view>
|
||||||
<text class="sub-value">¥{{ sub.order_amount / 100 }}</text>
|
<text class="sub-value">{{ getSubProgressValue(sub, task) }}</text>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@ -138,6 +138,10 @@
|
|||||||
<!-- 进度中 -->
|
<!-- 进度中 -->
|
||||||
<view v-else class="tier-progress">
|
<view v-else class="tier-progress">
|
||||||
<text class="progress-text">{{ getTierProgressText(task, tier) }}</text>
|
<text class="progress-text">{{ getTierProgressText(task, tier) }}</text>
|
||||||
|
<!-- 普通档位进度条(非 first_order 类型才显示) -->
|
||||||
|
<view class="tier-bar-bg" v-if="getTierProgressPercent(task, tier) !== ''">
|
||||||
|
<view class="tier-bar-fill" :style="{ width: getTierProgressPercent(task, tier) }"></view>
|
||||||
|
</view>
|
||||||
<!-- 限量进度提示 -->
|
<!-- 限量进度提示 -->
|
||||||
<text class="quota-text" v-if="tier.quota > 0">剩余 {{ tier.remaining }} 份</text>
|
<text class="quota-text" v-if="tier.quota > 0">剩余 {{ tier.remaining }} 份</text>
|
||||||
</view>
|
</view>
|
||||||
@ -156,7 +160,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, computed } from 'vue'
|
import { ref, reactive } from 'vue'
|
||||||
import { onLoad } from '@dcloudio/uni-app'
|
import { onLoad } from '@dcloudio/uni-app'
|
||||||
import { getTasks, getTaskProgress, claimTaskReward } from '../../api/appUser'
|
import { getTasks, getTaskProgress, claimTaskReward } from '../../api/appUser'
|
||||||
import { vibrateShort } from '@/utils/vibrate.js'
|
import { vibrateShort } from '@/utils/vibrate.js'
|
||||||
@ -177,7 +181,7 @@ const userProgress = reactive({
|
|||||||
})
|
})
|
||||||
|
|
||||||
// BUG修复:每个任务独立存储进度数据
|
// BUG修复:每个任务独立存储进度数据
|
||||||
const taskProgress = reactive({}) // { taskId: { orderCount, orderAmount, inviteCount, firstOrder } }
|
const taskProgress = reactive({}) // { taskId: { orderCount, orderAmount, inviteCount, firstOrder, subProgress } }
|
||||||
|
|
||||||
// 获取用户ID
|
// 获取用户ID
|
||||||
function getUserId() {
|
function getUserId() {
|
||||||
@ -215,6 +219,51 @@ function getTaskIcon(task) {
|
|||||||
return '⭐'
|
return '⭐'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeSubProgress(task, progressData) {
|
||||||
|
const rawList = Array.isArray(progressData?.sub_progress)
|
||||||
|
? progressData.sub_progress.map(item => ({
|
||||||
|
activity_id: item.activity_id,
|
||||||
|
order_amount: item.order_amount || 0,
|
||||||
|
order_count: item.order_count || 0
|
||||||
|
}))
|
||||||
|
: []
|
||||||
|
const hasGlobalTier = (task?.tiers || []).some(
|
||||||
|
t => (t.activity_id || 0) === 0 && (t.metric === 'order_amount' || t.metric === 'order_count')
|
||||||
|
)
|
||||||
|
const hasGlobalEntry = rawList.some(item => !item.activity_id)
|
||||||
|
if (hasGlobalTier && !hasGlobalEntry) {
|
||||||
|
rawList.unshift({
|
||||||
|
activity_id: 0,
|
||||||
|
order_amount: progressData?.order_amount || 0,
|
||||||
|
order_count: progressData?.order_count || 0,
|
||||||
|
label: '累计进度'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return rawList
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSubProgressLabel(sub) {
|
||||||
|
if (!sub) return ''
|
||||||
|
if (sub.label) return sub.label
|
||||||
|
if (sub.activity_id > 0) return `活动 ${sub.activity_id}`
|
||||||
|
return '整体进度'
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSubProgressValue(sub, task) {
|
||||||
|
if (!sub) return ''
|
||||||
|
const activityId = sub.activity_id || 0
|
||||||
|
if (hasTierMetric(task, activityId, 'order_amount')) {
|
||||||
|
return `¥${formatAmount(sub.order_amount || 0)}`
|
||||||
|
}
|
||||||
|
if (hasTierMetric(task, activityId, 'order_count')) {
|
||||||
|
return `${sub.order_count || 0}单`
|
||||||
|
}
|
||||||
|
if ((sub.order_amount || 0) > 0) {
|
||||||
|
return `¥${formatAmount(sub.order_amount)}`
|
||||||
|
}
|
||||||
|
return `${sub.order_count || 0}单`
|
||||||
|
}
|
||||||
|
|
||||||
// 获取任务状态类
|
// 获取任务状态类
|
||||||
function getTaskStatusClass(task) {
|
function getTaskStatusClass(task) {
|
||||||
const progress = userProgress.claimedTiers[task.id] || []
|
const progress = userProgress.claimedTiers[task.id] || []
|
||||||
@ -504,13 +553,15 @@ async function fetchData() {
|
|||||||
const p = result.value
|
const p = result.value
|
||||||
const taskId = list[index].id
|
const taskId = list[index].id
|
||||||
|
|
||||||
|
const currentTask = list[index]
|
||||||
|
const normalizedSubProgress = normalizeSubProgress(currentTask, p)
|
||||||
// BUG修复:每个任务独立存储进度数据
|
// BUG修复:每个任务独立存储进度数据
|
||||||
taskProgress[taskId] = {
|
taskProgress[taskId] = {
|
||||||
orderCount: p.order_count || 0,
|
orderCount: p.order_count || 0,
|
||||||
orderAmount: p.order_amount || 0,
|
orderAmount: p.order_amount || 0,
|
||||||
inviteCount: p.invite_count || 0,
|
inviteCount: p.invite_count || 0,
|
||||||
firstOrder: p.first_order || false,
|
firstOrder: p.first_order || false,
|
||||||
subProgress: p.sub_progress || [] // 新增:独立进度列表
|
subProgress: normalizedSubProgress // 新增:独立进度列表
|
||||||
}
|
}
|
||||||
|
|
||||||
// 聚合进度指标 (取各任务返回的最大值 - 仅用于顶部统计卡片显示)
|
// 聚合进度指标 (取各任务返回的最大值 - 仅用于顶部统计卡片显示)
|
||||||
@ -536,24 +587,88 @@ onLoad(() => {
|
|||||||
fetchData()
|
fetchData()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 计算普通档位进度条百分比
|
||||||
|
function getTierProgressPercent(task, tier) {
|
||||||
|
const metric = tier.metric || ''
|
||||||
|
// first_order 是布尔值,不显示进度条
|
||||||
|
if (metric === 'first_order') return ''
|
||||||
|
|
||||||
|
const threshold = tier.threshold || 0
|
||||||
|
if (threshold <= 0) return ''
|
||||||
|
|
||||||
|
const progress = taskProgress[task.id] || {}
|
||||||
|
|
||||||
|
let current = 0
|
||||||
|
|
||||||
|
// 如果档位关联了特定活动,从 subProgress 取值
|
||||||
|
if (tier.activity_id > 0 && progress.subProgress) {
|
||||||
|
const sub = progress.subProgress.find(s => s.activity_id === tier.activity_id)
|
||||||
|
if (sub) {
|
||||||
|
if (metric === 'order_amount') {
|
||||||
|
current = sub.order_amount || 0
|
||||||
|
} else if (metric === 'order_count') {
|
||||||
|
current = sub.order_count || 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
current = 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 使用任务总进度
|
||||||
|
if (metric === 'order_count') {
|
||||||
|
current = progress.orderCount || 0
|
||||||
|
} else if (metric === 'order_amount') {
|
||||||
|
current = progress.orderAmount || 0
|
||||||
|
} else if (metric === 'invite_count') {
|
||||||
|
current = progress.inviteCount || 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(current / threshold * 100, 100).toFixed(0) + '%'
|
||||||
|
}
|
||||||
|
|
||||||
// 计算子进度条宽度
|
// 计算子进度条宽度
|
||||||
function getSubProgressWidth(sub, task) {
|
function getSubProgressWidth(sub, task) {
|
||||||
// 尝试找该任务的最大金额门槛作为分母
|
if (!sub || !task) return '0%'
|
||||||
let maxThreshold = 0
|
const activityId = sub.activity_id || 0
|
||||||
if (task.tiers && task.tiers.length > 0) {
|
let metric = null
|
||||||
// 过滤出与金额相关的档位
|
if (hasTierMetric(task, activityId, 'order_amount')) {
|
||||||
const amountTiers = task.tiers.filter(t => t.metric === 'order_amount')
|
metric = 'order_amount'
|
||||||
if (amountTiers.length > 0) {
|
} else if (hasTierMetric(task, activityId, 'order_count')) {
|
||||||
maxThreshold = Math.max(...amountTiers.map(t => t.threshold || 0))
|
metric = 'order_count'
|
||||||
|
} else if ((sub.order_amount || 0) > 0) {
|
||||||
|
metric = 'order_amount'
|
||||||
|
} else {
|
||||||
|
metric = 'order_count'
|
||||||
}
|
}
|
||||||
|
const maxThreshold = getMaxThresholdByMetric(task, metric, activityId)
|
||||||
|
let denominator = maxThreshold
|
||||||
|
if (denominator <= 0) {
|
||||||
|
denominator = metric === 'order_count' ? Math.max(sub.order_count || 0, 100) : Math.max(sub.order_amount || 0, 20000)
|
||||||
}
|
}
|
||||||
|
const current = metric === 'order_count' ? (sub.order_count || 0) : (sub.order_amount || 0)
|
||||||
// 如果没有找到阈值,默认100% (或者可以设一个默认值如 200元)
|
const percent = denominator === 0 ? 0 : Math.min((current / denominator) * 100, 100)
|
||||||
if (maxThreshold === 0) maxThreshold = 20000 // 默认200元
|
|
||||||
|
|
||||||
const percent = Math.min((sub.order_amount || 0) / maxThreshold * 100, 100)
|
|
||||||
return `${percent}%`
|
return `${percent}%`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasTierMetric(task, activityId, metric) {
|
||||||
|
if (!task?.tiers || !metric) return false
|
||||||
|
const target = activityId || 0
|
||||||
|
return task.tiers.some(t => (t.activity_id || 0) === target && t.metric === metric)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMaxThresholdByMetric(task, metric, activityId) {
|
||||||
|
if (!task?.tiers || !metric) return 0
|
||||||
|
const target = activityId || 0
|
||||||
|
const related = task.tiers.filter(t => (t.activity_id || 0) === target && t.metric === metric)
|
||||||
|
if (related.length === 0) return 0
|
||||||
|
return Math.max(...related.map(t => t.threshold || 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAmount(cents) {
|
||||||
|
const value = Number(cents || 0) / 100
|
||||||
|
if (Number.isNaN(value)) return '0'
|
||||||
|
return Number.isInteger(value) ? value.toString() : value.toFixed(2)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@ -969,6 +1084,22 @@ function getSubProgressWidth(sub, task) {
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tier-bar-bg {
|
||||||
|
width: 120rpx;
|
||||||
|
height: 8rpx;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-radius: 4rpx;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tier-bar-fill {
|
||||||
|
height: 100%;
|
||||||
|
background: $brand-primary;
|
||||||
|
border-radius: 4rpx;
|
||||||
|
transition: width 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
.quota-text {
|
.quota-text {
|
||||||
font-size: 18rpx;
|
font-size: 18rpx;
|
||||||
color: $brand-primary;
|
color: $brand-primary;
|
||||||
|
|||||||
0
pages.json
Normal file → Executable file
0
pages/cabinet/index.vue
Normal file → Executable file
0
pages/index/index.vue
Normal file → Executable file
0
pages/login/index.vue
Normal file → Executable file
0
pages/mine/index.vue
Normal file → Executable file
0
pages/shop/index.vue
Normal file → Executable file
0
project.config.json
Normal file → Executable file
0
project.private.config.json
Normal file → Executable file
0
static/logo.png
Normal file → Executable file
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 92 KiB |
0
static/share_invite.png
Normal file → Executable file
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
0
static/tab/box.png
Normal file → Executable file
|
Before Width: | Height: | Size: 373 B After Width: | Height: | Size: 373 B |
0
static/tab/box_active.png
Normal file → Executable file
|
Before Width: | Height: | Size: 651 B After Width: | Height: | Size: 651 B |
0
static/tab/home.png
Normal file → Executable file
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
0
static/tab/home_active.png
Normal file → Executable file
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
0
static/tab/profile.png
Normal file → Executable file
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
0
static/tab/profile_active.png
Normal file → Executable file
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
0
static/tab/shop.png
Normal file → Executable file
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
0
static/tab/shop_active.png
Normal file → Executable file
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |
0
uni.promisify.adaptor.js
Normal file → Executable file
0
utils/activity.js
Normal file → Executable file
0
utils/cache.js
Normal file → Executable file
0
utils/checkPhone.js
Normal file → Executable file
0
utils/format.js
Normal file → Executable file
0
utils/nakama-adapter.js
Normal file → Executable file
0
utils/nakama-js/nakama-js.js
Normal file → Executable file
0
utils/nakamaManager.js
Normal file → Executable file
0
utils/payment.js
Normal file → Executable file
4
utils/request.js
Normal file → Executable file
@ -1,5 +1,5 @@
|
|||||||
// const BASE_URL = 'http://127.0.0.1:9991'
|
const BASE_URL = 'http://127.0.0.1:9991'
|
||||||
const BASE_URL = 'https://kdy.1024tool.vip'
|
// const BASE_URL = 'https://kdy.1024tool.vip'
|
||||||
let authModalShown = false
|
let authModalShown = false
|
||||||
|
|
||||||
function handleAuthExpired() {
|
function handleAuthExpired() {
|
||||||
|
|||||||