Browse Source

基本架构

master
DU 6 months ago
commit
ec544f37fa
  1. 94
      .cursorrules
  2. 25
      .gitignore
  3. BIN
      .swc/plugins/v7_windows_x86_64_0.104.8/1aec8de2802fda0360cd33b8c5db0b5f65f02c52b36e8dcc575625e06fc60bf0
  4. 113
      README.md
  5. 35
      babel.config.js
  6. 9
      config/dev.ts
  7. 80
      config/index.ts
  8. 36
      config/prod.ts
  9. 20269
      package-lock.json
  10. 79
      package.json
  11. 47
      project.config.json
  12. 51
      src/app.config.ts
  13. 66
      src/app.scss
  14. 27
      src/app.ts
  15. 49
      src/assets/README.md
  16. BIN
      src/assets/home.png
  17. BIN
      src/assets/home_s.png
  18. BIN
      src/assets/ms.png
  19. BIN
      src/assets/ms_s.png
  20. BIN
      src/assets/my.png
  21. BIN
      src/assets/my_s.png
  22. BIN
      src/assets/scan_s.png
  23. BIN
      src/assets/store.png
  24. BIN
      src/assets/store_s.png
  25. 37
      src/constants/config.ts
  26. 4
      src/custom-tab-bar/index.json.js
  27. 200
      src/custom-tab-bar/index.scss
  28. 110
      src/custom-tab-bar/index.tsx
  29. 4
      src/pages/index/index.config.ts
  30. 44
      src/pages/index/index.scss
  31. 43
      src/pages/index/index.tsx
  32. 4
      src/pages/message/index.config.ts
  33. 19
      src/pages/message/index.scss
  34. 28
      src/pages/message/index.tsx
  35. 4
      src/pages/scan/index.config.ts
  36. 31
      src/pages/scan/index.scss
  37. 43
      src/pages/scan/index.tsx
  38. 4
      src/pages/store/index.config.ts
  39. 19
      src/pages/store/index.scss
  40. 28
      src/pages/store/index.tsx
  41. 4
      src/pages/user/index.config.ts
  42. 57
      src/pages/user/index.scss
  43. 38
      src/pages/user/index.tsx
  44. 12
      src/store/index.ts
  45. 45
      src/store/user.ts
  46. 28
      tsconfig.json
  47. 20
      types/global.d.ts

94
.cursorrules

