chore: upload app_client

This commit is contained in:
ty200947752 2025-11-24 22:37:11 +08:00
commit f6f84d10d7
138 changed files with 13725 additions and 0 deletions

View File

@ -0,0 +1,46 @@
## 接口梳理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. 自测与联调,完成交付

View File

@ -0,0 +1,102 @@
## 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\_codeminiapp/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`balanceglobals.d.ts:773
* `GET /api/app/users/{user_id}/stats`apiDefinitions.js:126
* 响应: `App_user_stats_response`coupon\_count、item\_card\_count、points\_balanceglobals.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、112miniapp/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` 响应处理一致性。

View File

@ -0,0 +1,35 @@
## 目标
- 首页 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`,确认占位滑块与默认滚动文案显示。
- 在微信小程序/浏览器预览,验证滚动流畅度与样式适配。

17
App.vue Normal file
View File

@ -0,0 +1,17 @@
<script>
export default {
onLaunch: function() {
console.log('App Launch')
},
onShow: function() {
console.log('App Show')
},
onHide: function() {
console.log('App Hide')
}
}
</script>
<style>
/*每个页面公共css */
</style>

60
api/appUser.js Normal file
View File

@ -0,0 +1,60 @@
import { request, authRequest } from '../utils/request'
export function wechatLogin(code, invite_code) {
const data = invite_code ? { code, invite_code } : { code }
return request({ url: '/api/app/users/weixin/login', method: 'POST', data })
}
export function bindPhone(user_id, code, extraHeader = {}) {
return authRequest({ url: `/api/app/users/${user_id}/phone/bind`, method: 'POST', data: { code }, header: extraHeader })
}
export function getUserStats(user_id) {
return authRequest({ url: `/api/app/users/${user_id}/stats`, method: 'GET' })
}
export function getPointsBalance(user_id) {
return authRequest({ url: `/api/app/users/${user_id}/points/balance`, method: 'GET' })
}
export function getPointsRecords(user_id, page = 1, page_size = 20) {
return authRequest({ url: `/api/app/users/${user_id}/points`, method: 'GET', data: { page, page_size } })
}
export function getOrders(user_id, status, page = 1, page_size = 20) {
const data = { page, page_size }
if (status) data.status = status
return authRequest({ url: `/api/app/users/${user_id}/orders`, method: 'GET', data })
}
export function listAddresses(user_id) {
return authRequest({ url: `/api/app/users/${user_id}/addresses`, method: 'GET' })
}
export function addAddress(user_id, payload) {
return authRequest({ url: `/api/app/users/${user_id}/addresses`, method: 'POST', data: payload })
}
export function updateAddress(user_id, address_id, payload) {
return authRequest({ url: `/api/app/users/${user_id}/addresses/${address_id}`, method: 'PUT', data: payload })
}
export function deleteAddress(user_id, address_id) {
return authRequest({ url: `/api/app/users/${user_id}/addresses/${address_id}`, method: 'DELETE' })
}
export function setDefaultAddress(user_id, address_id) {
return authRequest({ url: `/api/app/users/${user_id}/addresses/${address_id}/default`, method: 'PUT' })
}
export function getActivityDetail(activity_id) {
return authRequest({ url: `/api/app/activities/${activity_id}`, method: 'GET' })
}
export function getActivityIssues(activity_id) {
return authRequest({ url: `/api/app/activities/${activity_id}/issues`, method: 'GET' })
}
export function getActivityIssueRewards(activity_id, issue_id) {
return authRequest({ url: `/api/app/activities/${activity_id}/issues/${issue_id}/rewards`, method: 'GET' })
}

146
console-1763271273297.log Normal file
View File

