first commit
@@ -0,0 +1,12 @@
|
|||||||
|
# http://editorconfig.org
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": ["taro/react"],
|
||||||
|
"rules": {
|
||||||
|
"react/jsx-uses-react": "off",
|
||||||
|
"react/react-in-jsx-scope": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
dist/
|
||||||
|
weapp/
|
||||||
|
alipay/
|
||||||
|
kwai/
|
||||||
|
dd/
|
||||||
|
qq/
|
||||||
|
tt/
|
||||||
|
swan/
|
||||||
|
deploy_versions/
|
||||||
|
.swc/
|
||||||
|
.temp/
|
||||||
|
.rn_temp/
|
||||||
|
node_modules/
|
||||||
|
.DS_Store
|
||||||
|
.idea
|
||||||
|
build/
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
registry "https://registry.npm.taobao.org"
|
||||||
|
disturl "https://npm.taobao.org/dist"
|
||||||
|
sass_binary_site "https://npm.taobao.org/mirrors/node-sass/"
|
||||||
|
sentrycli_cdnurl "https://npm.taobao.org/mirrors/sentry-cli/"
|
||||||
|
electron_mirror "https://npm.taobao.org/mirrors/electron/"
|
||||||
|
phantomjs_cdnurl "https://npm.taobao.org/mirrors/phantomjs/"
|
||||||
|
chromedriver_cdnurl "https://npm.taobao.org/mirrors/chromedriver/"
|
||||||
|
canvas_binary_host_mirror "https://npm.taobao.org/mirrors/node-canvas-prebuilt/"
|
||||||
|
operadriver_cdnurl "https://npm.taobao.org/mirrors/operadriver"
|
||||||
|
selenium_cdnurl "https://npm.taobao.org/mirrors/selenium"
|
||||||
|
node_inspector_cdnurl "https://npm.taobao.org/mirrors/node-inspector"
|
||||||
|
fsevents_binary_host_mirror "http://npm.taobao.org/mirrors/fsevents/"
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Build & Development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# WeChat mini-program
|
||||||
|
npm run dev:weapp # Watch/dev build → weapp/
|
||||||
|
npm run build:weapp # Production build → weapp/
|
||||||
|
|
||||||
|
# H5 (web)
|
||||||
|
npm run dev:h5 # Watch/dev build → build/
|
||||||
|
npm run build:h5 # Production build → build/
|
||||||
|
```
|
||||||
|
|
||||||
|
Output directories are determined by `TARO_ENV`: for H5 it's `build/`, for all other platforms it matches the platform name (e.g., `weapp/`).
|
||||||
|
|
||||||
|
No test or lint scripts are configured. ESLint (`eslint-config-taro`) and Stylelint are installed but must be run manually.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
This is a **Taro 3.6** cross-platform app using **React 18** (concurrent mode), **TypeScript**, and **Webpack 5**. It targets WeChat, Alipay, DingTalk, JD, Kwai, QQ, Baidu, Toutiao mini-programs, and H5 web.
|
||||||
|
|
||||||
|
### UI Library: `@antmjs/vantui`
|
||||||
|
|
||||||
|
The UI library is `@antmjs/vantui` (v3), a Taro-compatible port of Vant. Components are imported from `@antmjs/vantui` directly (e.g., `import { Button } from '@antmjs/vantui'`). The `babel-plugin-import` in `babel.config.js` handles on-demand style imports.
|
||||||
|
|
||||||
|
### Page Structure
|
||||||
|
|
||||||
|
Each page lives in `src/pages/<name>/` and consists of three files:
|
||||||
|
|
||||||
|
- `index.tsx` — page component (functional or class)
|
||||||
|
- `index.config.ts` — page-level config (title, navigation style, etc.)
|
||||||
|
- `index.less` — page styles
|
||||||
|
|
||||||
|
Pages must be registered in `src/app.config.ts` under the `pages` array.
|
||||||
|
|
||||||
|
### Path Alias
|
||||||
|
|
||||||
|
`@` maps to `src/` (configured in `config/index.js`).
|
||||||
|
|
||||||
|
### Theme & Global Styles
|
||||||
|
|
||||||
|
- `src/app.less` — global app styles (imported in `app.ts`)
|
||||||
|
- `src/styles/index.less` — theme variable overrides for Vant UI. It imports `@antmjs/vantui/es/style/var.less` and uncomments/redefines Less variables to customize the theme. This file is injected into every `.less` file via Less `modifyVars` (configured in `config/index.js` under both `mini.lessLoaderOption` and `h5.lessLoaderOption`).
|
||||||
|
|
||||||
|
### Webpack Customizations (`config/webpack/`)
|
||||||
|
|
||||||
|
- **`commonChain.js`** — Overrides Taro's default `script` rule `exclude` so that Babel also processes these `node_modules` packages: `taro`, `inversify`, `@antmjs`, `react-spring`, `recoil`, `buffer`, `qrcode`. Do NOT include "taro" in the project name or all node_modules will be recompiled.
|
||||||
|
- **`miniChain.js`** — Applied to mini-program builds. Adds:
|
||||||
|
- `MiniFixPlugin` — fixes path param encoding differences between WeChat/Douyin and Alipay/DingTalk
|
||||||
|
- `GlobalFixPlugin` — polyfills the `global` variable for Alipay, DingTalk, and Baidu mini-programs
|
||||||
|
- **`h5Chain.js`** — Applied to H5 builds. Adds `H5FixPlugin` for H5 compatibility fixes.
|
||||||
|
|
||||||
|
### Build Config Layers
|
||||||
|
|
||||||
|
`config/index.js` exports a function that merges the base config with `config/development.js` or `config/production.js` based on `NODE_ENV`. Environment variables:
|
||||||
|
|
||||||
|
| Variable | Purpose | Default |
|
||||||
|
|---|---|---|
|
||||||
|
| `TARO_ENV` | Target platform (`weapp`, `h5`, `alipay`, etc.) | `weapp` |
|
||||||
|
| `NODE_ENV` | Build mode | `production` |
|
||||||
|
| `API_ENV` | API environment selector | `real` |
|
||||||
|
|
||||||
|
- `designWidth: 750` — all px values are converted to rpx at this ratio
|
||||||
|
- `@tarojs/plugin-framework-react` is configured with `reactMode: "concurrent"` (React 18 concurrent features)
|
||||||
|
- `browserslist` targets iOS 9+ for production
|
||||||
|
|
||||||
|
### App Entry
|
||||||
|
|
||||||
|
- `src/app.ts` — class-based App component, renders `this.props.children` (the current page)
|
||||||
|
- `src/app.config.ts` — defines routes (`pages` array), window config, and app-level settings
|
||||||
|
- `src/index.html` — H5 entry HTML template
|
||||||
|
|
||||||
|
## VantUI Component API Reference (v3.7)
|
||||||
|
|
||||||
|
This project uses `@antmjs/vantui` v3.7. The official docs: https://antmjs.github.io/vantui/main/
|
||||||
|
|
||||||
|
**Always check the actual type definitions** in `node_modules/@antmjs/vantui/types/` when unsure about props. Key API notes for commonly used components:
|
||||||
|
|
||||||
|
### Search
|
||||||
|
- `shape`: `'square'` (default) | `'round'`
|
||||||
|
- `background`: background color string (default `#FFFFFF`)
|
||||||
|
- `onFocus`, `onBlur`, `onChange`, `onSearch`, `onClear`, `onCancel` — event callbacks
|
||||||
|
- `leftIcon`, `rightIcon` — icon name or image URL
|
||||||
|
- `showAction` + `actionText` — right-side action button
|
||||||
|
|
||||||
|
### Swiper (NOT the same as Taro's native Swiper)
|
||||||
|
- `autoPlay` — auto-play interval in ms (0 = disabled). **Not** `autoplay`
|
||||||
|
- `loop` — infinite loop mode. **Not** `circular`
|
||||||
|
- `paginationColor` — indicator dot color. **Not** `indicatorColor`
|
||||||
|
- `paginationVisible` — show/hide pagination dots
|
||||||
|
- `duration` — animation duration (default 500)
|
||||||
|
- `width` / `height` — card dimensions
|
||||||
|
- **No** `indicatorActiveColor` prop — only one `paginationColor`
|
||||||
|
- `onChange` callback: `(currPage: number) => void` (not an event object)
|
||||||
|
|
||||||
|
### SwiperItem
|
||||||
|
- Minimal component, only `className` and `children`. No special props.
|
||||||
|
|
||||||
|
### Grid / GridItem
|
||||||
|
- `Grid`: `columnNum`, `border`, `gutter`, `square`, `clickable`, `center`, `iconSize`
|
||||||
|
- `GridItem`: `icon` (string), `text` (ReactNode), `iconColor`, `dot`, `info`/`badge`, `url` + `linkType`
|
||||||
|
|
||||||
|
### Tabs / Tab (v3.7)
|
||||||
|
- `Tabs`:
|
||||||
|
- `active` — current tab index (number | string)
|
||||||
|
- `sticky` — enable sticky mode
|
||||||
|
- `offsetTop` — sticky offset in px. **Not** `stickyOffsetTop`
|
||||||
|
- `color`, `titleActiveColor`, `titleInactiveColor` — color props
|
||||||
|
- `type`: `'line'` | `'card'`
|
||||||
|
- `swipeable` — enable swipe gesture switching
|
||||||
|
- `animated` — enable tab switch animation
|
||||||
|
- `ellipsis` — text overflow ellipsis (default true)
|
||||||
|
- **No** `lineWidth` or `lineHeight` props
|
||||||
|
- `onChange`: `(e: { detail: { index: number; name?: string; title?: string } }) => void`
|
||||||
|
- `Tab`: `title` (ReactNode), `name` (identifier), `dot`, `info`, `disabled`, `titleStyle`
|
||||||
|
|
||||||
|
### Image (VantUI Image, not Taro's native Image)
|
||||||
|
- `src`, `fit` (`'contain' | 'cover' | 'fill' | 'widthFix' | 'heightFix' | 'none'`), `width`, `height`, `radius`
|
||||||
|
- `round` — circular image (boolean)
|
||||||
|
- `lazyLoad`, `showError`, `showLoading`
|
||||||
|
- `renderLoading`, `renderError` — custom render slots
|
||||||
|
|
||||||
|
### Common VantUI pattern: Taro's native components vs VantUI components
|
||||||
|
- Use `@tarojs/components` for: `View`, `Text`, `ScrollView`, `Image` (native, simpler)
|
||||||
|
- Use `@antmjs/vantui` for: styled UI components (Search, Swiper, Tabs, Grid, Button, etc.)
|
||||||
|
- VantUI component props extend Taro's `ViewProps`, so `onClick`/`onTap` and style props are always available
|
||||||
|
|
||||||
|
## Custom TabBar (`src/custom-tab-bar/`)
|
||||||
|
|
||||||
|
This project uses Taro's **custom tabBar** mechanism (`tabBar.custom: true` in `app.config.ts`). Key rules:
|
||||||
|
|
||||||
|
### Component location
|
||||||
|
- The custom tabBar component MUST be at `src/custom-tab-bar/index.tsx` — this is a Taro convention, not configurable
|
||||||
|
- Taro automatically renders this component on every tab page; do NOT import `<CustomTabBar />` in individual pages
|
||||||
|
|
||||||
|
### Do NOT use CoverView
|
||||||
|
- `CoverView` / `CoverImage` have known bugs on real devices: invisible, click events don't bubble
|
||||||
|
- Use regular `<View>` with `position: fixed; bottom: 0; z-index: 999` instead
|
||||||
|
|
||||||
|
### Active tab state
|
||||||
|
- Taro creates a **new instance** of the tabBar component on each page, so `useState` resets on every tab switch
|
||||||
|
- Derive the active tab from `Taro.getCurrentPages()` at render time — NOT from component state
|
||||||
|
- Pattern: `const route = Taro.getCurrentPages()[last]?.route; const activeKey = TAB_LIST.find(t => route.includes(t.key))`
|
||||||
|
|
||||||
|
### Tab switching
|
||||||
|
- Use `Taro.switchTab({ url: pagePath })` — this matches native tabBar behavior
|
||||||
|
- The `pagePath` format must match the `tabBar.list[].pagePath` in `app.config.ts`
|
||||||
|
|
||||||
|
### Center "publish" button
|
||||||
|
- The publish button is a UI-only element not in `tabBar.list`
|
||||||
|
- It triggers a VantUI `<Popup>` rendered as a sibling of the tabBar, NOT nested inside it
|
||||||
|
- The popup state can use `useState` since it's ephemeral and not shared across tabs
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/* eslint-disable import/no-commonjs */
|
||||||
|
// babel-preset-taro 更多选项和默认值:
|
||||||
|
// https://github.com/NervJS/taro/blob/next/packages/babel-preset-taro/README.md
|
||||||
|
module.exports = {
|
||||||
|
presets: [
|
||||||
|
[
|
||||||
|
'taro',
|
||||||
|
{
|
||||||
|
framework: 'react',
|
||||||
|
ts: true,
|
||||||
|
useBuiltIns: false,
|
||||||
|
hot: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
[
|
||||||
|
'import',
|
||||||
|
{
|
||||||
|
libraryName: '@antmjs/vantui',
|
||||||
|
libraryDirectory: 'es',
|
||||||
|
},
|
||||||
|
'@antmjs/vantui',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// eslint-disable-next-line import/no-commonjs
|
||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
NODE_ENV: '"development"',
|
||||||
|
},
|
||||||
|
defineConstants: {},
|
||||||
|
mini: {},
|
||||||
|
h5: {},
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
/* eslint-disable import/no-commonjs */
|
||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
const npath = require("path");
|
||||||
|
const pkg = require("../package.json");
|
||||||
|
const miniChain = require("./webpack/miniChain");
|
||||||
|
const h5Chain = require("./webpack/h5Chain");
|
||||||
|
|
||||||
|
process.env.TARO_ENV = process.env.TARO_ENV ?? "weapp"
|
||||||
|
process.env.NODE_ENV = process.env.NODE_ENV ?? 'production'
|
||||||
|
process.env.API_ENV = process.env.API_ENV ?? 'real'
|
||||||
|
|
||||||
|
const config = {
|
||||||
|
projectName: pkg.name,
|
||||||
|
date: "2022-8-10",
|
||||||
|
designWidth: 750,
|
||||||
|
deviceRatio: {
|
||||||
|
640: 2.34 / 2,
|
||||||
|
750: 1,
|
||||||
|
828: 1.81 / 2,
|
||||||
|
},
|
||||||
|
sourceRoot: "src",
|
||||||
|
outputRoot: process.env.TARO_ENV === "h5" ? "build" : process.env.TARO_ENV,
|
||||||
|
alias: {
|
||||||
|
"@": npath.resolve(process.cwd(), "src"),
|
||||||
|
},
|
||||||
|
defineConstants: {},
|
||||||
|
copy: {
|
||||||
|
patterns: [],
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
framework: "react",
|
||||||
|
compiler: "webpack5",
|
||||||
|
cache: {
|
||||||
|
enable: false, // Webpack 持久化缓存配置,建议开启。默认配置请参考:https://docs.taro.zone/docs/config-detail#cache
|
||||||
|
},
|
||||||
|
mini: {
|
||||||
|
webpackChain(chain) {
|
||||||
|
miniChain(chain);
|
||||||
|
},
|
||||||
|
lessLoaderOption: {
|
||||||
|
lessOptions: {
|
||||||
|
modifyVars: {
|
||||||
|
hack: `true; @import "${npath.join(
|
||||||
|
process.cwd(),
|
||||||
|
"src/styles/index.less"
|
||||||
|
)}";`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// 适用于全局引入样式
|
||||||
|
// additionalData: "@import '~/src/styles/index.less';",
|
||||||
|
},
|
||||||
|
postcss: {
|
||||||
|
pxtransform: {
|
||||||
|
enable: true,
|
||||||
|
config: {},
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
enable: true,
|
||||||
|
config: {
|
||||||
|
limit: 1024, // 设定转换尺寸上限
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cssModules: {
|
||||||
|
enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
|
||||||
|
config: {
|
||||||
|
namingPattern: "module", // 转换模式,取值为 global/module
|
||||||
|
generateScopedName: "[name]__[local]___[hash:base64:5]",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
miniCssExtractPluginOption: {
|
||||||
|
ignoreOrder: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
h5: {
|
||||||
|
webpackChain(chain) {
|
||||||
|
h5Chain(chain);
|
||||||
|
if (process.env.NODE_ENV === "production") {
|
||||||
|
chain.performance.maxEntrypointSize(1000000).maxAssetSize(512000);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
esnextModules: [/@antmjs[\\/]vantui/],
|
||||||
|
lessLoaderOption: {
|
||||||
|
lessOptions: {
|
||||||
|
modifyVars: {
|
||||||
|
// 或者可以通过 less 文件覆盖(文件路径为绝对路径)
|
||||||
|
hack: `true; @import "${npath.join(
|
||||||
|
process.cwd(),
|
||||||
|
"src/styles/index.less"
|
||||||
|
)}";`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
router: {
|
||||||
|
mode: "browser",
|
||||||
|
},
|
||||||
|
devServer: {
|
||||||
|
hot: false,
|
||||||
|
},
|
||||||
|
publicPath: "/",
|
||||||
|
staticDirectory: "static",
|
||||||
|
postcss: {
|
||||||
|
autoprefixer: {
|
||||||
|
enable: true,
|
||||||
|
config: {},
|
||||||
|
},
|
||||||
|
cssModules: {
|
||||||
|
enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
|
||||||
|
config: {
|
||||||
|
namingPattern: "module", // 转换模式,取值为 global/module
|
||||||
|
generateScopedName: "[name]__[local]___[hash:base64:5]",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
miniCssExtractPluginOption: {
|
||||||
|
ignoreOrder: true,
|
||||||
|
filename: "assets/css/[name].css",
|
||||||
|
chunkFilename: "assets/css/chunk/[name].css",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rn: {
|
||||||
|
appName: "taroDemo",
|
||||||
|
postcss: {
|
||||||
|
cssModules: {
|
||||||
|
enable: false, // 默认为 false,如需使用 css modules 功能,则设为 true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
["@tarojs/plugin-framework-react", { reactMode: "concurrent" }],
|
||||||
|
"@tarojs/plugin-platform-alipay-dd",
|
||||||
|
["@tarojs/plugin-platform-kwai"],
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = function (merge) {
|
||||||
|
return merge({}, config, require(`./${process.env.NODE_ENV}`));
|
||||||
|
};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// eslint-disable-next-line import/no-commonjs
|
||||||
|
module.exports = {
|
||||||
|
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,23 @@
|
|||||||
|
module.exports = function (chain) {
|
||||||
|
// taro内部的配置:scriptRule.exclude = [filename => /css-loader/.test(filename) || (/node_modules/.test(filename) && !(/taro/.test(filename)))];
|
||||||
|
// 下面重写exclude的配置,部分三方包需要babel,包括taro、@antmjs等
|
||||||
|
// 根据exclude可以看出,千万不要在项目名称上面带上taro字样,否则所有引用到node_modules的包都会重新被编译一次
|
||||||
|
// 以下配置将不再使用usage配置,因为根据小程序官方描述,ios9开始基本都已支持了,浏览器可以使用polyfill.io 国内可以用阿里云版的,index.html有引用
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 如果babel.config.js设置useBuiltIns:usage
|
||||||
|
* /tarojs[\\/](runtime|shared|plugin-platform|components)/.test(filename) 应该被exculde
|
||||||
|
* /tarojs[\\/](runtime|shared|plugin-platform)/.test(filename) 应该单独babel 且设置useBuiltIns:false
|
||||||
|
*/
|
||||||
|
chain.module
|
||||||
|
.rule('script')
|
||||||
|
.exclude.clear()
|
||||||
|
.add(
|
||||||
|
(filename) =>
|
||||||
|
/css-loader/.test(filename) ||
|
||||||
|
(/node_modules/.test(filename) &&
|
||||||
|
!/(taro)|(inversify)|(@antmjs)|(react-spring)|(recoil)|(buffer)|(qrcode)/.test(
|
||||||
|
filename,
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/* eslint-disable import/no-commonjs */
|
||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
const H5FixPlugin = require('@antmjs/plugin-h5-fix')
|
||||||
|
const commonChain = require('./commonChain')
|
||||||
|
|
||||||
|
module.exports = function (chain) {
|
||||||
|
chain.plugin('H5FixPlugin').use(new H5FixPlugin())
|
||||||
|
commonChain(chain)
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
/* eslint-disable import/no-commonjs */
|
||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
const MiniFixPlugin = require('@antmjs/plugin-mini-fix')
|
||||||
|
const GlobalFixPlugin = require('@antmjs/plugin-global-fix')
|
||||||
|
const commonChain = require('./commonChain')
|
||||||
|
|
||||||
|
module.exports = function (chain) {
|
||||||
|
// add @antmjs/plugin-mini-fix and @antmjs/mini-fix
|
||||||
|
// 解决微信小程序和抖音小程序的path上的params没有自动decode的问题,支付宝和钉钉是有decode过的
|
||||||
|
// 这个问题是因为微信抖音和支付宝钉钉原生小程序的返回结果就是不一致的,Taro目前是没有去处理的
|
||||||
|
chain.plugin('MiniFixPlugin').use(new MiniFixPlugin())
|
||||||
|
|
||||||
|
//解决支付宝小程序、钉钉小程序、百度小程序没有暴露全局变量global的问题
|
||||||
|
chain.plugin('GlobalFixPlugin').use(new GlobalFixPlugin())
|
||||||
|
commonChain(chain)
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
{
|
||||||
|
"name": "pure-project-vantui",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "",
|
||||||
|
"templateInfo": {
|
||||||
|
"name": "default",
|
||||||
|
"typescript": true,
|
||||||
|
"css": "less"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev:weapp": "npm run build:weapp -- --watch",
|
||||||
|
"dev:h5": "npm run build:h5 -- --watch",
|
||||||
|
"build:weapp": "taro build --type weapp",
|
||||||
|
"build:h5": "taro build --type h5"
|
||||||
|
},
|
||||||
|
"browserslist": {
|
||||||
|
"production": [
|
||||||
|
"ios >= 9"
|
||||||
|
],
|
||||||
|
"development": [
|
||||||
|
"last 1 version"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"dependencies": {
|
||||||
|
"@antmjs/mini-fix": "^2.3.21",
|
||||||
|
"@antmjs/vantui": "^3.1.6",
|
||||||
|
"@babel/runtime": "^7.7.7",
|
||||||
|
"@tarojs/components": "3.6.14",
|
||||||
|
"@tarojs/helper": "3.6.14",
|
||||||
|
"@tarojs/plugin-framework-react": "3.6.14",
|
||||||
|
"@tarojs/plugin-platform-alipay": "3.6.14",
|
||||||
|
"@tarojs/plugin-platform-alipay-dd": "^0.1.3",
|
||||||
|
"@tarojs/plugin-platform-h5": "3.6.14",
|
||||||
|
"@tarojs/plugin-platform-jd": "3.6.14",
|
||||||
|
"@tarojs/plugin-platform-kwai": "^2.0.0",
|
||||||
|
"@tarojs/plugin-platform-qq": "3.6.14",
|
||||||
|
"@tarojs/plugin-platform-swan": "3.6.14",
|
||||||
|
"@tarojs/plugin-platform-tt": "3.6.14",
|
||||||
|
"@tarojs/plugin-platform-weapp": "3.6.14",
|
||||||
|
"@tarojs/react": "3.6.14",
|
||||||
|
"@tarojs/router": "3.6.14",
|
||||||
|
"@tarojs/runtime": "3.6.14",
|
||||||
|
"@tarojs/shared": "3.6.14",
|
||||||
|
"@tarojs/taro": "3.6.14",
|
||||||
|
"@tarojs/taro-h5": "3.6.14",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@antmjs/plugin-global-fix": "^2.3.21",
|
||||||
|
"@antmjs/plugin-h5-fix": "^2.3.21",
|
||||||
|
"@antmjs/plugin-mini-fix": "^2.3.21",
|
||||||
|
"@babel/core": "^7.12.9",
|
||||||
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.5",
|
||||||
|
"@tarojs/cli": "3.6.14",
|
||||||
|
"@tarojs/webpack5-runner": "3.6.14",
|
||||||
|
"@types/react": "^18.0.26",
|
||||||
|
"@types/react-dom": "^18.0.10",
|
||||||
|
"@types/webpack-env": "^1.13.6",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^5.20.0",
|
||||||
|
"@typescript-eslint/parser": "^5.20.0",
|
||||||
|
"babel-plugin-import": "^1.13.5",
|
||||||
|
"babel-preset-taro": "3.6.14",
|
||||||
|
"eslint": "^8.12.0",
|
||||||
|
"eslint-config-taro": "3.6.14",
|
||||||
|
"eslint-plugin-import": "^2.12.0",
|
||||||
|
"eslint-plugin-react": "^7.8.2",
|
||||||
|
"eslint-plugin-react-hooks": "^4.2.0",
|
||||||
|
"react-refresh": "^0.11.0",
|
||||||
|
"stylelint": "^13.13.1",
|
||||||
|
"typescript": "^4.1.0",
|
||||||
|
"webpack": "5.69.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"miniprogramRoot": "./",
|
||||||
|
"projectname": "pure-project-vantui",
|
||||||
|
"description": "",
|
||||||
|
"appid": "wx7ed74d60503b5ee3",
|
||||||
|
"setting": {
|
||||||
|
"urlCheck": true,
|
||||||
|
"es6": false,
|
||||||
|
"postcss": false,
|
||||||
|
"minified": false,
|
||||||
|
"enhance": false
|
||||||
|
},
|
||||||
|
"compileType": "miniprogram"
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { createRequest } from '@/utils/request'
|
||||||
|
import type { ResponseStructure } from '@/utils/request'
|
||||||
|
|
||||||
|
// ==================== 类型定义 ====================
|
||||||
|
|
||||||
|
/** 图片资源 */
|
||||||
|
export interface ApiImage {
|
||||||
|
preview_url: string
|
||||||
|
file_name: string
|
||||||
|
id: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 轮播图项 */
|
||||||
|
export interface CarouselItem {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
image: ApiImage
|
||||||
|
link: string
|
||||||
|
status: number // 0: 启用
|
||||||
|
sort: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 宫格导航项 */
|
||||||
|
export interface GridNavItem {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
image: ApiImage
|
||||||
|
link: string
|
||||||
|
status: number // 0: 启用
|
||||||
|
sort: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 首页数据 */
|
||||||
|
export interface HomeData {
|
||||||
|
carousel: CarouselItem[]
|
||||||
|
gridNav: GridNavItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== API ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取首页数据(无需登录)
|
||||||
|
*/
|
||||||
|
export async function getHomeData(): Promise<ResponseStructure<HomeData>> {
|
||||||
|
return createRequest<HomeData>({
|
||||||
|
url: '/api/home',
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
export default defineAppConfig({
|
||||||
|
pages: [
|
||||||
|
'pages/index/index',
|
||||||
|
],
|
||||||
|
window: {
|
||||||
|
backgroundTextStyle: 'light',
|
||||||
|
navigationBarBackgroundColor: '#fff',
|
||||||
|
navigationBarTitleText: 'WeChat',
|
||||||
|
navigationBarTextStyle: 'black',
|
||||||
|
},
|
||||||
|
animation: false,
|
||||||
|
})
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
@import '@antmjs/vantui/es/style/var.less';
|
||||||
|
@import '@antmjs/vantui/lib/index.less';
|
||||||
|
|
||||||
|
page {
|
||||||
|
background: @page-back;
|
||||||
|
font-size: 28px;
|
||||||
|
font-family: @base-font-family;
|
||||||
|
color: @black;
|
||||||
|
}
|
||||||
|
|
||||||
|
view,
|
||||||
|
div {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
body,
|
||||||
|
html {
|
||||||
|
// NOTE: taro h5 ios上拉遮挡底部fixed元素
|
||||||
|
overflow: hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.van-cell-group--inset {
|
||||||
|
background: @white;
|
||||||
|
box-shadow: 0 14px 80px 0 rgba(138, 149, 158, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.van-cell__label {
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可以用的类
|
||||||
|
// .van-hairline,
|
||||||
|
// .van-hairline--top,
|
||||||
|
// .van-hairline--left,
|
||||||
|
// .van-hairline--right,
|
||||||
|
// .van-hairline--bottom,
|
||||||
|
// .van-hairline--top-bottom,
|
||||||
|
// .van-hairline--surround
|
||||||
|
// .van-ellipsis
|
||||||
|
// .van-multi-ellipsis--l2
|
||||||
|
// .van-multi-ellipsis--l3
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { Component } from 'react'
|
||||||
|
import './app.less'
|
||||||
|
|
||||||
|
class App extends Component {
|
||||||
|
|
||||||
|
componentDidMount () {}
|
||||||
|
|
||||||
|
componentDidShow () {}
|
||||||
|
|
||||||
|
componentDidHide () {}
|
||||||
|
|
||||||
|
componentDidCatchError () {}
|
||||||
|
|
||||||
|
// this.props.children 是将要会渲染的页面
|
||||||
|
render () {
|
||||||
|
return this.props.children
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
|
||||||
|
<meta content="width=device-width,initial-scale=1,user-scalable=no" name="viewport">
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-touch-fullscreen" content="yes">
|
||||||
|
<meta name="format-detection" content="telephone=no,address=no">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="white">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" >
|
||||||
|
<title>antmjs</title>
|
||||||
|
<script crossorigin="anonymous" src="https://polyfill.alicdn.com/polyfill.min.js?features=es2015%2Ces2016%2Ces2017%2Ces2018%2Ces2019%2Ces2020%2Ces2021%2Ces2022"></script>
|
||||||
|
<script><%= htmlWebpackPlugin.options.script %></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export default definePageConfig({
|
||||||
|
navigationBarTitleText: '校园美食',
|
||||||
|
navigationBarBackgroundColor: '#ff6b35',
|
||||||
|
navigationBarTextStyle: 'white',
|
||||||
|
})
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
.index {
|
||||||
|
position: relative;
|
||||||
|
min-height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
// 背景图层
|
||||||
|
.bg-image {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 内容层
|
||||||
|
.index-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 1. 搜索栏 ==========
|
||||||
|
.search-wrapper {
|
||||||
|
padding: 24px 32px 16px;
|
||||||
|
|
||||||
|
.van-search {
|
||||||
|
padding: 0;
|
||||||
|
border-radius: 36px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 16px rgba(255, 107, 53, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.van-search__content {
|
||||||
|
border-radius: 36px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 2. 轮播图 ==========
|
||||||
|
.swiper-wrapper {
|
||||||
|
margin: 8px 32px 20px;
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||||
|
|
||||||
|
.swiper-img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 3. 宫格导航 ==========
|
||||||
|
.grid-wrapper {
|
||||||
|
margin: 16px 32px 20px;
|
||||||
|
background: rgb(255 255 255);
|
||||||
|
border-radius: 20px;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
|
||||||
|
padding-bottom: 24px;
|
||||||
|
|
||||||
|
.grid-title {
|
||||||
|
font-size: 32px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.van-grid-item__content {
|
||||||
|
padding-top: 24px;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.van-icon--image {
|
||||||
|
width: 60px !important;
|
||||||
|
height: 60px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.van-grid-item__text {
|
||||||
|
font-size: 22px;
|
||||||
|
color: #555;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 4. 底部推荐卡片 ==========
|
||||||
|
.bottom-section {
|
||||||
|
padding: 0 32px;
|
||||||
|
|
||||||
|
.section-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 24px 28px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.98);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
width: 72px;
|
||||||
|
height: 72px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 18px;
|
||||||
|
margin-right: 20px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-info {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-desc {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-arrow {
|
||||||
|
font-size: 40px;
|
||||||
|
color: #ccc;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 每张卡片图标不同底色
|
||||||
|
.card-hot .card-icon {
|
||||||
|
background: #fff0ed;
|
||||||
|
}
|
||||||
|
.card-discount .card-icon {
|
||||||
|
background: #fff7e6;
|
||||||
|
}
|
||||||
|
.card-new .card-icon {
|
||||||
|
background: #e6f7ff;
|
||||||
|
}
|
||||||
|
.card-order .card-icon {
|
||||||
|
background: #f0edff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 底部留白
|
||||||
|
.bottom-spacer {
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
import { View, Image } from '@tarojs/components'
|
||||||
|
import { Search, Swiper, SwiperItem, Grid, GridItem } from '@antmjs/vantui'
|
||||||
|
import { useState, useEffect } from 'react'
|
||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
import { getHomeData } from '@/api/home'
|
||||||
|
import type { CarouselItem, GridNavItem } from '@/api/home'
|
||||||
|
import './index.less'
|
||||||
|
|
||||||
|
// 背景图
|
||||||
|
import bgImg from '../../../static/img/bg.png'
|
||||||
|
|
||||||
|
// ==================== 本地兜底数据 ====================
|
||||||
|
|
||||||
|
// ==================== 工具函数 ====================
|
||||||
|
|
||||||
|
/** 排序 + 过滤启用的项 */
|
||||||
|
function prepareItems<T extends { status: number; sort: number }>(items: T[]): T[] {
|
||||||
|
return items
|
||||||
|
.filter((item) => item.status === 0)
|
||||||
|
.sort((a, b) => a.sort - b.sort)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 处理导航跳转 */
|
||||||
|
function handleNavigate(link: string): void {
|
||||||
|
if (!link) return
|
||||||
|
if(link.startsWith('http')) {
|
||||||
|
window.location.href = link
|
||||||
|
} else {
|
||||||
|
Taro.navigateTo({ url: link }).catch(() => {
|
||||||
|
Taro.switchTab({ url: link }).catch(() => {
|
||||||
|
console.warn('导航失败:', link)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 页面组件 ====================
|
||||||
|
|
||||||
|
export default function Index() {
|
||||||
|
const [searchValue, setSearchValue] = useState('')
|
||||||
|
const [carousel, setCarousel] = useState<CarouselItem[]>([])
|
||||||
|
const [gridNav, setGridNav] = useState<GridNavItem[]>([])
|
||||||
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
|
// 获取首页数据
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchHomeData() {
|
||||||
|
try {
|
||||||
|
const res = await getHomeData()
|
||||||
|
if (res.success && res.data) {
|
||||||
|
// 轮播图:API 有数据则用,否则兜底
|
||||||
|
if (res.data.carousel?.length > 0) {
|
||||||
|
setCarousel(prepareItems(res.data.carousel))
|
||||||
|
}
|
||||||
|
// 宫格导航:API 有数据则用,否则兜底
|
||||||
|
if (res.data.gridNav?.length > 0) {
|
||||||
|
setGridNav(prepareItems(res.data.gridNav))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('获取首页数据失败,使用本地兜底数据', err)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchHomeData()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View className='index'>
|
||||||
|
{/* 背景图层 */}
|
||||||
|
<Image className='bg-image' src={bgImg} mode='widthFix' />
|
||||||
|
|
||||||
|
{/* 内容层 */}
|
||||||
|
<View className='index-content'>
|
||||||
|
{/* 1. 顶部搜索栏 */}
|
||||||
|
<View className='search-wrapper'>
|
||||||
|
<Search
|
||||||
|
value={searchValue}
|
||||||
|
onChange={(e) => setSearchValue(e.detail)}
|
||||||
|
shape='round'
|
||||||
|
placeholder='搜索美食、餐厅...'
|
||||||
|
background='rgba(255,255,255,0.9)'
|
||||||
|
onSearch={() => {
|
||||||
|
// 搜索逻辑
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 2. 中间轮播图 */}
|
||||||
|
<View className='swiper-wrapper'>
|
||||||
|
{carousel.length > 0 ? (
|
||||||
|
<Swiper
|
||||||
|
autoPlay={3000}
|
||||||
|
loop
|
||||||
|
paginationVisible
|
||||||
|
paginationColor='#ffffff'
|
||||||
|
duration={500}
|
||||||
|
height={220}
|
||||||
|
>
|
||||||
|
{carousel.map((item) => (
|
||||||
|
<SwiperItem key={item.id}>
|
||||||
|
<Image
|
||||||
|
className='swiper-img'
|
||||||
|
src={item.image.preview_url}
|
||||||
|
mode='aspectFill'
|
||||||
|
onClick={() => handleNavigate(item.link)}
|
||||||
|
/>
|
||||||
|
</SwiperItem>
|
||||||
|
))}
|
||||||
|
</Swiper>
|
||||||
|
) : (
|
||||||
|
<View className='swiper-placeholder'>暂无轮播图</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 3. 宫格导航 */}
|
||||||
|
<View className='grid-wrapper'>
|
||||||
|
{gridNav.length > 0 ? (
|
||||||
|
<Grid columnNum={5} border={false} gutter={12} iconSize={48}>
|
||||||
|
{gridNav.map((item) => (
|
||||||
|
<GridItem
|
||||||
|
key={item.id}
|
||||||
|
icon={item.image.preview_url}
|
||||||
|
text={item.title}
|
||||||
|
onClick={() => handleNavigate(item.link)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
) : (
|
||||||
|
<View className='grid-placeholder'>暂无分类</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 4. 底部推荐卡片 */}
|
||||||
|
<View className='bottom-section'>
|
||||||
|
<View className='section-card card-hot'>
|
||||||
|
<View className='card-icon'>🔥</View>
|
||||||
|
<View className='card-info'>
|
||||||
|
<View className='card-title'>热门推荐</View>
|
||||||
|
<View className='card-desc'>查看同学们都在点什么</View>
|
||||||
|
</View>
|
||||||
|
<View className='card-arrow'>›</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className='section-card card-discount'>
|
||||||
|
<View className='card-icon'>💰</View>
|
||||||
|
<View className='card-info'>
|
||||||
|
<View className='card-title'>限时优惠</View>
|
||||||
|
<View className='card-desc'>超值套餐低至9.9元</View>
|
||||||
|
</View>
|
||||||
|
<View className='card-arrow'>›</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className='section-card card-new'>
|
||||||
|
<View className='card-icon'>⭐</View>
|
||||||
|
<View className='card-info'>
|
||||||
|
<View className='card-title'>新品上架</View>
|
||||||
|
<View className='card-desc'>尝尝校园新味道</View>
|
||||||
|
</View>
|
||||||
|
<View className='card-arrow'>›</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View className='section-card card-order'>
|
||||||
|
<View className='card-icon'>📋</View>
|
||||||
|
<View className='card-info'>
|
||||||
|
<View className='card-title'>我的订单</View>
|
||||||
|
<View className='card-desc'>查看订单状态与历史</View>
|
||||||
|
</View>
|
||||||
|
<View className='card-arrow'>›</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 底部留白 */}
|
||||||
|
<View className='bottom-spacer' />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
@import '@antmjs/vantui/es/style/var.less';
|
||||||
|
|
||||||
|
// 这里可以重写主题
|
||||||
|
//@black: #1a1a1a;
|
||||||
|
//@white: #f7f7f7;
|
||||||
|
//@gray-1: #f7f8fa;
|
||||||
|
//@gray-2: #f2f3f5;
|
||||||
|
//@gray-3: #ededed;
|
||||||
|
//@gray-4: #dcdee0;
|
||||||
|
//@gray-5: #c8c9cc;
|
||||||
|
//@gray-6: #969799;
|
||||||
|
//@gray-7: #646566;
|
||||||
|
//@gray-8: #323233;
|
||||||
|
//@red: #ee0a24;
|
||||||
|
//@blue: #1989fa;
|
||||||
|
//@orange: #ff976a;
|
||||||
|
//@orange-dark: #ed6a0c;
|
||||||
|
//@orange-light: #fffbe8;
|
||||||
|
//@green: #0a4d2b;
|
||||||
|
//
|
||||||
|
//@pageBack: @gray-3;
|
||||||
|
//@navBack: rgba(237, 237, 237, 0.9);
|
||||||
|
//@popup-background-color: @gray-3;
|
||||||
|
//@backDropFilter: blur(20px);
|
||||||
|
//
|
||||||
|
//@popup-close-icon-color: @gray-5;
|
||||||
|
//@popup-close-icon-size: 40px;
|
||||||
|
//@popup-close-icon-margin: 24px;
|
||||||
|
//@button-plain-background-color: @gray-4;
|
||||||
|
//
|
||||||
|
// z-index
|
||||||
|
//@sticky-z-index: 800;
|
||||||
|
//@tabbar-z-index: 805;
|
||||||
|
//@navbar-z-index: 805;
|
||||||
|
//@goods-action-z-index: 806;
|
||||||
|
//@submit-bar-z-index: 806;
|
||||||
|
//@overlay-z-index: 1000;
|
||||||
|
//@dropdown-z-index: 1000;
|
||||||
|
//@popup-z-index: 1010;
|
||||||
|
//@popup-close-icon-z-index: 1010;
|
||||||
|
//@notify-z-index: 1500;
|
||||||
|
//
|
||||||
|
// Padding or Margin
|
||||||
|
//@padding-base: 8px;
|
||||||
|
//@padding-xs: @padding-base * 2;
|
||||||
|
//@padding-sm: @padding-base * 3;
|
||||||
|
//@padding-md: @padding-base * 4;
|
||||||
|
//@padding-lg: @padding-base * 6;
|
||||||
|
//@padding-xl: @padding-base * 8;
|
||||||
|
//
|
||||||
|
// Font
|
||||||
|
//@font-size-xs: 20px;
|
||||||
|
//@font-size-sm: 24px;
|
||||||
|
//@font-size-md: 28px;
|
||||||
|
//@font-size-lg: 32px;
|
||||||
|
//@font-weight-bold: 500;
|
||||||
|
//@line-height-xs: 28px;
|
||||||
|
//@line-height-sm: 36px;
|
||||||
|
//@line-height-md: 40px;
|
||||||
|
//@line-height-lg: 44px;
|
||||||
|
//@base-font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue',
|
||||||
|
// Helvetica, Segoe UI, Arial, Roboto, 'PingFang SC', 'miui', 'Hiragino Sans GB',
|
||||||
|
// 'Microsoft Yahei', sans-serif;
|
||||||
|
//@price-integer-font-family: Avenir-Heavy, PingFang SC, Helvetica Neue, Arial,
|
||||||
|
// sans-serif;
|
||||||
|
//
|
||||||
|
// Border
|
||||||
|
//@border-color: @gray-3;
|
||||||
|
//@border-width-base: 2px;
|
||||||
|
//@border-radius-sm: 4px;
|
||||||
|
//@border-radius-md: 8px;
|
||||||
|
//@border-radius-lg: 16px;
|
||||||
|
//@border-radius-max: 999px;
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
import Taro from '@tarojs/taro'
|
||||||
|
|
||||||
|
// ==================== 类型定义 ====================
|
||||||
|
|
||||||
|
/** 后端统一响应结构 */
|
||||||
|
export interface ResponseStructure<T = any> {
|
||||||
|
success: boolean
|
||||||
|
msg?: string
|
||||||
|
showType?: number // 0-2: Toast 提示; 3-5: Modal 通知; 99: 静默处理
|
||||||
|
description?: string
|
||||||
|
placement?: string
|
||||||
|
data?: T
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 请求配置(兼容 Taro.request.Option) */
|
||||||
|
export interface RequestConfig {
|
||||||
|
url: string
|
||||||
|
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'OPTIONS' | 'HEAD' | 'TRACE' | 'CONNECT'
|
||||||
|
data?: any
|
||||||
|
header?: Record<string, any>
|
||||||
|
timeout?: number
|
||||||
|
dataType?: 'json' | 'text' | 'base64' | 'arraybuffer'
|
||||||
|
responseType?: 'text' | 'arraybuffer'
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 配置常量 ====================
|
||||||
|
/** 请求超时时间 */
|
||||||
|
const REQUEST_TIMEOUT = 10000
|
||||||
|
/** 登录页面 */
|
||||||
|
const LOGIN_PATH = '/pages/login/index'
|
||||||
|
/** API 基础地址 */
|
||||||
|
const BASE_URL = 'http://localhost:8000'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析请求 URL,自动拼接 BASE_URL
|
||||||
|
*/
|
||||||
|
function resolveUrl(url: string): string {
|
||||||
|
// 已经是完整 URL
|
||||||
|
if (/^https?:\/\//i.test(url)) {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
return BASE_URL + url
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 错误处理 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理网络错误(HTTP 状态码错误)
|
||||||
|
*/
|
||||||
|
async function handleNetworkError(statusCode?: number): Promise<void> {
|
||||||
|
if (!statusCode) {
|
||||||
|
Taro.showToast({ title: '无法连接到服务器!', icon: 'none', duration: 2000 })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 401 未授权
|
||||||
|
if (statusCode === 401) {
|
||||||
|
Taro.showToast({ title: '您未登录,或者登录已经超时,请先登录!', icon: 'none', duration: 2500 })
|
||||||
|
|
||||||
|
// 清除本地认证信息
|
||||||
|
Taro.removeStorageSync('token')
|
||||||
|
Taro.removeStorageSync('refresh_token')
|
||||||
|
Taro.removeStorageSync('auth-storage')
|
||||||
|
|
||||||
|
// 避免在登录页重复跳转
|
||||||
|
const pages = Taro.getCurrentPages()
|
||||||
|
const currentRoute = pages.length > 0 ? pages[pages.length - 1].route : ''
|
||||||
|
if (currentRoute !== LOGIN_PATH.replace(/^\//, '')) {
|
||||||
|
Taro.redirectTo({ url: LOGIN_PATH })
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorMessages: Record<number, string> = {
|
||||||
|
302: '接口重定向了!',
|
||||||
|
400: '参数不正确!',
|
||||||
|
403: '您没有权限操作!',
|
||||||
|
404: '请求错误,未找到该资源!',
|
||||||
|
408: '请求超时!',
|
||||||
|
409: '系统已存在相同数据!',
|
||||||
|
500: '服务器内部错误!',
|
||||||
|
501: '服务未实现!',
|
||||||
|
502: '网关错误!',
|
||||||
|
503: '服务不可用!',
|
||||||
|
504: '服务暂时无法访问,请稍后再试!',
|
||||||
|
505: 'HTTP 版本不受支持!',
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = errorMessages[statusCode] || `连接错误 (状态码: ${statusCode})`
|
||||||
|
Taro.showToast({ title: message, icon: 'none', duration: 2500 })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理业务错误(接口返回的业务逻辑错误)
|
||||||
|
*/
|
||||||
|
async function handleBusinessError(data: ResponseStructure): Promise<void> {
|
||||||
|
const { msg = '', showType = 0, description = '' } = data
|
||||||
|
|
||||||
|
switch (showType) {
|
||||||
|
case 99: // 静默处理
|
||||||
|
break
|
||||||
|
case 0: // Toast 成功
|
||||||
|
if (msg) Taro.showToast({ title: msg, icon: 'success', duration: 2000 })
|
||||||
|
break
|
||||||
|
case 1: // Toast 警告
|
||||||
|
if (msg) Taro.showToast({ title: msg, icon: 'none', duration: 2500 })
|
||||||
|
break
|
||||||
|
case 2: // Toast 错误
|
||||||
|
if (msg) Taro.showToast({ title: msg, icon: 'error', duration: 2500 })
|
||||||
|
break
|
||||||
|
case 3: // Modal 成功
|
||||||
|
if (msg) Taro.showModal({ title: msg, content: description || '', showCancel: false })
|
||||||
|
break
|
||||||
|
case 4: // Modal 警告
|
||||||
|
if (msg) Taro.showModal({ title: msg, content: description || '', showCancel: false })
|
||||||
|
break
|
||||||
|
case 5: // Modal 错误
|
||||||
|
if (msg) Taro.showModal({ title: msg, content: description || '', showCancel: false })
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
if (msg) Taro.showToast({ title: msg, icon: 'none', duration: 2500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 核心请求函数 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建请求
|
||||||
|
* @param config - 请求配置
|
||||||
|
* @returns Promise<ResponseStructure<Data>>
|
||||||
|
*/
|
||||||
|
export async function createRequest<Data = any>(
|
||||||
|
config: RequestConfig,
|
||||||
|
): Promise<ResponseStructure<Data>> {
|
||||||
|
|
||||||
|
// 2. 构建 headers
|
||||||
|
const header: Record<string, any> = { ...(config.header || {}) }
|
||||||
|
|
||||||
|
// 附加国际化语言
|
||||||
|
try {
|
||||||
|
const language = Taro.getStorageSync('i18nextLng')
|
||||||
|
if (language) {
|
||||||
|
header['User-Language'] = language
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// storage 读取失败时忽略
|
||||||
|
}
|
||||||
|
|
||||||
|
// 附加 Token(排除登录接口)
|
||||||
|
try {
|
||||||
|
const token = Taro.getStorageSync('token')
|
||||||
|
if (token) {
|
||||||
|
header['Authorization'] = `Bearer ${token}`
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// storage 读取失败时忽略
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 4. 发起请求
|
||||||
|
const task = Taro.request<ResponseStructure<Data>>({
|
||||||
|
url: resolveUrl(config.url),
|
||||||
|
method: config.method || 'GET',
|
||||||
|
data: config.data,
|
||||||
|
header,
|
||||||
|
timeout: config.timeout || REQUEST_TIMEOUT,
|
||||||
|
dataType: config.dataType || 'json',
|
||||||
|
responseType: config.responseType,
|
||||||
|
})
|
||||||
|
|
||||||
|
const response = await task
|
||||||
|
|
||||||
|
const body = response.data as ResponseStructure<Data>
|
||||||
|
|
||||||
|
if (body?.success) {
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. 业务失败:显示错误提示并 reject
|
||||||
|
if (body) {
|
||||||
|
await handleBusinessError(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(body)
|
||||||
|
} catch (err: any) {
|
||||||
|
|
||||||
|
// 请求被取消(去重或手动取消)
|
||||||
|
if (err?.errMsg?.includes('abort') || err?.errMsg?.includes('cancel')) {
|
||||||
|
console.warn('请求已取消:', err.errMsg)
|
||||||
|
return Promise.reject(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 有 HTTP 状态码但非 2xx
|
||||||
|
if (err?.statusCode) {
|
||||||
|
await handleNetworkError(err.statusCode)
|
||||||
|
return Promise.reject(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 网络超时
|
||||||
|
if (err?.errMsg?.includes('timeout')) {
|
||||||
|
Taro.showToast({ title: '请求超时,请稍后重试!', icon: 'none', duration: 2500 })
|
||||||
|
return Promise.reject(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 网络连接失败
|
||||||
|
if (err?.errMsg?.includes('fail') || err?.message?.includes('Network Error')) {
|
||||||
|
Taro.showToast({ title: '网络连接失败,请检查网络!', icon: 'none', duration: 2500 })
|
||||||
|
return Promise.reject(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他未知错误
|
||||||
|
Taro.showToast({
|
||||||
|
title: `网络错误: ${err?.errMsg || err?.message || '未知错误'}`,
|
||||||
|
icon: 'none',
|
||||||
|
duration: 2500,
|
||||||
|
})
|
||||||
|
return Promise.reject(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default createRequest
|
||||||
|
After Width: | Height: | Size: 300 KiB |
|
After Width: | Height: | Size: 345 KiB |
|
After Width: | Height: | Size: 646 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 8.2 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "es2017",
|
||||||
|
"module": "commonjs",
|
||||||
|
"removeComments": false,
|
||||||
|
"preserveConstEnums": true,
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"outDir": "lib",
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
"rootDir": ".",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"typeRoots": [
|
||||||
|
"node_modules/@types"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"include": ["./src", "./types"],
|
||||||
|
"compileOnSave": false
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/// <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'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||