chore: 初始化项目 — uni-app + Vue3 + TypeScript 脚手架

This commit is contained in:
jacobxu666
2026-06-14 17:16:18 +08:00
commit 046e84a564
37 changed files with 13763 additions and 0 deletions
+22
View File
@@ -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?
+19
View File
@@ -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
}
})
+15
View File
@@ -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"
}
+10
View File
@@ -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
View File
@@ -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>
+8810
View File
File diff suppressed because it is too large Load Diff
+73
View File
@@ -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"
}
}
+49
View File
@@ -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
})
}
})
},
})
+4
View File
@@ -0,0 +1,4 @@
{
"usingComponents": {
}
}
+27
View File
@@ -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>
+62
View File
@@ -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;
}
+41
View File
@@ -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": {}
}
+22
View File
@@ -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"
}
+10
View File
@@ -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 {
}
}
+7
View File
@@ -0,0 +1,7 @@
{
"desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html",
"rules": [{
"action": "allow",
"page": "*"
}]
}
+33
View File
@@ -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>
+2
View File
@@ -0,0 +1,2 @@
export * from './user'
export * from './request'
+103
View File
@@ -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
}
+25
View File
@@ -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')
}
+8
View File
@@ -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
View File
@@ -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,
}
}
+1
View File
@@ -0,0 +1 @@
{
+56
View File
@@ -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": "我的"
}
]
}
}
+169
View File
@@ -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>
+156
View File
@@ -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>
+239
View File
@@ -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>
+6
View File
@@ -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

+41
View File
@@ -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
}
})
+76
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: #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;
+44
View File
@@ -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
}
+13
View File
@@ -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"]
}
+19
View File
@@ -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
}
+7
View File
@@ -0,0 +1,7 @@
import { defineConfig } from "vite";
import uni from "@dcloudio/vite-plugin-uni";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [uni()],
});