chore: 初始化项目 — uni-app + Vue3 + TypeScript 脚手架
This commit is contained in:
+22
@@ -0,0 +1,22 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
.DS_Store
|
||||||
|
dist
|
||||||
|
*.local
|
||||||
|
.superpowers/
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
// app.js
|
||||||
|
App({
|
||||||
|
onLaunch() {
|
||||||
|
// 展示本地存储能力
|
||||||
|
const logs = wx.getStorageSync('logs') || []
|
||||||
|
logs.unshift(Date.now())
|
||||||
|
wx.setStorageSync('logs', logs)
|
||||||
|
|
||||||
|
// 登录
|
||||||
|
wx.login({
|
||||||
|
success: res => {
|
||||||
|
// 发送 res.code 到后台换取 openId, sessionKey, unionId
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
globalData: {
|
||||||
|
userInfo: null
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"pages": [
|
||||||
|
"pages/index/index",
|
||||||
|
"pages/logs/logs"
|
||||||
|
],
|
||||||
|
"window": {
|
||||||
|
"navigationBarTextStyle": "black",
|
||||||
|
"navigationBarTitleText": "Weixin",
|
||||||
|
"navigationBarBackgroundColor": "#ffffff"
|
||||||
|
},
|
||||||
|
"style": "v2",
|
||||||
|
"componentFramework": "glass-easel",
|
||||||
|
"sitemapLocation": "sitemap.json",
|
||||||
|
"lazyCodeLoading": "requiredComponents"
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
/**app.wxss**/
|
||||||
|
.container {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 200rpx 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,337 @@
|
|||||||
|
# 湖南12301文旅综合平台 — 第一迭代设计文档
|
||||||
|
|
||||||
|
**文档版本**:v1.0
|
||||||
|
**创建日期**:2026-06-13
|
||||||
|
**实现方案**:方案 A — 由底向上
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、迭代范围
|
||||||
|
|
||||||
|
第一迭代聚焦 **基础层搭建 + 用户端核心流程**,具体包含:
|
||||||
|
|
||||||
|
| 阶段 | 内容 |
|
||||||
|
|------|------|
|
||||||
|
| **Phase 1:基础层** | 分包目录、路由配置、API 层改造、Store 设计、环境配置、共享组件 |
|
||||||
|
| **Phase 2:用户端核心页面** | 首页、年卡列表/详情、确认购买、激活年卡、入园码 |
|
||||||
|
|
||||||
|
**不包含在第一迭代**:
|
||||||
|
- 管理端页面(卡管理、景区管理)→ 第二迭代
|
||||||
|
- H5 版本登录(OAuth / 手机号)→ 后续扩展
|
||||||
|
- 权益查看、景区预约、预约记录、使用记录 → 后续迭代
|
||||||
|
- 真实微信支付接入 → 第一版用模拟支付
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、项目架构
|
||||||
|
|
||||||
|
### 2.1 分包目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
zhy-test/
|
||||||
|
├── src/
|
||||||
|
│ ├── pages/ # 主包(共享入口)
|
||||||
|
│ │ ├── index/index.vue # 入口页(改造:默认跳转用户端首页)
|
||||||
|
│ │ └── login/login.vue # 登录页(改造为微信授权登录)
|
||||||
|
│ │
|
||||||
|
│ ├── packages/
|
||||||
|
│ │ ├── user/ # 用户端分包
|
||||||
|
│ │ │ └── pages/
|
||||||
|
│ │ │ ├── index/index.vue # 用户首页(宫格式)
|
||||||
|
│ │ │ ├── card/list.vue # 年卡列表
|
||||||
|
│ │ │ ├── card/detail.vue # 年卡详情
|
||||||
|
│ │ │ ├── card/buy.vue # 确认购买(模拟支付)
|
||||||
|
│ │ │ ├── card/activate.vue # 激活年卡
|
||||||
|
│ │ │ ├── card/qrcode.vue # 入园码(卡片式)
|
||||||
|
│ │ │ ├── card/bind.vue # 绑定年卡
|
||||||
|
│ │ │ └── profile/index.vue # 个人中心
|
||||||
|
│ │ │
|
||||||
|
│ │ └── admin/ # 管理端分包(第二迭代,暂不实现)
|
||||||
|
│ │
|
||||||
|
│ ├── api/ # 接口层
|
||||||
|
│ │ ├── request.ts # HTTP 封装(改造现有)
|
||||||
|
│ │ ├── user.ts # 用户/登录接口(改造)
|
||||||
|
│ │ ├── card.ts # 年卡接口(新增)
|
||||||
|
│ │ └── order.ts # 订单/支付接口(新增)
|
||||||
|
│ │
|
||||||
|
│ ├── store/ # Pinia Store
|
||||||
|
│ │ ├── user.ts # 用户状态(改造现有)
|
||||||
|
│ │ └── card.ts # 年卡状态(新增)
|
||||||
|
│ │
|
||||||
|
│ ├── components/ # 共享组件(新增)
|
||||||
|
│ │ ├── card-item.vue # 年卡卡片组件
|
||||||
|
│ │ └── empty-state.vue # 空状态组件
|
||||||
|
│ │
|
||||||
|
│ ├── config/ # 环境配置(新增)
|
||||||
|
│ │ └── index.ts # 根据环境变量切换 baseURL
|
||||||
|
│ │
|
||||||
|
│ └── utils/ # 工具函数(扩展现有)
|
||||||
|
│ └── index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 pages.json 路由配置
|
||||||
|
|
||||||
|
主包保留 `pages/index`(入口跳转)和 `pages/login`,用户端页面全部放入 `packages/user` 分包。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"pages": [
|
||||||
|
{ "path": "pages/index/index", "style": { "navigationBarTitleText": "湖南12301" } },
|
||||||
|
{ "path": "pages/login/login", "style": { "navigationBarTitleText": "登录" } }
|
||||||
|
],
|
||||||
|
"subPackages": [
|
||||||
|
{
|
||||||
|
"root": "packages/user",
|
||||||
|
"pages": [
|
||||||
|
{ "path": "pages/index/index", "style": { "navigationBarTitleText": "文旅专区" } },
|
||||||
|
{ "path": "pages/card/list", "style": { "navigationBarTitleText": "年卡列表" } },
|
||||||
|
{ "path": "pages/card/detail", "style": { "navigationBarTitleText": "年卡详情" } },
|
||||||
|
{ "path": "pages/card/buy", "style": { "navigationBarTitleText": "确认购买" } },
|
||||||
|
{ "path": "pages/card/activate", "style": { "navigationBarTitleText": "激活年卡" } },
|
||||||
|
{ "path": "pages/card/qrcode", "style": { "navigationBarTitleText": "入园码" } },
|
||||||
|
{ "path": "pages/card/bind", "style": { "navigationBarTitleText": "绑定年卡" } },
|
||||||
|
{ "path": "pages/profile/index", "style": { "navigationBarTitleText": "个人中心" } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"tabBar": {
|
||||||
|
"color": "#7A7E83",
|
||||||
|
"selectedColor": "#4A90D9",
|
||||||
|
"backgroundColor": "#ffffff",
|
||||||
|
"list": [
|
||||||
|
{ "pagePath": "packages/user/pages/index/index", "text": "首页" },
|
||||||
|
{ "pagePath": "packages/user/pages/profile/index", "text": "我的" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 多环境配置
|
||||||
|
|
||||||
|
| 环境 | baseURL | 触发方式 | 配置文件 |
|
||||||
|
|------|---------|----------|----------|
|
||||||
|
| 本地开发 | `http://localhost:80` | `pnpm dev:h5` | `.env.local` |
|
||||||
|
| 开发环境 | `https://dscp-dev.hn12301.net` | `--mode dev` | `.env.dev` |
|
||||||
|
| 预发布 | `https://dscp-uat.hn12301.net` | `--mode uat` | `.env.uat` |
|
||||||
|
|
||||||
|
通过 `config/index.ts` 统一读取 `import.meta.env.VITE_API_BASE_URL`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、API 层设计
|
||||||
|
|
||||||
|
### 3.1 request.ts 改造
|
||||||
|
|
||||||
|
**改造要点**:
|
||||||
|
- `baseURL` 从 `config/index.ts` 读取(替代空字符串)
|
||||||
|
- Token 管理保持现有 `uni.getStorageSync` / `uni.setStorageSync` 模式
|
||||||
|
- 请求拦截器自动注入 `Authorization: Bearer <token>`
|
||||||
|
- 响应拦截器:`code === 200` 成功,`401` 清除 token 并跳转登录页
|
||||||
|
- 保留 `get()` / `post()` / `put()` / `del()` 便捷方法
|
||||||
|
|
||||||
|
### 3.2 接口模块定义
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// api/user.ts(改造)
|
||||||
|
wxLogin(code: string) // POST /manage-api/auth/wx-login → { accessToken, refreshToken, userId }
|
||||||
|
getUserInfo() // GET /manage-api/user/info → UserInfo
|
||||||
|
|
||||||
|
// api/card.ts(新增)
|
||||||
|
getCardList(params?) // GET /manage-api/card/list → CardInfo[]
|
||||||
|
getCardDetail(id: string) // GET /manage-api/card/detail?id=xxx → CardDetail
|
||||||
|
activateCard(cardId, data) // POST /manage-api/card/activate → boolean
|
||||||
|
bindCard(cardNo: string) // POST /manage-api/card/bind → boolean
|
||||||
|
getMyCards() // GET /manage-api/card/my-list → MyCard[]
|
||||||
|
getEntryQrCode(cardId: string) // GET /manage-api/card/qrcode?cardId=xxx → { qrCode, expireTime }
|
||||||
|
|
||||||
|
// api/order.ts(新增)
|
||||||
|
createOrder(data) // POST /manage-api/order/create → OrderInfo
|
||||||
|
mockPay(orderId: string) // POST /manage-api/order/mock-pay → { success: true }(模拟支付)
|
||||||
|
getOrderList(params?) // GET /manage-api/order/list → OrderInfo[]
|
||||||
|
getOrderDetail(id: string) // GET /manage-api/order/detail?id=xxx → OrderDetail
|
||||||
|
```
|
||||||
|
|
||||||
|
> **注意**:接口路径和参数格式需在实现时与后端实际接口对齐。以上为预定义,实现阶段根据后端实际接口调整。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、Store 设计
|
||||||
|
|
||||||
|
### 4.1 user store(改造现有)
|
||||||
|
|
||||||
|
```
|
||||||
|
State:
|
||||||
|
token: string // 访问令牌
|
||||||
|
refreshToken: string // 刷新令牌
|
||||||
|
userInfo: UserInfo | null // 用户信息
|
||||||
|
|
||||||
|
Computed:
|
||||||
|
isLogin: boolean // !!token
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
wxLogin(): Promise // 调用 wx.login() 获取 code,发送后端换取 token
|
||||||
|
logout(): void // 清除状态和 storage
|
||||||
|
fetchUserInfo(): Promise // 获取当前用户信息
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 card store(新增)
|
||||||
|
|
||||||
|
```
|
||||||
|
State:
|
||||||
|
cardList: CardInfo[] // 年卡列表
|
||||||
|
currentCard: CardDetail // 当前查看的年卡详情
|
||||||
|
myCards: MyCard[] // 我的年卡列表
|
||||||
|
|
||||||
|
Actions:
|
||||||
|
fetchCardList(params?): Promise // 获取年卡列表
|
||||||
|
fetchCardDetail(id): Promise // 获取年卡详情
|
||||||
|
fetchMyCards(): Promise // 获取我的年卡
|
||||||
|
activateCard(cardId, data): Promise // 激活年卡
|
||||||
|
bindCard(cardNo): Promise // 绑定年卡
|
||||||
|
getEntryQrCode(cardId): Promise // 获取入园码
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、微信登录设计
|
||||||
|
|
||||||
|
### 5.1 登录流程
|
||||||
|
|
||||||
|
```
|
||||||
|
用户打开小程序
|
||||||
|
→ 检查本地是否有 token
|
||||||
|
→ 有 token → 直接进入用户端首页
|
||||||
|
→ 无 token → 跳转登录页
|
||||||
|
→ 用户点击"微信登录"按钮
|
||||||
|
→ uni.login() 获取 code
|
||||||
|
→ POST /manage-api/auth/wx-login { code }
|
||||||
|
→ 后端返回 { accessToken, refreshToken, userId }
|
||||||
|
→ 存入 Store + Storage
|
||||||
|
→ 跳转用户端首页
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 平台策略
|
||||||
|
|
||||||
|
| 平台 | 第一迭代 | 后续扩展 |
|
||||||
|
|------|----------|----------|
|
||||||
|
| 微信小程序 | `wx.login()` 授权登录 | — |
|
||||||
|
| H5 | 不支持(提示请在微信小程序中使用) | 微信 OAuth / 手机号登录 |
|
||||||
|
|
||||||
|
### 5.3 降级方案
|
||||||
|
|
||||||
|
如果后端微信登录接口尚未就绪,可临时使用现有账号密码登录接口,在 `user store` 的 `wxLogin()` 方法中预留切换点。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、用户端页面设计
|
||||||
|
|
||||||
|
### 6.1 首页(宫格式)
|
||||||
|
|
||||||
|
**路由**:`/packages/user/pages/index/index`
|
||||||
|
|
||||||
|
**布局**:
|
||||||
|
- 顶部:标题栏 "湖南12301文旅专区"
|
||||||
|
- 中部:2×2 宫格功能入口(购买年卡 / 激活绑定 / 景区预约 / 我的记录)。其中"景区预约"和"我的记录"在第一迭代中点击后弹出"即将上线"提示
|
||||||
|
- 下部:推荐年卡列表(卡片式展示)
|
||||||
|
- 底部:TabBar(首页 / 我的)— 权益 Tab 在后续迭代中补充
|
||||||
|
|
||||||
|
### 6.2 年卡列表
|
||||||
|
|
||||||
|
**路由**:`/packages/user/pages/card/list`
|
||||||
|
|
||||||
|
**布局**:
|
||||||
|
- 顶部搜索栏
|
||||||
|
- 分类筛选标签
|
||||||
|
- 年卡卡片列表(使用 card-item 组件)
|
||||||
|
- 每个卡片:封面图 + 名称 + 价格 + 简要权益
|
||||||
|
|
||||||
|
### 6.3 年卡详情
|
||||||
|
|
||||||
|
**路由**:`/packages/user/pages/card/detail`
|
||||||
|
|
||||||
|
**布局**:
|
||||||
|
- 顶部封面大图
|
||||||
|
- 年卡名称 + 价格(原价划线)
|
||||||
|
- 权益说明列表
|
||||||
|
- 权益景区横滑卡片
|
||||||
|
- 底部固定按钮:立即购买
|
||||||
|
|
||||||
|
### 6.4 确认购买(模拟支付)
|
||||||
|
|
||||||
|
**路由**:`/packages/user/pages/card/buy`
|
||||||
|
|
||||||
|
**布局**:
|
||||||
|
- 订单信息卡片(卡种名称 / 价格 / 有效期)
|
||||||
|
- 支付方式选择(第一版仅"模拟支付")
|
||||||
|
- 底部按钮:确认支付
|
||||||
|
- 支付成功后跳转到激活页面
|
||||||
|
|
||||||
|
### 6.5 激活年卡
|
||||||
|
|
||||||
|
**路由**:`/packages/user/pages/card/activate`
|
||||||
|
|
||||||
|
**布局**:
|
||||||
|
- 年卡信息展示
|
||||||
|
- 实名信息表单(姓名 / 身份证号 / 手机号)
|
||||||
|
- 底部按钮:立即激活
|
||||||
|
- 激活成功后跳转到入园码页面
|
||||||
|
|
||||||
|
### 6.6 入园码(卡片式)
|
||||||
|
|
||||||
|
**路由**:`/packages/user/pages/card/qrcode`
|
||||||
|
|
||||||
|
**布局**:
|
||||||
|
- 年卡信息卡片(渐变色背景,显示姓名和有效期)
|
||||||
|
- 二维码区域(白色卡片,居中显示)
|
||||||
|
- 提示文字:"扫码入园 · 每60秒自动刷新"
|
||||||
|
- 底部操作按钮:预约景区 / 刷新二维码
|
||||||
|
|
||||||
|
### 6.7 个人中心
|
||||||
|
|
||||||
|
**路由**:`/packages/user/pages/profile/index`(TabBar 页面)
|
||||||
|
|
||||||
|
**布局**:
|
||||||
|
- 用户头像 + 昵称卡片
|
||||||
|
- 我的年卡入口
|
||||||
|
- 订单管理入口
|
||||||
|
- 使用记录入口
|
||||||
|
- 退出登录按钮
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、页面导航流程
|
||||||
|
|
||||||
|
```
|
||||||
|
小程序启动
|
||||||
|
→ pages/index/index(入口页,自动跳转)
|
||||||
|
→ packages/user/pages/index/index(用户首页)
|
||||||
|
|
||||||
|
核心购买流程:
|
||||||
|
用户首页 → 年卡列表 → 年卡详情 → 确认购买 → 模拟支付 → 激活年卡 → 入园码
|
||||||
|
|
||||||
|
TabBar 常驻页面:
|
||||||
|
首页(packages/user/pages/index/index)
|
||||||
|
我的(packages/user/pages/profile/index)
|
||||||
|
|
||||||
|
非 Tab 页面(navigateTo):
|
||||||
|
年卡列表、年卡详情、确认购买、激活年卡、入园码、绑定年卡
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、共享组件
|
||||||
|
|
||||||
|
| 组件 | 路径 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| `card-item.vue` | `components/card-item.vue` | 年卡卡片展示(封面/名称/价格/权益摘要) |
|
||||||
|
| `empty-state.vue` | `components/empty-state.vue` | 空状态提示(图标 + 文案 + 操作按钮) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、关键约束
|
||||||
|
|
||||||
|
1. **接口对齐**:API 路径和参数为预定义,实现时需与后端实际接口对齐
|
||||||
|
2. **模拟支付**:第一版不接入真实微信支付,使用模拟支付接口跑通流程
|
||||||
|
3. **UI 优先级**:功能先行,使用 uni-ui 默认样式,后续迭代美化
|
||||||
|
4. **仅小程序**:第一迭代仅支持微信小程序平台,H5 登录后缀扩展
|
||||||
|
5. **管理端预留**:`packages/admin/` 目录结构预留,不在第一迭代实现
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
# 湖南12301文旅综合平台 - 规格说明书
|
||||||
|
|
||||||
|
**文档版本**:v1.0
|
||||||
|
**最后更新**:2026-06-13
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、项目基本信息
|
||||||
|
|
||||||
|
| 项 | 内容 |
|
||||||
|
|---|---|
|
||||||
|
| **项目名称** | 湖南12301文旅综合平台 |
|
||||||
|
| **技术栈** | uni-app + Vue 3 + Composition API + TypeScript + Pinia + uni-ui |
|
||||||
|
| **构建工具** | Vite |
|
||||||
|
| **环境配置** | local / dev / uat 三套 |
|
||||||
|
| **接口架构** | 后端统一 API 层封装所有第三方 |
|
||||||
|
| **目标平台** | 微信小程序 + H5 + App(Android/iOS) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、双端架构
|
||||||
|
|
||||||
|
### 2.1 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
湖南12301/
|
||||||
|
├── src/
|
||||||
|
│ ├── pages/ # 主包(共享页面)
|
||||||
|
│ │ ├── index/ # 首页(双端入口选择)
|
||||||
|
│ │ └── login/ # 登录页
|
||||||
|
│ ├── packages/
|
||||||
|
│ │ ├── admin/ # 【综合管理端】商家/管理员
|
||||||
|
│ │ │ └── pages/
|
||||||
|
│ │ │ ├── index/ # 管理端首页
|
||||||
|
│ │ │ ├── card/ # 卡管理
|
||||||
|
│ │ │ └── scenic/ # 景区管理
|
||||||
|
│ │ └── user/ # 【文旅专区端】C端用户
|
||||||
|
│ │ └── pages/
|
||||||
|
│ │ ├── index/ # 用户端首页
|
||||||
|
│ │ ├── card/ # 年卡购买/激活/绑定/入园码
|
||||||
|
│ │ ├── rights/ # 权益查看(免票景区/特惠商品)
|
||||||
|
│ │ ├── scenic/ # 景区预约
|
||||||
|
│ │ ├── booking/ # 预约记录
|
||||||
|
│ │ ├── usage/ # 使用记录
|
||||||
|
│ │ └── profile/ # 个人中心(我的年卡/订单)
|
||||||
|
│ ├── components/ # 共享组件
|
||||||
|
│ ├── api/ # 共享接口层
|
||||||
|
│ ├── store/ # Pinia 共享状态
|
||||||
|
│ └── utils/ # 工具函数
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 发布模式
|
||||||
|
|
||||||
|
| 模式 | 编译命令 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| 聚合版 | `npm run build:all` | 双端合一,首页选择入口 |
|
||||||
|
| 管理端独立版 | `npm run build:admin` | 商家/管理员独立小程序 |
|
||||||
|
| 用户端独立版 | `npm run build:user` | C端用户独立小程序 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、综合管理端功能(商家/管理员)
|
||||||
|
|
||||||
|
### 3.1 TabBar 设计
|
||||||
|
|
||||||
|
| Tab | 路由 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| 🏠 | 首页 | 数据概览 |
|
||||||
|
| 🎫 | 卡管理 | 年卡列表/编辑 |
|
||||||
|
| 📍 | 景区管理 | 景区列表/编辑 |
|
||||||
|
| 👤 | 我的 | 个人设置 |
|
||||||
|
|
||||||
|
### 3.2 功能模块
|
||||||
|
|
||||||
|
| 模块 | 路由 | 功能 |
|
||||||
|
|---|---|---|
|
||||||
|
| **首页** | `/packages/admin/pages/index/index` | 数据概览(待扩展) |
|
||||||
|
| **卡管理** | `/packages/admin/pages/card/list` | 年卡列表、创建/编辑年卡 |
|
||||||
|
| **景区管理** | `/packages/admin/pages/scenic/list` | 景区列表、创建/编辑景区 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、文旅专区端功能(C端用户)
|
||||||
|
|
||||||
|
### 4.1 TabBar 设计
|
||||||
|
|
||||||
|
| Tab | 路由 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| 🏠 | 首页 | 年卡展示、推荐景区、Banner |
|
||||||
|
| 🎫 | 权益 | 免票景区、特惠商品 |
|
||||||
|
| 👤 | 我的 | 我的年卡、订单、预约、积分等 |
|
||||||
|
|
||||||
|
### 4.2 功能模块
|
||||||
|
|
||||||
|
| 序号 | 模块 | 路由 | 功能 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | **年卡购买** | `/packages/user/pages/card/buy` | 卡种列表 → 选择卡种 → 确认订单 → 微信支付 |
|
||||||
|
| 2 | **年卡激活** | `/packages/user/pages/card/activate` | 激活已购买的年卡 |
|
||||||
|
| 3 | **年卡绑定** | `/packages/user/pages/card/bind` | 输入卡号绑定已有年卡 |
|
||||||
|
| 4 | **入园码** | `/packages/user/pages/card/qrcode` | 生成/展示入园二维码 |
|
||||||
|
| 5 | **权益查看** | `/packages/user/pages/rights/index` | 免票景区列表、特惠商品列表(含搜索/筛选/排序) |
|
||||||
|
| 6 | **景区预约** | `/packages/user/pages/scenic/book` | 选择景区 → 选择时间 → 确认预约 |
|
||||||
|
| 7 | **预约记录** | `/packages/user/pages/booking/list` | 查看/取消已预约记录 |
|
||||||
|
| 8 | **使用记录** | `/packages/user/pages/usage/list` | 已入园/已使用记录 |
|
||||||
|
| 9 | **订单管理** | `/packages/user/pages/profile/order` | 全部/待支付/已完成订单 |
|
||||||
|
| 10 | **我的年卡** | `/packages/user/pages/profile/my-card` | 已购年卡列表、到期时间 |
|
||||||
|
|
||||||
|
### 4.3 权益查看子功能
|
||||||
|
|
||||||
|
| 功能 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| **搜索** | 关键词搜索景区/商品 |
|
||||||
|
| **分类筛选** | 按分类筛选(景区/影院/温泉/亲子乐园等) |
|
||||||
|
| **排序** | 按距离/热度/时间排序 |
|
||||||
|
|
||||||
|
### 4.4 核心业务流程
|
||||||
|
|
||||||
|
```
|
||||||
|
购买年卡:
|
||||||
|
年卡展示 → 选择卡种 → 微信支付 → 支付成功 → 年卡激活 → 我的年卡
|
||||||
|
|
||||||
|
权益使用:
|
||||||
|
查看权益 → 选择景区 → 景区预约 → 生成入园码 → 入园核销 → 使用记录
|
||||||
|
|
||||||
|
年卡绑定:
|
||||||
|
已有年卡 → 输入卡号 → 验证 → 绑定成功 → 查看权益
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、API 接口模块
|
||||||
|
|
||||||
|
| 模块 | 接口描述 |
|
||||||
|
|---|---|
|
||||||
|
| **用户模块** | 登录、获取用户信息 |
|
||||||
|
| **年卡模块** | 卡种列表、卡种详情、年卡激活、年卡绑定、我的年卡列表 |
|
||||||
|
| **权益模块** | 免票景区列表、特惠商品列表、景区/商品详情 |
|
||||||
|
| **预约模块** | 创建预约、取消预约、预约列表 |
|
||||||
|
| **核销模块** | 生成入园码、验证入园码 |
|
||||||
|
| **订单模块** | 创建订单、微信支付、订单列表、订单详情 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、环境配置
|
||||||
|
|
||||||
|
| 环境 | 标识 | 用途 |
|
||||||
|
|---|---|---|
|
||||||
|
| 本地开发 | `local` | 本机开发调试 |
|
||||||
|
| 测试环境 | `dev` | 团队联调、测试验证 |
|
||||||
|
| 预发布环境 | `uat` | 生产环境验证、灰度测试 |
|
||||||
+20
@@ -0,0 +1,20 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<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="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Generated
+8810
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,73 @@
|
|||||||
|
{
|
||||||
|
"name": "uni-preset-vue",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev:custom": "uni -p",
|
||||||
|
"dev:h5": "uni",
|
||||||
|
"dev:h5:ssr": "uni --ssr",
|
||||||
|
"dev:mp-alipay": "uni -p mp-alipay",
|
||||||
|
"dev:mp-baidu": "uni -p mp-baidu",
|
||||||
|
"dev:mp-jd": "uni -p mp-jd",
|
||||||
|
"dev:mp-kuaishou": "uni -p mp-kuaishou",
|
||||||
|
"dev:mp-lark": "uni -p mp-lark",
|
||||||
|
"dev:mp-qq": "uni -p mp-qq",
|
||||||
|
"dev:mp-toutiao": "uni -p mp-toutiao",
|
||||||
|
"dev:mp-harmony": "uni -p mp-harmony",
|
||||||
|
"dev:mp-weixin": "uni -p mp-weixin",
|
||||||
|
"dev:mp-xhs": "uni -p mp-xhs",
|
||||||
|
"dev:quickapp-webview": "uni -p quickapp-webview",
|
||||||
|
"dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
|
||||||
|
"dev:quickapp-webview-union": "uni -p quickapp-webview-union",
|
||||||
|
"build:custom": "uni build -p",
|
||||||
|
"build:h5": "uni build",
|
||||||
|
"build:h5:ssr": "uni build --ssr",
|
||||||
|
"build:mp-alipay": "uni build -p mp-alipay",
|
||||||
|
"build:mp-baidu": "uni build -p mp-baidu",
|
||||||
|
"build:mp-jd": "uni build -p mp-jd",
|
||||||
|
"build:mp-kuaishou": "uni build -p mp-kuaishou",
|
||||||
|
"build:mp-lark": "uni build -p mp-lark",
|
||||||
|
"build:mp-qq": "uni build -p mp-qq",
|
||||||
|
"build:mp-toutiao": "uni build -p mp-toutiao",
|
||||||
|
"build:mp-harmony": "uni build -p mp-harmony",
|
||||||
|
"build:mp-weixin": "uni build -p mp-weixin",
|
||||||
|
"build:mp-xhs": "uni build -p mp-xhs",
|
||||||
|
"build:quickapp-webview": "uni build -p quickapp-webview",
|
||||||
|
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
|
||||||
|
"build:quickapp-webview-union": "uni build -p quickapp-webview-union",
|
||||||
|
"type-check": "vue-tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@dcloudio/uni-app": "3.0.0-4080420251103001",
|
||||||
|
"@dcloudio/uni-app-harmony": "3.0.0-4080420251103001",
|
||||||
|
"@dcloudio/uni-app-plus": "3.0.0-4080420251103001",
|
||||||
|
"@dcloudio/uni-components": "3.0.0-4080420251103001",
|
||||||
|
"@dcloudio/uni-h5": "3.0.0-4080420251103001",
|
||||||
|
"@dcloudio/uni-mp-alipay": "3.0.0-4080420251103001",
|
||||||
|
"@dcloudio/uni-mp-baidu": "3.0.0-4080420251103001",
|
||||||
|
"@dcloudio/uni-mp-harmony": "3.0.0-4080420251103001",
|
||||||
|
"@dcloudio/uni-mp-jd": "3.0.0-4080420251103001",
|
||||||
|
"@dcloudio/uni-mp-kuaishou": "3.0.0-4080420251103001",
|
||||||
|
"@dcloudio/uni-mp-lark": "3.0.0-4080420251103001",
|
||||||
|
"@dcloudio/uni-mp-qq": "3.0.0-4080420251103001",
|
||||||
|
"@dcloudio/uni-mp-toutiao": "3.0.0-4080420251103001",
|
||||||
|
"@dcloudio/uni-mp-weixin": "3.0.0-4080420251103001",
|
||||||
|
"@dcloudio/uni-mp-xhs": "3.0.0-4080420251103001",
|
||||||
|
"@dcloudio/uni-quickapp-webview": "3.0.0-4080420251103001",
|
||||||
|
"@dcloudio/uni-ui": "^1.5.12",
|
||||||
|
"pinia": "^3.0.4",
|
||||||
|
"vue": "^3.5.38",
|
||||||
|
"vue-i18n": "^9.1.9"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@dcloudio/types": "^3.4.8",
|
||||||
|
"@dcloudio/uni-automator": "3.0.0-4080420251103001",
|
||||||
|
"@dcloudio/uni-cli-shared": "3.0.0-4080420251103001",
|
||||||
|
"@dcloudio/uni-stacktracey": "3.0.0-4080420251103001",
|
||||||
|
"@dcloudio/vite-plugin-uni": "3.0.0-4080420251103001",
|
||||||
|
"@vue/runtime-core": "^3.5.38",
|
||||||
|
"@vue/tsconfig": "^0.1.3",
|
||||||
|
"typescript": "^4.9.4",
|
||||||
|
"vite": "5.2.8",
|
||||||
|
"vue-tsc": "^1.0.24"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// index.js
|
||||||
|
const defaultAvatarUrl = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0'
|
||||||
|
|
||||||
|
Page({
|
||||||
|
data: {
|
||||||
|
motto: 'Hello World',
|
||||||
|
userInfo: {
|
||||||
|
avatarUrl: defaultAvatarUrl,
|
||||||
|
nickName: '',
|
||||||
|
},
|
||||||
|
hasUserInfo: false,
|
||||||
|
canIUseGetUserProfile: wx.canIUse('getUserProfile'),
|
||||||
|
canIUseNicknameComp: wx.canIUse('input.type.nickname'),
|
||||||
|
},
|
||||||
|
bindViewTap() {
|
||||||
|
wx.navigateTo({
|
||||||
|
url: '../logs/logs'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onChooseAvatar(e) {
|
||||||
|
const { avatarUrl } = e.detail
|
||||||
|
const { nickName } = this.data.userInfo
|
||||||
|
this.setData({
|
||||||
|
"userInfo.avatarUrl": avatarUrl,
|
||||||
|
hasUserInfo: nickName && avatarUrl && avatarUrl !== defaultAvatarUrl,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onInputChange(e) {
|
||||||
|
const nickName = e.detail.value
|
||||||
|
const { avatarUrl } = this.data.userInfo
|
||||||
|
this.setData({
|
||||||
|
"userInfo.nickName": nickName,
|
||||||
|
hasUserInfo: nickName && avatarUrl && avatarUrl !== defaultAvatarUrl,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
getUserProfile(e) {
|
||||||
|
// 推荐使用wx.getUserProfile获取用户信息,开发者每次通过该接口获取用户个人信息均需用户确认,开发者妥善保管用户快速填写的头像昵称,避免重复弹窗
|
||||||
|
wx.getUserProfile({
|
||||||
|
desc: '展示用户信息', // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写
|
||||||
|
success: (res) => {
|
||||||
|
console.log(res)
|
||||||
|
this.setData({
|
||||||
|
userInfo: res.userInfo,
|
||||||
|
hasUserInfo: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"usingComponents": {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<!--index.wxml-->
|
||||||
|
<scroll-view class="scrollarea" scroll-y type="list">
|
||||||
|
<view class="container">
|
||||||
|
<view class="userinfo">
|
||||||
|
<block wx:if="{{canIUseNicknameComp && !hasUserInfo}}">
|
||||||
|
<button class="avatar-wrapper" open-type="chooseAvatar" bind:chooseavatar="onChooseAvatar">
|
||||||
|
<image class="avatar" src="{{userInfo.avatarUrl}}"></image>
|
||||||
|
</button>
|
||||||
|
<view class="nickname-wrapper">
|
||||||
|
<text class="nickname-label">昵称</text>
|
||||||
|
<input type="nickname" class="nickname-input" placeholder="请输入昵称" bind:change="onInputChange" />
|
||||||
|
</view>
|
||||||
|
</block>
|
||||||
|
<block wx:elif="{{!hasUserInfo}}">
|
||||||
|
<button wx:if="{{canIUseGetUserProfile}}" bindtap="getUserProfile"> 获取头像昵称 </button>
|
||||||
|
<view wx:else> 请使用2.10.4及以上版本基础库 </view>
|
||||||
|
</block>
|
||||||
|
<block wx:else>
|
||||||
|
<image bindtap="bindViewTap" class="userinfo-avatar" src="{{userInfo.avatarUrl}}" mode="cover"></image>
|
||||||
|
<text class="userinfo-nickname">{{userInfo.nickName}}</text>
|
||||||
|
</block>
|
||||||
|
</view>
|
||||||
|
<view class="usermotto">
|
||||||
|
<text class="user-motto">{{motto}}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/**index.wxss**/
|
||||||
|
page {
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.scrollarea {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userinfo {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
color: #aaa;
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userinfo-avatar {
|
||||||
|
overflow: hidden;
|
||||||
|
width: 128rpx;
|
||||||
|
height: 128rpx;
|
||||||
|
margin: 20rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usermotto {
|
||||||
|
margin-top: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-wrapper {
|
||||||
|
padding: 0;
|
||||||
|
width: 56px !important;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-top: 40px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
display: block;
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nickname-wrapper {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
padding: 16px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border-top: .5px solid rgba(0, 0, 0, 0.1);
|
||||||
|
border-bottom: .5px solid rgba(0, 0, 0, 0.1);
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nickname-label {
|
||||||
|
width: 105px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nickname-input {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"compileType": "miniprogram",
|
||||||
|
"libVersion": "trial",
|
||||||
|
"packOptions": {
|
||||||
|
"ignore": [],
|
||||||
|
"include": []
|
||||||
|
},
|
||||||
|
"setting": {
|
||||||
|
"coverView": true,
|
||||||
|
"es6": true,
|
||||||
|
"postcss": true,
|
||||||
|
"minified": true,
|
||||||
|
"enhance": true,
|
||||||
|
"showShadowRootInWxmlPanel": true,
|
||||||
|
"packNpmRelationList": [],
|
||||||
|
"babelSetting": {
|
||||||
|
"ignore": [],
|
||||||
|
"disablePlugins": [],
|
||||||
|
"outputPath": ""
|
||||||
|
},
|
||||||
|
"uglifyFileName": false,
|
||||||
|
"uploadWithSourceMap": true,
|
||||||
|
"packNpmManually": false,
|
||||||
|
"minifyWXSS": true,
|
||||||
|
"localPlugins": false,
|
||||||
|
"condition": false,
|
||||||
|
"swc": false,
|
||||||
|
"disableSWC": true,
|
||||||
|
"compileWorklet": false,
|
||||||
|
"disableUseStrict": false,
|
||||||
|
"minifyWXML": true,
|
||||||
|
"useCompilerPlugins": false
|
||||||
|
},
|
||||||
|
"condition": {},
|
||||||
|
"editorSetting": {
|
||||||
|
"tabIndent": "auto",
|
||||||
|
"tabSize": 2
|
||||||
|
},
|
||||||
|
"appid": "wx636bc46a6570e9fd",
|
||||||
|
"simulatorPluginLibVersion": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"description": "项目私有配置文件。此文件中的内容将覆盖 project.config.json 中的相同字段。项目的改动优先同步到此文件中。详见文档:https://developers.weixin.qq.com/miniprogram/dev/devtools/projectconfig.html",
|
||||||
|
"projectname": "%E8%B4%A8%E6%83%A0%E6%B8%B8%E6%B5%8B%E8%AF%95",
|
||||||
|
"setting": {
|
||||||
|
"compileHotReLoad": true,
|
||||||
|
"urlCheck": true,
|
||||||
|
"coverView": true,
|
||||||
|
"lazyloadPlaceholderEnable": false,
|
||||||
|
"skylineRenderEnable": false,
|
||||||
|
"preloadBackgroundData": false,
|
||||||
|
"autoAudits": false,
|
||||||
|
"showShadowRootInWxmlPanel": true,
|
||||||
|
"showES6CompileOption": false,
|
||||||
|
"bigPackageSizeSupport": false,
|
||||||
|
"useApiHook": true,
|
||||||
|
"checkInvalidKey": true,
|
||||||
|
"useStaticServer": false,
|
||||||
|
"useLanDebug": false,
|
||||||
|
"ignoreDevUnusedFiles": true
|
||||||
|
},
|
||||||
|
"libVersion": "3.16.1"
|
||||||
|
}
|
||||||
Vendored
+10
@@ -0,0 +1,10 @@
|
|||||||
|
/// <reference types='@dcloudio/types' />
|
||||||
|
import 'vue'
|
||||||
|
|
||||||
|
declare module '@vue/runtime-core' {
|
||||||
|
type Hooks = App.AppInstance & Page.PageInstance;
|
||||||
|
|
||||||
|
interface ComponentCustomOptions extends Hooks {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
|
||||||
|
"rules": [{
|
||||||
|
"action": "allow",
|
||||||
|
"page": "*"
|
||||||
|
}]
|
||||||
|
}
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { onLaunch, onShow, onHide } from "@dcloudio/uni-app"
|
||||||
|
|
||||||
|
onLaunch(() => {
|
||||||
|
const logs = uni.getStorageSync('logs') || []
|
||||||
|
logs.unshift(Date.now())
|
||||||
|
uni.setStorageSync('logs', logs)
|
||||||
|
})
|
||||||
|
|
||||||
|
onShow(() => {
|
||||||
|
})
|
||||||
|
|
||||||
|
onHide(() => {
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
padding: 30rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-center {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export * from './user'
|
||||||
|
export * from './request'
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
interface RequestOptions {
|
||||||
|
url: string
|
||||||
|
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
|
||||||
|
data?: any
|
||||||
|
header?: any
|
||||||
|
needToken?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResponseData<T = any> {
|
||||||
|
code: number
|
||||||
|
message: string
|
||||||
|
data: T
|
||||||
|
}
|
||||||
|
|
||||||
|
const BASE_URL = ''
|
||||||
|
const TOKEN_KEY = 'app_token'
|
||||||
|
|
||||||
|
function getToken(): string {
|
||||||
|
return uni.getStorageSync(TOKEN_KEY) || ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function setToken(token: string): void {
|
||||||
|
uni.setStorageSync(TOKEN_KEY, token)
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeToken(): void {
|
||||||
|
uni.removeStorageSync(TOKEN_KEY)
|
||||||
|
}
|
||||||
|
|
||||||
|
function request<T = any>(options: RequestOptions): Promise<T> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const { url, method = 'GET', data, needToken = true } = options
|
||||||
|
|
||||||
|
const header: any = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...options.header
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needToken) {
|
||||||
|
const token = getToken()
|
||||||
|
if (token) {
|
||||||
|
header['Authorization'] = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uni.request({
|
||||||
|
url: BASE_URL + url,
|
||||||
|
method,
|
||||||
|
data,
|
||||||
|
header,
|
||||||
|
success: (res: any) => {
|
||||||
|
const result = res.data as ResponseData<T>
|
||||||
|
if (res.statusCode === 200) {
|
||||||
|
if (result.code === 0 || result.code === 200) {
|
||||||
|
resolve(result.data as T)
|
||||||
|
} else if (result.code === 401) {
|
||||||
|
removeToken()
|
||||||
|
uni.showToast({ title: '请先登录', icon: 'none' })
|
||||||
|
reject(new Error(result.message || '未授权'))
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: result.message || '请求失败', icon: 'none' })
|
||||||
|
reject(new Error(result.message || '请求失败'))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
uni.showToast({ title: '网络错误', icon: 'none' })
|
||||||
|
reject(new Error('网络错误'))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fail: (err: any) => {
|
||||||
|
uni.showToast({ title: '网络连接失败', icon: 'none' })
|
||||||
|
reject(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function get<T = any>(url: string, data?: any, needToken: boolean = true): Promise<T> {
|
||||||
|
return request<T>({ url, method: 'GET', data, needToken })
|
||||||
|
}
|
||||||
|
|
||||||
|
function post<T = any>(url: string, data?: any, needToken: boolean = true): Promise<T> {
|
||||||
|
return request<T>({ url, method: 'POST', data, needToken })
|
||||||
|
}
|
||||||
|
|
||||||
|
function put<T = any>(url: string, data?: any, needToken: boolean = true): Promise<T> {
|
||||||
|
return request<T>({ url, method: 'PUT', data, needToken })
|
||||||
|
}
|
||||||
|
|
||||||
|
function del<T = any>(url: string, data?: any, needToken: boolean = true): Promise<T> {
|
||||||
|
return request<T>({ url, method: 'DELETE', data, needToken })
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
request,
|
||||||
|
get,
|
||||||
|
post,
|
||||||
|
put,
|
||||||
|
del,
|
||||||
|
getToken,
|
||||||
|
setToken,
|
||||||
|
removeToken,
|
||||||
|
BASE_URL
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { get, post } from './request'
|
||||||
|
|
||||||
|
export interface UserInfo {
|
||||||
|
id: number
|
||||||
|
nickname: string
|
||||||
|
avatarUrl: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginParams {
|
||||||
|
username: string
|
||||||
|
password: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoginResult {
|
||||||
|
token: string
|
||||||
|
userInfo: UserInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
export function login(params: LoginParams): Promise<LoginResult> {
|
||||||
|
return post<LoginResult>('/api/login', params, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUserInfo(): Promise<UserInfo> {
|
||||||
|
return get<UserInfo>('/api/user/info')
|
||||||
|
}
|
||||||
Vendored
+8
@@ -0,0 +1,8 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
|
declare module '*.vue' {
|
||||||
|
import { DefineComponent } from 'vue'
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
||||||
|
const component: DefineComponent<{}, {}, any>
|
||||||
|
export default component
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
import { createSSRApp } from 'vue'
|
||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import App from './App.vue'
|
||||||
|
|
||||||
|
export function createApp() {
|
||||||
|
const app = createSSRApp(App)
|
||||||
|
const pinia = createPinia()
|
||||||
|
app.use(pinia)
|
||||||
|
return {
|
||||||
|
app,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
"easycom": {
|
||||||
|
"autoscan": true,
|
||||||
|
"custom": {
|
||||||
|
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"path": "pages/index/index",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "首页"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/login/login",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "登录"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/profile/profile",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "个人中心"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"path": "pages/logs/logs",
|
||||||
|
"style": {
|
||||||
|
"navigationBarTitleText": "日志"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"globalStyle": {
|
||||||
|
"navigationBarTextStyle": "black",
|
||||||
|
"navigationBarTitleText": "zhy-test",
|
||||||
|
"navigationBarBackgroundColor": "#F8F8F8",
|
||||||
|
"backgroundColor": "#F8F8F8"
|
||||||
|
},
|
||||||
|
"tabBar": {
|
||||||
|
"color": "#7A7E83",
|
||||||
|
"selectedColor": "#007AFF",
|
||||||
|
"borderStyle": "black",
|
||||||
|
"backgroundColor": "#ffffff",
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"pagePath": "pages/index/index",
|
||||||
|
"text": "首页"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pagePath": "pages/profile/profile",
|
||||||
|
"text": "我的"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<view class="hero">
|
||||||
|
<image class="logo" src="/static/logo.png" mode="aspectFit" />
|
||||||
|
<text class="title">zhy-test</text>
|
||||||
|
<text class="subtitle">uni-app + Vue3 + TypeScript</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card-list">
|
||||||
|
<view class="card" @click="goToProfile">
|
||||||
|
<view class="card-icon">👤</view>
|
||||||
|
<view class="card-content">
|
||||||
|
<text class="card-title">个人中心</text>
|
||||||
|
<text class="card-desc">查看个人信息</text>
|
||||||
|
</view>
|
||||||
|
<view class="card-arrow">›</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card" @click="goToLogs">
|
||||||
|
<view class="card-icon">📝</view>
|
||||||
|
<view class="card-content">
|
||||||
|
<text class="card-title">日志记录</text>
|
||||||
|
<text class="card-desc">查看应用启动日志</text>
|
||||||
|
</view>
|
||||||
|
<view class="card-arrow">›</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card" @click="goToLogin">
|
||||||
|
<view class="card-icon">🔑</view>
|
||||||
|
<view class="card-content">
|
||||||
|
<text class="card-title">登录</text>
|
||||||
|
<text class="card-desc">用户登录页面</text>
|
||||||
|
</view>
|
||||||
|
<view class="card-arrow">›</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="info">
|
||||||
|
<text class="info-title">支持多端发布</text>
|
||||||
|
<view class="tag-list">
|
||||||
|
<text class="tag">微信小程序</text>
|
||||||
|
<text class="tag">H5</text>
|
||||||
|
<text class="tag">App</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
function goToProfile() {
|
||||||
|
uni.switchTab({ url: '/pages/profile/profile' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToLogs() {
|
||||||
|
uni.navigateTo({ url: '/pages/logs/logs' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToLogin() {
|
||||||
|
uni.navigateTo({ url: '/pages/login/login' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
padding: 30rpx;
|
||||||
|
min-height: 100vh;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 60rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 160rpx;
|
||||||
|
height: 160rpx;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-list {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 30rpx;
|
||||||
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon {
|
||||||
|
font-size: 40rpx;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 6rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-desc {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-arrow {
|
||||||
|
font-size: 40rpx;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
padding: 30rpx;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag-list {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20rpx;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tag {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 10rpx 30rpx;
|
||||||
|
background: #e8f4ff;
|
||||||
|
color: #007aff;
|
||||||
|
border-radius: 30rpx;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<view class="header">
|
||||||
|
<text class="welcome">欢迎使用</text>
|
||||||
|
<text class="app-name">zhy-test</text>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form">
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="label">账号</text>
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
v-model="form.username"
|
||||||
|
placeholder="请输入账号"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="label">密码</text>
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
v-model="form.password"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
password
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<button class="btn-primary" @click="handleLogin" :disabled="loading">
|
||||||
|
{{ loading ? '登录中...' : '登 录' }}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<view class="tips">
|
||||||
|
<text>当前为演示版本,后端接口未配置时无法真实登录</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive } from 'vue'
|
||||||
|
import { useUserStore } from '../../store/user'
|
||||||
|
import { showToast } from '../../utils'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const form = reactive({
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
async function handleLogin() {
|
||||||
|
if (!form.username || !form.password) {
|
||||||
|
showToast('请输入账号和密码')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
await userStore.login({
|
||||||
|
username: form.username,
|
||||||
|
password: form.password
|
||||||
|
})
|
||||||
|
showToast('登录成功', 'success')
|
||||||
|
setTimeout(() => {
|
||||||
|
uni.switchTab({ url: '/pages/profile/profile' })
|
||||||
|
}, 1000)
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error('登录失败:', err)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
padding: 60rpx 40rpx;
|
||||||
|
min-height: 100vh;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 80rpx;
|
||||||
|
padding-top: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome {
|
||||||
|
display: block;
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-name {
|
||||||
|
display: block;
|
||||||
|
font-size: 56rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: block;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
width: 100%;
|
||||||
|
height: 80rpx;
|
||||||
|
padding: 0 20rpx;
|
||||||
|
background: #f8f8f8;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
width: 100%;
|
||||||
|
height: 90rpx;
|
||||||
|
line-height: 90rpx;
|
||||||
|
background: #007aff;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
font-size: 32rpx;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary[disabled] {
|
||||||
|
background: #99c9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips {
|
||||||
|
margin-top: 30rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tips text {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
<template>
|
||||||
|
<view class="container">
|
||||||
|
<view class="user-card">
|
||||||
|
<image
|
||||||
|
class="avatar"
|
||||||
|
:src="userStore.userInfo?.avatarUrl || defaultAvatar"
|
||||||
|
mode="aspectFill"
|
||||||
|
/>
|
||||||
|
<view class="user-info">
|
||||||
|
<text class="nickname">{{ userStore.userInfo?.nickname || '未登录' }}</text>
|
||||||
|
<text class="user-id">ID: {{ userStore.userInfo?.id || '---' }}</text>
|
||||||
|
</view>
|
||||||
|
<button v-if="!userStore.isLogin" class="login-btn" size="mini" @click="goLogin">
|
||||||
|
登录
|
||||||
|
</button>
|
||||||
|
<button v-else class="login-btn" size="mini" @click="handleLogout">
|
||||||
|
退出
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<view class="section-title">功能菜单</view>
|
||||||
|
|
||||||
|
<view class="menu-list">
|
||||||
|
<view class="menu-item" @click="goToLogs">
|
||||||
|
<view class="menu-icon">📝</view>
|
||||||
|
<text class="menu-text">查看日志</text>
|
||||||
|
<view class="menu-arrow">›</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="menu-item" @click="showAbout">
|
||||||
|
<view class="menu-icon">ℹ️</view>
|
||||||
|
<text class="menu-text">关于应用</text>
|
||||||
|
<view class="menu-arrow">›</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="menu-item" @click="clearCache">
|
||||||
|
<view class="menu-icon">🗑️</view>
|
||||||
|
<text class="menu-text">清除缓存</text>
|
||||||
|
<view class="menu-arrow">›</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="section">
|
||||||
|
<view class="section-title">技术栈</view>
|
||||||
|
<view class="tech-list">
|
||||||
|
<view class="tech-item">
|
||||||
|
<text class="tech-label">框架</text>
|
||||||
|
<text class="tech-value">uni-app</text>
|
||||||
|
</view>
|
||||||
|
<view class="tech-item">
|
||||||
|
<text class="tech-label">Vue</text>
|
||||||
|
<text class="tech-value">3.x</text>
|
||||||
|
</view>
|
||||||
|
<view class="tech-item">
|
||||||
|
<text class="tech-label">构建</text>
|
||||||
|
<text class="tech-value">Vite</text>
|
||||||
|
</view>
|
||||||
|
<view class="tech-item">
|
||||||
|
<text class="tech-label">语言</text>
|
||||||
|
<text class="tech-value">TypeScript</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useUserStore } from '../../store/user'
|
||||||
|
import { showToast } from '../../utils'
|
||||||
|
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const defaultAvatar = 'https://mmbiz.qpic.cn/mmbiz/icTdbqWNOwNRna42FI242Lcia07jQodd2FJGIYQfG0LAJGFxM4FbnQP6yfMxBgJ0F3YRqJCJ1aPAK2dQagdusBZg/0'
|
||||||
|
|
||||||
|
function goLogin() {
|
||||||
|
uni.navigateTo({ url: '/pages/login/login' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleLogout() {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '确定要退出登录吗?',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
userStore.logout()
|
||||||
|
showToast('已退出登录', 'success')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToLogs() {
|
||||||
|
uni.navigateTo({ url: '/pages/logs/logs' })
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAbout() {
|
||||||
|
uni.showModal({
|
||||||
|
title: '关于',
|
||||||
|
content: 'zhy-test uni-app 项目\n\n支持多端发布:\n- 微信小程序\n- H5\n- Android/iOS App',
|
||||||
|
showCancel: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearCache() {
|
||||||
|
uni.showModal({
|
||||||
|
title: '提示',
|
||||||
|
content: '确定要清除缓存吗?',
|
||||||
|
success: (res) => {
|
||||||
|
if (res.confirm) {
|
||||||
|
uni.clearStorageSync()
|
||||||
|
showToast('缓存已清除', 'success')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.container {
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 30rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 40rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 120rpx;
|
||||||
|
height: 120rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 30rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nickname {
|
||||||
|
font-size: 32rpx;
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-id {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
font-size: 26rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 20rpx;
|
||||||
|
padding: 30rpx;
|
||||||
|
margin-bottom: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 20rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-list {
|
||||||
|
border-radius: 12rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 24rpx 0;
|
||||||
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-icon {
|
||||||
|
font-size: 36rpx;
|
||||||
|
margin-right: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-text {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-arrow {
|
||||||
|
font-size: 36rpx;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-item {
|
||||||
|
width: calc(50% - 10rpx);
|
||||||
|
background: #f8f8f8;
|
||||||
|
padding: 20rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-value {
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
export {}
|
||||||
|
|
||||||
|
declare module "vue" {
|
||||||
|
type Hooks = App.AppInstance & Page.PageInstance;
|
||||||
|
interface ComponentCustomOptions extends Hooks {}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
@@ -0,0 +1,41 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { getToken, setToken, removeToken } from '../api/request'
|
||||||
|
import { getUserInfo, login as apiLogin, type UserInfo, type LoginParams } from '../api/user'
|
||||||
|
|
||||||
|
export const useUserStore = defineStore('user', () => {
|
||||||
|
const token = ref<string>(getToken())
|
||||||
|
const userInfo = ref<UserInfo | null>(null)
|
||||||
|
|
||||||
|
const isLogin = computed(() => !!token.value)
|
||||||
|
|
||||||
|
async function login(params: LoginParams) {
|
||||||
|
const result = await apiLogin(params)
|
||||||
|
token.value = result.token
|
||||||
|
userInfo.value = result.userInfo
|
||||||
|
setToken(result.token)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchUserInfo() {
|
||||||
|
if (!token.value) return
|
||||||
|
const info = await getUserInfo()
|
||||||
|
userInfo.value = info
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
function logout() {
|
||||||
|
token.value = ''
|
||||||
|
userInfo.value = null
|
||||||
|
removeToken()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
userInfo,
|
||||||
|
isLogin,
|
||||||
|
login,
|
||||||
|
fetchUserInfo,
|
||||||
|
logout
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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: #fff;
|
||||||
|
$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: 16;
|
||||||
|
|
||||||
|
/* 图片尺寸 */
|
||||||
|
$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: #555; // 二级标题颜色
|
||||||
|
$uni-font-size-subtitle: 18px;
|
||||||
|
$uni-color-paragraph: #3f536e; // 文章段落颜色
|
||||||
|
$uni-font-size-paragraph: 15px;
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
const formatNumber = (n: number): string => {
|
||||||
|
const str = n.toString()
|
||||||
|
return str[1] ? str : '0' + str
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(date: Date | number | string): string {
|
||||||
|
const d = new Date(date)
|
||||||
|
const year = d.getFullYear()
|
||||||
|
const month = d.getMonth() + 1
|
||||||
|
const day = d.getDate()
|
||||||
|
const hour = d.getHours()
|
||||||
|
const minute = d.getMinutes()
|
||||||
|
const second = d.getSeconds()
|
||||||
|
return `${year}/${month}/${day} ${[hour, minute, second].map(formatNumber).join(':')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function showToast(title: string, icon: 'success' | 'loading' | 'error' | 'none' = 'none'): void {
|
||||||
|
uni.showToast({
|
||||||
|
title,
|
||||||
|
icon,
|
||||||
|
duration: 2000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateTo(url: string): void {
|
||||||
|
uni.navigateTo({ url })
|
||||||
|
}
|
||||||
|
|
||||||
|
function redirectTo(url: string): void {
|
||||||
|
uni.redirectTo({ url })
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTab(url: string): void {
|
||||||
|
uni.switchTab({ url })
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
formatTime,
|
||||||
|
formatNumber,
|
||||||
|
showToast,
|
||||||
|
navigateTo,
|
||||||
|
redirectTo,
|
||||||
|
switchTab
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"extends": "@vue/tsconfig/tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"sourceMap": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
"lib": ["esnext", "dom"],
|
||||||
|
"types": ["@dcloudio/types"]
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
const formatTime = date => {
|
||||||
|
const year = date.getFullYear()
|
||||||
|
const month = date.getMonth() + 1
|
||||||
|
const day = date.getDate()
|
||||||
|
const hour = date.getHours()
|
||||||
|
const minute = date.getMinutes()
|
||||||
|
const second = date.getSeconds()
|
||||||
|
|
||||||
|
return `${[year, month, day].map(formatNumber).join('/')} ${[hour, minute, second].map(formatNumber).join(':')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatNumber = n => {
|
||||||
|
n = n.toString()
|
||||||
|
return n[1] ? n : `0${n}`
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
formatTime
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from "vite";
|
||||||
|
import uni from "@dcloudio/vite-plugin-uni";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [uni()],
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user