@ -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/) |
||||||
@ -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 |
||||||
@ -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", |
||||||
|
], |
||||||
|
], |
||||||
|
} |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
export default { |
||||||
|
env: { |
||||||
|
NODE_ENV: '"development"' |
||||||
|
}, |
||||||
|
defineConstants: { |
||||||
|
}, |
||||||
|
mini: {}, |
||||||
|
h5: {} |
||||||
|
} |
||||||
@ -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) |
||||||
|
}) |
||||||
@ -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') })
|
||||||
|
// }))
|
||||||
|
// }
|
||||||
|
} |
||||||
|
} |
||||||
@ -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" |
||||||
|
} |
||||||
|
} |
||||||
@ -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": {} |
||||||
|
} |
||||||
@ -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: '你的位置信息将用于查找附近的台球馆', |
||||||
|
}, |
||||||
|
}, |
||||||
|
}) |
||||||
@ -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; |
||||||
|
} |
||||||
@ -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 |
||||||
@ -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' |
||||||
|
// ... |
||||||
|
``` |
||||||
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 7.8 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
@ -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 |
||||||
@ -0,0 +1,4 @@ |
|||||||
|
export default { |
||||||
|
component: true, |
||||||
|
usingComponents: {}, |
||||||
|
} |
||||||
@ -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; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -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 |
||||||
@ -0,0 +1,4 @@ |
|||||||
|
export default { |
||||||
|
navigationBarTitleText: '首页', |
||||||
|
usingComponents: {}, |
||||||
|
} |
||||||
@ -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; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -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 |
||||||
@ -0,0 +1,4 @@ |
|||||||
|
export default { |
||||||
|
navigationBarTitleText: '消息', |
||||||
|
usingComponents: {}, |
||||||
|
} |
||||||
@ -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; |
||||||
|
} |
||||||
|
} |
||||||
@ -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 |
||||||
@ -0,0 +1,4 @@ |
|||||||
|
export default { |
||||||
|
navigationBarTitleText: '扫码', |
||||||
|
usingComponents: {}, |
||||||
|
} |
||||||
@ -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; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -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 |
||||||
@ -0,0 +1,4 @@ |
|||||||
|
export default { |
||||||
|
navigationBarTitleText: '门店', |
||||||
|
usingComponents: {}, |
||||||
|
} |
||||||
@ -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; |
||||||
|
} |
||||||
|
} |
||||||
@ -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 |
||||||
@ -0,0 +1,4 @@ |
|||||||
|
export default { |
||||||
|
navigationBarTitleText: '我的', |
||||||
|
usingComponents: {}, |
||||||
|
} |
||||||
@ -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; |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -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 |
||||||
@ -0,0 +1,12 @@ |
|||||||
|
/** |
||||||
|
* Store 统一导出 - MobX |
||||||
|
*/ |
||||||
|
import { userStore as _userStore } from './user' |
||||||
|
|
||||||
|
// 重新导出
|
||||||
|
export const userStore = _userStore |
||||||
|
|
||||||
|
// 导出所有 store 的集合
|
||||||
|
export const stores = { |
||||||
|
userStore: _userStore, |
||||||
|
} |
||||||
@ -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() |
||||||
@ -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"] |
||||||
|
} |
||||||
@ -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 |
||||||