@ -0,0 +1,146 @@
uni.api.esm.js:502 App Launch
uni.api.esm.js:502 App Show
VM75:363 Error: not node js file system!path:/saaa_config.json; go __invokeHandler__ readFile worker? false
h @ VM75:363
a.WeixinJSCore.invokeHandler @ VM75:363
b @ WAServiceMainContext.js:1
invoke @ WAServiceMainContext.js:1
invoke @ WAServiceMainContext.js:1
u @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
ae @ WAServiceMainContext.js:1
o @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
QX @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
c.emit @ WAServiceMainContext.js:1
emitInternal @ WAServiceMainContext.js:1
e @ WAServiceMainContext.js:1
c.emit @ WAServiceMainContext.js:1
emitInternal @ WAServiceMainContext.js:1
p @ WAServiceMainContext.js:1
_ @ WAServiceMainContext.js:1
B @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
a @ WAServiceMainContext.js:1
s @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
c.emit @ WAServiceMainContext.js:1
emit @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
_emit @ WAServiceMainContext.js:1
emit @ WAServiceMainContext.js:1
emit @ WAServiceMainContext.js:1
subscribeHandler @ WAServiceMainContext.js:1
ret.subscribeHandler
Show 8 more frames
uni.api.esm.js:502 mine onShow token: isLogin: false phoneBound: false
uni.api.esm.js:502 mine login modal confirm: true
WAServiceMainContext.js:1 [wxapplib]] backgroundfetch privacy fail {"errno":101,"errMsg":"private_getBackgroundFetchData:fail private_getBackgroundFetchData:fail:jsapi invalid request data"}
(anonymous) @ WAServiceMainContext.js:1
i @ WAServiceMainContext.js:1
s @ WAServiceMainContext.js:1
fail @ WAServiceMainContext.js:1
p @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
u @ WAServiceMainContext.js:1
fail @ WAServiceMainContext.js:1
p @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
G @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
r @ WAServiceMainContext.js:1
s @ WAServiceMainContext.js:1
callAndRemove @ WAServiceMainContext.js:1
invokeCallbackHandler @ WAServiceMainContext.js:1
ret.invokeCallbackHandler
uni.api.esm.js:502 login_flow start getPhoneNumber, codeExists: true
uni.api.esm.js:502 login_flow uni.login success, loginCode exists: true
uni.api.esm.js:502 HTTP request POST /api/app/users/weixin/login data {code: "0f1tQZFa1a5TFK0U6UGa11gIyJ0tQZF6"} headers {Accept: "application/json", content-type: "application/json", X-Requested-With: "XMLHttpRequest", Accept-Language: "zh_CN", X-App-Client: "uni-app", …}
uni.api.esm.js:502 HTTP response POST /api/app/users/weixin/login status 200 body {user_id: 813, nickname: "善良的巴乔", avatar: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAA…BNCqAnHlGBRgAAQAA//8HSv0f1XCh/AAAAABJRU5ErkJggg==", invite_code: "AZCHW75Z", token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ODEzL…MzNH0.ZUItPpkDEbAhTy8g80PGRxLxQ_JXqS4jlu_vz7IrMAU"}avatar: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACAAQMAAAD58POIAAAABlBMVEUAAABmAADpPQ5dAAAAAXRSTlMAQObYZgAAAD9JREFUeJxiAIH//xHkSBdAEofwRrjAKEAFsMQCTzQjXgCRSMDsES4AoWAALj6iBRABNCqAnHlGBRgAAQAA//8HSv0f1XCh/AAAAABJRU5ErkJggg=="invite_code: "AZCHW75Z"nickname: "善良的巴乔"token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ODEzLCJ1c2VybmFtZSI6IuWWhOiJr-eahOW3tOS5lCIsIm5pY2tuYW1lIjoi5ZaE6Imv55qE5be05LmUIiwiaXNfc3VwZXIiOjAsInBsYXRmb3JtIjoiQVBQIiwiZXhwIjoxNzY1ODc3MzM0LCJuYmYiOjE3NjMyODUzMzQsImlhdCI6MTc2MzI4NTMzNH0.ZUItPpkDEbAhTy8g80PGRxLxQ_JXqS4jlu_vz7IrMAU"user_id: 813[[Prototype]]: Object
uni.api.esm.js:502 login_flow wechatLogin response user_id: 813
uni.api.esm.js:502 login_flow token stored
uni.api.esm.js:502 login_flow user_id stored: 813
uni.api.esm.js:502 login_flow bindPhone start
uni.api.esm.js:502 HTTP request POST /api/app/users/813/phone/bind data {code: "c568695e007ec9d5dca1ecaf4d5452dd378fd42542d4a90fd9ad7b942b5d4248"} headers {Accept: "application/json", content-type: "application/json", X-Requested-With: "XMLHttpRequest", Accept-Language: "zh_CN", X-App-Client: "uni-app", …}
uni.api.esm.js:502 HTTP response POST /api/app/users/813/phone/bind status 401 body {code: 10103, message: "您的账号登录过期,请重新登录。"}
uni.api.esm.js:502 login_flow bindPhone 401, try re-login and retry
__f__ @ uni.api.esm.js:502
Ku.wu.__f__ @ mp.esm.js:523
(anonymous) @ index.vue:59
s @ app-service.js:1219
(anonymous) @ app-service.js:1219
(anonymous) @ app-service.js:1219
asyncGeneratorStep @ app-service.js:1174
i @ app-service.js:1174
Promise.then (async)
asyncGeneratorStep @ app-service.js:1174
c @ app-service.js:1174
Promise.then (async)
asyncGeneratorStep @ app-service.js:1174
c @ app-service.js:1174
(anonymous) @ app-service.js:1174
(anonymous) @ app-service.js:1174
(anonymous) @ index.vue:95
I.forEach.v.<computed> @ WAServiceMainContext.js:1
p @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
i @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
G @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
r @ WAServiceMainContext.js:1
s @ WAServiceMainContext.js:1
callAndRemove @ WAServiceMainContext.js:1
invokeCallbackHandler @ WAServiceMainContext.js:1
ret.invokeCallbackHandler
uni.api.esm.js:502 HTTP request POST /api/app/users/weixin/login data {code: "0e15cAll2FVxFg4MxDnl2v9KUj15cAlO"} headers {Accept: "application/json", content-type: "application/json", X-Requested-With: "XMLHttpRequest", Accept-Language: "zh_CN", X-App-Client: "uni-app", …}
uni.api.esm.js:502 HTTP response POST /api/app/users/weixin/login status 200 body {user_id: 813, nickname: "善良的巴乔", avatar: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAA…BNCqAnHlGBRgAAQAA//8HSv0f1XCh/AAAAABJRU5ErkJggg==", invite_code: "AZCHW75Z", token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ODEzL…MzNH0.ZUItPpkDEbAhTy8g80PGRxLxQ_JXqS4jlu_vz7IrMAU"}
uni.api.esm.js:502 HTTP request POST /api/app/users/813/phone/bind data {code: "c568695e007ec9d5dca1ecaf4d5452dd378fd42542d4a90fd9ad7b942b5d4248"} headers {Accept: "application/json", content-type: "application/json", X-Requested-With: "XMLHttpRequest", Accept-Language: "zh_CN", X-App-Client: "uni-app", …}
uni.api.esm.js:502 HTTP response POST /api/app/users/813/phone/bind status 401 body {code: 10103, message: "您的账号登录过期,请重新登录。"}
uni.api.esm.js:502 login_flow error: 您的账号登录过期,请重新登录。 status: 401
__f__ @ uni.api.esm.js:502
Ku.wu.__f__ @ mp.esm.js:523
(anonymous) @ index.vue:90
s @ app-service.js:1219
(anonymous) @ app-service.js:1219
(anonymous) @ app-service.js:1219
asyncGeneratorStep @ app-service.js:1174
i @ app-service.js:1174
Promise.then (async)
asyncGeneratorStep @ app-service.js:1174
c @ app-service.js:1174
Promise.then (async)
asyncGeneratorStep @ app-service.js:1174
c @ app-service.js:1174
Promise.then (async)
asyncGeneratorStep @ app-service.js:1174
i @ app-service.js:1174
Promise.then (async)
asyncGeneratorStep @ app-service.js:1174
c @ app-service.js:1174
Promise.then (async)
asyncGeneratorStep @ app-service.js:1174
c @ app-service.js:1174
(anonymous) @ app-service.js:1174
(anonymous) @ app-service.js:1174
(anonymous) @ index.vue:95
I.forEach.v.<computed> @ WAServiceMainContext.js:1
p @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
i @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
G @ WAServiceMainContext.js:1
(anonymous) @ WAServiceMainContext.js:1
r @ WAServiceMainContext.js:1
s @ WAServiceMainContext.js:1
callAndRemove @ WAServiceMainContext.js:1
invokeCallbackHandler @ WAServiceMainContext.js:1
ret.invokeCallbackHandler

20
index.html Normal file
View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<script>
var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') + '" />')
</script>
<title></title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/main.js"></script>
</body>
</html>

22
main.js Normal file
View File

@ -0,0 +1,22 @@
import App from './App'
// #ifndef VUE3
import Vue from 'vue'
import './uni.promisify.adaptor'
Vue.config.productionTip = false
App.mpType = 'app'
const app = new Vue({
...App
})
app.$mount()
// #endif
// #ifdef VUE3
import { createSSRApp } from 'vue'
export function createApp() {
const app = createSSRApp(App)
return {
app
}
}
// #endif

75
manifest.json Normal file
View File

@ -0,0 +1,75 @@
{
"name" : "app_client",
"appid" : "",
"description" : "",
"versionName" : "1.0.0",
"versionCode" : "100",
"transformPx" : false,
/* 5+App */
"app-plus" : {
"usingComponents" : true,
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3,
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
},
/* */
"modules" : {},
/* */
"distribute" : {
/* android */
"android" : {
"permissions" : [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
/* ios */
"ios" : {},
/* SDK */
"sdkConfigs" : {}
}
},
/* */
"quickapp" : {},
/* */
"mp-weixin" : {
"appid" : "wx26ad074017e1e63f",
"setting" : {
"urlCheck" : false,
"minified" : true,
"es6" : true,
"postcss" : true
},
"usingComponents" : true
},
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"uniStatistics" : {
"enable" : false
},
"vueVersion" : "3"
}

121
pages.json Normal file
View File

@ -0,0 +1,121 @@
{
"pages": [ //pageshttps://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "uni-app"
}
}
,
{
"path": "pages/login/index",
"style": {
"navigationBarTitleText": "登录"
}
}
,
{
"path": "pages/shop/index",
"style": {
"navigationBarTitleText": "商城"
}
}
,
{
"path": "pages/cabinet/index",
"style": {
"navigationBarTitleText": "货柜"
}
}
,
{
"path": "pages/mine/index",
"style": {
"navigationBarTitleText": "我的"
}
}
,
{
"path": "pages/points/index",
"style": {
"navigationBarTitleText": "积分记录"
}
}
,
{
"path": "pages/orders/index",
"style": {
"navigationBarTitleText": "我的订单"
}
}
,
{
"path": "pages/address/index",
"style": {
"navigationBarTitleText": "地址管理"
}
}
,
{
"path": "pages/address/edit",
"style": {
"navigationBarTitleText": "编辑地址"
}
}
,
{
"path": "pages/help/index",
"style": {
"navigationBarTitleText": "使用帮助"
}
}
,
{
"path": "pages/agreement/user",
"style": {
"navigationBarTitleText": "用户协议"
}
}
,
{
"path": "pages/agreement/purchase",
"style": {
"navigationBarTitleText": "购买协议"
}
}
,
{
"path": "pages/activity/yifanshang/index",
"style": { "navigationBarTitleText": "一番赏" }
}
,
{
"path": "pages/activity/wuxianshang/index",
"style": { "navigationBarTitleText": "无限赏" }
}
,
{
"path": "pages/activity/duiduipeng/index",
"style": { "navigationBarTitleText": "对对碰" }
}
],
"tabBar": {
"color": "#7A7E83",
"selectedColor": "#007AFF",
"backgroundColor": "#FFFFFF",
"borderStyle": "black",
"list": [
{ "pagePath": "pages/index/index", "text": "首页", "iconPath": "static/tab/home.png", "selectedIconPath": "static/tab/home_active.png" },
{ "pagePath": "pages/shop/index", "text": "商城", "iconPath": "static/tab/shop.png", "selectedIconPath": "static/tab/shop_active.png" },
{ "pagePath": "pages/cabinet/index", "text": "货柜", "iconPath": "static/tab/box.png", "selectedIconPath": "static/tab/box_active.png" },
{ "pagePath": "pages/mine/index", "text": "我的", "iconPath": "static/tab/profile.png", "selectedIconPath": "static/tab/profile_active.png" }
]
},
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
"uniIdRouter": {}
}

View File

@ -0,0 +1,65 @@
<template>
<scroll-view class="page" scroll-y>
<view class="banner" v-if="detail.banner">
<image class="banner-img" :src="detail.banner" mode="widthFix" />
</view>
<view class="header">
<view class="title">{{ detail.name || detail.title || '-' }}</view>
<view class="meta">分类{{ detail.category_name || '对对碰' }}</view>
<view class="meta" v-if="detail.price_draw !== undefined">参与价{{ detail.price_draw }}</view>
<view class="meta" v-if="detail.status !== undefined">状态{{ statusText }}</view>
</view>
<view class="actions">
<button class="btn" @click="onPreviewBanner">查看图片</button>
<button class="btn primary" @click="onParticipate">立即参与</button>
</view>
</scroll-view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getActivityDetail } from '../../api/appUser'
const detail = ref({})
const statusText = ref('')
function statusToText(s) {
if (s === 1) return '进行中'
if (s === 0) return '未开始'
if (s === 2) return '已结束'
return String(s || '')
}
async function fetchDetail(id) {
const data = await getActivityDetail(id)
detail.value = data || {}
statusText.value = statusToText(detail.value.status)
}
function onPreviewBanner() {
const url = detail.value.banner || ''
if (url) uni.previewImage({ urls: [url], current: url })
}
function onParticipate() {
uni.showToast({ title: '功能待接入', icon: 'none' })
}
onLoad((opts) => {
const id = (opts && opts.id) || ''
if (id) fetchDetail(id)
})
</script>
<style scoped>
.page { height: 100vh }
.banner { padding: 24rpx }
.banner-img { width: 100% }
.header { padding: 0 24rpx }
.title { font-size: 36rpx; font-weight: 700 }
.meta { margin-top: 8rpx; font-size: 26rpx; color: #666 }
.actions { display: flex; padding: 24rpx; gap: 16rpx }
.btn { flex: 1 }
.primary { background-color: #007AFF; color: #fff }
</style>

View File

@ -0,0 +1,153 @@
<template>
<scroll-view class="page" scroll-y>
<view class="banner" v-if="detail.banner">
<image class="banner-img" :src="detail.banner" mode="widthFix" />
</view>
<view class="header">
<view class="title">{{ detail.name || detail.title || '-' }}</view>
<view class="meta">分类{{ detail.category_name || '对对碰' }}</view>
<view class="meta" v-if="detail.price_draw !== undefined">参与价{{ detail.price_draw }}</view>
<view class="meta" v-if="detail.status !== undefined">状态{{ statusText }}</view>
</view>
<view class="actions">
<button class="btn" @click="onPreviewBanner">查看图片</button>
<button class="btn primary" @click="onParticipate">立即参与</button>
</view>
<view class="issues">
<view class="issues-title">期数</view>
<view v-if="issues.length" class="issues-list">
<view class="issue-item" v-for="it in issues" :key="it.id">
<text class="issue-title">{{ it.title || ('第' + (it.no || it.index || it.issue_no || '-') + '期') }}</text>
<text class="issue-status" v-if="it.status_text">{{ it.status_text }}</text>
<view class="rewards" v-if="rewardsMap[it.id] && rewardsMap[it.id].length">
<view class="reward" v-for="rw in rewardsMap[it.id]" :key="rw.id">
<image v-if="rw.image" class="reward-img" :src="rw.image" mode="aspectFill" />
<view class="reward-texts">
<text class="reward-title">{{ rw.title }}</text>
<text class="reward-meta" v-if="rw.rarity || rw.odds">{{ [rw.rarity, rw.odds].filter(Boolean).join(' · ') }}</text>
</view>
</view>
</view>
<view class="rewards-empty" v-else>暂无奖励配置</view>
</view>
</view>
<view v-else class="issues-empty">暂无期数</view>
</view>
</scroll-view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getActivityDetail, getActivityIssues, getActivityIssueRewards } from '../../../api/appUser'
const detail = ref({})
const statusText = ref('')
const issues = ref([])
const rewardsMap = ref({})
function statusToText(s) {
if (s === 1) return '进行中'
if (s === 0) return '未开始'
if (s === 2) return '已结束'
return String(s || '')
}
async function fetchDetail(id) {
const data = await getActivityDetail(id)
detail.value = data || {}
statusText.value = statusToText(detail.value.status)
}
function unwrap(list) {
if (Array.isArray(list)) return list
const obj = list || {}
const data = obj.data || {}
const arr = obj.list || obj.items || data.list || data.items || data
return Array.isArray(arr) ? arr : []
}
function normalizeIssues(list) {
const arr = unwrap(list)
return arr.map((i, idx) => ({
id: i.id ?? String(idx),
title: i.title ?? i.name ?? '',
no: i.no ?? i.index ?? i.issue_no ?? i.issue_number ?? null,
status_text: i.status_text ?? (i.status === 1 ? '进行中' : i.status === 0 ? '未开始' : i.status === 2 ? '已结束' : '')
}))
}
function normalizeRewards(list) {
const arr = unwrap(list)
return arr.map((i, idx) => ({
id: i.id ?? String(idx),
title: i.title ?? i.name ?? '',
image: i.image ?? i.img ?? i.pic ?? i.banner ?? '',
odds: i.odds ?? i.rate ?? i.probability ?? i.prob ?? '',
rarity: i.rarity ?? i.rarity_name ?? ''
}))
}
async function fetchRewardsForIssues(activityId) {
const list = issues.value || []
const promises = list.map(it => getActivityIssueRewards(activityId, it.id))
const results = await Promise.allSettled(promises)
results.forEach((res, i) => {
const issueId = list[i] && list[i].id
if (!issueId) return
const value = res.status === 'fulfilled' ? normalizeRewards(res.value) : []
rewardsMap.value = { ...(rewardsMap.value || {}), [issueId]: value }
})
}
async function fetchIssues(id) {
const data = await getActivityIssues(id)
issues.value = normalizeIssues(data)
await fetchRewardsForIssues(id)
}
function onPreviewBanner() {
const url = detail.value.banner || ''
if (url) uni.previewImage({ urls: [url], current: url })
}
function onParticipate() {
uni.showToast({ title: '功能待接入', icon: 'none' })
}
onLoad((opts) => {
const id = (opts && opts.id) || ''
if (id) {
fetchDetail(id)
fetchIssues(id)
}
})
</script>
<style scoped>
.page { height: 100vh }
.banner { padding: 24rpx }
.banner-img { width: 100% }
.header { padding: 0 24rpx }
.title { font-size: 36rpx; font-weight: 700 }
.meta { margin-top: 8rpx; font-size: 26rpx; color: #666 }
.actions { display: flex; padding: 24rpx; gap: 16rpx }
.btn { flex: 1 }
.primary { background-color: #007AFF; color: #fff }
.issues { background: #fff; border-radius: 12rpx; margin: 0 24rpx 24rpx; padding: 16rpx }
.issues-title { font-size: 30rpx; font-weight: 600; margin-bottom: 12rpx }
.issues-list { }
.issue-item { display: flex; justify-content: space-between; padding: 12rpx 0; border-bottom: 1rpx solid #f0f0f0 }
.issue-item:last-child { border-bottom: 0 }
.issue-title { font-size: 26rpx }
.issue-status { font-size: 24rpx; color: #666 }
.rewards { width: 100%; margin-top: 12rpx }
.reward { display: flex; align-items: center; margin-bottom: 8rpx }
.reward-img { width: 80rpx; height: 80rpx; border-radius: 8rpx; margin-right: 12rpx; background: #f5f5f5 }
.reward-texts { display: flex; flex-direction: column }
.reward-title { font-size: 26rpx }
.reward-meta { font-size: 22rpx; color: #888 }
.rewards-empty { font-size: 24rpx; color: #999 }
.issues-empty { font-size: 24rpx; color: #999 }
</style>

View File

@ -0,0 +1,65 @@
<template>
<scroll-view class="page" scroll-y>
<view class="banner" v-if="detail.banner">
<image class="banner-img" :src="detail.banner" mode="widthFix" />
</view>
<view class="header">
<view class="title">{{ detail.name || detail.title || '-' }}</view>
<view class="meta">分类{{ detail.category_name || '无限赏' }}</view>
<view class="meta" v-if="detail.price_draw !== undefined">单次抽选{{ detail.price_draw }}</view>
<view class="meta" v-if="detail.status !== undefined">状态{{ statusText }}</view>
</view>
<view class="actions">
<button class="btn" @click="onPreviewBanner">查看图片</button>
<button class="btn primary" @click="onParticipate">立即参与</button>
</view>
</scroll-view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getActivityDetail } from '../../api/appUser'
const detail = ref({})
const statusText = ref('')
function statusToText(s) {
if (s === 1) return '进行中'
if (s === 0) return '未开始'
if (s === 2) return '已结束'
return String(s || '')
}
async function fetchDetail(id) {
const data = await getActivityDetail(id)
detail.value = data || {}
statusText.value = statusToText(detail.value.status)
}
function onPreviewBanner() {
const url = detail.value.banner || ''
if (url) uni.previewImage({ urls: [url], current: url })
}
function onParticipate() {
uni.showToast({ title: '功能待接入', icon: 'none' })
}
onLoad((opts) => {
const id = (opts && opts.id) || ''
if (id) fetchDetail(id)
})
</script>
<style scoped>
.page { height: 100vh }
.banner { padding: 24rpx }
.banner-img { width: 100% }
.header { padding: 0 24rpx }
.title { font-size: 36rpx; font-weight: 700 }
.meta { margin-top: 8rpx; font-size: 26rpx; color: #666 }
.actions { display: flex; padding: 24rpx; gap: 16rpx }
.btn { flex: 1 }
.primary { background-color: #007AFF; color: #fff }
</style>

View File

@ -0,0 +1,151 @@
<template>
<scroll-view class="page" scroll-y>
<view class="banner" v-if="detail.banner">
<image class="banner-img" :src="detail.banner" mode="widthFix" />
</view>
<view class="header">
<view class="title">{{ detail.name || detail.title || '-' }}</view>
<view class="meta">分类{{ detail.category_name || '无限赏' }}</view>
<view class="meta" v-if="detail.price_draw !== undefined">单次抽选{{ detail.price_draw }}</view>
<view class="meta" v-if="detail.status !== undefined">状态{{ statusText }}</view>
</view>
<view class="actions">
<button class="btn" @click="onPreviewBanner">查看图片</button>
<button class="btn primary" @click="onParticipate">立即参与</button>
</view>
<view class="issues">
<view class="issues-title">期数</view>
<view v-if="issues.length" class="issues-list">
<view class="issue-item" v-for="it in issues" :key="it.id">
<text class="issue-title">{{ it.title || ('第' + (it.no || it.index || it.issue_no || '-') + '期') }}</text>
<text class="issue-status" v-if="it.status_text">{{ it.status_text }}</text>
<view class="rewards" v-if="rewardsMap[it.id] && rewardsMap[it.id].length">
<view class="reward" v-for="rw in rewardsMap[it.id]" :key="rw.id">
<image v-if="rw.image" class="reward-img" :src="rw.image" mode="aspectFill" />
<view class="reward-texts">
<text class="reward-title">{{ rw.title }}</text>
<text class="reward-meta" v-if="rw.rarity || rw.odds">{{ [rw.rarity, rw.odds].filter(Boolean).join(' · ') }}</text>
</view>
</view>
</view>
<view class="rewards-empty" v-else>暂无奖励配置</view>
</view>
</view>
<view v-else class="issues-empty">暂无期数</view>
</view>
</scroll-view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getActivityDetail, getActivityIssues, getActivityIssueRewards } from '../../../api/appUser'
const detail = ref({})
const statusText = ref('')
const issues = ref([])
const rewardsMap = ref({})
function statusToText(s) {
if (s === 1) return '进行中'
if (s === 0) return '未开始'
if (s === 2) return '已结束'
return String(s || '')
}
async function fetchDetail(id) {
const data = await getActivityDetail(id)
detail.value = data || {}
statusText.value = statusToText(detail.value.status)
}
function unwrap(list) {
if (Array.isArray(list)) return list
const obj = list || {}
const data = obj.data || {}
const arr = obj.list || obj.items || data.list || data.items || data
return Array.isArray(arr) ? arr : []
}
function normalizeIssues(list) {
const arr = unwrap(list)
return arr.map((i, idx) => ({
id: i.id ?? String(idx),
title: i.title ?? i.name ?? '',
no: i.no ?? i.index ?? i.issue_no ?? i.issue_number ?? null,
status_text: i.status_text ?? (i.status === 1 ? '进行中' : i.status === 0 ? '未开始' : i.status === 2 ? '已结束' : '')
}))
}
function normalizeRewards(list) {
const arr = unwrap(list)
return arr.map((i, idx) => ({
id: i.id ?? String(idx),
title: i.title ?? i.name ?? '',
image: i.image ?? i.img ?? i.pic ?? i.banner ?? '',
odds: i.odds ?? i.rate ?? i.probability ?? i.prob ?? '',
rarity: i.rarity ?? i.rarity_name ?? ''
}))
}
async function fetchRewardsForIssues(activityId) {
const list = issues.value || []
const promises = list.map(it => getActivityIssueRewards(activityId, it.id))
const results = await Promise.allSettled(promises)
results.forEach((res, i) => {
const issueId = list[i] && list[i].id
if (!issueId) return
const value = res.status === 'fulfilled' ? normalizeRewards(res.value) : []
rewardsMap.value = { ...(rewardsMap.value || {}), [issueId]: value }
})
}
async function fetchIssues(id) {
const data = await getActivityIssues(id)
issues.value = normalizeIssues(data)
await fetchRewardsForIssues(id)
}
function onPreviewBanner() {
const url = detail.value.banner || ''
if (url) uni.previewImage({ urls: [url], current: url })
}
function onParticipate() {
uni.showToast({ title: '功能待接入', icon: 'none' })
}
onLoad((opts) => {
const id = (opts && opts.id) || ''
if (id) {
fetchDetail(id)
fetchIssues(id)
}
})
</script>
<style scoped>
.page { height: 100vh }
.banner { padding: 24rpx }
.banner-img { width: 100% }
.header { padding: 0 24rpx }
.title { font-size: 36rpx; font-weight: 700 }
.meta { margin-top: 8rpx; font-size: 26rpx; color: #666 }
.actions { display: flex; padding: 24rpx; gap: 16rpx }
.btn { flex: 1 }
.primary { background-color: #007AFF; color: #fff }
.issues { background: #fff; border-radius: 12rpx; margin: 0 24rpx 24rpx; padding: 16rpx }
.issues-title { font-size: 30rpx; font-weight: 600; margin-bottom: 12rpx }
.issues-list { }
.issue-item { display: flex; justify-content: space-between; padding: 12rpx 0; border-bottom: 1rpx solid #f0f0f0 }
.issue-item:last-child { border-bottom: 0 }
.issue-title { font-size: 26rpx }
.issue-status { font-size: 24rpx; color: #666 }
.rewards { width: 100%; margin-top: 12rpx }
.reward { display: flex; align-items: center; margin-bottom: 8rpx }
.reward-img { width: 80rpx; height: 80rpx; border-radius: 8rpx; margin-right: 12rpx; background: #f5f5f5 }
.reward-texts { display: flex; flex-direction: column }
.reward-title { font-size: 26rpx }
.reward-meta { font-size: 22rpx; color: #888 }
.rewards-empty { font-size: 24rpx; color: #999 }
.issues-empty { font-size: 24rpx; color: #999 }
</style>

View File

@ -0,0 +1,66 @@
<template>
<scroll-view class="page" scroll-y>
<view class="banner" v-if="detail.banner">
<image class="banner-img" :src="detail.banner" mode="widthFix" />
</view>
<view class="header">
<view class="title">{{ detail.name || detail.title || '-' }}</view>
<view class="meta">分类{{ detail.category_name || '一番赏' }}</view>
<view class="meta" v-if="detail.price_draw !== undefined">抽选价{{ detail.price_draw }}</view>
<view class="meta" v-if="detail.status !== undefined">状态{{ statusText }}</view>
</view>
<view class="actions">
<button class="btn" @click="onPreviewBanner">查看图片</button>
<button class="btn primary" @click="onParticipate">立即参与</button>
</view>
</scroll-view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getActivityDetail } from '../../api/appUser'
const detail = ref({})
function statusToText(s) {
if (s === 1) return '进行中'
if (s === 0) return '未开始'
if (s === 2) return '已结束'
return String(s || '')
}
const statusText = ref('')
async function fetchDetail(id) {
const data = await getActivityDetail(id)
detail.value = data || {}
statusText.value = statusToText(detail.value.status)
}
function onPreviewBanner() {
const url = detail.value.banner || ''
if (url) uni.previewImage({ urls: [url], current: url })
}
function onParticipate() {
uni.showToast({ title: '功能待接入', icon: 'none' })
}
onLoad((opts) => {
const id = (opts && opts.id) || ''
if (id) fetchDetail(id)
})
</script>
<style scoped>
.page { height: 100vh }
.banner { padding: 24rpx }
.banner-img { width: 100% }
.header { padding: 0 24rpx }
.title { font-size: 36rpx; font-weight: 700 }
.meta { margin-top: 8rpx; font-size: 26rpx; color: #666 }
.actions { display: flex; padding: 24rpx; gap: 16rpx }
.btn { flex: 1 }
.primary { background-color: #007AFF; color: #fff }
</style>

View File

@ -0,0 +1,151 @@
<template>
<scroll-view class="page" scroll-y>
<view class="banner" v-if="detail.banner">
<image class="banner-img" :src="detail.banner" mode="widthFix" />
</view>
<view class="header">
<view class="title">{{ detail.name || detail.title || '-' }}</view>
<view class="meta">分类{{ detail.category_name || '一番赏' }}</view>
<view class="meta" v-if="detail.price_draw !== undefined">抽选价{{ detail.price_draw }}</view>
<view class="meta" v-if="detail.status !== undefined">状态{{ statusText }}</view>
</view>
<view class="actions">
<button class="btn" @click="onPreviewBanner">查看图片</button>
<button class="btn primary" @click="onParticipate">立即参与</button>
</view>
<view class="issues">
<view class="issues-title">期数</view>
<view v-if="issues.length" class="issues-list">
<view class="issue-item" v-for="it in issues" :key="it.id">
<text class="issue-title">{{ it.title || ('第' + (it.no || it.index || it.issue_no || '-') + '期') }}</text>
<text class="issue-status" v-if="it.status_text">{{ it.status_text }}</text>
<view class="rewards" v-if="rewardsMap[it.id] && rewardsMap[it.id].length">
<view class="reward" v-for="rw in rewardsMap[it.id]" :key="rw.id">
<image v-if="rw.image" class="reward-img" :src="rw.image" mode="aspectFill" />
<view class="reward-texts">
<text class="reward-title">{{ rw.title }}</text>
<text class="reward-meta" v-if="rw.rarity || rw.odds">{{ [rw.rarity, rw.odds].filter(Boolean).join(' · ') }}</text>
</view>
</view>
</view>
<view class="rewards-empty" v-else>暂无奖励配置</view>
</view>
</view>
<view v-else class="issues-empty">暂无期数</view>
</view>
</scroll-view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { getActivityDetail, getActivityIssues, getActivityIssueRewards } from '../../../api/appUser'
const detail = ref({})
const issues = ref([])
const rewardsMap = ref({})
function statusToText(s) {
if (s === 1) return '进行中'
if (s === 0) return '未开始'
if (s === 2) return '已结束'
return String(s || '')
}
const statusText = ref('')
async function fetchDetail(id) {
const data = await getActivityDetail(id)
detail.value = data || {}
statusText.value = statusToText(detail.value.status)
}
function unwrap(list) {
if (Array.isArray(list)) return list
const obj = list || {}
const data = obj.data || {}
const arr = obj.list || obj.items || data.list || data.items || data
return Array.isArray(arr) ? arr : []
}
function normalizeIssues(list) {
const arr = unwrap(list)
return arr.map((i, idx) => ({
id: i.id ?? String(idx),
title: i.title ?? i.name ?? '',
no: i.no ?? i.index ?? i.issue_no ?? i.issue_number ?? null,
status_text: i.status_text ?? (i.status === 1 ? '进行中' : i.status === 0 ? '未开始' : i.status === 2 ? '已结束' : '')
}))
}
function normalizeRewards(list) {
const arr = unwrap(list)
return arr.map((i, idx) => ({
id: i.id ?? String(idx),
title: i.title ?? i.name ?? '',
image: i.image ?? i.img ?? i.pic ?? i.banner ?? '',
odds: i.odds ?? i.rate ?? i.probability ?? i.prob ?? '',
rarity: i.rarity ?? i.rarity_name ?? ''
}))
}
async function fetchRewardsForIssues(activityId) {
const list = issues.value || []
const promises = list.map(it => getActivityIssueRewards(activityId, it.id))
const results = await Promise.allSettled(promises)
results.forEach((res, i) => {
const issueId = list[i] && list[i].id
if (!issueId) return
const value = res.status === 'fulfilled' ? normalizeRewards(res.value) : []
rewardsMap.value = { ...(rewardsMap.value || {}), [issueId]: value }
})
}
async function fetchIssues(id) {
const data = await getActivityIssues(id)
issues.value = normalizeIssues(data)
await fetchRewardsForIssues(id)
}
function onPreviewBanner() {
const url = detail.value.banner || ''
if (url) uni.previewImage({ urls: [url], current: url })
}
function onParticipate() {
uni.showToast({ title: '功能待接入', icon: 'none' })
}
onLoad((opts) => {
const id = (opts && opts.id) || ''
if (id) {
fetchDetail(id)
fetchIssues(id)
}
})
</script>
<style scoped>
.page { height: 100vh }
.banner { padding: 24rpx }
.banner-img { width: 100% }
.header { padding: 0 24rpx }
.title { font-size: 36rpx; font-weight: 700 }
.meta { margin-top: 8rpx; font-size: 26rpx; color: #666 }
.actions { display: flex; padding: 24rpx; gap: 16rpx }
.btn { flex: 1 }
.primary { background-color: #007AFF; color: #fff }
.issues { background: #fff; border-radius: 12rpx; margin: 0 24rpx 24rpx; padding: 16rpx }
.issues-title { font-size: 30rpx; font-weight: 600; margin-bottom: 12rpx }
.issues-list { }
.issue-item { display: flex; justify-content: space-between; padding: 12rpx 0; border-bottom: 1rpx solid #f0f0f0 }
.issue-item:last-child { border-bottom: 0 }
.issue-title { font-size: 26rpx }
.issue-status { font-size: 24rpx; color: #666 }
.rewards { width: 100%; margin-top: 12rpx }
.reward { display: flex; align-items: center; margin-bottom: 8rpx }
.reward-img { width: 80rpx; height: 80rpx; border-radius: 8rpx; margin-right: 12rpx; background: #f5f5f5 }
.reward-texts { display: flex; flex-direction: column }
.reward-title { font-size: 26rpx }
.reward-meta { font-size: 22rpx; color: #888 }
.rewards-empty { font-size: 24rpx; color: #999 }
.issues-empty { font-size: 24rpx; color: #999 }
</style>

158
pages/address/edit.vue Normal file
View File

@ -0,0 +1,158 @@
<template>
<view class="wrap">
<view class="form-item">
<text class="label">姓名</text>
<input class="input" v-model="name" placeholder="请输入姓名" />
</view>
<view class="form-item">
<text class="label">手机号</text>
<input class="input" v-model="mobile" placeholder="请输入手机号" />
</view>
<view class="form-item">
<text class="label">省份</text>
<input class="input" v-model="province" placeholder="请输入省份" />
</view>
<view class="form-item">
<text class="label">城市</text>
<input class="input" v-model="city" placeholder="请输入城市" />
</view>
<view class="form-item">
<text class="label">区县</text>
<input class="input" v-model="district" placeholder="请输入区县" />
</view>
<view class="form-item">
<text class="label">详细地址</text>
<input class="input" v-model="detail" placeholder="请输入详细地址" />
</view>
<view class="form-item">
<text class="label">设为默认</text>
<switch :checked="isDefault" @change="e => isDefault = e.detail.value" />
</view>
<button class="submit" :disabled="loading" @click="onSubmit">保存</button>
<view v-if="error" class="error">{{ error }}</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { addAddress, updateAddress, listAddresses, setDefaultAddress } from '../../api/appUser'
const id = ref('')
const name = ref('')
const mobile = ref('')
const province = ref('')
const city = ref('')
const district = ref('')
const detail = ref('')
let isDefault = false
const loading = ref(false)
const error = ref('')
function fill(data) {
name.value = data.name || data.realname || ''
mobile.value = data.mobile || data.phone || ''
province.value = data.province || ''
city.value = data.city || ''
district.value = data.district || ''
detail.value = data.address || data.detail || ''
isDefault = !!data.is_default
}
async function init(idParam) {
if (!idParam) {
const data = uni.getStorageSync('edit_address') || {}
if (data && data.id) fill(data)
return
}
const user_id = uni.getStorageSync('user_id')
try {
const list = await listAddresses(user_id)
const arr = Array.isArray(list) ? list : (list && (list.list || list.items)) || []
const found = arr.find(a => String(a.id) === String(idParam))
if (found) fill(found)
} catch (e) {}
}
async function onSubmit() {
const user_id = uni.getStorageSync('user_id')
if (!name.value || !mobile.value || !province.value || !city.value || !district.value || !detail.value) {
uni.showToast({ title: '请完善必填信息', icon: 'none' })
return
}
loading.value = true
error.value = ''
const payload = {
name: name.value,
mobile: mobile.value,
province: province.value,
city: city.value,
district: district.value,
address: detail.value,
is_default: isDefault
}
try {
let savedId = id.value
if (id.value) {
await updateAddress(user_id, id.value, payload)
savedId = id.value
} else {
try {
const res = await addAddress(user_id, payload)
savedId = (res && (res.id || res.address_id)) || ''
} catch (eAdd) {
const sc = eAdd && eAdd.statusCode
const bc = (eAdd && eAdd.data && (eAdd.data.code || eAdd.code)) || undefined
const msg = eAdd && (eAdd.message || eAdd.errMsg || '')
const isUniqueErr = sc === 400 && (bc === 10011 || (msg && msg.toLowerCase().includes('unique')))
if (isUniqueErr) {
try {
const list = await listAddresses(user_id)
const arr = Array.isArray(list) ? list : (list && (list.list || list.items)) || []
const found = arr.find(a => (a.mobile === mobile.value || a.phone === mobile.value) && (a.address === detail.value || a.detail === detail.value) && (a.city === city.value) && (a.district === district.value) && (a.province === province.value))
if (found) {
savedId = found.id
await updateAddress(user_id, savedId, payload)
}
} catch (_) {}
} else {
throw eAdd
}
}
}
if (isDefault) {
if (!savedId) {
try {
const list = await listAddresses(user_id)
const arr = Array.isArray(list) ? list : (list && (list.list || list.items)) || []
const found = arr.find(a => (a.mobile === mobile.value || a.phone === mobile.value) && (a.address === detail.value || a.detail === detail.value))
if (found) savedId = found.id
} catch (_) {}
}
if (savedId) {
await setDefaultAddress(user_id, savedId)
}
}
uni.showToast({ title: '保存成功', icon: 'success' })
uni.navigateBack()
} catch (e) {
error.value = e && (e.message || e.errMsg) || '保存失败'
} finally {
loading.value = false
}
}
onLoad((opts) => {
id.value = (opts && opts.id) || ''
init(id.value)
})
</script>
<style scoped>
.wrap { padding: 24rpx }
.form-item { display: flex; align-items: center; background: #fff; border-radius: 12rpx; padding: 16rpx; margin-bottom: 12rpx }
.label { width: 160rpx; font-size: 28rpx; color: #666 }
.input { flex: 1; font-size: 28rpx }
.submit { width: 100%; margin-top: 20rpx }
.error { color: #e43; margin-top: 12rpx }
</style>

130
pages/address/index.vue Normal file
View File

@ -0,0 +1,130 @@
<template>
<view class="wrap">
<view class="header">
<button class="add" @click="toAdd">新增地址</button>
</view>
<view v-if="error" class="error">{{ error }}</view>
<view v-if="list.length === 0 && !loading" class="empty">暂无地址</view>
<view v-for="item in list" :key="item.id" class="addr">
<view class="addr-main">
<view class="addr-row">
<text class="name">姓名{{ item.name || item.realname }}</text>
</view>
<view class="addr-row">
<text class="phone">手机号{{ item.phone || item.mobile }}</text>
</view>
<view class="addr-row" v-if="item.is_default">
<text class="default">默认</text>
</view>
<view class="addr-row">
<text class="region">省市区{{ item.province }}{{ item.city }}{{ item.district }}</text>
</view>
<view class="addr-row">
<text class="detail">详细地址{{ item.address || item.detail }}</text>
</view>
</view>
<view class="addr-actions">
<button size="mini" @click="toEdit(item)">编辑</button>
<button size="mini" type="warn" @click="onDelete(item)">删除</button>
<button size="mini" :disabled="item.is_default" @click="onSetDefault(item)">设为默认</button>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import { listAddresses, deleteAddress, setDefaultAddress } from '../../api/appUser'
const list = ref([])
const loading = ref(false)
const error = ref('')
async function fetchList() {
const user_id = uni.getStorageSync('user_id')
const token = uni.getStorageSync('token')
const phoneBound = !!uni.getStorageSync('phone_bound')
if (!user_id || !token || !phoneBound) {
uni.showModal({
title: '提示',
content: '请先登录并绑定手机号',
confirmText: '去登录',
success: (res) => {
if (res.confirm) {
uni.navigateTo({ url: '/pages/login/index' })
}
}
})
return
}
loading.value = true
error.value = ''
try {
const data = await listAddresses(user_id)
list.value = Array.isArray(data) ? data : (data && (data.list || data.items)) || []
} catch (e) {
error.value = e && (e.message || e.errMsg) || '获取地址失败'
} finally {
loading.value = false
}
}
function toAdd() {
uni.removeStorageSync('edit_address')
uni.navigateTo({ url: '/pages/address/edit' })
}
function toEdit(item) {
uni.setStorageSync('edit_address', item)
uni.navigateTo({ url: `/pages/address/edit?id=${item.id}` })
}
function onDelete(item) {
const user_id = uni.getStorageSync('user_id')
uni.showModal({
title: '确认删除',
content: '确定删除该地址吗?',
success: async (res) => {
if (res.confirm) {
try {
await deleteAddress(user_id, item.id)
fetchList()
} catch (e) {
uni.showToast({ title: '删除失败', icon: 'none' })
}
}
}
})
}
async function onSetDefault(item) {
try {
const user_id = uni.getStorageSync('user_id')
await setDefaultAddress(user_id, item.id)
fetchList()
} catch (e) {
uni.showToast({ title: '设置失败', icon: 'none' })
}
}
onLoad(() => {
fetchList()
})
</script>
<style scoped>
.wrap { padding: 24rpx }
.header { display: flex; justify-content: flex-end; margin-bottom: 12rpx }
.add { font-size: 28rpx }
.addr { background: #fff; border-radius: 12rpx; padding: 20rpx; margin-bottom: 16rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04) }
.addr-row { display: flex; align-items: center; margin-bottom: 8rpx }
.name { font-size: 30rpx; margin-right: 12rpx }
.phone { font-size: 28rpx; color: #666 }
.default { font-size: 24rpx; color: #007AFF; margin-left: 10rpx }
.region { font-size: 26rpx; color: #666 }
.detail { font-size: 26rpx; color: #333 }
.addr-actions { display: flex; justify-content: flex-end; gap: 12rpx; margin-top: 12rpx }
.empty { text-align: center; color: #999; margin-top: 40rpx }
.error { color: #e43; margin-bottom: 12rpx }
</style>

View File

@ -0,0 +1,69 @@
<template>
<scroll-view class="page" scroll-y>
<view class="h1">购买协议</view>
<view class="meta">生效日期2025年11月18日</view>
<view class="meta">运营方公司全称</view>
<view class="p">购买协议适用于您在您的小程序名称以下简称本平台购买盲盒商品的行为当您点击立即购买并完成支付时即视为您已阅读理解并同意本协议全部内容</view>
<view class="h2">商品说明</view>
<view class="ol">
<view class="li">盲盒特性本平台所售盲盒为系列化商品包装外观一致内部款式随机具体款式无法提前指定或预知</view>
<view class="li">概率公示各款式含隐藏款特殊款的抽取概率已在商品详情页明确公示确保透明公正</view>
<view class="li">商品展示页面图片描述仅供参考实际商品以实物为准</view>
</view>
<view class="h2">购买规则</view>
<view class="ol">
<view class="li">购买资格仅限已注册并通过身份验证的用户购买</view>
<view class="li">支付方式支持微信支付等平台认可的支付方式支付成功即视为订单成立</view>
</view>
<view class="h2">发货与物流</view>
<view class="ol">
<view class="li">发货时效发货订单提交成功后本平台将在3-15个工作日内安排发货预售商品按页面说明执行</view>
<view class="li">物流信息您可在我的订单中查看物流状态因地址错误联系不畅导致的配送失败责任由您承担</view>
</view>
<view class="h2">售后服务</view>
<view class="ol">
<view class="li">质量问题如商品破损漏发错发非盲盒系列商品请在签收后2到4小时内联系客服并提供凭证如开箱视频照片经核实后平台将为您补发换货或退款</view>
<view class="li">非质量问题如抽中重复款式不喜欢款式未抽中隐藏款等不支持无理由退换货</view>
<view class="li">拆封后商品出于卫生与二次销售考虑已拆封盲盒恕不退换质量问题除外</view>
</view>
<view class="h2">价格与促销</view>
<view class="ol">
<view class="li">商品价格以页面实时显示为准平台有权根据市场情况调整价格</view>
<view class="li">优惠券折扣活动需遵守对应规则不可叠加或兑现</view>
</view>
<view class="h2">特别提示</view>
<view class="ol">
<view class="li">盲盒不具备投资属性本平台不承诺保值升值或回购</view>
<view class="li">隐藏款抽取属小概率事件请勿抱有赌博心态理性消费</view>
</view>
<view class="h2">免责情形</view>
<view class="ul">
<view class="li">因不可抗力导致无法发货或延迟</view>
<view class="li">用户提供错误收货信息</view>
<view class="li">因第三方物流原因造成的商品损毁或丢失平台将协助索赔</view>
</view>
<view class="h2">协议效力</view>
<view class="p">本购买协议为用户协议的补充两者冲突时以本协议中关于交易的条款为准未尽事宜依照消费者权益保护法电子商务法等法律法规执行</view>
<view class="h2">联系我们</view>
<view class="p">售后专线service@yourdomain.com</view>
<view class="p">工作时间工作日 9:0018:00</view>
<view class="p">运营主体公司全称</view>
<view class="p">统一社会信用代码XXXXXXXXXXXXXX</view>
<view class="tip">理性消费提醒盲盒是一种娱乐消费形式请根据自身经济能力合理购买切勿沉迷或过度投入</view>
</scroll-view>
</template>
<script>
export default {}
</script>
<style scoped>
.page { height: 100vh }
.h1 { font-size: 36rpx; font-weight: 700; padding: 24rpx }
.meta { padding: 0 24rpx; font-size: 24rpx; color: #666 }
.h2 { font-size: 30rpx; font-weight: 600; padding: 20rpx 24rpx 8rpx }
.p { padding: 0 24rpx; font-size: 26rpx; line-height: 1.6 }
.ul, .ol { padding: 0 24rpx }
.ul .li, .ol .li { font-size: 26rpx; line-height: 1.6; margin: 8rpx 0 }
.tip { padding: 16rpx 24rpx; font-size: 24rpx; color: #999 }
</style>

68
pages/agreement/user.vue Normal file
View File

@ -0,0 +1,68 @@
<template>
<scroll-view class="page" scroll-y>
<view class="h1">用户协议</view>
<view class="meta">生效日期2025年11月18日</view>
<view class="meta">运营方公司全称</view>
<view class="p">欢迎您使用您的小程序名称以下简称本平台提供的服务请您在注册登录或使用本平台前认真阅读并充分理解本用户协议以下简称本协议一旦您完成注册登录或以任何方式使用本平台服务即视为您已完全接受本协议全部条款如您不同意请勿使用本平台</view>
<view class="h2">协议范围</view>
<view class="p">本协议规范您作为用户在本平台注册浏览互动参与活动等行为是您与本平台之间的基本权利义务约定</view>
<view class="h2">用户资格</view>
<view class="ol">
<view class="li">您须为年满18周岁的自然人具备完全民事行为能力</view>
<view class="li">未成年人如需使用本平台须在法定监护人知情并同意下进行监护人应承担相应责任</view>
</view>
<view class="h2">账号与安全</view>
<view class="ol">
<view class="li">您应提供真实准确完整的注册信息并对账号下的一切行为负责</view>
<view class="li">请妥善保管账号密码因泄露出借或被盗用造成的损失由您自行承担</view>
</view>
<view class="h2">用户行为规范</view>
<view class="p">您承诺不从事以下行为</view>
<view class="ul">
<view class="li">发布违法侵权色情暴力或虚假信息</view>
<view class="li">利用技术手段干扰系统正常运行如刷量外挂爬虫</view>
<view class="li">恶意投诉敲诈勒索或损害平台商誉</view>
<view class="li">转售账号或用于商业牟利除非平台明确授权</view>
</view>
<view class="h2">隐私保护</view>
<view class="p">我们尊重并保护您的个人信息关于数据收集使用及保护的具体规则请参见隐私政策链接未经您同意我们不会向第三方共享您的个人信息法律法规另有规定除外</view>
<view class="h2">知识产权</view>
<view class="ol">
<view class="li">本平台所有内容包括但不限于界面设计文字图片盲盒形象LOGO的知识产权归本平台或其许可方所有</view>
<view class="li">未经书面授权您不得复制传播修改或用于商业用途</view>
</view>
<view class="h2">免责条款</view>
<view class="ol">
<view class="li">因不可抗力如地震网络攻击政府行为导致服务中断本平台不承担责任</view>
<view class="li">您因违反本协议造成自身或第三方损失的由您自行承担</view>
</view>
<view class="h2">协议变更与终止</view>
<view class="ol">
<view class="li">本平台有权根据业务或法律变化修订本协议修订后将通过公告或推送通知您继续使用即视为接受新条款</view>
<view class="li">您可随时注销账号终止使用平台有权对违规账号采取限制或封禁措施</view>
</view>
<view class="h2">法律适用与争议解决</view>
<view class="p">本协议适用中华人民共和国法律因本协议引起的争议双方应协商解决协商不成的提交本平台运营方所在地有管辖权的人民法院诉讼解决</view>
<view class="h2">联系我们</view>
<view class="p">客服邮箱service@yourdomain.com</view>
<view class="p">客服电话400-XXX-XXXX工作日 9:0018:00</view>
<view class="p">运营主体公司全称</view>
<view class="p">地址公司注册地址</view>
<view class="tip">温馨提示盲盒具有随机性和娱乐性请理性参与避免沉迷未成年人禁止参与购买</view>
</scroll-view>
</template>
<script>
export default {}
</script>
<style scoped>
.page { height: 100vh }
.h1 { font-size: 36rpx; font-weight: 700; padding: 24rpx }
.meta { padding: 0 24rpx; font-size: 24rpx; color: #666 }
.h2 { font-size: 30rpx; font-weight: 600; padding: 20rpx 24rpx 8rpx }
.p { padding: 0 24rpx; font-size: 26rpx; line-height: 1.6 }
.ul, .ol { padding: 0 24rpx }
.ul .li, .ol .li { font-size: 26rpx; line-height: 1.6; margin: 8rpx 0 }
.tip { padding: 16rpx 24rpx; font-size: 24rpx; color: #999 }
</style>

28
pages/cabinet/index.vue Normal file
View File

@ -0,0 +1,28 @@
<template>
<view class="wrap">货柜</view>
</template>
<script setup>
import { onShow } from '@dcloudio/uni-app'
onShow(() => {
const token = uni.getStorageSync('token')
const phoneBound = !!uni.getStorageSync('phone_bound')
console.log('cabinet onShow token:', token, 'isLogin:', !!token, 'phoneBound:', phoneBound)
if (!token || !phoneBound) {
uni.showModal({
title: '提示',
content: '请先登录并绑定手机号',
confirmText: '去登录',
success: (res) => {
if (res.confirm) {
uni.navigateTo({ url: '/pages/login/index' })
}
}
})
}
})
</script>
<style scoped>
.wrap { padding: 40rpx }
</style>

28
pages/help/index.vue Normal file
View File

@ -0,0 +1,28 @@
<template>
<view class="wrap">
<view class="card" @click="toUser">
<view class="title">用户协议</view>
<view class="desc">查看平台使用条款与隐私说明</view>
</view>
<view class="card" @click="toPurchase">
<view class="title">购买协议</view>
<view class="desc">查看盲盒购买规则与售后政策</view>
</view>
</view>
</template>
<script>
export default {
methods: {
toUser() { uni.navigateTo({ url: '/pages/agreement/user' }) },
toPurchase() { uni.navigateTo({ url: '/pages/agreement/purchase' }) }
}
}
</script>
<style scoped>
.wrap { padding: 24rpx }
.card { background: #fff; border-radius: 12rpx; padding: 24rpx; margin-bottom: 16rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04) }
.title { font-size: 30rpx; font-weight: 600 }
.desc { margin-top: 8rpx; font-size: 24rpx; color: #666 }
</style>

224
pages/index/index.vue Normal file
View File

@ -0,0 +1,224 @@
<template>
<view class="page">
<view class="notice-bar">
<swiper class="notice-swiper" vertical circular autoplay interval="3000" duration="300">
<swiper-item v-for="n in displayNotices" :key="n.id">
<view class="notice-item">{{ n.text }}</view>
</swiper-item>
</swiper>
</view>
<view class="banner-box">
<swiper class="banner-swiper" circular autoplay interval="4000" duration="500">
<swiper-item v-for="b in displayBanners" :key="b.id">
<image v-if="b.image" class="banner-image" :src="b.image" mode="aspectFill" @tap="onBannerTap(b)" />
<view v-else class="banner-fallback">
<text class="banner-fallback-text">{{ b.title || '敬请期待' }}</text>
</view>
</swiper-item>
</swiper>
</view>
<view class="activity-section">
<view class="section-title">活动</view>
<view v-if="activities.length" class="activity-grid">
<view class="activity-item" v-for="a in activities" :key="a.id" @tap="onActivityTap(a)">
<image v-if="a.image" class="activity-thumb" :src="a.image" mode="aspectFill" />
<view v-else class="banner-fallback">
<text class="banner-fallback-text">{{ a.title || '活动敬请期待' }}</text>
</view>
<text class="activity-name">{{ a.title }}</text>
<text class="activity-desc" v-if="a.subtitle">{{ a.subtitle }}</text>
</view>
</view>
<view v-else class="activity-empty">暂无活动</view>
</view>
</view>
</template>
<script>
import { authRequest, request } from '../../utils/request.js'
export default {
data() {
return {
notices: [],
banners: [],
activities: []
}
},
computed: {
displayNotices() {
if (Array.isArray(this.notices) && this.notices.length) return this.notices
return [
{ id: 'n1', text: '欢迎光临' },
{ id: 'n2', text: '最新活动敬请期待' }
]
},
displayBanners() {
if (Array.isArray(this.banners) && this.banners.length) return this.banners
return [
{ id: 'ph-1', title: '精彩内容即将上线', image: '' },
{ id: 'ph-2', title: '敬请期待', image: '' },
{ id: 'ph-3', title: '更多活动请关注', image: '' }
]
}
},
onShow() {
const token = uni.getStorageSync('token')
const phoneBound = !!uni.getStorageSync('phone_bound')
if (!token || !phoneBound) {
uni.showModal({
title: '提示',
content: '请先登录并绑定手机号',
confirmText: '去登录',
success: (res) => {
if (res.confirm) {
uni.navigateTo({ url: '/pages/login/index' })
}
}
})
}
try { console.log('home onShow', { token: !!token, phoneBound }) } catch (_) {}
this.loadHomeData()
},
methods: {
toArray(x) { return Array.isArray(x) ? x : [] },
unwrap(list) {
if (Array.isArray(list)) return list
const obj = list || {}
const arr = obj.list || obj.items || obj.data || []
return Array.isArray(arr) ? arr : []
},
cleanUrl(u) {
const s = String(u || '').trim()
const m = s.match(/https?:\/\/[^\s'"`]+/)
if (m && m[0]) return m[0]
return s.replace(/[`'\"]/g, '').trim()
},
apiGet(url) {
const token = uni.getStorageSync('token')
const fn = token ? authRequest : request
return fn({ url })
},
normalizeNotices(list) {
const arr = this.unwrap(list)
return arr.map((i, idx) => ({
id: i.id ?? String(idx),
text: i.content ?? i.text ?? i.title ?? ''
})).filter(i => i.text)
},
normalizeBanners(list) {
const arr = this.unwrap(list)
console.log('normalizeBanners input', list, 'unwrapped', arr)
const mapped = arr.map((i, idx) => ({
id: i.id ?? String(idx),
title: i.title ?? '',
image: this.cleanUrl(i.imageUrl ?? i.image_url ?? i.image ?? i.img ?? i.pic ?? ''),
link: this.cleanUrl(i.linkUrl ?? i.link_url ?? i.link ?? i.url ?? ''),
sort: typeof i.sort === 'number' ? i.sort : 0
})).filter(i => i.image)
mapped.sort((a, b) => a.sort - b.sort)
console.log('normalizeBanners mapped', mapped)
return mapped
},
normalizeActivities(list) {
const arr = this.unwrap(list)
console.log('normalizeActivities input', list, 'unwrapped', arr)
const mapped = arr.map((i, idx) => ({
id: i.id ?? String(idx),
image: this.cleanUrl(i.banner ?? i.coverUrl ?? i.cover_url ?? i.image ?? i.img ?? i.pic ?? ''),
title: i.title ?? i.name ?? '',
subtitle: this.buildActivitySubtitle(i),
link: this.cleanUrl(i.linkUrl ?? i.link_url ?? i.link ?? i.url ?? ''),
category_name: (i.category_name ?? i.categoryName ?? '').trim(),
category_id: i.activity_category_id ?? i.category_id ?? i.categoryId ?? null
})).filter(i => i.image || i.title)
console.log('normalizeActivities mapped', mapped)
return mapped
},
buildActivitySubtitle(i) {
const base = i.subTitle ?? i.sub_title ?? i.subtitle ?? i.desc ?? i.description ?? ''
if (base) return base
const cat = i.category_name ?? i.categoryName ?? ''
const price = (i.price_draw !== undefined && i.price_draw !== null) ? `${i.price_draw}` : ''
const parts = [cat, price].filter(Boolean)
return parts.join(' · ')
},
async loadHomeData() {
const results = await Promise.allSettled([
this.apiGet('/api/app/notices'),
this.apiGet('/api/app/banners'),
this.apiGet('/api/app/activities')
])
const [nRes, bRes, acRes] = results
if (nRes.status === 'fulfilled') {
console.log('notices ok', nRes.value)
this.notices = this.normalizeNotices(nRes.value)
} else {
console.error('notices error', nRes.reason)
this.notices = []
}
if (bRes.status === 'fulfilled') {
console.log('banners ok', bRes.value)
this.banners = this.normalizeBanners(bRes.value)
} else {
console.error('banners error', bRes.reason)
this.banners = []
}
if (acRes.status === 'fulfilled') {
console.log('activities ok', acRes.value)
this.activities = this.normalizeActivities(acRes.value)
} else {
console.error('activities error', acRes.reason)
this.activities = []
}
console.log('home normalized', { notices: this.notices, banners: this.banners, activities: this.activities })
},
onBannerTap(b) {
const imgs = (Array.isArray(this.banners) ? this.banners : []).map(x => x.image).filter(Boolean)
const current = b && b.image
if (current) {
uni.previewImage({ urls: imgs.length ? imgs : [current], current })
return
}
if (b.link && /^\/.+/.test(b.link)) {
uni.navigateTo({ url: b.link })
}
},
onActivityTap(a) {
const name = (a.category_name || a.categoryName || '').trim()
const id = a.id
let path = ''
if (name === '一番赏') path = '/pages/activity/yifanshang/index'
else if (name === '无限赏') path = '/pages/activity/wuxianshang/index'
else if (name === '对对碰') path = '/pages/activity/duiduipeng/index'
if (path && id) {
uni.navigateTo({ url: `${path}?id=${id}` })
return
}
if (a.link && /^\/.+/.test(a.link)) {
uni.navigateTo({ url: a.link })
}
}
}
}
</script>
<style>
.page { padding: 24rpx }
.notice-bar { height: 64rpx; background: #fff4e6; border-radius: 8rpx; overflow: hidden; margin-bottom: 24rpx; }
.notice-swiper { height: 64rpx }
.notice-item { height: 64rpx; line-height: 64rpx; padding: 0 24rpx; color: #a15c00; font-size: 26rpx }
.banner-box { margin-bottom: 24rpx }
.banner-swiper { width: 100%; height: 320rpx; border-radius: 12rpx; overflow: hidden }
.banner-image { width: 100%; height: 320rpx }
.banner-fallback { width: 100%; height: 320rpx; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #f5f5f5, #eaeaea) }
.banner-fallback-text { color: #666; font-size: 28rpx }
.activity-section { background: #ffffff; border-radius: 12rpx; padding: 24rpx }
.section-title { font-size: 30rpx; font-weight: 600; margin-bottom: 16rpx }
.activity-grid { display: flex; flex-wrap: wrap; margin: -12rpx }
.activity-item { width: 50%; padding: 12rpx }
.activity-thumb { width: 100%; height: 200rpx; border-radius: 8rpx }
.activity-name { display: block; margin-top: 8rpx; font-size: 26rpx; color: #222 }
.activity-desc { display: block; margin-top: 4rpx; font-size: 22rpx; color: #888 }
</style>

145
pages/login/index.vue Normal file
View File

@ -0,0 +1,145 @@
<template>
<view class="container">
<image class="logo" src="/static/logo.png" mode="widthFix"></image>
<view class="title">微信登录</view>
<!-- #ifdef MP-WEIXIN -->
<button class="btn" open-type="getPhoneNumber" :disabled="loading" @getphonenumber="onGetPhoneNumber">授权手机号快速登录</button>
<!-- #endif -->
<view class="agreements">
<text>注册或登录即表示您已阅读并同意</text>
<text class="link" @tap="toUserAgreement">用户协议</text>
<text></text>
<text class="link" @tap="toPurchaseAgreement">购买协议</text>
</view>
<view v-if="needBindPhone" class="tip">登录成功请绑定手机号以完成登录</view>
<view v-if="error" class="error">{{ error }}</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { wechatLogin, bindPhone, getUserStats, getPointsBalance } from '../../api/appUser'
const loading = ref(false)
const error = ref('')
const needBindPhone = ref(false)
const loggedIn = computed(() => !!uni.getStorageSync('token'))
function onLogin() {}
function toUserAgreement() { uni.navigateTo({ url: '/pages/agreement/user' }) }
function toPurchaseAgreement() { uni.navigateTo({ url: '/pages/agreement/purchase' }) }
function onGetPhoneNumber(e) {
const phoneCode = e.detail.code
console.log('login_flow start getPhoneNumber, codeExists:', !!phoneCode)
if (!phoneCode) {
uni.showToast({ title: '未授权手机号', icon: 'none' })
console.error('login_flow error: missing phoneCode')
return
}
loading.value = true
error.value = ''
uni.login({
provider: 'weixin',
success: async (res) => {
try {
const loginCode = res.code
console.log('login_flow uni.login success, loginCode exists:', !!loginCode)
const data = await wechatLogin(loginCode)
console.log('login_flow wechatLogin response user_id:', data && data.user_id)
const token = data && data.token
const user_id = data && data.user_id
const avatar = data && data.avatar
const nickname = data && data.nickname
const invite_code = data && data.invite_code
uni.setStorageSync('user_info', data || {})
if (token) {
uni.setStorageSync('token', token)
console.log('login_flow token stored')
}
if (user_id) {
uni.setStorageSync('user_id', user_id)
console.log('login_flow user_id stored:', user_id)
}
if (avatar) {
uni.setStorageSync('avatar', avatar)
}
if (nickname) {
uni.setStorageSync('nickname', nickname)
}
if (invite_code) {
uni.setStorageSync('invite_code', invite_code)
}
console.log('login_flow bindPhone start')
try {
// token
await new Promise(r => setTimeout(r, 600))
const bindRes = await bindPhone(user_id, phoneCode, { 'X-Suppress-Auth-Modal': true })
const phoneNumber = (bindRes && (bindRes.phone || bindRes.phone_number || bindRes.mobile)) || ''
if (phoneNumber) uni.setStorageSync('phone_number', phoneNumber)
} catch (bindErr) {
if (bindErr && bindErr.statusCode === 401) {
console.warn('login_flow bindPhone 401, try re-login and retry')
// code
const relogin = await new Promise((resolve, reject) => {
uni.login({ provider: 'weixin', success: resolve, fail: reject })
})
const data2 = await wechatLogin(relogin.code)
const token2 = data2 && data2.token
const user2 = data2 && data2.user_id
if (token2) uni.setStorageSync('token', token2)
if (user2) uni.setStorageSync('user_id', user2)
//
await new Promise(r => setTimeout(r, 600))
const bindRes2 = await bindPhone(user2 || user_id, phoneCode, { 'X-Suppress-Auth-Modal': true })
const phoneNumber2 = (bindRes2 && (bindRes2.phone || bindRes2.phone_number || bindRes2.mobile)) || ''
if (phoneNumber2) uni.setStorageSync('phone_number', phoneNumber2)
} else {
throw bindErr
}
}
uni.setStorageSync('phone_bound', true)
console.log('login_flow bindPhone success, phone_bound stored')
try {
const stats = await getUserStats(user_id)
console.log('login_flow getUserStats success')
const balance = await getPointsBalance(user_id)
console.log('login_flow getPointsBalance success')
uni.setStorageSync('user_stats', stats)
const b = balance && balance.balance !== undefined ? balance.balance : balance
uni.setStorageSync('points_balance', b)
} catch (e) {
console.error('login_flow fetch stats/points error:', e && (e.message || e.errMsg))
}
uni.showToast({ title: '登录并绑定成功', icon: 'success' })
console.log('login_flow navigate to index')
uni.reLaunch({ url: '/pages/index/index' })
} catch (err) {
console.error('login_flow error:', err && (err.message || err.errMsg), 'status:', err && err.statusCode)
error.value = err.message || '登录或绑定失败'
} finally {
loading.value = false
}
},
fail: (e) => {
console.error('login_flow uni.login fail:', e && e.errMsg)
error.value = '微信登录失败'
loading.value = false
}
})
}
</script>
<style scoped>
.container { padding: 40rpx; display: flex; flex-direction: column; align-items: center }
.logo { width: 200rpx; margin-top: 100rpx; margin-bottom: 40rpx }
.title { font-size: 36rpx; margin-bottom: 20rpx }
.btn { width: 80%; margin-top: 20rpx }
.agreements { margin-top: 16rpx; font-size: 24rpx; color: #666; display: flex; flex-wrap: wrap; justify-content: center }
.link { color: #007AFF; margin: 0 6rpx }
.tip { color: #666; margin-top: 12rpx }
.error { color: #e43; margin-top: 20rpx }
</style>

176
pages/mine/index.vue Normal file
View File

@ -0,0 +1,176 @@
<template>
<view class="wrap">
<view class="header">
<image class="avatar" :src="avatar || '/static/logo.png'" mode="aspectFill"></image>
<view class="profile">
<view class="nickname">{{ nickname || '未登录' }}</view>
<view class="userid">ID{{ userId || '-' }}</view>
</view>
</view>
<view class="info">
<view class="info-item">
<text class="info-label">手机号</text>
<text class="info-value">{{ phoneNumber || '未绑定' }}</text>
</view>
<view class="info-item">
<text class="info-label">邀请码</text>
<text class="info-value">{{ inviteCode || '-' }}</text>
</view>
</view>
<view class="stats">
<view class="stat">
<view class="stat-label" @click="toPoints">积分</view>
<view class="stat-value">{{ pointsBalance }}</view>
</view>
<view class="stat">
<view class="stat-label">卡券</view>
<view class="stat-value">{{ stats.coupon_count || 0 }}</view>
</view>
<view class="stat">
<view class="stat-label">道具卡</view>
<view class="stat-value">{{ stats.item_card_count || 0 }}</view>
</view>
</view>
<button class="refresh" @click="refresh" :disabled="loading">刷新数据</button>
<view v-if="error" class="error">{{ error }}</view>
</view>
<view class="orders">
<view class="orders-title">我的订单</view>
<view class="orders-cats">
<view class="orders-cat" @click="toOrders('pending')">
<view class="orders-cat-title">待付款</view>
</view>
<view class="orders-cat" @click="toOrders('completed')">
<view class="orders-cat-title">已完成</view>
</view>
</view>
</view>
<view class="addresses">
<view class="addresses-title">地址管理</view>
<view class="addresses-entry" @click="toAddresses">
<text class="addresses-text">管理收货地址</text>
<text class="addresses-arrow"></text>
</view>
</view>
<view class="addresses">
<view class="addresses-title">使用帮助</view>
<view class="addresses-entry" @click="toHelp">
<text class="addresses-text">查看协议与说明</text>
<text class="addresses-arrow"></text>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onShow, onLoad } from '@dcloudio/uni-app'
import { getUserStats, getPointsBalance } from '../../api/appUser'
const avatar = ref(uni.getStorageSync('avatar') || '')
const nickname = ref(uni.getStorageSync('nickname') || '')
const userId = ref(uni.getStorageSync('user_id') || '')
const phoneNumber = ref(uni.getStorageSync('phone_number') || '')
const inviteCode = ref(uni.getStorageSync('invite_code') || (uni.getStorageSync('user_info') || {}).invite_code || '')
const pointsBalance = ref(uni.getStorageSync('points_balance') || 0)
const stats = ref(uni.getStorageSync('user_stats') || {})
const loading = ref(false)
const error = ref('')
async function refresh() {
const token = uni.getStorageSync('token')
const phoneBound = !!uni.getStorageSync('phone_bound')
if (!token || !phoneBound) {
uni.showModal({
title: '提示',
content: '请先登录并绑定手机号',
confirmText: '去登录',
success: (res) => {
if (res.confirm) {
uni.navigateTo({ url: '/pages/login/index' })
}
}
})
return
}
const user_id = uni.getStorageSync('user_id')
loading.value = true
error.value = ''
try {
const s = await getUserStats(user_id)
stats.value = s || {}
uni.setStorageSync('user_stats', stats.value)
const b = await getPointsBalance(user_id)
const balance = b && b.balance !== undefined ? b.balance : b
pointsBalance.value = balance || 0
uni.setStorageSync('points_balance', pointsBalance.value)
} catch (e) {
error.value = e && (e.message || e.errMsg) || '数据获取失败'
} finally {
loading.value = false
}
}
function toPoints() {
uni.navigateTo({ url: '/pages/points/index' })
}
function toOrders(status) {
const s = status === 'completed' ? 'completed' : 'pending'
uni.navigateTo({ url: `/pages/orders/index?status=${s}` })
}
function toAddresses() {
uni.navigateTo({ url: '/pages/address/index' })
}
function toHelp() {
uni.navigateTo({ url: '/pages/help/index' })
}
onShow(() => {
avatar.value = uni.getStorageSync('avatar') || avatar.value
nickname.value = uni.getStorageSync('nickname') || nickname.value
userId.value = uni.getStorageSync('user_id') || userId.value
phoneNumber.value = uni.getStorageSync('phone_number') || phoneNumber.value
const ui = uni.getStorageSync('user_info') || {}
inviteCode.value = uni.getStorageSync('invite_code') || ui.invite_code || inviteCode.value
pointsBalance.value = uni.getStorageSync('points_balance') || pointsBalance.value
const s = uni.getStorageSync('user_stats')
if (s) stats.value = s
refresh()
})
onLoad(() => {
refresh()
})
</script>
<style scoped>
.wrap { padding: 40rpx }
.header { display: flex; align-items: center; margin-bottom: 24rpx }
.avatar { width: 120rpx; height: 120rpx; border-radius: 60rpx; background-color: #f5f5f5 }
.profile { margin-left: 20rpx; display: flex; flex-direction: column }
.nickname { font-size: 32rpx }
.userid { margin-top: 6rpx; font-size: 24rpx; color: #999 }
.info { display: flex; flex-direction: column; background: #fff; border-radius: 12rpx; padding: 20rpx; margin-bottom: 20rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04) }
.info-item { display: flex; justify-content: space-between; margin-bottom: 12rpx }
.info-label { color: #666; font-size: 24rpx }
.info-value { font-size: 28rpx }
.stats { display: flex; background: #fafafa; border-radius: 12rpx; padding: 20rpx; justify-content: space-between; margin-bottom: 20rpx }
.stat { flex: 1; align-items: center }
.stat-label { color: #666; font-size: 24rpx }
.stat-value { font-size: 36rpx; margin-top: 8rpx }
.orders { background: #fff; border-radius: 12rpx; padding: 20rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04); margin-bottom: 20rpx }
.orders-title { font-size: 30rpx; margin-bottom: 12rpx }
.orders-cats { display: flex; justify-content: space-between }
.orders-cat { flex: 1; background: #f7f7f7; border-radius: 12rpx; padding: 20rpx; margin-right: 12rpx }
.orders-cat:last-child { margin-right: 0 }
.orders-cat-title { font-size: 28rpx; text-align: center }
.addresses { background: #fff; border-radius: 12rpx; padding: 20rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04); margin-bottom: 20rpx }
.addresses-title { font-size: 30rpx; margin-bottom: 12rpx }
.addresses-entry { display: flex; justify-content: space-between; align-items: center; background: #f7f7f7; border-radius: 12rpx; padding: 20rpx }
.addresses-text { font-size: 28rpx }
.addresses-arrow { font-size: 28rpx; color: #999 }
.refresh { width: 100%; margin-top: 12rpx }
.error { color: #e43; margin-top: 20rpx }
</style>

149
pages/orders/index.vue Normal file
View File

@ -0,0 +1,149 @@
<template>
<view class="wrap">
<view class="tabs">
<view class="tab" :class="{ active: currentTab === 'pending' }" @click="switchTab('pending')">待付款</view>
<view class="tab" :class="{ active: currentTab === 'completed' }" @click="switchTab('completed')">已完成</view>
</view>
<view v-if="error" class="error">{{ error }}</view>
<view v-if="orders.length === 0 && !loading" class="empty">暂无订单</view>
<view v-for="item in orders" :key="item.id || item.order_no" class="order">
<view class="order-main">
<view class="order-title">{{ item.title || item.subject || '订单' }}</view>
<view class="order-sub">{{ formatTime(item.created_at || item.time) }}</view>
</view>
<view class="order-right">
<view class="order-amount">{{ formatAmount(item.total_amount || item.amount || item.price) }}</view>
<view class="order-status">{{ statusText(item.status || item.pay_status || item.state) }}</view>
</view>
</view>
<view v-if="loadingMore" class="loading">加载中...</view>
<view v-else-if="!hasMore && orders.length > 0" class="end">没有更多了</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad, onReachBottom } from '@dcloudio/uni-app'
import { getOrders } from '../../api/appUser'
const currentTab = ref('pending')
const orders = ref([])
const loading = ref(false)
const loadingMore = ref(false)
const error = ref('')
const page = ref(1)
const pageSize = ref(20)
const hasMore = ref(true)
function formatTime(t) {
if (!t) return ''
const d = typeof t === 'string' ? new Date(t) : new Date(t)
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const hh = String(d.getHours()).padStart(2, '0')
const mm = String(d.getMinutes()).padStart(2, '0')
return `${y}-${m}-${day} ${hh}:${mm}`
}
function formatAmount(a) {
if (a === undefined || a === null) return ''
const n = Number(a)
if (Number.isNaN(n)) return String(a)
return `¥${n.toFixed(2)}`
}
function statusText(s) {
const v = String(s || '').toLowerCase()
if (v.includes('pend')) return '待付款'
if (v.includes('paid') || v.includes('complete') || v.includes('done')) return '已完成'
return s || ''
}
function switchTab(tab) {
if (currentTab.value === tab) return
currentTab.value = tab
fetchOrders(false)
}
function apiStatus() {
return currentTab.value === 'pending' ? 'pending' : 'completed'
}
async function fetchOrders(append) {
const user_id = uni.getStorageSync('user_id')
const token = uni.getStorageSync('token')
const phoneBound = !!uni.getStorageSync('phone_bound')
if (!user_id || !token || !phoneBound) {
uni.showModal({
title: '提示',
content: '请先登录并绑定手机号',
confirmText: '去登录',
success: (res) => {
if (res.confirm) {
uni.navigateTo({ url: '/pages/login/index' })
}
}
})
return
}
if (append) {
if (!hasMore.value || loadingMore.value) return
loadingMore.value = true
page.value = page.value + 1
} else {
loading.value = true
page.value = 1
hasMore.value = true
orders.value = []
}
error.value = ''
try {
const list = await getOrders(user_id, apiStatus(), page.value, pageSize.value)
const items = Array.isArray(list) ? list : (list && list.items) || []
const total = (list && list.total) || 0
orders.value = append ? orders.value.concat(items) : items
if (total) {
hasMore.value = orders.value.length < total
} else {
hasMore.value = items.length === pageSize.value
}
} catch (e) {
error.value = e && (e.message || e.errMsg) || '获取订单失败'
} finally {
if (append) {
loadingMore.value = false
} else {
loading.value = false
}
}
}
onLoad((opts) => {
const s = (opts && opts.status) || ''
if (s === 'completed' || s === 'pending') currentTab.value = s
fetchOrders(false)
})
onReachBottom(() => {
fetchOrders(true)
})
</script>
<style scoped>
.wrap { padding: 24rpx }
.tabs { display: flex; background: #fff; border-radius: 12rpx; padding: 8rpx; margin-bottom: 16rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04) }
.tab { flex: 1; text-align: center; padding: 16rpx 0; font-size: 28rpx; color: #666 }
.tab.active { color: #007AFF; font-weight: 600 }
.order { display: flex; justify-content: space-between; align-items: center; background: #fff; border-radius: 12rpx; padding: 20rpx; margin-bottom: 16rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04) }
.order-main { display: flex; flex-direction: column }
.order-title { font-size: 28rpx; color: #333 }
.order-sub { font-size: 24rpx; color: #999; margin-top: 6rpx }
.order-right { display: flex; flex-direction: column; align-items: flex-end }
.order-amount { font-size: 28rpx; color: #333 }
.order-status { font-size: 24rpx; color: #666; margin-top: 6rpx }
.empty { text-align: center; color: #999; margin-top: 40rpx }
.error { color: #e43; margin-bottom: 12rpx }
.loading { text-align: center; color: #666; margin: 20rpx 0 }
.end { text-align: center; color: #999; margin: 20rpx 0 }
</style>

117
pages/points/index.vue Normal file
View File

@ -0,0 +1,117 @@
<template>
<view class="wrap">
<view v-if="error" class="error">{{ error }}</view>
<view v-if="records.length === 0 && !loading" class="empty">暂无积分记录</view>
<view v-for="item in records" :key="item.id || item.time || item.created_at" class="record">
<view class="record-main">
<view class="record-title">{{ item.title || item.reason || '变更' }}</view>
<view class="record-time">{{ formatTime(item.time || item.created_at) }}</view>
</view>
<view class="record-amount" :class="{ inc: (item.change || item.amount || 0) > 0, dec: (item.change || item.amount || 0) < 0 }">
{{ (item.change ?? item.amount ?? 0) > 0 ? '+' : '' }}{{ item.change ?? item.amount ?? 0 }}
</view>
</view>
<view v-if="loadingMore" class="loading">加载中...</view>
<view v-else-if="!hasMore && records.length > 0" class="end">没有更多了</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import { onLoad, onReachBottom } from '@dcloudio/uni-app'
import { getPointsRecords } from '../../api/appUser'
const records = ref([])
const loading = ref(false)
const loadingMore = ref(false)
const error = ref('')
const page = ref(1)
const pageSize = ref(20)
const hasMore = ref(true)
function formatTime(t) {
if (!t) return ''
const d = typeof t === 'string' ? new Date(t) : new Date(t)
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const hh = String(d.getHours()).padStart(2, '0')
const mm = String(d.getMinutes()).padStart(2, '0')
return `${y}-${m}-${day} ${hh}:${mm}`
}
async function fetchRecords(append = false) {
const user_id = uni.getStorageSync('user_id')
const token = uni.getStorageSync('token')
const phoneBound = !!uni.getStorageSync('phone_bound')
if (!user_id || !token || !phoneBound) {
uni.showModal({
title: '提示',
content: '请先登录并绑定手机号',
confirmText: '去登录',
success: (res) => {
if (res.confirm) {
uni.navigateTo({ url: '/pages/login/index' })
}
}
})
return
}
if (append) {
if (!hasMore.value || loadingMore.value) return
loadingMore.value = true
page.value = page.value + 1
} else {
loading.value = true
page.value = 1
hasMore.value = true
}
error.value = ''
try {
const list = await getPointsRecords(user_id, page.value, pageSize.value)
const items = Array.isArray(list) ? list : (list && list.items) || []
const total = (list && list.total) || 0
if (append) {
records.value = records.value.concat(items)
} else {
records.value = items
}
if (total) {
hasMore.value = records.value.length < total
} else {
hasMore.value = items.length === pageSize.value
}
} catch (e) {
error.value = e && (e.message || e.errMsg) || '获取积分记录失败'
} finally {
if (append) {
loadingMore.value = false
} else {
loading.value = false
}
}
}
onLoad(() => {
fetchRecords(false)
})
onReachBottom(() => {
fetchRecords(true)
})
</script>
<style scoped>
.wrap { padding: 24rpx }
.record { display: flex; justify-content: space-between; align-items: center; background: #fff; border-radius: 12rpx; padding: 20rpx; margin-bottom: 16rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04) }
.record-main { display: flex; flex-direction: column }
.record-title { font-size: 28rpx; color: #333 }
.record-time { font-size: 24rpx; color: #999; margin-top: 6rpx }
.record-amount { font-size: 32rpx }
.record-amount.inc { color: #18a058 }
.record-amount.dec { color: #d03050 }
.empty { text-align: center; color: #999; margin-top: 40rpx }
.error { color: #e43; margin-bottom: 12rpx }
.loading { text-align: center; color: #666; margin: 20rpx 0 }
.end { text-align: center; color: #999; margin: 20rpx 0 }
</style>

289
pages/shop/index.vue Normal file
View File

@ -0,0 +1,289 @@
<template>
<view class="page">
<view v-if="loading" class="loading-wrap"><view class="spinner"></view></view>
<view class="products-section" v-else>
<view class="section-title">商品</view>
<view class="toolbar">
<input class="search" v-model="keyword" placeholder="搜索商品" confirm-type="search" @confirm="onSearchConfirm" />
<view class="filters">
<input class="price" type="number" v-model="minPrice" placeholder="最低价" />
<text class="dash">-</text>
<input class="price" type="number" v-model="maxPrice" placeholder="最高价" />
<button class="apply-btn" size="mini" @tap="onApplyFilters">筛选</button>
</view>
</view>
<view v-if="displayCount" class="products-columns">
<view class="column" v-for="(col, ci) in columns" :key="ci">
<view class="product-item" v-for="p in col" :key="p.id" @tap="onProductTap(p)">
<view class="product-card">
<view class="thumb-wrap">
<image class="product-thumb" :class="{ visible: isLoaded(p) }" :src="p.image" mode="widthFix" lazy-load="true" @load="onImageLoad(p)" @error="onImageError(p)" />
<view v-if="!isLoaded(p)" class="skeleton"></view>
<view class="badge">
<text class="badge-price" v-if="p.price !== null">{{ p.price }}</text>
<text class="badge-points" v-if="p.points !== null">{{ p.points }}积分</text>
</view>
</view>
<text class="product-title">{{ p.title }}</text>
<view class="product-extra" v-if="p.stock !== null">
<text class="stock">库存 {{ p.stock }}</text>
</view>
</view>
</view>
</view>
</view>
<view v-else class="empty">暂无商品</view>
</view>
</view>
</template>
<script setup>
import { onShow } from '@dcloudio/uni-app'
import { ref, computed } from 'vue'
import { request, authRequest } from '../../utils/request.js'
const products = ref([])
const columns = ref([[], []])
const colHeights = ref([0, 0])
const CACHE_KEY = 'products_cache_v1'
const TTL_MS = 10 * 60 * 1000
const loading = ref(false)
const keyword = ref('')
const minPrice = ref('')
const maxPrice = ref('')
const displayCount = computed(() => (columns.value[0].length + columns.value[1].length))
const loadedMap = ref({})
function getKey(p) { return String((p && p.id) ?? '') + '|' + String((p && p.image) ?? '') }
function unwrap(list) {
if (Array.isArray(list)) return list
const obj = list || {}
const data = obj.data || {}
const arr = obj.list || obj.items || data.list || data.items || data
return Array.isArray(arr) ? arr : []
}
function cleanUrl(u) {
const s = String(u || '').trim()
const m = s.match(/https?:\/\/[^\s'"`]+/)
if (m && m[0]) return m[0]
return s.replace(/[`'\"]/g, '').trim()
}
function apiGet(url, data = {}) {
const token = uni.getStorageSync('token')
const fn = token ? authRequest : request
return fn({ url, method: 'GET', data })
}
function getCachedProducts() {
try {
const obj = uni.getStorageSync(CACHE_KEY)
if (obj && Array.isArray(obj.data) && typeof obj.ts === 'number') {
const fresh = Date.now() - obj.ts < TTL_MS
if (fresh) return obj.data
}
} catch (_) {}
return null
}
function setCachedProducts(list) {
try {
uni.setStorageSync(CACHE_KEY, { data: Array.isArray(list) ? list : [], ts: Date.now() })
} catch (_) {}
}
function estimateHeight(p) {
const base = 220
const len = String(p.title || '').length
const lines = Math.min(2, Math.ceil(len / 12))
const titleH = lines * 36
const stockH = p.stock !== null && p.stock !== undefined ? 34 : 0
const padding = 28
return base + titleH + stockH + padding
}
function distributeToColumns(list) {
const arr = Array.isArray(list) ? list : []
const cols = Array.from({ length: 2 }, () => [])
const hs = [0, 0]
for (let i = 0; i < arr.length; i++) {
const h = estimateHeight(arr[i])
const idx = hs[0] <= hs[1] ? 0 : 1
cols[idx].push(arr[i])
hs[idx] += h
}
columns.value = cols
colHeights.value = hs
const presentKeys = new Set(arr.map(getKey))
const next = {}
const prev = loadedMap.value || {}
for (const k in prev) { if (presentKeys.has(k)) next[k] = prev[k] }
loadedMap.value = next
}
function extractListAndTotal(payload) {
if (Array.isArray(payload)) return { list: payload, total: payload.length }
const obj = payload || {}
const data = obj.data || {}
const list = obj.list || obj.items || data.list || data.items || []
const totalRaw = obj.total ?? data.total
const total = typeof totalRaw === 'number' ? totalRaw : (Array.isArray(list) ? list.length : 0)
return { list: Array.isArray(list) ? list : [], total }
}
function normalizeProducts(list) {
const arr = unwrap(list)
return arr.map((i, idx) => ({
id: i.id ?? i.productId ?? i._id ?? i.sku_id ?? String(idx),
image: cleanUrl(i.main_image ?? i.imageUrl ?? i.image_url ?? i.image ?? i.img ?? i.pic ?? ''),
title: i.title ?? i.name ?? i.product_name ?? i.sku_name ?? '',
price: i.price_sale ?? i.price ?? i.price_min ?? i.amount ?? null,
points: i.points_required ?? i.points ?? i.integral ?? null,
stock: i.stock ?? i.inventory ?? i.quantity ?? null,
link: cleanUrl(i.linkUrl ?? i.link_url ?? i.link ?? i.url ?? '')
})).filter(i => i.image || i.title)
}
function onProductTap(p) {
const imgs = (Array.isArray(products.value) ? products.value : []).map(x => x.image).filter(Boolean)
const current = p && p.image
if (current) {
uni.previewImage({ urls: imgs.length ? imgs : [current], current })
return
}
if (p.link && /^\/.+/.test(p.link)) {
uni.navigateTo({ url: p.link })
}
}
function applyFilters() {
const k = String(keyword.value || '').trim().toLowerCase()
const min = Number(minPrice.value)
const max = Number(maxPrice.value)
const hasMin = !isNaN(min) && String(minPrice.value).trim() !== ''
const hasMax = !isNaN(max) && String(maxPrice.value).trim() !== ''
const list = Array.isArray(products.value) ? products.value : []
const filtered = list.filter(p => {
const title = String(p.title || '').toLowerCase()
if (k && !title.includes(k)) return false
const priceNum = typeof p.price === 'number' ? p.price : Number(p.price)
if (hasMin) {
if (isNaN(priceNum)) return false
if (priceNum < min) return false
}
if (hasMax) {
if (isNaN(priceNum)) return false
if (priceNum > max) return false
}
return true
})
distributeToColumns(filtered)
}
function onSearchConfirm() { applyFilters() }
function onApplyFilters() { applyFilters() }
async function loadProducts() {
try {
const cached = getCachedProducts()
if (cached) {
products.value = cached
distributeToColumns(cached)
return
}
const first = await apiGet('/api/app/products', { page: 1 })
const { list: firstList, total } = extractListAndTotal(first)
const pageSize = 20
const totalPages = Math.max(1, Math.ceil(((typeof total === 'number' ? total : 0)) / pageSize))
if (totalPages <= 1) {
const normalized = normalizeProducts(firstList)
products.value = normalized
distributeToColumns(normalized)
setCachedProducts(normalized)
return
}
const tasks = []
for (let p = 2; p <= totalPages; p++) {
tasks.push(apiGet('/api/app/products', { page: p }))
}
const results = await Promise.allSettled(tasks)
const restLists = results.map(r => {
if (r.status === 'fulfilled') {
const { list } = extractListAndTotal(r.value)
return Array.isArray(list) ? list : []
}
return []
})
const merged = [firstList, ...restLists].flat()
const normalized = normalizeProducts(merged)
products.value = normalized
distributeToColumns(normalized)
setCachedProducts(normalized)
} catch (e) {
products.value = []
columns.value = [[], []]
colHeights.value = [0, 0]
const presentKeys = new Set([])
const next = {}
const prev = loadedMap.value || {}
for (const k in prev) { if (presentKeys.has(k)) next[k] = prev[k] }
loadedMap.value = next
}
}
function isLoaded(p) { return !!(loadedMap.value && loadedMap.value[getKey(p)]) }
function onImageLoad(p) { const k = getKey(p); if (!k) return; loadedMap.value = { ...(loadedMap.value || {}), [k]: true } }
function onImageError(p) { const k = getKey(p); if (!k) return; const prev = { ...(loadedMap.value || {}) }; delete prev[k]; loadedMap.value = prev }
onShow(async () => {
const token = uni.getStorageSync('token')
const phoneBound = !!uni.getStorageSync('phone_bound')
if (!token || !phoneBound) {
uni.showModal({
title: '提示',
content: '请先登录并绑定手机号',
confirmText: '去登录',
success: (res) => {
if (res.confirm) {
uni.navigateTo({ url: '/pages/login/index' })
}
}
})
return
}
loading.value = true
await loadProducts()
loading.value = false
})
</script>
<style scoped>
.page { padding: 24rpx }
.section-title { font-size: 30rpx; font-weight: 600; margin-bottom: 16rpx }
.products-section { background: #ffffff; border-radius: 12rpx; padding: 24rpx; margin-top: 24rpx }
.loading-wrap { min-height: 60vh; display: flex; align-items: center; justify-content: center }
.spinner { width: 56rpx; height: 56rpx; border: 6rpx solid rgba(0,122,255,0.15); border-top-color: #007AFF; border-radius: 50%; animation: spin 1s linear infinite }
@keyframes spin { from { transform: rotate(0) } to { transform: rotate(360deg) } }
.toolbar { display: flex; flex-direction: column; gap: 12rpx; margin-bottom: 16rpx }
.search { background: #f6f8ff; border: 1rpx solid rgba(0,122,255,0.25); border-radius: 999rpx; padding: 14rpx 20rpx; font-size: 26rpx }
.filters { display: flex; align-items: center; gap: 12rpx }
.price { flex: 1; background: #f6f8ff; border: 1rpx solid rgba(0,122,255,0.25); border-radius: 999rpx; padding: 12rpx 16rpx; font-size: 26rpx }
.dash { color: #888; font-size: 26rpx }
.apply-btn { background: #007AFF; color: #fff; border-radius: 999rpx; padding: 0 20rpx }
.products-columns { display: flex; gap: 12rpx }
.column { flex: 1 }
.product-item { margin-bottom: 12rpx }
.empty { padding: 40rpx; color: #888; text-align: center }
.product-card { background: #fff; border-radius: 16rpx; overflow: hidden; box-shadow: 0 6rpx 16rpx rgba(0,122,255,0.08); transition: transform .15s ease }
.product-item:active .product-card { transform: scale(0.98) }
.thumb-wrap { position: relative }
.product-thumb { width: 100%; height: auto; display: block; opacity: 0; transition: opacity .25s ease; z-index: 0 }
.product-thumb.visible { opacity: 1 }
.skeleton { position: absolute; left: 0; top: 0; right: 0; bottom: 0; background: linear-gradient(90deg, #eef2ff 25%, #f6f8ff 37%, #eef2ff 63%); background-size: 400% 100%; animation: shimmer 1.2s ease infinite; z-index: 1 }
@keyframes shimmer { 0% { background-position: 100% 0 } 100% { background-position: 0 0 } }
.thumb-wrap { background: #f6f8ff; min-height: 220rpx }
.badge { position: absolute; left: 12rpx; bottom: 12rpx; display: flex; gap: 8rpx }
.badge-price { background: #007AFF; color: #fff; font-size: 22rpx; padding: 6rpx 12rpx; border-radius: 999rpx; box-shadow: 0 2rpx 8rpx rgba(0,122,255,0.25) }
.badge-points { background: rgba(0,122,255,0.85); color: #fff; font-size: 22rpx; padding: 6rpx 12rpx; border-radius: 999rpx }
.product-title { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; margin: 12rpx; font-size: 26rpx; color: #222 }
.product-extra { display: flex; justify-content: flex-end; align-items: center; margin: 0 12rpx 12rpx }
.stock { font-size: 22rpx; color: #888 }
</style>

BIN
static/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
static/tab/box.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 B

BIN
static/tab/box_active.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 651 B

BIN
static/tab/home.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
static/tab/home_active.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
static/tab/profile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
static/tab/shop.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

BIN
static/tab/shop_active.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

13
uni.promisify.adaptor.js Normal file
View File

@ -0,0 +1,13 @@
uni.addInterceptor({
returnValue (res) {
if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) {
return res;
}
return new Promise((resolve, reject) => {
res.then((res) => {
if (!res) return resolve(res)
return res[0] ? reject(res[0]) : resolve(res[1])
});
});
},
});

76
uni.scss Normal file
View File

@ -0,0 +1,76 @@
/**
* 这里是uni-app内置的常用样式变量
*
* uni-app 官方扩展插件及插件市场https://ext.dcloud.net.cn上很多三方插件均使用了这些样式变量
* 如果你是插件开发者建议你使用scss预处理并在插件代码中直接使用这些变量无需 import 这个文件方便用户通过搭积木的方式开发整体风格一致的App
*
*/
/**
* 如果你是App开发者插件使用者你可以通过修改这些变量来定制自己的插件主题实现自定义主题功能
*
* 如果你的项目同样使用了scss预处理你也可以直接在你的 scss 代码中使用如下变量同时无需 import 这个文件
*/
/* 颜色变量 */
/* 行为相关颜色 */
$uni-color-primary: #007aff;
$uni-color-success: #4cd964;
$uni-color-warning: #f0ad4e;
$uni-color-error: #dd524d;
/* 文字基本颜色 */
$uni-text-color:#333;//基本色
$uni-text-color-inverse:#fff;//反色
$uni-text-color-grey:#999;//辅助灰色如加载更多的提示信息
$uni-text-color-placeholder: #808080;
$uni-text-color-disable:#c0c0c0;
/* 背景颜色 */
$uni-bg-color:#ffffff;
$uni-bg-color-grey:#f8f8f8;
$uni-bg-color-hover:#f1f1f1;//点击状态颜色
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
/* 边框颜色 */
$uni-border-color:#c8c7cc;
/* 尺寸变量 */
/* 文字尺寸 */
$uni-font-size-sm:12px;
$uni-font-size-base:14px;
$uni-font-size-lg:16px;
/* 图片尺寸 */
$uni-img-size-sm:20px;
$uni-img-size-base:26px;
$uni-img-size-lg:40px;
/* Border Radius */
$uni-border-radius-sm: 2px;
$uni-border-radius-base: 3px;
$uni-border-radius-lg: 6px;
$uni-border-radius-circle: 50%;
/* 水平间距 */
$uni-spacing-row-sm: 5px;
$uni-spacing-row-base: 10px;
$uni-spacing-row-lg: 15px;
/* 垂直间距 */
$uni-spacing-col-sm: 4px;
$uni-spacing-col-base: 8px;
$uni-spacing-col-lg: 12px;
/* 透明度 */
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
/* 文章场景相关 */
$uni-color-title: #2C405A; // 文章标题颜色
$uni-font-size-title:20px;
$uni-color-subtitle: #555555; // 二级标题颜色
$uni-font-size-subtitle:26px;
$uni-color-paragraph: #3F536E; // 文章段落颜色
$uni-font-size-paragraph:15px;

View File

@ -0,0 +1 @@
{"version":3,"file":"appUser.js","sources":["api/appUser.js"],"sourcesContent":["import { request, authRequest } from '../utils/request'\n\nexport function wechatLogin(code, invite_code) {\n const data = invite_code ? { code, invite_code } : { code }\n return request({ url: '/api/app/users/weixin/login', method: 'POST', data })\n}\n\nexport function bindPhone(user_id, code, extraHeader = {}) {\n return authRequest({ url: `/api/app/users/${user_id}/phone/bind`, method: 'POST', data: { code }, header: extraHeader })\n}\n\nexport function getUserStats(user_id) {\n return authRequest({ url: `/api/app/users/${user_id}/stats`, method: 'GET' })\n}\n\nexport function getPointsBalance(user_id) {\n return authRequest({ url: `/api/app/users/${user_id}/points/balance`, method: 'GET' })\n}\n\nexport function getPointsRecords(user_id, page = 1, page_size = 20) {\n return authRequest({ url: `/api/app/users/${user_id}/points`, method: 'GET', data: { page, page_size } })\n}\n\nexport function getOrders(user_id, status, page = 1, page_size = 20) {\n const data = { page, page_size }\n if (status) data.status = status\n return authRequest({ url: `/api/app/users/${user_id}/orders`, method: 'GET', data })\n}\n\nexport function listAddresses(user_id) {\n return authRequest({ url: `/api/app/users/${user_id}/addresses`, method: 'GET' })\n}\n\nexport function addAddress(user_id, payload) {\n return authRequest({ url: `/api/app/users/${user_id}/addresses`, method: 'POST', data: payload })\n}\n\nexport function updateAddress(user_id, address_id, payload) {\n return authRequest({ url: `/api/app/users/${user_id}/addresses/${address_id}`, method: 'PUT', data: payload })\n}\n\nexport function deleteAddress(user_id, address_id) {\n return authRequest({ url: `/api/app/users/${user_id}/addresses/${address_id}`, method: 'DELETE' })\n}\n\nexport function setDefaultAddress(user_id, address_id) {\n return authRequest({ url: `/api/app/users/${user_id}/addresses/${address_id}/default`, method: 'PUT' })\n}\n\nexport function getActivityDetail(activity_id) {\n return authRequest({ url: `/api/app/activities/${activity_id}`, method: 'GET' })\n}\n\nexport function getActivityIssues(activity_id) {\n return authRequest({ url: `/api/app/activities/${activity_id}/issues`, method: 'GET' })\n}\n\nexport function getActivityIssueRewards(activity_id, issue_id) {\n return authRequest({ url: `/api/app/activities/${activity_id}/issues/${issue_id}/rewards`, method: 'GET' })\n}"],"names":["request","authRequest"],"mappings":";;AAEO,SAAS,YAAY,MAAM,aAAa;AAC7C,QAAM,OAAO,cAAc,EAAE,MAAM,YAAW,IAAK,EAAE,KAAM;AAC3D,SAAOA,cAAAA,QAAQ,EAAE,KAAK,+BAA+B,QAAQ,QAAQ,MAAM;AAC7E;AAEO,SAAS,UAAU,SAAS,MAAM,cAAc,CAAA,GAAI;AACzD,SAAOC,cAAAA,YAAY,EAAE,KAAK,kBAAkB,OAAO,eAAe,QAAQ,QAAQ,MAAM,EAAE,KAAM,GAAE,QAAQ,YAAW,CAAE;AACzH;AAEO,SAAS,aAAa,SAAS;AACpC,SAAOA,cAAW,YAAC,EAAE,KAAK,kBAAkB,OAAO,UAAU,QAAQ,OAAO;AAC9E;AAEO,SAAS,iBAAiB,SAAS;AACxC,SAAOA,cAAW,YAAC,EAAE,KAAK,kBAAkB,OAAO,mBAAmB,QAAQ,OAAO;AACvF;AAEO,SAAS,iBAAiB,SAAS,OAAO,GAAG,YAAY,IAAI;AAClE,SAAOA,cAAW,YAAC,EAAE,KAAK,kBAAkB,OAAO,WAAW,QAAQ,OAAO,MAAM,EAAE,MAAM,UAAW,EAAA,CAAE;AAC1G;AAEO,SAAS,UAAU,SAAS,QAAQ,OAAO,GAAG,YAAY,IAAI;AACnE,QAAM,OAAO,EAAE,MAAM,UAAW;AAChC,MAAI;AAAQ,SAAK,SAAS;AAC1B,SAAOA,0BAAY,EAAE,KAAK,kBAAkB,OAAO,WAAW,QAAQ,OAAO,MAAM;AACrF;AAEO,SAAS,cAAc,SAAS;AACrC,SAAOA,cAAW,YAAC,EAAE,KAAK,kBAAkB,OAAO,cAAc,QAAQ,OAAO;AAClF;AAEO,SAAS,WAAW,SAAS,SAAS;AAC3C,SAAOA,0BAAY,EAAE,KAAK,kBAAkB,OAAO,cAAc,QAAQ,QAAQ,MAAM,QAAO,CAAE;AAClG;AAEO,SAAS,cAAc,SAAS,YAAY,SAAS;AAC1D,SAAOA,cAAW,YAAC,EAAE,KAAK,kBAAkB,OAAO,cAAc,UAAU,IAAI,QAAQ,OAAO,MAAM,QAAO,CAAE;AAC/G;AAEO,SAAS,cAAc,SAAS,YAAY;AACjD,SAAOA,0BAAY,EAAE,KAAK,kBAAkB,OAAO,cAAc,UAAU,IAAI,QAAQ,SAAQ,CAAE;AACnG;AAEO,SAAS,kBAAkB,SAAS,YAAY;AACrD,SAAOA,0BAAY,EAAE,KAAK,kBAAkB,OAAO,cAAc,UAAU,YAAY,QAAQ,MAAK,CAAE;AACxG;AAEO,SAAS,kBAAkB,aAAa;AAC7C,SAAOA,cAAW,YAAC,EAAE,KAAK,uBAAuB,WAAW,IAAI,QAAQ,OAAO;AACjF;AAEO,SAAS,kBAAkB,aAAa;AAC7C,SAAOA,cAAW,YAAC,EAAE,KAAK,uBAAuB,WAAW,WAAW,QAAQ,OAAO;AACxF;AAEO,SAAS,wBAAwB,aAAa,UAAU;AAC7D,SAAOA,0BAAY,EAAE,KAAK,uBAAuB,WAAW,WAAW,QAAQ,YAAY,QAAQ,MAAK,CAAE;AAC5G;;;;;;;;;;;;;;;"}

View File

@ -0,0 +1 @@
{"version":3,"file":"app.js","sources":["App.vue","main.js"],"sourcesContent":["<script>\r\n\texport default {\r\n\t\tonLaunch: function() {\r\n\t\t\tconsole.log('App Launch')\r\n\t\t},\r\n\t\tonShow: function() {\r\n\t\t\tconsole.log('App Show')\r\n\t\t},\r\n\t\tonHide: function() {\r\n\t\t\tconsole.log('App Hide')\r\n\t\t}\r\n\t}\r\n</script>\r\n\r\n<style>\r\n\t/*每个页面公共css */\r\n</style>\n","import App from './App'\n\n// #ifndef VUE3\nimport Vue from 'vue'\nimport './uni.promisify.adaptor'\nVue.config.productionTip = false\nApp.mpType = 'app'\nconst app = new Vue({\n ...App\n})\napp.$mount()\n// #endif\n\n// #ifdef VUE3\nimport { createSSRApp } from 'vue'\nexport function createApp() {\n const app = createSSRApp(App)\n return {\n app\n }\n}\n// #endif"],"names":["uni","createSSRApp","App"],"mappings":";;;;;;;;;;;;;;;;;;;;AACC,MAAK,YAAU;AAAA,EACd,UAAU,WAAW;AACpBA,kBAAAA,MAAA,MAAA,OAAA,gBAAY,YAAY;AAAA,EACxB;AAAA,EACD,QAAQ,WAAW;AAClBA,kBAAAA,MAAY,MAAA,OAAA,gBAAA,UAAU;AAAA,EACtB;AAAA,EACD,QAAQ,WAAW;AAClBA,kBAAAA,MAAY,MAAA,OAAA,iBAAA,UAAU;AAAA,EACvB;AACD;ACIM,SAAS,YAAY;AAC1B,QAAM,MAAMC,cAAY,aAACC,SAAG;AAC5B,SAAO;AAAA,IACL;AAAA,EACD;AACH;;;"}

View File

@ -0,0 +1 @@
{"version":3,"file":"assets.js","sources":["static/logo.png"],"sourcesContent":["export default \"__VITE_ASSET__46719607__\""],"names":[],"mappings":";AAAA,MAAe,aAAA;;"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
{"version":3,"file":"index.js","sources":["pages/cabinet/index.vue","../../迅雷下载/HBuilderX.4.57.2025032507/HBuilderX/plugins/uniapp-cli-vite/uniPage:/cGFnZXMvY2FiaW5ldC9pbmRleC52dWU"],"sourcesContent":["<template>\n <view class=\"wrap\">货柜</view>\n</template>\n\n<script setup>\nimport { onShow } from '@dcloudio/uni-app'\nonShow(() => {\n const token = uni.getStorageSync('token')\n const phoneBound = !!uni.getStorageSync('phone_bound')\n console.log('cabinet onShow token:', token, 'isLogin:', !!token, 'phoneBound:', phoneBound)\n if (!token || !phoneBound) {\n uni.showModal({\n title: '提示',\n content: '请先登录并绑定手机号',\n confirmText: '去登录',\n success: (res) => {\n if (res.confirm) {\n uni.navigateTo({ url: '/pages/login/index' })\n }\n }\n })\n }\n})\n</script>\n\n<style scoped>\n.wrap { padding: 40rpx }\n</style>","import MiniProgramPage from 'D:/project/app_client/pages/cabinet/index.vue'\nwx.createPage(MiniProgramPage)"],"names":["onShow","uni"],"mappings":";;;;;AAMAA,kBAAAA,OAAO,MAAM;AACX,YAAM,QAAQC,cAAAA,MAAI,eAAe,OAAO;AACxC,YAAM,aAAa,CAAC,CAACA,oBAAI,eAAe,aAAa;AACrDA,oBAAAA,MAAY,MAAA,OAAA,iCAAA,yBAAyB,OAAO,YAAY,CAAC,CAAC,OAAO,eAAe,UAAU;AAC1F,UAAI,CAAC,SAAS,CAAC,YAAY;AACzBA,sBAAAA,MAAI,UAAU;AAAA,UACZ,OAAO;AAAA,UACP,SAAS;AAAA,UACT,aAAa;AAAA,UACb,SAAS,CAAC,QAAQ;AAChB,gBAAI,IAAI,SAAS;AACfA,4BAAAA,MAAI,WAAW,EAAE,KAAK,qBAAoB,CAAE;AAAA,YAC7C;AAAA,UACF;AAAA,QACP,CAAK;AAAA,MACF;AAAA,IACH,CAAC;;;;;;;ACrBD,GAAG,WAAW,eAAe;"}

View File

@ -0,0 +1 @@
{"version":3,"file":"index.js","sources":["pages/help/index.vue","../../迅雷下载/HBuilderX.4.57.2025032507/HBuilderX/plugins/uniapp-cli-vite/uniPage:/cGFnZXMvaGVscC9pbmRleC52dWU"],"sourcesContent":["<template>\n <view class=\"wrap\">\n <view class=\"card\" @click=\"toUser\">\n <view class=\"title\">用户协议</view>\n <view class=\"desc\">查看平台使用条款与隐私说明</view>\n </view>\n <view class=\"card\" @click=\"toPurchase\">\n <view class=\"title\">购买协议</view>\n <view class=\"desc\">查看盲盒购买规则与售后政策</view>\n </view>\n </view>\n</template>\n\n<script>\nexport default {\n methods: {\n toUser() { uni.navigateTo({ url: '/pages/agreement/user' }) },\n toPurchase() { uni.navigateTo({ url: '/pages/agreement/purchase' }) }\n }\n}\n</script>\n\n<style scoped>\n.wrap { padding: 24rpx }\n.card { background: #fff; border-radius: 12rpx; padding: 24rpx; margin-bottom: 16rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04) }\n.title { font-size: 30rpx; font-weight: 600 }\n.desc { margin-top: 8rpx; font-size: 24rpx; color: #666 }\n</style>","import MiniProgramPage from 'D:/project/app_client/pages/help/index.vue'\nwx.createPage(MiniProgramPage)"],"names":["uni"],"mappings":";;AAcA,MAAK,YAAU;AAAA,EACb,SAAS;AAAA,IACP,SAAS;AAAEA,oBAAAA,MAAI,WAAW,EAAE,KAAK,wBAAsB,CAAG;AAAA,IAAG;AAAA,IAC7D,aAAa;AAAEA,oBAAAA,MAAI,WAAW,EAAE,KAAK,4BAA4B,CAAC;AAAA,IAAE;AAAA,EACtE;AACF;;;;;;;;AClBA,GAAG,WAAW,eAAe;"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,63 @@
"use strict";
const utils_request = require("../utils/request.js");
function wechatLogin(code, invite_code) {
const data = invite_code ? { code, invite_code } : { code };
return utils_request.request({ url: "/api/app/users/weixin/login", method: "POST", data });
}
function bindPhone(user_id, code, extraHeader = {}) {
return utils_request.authRequest({ url: `/api/app/users/${user_id}/phone/bind`, method: "POST", data: { code }, header: extraHeader });
}
function getUserStats(user_id) {
return utils_request.authRequest({ url: `/api/app/users/${user_id}/stats`, method: "GET" });
}
function getPointsBalance(user_id) {
return utils_request.authRequest({ url: `/api/app/users/${user_id}/points/balance`, method: "GET" });
}
function getPointsRecords(user_id, page = 1, page_size = 20) {
return utils_request.authRequest({ url: `/api/app/users/${user_id}/points`, method: "GET", data: { page, page_size } });
}
function getOrders(user_id, status, page = 1, page_size = 20) {
const data = { page, page_size };
if (status)
data.status = status;
return utils_request.authRequest({ url: `/api/app/users/${user_id}/orders`, method: "GET", data });
}
function listAddresses(user_id) {
return utils_request.authRequest({ url: `/api/app/users/${user_id}/addresses`, method: "GET" });
}
function addAddress(user_id, payload) {
return utils_request.authRequest({ url: `/api/app/users/${user_id}/addresses`, method: "POST", data: payload });
}
function updateAddress(user_id, address_id, payload) {
return utils_request.authRequest({ url: `/api/app/users/${user_id}/addresses/${address_id}`, method: "PUT", data: payload });
}
function deleteAddress(user_id, address_id) {
return utils_request.authRequest({ url: `/api/app/users/${user_id}/addresses/${address_id}`, method: "DELETE" });
}
function setDefaultAddress(user_id, address_id) {
return utils_request.authRequest({ url: `/api/app/users/${user_id}/addresses/${address_id}/default`, method: "PUT" });
}
function getActivityDetail(activity_id) {
return utils_request.authRequest({ url: `/api/app/activities/${activity_id}`, method: "GET" });
}
function getActivityIssues(activity_id) {
return utils_request.authRequest({ url: `/api/app/activities/${activity_id}/issues`, method: "GET" });
}
function getActivityIssueRewards(activity_id, issue_id) {
return utils_request.authRequest({ url: `/api/app/activities/${activity_id}/issues/${issue_id}/rewards`, method: "GET" });
}
exports.addAddress = addAddress;
exports.bindPhone = bindPhone;
exports.deleteAddress = deleteAddress;
exports.getActivityDetail = getActivityDetail;
exports.getActivityIssueRewards = getActivityIssueRewards;
exports.getActivityIssues = getActivityIssues;
exports.getOrders = getOrders;
exports.getPointsBalance = getPointsBalance;
exports.getPointsRecords = getPointsRecords;
exports.getUserStats = getUserStats;
exports.listAddresses = listAddresses;
exports.setDefaultAddress = setDefaultAddress;
exports.updateAddress = updateAddress;
exports.wechatLogin = wechatLogin;
//# sourceMappingURL=../../.sourcemap/mp-weixin/api/appUser.js.map

40
unpackage/dist/dev/mp-weixin/app.js vendored Normal file
View File

@ -0,0 +1,40 @@
"use strict";
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
const common_vendor = require("./common/vendor.js");
if (!Math) {
"./pages/index/index.js";
"./pages/login/index.js";
"./pages/shop/index.js";
"./pages/cabinet/index.js";
"./pages/mine/index.js";
"./pages/points/index.js";
"./pages/orders/index.js";
"./pages/address/index.js";
"./pages/address/edit.js";
"./pages/help/index.js";
"./pages/agreement/user.js";
"./pages/agreement/purchase.js";
"./pages/activity/yifanshang/index.js";
"./pages/activity/wuxianshang/index.js";
"./pages/activity/duiduipeng/index.js";
}
const _sfc_main = {
onLaunch: function() {
common_vendor.index.__f__("log", "at App.vue:4", "App Launch");
},
onShow: function() {
common_vendor.index.__f__("log", "at App.vue:7", "App Show");
},
onHide: function() {
common_vendor.index.__f__("log", "at App.vue:10", "App Hide");
}
};
function createApp() {
const app = common_vendor.createSSRApp(_sfc_main);
return {
app
};
}
createApp().app.mount("#app");
exports.createApp = createApp;
//# sourceMappingURL=../.sourcemap/mp-weixin/app.js.map

58
unpackage/dist/dev/mp-weixin/app.json vendored Normal file
View File

@ -0,0 +1,58 @@
{
"pages": [
"pages/index/index",
"pages/login/index",
"pages/shop/index",
"pages/cabinet/index",
"pages/mine/index",
"pages/points/index",
"pages/orders/index",
"pages/address/index",
"pages/address/edit",
"pages/help/index",
"pages/agreement/user",
"pages/agreement/purchase",
"pages/activity/yifanshang/index",
"pages/activity/wuxianshang/index",
"pages/activity/duiduipeng/index"
],
"window": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
},
"tabBar": {
"color": "#7A7E83",
"selectedColor": "#007AFF",
"backgroundColor": "#FFFFFF",
"borderStyle": "black",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页",
"iconPath": "static/tab/home.png",
"selectedIconPath": "static/tab/home_active.png"
},
{
"pagePath": "pages/shop/index",
"text": "商城",
"iconPath": "static/tab/shop.png",
"selectedIconPath": "static/tab/shop_active.png"
},
{
"pagePath": "pages/cabinet/index",
"text": "货柜",
"iconPath": "static/tab/box.png",
"selectedIconPath": "static/tab/box_active.png"
},
{
"pagePath": "pages/mine/index",
"text": "我的",
"iconPath": "static/tab/profile.png",
"selectedIconPath": "static/tab/profile_active.png"
}
]
},
"usingComponents": {}
}

3
unpackage/dist/dev/mp-weixin/app.wxss vendored Normal file
View File

@ -0,0 +1,3 @@
/*每个页面公共css */
page{--status-bar-height:25px;--top-window-height:0px;--window-top:0px;--window-bottom:0px;--window-left:0px;--window-right:0px;--window-magin:0px}[data-c-h="true"]{display: none !important;}

View File

@ -0,0 +1,4 @@
"use strict";
const _imports_0 = "/static/logo.png";
exports._imports_0 = _imports_0;
//# sourceMappingURL=../../.sourcemap/mp-weixin/common/assets.js.map

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,137 @@
"use strict";
const common_vendor = require("../../../common/vendor.js");
const api_appUser = require("../../../api/appUser.js");
const _sfc_main = {
__name: "index",
setup(__props) {
const detail = common_vendor.ref({});
const statusText = common_vendor.ref("");
const issues = common_vendor.ref([]);
const rewardsMap = common_vendor.ref({});
function statusToText(s) {
if (s === 1)
return "进行中";
if (s === 0)
return "未开始";
if (s === 2)
return "已结束";
return String(s || "");
}
async function fetchDetail(id) {
const data = await api_appUser.getActivityDetail(id);
detail.value = data || {};
statusText.value = statusToText(detail.value.status);
}
function unwrap(list) {
if (Array.isArray(list))
return list;
const obj = list || {};
const data = obj.data || {};
const arr = obj.list || obj.items || data.list || data.items || data;
return Array.isArray(arr) ? arr : [];
}
function normalizeIssues(list) {
const arr = unwrap(list);
return arr.map((i, idx) => ({
id: i.id ?? String(idx),
title: i.title ?? i.name ?? "",
no: i.no ?? i.index ?? i.issue_no ?? i.issue_number ?? null,
status_text: i.status_text ?? (i.status === 1 ? "进行中" : i.status === 0 ? "未开始" : i.status === 2 ? "已结束" : "")
}));
}
function normalizeRewards(list) {
const arr = unwrap(list);
return arr.map((i, idx) => ({
id: i.id ?? String(idx),
title: i.title ?? i.name ?? "",
image: i.image ?? i.img ?? i.pic ?? i.banner ?? "",
odds: i.odds ?? i.rate ?? i.probability ?? i.prob ?? "",
rarity: i.rarity ?? i.rarity_name ?? ""
}));
}
async function fetchRewardsForIssues(activityId) {
const list = issues.value || [];
const promises = list.map((it) => api_appUser.getActivityIssueRewards(activityId, it.id));
const results = await Promise.allSettled(promises);
results.forEach((res, i) => {
const issueId = list[i] && list[i].id;
if (!issueId)
return;
const value = res.status === "fulfilled" ? normalizeRewards(res.value) : [];
rewardsMap.value = { ...rewardsMap.value || {}, [issueId]: value };
});
}
async function fetchIssues(id) {
const data = await api_appUser.getActivityIssues(id);
issues.value = normalizeIssues(data);
await fetchRewardsForIssues(id);
}
function onPreviewBanner() {
const url = detail.value.banner || "";
if (url)
common_vendor.index.previewImage({ urls: [url], current: url });
}
function onParticipate() {
common_vendor.index.showToast({ title: "功能待接入", icon: "none" });
}
common_vendor.onLoad((opts) => {
const id = opts && opts.id || "";
if (id) {
fetchDetail(id);
fetchIssues(id);
}
});
return (_ctx, _cache) => {
return common_vendor.e({
a: detail.value.banner
}, detail.value.banner ? {
b: detail.value.banner
} : {}, {
c: common_vendor.t(detail.value.name || detail.value.title || "-"),
d: common_vendor.t(detail.value.category_name || "对对碰"),
e: detail.value.price_draw !== void 0
}, detail.value.price_draw !== void 0 ? {
f: common_vendor.t(detail.value.price_draw)
} : {}, {
g: detail.value.status !== void 0
}, detail.value.status !== void 0 ? {
h: common_vendor.t(statusText.value)
} : {}, {
i: common_vendor.o(onPreviewBanner),
j: common_vendor.o(onParticipate),
k: issues.value.length
}, issues.value.length ? {
l: common_vendor.f(issues.value, (it, k0, i0) => {
return common_vendor.e({
a: common_vendor.t(it.title || "第" + (it.no || it.index || it.issue_no || "-") + "期"),
b: it.status_text
}, it.status_text ? {
c: common_vendor.t(it.status_text)
} : {}, {
d: rewardsMap.value[it.id] && rewardsMap.value[it.id].length
}, rewardsMap.value[it.id] && rewardsMap.value[it.id].length ? {
e: common_vendor.f(rewardsMap.value[it.id], (rw, k1, i1) => {
return common_vendor.e({
a: rw.image
}, rw.image ? {
b: rw.image
} : {}, {
c: common_vendor.t(rw.title),
d: rw.rarity || rw.odds
}, rw.rarity || rw.odds ? {
e: common_vendor.t([rw.rarity, rw.odds].filter(Boolean).join(" · "))
} : {}, {
f: rw.id
});
})
} : {}, {
f: it.id
});
})
} : {});
};
}
};
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["__scopeId", "data-v-e6d1f6ed"]]);
wx.createPage(MiniProgramPage);
//# sourceMappingURL=../../../../.sourcemap/mp-weixin/pages/activity/duiduipeng/index.js.map

View File

@ -0,0 +1,4 @@
{
"navigationBarTitleText": "对对碰",
"usingComponents": {}
}

View File

@ -0,0 +1 @@
<scroll-view class="page data-v-e6d1f6ed" scroll-y><view wx:if="{{a}}" class="banner data-v-e6d1f6ed"><image class="banner-img data-v-e6d1f6ed" src="{{b}}" mode="widthFix"/></view><view class="header data-v-e6d1f6ed"><view class="title data-v-e6d1f6ed">{{c}}</view><view class="meta data-v-e6d1f6ed">分类:{{d}}</view><view wx:if="{{e}}" class="meta data-v-e6d1f6ed">参与价:¥{{f}}</view><view wx:if="{{g}}" class="meta data-v-e6d1f6ed">状态:{{h}}</view></view><view class="actions data-v-e6d1f6ed"><button class="btn data-v-e6d1f6ed" bindtap="{{i}}">查看图片</button><button class="btn primary data-v-e6d1f6ed" bindtap="{{j}}">立即参与</button></view><view class="issues data-v-e6d1f6ed"><view class="issues-title data-v-e6d1f6ed">期数</view><view wx:if="{{k}}" class="issues-list data-v-e6d1f6ed"><view wx:for="{{l}}" wx:for-item="it" wx:key="f" class="issue-item data-v-e6d1f6ed"><text class="issue-title data-v-e6d1f6ed">{{it.a}}</text><text wx:if="{{it.b}}" class="issue-status data-v-e6d1f6ed">{{it.c}}</text><view wx:if="{{it.d}}" class="rewards data-v-e6d1f6ed"><view wx:for="{{it.e}}" wx:for-item="rw" wx:key="f" class="reward data-v-e6d1f6ed"><image wx:if="{{rw.a}}" class="reward-img data-v-e6d1f6ed" src="{{rw.b}}" mode="aspectFill"/><view class="reward-texts data-v-e6d1f6ed"><text class="reward-title data-v-e6d1f6ed">{{rw.c}}</text><text wx:if="{{rw.d}}" class="reward-meta data-v-e6d1f6ed">{{rw.e}}</text></view></view></view><view wx:else class="rewards-empty data-v-e6d1f6ed">暂无奖励配置</view></view></view><view wx:else class="issues-empty data-v-e6d1f6ed">暂无期数</view></view></scroll-view>

View File

@ -0,0 +1,49 @@
.page.data-v-e6d1f6ed { height: 100vh
}
.banner.data-v-e6d1f6ed { padding: 24rpx
}
.banner-img.data-v-e6d1f6ed { width: 100%
}
.header.data-v-e6d1f6ed { padding: 0 24rpx
}
.title.data-v-e6d1f6ed { font-size: 36rpx; font-weight: 700
}
.meta.data-v-e6d1f6ed { margin-top: 8rpx; font-size: 26rpx; color: #666
}
.actions.data-v-e6d1f6ed { display: flex; padding: 24rpx; gap: 16rpx
}
.btn.data-v-e6d1f6ed { flex: 1
}
.primary.data-v-e6d1f6ed { background-color: #007AFF; color: #fff
}
.issues.data-v-e6d1f6ed { background: #fff; border-radius: 12rpx; margin: 0 24rpx 24rpx; padding: 16rpx
}
.issues-title.data-v-e6d1f6ed { font-size: 30rpx; font-weight: 600; margin-bottom: 12rpx
}
.issues-list.data-v-e6d1f6ed {
}
.issue-item.data-v-e6d1f6ed { display: flex; justify-content: space-between; padding: 12rpx 0; border-bottom: 1rpx solid #f0f0f0
}
.issue-item.data-v-e6d1f6ed:last-child { border-bottom: 0
}
.issue-title.data-v-e6d1f6ed { font-size: 26rpx
}
.issue-status.data-v-e6d1f6ed { font-size: 24rpx; color: #666
}
.rewards.data-v-e6d1f6ed { width: 100%; margin-top: 12rpx
}
.reward.data-v-e6d1f6ed { display: flex; align-items: center; margin-bottom: 8rpx
}
.reward-img.data-v-e6d1f6ed { width: 80rpx; height: 80rpx; border-radius: 8rpx; margin-right: 12rpx; background: #f5f5f5
}
.reward-texts.data-v-e6d1f6ed { display: flex; flex-direction: column
}
.reward-title.data-v-e6d1f6ed { font-size: 26rpx
}
.reward-meta.data-v-e6d1f6ed { font-size: 22rpx; color: #888
}
.rewards-empty.data-v-e6d1f6ed { font-size: 24rpx; color: #999
}
.issues-empty.data-v-e6d1f6ed { font-size: 24rpx; color: #999
}

View File

@ -0,0 +1,137 @@
"use strict";
const common_vendor = require("../../../common/vendor.js");
const api_appUser = require("../../../api/appUser.js");
const _sfc_main = {
__name: "index",
setup(__props) {
const detail = common_vendor.ref({});
const statusText = common_vendor.ref("");
const issues = common_vendor.ref([]);
const rewardsMap = common_vendor.ref({});
function statusToText(s) {
if (s === 1)
return "进行中";
if (s === 0)
return "未开始";
if (s === 2)
return "已结束";
return String(s || "");
}
async function fetchDetail(id) {
const data = await api_appUser.getActivityDetail(id);
detail.value = data || {};
statusText.value = statusToText(detail.value.status);
}
function unwrap(list) {
if (Array.isArray(list))
return list;
const obj = list || {};
const data = obj.data || {};
const arr = obj.list || obj.items || data.list || data.items || data;
return Array.isArray(arr) ? arr : [];
}
function normalizeIssues(list) {
const arr = unwrap(list);
return arr.map((i, idx) => ({
id: i.id ?? String(idx),
title: i.title ?? i.name ?? "",
no: i.no ?? i.index ?? i.issue_no ?? i.issue_number ?? null,
status_text: i.status_text ?? (i.status === 1 ? "进行中" : i.status === 0 ? "未开始" : i.status === 2 ? "已结束" : "")
}));
}
function normalizeRewards(list) {
const arr = unwrap(list);
return arr.map((i, idx) => ({
id: i.id ?? String(idx),
title: i.title ?? i.name ?? "",
image: i.image ?? i.img ?? i.pic ?? i.banner ?? "",
odds: i.odds ?? i.rate ?? i.probability ?? i.prob ?? "",
rarity: i.rarity ?? i.rarity_name ?? ""
}));
}
async function fetchRewardsForIssues(activityId) {
const list = issues.value || [];
const promises = list.map((it) => api_appUser.getActivityIssueRewards(activityId, it.id));
const results = await Promise.allSettled(promises);
results.forEach((res, i) => {
const issueId = list[i] && list[i].id;
if (!issueId)
return;
const value = res.status === "fulfilled" ? normalizeRewards(res.value) : [];
rewardsMap.value = { ...rewardsMap.value || {}, [issueId]: value };
});
}
async function fetchIssues(id) {
const data = await api_appUser.getActivityIssues(id);
issues.value = normalizeIssues(data);
await fetchRewardsForIssues(id);
}
function onPreviewBanner() {
const url = detail.value.banner || "";
if (url)
common_vendor.index.previewImage({ urls: [url], current: url });
}
function onParticipate() {
common_vendor.index.showToast({ title: "功能待接入", icon: "none" });
}
common_vendor.onLoad((opts) => {
const id = opts && opts.id || "";
if (id) {
fetchDetail(id);
fetchIssues(id);
}
});
return (_ctx, _cache) => {
return common_vendor.e({
a: detail.value.banner
}, detail.value.banner ? {
b: detail.value.banner
} : {}, {
c: common_vendor.t(detail.value.name || detail.value.title || "-"),
d: common_vendor.t(detail.value.category_name || "无限赏"),
e: detail.value.price_draw !== void 0
}, detail.value.price_draw !== void 0 ? {
f: common_vendor.t(detail.value.price_draw)
} : {}, {
g: detail.value.status !== void 0
}, detail.value.status !== void 0 ? {
h: common_vendor.t(statusText.value)
} : {}, {
i: common_vendor.o(onPreviewBanner),
j: common_vendor.o(onParticipate),
k: issues.value.length
}, issues.value.length ? {
l: common_vendor.f(issues.value, (it, k0, i0) => {
return common_vendor.e({
a: common_vendor.t(it.title || "第" + (it.no || it.index || it.issue_no || "-") + "期"),
b: it.status_text
}, it.status_text ? {
c: common_vendor.t(it.status_text)
} : {}, {
d: rewardsMap.value[it.id] && rewardsMap.value[it.id].length
}, rewardsMap.value[it.id] && rewardsMap.value[it.id].length ? {
e: common_vendor.f(rewardsMap.value[it.id], (rw, k1, i1) => {
return common_vendor.e({
a: rw.image
}, rw.image ? {
b: rw.image
} : {}, {
c: common_vendor.t(rw.title),
d: rw.rarity || rw.odds
}, rw.rarity || rw.odds ? {
e: common_vendor.t([rw.rarity, rw.odds].filter(Boolean).join(" · "))
} : {}, {
f: rw.id
});
})
} : {}, {
f: it.id
});
})
} : {});
};
}
};
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["__scopeId", "data-v-b5fdbf1f"]]);
wx.createPage(MiniProgramPage);
//# sourceMappingURL=../../../../.sourcemap/mp-weixin/pages/activity/wuxianshang/index.js.map

View File

@ -0,0 +1,4 @@
{
"navigationBarTitleText": "无限赏",
"usingComponents": {}
}

View File

@ -0,0 +1 @@
<scroll-view class="page data-v-b5fdbf1f" scroll-y><view wx:if="{{a}}" class="banner data-v-b5fdbf1f"><image class="banner-img data-v-b5fdbf1f" src="{{b}}" mode="widthFix"/></view><view class="header data-v-b5fdbf1f"><view class="title data-v-b5fdbf1f">{{c}}</view><view class="meta data-v-b5fdbf1f">分类:{{d}}</view><view wx:if="{{e}}" class="meta data-v-b5fdbf1f">单次抽选:¥{{f}}</view><view wx:if="{{g}}" class="meta data-v-b5fdbf1f">状态:{{h}}</view></view><view class="actions data-v-b5fdbf1f"><button class="btn data-v-b5fdbf1f" bindtap="{{i}}">查看图片</button><button class="btn primary data-v-b5fdbf1f" bindtap="{{j}}">立即参与</button></view><view class="issues data-v-b5fdbf1f"><view class="issues-title data-v-b5fdbf1f">期数</view><view wx:if="{{k}}" class="issues-list data-v-b5fdbf1f"><view wx:for="{{l}}" wx:for-item="it" wx:key="f" class="issue-item data-v-b5fdbf1f"><text class="issue-title data-v-b5fdbf1f">{{it.a}}</text><text wx:if="{{it.b}}" class="issue-status data-v-b5fdbf1f">{{it.c}}</text><view wx:if="{{it.d}}" class="rewards data-v-b5fdbf1f"><view wx:for="{{it.e}}" wx:for-item="rw" wx:key="f" class="reward data-v-b5fdbf1f"><image wx:if="{{rw.a}}" class="reward-img data-v-b5fdbf1f" src="{{rw.b}}" mode="aspectFill"/><view class="reward-texts data-v-b5fdbf1f"><text class="reward-title data-v-b5fdbf1f">{{rw.c}}</text><text wx:if="{{rw.d}}" class="reward-meta data-v-b5fdbf1f">{{rw.e}}</text></view></view></view><view wx:else class="rewards-empty data-v-b5fdbf1f">暂无奖励配置</view></view></view><view wx:else class="issues-empty data-v-b5fdbf1f">暂无期数</view></view></scroll-view>

View File

@ -0,0 +1,49 @@
.page.data-v-b5fdbf1f { height: 100vh
}
.banner.data-v-b5fdbf1f { padding: 24rpx
}
.banner-img.data-v-b5fdbf1f { width: 100%
}
.header.data-v-b5fdbf1f { padding: 0 24rpx
}
.title.data-v-b5fdbf1f { font-size: 36rpx; font-weight: 700
}
.meta.data-v-b5fdbf1f { margin-top: 8rpx; font-size: 26rpx; color: #666
}
.actions.data-v-b5fdbf1f { display: flex; padding: 24rpx; gap: 16rpx
}
.btn.data-v-b5fdbf1f { flex: 1
}
.primary.data-v-b5fdbf1f { background-color: #007AFF; color: #fff
}
.issues.data-v-b5fdbf1f { background: #fff; border-radius: 12rpx; margin: 0 24rpx 24rpx; padding: 16rpx
}
.issues-title.data-v-b5fdbf1f { font-size: 30rpx; font-weight: 600; margin-bottom: 12rpx
}
.issues-list.data-v-b5fdbf1f {
}
.issue-item.data-v-b5fdbf1f { display: flex; justify-content: space-between; padding: 12rpx 0; border-bottom: 1rpx solid #f0f0f0
}
.issue-item.data-v-b5fdbf1f:last-child { border-bottom: 0
}
.issue-title.data-v-b5fdbf1f { font-size: 26rpx
}
.issue-status.data-v-b5fdbf1f { font-size: 24rpx; color: #666
}
.rewards.data-v-b5fdbf1f { width: 100%; margin-top: 12rpx
}
.reward.data-v-b5fdbf1f { display: flex; align-items: center; margin-bottom: 8rpx
}
.reward-img.data-v-b5fdbf1f { width: 80rpx; height: 80rpx; border-radius: 8rpx; margin-right: 12rpx; background: #f5f5f5
}
.reward-texts.data-v-b5fdbf1f { display: flex; flex-direction: column
}
.reward-title.data-v-b5fdbf1f { font-size: 26rpx
}
.reward-meta.data-v-b5fdbf1f { font-size: 22rpx; color: #888
}
.rewards-empty.data-v-b5fdbf1f { font-size: 24rpx; color: #999
}
.issues-empty.data-v-b5fdbf1f { font-size: 24rpx; color: #999
}

View File

@ -0,0 +1,137 @@
"use strict";
const common_vendor = require("../../../common/vendor.js");
const api_appUser = require("../../../api/appUser.js");
const _sfc_main = {
__name: "index",
setup(__props) {
const detail = common_vendor.ref({});
const issues = common_vendor.ref([]);
const rewardsMap = common_vendor.ref({});
function statusToText(s) {
if (s === 1)
return "进行中";
if (s === 0)
return "未开始";
if (s === 2)
return "已结束";
return String(s || "");
}
const statusText = common_vendor.ref("");
async function fetchDetail(id) {
const data = await api_appUser.getActivityDetail(id);
detail.value = data || {};
statusText.value = statusToText(detail.value.status);
}
function unwrap(list) {
if (Array.isArray(list))
return list;
const obj = list || {};
const data = obj.data || {};
const arr = obj.list || obj.items || data.list || data.items || data;
return Array.isArray(arr) ? arr : [];
}
function normalizeIssues(list) {
const arr = unwrap(list);
return arr.map((i, idx) => ({
id: i.id ?? String(idx),
title: i.title ?? i.name ?? "",
no: i.no ?? i.index ?? i.issue_no ?? i.issue_number ?? null,
status_text: i.status_text ?? (i.status === 1 ? "进行中" : i.status === 0 ? "未开始" : i.status === 2 ? "已结束" : "")
}));
}
function normalizeRewards(list) {
const arr = unwrap(list);
return arr.map((i, idx) => ({
id: i.id ?? String(idx),
title: i.title ?? i.name ?? "",
image: i.image ?? i.img ?? i.pic ?? i.banner ?? "",
odds: i.odds ?? i.rate ?? i.probability ?? i.prob ?? "",
rarity: i.rarity ?? i.rarity_name ?? ""
}));
}
async function fetchRewardsForIssues(activityId) {
const list = issues.value || [];
const promises = list.map((it) => api_appUser.getActivityIssueRewards(activityId, it.id));
const results = await Promise.allSettled(promises);
results.forEach((res, i) => {
const issueId = list[i] && list[i].id;
if (!issueId)
return;
const value = res.status === "fulfilled" ? normalizeRewards(res.value) : [];
rewardsMap.value = { ...rewardsMap.value || {}, [issueId]: value };
});
}
async function fetchIssues(id) {
const data = await api_appUser.getActivityIssues(id);
issues.value = normalizeIssues(data);
await fetchRewardsForIssues(id);
}
function onPreviewBanner() {
const url = detail.value.banner || "";
if (url)
common_vendor.index.previewImage({ urls: [url], current: url });
}
function onParticipate() {
common_vendor.index.showToast({ title: "功能待接入", icon: "none" });
}
common_vendor.onLoad((opts) => {
const id = opts && opts.id || "";
if (id) {
fetchDetail(id);
fetchIssues(id);
}
});
return (_ctx, _cache) => {
return common_vendor.e({
a: detail.value.banner
}, detail.value.banner ? {
b: detail.value.banner
} : {}, {
c: common_vendor.t(detail.value.name || detail.value.title || "-"),
d: common_vendor.t(detail.value.category_name || "一番赏"),
e: detail.value.price_draw !== void 0
}, detail.value.price_draw !== void 0 ? {
f: common_vendor.t(detail.value.price_draw)
} : {}, {
g: detail.value.status !== void 0
}, detail.value.status !== void 0 ? {
h: common_vendor.t(statusText.value)
} : {}, {
i: common_vendor.o(onPreviewBanner),
j: common_vendor.o(onParticipate),
k: issues.value.length
}, issues.value.length ? {
l: common_vendor.f(issues.value, (it, k0, i0) => {
return common_vendor.e({
a: common_vendor.t(it.title || "第" + (it.no || it.index || it.issue_no || "-") + "期"),
b: it.status_text
}, it.status_text ? {
c: common_vendor.t(it.status_text)
} : {}, {
d: rewardsMap.value[it.id] && rewardsMap.value[it.id].length
}, rewardsMap.value[it.id] && rewardsMap.value[it.id].length ? {
e: common_vendor.f(rewardsMap.value[it.id], (rw, k1, i1) => {
return common_vendor.e({
a: rw.image
}, rw.image ? {
b: rw.image
} : {}, {
c: common_vendor.t(rw.title),
d: rw.rarity || rw.odds
}, rw.rarity || rw.odds ? {
e: common_vendor.t([rw.rarity, rw.odds].filter(Boolean).join(" · "))
} : {}, {
f: rw.id
});
})
} : {}, {
f: it.id
});
})
} : {});
};
}
};
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["__scopeId", "data-v-3c32883e"]]);
wx.createPage(MiniProgramPage);
//# sourceMappingURL=../../../../.sourcemap/mp-weixin/pages/activity/yifanshang/index.js.map

View File

@ -0,0 +1,4 @@
{
"navigationBarTitleText": "一番赏",
"usingComponents": {}
}

View File

@ -0,0 +1 @@
<scroll-view class="page data-v-3c32883e" scroll-y><view wx:if="{{a}}" class="banner data-v-3c32883e"><image class="banner-img data-v-3c32883e" src="{{b}}" mode="widthFix"/></view><view class="header data-v-3c32883e"><view class="title data-v-3c32883e">{{c}}</view><view class="meta data-v-3c32883e">分类:{{d}}</view><view wx:if="{{e}}" class="meta data-v-3c32883e">抽选价:¥{{f}}</view><view wx:if="{{g}}" class="meta data-v-3c32883e">状态:{{h}}</view></view><view class="actions data-v-3c32883e"><button class="btn data-v-3c32883e" bindtap="{{i}}">查看图片</button><button class="btn primary data-v-3c32883e" bindtap="{{j}}">立即参与</button></view><view class="issues data-v-3c32883e"><view class="issues-title data-v-3c32883e">期数</view><view wx:if="{{k}}" class="issues-list data-v-3c32883e"><view wx:for="{{l}}" wx:for-item="it" wx:key="f" class="issue-item data-v-3c32883e"><text class="issue-title data-v-3c32883e">{{it.a}}</text><text wx:if="{{it.b}}" class="issue-status data-v-3c32883e">{{it.c}}</text><view wx:if="{{it.d}}" class="rewards data-v-3c32883e"><view wx:for="{{it.e}}" wx:for-item="rw" wx:key="f" class="reward data-v-3c32883e"><image wx:if="{{rw.a}}" class="reward-img data-v-3c32883e" src="{{rw.b}}" mode="aspectFill"/><view class="reward-texts data-v-3c32883e"><text class="reward-title data-v-3c32883e">{{rw.c}}</text><text wx:if="{{rw.d}}" class="reward-meta data-v-3c32883e">{{rw.e}}</text></view></view></view><view wx:else class="rewards-empty data-v-3c32883e">暂无奖励配置</view></view></view><view wx:else class="issues-empty data-v-3c32883e">暂无期数</view></view></scroll-view>

View File

@ -0,0 +1,49 @@
.page.data-v-3c32883e { height: 100vh
}
.banner.data-v-3c32883e { padding: 24rpx
}
.banner-img.data-v-3c32883e { width: 100%
}
.header.data-v-3c32883e { padding: 0 24rpx
}
.title.data-v-3c32883e { font-size: 36rpx; font-weight: 700
}
.meta.data-v-3c32883e { margin-top: 8rpx; font-size: 26rpx; color: #666
}
.actions.data-v-3c32883e { display: flex; padding: 24rpx; gap: 16rpx
}
.btn.data-v-3c32883e { flex: 1
}
.primary.data-v-3c32883e { background-color: #007AFF; color: #fff
}
.issues.data-v-3c32883e { background: #fff; border-radius: 12rpx; margin: 0 24rpx 24rpx; padding: 16rpx
}
.issues-title.data-v-3c32883e { font-size: 30rpx; font-weight: 600; margin-bottom: 12rpx
}
.issues-list.data-v-3c32883e {
}
.issue-item.data-v-3c32883e { display: flex; justify-content: space-between; padding: 12rpx 0; border-bottom: 1rpx solid #f0f0f0
}
.issue-item.data-v-3c32883e:last-child { border-bottom: 0
}
.issue-title.data-v-3c32883e { font-size: 26rpx
}
.issue-status.data-v-3c32883e { font-size: 24rpx; color: #666
}
.rewards.data-v-3c32883e { width: 100%; margin-top: 12rpx
}
.reward.data-v-3c32883e { display: flex; align-items: center; margin-bottom: 8rpx
}
.reward-img.data-v-3c32883e { width: 80rpx; height: 80rpx; border-radius: 8rpx; margin-right: 12rpx; background: #f5f5f5
}
.reward-texts.data-v-3c32883e { display: flex; flex-direction: column
}
.reward-title.data-v-3c32883e { font-size: 26rpx
}
.reward-meta.data-v-3c32883e { font-size: 22rpx; color: #888
}
.rewards-empty.data-v-3c32883e { font-size: 24rpx; color: #999
}
.issues-empty.data-v-3c32883e { font-size: 24rpx; color: #999
}

View File

@ -0,0 +1,144 @@
"use strict";
const common_vendor = require("../../common/vendor.js");
const api_appUser = require("../../api/appUser.js");
const _sfc_main = {
__name: "edit",
setup(__props) {
const id = common_vendor.ref("");
const name = common_vendor.ref("");
const mobile = common_vendor.ref("");
const province = common_vendor.ref("");
const city = common_vendor.ref("");
const district = common_vendor.ref("");
const detail = common_vendor.ref("");
let isDefault = false;
const loading = common_vendor.ref(false);
const error = common_vendor.ref("");
function fill(data) {
name.value = data.name || data.realname || "";
mobile.value = data.mobile || data.phone || "";
province.value = data.province || "";
city.value = data.city || "";
district.value = data.district || "";
detail.value = data.address || data.detail || "";
isDefault = !!data.is_default;
}
async function init(idParam) {
if (!idParam) {
const data = common_vendor.index.getStorageSync("edit_address") || {};
if (data && data.id)
fill(data);
return;
}
const user_id = common_vendor.index.getStorageSync("user_id");
try {
const list = await api_appUser.listAddresses(user_id);
const arr = Array.isArray(list) ? list : list && (list.list || list.items) || [];
const found = arr.find((a) => String(a.id) === String(idParam));
if (found)
fill(found);
} catch (e) {
}
}
async function onSubmit() {
const user_id = common_vendor.index.getStorageSync("user_id");
if (!name.value || !mobile.value || !province.value || !city.value || !district.value || !detail.value) {
common_vendor.index.showToast({ title: "请完善必填信息", icon: "none" });
return;
}
loading.value = true;
error.value = "";
const payload = {
name: name.value,
mobile: mobile.value,
province: province.value,
city: city.value,
district: district.value,
address: detail.value,
is_default: isDefault
};
try {
let savedId = id.value;
if (id.value) {
await api_appUser.updateAddress(user_id, id.value, payload);
savedId = id.value;
} else {
try {
const res = await api_appUser.addAddress(user_id, payload);
savedId = res && (res.id || res.address_id) || "";
} catch (eAdd) {
const sc = eAdd && eAdd.statusCode;
const bc = eAdd && eAdd.data && (eAdd.data.code || eAdd.code) || void 0;
const msg = eAdd && (eAdd.message || eAdd.errMsg || "");
const isUniqueErr = sc === 400 && (bc === 10011 || msg && msg.toLowerCase().includes("unique"));
if (isUniqueErr) {
try {
const list = await api_appUser.listAddresses(user_id);
const arr = Array.isArray(list) ? list : list && (list.list || list.items) || [];
const found = arr.find((a) => (a.mobile === mobile.value || a.phone === mobile.value) && (a.address === detail.value || a.detail === detail.value) && a.city === city.value && a.district === district.value && a.province === province.value);
if (found) {
savedId = found.id;
await api_appUser.updateAddress(user_id, savedId, payload);
}
} catch (_) {
}
} else {
throw eAdd;
}
}
}
if (isDefault) {
if (!savedId) {
try {
const list = await api_appUser.listAddresses(user_id);
const arr = Array.isArray(list) ? list : list && (list.list || list.items) || [];
const found = arr.find((a) => (a.mobile === mobile.value || a.phone === mobile.value) && (a.address === detail.value || a.detail === detail.value));
if (found)
savedId = found.id;
} catch (_) {
}
}
if (savedId) {
await api_appUser.setDefaultAddress(user_id, savedId);
}
}
common_vendor.index.showToast({ title: "保存成功", icon: "success" });
common_vendor.index.navigateBack();
} catch (e) {
error.value = e && (e.message || e.errMsg) || "保存失败";
} finally {
loading.value = false;
}
}
common_vendor.onLoad((opts) => {
id.value = opts && opts.id || "";
init(id.value);
});
return (_ctx, _cache) => {
return common_vendor.e({
a: name.value,
b: common_vendor.o(($event) => name.value = $event.detail.value),
c: mobile.value,
d: common_vendor.o(($event) => mobile.value = $event.detail.value),
e: province.value,
f: common_vendor.o(($event) => province.value = $event.detail.value),
g: city.value,
h: common_vendor.o(($event) => city.value = $event.detail.value),
i: district.value,
j: common_vendor.o(($event) => district.value = $event.detail.value),
k: detail.value,
l: common_vendor.o(($event) => detail.value = $event.detail.value),
m: common_vendor.unref(isDefault),
n: common_vendor.o((e) => common_vendor.isRef(isDefault) ? isDefault.value = e.detail.value : isDefault = e.detail.value),
o: loading.value,
p: common_vendor.o(onSubmit),
q: error.value
}, error.value ? {
r: common_vendor.t(error.value)
} : {});
};
}
};
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["__scopeId", "data-v-dcb1f0d8"]]);
wx.createPage(MiniProgramPage);
//# sourceMappingURL=../../../.sourcemap/mp-weixin/pages/address/edit.js.map

View File

@ -0,0 +1,4 @@
{
"navigationBarTitleText": "编辑地址",
"usingComponents": {}
}

View File

@ -0,0 +1 @@
<view class="wrap data-v-dcb1f0d8"><view class="form-item data-v-dcb1f0d8"><text class="label data-v-dcb1f0d8">姓名</text><input class="input data-v-dcb1f0d8" placeholder="请输入姓名" value="{{a}}" bindinput="{{b}}"/></view><view class="form-item data-v-dcb1f0d8"><text class="label data-v-dcb1f0d8">手机号</text><input class="input data-v-dcb1f0d8" placeholder="请输入手机号" value="{{c}}" bindinput="{{d}}"/></view><view class="form-item data-v-dcb1f0d8"><text class="label data-v-dcb1f0d8">省份</text><input class="input data-v-dcb1f0d8" placeholder="请输入省份" value="{{e}}" bindinput="{{f}}"/></view><view class="form-item data-v-dcb1f0d8"><text class="label data-v-dcb1f0d8">城市</text><input class="input data-v-dcb1f0d8" placeholder="请输入城市" value="{{g}}" bindinput="{{h}}"/></view><view class="form-item data-v-dcb1f0d8"><text class="label data-v-dcb1f0d8">区县</text><input class="input data-v-dcb1f0d8" placeholder="请输入区县" value="{{i}}" bindinput="{{j}}"/></view><view class="form-item data-v-dcb1f0d8"><text class="label data-v-dcb1f0d8">详细地址</text><input class="input data-v-dcb1f0d8" placeholder="请输入详细地址" value="{{k}}" bindinput="{{l}}"/></view><view class="form-item data-v-dcb1f0d8"><text class="label data-v-dcb1f0d8">设为默认</text><switch class="data-v-dcb1f0d8" checked="{{m}}" bindchange="{{n}}"/></view><button class="submit data-v-dcb1f0d8" disabled="{{o}}" bindtap="{{p}}">保存</button><view wx:if="{{q}}" class="error data-v-dcb1f0d8">{{r}}</view></view>

View File

@ -0,0 +1,13 @@
.wrap.data-v-dcb1f0d8 { padding: 24rpx
}
.form-item.data-v-dcb1f0d8 { display: flex; align-items: center; background: #fff; border-radius: 12rpx; padding: 16rpx; margin-bottom: 12rpx
}
.label.data-v-dcb1f0d8 { width: 160rpx; font-size: 28rpx; color: #666
}
.input.data-v-dcb1f0d8 { flex: 1; font-size: 28rpx
}
.submit.data-v-dcb1f0d8 { width: 100%; margin-top: 20rpx
}
.error.data-v-dcb1f0d8 { color: #e43; margin-top: 12rpx
}

View File

@ -0,0 +1,107 @@
"use strict";
const common_vendor = require("../../common/vendor.js");
const api_appUser = require("../../api/appUser.js");
const _sfc_main = {
__name: "index",
setup(__props) {
const list = common_vendor.ref([]);
const loading = common_vendor.ref(false);
const error = common_vendor.ref("");
async function fetchList() {
const user_id = common_vendor.index.getStorageSync("user_id");
const token = common_vendor.index.getStorageSync("token");
const phoneBound = !!common_vendor.index.getStorageSync("phone_bound");
if (!user_id || !token || !phoneBound) {
common_vendor.index.showModal({
title: "提示",
content: "请先登录并绑定手机号",
confirmText: "去登录",
success: (res) => {
if (res.confirm) {
common_vendor.index.navigateTo({ url: "/pages/login/index" });
}
}
});
return;
}
loading.value = true;
error.value = "";
try {
const data = await api_appUser.listAddresses(user_id);
list.value = Array.isArray(data) ? data : data && (data.list || data.items) || [];
} catch (e) {
error.value = e && (e.message || e.errMsg) || "获取地址失败";
} finally {
loading.value = false;
}
}
function toAdd() {
common_vendor.index.removeStorageSync("edit_address");
common_vendor.index.navigateTo({ url: "/pages/address/edit" });
}
function toEdit(item) {
common_vendor.index.setStorageSync("edit_address", item);
common_vendor.index.navigateTo({ url: `/pages/address/edit?id=${item.id}` });
}
function onDelete(item) {
const user_id = common_vendor.index.getStorageSync("user_id");
common_vendor.index.showModal({
title: "确认删除",
content: "确定删除该地址吗?",
success: async (res) => {
if (res.confirm) {
try {
await api_appUser.deleteAddress(user_id, item.id);
fetchList();
} catch (e) {
common_vendor.index.showToast({ title: "删除失败", icon: "none" });
}
}
}
});
}
async function onSetDefault(item) {
try {
const user_id = common_vendor.index.getStorageSync("user_id");
await api_appUser.setDefaultAddress(user_id, item.id);
fetchList();
} catch (e) {
common_vendor.index.showToast({ title: "设置失败", icon: "none" });
}
}
common_vendor.onLoad(() => {
fetchList();
});
return (_ctx, _cache) => {
return common_vendor.e({
a: common_vendor.o(toAdd),
b: error.value
}, error.value ? {
c: common_vendor.t(error.value)
} : {}, {
d: list.value.length === 0 && !loading.value
}, list.value.length === 0 && !loading.value ? {} : {}, {
e: common_vendor.f(list.value, (item, k0, i0) => {
return common_vendor.e({
a: common_vendor.t(item.name || item.realname),
b: common_vendor.t(item.phone || item.mobile),
c: item.is_default
}, item.is_default ? {} : {}, {
d: common_vendor.t(item.province),
e: common_vendor.t(item.city),
f: common_vendor.t(item.district),
g: common_vendor.t(item.address || item.detail),
h: common_vendor.o(($event) => toEdit(item), item.id),
i: common_vendor.o(($event) => onDelete(item), item.id),
j: item.is_default,
k: common_vendor.o(($event) => onSetDefault(item), item.id),
l: item.id
});
})
});
};
}
};
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["__scopeId", "data-v-c47feaaa"]]);
wx.createPage(MiniProgramPage);
//# sourceMappingURL=../../../.sourcemap/mp-weixin/pages/address/index.js.map

View File

@ -0,0 +1,4 @@
{
"navigationBarTitleText": "地址管理",
"usingComponents": {}
}

View File

@ -0,0 +1 @@
<view class="wrap data-v-c47feaaa"><view class="header data-v-c47feaaa"><button class="add data-v-c47feaaa" bindtap="{{a}}">新增地址</button></view><view wx:if="{{b}}" class="error data-v-c47feaaa">{{c}}</view><view wx:if="{{d}}" class="empty data-v-c47feaaa">暂无地址</view><view wx:for="{{e}}" wx:for-item="item" wx:key="l" class="addr data-v-c47feaaa"><view class="addr-main data-v-c47feaaa"><view class="addr-row data-v-c47feaaa"><text class="name data-v-c47feaaa">姓名:{{item.a}}</text></view><view class="addr-row data-v-c47feaaa"><text class="phone data-v-c47feaaa">手机号:{{item.b}}</text></view><view wx:if="{{item.c}}" class="addr-row data-v-c47feaaa"><text class="default data-v-c47feaaa">默认</text></view><view class="addr-row data-v-c47feaaa"><text class="region data-v-c47feaaa">省市区:{{item.d}}{{item.e}}{{item.f}}</text></view><view class="addr-row data-v-c47feaaa"><text class="detail data-v-c47feaaa">详细地址:{{item.g}}</text></view></view><view class="addr-actions data-v-c47feaaa"><button class="data-v-c47feaaa" size="mini" bindtap="{{item.h}}">编辑</button><button class="data-v-c47feaaa" size="mini" type="warn" bindtap="{{item.i}}">删除</button><button class="data-v-c47feaaa" size="mini" disabled="{{item.j}}" bindtap="{{item.k}}">设为默认</button></view></view></view>

View File

@ -0,0 +1,27 @@
.wrap.data-v-c47feaaa { padding: 24rpx
}
.header.data-v-c47feaaa { display: flex; justify-content: flex-end; margin-bottom: 12rpx
}
.add.data-v-c47feaaa { font-size: 28rpx
}
.addr.data-v-c47feaaa { background: #fff; border-radius: 12rpx; padding: 20rpx; margin-bottom: 16rpx; box-shadow: 0 2rpx 8rpx rgba(0,0,0,0.04)
}
.addr-row.data-v-c47feaaa { display: flex; align-items: center; margin-bottom: 8rpx
}
.name.data-v-c47feaaa { font-size: 30rpx; margin-right: 12rpx
}
.phone.data-v-c47feaaa { font-size: 28rpx; color: #666
}
.default.data-v-c47feaaa { font-size: 24rpx; color: #007AFF; margin-left: 10rpx
}
.region.data-v-c47feaaa { font-size: 26rpx; color: #666
}
.detail.data-v-c47feaaa { font-size: 26rpx; color: #333
}
.addr-actions.data-v-c47feaaa { display: flex; justify-content: flex-end; gap: 12rpx; margin-top: 12rpx
}
.empty.data-v-c47feaaa { text-align: center; color: #999; margin-top: 40rpx
}
.error.data-v-c47feaaa { color: #e43; margin-bottom: 12rpx
}

View File

@ -0,0 +1,9 @@
"use strict";
const common_vendor = require("../../common/vendor.js");
const _sfc_main = {};
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return {};
}
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render], ["__scopeId", "data-v-5512f5e4"]]);
wx.createPage(MiniProgramPage);
//# sourceMappingURL=../../../.sourcemap/mp-weixin/pages/agreement/purchase.js.map

View File

@ -0,0 +1,4 @@
{
"navigationBarTitleText": "购买协议",
"usingComponents": {}
}

View File

@ -0,0 +1 @@
<scroll-view class="page data-v-5512f5e4" scroll-y><view class="h1 data-v-5512f5e4">购买协议</view><view class="meta data-v-5512f5e4">生效日期2025年11月18日</view><view class="meta data-v-5512f5e4">运营方:【公司全称】</view><view class="p data-v-5512f5e4">本《购买协议》适用于您在【您的小程序名称】(以下简称“本平台”)购买盲盒商品的行为。当您点击“立即购买”并完成支付时,即视为您已阅读、理解并同意本协议全部内容。</view><view class="h2 data-v-5512f5e4">一、商品说明</view><view class="ol data-v-5512f5e4"><view class="li data-v-5512f5e4">盲盒特性:本平台所售盲盒为系列化商品,包装外观一致,内部款式随机,具体款式无法提前指定或预知。</view><view class="li data-v-5512f5e4">概率公示:各款式(含隐藏款、特殊款)的抽取概率已在商品详情页明确公示,确保透明公正。</view><view class="li data-v-5512f5e4">商品展示:页面图片、描述仅供参考,实际商品以实物为准。</view></view><view class="h2 data-v-5512f5e4">二、购买规则</view><view class="ol data-v-5512f5e4"><view class="li data-v-5512f5e4">购买资格:仅限已注册并通过身份验证的用户购买。</view><view class="li data-v-5512f5e4">支付方式:支持微信支付等平台认可的支付方式,支付成功即视为订单成立。</view></view><view class="h2 data-v-5512f5e4">三、发货与物流</view><view class="ol data-v-5512f5e4"><view class="li data-v-5512f5e4">发货时效发货订单提交成功后本平台将在3-15个工作日内安排发货预售商品按页面说明执行。</view><view class="li data-v-5512f5e4">物流信息:您可在“我的订单”中查看物流状态。因地址错误、联系不畅导致的配送失败,责任由您承担。</view></view><view class="h2 data-v-5512f5e4">四、售后服务</view><view class="ol data-v-5512f5e4"><view class="li data-v-5512f5e4">质量问题如商品破损、漏发、错发、非盲盒系列商品请在签收后2到4小时内联系客服并提供凭证如开箱视频、照片经核实后平台将为您补发、换货或退款。</view><view class="li data-v-5512f5e4">非质量问题(如抽中重复款式、不喜欢款式、未抽中隐藏款等):不支持无理由退换货。</view><view class="li data-v-5512f5e4">拆封后商品:出于卫生与二次销售考虑,已拆封盲盒恕不退换(质量问题除外)。</view></view><view class="h2 data-v-5512f5e4">五、价格与促销</view><view class="ol data-v-5512f5e4"><view class="li data-v-5512f5e4">商品价格以页面实时显示为准,平台有权根据市场情况调整价格。</view><view class="li data-v-5512f5e4">优惠券、折扣活动需遵守对应规则,不可叠加或兑现。</view></view><view class="h2 data-v-5512f5e4">六、特别提示</view><view class="ol data-v-5512f5e4"><view class="li data-v-5512f5e4">盲盒不具备投资属性,本平台不承诺保值、升值或回购。</view><view class="li data-v-5512f5e4">隐藏款抽取属小概率事件,请勿抱有赌博心态,理性消费。</view></view><view class="h2 data-v-5512f5e4">七、免责情形</view><view class="ul data-v-5512f5e4"><view class="li data-v-5512f5e4">因不可抗力导致无法发货或延迟;</view><view class="li data-v-5512f5e4">用户提供错误收货信息;</view><view class="li data-v-5512f5e4">因第三方物流原因造成的商品损毁或丢失(平台将协助索赔)。</view></view><view class="h2 data-v-5512f5e4">八、协议效力</view><view class="p data-v-5512f5e4">本购买协议为《用户协议》的补充,两者冲突时,以本协议中关于交易的条款为准。未尽事宜,依照《消费者权益保护法》《电子商务法》等法律法规执行。</view><view class="h2 data-v-5512f5e4">九、联系我们</view><view class="p data-v-5512f5e4">售后专线service@yourdomain.com</view><view class="p data-v-5512f5e4">工作时间:工作日 9:0018:00</view><view class="p data-v-5512f5e4">运营主体:【公司全称】</view><view class="p data-v-5512f5e4">统一社会信用代码【XXXXXXXXXXXXXX】</view><view class="tip data-v-5512f5e4">理性消费提醒:盲盒是一种娱乐消费形式,请根据自身经济能力合理购买,切勿沉迷或过度投入。</view></scroll-view>

View File

@ -0,0 +1,17 @@
.page.data-v-5512f5e4 { height: 100vh
}
.h1.data-v-5512f5e4 { font-size: 36rpx; font-weight: 700; padding: 24rpx
}
.meta.data-v-5512f5e4 { padding: 0 24rpx; font-size: 24rpx; color: #666
}
.h2.data-v-5512f5e4 { font-size: 30rpx; font-weight: 600; padding: 20rpx 24rpx 8rpx
}
.p.data-v-5512f5e4 { padding: 0 24rpx; font-size: 26rpx; line-height: 1.6
}
.ul.data-v-5512f5e4, .ol.data-v-5512f5e4 { padding: 0 24rpx
}
.ul .li.data-v-5512f5e4, .ol .li.data-v-5512f5e4 { font-size: 26rpx; line-height: 1.6; margin: 8rpx 0
}
.tip.data-v-5512f5e4 { padding: 16rpx 24rpx; font-size: 24rpx; color: #999
}

View File

@ -0,0 +1,9 @@
"use strict";
const common_vendor = require("../../common/vendor.js");
const _sfc_main = {};
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return {};
}
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render], ["__scopeId", "data-v-ff1e87b4"]]);
wx.createPage(MiniProgramPage);
//# sourceMappingURL=../../../.sourcemap/mp-weixin/pages/agreement/user.js.map

View File

@ -0,0 +1,4 @@
{
"navigationBarTitleText": "用户协议",
"usingComponents": {}
}

View File

@ -0,0 +1 @@
<scroll-view class="page data-v-ff1e87b4" scroll-y><view class="h1 data-v-ff1e87b4">用户协议</view><view class="meta data-v-ff1e87b4">生效日期2025年11月18日</view><view class="meta data-v-ff1e87b4">运营方:【公司全称】</view><view class="p data-v-ff1e87b4">欢迎您使用【您的小程序名称】(以下简称“本平台”)提供的服务。请您在注册、登录或使用本平台前,认真阅读并充分理解本《用户协议》(以下简称“本协议”)。一旦您完成注册、登录或以任何方式使用本平台服务,即视为您已完全接受本协议全部条款。如您不同意,请勿使用本平台。</view><view class="h2 data-v-ff1e87b4">一、协议范围</view><view class="p data-v-ff1e87b4">本协议规范您作为用户在本平台注册、浏览、互动、参与活动等行为,是您与本平台之间的基本权利义务约定。</view><view class="h2 data-v-ff1e87b4">二、用户资格</view><view class="ol data-v-ff1e87b4"><view class="li data-v-ff1e87b4">您须为年满18周岁的自然人具备完全民事行为能力。</view><view class="li data-v-ff1e87b4">未成年人如需使用本平台,须在法定监护人知情并同意下进行,监护人应承担相应责任。</view></view><view class="h2 data-v-ff1e87b4">三、账号与安全</view><view class="ol data-v-ff1e87b4"><view class="li data-v-ff1e87b4">您应提供真实、准确、完整的注册信息,并对账号下的一切行为负责。</view><view class="li data-v-ff1e87b4">请妥善保管账号密码,因泄露、出借或被盗用造成的损失由您自行承担。</view></view><view class="h2 data-v-ff1e87b4">四、用户行为规范</view><view class="p data-v-ff1e87b4">您承诺不从事以下行为:</view><view class="ul data-v-ff1e87b4"><view class="li data-v-ff1e87b4">发布违法、侵权、色情、暴力或虚假信息;</view><view class="li data-v-ff1e87b4">利用技术手段干扰系统正常运行(如刷量、外挂、爬虫);</view><view class="li data-v-ff1e87b4">恶意投诉、敲诈勒索或损害平台商誉;</view><view class="li data-v-ff1e87b4">转售账号或用于商业牟利(除非平台明确授权)。</view></view><view class="h2 data-v-ff1e87b4">五、隐私保护</view><view class="p data-v-ff1e87b4">我们尊重并保护您的个人信息。关于数据收集、使用及保护的具体规则,请参见《隐私政策》(链接)。未经您同意,我们不会向第三方共享您的个人信息,法律法规另有规定除外。</view><view class="h2 data-v-ff1e87b4">六、知识产权</view><view class="ol data-v-ff1e87b4"><view class="li data-v-ff1e87b4">本平台所有内容包括但不限于界面设计、文字、图片、盲盒形象、LOGO的知识产权归本平台或其许可方所有。</view><view class="li data-v-ff1e87b4">未经书面授权,您不得复制、传播、修改或用于商业用途。</view></view><view class="h2 data-v-ff1e87b4">七、免责条款</view><view class="ol data-v-ff1e87b4"><view class="li data-v-ff1e87b4">因不可抗力(如地震、网络攻击、政府行为)导致服务中断,本平台不承担责任。</view><view class="li data-v-ff1e87b4">您因违反本协议造成自身或第三方损失的,由您自行承担。</view></view><view class="h2 data-v-ff1e87b4">八、协议变更与终止</view><view class="ol data-v-ff1e87b4"><view class="li data-v-ff1e87b4">本平台有权根据业务或法律变化修订本协议,修订后将通过公告或推送通知您。继续使用即视为接受新条款。</view><view class="li data-v-ff1e87b4">您可随时注销账号终止使用;平台有权对违规账号采取限制或封禁措施。</view></view><view class="h2 data-v-ff1e87b4">九、法律适用与争议解决</view><view class="p data-v-ff1e87b4">本协议适用中华人民共和国法律。因本协议引起的争议,双方应协商解决;协商不成的,提交本平台运营方所在地有管辖权的人民法院诉讼解决。</view><view class="h2 data-v-ff1e87b4">十、联系我们</view><view class="p data-v-ff1e87b4">客服邮箱service@yourdomain.com</view><view class="p data-v-ff1e87b4">客服电话400-XXX-XXXX工作日 9:0018:00</view><view class="p data-v-ff1e87b4">运营主体:【公司全称】</view><view class="p data-v-ff1e87b4">地址:【公司注册地址】</view><view class="tip data-v-ff1e87b4">温馨提示:盲盒具有随机性和娱乐性,请理性参与,避免沉迷。未成年人禁止参与购买。</view></scroll-view>

View File

@ -0,0 +1,17 @@
.page.data-v-ff1e87b4 { height: 100vh
}
.h1.data-v-ff1e87b4 { font-size: 36rpx; font-weight: 700; padding: 24rpx
}
.meta.data-v-ff1e87b4 { padding: 0 24rpx; font-size: 24rpx; color: #666
}
.h2.data-v-ff1e87b4 { font-size: 30rpx; font-weight: 600; padding: 20rpx 24rpx 8rpx
}
.p.data-v-ff1e87b4 { padding: 0 24rpx; font-size: 26rpx; line-height: 1.6
}
.ul.data-v-ff1e87b4, .ol.data-v-ff1e87b4 { padding: 0 24rpx
}
.ul .li.data-v-ff1e87b4, .ol .li.data-v-ff1e87b4 { font-size: 26rpx; line-height: 1.6; margin: 8rpx 0
}
.tip.data-v-ff1e87b4 { padding: 16rpx 24rpx; font-size: 24rpx; color: #999
}

View File

@ -0,0 +1,30 @@
"use strict";
const common_vendor = require("../../common/vendor.js");
const _sfc_main = {
__name: "index",
setup(__props) {
common_vendor.onShow(() => {
const token = common_vendor.index.getStorageSync("token");
const phoneBound = !!common_vendor.index.getStorageSync("phone_bound");
common_vendor.index.__f__("log", "at pages/cabinet/index.vue:10", "cabinet onShow token:", token, "isLogin:", !!token, "phoneBound:", phoneBound);
if (!token || !phoneBound) {
common_vendor.index.showModal({
title: "提示",
content: "请先登录并绑定手机号",
confirmText: "去登录",
success: (res) => {
if (res.confirm) {
common_vendor.index.navigateTo({ url: "/pages/login/index" });
}
}
});
}
});
return (_ctx, _cache) => {
return {};
};
}
};
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["__scopeId", "data-v-814d09c7"]]);
wx.createPage(MiniProgramPage);
//# sourceMappingURL=../../../.sourcemap/mp-weixin/pages/cabinet/index.js.map

View File

@ -0,0 +1,4 @@
{
"navigationBarTitleText": "货柜",
"usingComponents": {}
}

View File

@ -0,0 +1 @@
<view class="wrap data-v-814d09c7">货柜</view>

View File

@ -0,0 +1,3 @@
.wrap.data-v-814d09c7 { padding: 40rpx
}

View File

@ -0,0 +1,21 @@
"use strict";
const common_vendor = require("../../common/vendor.js");
const _sfc_main = {
methods: {
toUser() {
common_vendor.index.navigateTo({ url: "/pages/agreement/user" });
},
toPurchase() {
common_vendor.index.navigateTo({ url: "/pages/agreement/purchase" });
}
}
};
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return {
a: common_vendor.o((...args) => $options.toUser && $options.toUser(...args)),
b: common_vendor.o((...args) => $options.toPurchase && $options.toPurchase(...args))
};
}
const MiniProgramPage = /* @__PURE__ */ common_vendor._export_sfc(_sfc_main, [["render", _sfc_render], ["__scopeId", "data-v-e6af2099"]]);
wx.createPage(MiniProgramPage);
//# sourceMappingURL=../../../.sourcemap/mp-weixin/pages/help/index.js.map

View File

@ -0,0 +1,4 @@
{
"navigationBarTitleText": "使用帮助",
"usingComponents": {}
}

View File

@ -0,0 +1 @@
<view class="wrap data-v-e6af2099"><view class="card data-v-e6af2099" bindtap="{{a}}"><view class="title data-v-e6af2099">用户协议</view><view class="desc data-v-e6af2099">查看平台使用条款与隐私说明</view></view><view class="card data-v-e6af2099" bindtap="{{b}}"><view class="title data-v-e6af2099">购买协议</view><view class="desc data-v-e6af2099">查看盲盒购买规则与售后政策</view></view></view>

Some files were not shown because too many files have changed in this diff Show More