@ -0,0 +1,94 @@ @@ -0,0 +1,94 @@
# Billiard App - 台球馆小程序开发规范
## 项目技术栈
- **框架**: Taro 4.0.9
- **UI 框架**: React 18
- **语言**: TypeScript
- **状态管理**: MobX 6
- **UI 组件库**: @taroify/core 0.9.0
- **样式**: SCSS
- **构建工具**: Webpack 5
- **包管理器**: npm
## 项目结构
```
billiard-app/
├── src/
│ ├── pages/ # 页面目录
│ │ ├── index/ # 首页
│ │ ├── store/ # 门店列表
│ │ ├── scan/ # 扫码
│ │ ├── message/ # 消息
│ │ └── user/ # 个人中心
│ ├── store/ # MobX 状态管理
│ ├── custom-tab-bar/ # 自定义 TabBar
│ ├── constants/ # 常量配置
│ ├── app.ts # 入口文件
│ └── app.config.ts # 全局配置
├── config/ # 编译配置
├── babel.config.js # Babel 配置
└── package.json # 项目依赖
```
## 代码规范
### 1. MobX 使用
所有组件必须用 `observer` 包裹才能响应状态变化:
```typescript
import { observer } from 'mobx-react'
import { userStore } from '../../store'
const MyPage = observer(() => {
const { userInfo } = userStore
return <View>{userInfo?.nickname}</View>
})
```
### 2. Taroify 组件
按需导入(已配置 babel-plugin-import):
```typescript
import { Button, Toast } from '@taroify/core'
import { HomeOutlined } from '@taroify/icons'
```
### 3. TypeScript
所有变量和函数必须有类型定义。
### 4. 样式规范
- 使用 SCSS
- designWidth 为 750
- 使用 px,会自动转换为 rpx
- 使用 BEM 命名规范
## 常用命令
```bash
# 开发
npm run dev:weapp # 微信小程序
npm run dev:h5 # H5
# 编译
npm run build:weapp # 编译微信小程序
npm run build:h5 # 编译 H5
```
## 注意事项
1. MobX 组件必须用 `observer` 包裹
2. Taroify 样式已全局引入
3. 修改 TabBar 需同时更新 `custom-tab-bar/` 和 `app.config.ts`
4. 状态持久化使用 `Taro.setStorageSync`
## 相关文档
- [Taro 官方文档](https://taro-docs.jd.com/)
- [Taroify 文档](https://taroify.github.io/taroify.com/)
- [MobX 文档](https://mobx.js.org/)

25
.gitignore vendored

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
# 依赖
node_modules/
# 构建产物
dist/
# 日志
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# 编辑器
.vscode/
.idea/
*.swp
*.swo
*~
# 系统文件
.DS_Store
Thumbs.db
# 环境变量
.env
.env.local

BIN
.swc/plugins/v7_windows_x86_64_0.104.8/1aec8de2802fda0360cd33b8c5db0b5f65f02c52b36e8dcc575625e06fc60bf0

Binary file not shown.

113
README.md

@ -0,0 +1,113 @@ @@ -0,0 +1,113 @@
# Billiard App - 台球馆自助小程序
一个基于 Taro 4.x + React + TypeScript + MobX + Taroify 的台球馆自助小程序。
## 技术栈
- ⚡ **Taro 4.0.9** - 跨平台小程序开发框架
- ⚛ **React 18** - UI 框架
- 📘 **TypeScript** - 类型安全
- 🔥 **MobX 6** - 状态管理
- 🎨 **Taroify 0.9.0** - UI 组件库
- 💅 **SCSS** - 样式预处理
- 📦 **Webpack 5** - 构建工具
## 项目特点
- ✅ TypeScript 全面支持
- ✅ MobX 响应式状态管理
- ✅ Taroify 组件库按需引入
- ✅ 5 个 TabBar 页面完整结构
- ✅ 自定义 TabBar 实现
- ✅ Webpack 5 构建
## 快速开始
### 1. 安装依赖
```bash
cd billiard-app
npm install
```
### 2. 开发
```bash
# 微信小程序
npm run dev:weapp
# H5
npm run dev:h5
```
### 3. 预览
- **微信小程序**:使用微信开发者工具打开 `dist/` 目录
- **H5**:浏览器访问开发服务器地址
### 4. 编译
```bash
# 微信小程序
npm run build:weapp
# H5
npm run build:h5
```
## 页面结构
项目包含 5 个 TabBar 页面:
| 页面 | 路径 | 说明 |
|------|------|------|
| 首页 | `/pages/index/index` | 主页面,扫码开台入口 |
| 门店 | `/pages/store/index` | 门店列表 |
| 扫码 | `/pages/scan/index` | 扫码功能(中间大按钮) |
| 消息 | `/pages/message/index` | 消息列表 |
| 我的 | `/pages/user/index` | 个人中心 |
## MobX 使用示例
```typescript
import { observer } from 'mobx-react'
import { userStore } from '../../store'
const MyPage = observer(() => {
const { userInfo, balance } = userStore
return (
<View>
<Text>{userInfo?.nickname}</Text>
<Text>余额: ¥{balance}</Text>
</View>
)
})
export default MyPage
```
## Taroify 组件使用
```typescript
import { Button, Toast, Dialog } from '@taroify/core'
import { HomeOutlined } from '@taroify/icons'
// 使用组件
<Button color="primary">按钮</Button>
<HomeOutlined size={24} />
```
## 开发规范
详见 [.cursorrules](./.cursorrules)
## 注意事项
1. **MobX 响应式**:组件必须用 `observer` 包裹
2. **样式单位**:使用 `px`,自动转换为 `rpx`
3. **TabBar 配置**:修改需同步更新配置文件
## License
MIT

35
babel.config.js

@ -0,0 +1,35 @@ @@ -0,0 +1,35 @@
// babel-preset-taro 更多选项和默认值:
// https://docs.taro.zone/docs/next/babel-config
module.exports = {
presets: [
['taro', {
framework: 'react',
ts: true,
compiler: 'webpack5',
reactRuntime: 'automatic',
}]
],
plugins: [
// Taroify 组件按需导入配置
[
"import",
{
libraryName: "@taroify/core",
libraryDirectory: "",
style: false, // 样式已在 app.ts 中全局导入
},
"@taroify/core",
],
[
"import",
{
libraryName: "@taroify/icons",
libraryDirectory: "",
camel2DashComponentName: false,
style: false,
customName: (name) => name === "Icon" ? "@taroify/icons/van/VanIcon" : `@taroify/icons/${name}`,
},
"@taroify/icons",
],
],
}

9
config/dev.ts

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
export default {
env: {
NODE_ENV: '"development"'
},
defineConstants: {
},
mini: {},
h5: {}
}

80
config/index.ts

@ -0,0 +1,80 @@ @@ -0,0 +1,80 @@
import { defineConfig, type UserConfigExport } from '@tarojs/cli'
// https://taro-docs.jd.com/docs/next/config#defineconfig-辅助函数
export default defineConfig<'webpack5'>(async (merge, { command, mode }) => {
const baseConfig: UserConfigExport<'webpack5'> = {
projectName: 'billiard-app',
date: '2026-1-15',
designWidth: 750,
deviceRatio: {
640: 2.34 / 2,
750: 1,
375: 2,
828: 1.81 / 2
},
sourceRoot: 'src',
outputRoot: 'dist',
plugins: [],
defineConstants: {
},
copy: {
patterns: [
],
options: {
}
},
framework: 'react',
compiler: 'webpack5',
mini: {
postcss: {
pxtransform: {
enable: true,
config: {
}
},
cssModules: {
enable: false,
config: {
namingPattern: 'module',
generateScopedName: '[name]__[local]___[hash:base64:5]'
}
}
},
},
h5: {
publicPath: '/',
staticDirectory: 'static',
miniCssExtractPluginOption: {
ignoreOrder: true,
filename: 'css/[name].[hash].css',
chunkFilename: 'css/[name].[chunkhash].css'
},
postcss: {
autoprefixer: {
enable: true,
config: {}
},
cssModules: {
enable: false,
config: {
namingPattern: 'module',
generateScopedName: '[name]__[local]___[hash:base64:5]'
}
}
},
},
rn: {
appName: 'billiardApp',
postcss: {
cssModules: {
enable: false,
}
}
}
}
if (process.env.NODE_ENV === 'development') {
return merge({}, baseConfig)
}
return merge({}, baseConfig)
})

36
config/prod.ts

@ -0,0 +1,36 @@ @@ -0,0 +1,36 @@
export default {
env: {
NODE_ENV: '"production"'
},
defineConstants: {
},
mini: {},
h5: {
/**
* WebpackChain
* @docs https://github.com/neutrinojs/webpack-chain
*/
// webpackChain (chain) {
// /**
// * 如果 h5 端编译后体积过大,可以使用 webpack-bundle-analyzer 插件对打包体积进行分析。
// * @docs https://github.com/webpack-contrib/webpack-bundle-analyzer
// */
// chain.plugin('analyzer')
// .use(require('webpack-bundle-analyzer').BundleAnalyzerPlugin, [])
// /**
// * 如果 h5 端首屏加载时间过长,可以使用 prerender-spa-plugin 插件预加载首页。
// * @docs https://github.com/chrisvfritz/prerender-spa-plugin
// */
// const path = require('path')
// const Prerender = require('prerender-spa-plugin')
// const staticDir = path.join(__dirname, '..', 'dist')
// chain
// .plugin('prerender')
// .use(new Prerender({
// staticDir,
// routes: [ '/pages/index/index' ],
// postProcess: (context) => ({ ...context, outputPath: path.join(staticDir, 'index.html') })
// }))
// }
}
}

20269
package-lock.json generated

File diff suppressed because it is too large Load Diff

79
package.json

@ -0,0 +1,79 @@ @@ -0,0 +1,79 @@
{
"name": "billiard-app",
"version": "1.0.0",
"private": true,
"description": "台球馆自助小程序 - React + TypeScript + MobX + Taroify",
"templateInfo": {
"name": "default",
"typescript": true,
"css": "Sass",
"framework": "React"
},
"scripts": {
"build:weapp": "taro build --type weapp",
"build:swan": "taro build --type swan",
"build:alipay": "taro build --type alipay",
"build:tt": "taro build --type tt",
"build:h5": "taro build --type h5",
"build:rn": "taro build --type rn",
"build:qq": "taro build --type qq",
"build:jd": "taro build --type jd",
"dev:weapp": "npm run build:weapp -- --watch",
"dev:swan": "npm run build:swan -- --watch",
"dev:alipay": "npm run build:alipay -- --watch",
"dev:tt": "npm run build:tt -- --watch",
"dev:h5": "npm run build:h5 -- --watch",
"dev:rn": "npm run build:rn -- --watch",
"dev:qq": "npm run build:qq -- --watch",
"dev:jd": "npm run build:jd -- --watch"
},
"browserslist": [
"defaults and fully supports es6-module",
"maintained node versions"
],
"author": "",
"dependencies": {
"@babel/runtime": "^7.24.4",
"@taroify/core": "^0.9.0",
"@taroify/icons": "^0.9.0",
"@tarojs/components": "4.0.9",
"@tarojs/helper": "4.0.9",
"@tarojs/plugin-framework-react": "4.0.9",
"@tarojs/plugin-platform-alipay": "4.0.9",
"@tarojs/plugin-platform-h5": "4.0.9",
"@tarojs/plugin-platform-jd": "4.0.9",
"@tarojs/plugin-platform-qq": "4.0.9",
"@tarojs/plugin-platform-swan": "4.0.9",
"@tarojs/plugin-platform-tt": "4.0.9",
"@tarojs/plugin-platform-weapp": "4.0.9",
"@tarojs/react": "4.0.9",
"@tarojs/runtime": "4.0.9",
"@tarojs/shared": "4.0.9",
"@tarojs/taro": "4.0.9",
"mobx": "^6.15.0",
"mobx-react": "^9.2.1",
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"devDependencies": {
"@babel/core": "^7.24.4",
"@babel/plugin-proposal-class-properties": "7.14.5",
"@babel/preset-react": "^7.24.1",
"@tarojs/cli": "4.0.9",
"@tarojs/webpack5-runner": "4.0.9",
"@types/react": "^18.0.0",
"@types/webpack-env": "^1.18.0",
"babel-plugin-import": "^1.13.8",
"babel-preset-taro": "4.0.9",
"eslint": "^8.57.0",
"eslint-config-taro": "4.0.9",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.4.0",
"postcss": "^8.4.38",
"sass": "^1.75.0",
"sass-loader": "^13.3.0",
"stylelint": "^16.4.0",
"typescript": "^5.4.5",
"webpack": "^5.88.0"
}
}

47
project.config.json

@ -0,0 +1,47 @@ @@ -0,0 +1,47 @@
{
"miniprogramRoot": "dist/",
"projectname": "billiard-app",
"description": "台球馆自助小程序",
"appid": "touristappid",
"setting": {
"urlCheck": false,
"es6": true,
"enhance": true,
"postcss": true,
"preloadBackgroundData": false,
"minified": true,
"newFeature": false,
"coverView": true,
"nodeModules": false,
"autoAudits": false,
"showShadowRootInWxmlPanel": true,
"scopeDataCheck": false,
"uglifyFileName": false,
"checkInvalidKey": true,
"checkSiteMap": true,
"uploadWithSourceMap": true,
"compileHotReLoad": false,
"lazyloadPlaceholderEnable": false,
"useMultiFrameRuntime": true,
"useApiHook": true,
"useApiHostProcess": true,
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
},
"enableEngineNative": false,
"useIsolateContext": true,
"userConfirmedBundleSwitch": false,
"packNpmManually": false,
"packNpmRelationList": [],
"minifyWXSS": true,
"disableUseStrict": false,
"minifyWXML": true,
"showES6CompileOption": false,
"useCompilerPlugins": false
},
"compileType": "miniprogram",
"libVersion": "2.19.4",
"condition": {}
}

51
src/app.config.ts

@ -0,0 +1,51 @@ @@ -0,0 +1,51 @@
export default defineAppConfig({
pages: [
'pages/index/index',
'pages/store/index',
'pages/scan/index',
'pages/message/index',
'pages/user/index',
],
window: {
backgroundTextStyle: 'light',
navigationBarBackgroundColor: '#fff',
navigationBarTitleText: '台球馆',
navigationBarTextStyle: 'black',
},
// 自定义 TabBar
tabBar: {
custom: true,
color: '#999',
selectedColor: '#fff',
backgroundColor: '#121212',
borderStyle: 'black',
list: [
{
pagePath: 'pages/index/index',
text: '首页',
},
{
pagePath: 'pages/store/index',
text: '门店',
},
{
pagePath: 'pages/scan/index',
text: '扫码',
},
{
pagePath: 'pages/message/index',
text: '消息',
},
{
pagePath: 'pages/user/index',
text: '我的',
},
],
},
// 权限配置
permission: {
'scope.userLocation': {
desc: '你的位置信息将用于查找附近的台球馆',
},
},
})

66
src/app.scss

@ -0,0 +1,66 @@ @@ -0,0 +1,66 @@
/**
* 全局样式
*/
/* 重置样式 */
page {
background-color: #f5f5f5;
font-size: 28px;
line-height: 1.6;
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', Helvetica, Segoe UI, Arial, Roboto, 'PingFang SC', 'miui', 'Hiragino Sans GB', 'Microsoft Yahei', sans-serif;
}
/* 通用样式 */
.container {
min-height: 100vh;
padding-bottom: calc(env(safe-area-inset-bottom) + 100px);
}
/* 文本溢出省略 */
.ellipsis {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ellipsis-2 {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
}
/* 清除浮动 */
.clearfix::after {
content: '';
display: block;
clear: both;
}
/* Flex 布局 */
.flex {
display: flex;
}
.flex-center {
display: flex;
align-items: center;
justify-content: center;
}
.flex-between {
display: flex;
align-items: center;
justify-content: space-between;
}
/* 占位图 */
.placeholder {
display: flex;
align-items: center;
justify-content: center;
min-height: 400px;
color: #999;
font-size: 28px;
}

27
src/app.ts

@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
import { PropsWithChildren } from 'react'
import { useLaunch } from '@tarojs/taro'
import Taro from '@tarojs/taro'
import { userStore } from './store'
import './app.scss'
// 导入 taroify 全局样式
import '@taroify/core/index.scss'
function App({ children }: PropsWithChildren<any>) {
useLaunch(() => {
console.log('台球馆小程序启动')
// 初始化:从本地存储恢复用户信息
try {
const savedUserInfo = Taro.getStorageSync('userInfo')
if (savedUserInfo) {
userStore.setUserInfo(savedUserInfo)
}
} catch (error) {
console.warn('初始化用户信息失败:', error)
}
})
return children
}
export default App

49
src/assets/README.md

@ -0,0 +1,49 @@ @@ -0,0 +1,49 @@
# 图标资源说明
## 占位图标
当前使用 `home_s.png` 作为所有 TabBar 图标的占位图。
## 需要替换的图标
请准备以下图标(建议尺寸 48x48px 或 96x96px @2x):
### 首页
- `home.png` - 未选中状态
- `home_s.png` - 选中状态
### 门店
- `store.png` - 未选中状态
- `store_s.png` - 选中状态
### 扫码(中间大按钮)
- `scan.png` - 扫码图标(建议 56x56px)
### 消息
- `message.png` - 未选中状态
- `message_s.png` - 选中状态
### 我的
- `user.png` - 未选中状态
- `user_s.png` - 选中状态
## 图标规范
- 格式:PNG(支持透明背景)
- 尺寸:48x48px(普通屏)/ 96x96px(@2x 高清屏)
- 颜色:
- 未选中:#999999 或 #666666
- 选中:#667eea(主题色)
- 风格:线性图标,简洁清晰
## 使用方法
替换图标后,在 `custom-tab-bar/index.tsx` 中导入:
```typescript
import homeIcon from '../assets/home.png'
import homeActiveIcon from '../assets/home_s.png'
import storeIcon from '../assets/store.png'
import storeActiveIcon from '../assets/store_s.png'
// ...
```

BIN
src/assets/home.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

BIN
src/assets/home_s.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
src/assets/ms.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
src/assets/ms_s.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

BIN
src/assets/my.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

BIN
src/assets/my_s.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

BIN
src/assets/scan_s.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
src/assets/store.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

BIN
src/assets/store_s.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

37
src/constants/config.ts

@ -0,0 +1,37 @@ @@ -0,0 +1,37 @@
/**
*
*/
// TabBar 配置
export const TAB_BAR_CONFIG = {
HOME: {
path: '/pages/index/index',
text: '首页',
},
STORE: {
path: '/pages/store/index',
text: '门店',
},
SCAN: {
path: '/pages/scan/index',
text: '扫码',
isCenter: true,
},
MESSAGE: {
path: '/pages/message/index',
text: '消息',
},
USER: {
path: '/pages/user/index',
text: '我的',
},
} as const
// 页面路径
export const PAGE_PATHS = {
HOME: '/pages/index/index',
STORE: '/pages/store/index',
SCAN: '/pages/scan/index',
MESSAGE: '/pages/message/index',
USER: '/pages/user/index',
} as const

4
src/custom-tab-bar/index.json.js

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
export default {
component: true,
usingComponents: {},
}

200
src/custom-tab-bar/index.scss

@ -0,0 +1,200 @@ @@ -0,0 +1,200 @@
/**
* 自定义 TabBar 样式 - 中间凸起
* 参考https://developer.aliyun.com/article/1228635
* 已适配安全区域iPhone X 等刘海屏
*
* 注意Taro px 会被转换为 rpx使用 Px (大写P) 可避免转换
*/
.custom-tab-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
width: 100%;
height: 100Px; /* 继续减小高度,给凸起留足空间 */
z-index: 9999;
pointer-events: none;
// 背景白色层 #0E0D0D
.tab-bar-bg {
position: absolute;
top: 50px;
bottom: 0;
left: 0;
right: 0;
// height: 10Px; /* TabBar 实际高度 */
background: #0E0D0D;
box-shadow: 0 -2Px 6Px rgba(0, 0, 0, 0.05);
z-index: 1;
pointer-events: auto;
}
// 中间凸起的圆形白色背景
.tab-bar-center-bg {
position: absolute;
bottom: 25Px; /* 凸起高度 - 确保顶部不被截取 */
left: 50%;
transform: translateX(-50%);
width: 120Px;
height: 120Px;
background: #ffffff;
border-radius: 50%;
// box-shadow: 0 -2Px 8Px rgba(0, 0, 0, 0.08);
z-index: 2;
pointer-events: auto;
}
// TabBar 内容层
.tab-bar-content {
position: absolute;
bottom: 0;
left: 0;
right: 0;
// top: -60px;
display: flex;
align-items: flex-end;
justify-content: space-around;
height: 100%;
padding-bottom: 5Px;
padding-bottom: calc(5Px + env(safe-area-inset-bottom));
z-index: 3;
pointer-events: auto;
overflow: visible; /* 重要:允许子元素溢出显示 */
}
// Tab
.tab-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-end;
height: 70Px; /* 普通项高度 */
position: relative;
top: 30px;
pointer-events: auto;
// 普通 Tab 项内容
.tab-content {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding-bottom: 4Px;
.tab-icon {
width: 41px;
height: 41px;
margin-bottom: 2Px;
}
.tab-text {
font-size: 20px;
color: #999999;
// line-height: 28Px;
}
}
// 选中状态
&.active {
.tab-text {
color: #ffffff;
font-weight: 500;
}
}
// 中间扫码按钮 - 凸起样式
&.center {
height: 100%; /* 占满整个容器 */
justify-content: flex-end;
.scan-wrapper {
position: absolute;
bottom: 10Px; /* 调整位置让按钮向上凸起,同时留出底部空间 */
left: 50%;
transform: translateX(-50%);
z-index: 10;
.scan-button {
width: 129px;
height: 129px;
border-radius: 50%;
// background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4Px 12Px rgba(102, 126, 234, 0.4);
// border: 5Px solid #ffffff;
.scan-icon {
width: 129px;
height: 129px;
}
}
}
}
}
}
// iPhone X 及以上设备特殊适配
@supports (bottom: env(safe-area-inset-bottom)) {
.custom-tab-bar {
.tab-bar-bg {
// 使用 constant 作为 env 的降级方案iOS 11.0-11.2
height: calc(60Px + constant(safe-area-inset-bottom));
height: calc(60Px + env(safe-area-inset-bottom));
}
}
}
// 适配不同屏幕尺寸
@media screen and (max-width: 320Px) {
.custom-tab-bar {
height: 90Px;
.tab-bar-bg {
height: 55Px;
}
.tab-bar-center-bg {
width: 65Px;
height: 65Px;
bottom: 22Px;
}
.tab-item {
height: 55Px;
&.center .scan-wrapper {
bottom: 10Px;
.scan-button {
width: 58Px;
height: 58Px;
border-width: 4Px;
.scan-icon {
width: 34Px;
height: 34Px;
}
}
}
}
.tab-item .tab-content {
.tab-icon {
width: 28Px;
height: 28Px;
}
.tab-text {
font-size: 20px;
}
}
.tab-item.active .tab-content .tab-text {
color: #ffffff;
}
}
}

110
src/custom-tab-bar/index.tsx

@ -0,0 +1,110 @@ @@ -0,0 +1,110 @@
/**
* TabBar -
* https://developer.aliyun.com/article/1228635
*/
import { CoverView, CoverImage } from '@tarojs/components'
import Taro from '@tarojs/taro'
import { observer } from 'mobx-react'
import { TAB_BAR_CONFIG } from '../constants/config'
import './index.scss'
// 导入图标资源
import homeIcon from '../assets/home.png'
import homeActiveIcon from '../assets/home_s.png'
import storeIcon from '../assets/store.png'
import storeActiveIcon from '../assets/store_s.png'
import scanIcon from '../assets/scan_s.png'
import messageIcon from '../assets/ms.png'
import messageActiveIcon from '../assets/ms_s.png'
import userIcon from '../assets/my.png'
import userActiveIcon from '../assets/my_s.png'
// 图标映射配置
const TAB_ICONS = {
'/pages/index/index': { normal: homeIcon, active: homeActiveIcon },
'/pages/store/index': { normal: storeIcon, active: storeActiveIcon },
'/pages/scan/index': { normal: scanIcon, active: scanIcon },
'/pages/message/index': { normal: messageIcon, active: messageActiveIcon },
'/pages/user/index': { normal: userIcon, active: userActiveIcon },
}
const TAB_BAR_ITEMS = [
TAB_BAR_CONFIG.HOME,
TAB_BAR_CONFIG.STORE,
TAB_BAR_CONFIG.SCAN,
TAB_BAR_CONFIG.MESSAGE,
TAB_BAR_CONFIG.USER,
]
const CustomTabBar = observer(() => {
const pages = Taro.getCurrentPages()
const currentPage = pages[pages.length - 1]
const currentPath = '/' + currentPage.route
const handleTabClick = (path: string) => {
if (path === currentPath) return
Taro.switchTab({
url: path,
fail: () => {
Taro.navigateTo({ url: path })
},
})
}
// 获取图标
const getIcon = (path: string, isActive: boolean) => {
const icons = TAB_ICONS[path]
if (!icons) return homeIcon
return isActive ? icons.active : icons.normal
}
return (
<CoverView className="custom-tab-bar">
{/* 背景白色层 */}
<CoverView className="tab-bar-bg" />
{/* 中间凸起的圆形背景 */}
{/* <CoverView className="tab-bar-center-bg" /> */}
{/* TabBar 项目 */}
<CoverView className="tab-bar-content">
{TAB_BAR_ITEMS.map((item, index) => {
const isActive = currentPath === item.path
const isCenter = 'isCenter' in item && item.isCenter
return (
<CoverView
key={item.path}
className={`tab-item ${isActive ? 'active' : ''} ${isCenter ? 'center' : ''}`}
onClick={() => handleTabClick(item.path)}
>
{isCenter ? (
// 中间扫码按钮 - 凸起样式
<CoverView className="scan-wrapper">
<CoverView className="scan-button">
<CoverImage
className="scan-icon"
src={scanIcon}
/>
</CoverView>
</CoverView>
) : (
// 普通 Tab 项
<CoverView className="tab-content">
<CoverImage
className="tab-icon"
src={getIcon(item.path, isActive)}
/>
<CoverView className="tab-text">{item.text}</CoverView>
</CoverView>
)}
</CoverView>
)
})}
</CoverView>
</CoverView>
)
})
export default CustomTabBar

4
src/pages/index/index.config.ts

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
export default {
navigationBarTitleText: '首页',
usingComponents: {},
}

44
src/pages/index/index.scss

@ -0,0 +1,44 @@ @@ -0,0 +1,44 @@
.index-page {
min-height: 100vh;
// background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
.header {
padding: 80px 32px 40px;
text-align: center;
.title {
font-size: 48px;
font-weight: bold;
color: #fff;
}
}
.content {
padding: 32px;
.welcome {
background: #fff;
border-radius: 16px;
padding: 40px 32px;
text-align: center;
margin-bottom: 40px;
.welcome-text {
font-size: 32px;
color: #333;
display: block;
margin-bottom: 16px;
}
.user-name {
font-size: 36px;
font-weight: bold;
color: #667eea;
}
}
.actions {
margin-top: 40px;
}
}
}

43
src/pages/index/index.tsx

@ -0,0 +1,43 @@ @@ -0,0 +1,43 @@
/**
*
*/
import { View, Text } from '@tarojs/components'
import { useLoad } from '@tarojs/taro'
import { observer } from 'mobx-react'
import { Button } from '@taroify/core'
import { userStore } from '../../store'
import './index.scss'
const Index = observer(() => {
useLoad(() => {
console.log('首页加载')
})
return (
<View className="index-page">
<View className="header">
<Text className="title"></Text>
</View>
<View className="content">
<View className="welcome">
<Text className="welcome-text">
使
</Text>
{userStore.userInfo && (
<Text className="user-name">{userStore.userInfo.nickname}</Text>
)}
</View>
<View className="actions">
<Button color="primary" block></Button>
<Button color="success" block style={{ marginTop: '20px' }}>
</Button>
</View>
</View>
</View>
)
})
export default Index

4
src/pages/message/index.config.ts

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
export default {
navigationBarTitleText: '消息',
usingComponents: {},
}

19
src/pages/message/index.scss

@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
.message-page {
min-height: 100vh;
background-color: #f5f5f5;
.header {
padding: 32px;
background: #fff;
.title {
font-size: 36px;
font-weight: bold;
color: #333;
}
}
.content {
padding: 32px;
}
}

28
src/pages/message/index.tsx

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
/**
*
*/
import { View, Text } from '@tarojs/components'
import { useLoad } from '@tarojs/taro'
import { observer } from 'mobx-react'
import './index.scss'
const MessageIndex = observer(() => {
useLoad(() => {
console.log('消息页面加载')
})
return (
<View className="message-page">
<View className="header">
<Text className="title"></Text>
</View>
<View className="content">
<View className="placeholder">
<Text>...</Text>
</View>
</View>
</View>
)
})
export default MessageIndex

4
src/pages/scan/index.config.ts

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
export default {
navigationBarTitleText: '扫码',
usingComponents: {},
}

31
src/pages/scan/index.scss

@ -0,0 +1,31 @@ @@ -0,0 +1,31 @@
.scan-page {
min-height: 100vh;
background-color: #f5f5f5;
.header {
padding: 32px;
background: #fff;
.title {
font-size: 36px;
font-weight: bold;
color: #333;
}
}
.content {
padding: 80px 32px;
text-align: center;
.scan-btn {
width: 400px;
height: 400px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: #fff;
font-size: 48px;
font-weight: bold;
border: none;
}
}
}

43
src/pages/scan/index.tsx

@ -0,0 +1,43 @@ @@ -0,0 +1,43 @@
/**
*
*/
import { View, Text, Button } from '@tarojs/components'
import Taro, { useLoad } from '@tarojs/taro'
import { observer } from 'mobx-react'
import './index.scss'
const ScanIndex = observer(() => {
useLoad(() => {
console.log('扫码页面加载')
})
const handleScan = () => {
Taro.scanCode({
success: (res) => {
Taro.showToast({
title: '扫码成功',
icon: 'success',
})
console.log('扫码结果:', res)
},
fail: (err) => {
console.error('扫码失败:', err)
}
})
}
return (
<View className="scan-page">
<View className="header">
<Text className="title"></Text>
</View>
<View className="content">
<Button className="scan-btn" onClick={handleScan}>
</Button>
</View>
</View>
)
})
export default ScanIndex

4
src/pages/store/index.config.ts

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
export default {
navigationBarTitleText: '门店',
usingComponents: {},
}

19
src/pages/store/index.scss

@ -0,0 +1,19 @@ @@ -0,0 +1,19 @@
.store-page {
min-height: 100vh;
background-color: #f5f5f5;
.header {
padding: 32px;
background: #fff;
.title {
font-size: 36px;
font-weight: bold;
color: #333;
}
}
.content {
padding: 32px;
}
}

28
src/pages/store/index.tsx

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
/**
*
*/
import { View, Text } from '@tarojs/components'
import { useLoad } from '@tarojs/taro'
import { observer } from 'mobx-react'
import './index.scss'
const StoreIndex = observer(() => {
useLoad(() => {
console.log('门店页面加载')
})
return (
<View className="store-page">
<View className="header">
<Text className="title"></Text>
</View>
<View className="content">
<View className="placeholder">
<Text>...</Text>
</View>
</View>
</View>
)
})
export default StoreIndex

4
src/pages/user/index.config.ts

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
export default {
navigationBarTitleText: '我的',
usingComponents: {},
}

57
src/pages/user/index.scss

@ -0,0 +1,57 @@ @@ -0,0 +1,57 @@
.user-page {
min-height: 100vh;
background-color: #f5f5f5;
.header {
padding: 60px 32px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
text-align: center;
.avatar {
width: 120px;
height: 120px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
margin: 0 auto 20px;
display: flex;
align-items: center;
justify-content: center;
.avatar-text {
font-size: 48px;
color: #fff;
font-weight: bold;
}
}
.nickname {
font-size: 32px;
color: #fff;
font-weight: 500;
}
}
.content {
padding: 32px;
.info-card {
background: #fff;
border-radius: 16px;
padding: 32px;
display: flex;
justify-content: space-between;
align-items: center;
.label {
font-size: 28px;
color: #666;
}
.value {
font-size: 36px;
font-weight: bold;
color: #667eea;
}
}
}
}

38
src/pages/user/index.tsx

@ -0,0 +1,38 @@ @@ -0,0 +1,38 @@
/**
*
*/
import { View, Text } from '@tarojs/components'
import { useLoad } from '@tarojs/taro'
import { observer } from 'mobx-react'
import { userStore } from '../../store'
import './index.scss'
const UserIndex = observer(() => {
useLoad(() => {
console.log('个人中心页面加载')
})
return (
<View className="user-page">
<View className="header">
<View className="avatar">
<Text className="avatar-text">
{userStore.userInfo?.nickname?.[0] || '未'}
</Text>
</View>
<Text className="nickname">
{userStore.userInfo?.nickname || '未登录'}
</Text>
</View>
<View className="content">
<View className="info-card">
<Text className="label"></Text>
<Text className="value">¥ {userStore.balance.toFixed(2)}</Text>
</View>
</View>
</View>
)
})
export default UserIndex

12
src/store/index.ts

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
/**
* Store - MobX
*/
import { userStore as _userStore } from './user'
// 重新导出
export const userStore = _userStore
// 导出所有 store 的集合
export const stores = {
userStore: _userStore,
}

45
src/store/user.ts

@ -0,0 +1,45 @@ @@ -0,0 +1,45 @@
/**
* - MobX
*/
import { makeAutoObservable } from 'mobx'
import Taro from '@tarojs/taro'
export interface UserInfo {
id: string
nickname: string
avatar?: string
phone?: string
}
class UserStore {
userInfo: UserInfo | null = null
isLoggedIn: boolean = false
balance: number = 0
constructor() {
makeAutoObservable(this)
}
setUserInfo = (userInfo: UserInfo | null) => {
this.userInfo = userInfo
this.isLoggedIn = !!userInfo
if (userInfo) {
Taro.setStorageSync('userInfo', userInfo)
} else {
Taro.removeStorageSync('userInfo')
}
}
setBalance = (balance: number) => {
this.balance = balance
}
logout = () => {
this.setUserInfo(null)
Taro.removeStorageSync('token')
Taro.removeStorageSync('userInfo')
}
}
export const userStore = new UserStore()

28
tsconfig.json

@ -0,0 +1,28 @@ @@ -0,0 +1,28 @@
{
"compilerOptions": {
"target": "ES2017",
"module": "ESNext",
"lib": ["ESNext", "DOM"],
"jsx": "react-jsx",
"useDefineForClassFields": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"typeRoots": [
"node_modules/@types",
"types"
],
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src", "types"],
"exclude": ["node_modules", "dist"]
}

20
types/global.d.ts vendored

@ -0,0 +1,20 @@ @@ -0,0 +1,20 @@
/// <reference types="@tarojs/taro" />
declare module '*.png'
declare module '*.gif'
declare module '*.jpg'
declare module '*.jpeg'
declare module '*.svg'
declare module '*.css'
declare module '*.less'
declare module '*.scss'
declare module '*.sass'
declare module '*.styl'
declare namespace NodeJS {
interface ProcessEnv {
TARO_ENV: 'weapp' | 'swan' | 'alipay' | 'h5' | 'rn' | 'tt' | 'quickapp' | 'qq' | 'jd'
}
}
declare function defineAppConfig(config: import('@tarojs/taro').Config): import('@tarojs/taro').Config
Loading…
Cancel
Save