hookehuyr

chore: 补充 Taro 小程序开发配置体系并格式化代码

- 新增全局规则:
  - taro-patterns.md(Taro 开发规范)
  - miniprogram-checklist.md(小程序检查清单)
  - taro-cross-platform.md(多端兼容指南)

- 新增项目配置:
  - .eslintrc.js(ESLint 规则)
  - .prettierrc(Prettier 格式化)
  - .lintstagedrc.js(Git 暂存检查)
  - .editorconfig(编辑器统一配置)
  - scripts/setup-husky.sh(Husky 自动安装)

- 新增文档:
  - docs/development-guide.md(完整开发指南)
  - docs/config-quick-reference.md(配置快速参考)

- 更新 package.json:
  - 添加代码质量相关命令(lint、format等)

- 使用 Prettier 格式化所有代码

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Showing 84 changed files with 2239 additions and 967 deletions
{
"permissions": {
"allow": [
"Skill(glm-plan-usage:usage-query-skill)"
"Skill(glm-plan-usage:usage-query-skill)",
"Bash(pnpm add:*)",
"Bash(chmod:*)",
"Bash(bash:*)",
"Bash(pnpm lint:no-fix:*)",
"Bash(pnpm lint:*)",
"Bash(pnpm list:*)",
"Bash(pnpm format:*)",
"Bash(git add:*)"
]
}
}
......
# http://editorconfig.org
# EditorConfig 配置
# https://editorconfig.org
root = true
# 所有文件
[*]
charset = utf-8
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
# Vue 文件
[*.{vue,js,jsx,ts,tsx}]
indent_style = space
indent_size = 2
# Less/SCSS 文件
[*.{less,scss,css}]
indent_style = space
indent_size = 2
# JSON 文件
[*.json]
indent_style = space
indent_size = 2
# Markdown 文件
[*.md]
trim_trailing_whitespace = false
indent_style = space
indent_size = 2
# 配置文件
[*.{yml,yaml}]
indent_style = space
indent_size = 2
# Shell 脚本
[*.sh]
end_of_line = lf
# Windows 特定
[*.{bat,cmd}]
end_of_line = crlf
......
module.exports = {
root: true,
env: {
browser: true,
node: true,
es2021: true
},
extends: [
'eslint:recommended'
// 暂时不使用 '@vue/eslint-config-prettier',手动配置规则避免冲突
],
plugins: ['vue'],
parser: 'vue-eslint-parser', // 使用 Vue parser
parserOptions: {
parser: 'espree', // JavaScript parser
ecmaVersion: 2021,
sourceType: 'module',
ecmaFeatures: {
jsx: true
}
},
rules: {
// Vue 规则(手动配置)
'vue/multi-word-component-names': 'off', // 允许单词组件名
'vue/no-v-html': 'warn', // 警告使用 v-html(XSS 风险)
'vue/require-default-prop': 'off', // 不强制 prop 默认值
'vue/require-prop-types': 'off', // 不强制 prop 类型(使用 JSDoc)
'vue/no-unused-vars': 'warn', // 警告未使用的变量
'vue/no-unused-components': 'warn', // 警告未使用的组件
// 通用规则
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], // 警告未使用的变量
'prefer-const': 'error', // 优先使用 const
'no-var': 'error', // 禁止使用 var
'eqeqeq': ['error', 'always'], // 强制使用 === 和 !==
'curly': ['error', 'all'], // 强制使用大括号
'brace-style': ['error', '1tbs'], // 大括号风格
'quotes': ['error', 'single', { avoidEscape: true }], // 单引号
'semi': ['error', 'never'], // 不使用分号
'comma-dangle': ['error', 'never'], // 不允许尾随逗号
'arrow-parens': ['error', 'as-needed'], // 箭头函数参数括号
'object-curly-spacing': ['error', 'always'], // 对象大括号空格
'array-bracket-spacing': ['error', 'never'], // 数组方括号无空格
// 代码质量
'no-duplicate-imports': 'error', // 禁止重复导入
'no-useless-return': 'error', // 禁止无用的 return
'no-else-return': 'error', // 禁止 else return(提前 return)
'prefer-template': 'error', // 优先使用模板字符串
'template-curly-spacing': 'error', // 模板字符串大括号内无空格
'object-shorthand': ['error', 'always'], // 对象属性简写
// 放宽一些规则,避免过多警告
'no-prototype-builtins': 'off', // 允许使用原型方法
'no-nested-ternary': 'off', // 允许嵌套三元表达式
'no-param-reassign': 'off', // 允许修改参数
'consistent-return': 'off' // 不要求一致的 return
},
globals: {
// Taro 全局变量
wx: 'readonly',
getCurrentPages: 'readonly',
getApp: 'readonly',
// NutUI 全局变量
NutUI: 'readonly'
},
overrides: [
// 测试文件规则
{
files: ['**/__tests__/**/*', '**/*.test.js', '**/*.spec.js'],
env: {
jest: true,
node: true
},
rules: {
'no-console': 'off'
}
}
]
}
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# 验证 commit 信息格式(可选)
# commit_regex='^(feat|fix|docs|style|refactor|test|chore|build|ci|perf|revert)(\(.+\))?: .{1,50}'
#
#if ! grep -qE "$commit_regex" "$1"; then
# echo "❌ Commit 信息格式不正确"
# echo "✅ 正确格式: type(scope): subject"
# echo "📝 类型: feat, fix, docs, style, refactor, test, chore, etc."
# exit 1
#fi
echo "✅ Commit 信息验证通过"
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
echo "🔍 运行代码检查..."
pnpm lint-staged
module.exports = {
// Vue 文件
'*.{vue,js,jsx,ts,tsx}': [
'eslint --fix',
'prettier --write'
],
// 样式文件
'*.{less,scss,css}': [
'prettier --write'
],
// JSON/配置文件
'*.{json,md,yml,yaml}': [
'prettier --write'
]
}
# 忽略构建输出
dist/
build/
# 忽略依赖
node_modules/
# 忽略配置文件
.prettierignore
.eslintrc.js
# 忽略锁文件
package-lock.json
pnpm-lock.yaml
yarn.lock
# 忽略日志
*.log
logs/
# 忽略临时文件
*.tmp
.cache/
# 忽略覆盖率报告
coverage/
# 忽略 Taro 配置
config/prod.config.js
config/dev.config.js
config/index.js
# 忽略静态资源
src/assets/fonts/
src/assets/images/
# 忽略特定文件
miniprogram_npm/
{
"semi": false,
"singleQuote": true,
"quoteProps": "as-needed",
"trailingComma": "none",
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "avoid",
"printWidth": 100,
"tabWidth": 2,
"useTabs": false,
"endOfLine": "lf",
"vueIndentScriptAndStyle": false,
"singleAttributePerLine": false,
"overrides": [
{
"files": "*.vue",
"options": {
"parser": "vue"
}
},
{
"files": "*.less",
"options": {
"parser": "less"
}
}
]
}
......@@ -23,12 +23,70 @@
## 开发命令
```bash
# 安装依赖
pnpm install
pnpm dev:weapp
pnpm build:weapp
pnpm lint
# 开发模式
pnpm dev:weapp # 微信小程序
pnpm dev:h5 # H5 端
# 构建
pnpm build:weapp # 微信小程序生产包
pnpm build:h5 # H5 端生产包
# 代码质量
pnpm lint # ESLint 检查并修复
pnpm format # Prettier 格式化
pnpm lint:no-fix # 仅检查不修复
```
## 代码质量保障
项目已配置完整的代码质量保障体系:
### ✅ 已配置工具
- **ESLint**:代码风格检查和潜在错误检测
- **Prettier**:代码格式化统一
- **EditorConfig**:编辑器配置统一
- **Husky**:Git Hooks 自动化
- **lint-staged**:提交前仅检查本次修改文件
### 📋 初始配置步骤
```bash
# 1. 安装代码质量相关依赖
pnpm add -D eslint prettier eslint-plugin-vue husky lint-staged
# 2. 初始化 Husky(自动设置 Git Hooks)
bash scripts/setup-husky.sh
# 3. 提交代码时会自动运行检查
git add .
git commit -m "feat: 添加新功能"
```
### 📚 详细配置文档
查看 `docs/development-guide.md` 获取完整的配置使用说明。
### 🎯 Claude Code 开发配置
项目配置了完善的 Claude Code 全局规则,支持:
- Vue 3 最佳实践
- Taro 开发规范
- 小程序特性适配
- 多端兼容处理
- 代码审查清单
全局规则位于 `~/.claude/rules/`
- `taro-patterns.md` - Taro 开发规范
- `miniprogram-checklist.md` - 小程序检查清单
- `taro-cross-platform.md` - 多端兼容指南
- `vue-patterns.md` - Vue 3 最佳实践
- `frontend-testing.md` - 前端测试指南
- `code-review.md` - 代码审查清单
## 项目结构
```text
......
# 配置快速参考
## 🚀 快速开始
### 1. 安装依赖
```bash
# 安装项目依赖
pnpm install
# 安装代码质量工具(首次)
pnpm add -D eslint prettier eslint-plugin-vue husky lint-staged @vue/eslint-config-prettier
# 初始化 Git Hooks
bash scripts/setup-husky.sh
```
### 2. 日常开发
```bash
# 启动开发服务器
pnpm dev:weapp
# 代码检查与格式化
pnpm lint # ESLint 检查并修复
pnpm format # Prettier 格式化
# 提交代码(自动检查)
git add .
git commit -m "feat: 添加新功能"
```
## 📁 配置文件说明
### 项目配置
| 文件 | 用途 |
|------|------|
| `.eslintrc.js` | ESLint 规则配置 |
| `.prettierrc` | Prettier 格式化配置 |
| `.editorconfig` | 编辑器统一配置 |
| `.lintstagedrc.js` | Git 暂存文件检查配置 |
| `.husky/pre-commit` | Git 提交前钩子 |
### 全局配置(`~/.claude/rules/`)
| 文件 | 用途 |
|------|------|
| `taro-patterns.md` | Taro 开发规范 |
| `miniprogram-checklist.md` | 小程序检查清单 |
| `taro-cross-platform.md` | 多端兼容指南 |
| `vue-patterns.md` | Vue 3 最佳实践 |
| `frontend-testing.md` | 前端测试指南 |
| `code-review.md` | 代码审查清单 |
| `tailwindcss-guide.md` | TailwindCSS 使用规范 |
| `frontend-performance.md` | 前端性能优化 |
## 🎯 常用命令
### 开发命令
```bash
pnpm dev:weapp # 微信小程序开发模式
pnpm dev:h5 # H5 端开发模式
pnpm build:weapp # 微信小程序构建
pnpm build:h5 # H5 端构建
```
### 代码质量命令
```bash
pnpm lint # ESLint 检查并修复
pnpm lint:no-fix # ESLint 仅检查
pnpm format # Prettier 格式化
pnpm format:check # Prettier 检查
```
### Git 命令
```bash
git add . # 添加所有文件
git commit -m "feat: xxx" # 提交(自动运行检查)
git commit --no-verify -m "xxx" # 跳过检查(不推荐)
```
## 📝 Commit 规范
推荐使用 Conventional Commits 格式:
```
feat: 新功能
fix: 修复 bug
docs: 文档更新
style: 代码格式
refactor: 重构
test: 测试相关
chore: 构建过程或辅助工具
```
### 示例
```bash
git commit -m "feat(booking): 添加预约日期选择功能"
git commit -m "fix(auth): 修复登录时 token 未持久化的问题"
git commit -m "docs: 更新开发文档"
```
## ⚙️ VS Code 配置
### 推荐插件
```bash
# 安装推荐插件
code --install-extension dbaeumer.vscode-eslint
code --install-extension esbenp.prettier-vscode
code --install-extension EditorConfig.EditorConfig
code --install-extension Vue.volar
code --install-extension taro.vscode-tarojs
```
### 工作区配置
创建 `.vscode/settings.json`
```json
{
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"files.eol": "\n",
"files.trimTrailingWhitespace": true
}
```
## 🔍 常见问题
### Q: Husky 钩子不生效?
```bash
# 重新初始化 Husky
npx husky install
npx husky add .husky/pre-commit "pnpm lint-staged"
chmod +x .husky/pre-commit
```
### Q: ESLint 和 Prettier 冲突?
```bash
# 安装 @vue/eslint-config-prettier
pnpm add -D @vue/eslint-config-prettier
# 在 .eslintrc.js 的 extends 最后添加
'@vue/eslint-config-prettier'
```
### Q: 想跳过检查临时提交?
```bash
# ⚠️ 不推荐,仅用于紧急情况
git commit --no-verify -m "feat: 临时提交"
```
## 📋 代码审查清单
提交代码前检查:
- [ ] 代码已通过 `pnpm lint`
- [ ] 代码已通过 `pnpm format`
- [ ]`console.log``debugger`
- [ ] 无注释掉的代码
- [ ] 代码有必要的注释
- [ ] 功能测试通过
- [ ] 文档已更新(如需要)
## 🎨 代码风格
### Vue 组件
```vue
<script setup>
// 1. 导入
import { ref, computed } from 'vue'
// 2. Props/Emits
const props = defineProps({})
const emit = defineEmits({})
// 3. 响应式状态
const count = ref(0)
// 4. Computed
const double = computed(() => count.value * 2)
// 5. 方法
const increment = () => { count.value++ }
// 6. 生命周期
onMounted(() => { init() })
// 7. Watch
watch(count, (val) => { track(val) })
</script>
```
### 命名规范
```javascript
// 组件:PascalCase
UserCard.vue
BookingList.vue
// 函数/变量:camelCase
getUserInfo
handleSubmit
// 常量:UPPER_SNAKE_CASE
API_BASE_URL
MAX_RETRY_COUNT
// 文件夹:kebab-case
use-offline-booking.js
auth-redirect.js
```
## 🔐 安全检查
提交前确认:
- [ ] 无硬编码密钥/Token
- [ ] 用户输入已验证
- [ ] API 错误信息不泄露敏感数据
- [ ] XSS 防护(避免 `v-html` 或净化)
- [ ] 敏感数据不存储在 localStorage
## 🚀 性能检查
部署前确认:
- [ ] 主包体积 < 2MB
- [ ] 单个分包 < 2MB
- [ ] 首屏渲染 < 2s
- [ ] 图片已优化(CDN 参数)
- [ ] 长列表使用虚拟滚动
- [ ] 路由懒加载已配置
## 📚 参考资源
- [完整开发指南](./development-guide.md)
- [项目 CLAUDE.md](../CLAUDE.md)
- [Taro 官方文档](https://docs.taro.zone/)
- [Vue 3 官方文档](https://cn.vuejs.org/)
- [NutUI 官方文档](https://nutui.jd.com/4/taro/)
## 🎯 下一步
1. ✅ 安装依赖并初始化 Husky
2. ✅ 配置 VS Code
3. ✅ 运行 `pnpm dev:weapp` 启动开发
4. ✅ 开始编码,享受自动代码检查
5. ✅ 查看全局配置了解最佳实践
祝开发愉快!🎉
# Taro 小程序开发配置指南
## 配置概览
项目已配置完整的代码质量保障体系:
### ✅ 已创建的配置文件
#### 1. ESLint 配置(`.eslintrc.js`)
- Vue 3 推荐规则
- Taro 小程序适配规则
- 代码风格检查
- 潜在错误检测
#### 2. Prettier 配置(`.prettierrc`)
- 代码格式化规则
- 统一代码风格
- 与 ESLint 无冲突集成
#### 3. EditorConfig 配置(`.editorconfig`)
- 编辑器统一配置
- 缩进、换行符等
#### 4. lint-staged 配置(`.lintstagedrc.js`)
- Git 暂存文件检查
- 仅检查本次修改的文件
#### 5. Husky 配置(`scripts/setup-husky.sh`)
- Git Hooks 自动化
- 提交前自动检查
## 快速开始
### 步骤 1:安装依赖
```bash
# 安装 ESLint 相关依赖
pnpm add -D eslint prettier eslint-plugin-vue @vue/eslint-config-prettier
# 安装 Husky 和 lint-staged
pnpm add -D husky lint-staged
```
### 步骤 2:初始化 Husky
```bash
# 方式 1:使用自动安装脚本
bash scripts/setup-husky.sh
# 方式 2:手动安装
npx husky install
npx husky add .husky/pre-commit "pnpm lint-staged"
npx husky add .husky/commit-msg "npx commitlint --edit \$1"
```
### 步骤 3:更新 package.json
`package.json``scripts` 中添加:
```json
{
"scripts": {
"dev:weapp": "npm run build:weapp -- --watch",
"build:weapp": "taro build --type weapp",
"lint": "eslint \"src/**/*.{js,jsx,vue,ts,tsx}\" --fix",
"lint:no-fix": "eslint \"src/**/*.{js,jsx,vue,ts,tsx}\"",
"format": "prettier --write \"src/**/*.{js,jsx,vue,ts,tsx,less,css,json,md}\"",
"format:check": "prettier --check \"src/**/*.{js,jsx,vue,ts,tsx,less,css,json,md}\"",
"prepare": "husky install"
}
}
```
## 使用说明
### 日常开发
#### 1. 开发前检查
```bash
# 启动开发服务器
pnpm dev:weapp
# 代码会自动检查,但有错误时不会阻止编译
```
#### 2. 提交代码
```bash
# 添加文件到暂存区
git add .
# 提交代码(会自动运行 lint-staged)
git commit -m "feat: 添加新功能"
# 如果检查失败,修复后再次提交
git add .
git commit -m "feat: 添加新功能"
```
#### 3. 手动检查代码
```bash
# 检查并自动修复
pnpm lint
# 仅检查不修复
pnpm lint:no-fix
# 格式化代码
pnpm format
# 检查代码格式
pnpm format:check
```
### 跳过检查(不推荐)
```bash
# 跳过 lint-staged 检查
git commit --no-verify -m "feat: 临时提交"
# ⚠️ 注意:仅用于紧急情况,平时不要使用
```
## 编辑器集成
### VS Code
#### 1. 安装插件
推荐安装以下 VS Code 插件:
```json
{
"recommendations": [
"dbaeumer.vscode-eslint", // ESLint
"esbenp.prettier-vscode", // Prettier
"EditorConfig.EditorConfig", // EditorConfig
"Vue.volar", // Vue 语言支持
"taro.vscode-tarojs" // Taro 开发工具
]
}
```
#### 2. 配置 VS Code
创建 `.vscode/settings.json`
```json
{
// ESLint
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
// Prettier
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
// Vue
"volar.autoCompleteRefs": true,
"volar.codeLens.pugTools": false,
"volar.completion.autoImportComponent": true,
// 文件
"files.eol": "\n",
"files.trimTrailingWhitespace": true,
"files.insertFinalNewline": true
}
```
#### 3. 配置 VS Code 任务
创建 `.vscode/tasks.json`
```json
{
"version": "2.0.0",
"tasks": [
{
"label": "Lint: 检查并修复",
"type": "npm",
"script": "lint",
"problemMatcher": []
},
{
"label": "Format: 格式化代码",
"type": "npm",
"script": "format",
"problemMatcher": []
},
{
"label": "Dev: 启动微信小程序",
"type": "npm",
"script": "dev:weapp",
"problemMatcher": []
}
]
}
```
### WebStorm / IntelliJ IDEA
1. **启用 ESLint**
- Settings → Languages & Frameworks → JavaScript → Code Quality Tools → ESLint
- 选择 "Automatic ESLint configuration"
- 勾选 "Run eslint --fix on save"
2. **启用 Prettier**
- Settings → Languages & Frameworks → JavaScript → Prettier
- 选择 "Run on save for files"
- 勾选 "On code reformat"
3. **启用 EditorConfig**
- Settings → Editor → Code Style → EditorConfig
- 勾选 "Enable EditorConfig support"
## 配置详解
### ESLint 规则说明
#### Vue 相关规则
```javascript
'vue/multi-word-component-names': 'off', // 允许单词组件名(如 Home.vue)
'vue/no-v-html': 'warn', // 警告使用 v-html(XSS 风险)
'vue/require-default-prop': 'off', // 不强制 prop 默认值
'vue/no-unused-vars': 'warn', // 警告未使用的变量
```
#### 通用规则
```javascript
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', // 生产环境警告 console
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', // 生产环境禁止 debugger
'prefer-const': 'error', // 优先使用 const
'no-var': 'error', // 禁止使用 var
'eqeqeq': ['error', 'always'], // 强制使用 ===
'semi': ['error', 'never'], // 不使用分号
'quotes': ['error', 'single'], // 使用单引号
```
### Prettier 规则说明
```json
{
"semi": false, // 不使用分号
"singleQuote": true, // 使用单引号
"trailingComma": "none", // 不使用尾随逗号
"printWidth": 100, // 每行最大 100 字符
"tabWidth": 2, // 缩进 2 空格
"endOfLine": "lf" // 使用 LF 换行符
}
```
## 常见问题
### Q1: Husky 钩子不生效
**问题**:提交代码时没有自动运行检查
**解决方案**
```bash
# 1. 检查 Husky 是否安装
ls -la .husky/
# 2. 重新安装 Husky
pnpm add -D husky
npx husky install
# 3. 添加 pre-commit 钩子
npx husky add .husky/pre-commit "pnpm lint-staged"
# 4. 检查钩子文件
cat .husky/pre-commit
```
### Q2: ESLint 和 Prettier 冲突
**问题**:ESLint 和 Prettier 对同一处代码有不同规则
**解决方案**
```bash
# 安装 @vue/eslint-config-prettier
pnpm add -D @vue/eslint-config-prettier
# 在 .eslintrc.js 中添加
{
"extends": [
'plugin:vue/vue3-recommended',
'@vue/eslint-config-prettier' // 放在最后
]
}
```
### Q3: lint-staged 检查所有文件
**问题**:每次提交都检查所有文件,很慢
**解决方案**
```bash
# 确保 lint-staged 配置正确
cat .lintstagedrc.js
# 应该配置为:
{
"*.{js,jsx,vue}": ["eslint --fix", "prettier --write"]
}
```
### Q4: 提交被阻止,但不想修复
**问题**:代码有问题但想临时提交
**解决方案**
```bash
# ⚠️ 不推荐,仅用于紧急情况
git commit --no-verify -m "feat: 临时提交"
```
### Q5: Taro API 报 ESLint 错误
**问题**:使用了 `wx` 全局变量报错
**解决方案**
```javascript
// .eslintrc.js 中已配置
globals: {
wx: 'readonly',
getCurrentPages: 'readonly',
getApp: 'readonly'
}
```
## 最佳实践
### 1. 提交前自检
```bash
# 1. 拉取最新代码
git pull origin develop
# 2. 运行检查
pnpm lint
pnpm format:check
# 3. 运行测试(如果有)
pnpm test
# 4. 提交代码
git add .
git commit -m "feat: 添加新功能"
```
### 2. Commit 信息规范
推荐使用 Conventional Commits 格式:
```
feat: 新功能
fix: 修复 bug
docs: 文档更新
style: 代码格式(不影响代码运行的变动)
refactor: 重构(既不是新增功能,也不是修改 bug 的代码变动)
test: 测试相关
chore: 构建过程或辅助工具的变动
```
示例:
```bash
git commit -m "feat(booking): 添加预约日期选择功能"
git commit -m "fix(auth): 修复登录时 token 未持久化的问题"
git commit -m "docs: 更新开发文档"
```
### 3. 代码审查清单
在提交 PR 前检查:
- [ ] 代码已通过 ESLint 检查
- [ ] 代码已通过 Prettier 格式化
- [ ]`console.log``debugger`
- [ ] 无注释掉的代码
- [ ] 代码有必要的注释
- [ ] 测试已通过(如果有)
- [ ] 文档已更新(如需要)
## 团队协作
### 统一开发环境
确保团队成员使用相同的配置:
```bash
# 1. 克隆项目
git clone <repo-url>
# 2. 安装依赖
pnpm install
# 3. 初始化 Husky
pnpm prepare
# 4. 安装 VS Code 插件
code --install-extension dbaeumer.vscode-eslint
code --install-extension esbenp.prettier-vscode
code --install-extension Vue.volar
```
### 代码审查流程
1. **开发者**:提交代码前运行 `pnpm lint`
2. **Husky**:自动运行 `lint-staged`
3. **CI/CD**:运行完整的 `pnpm lint``pnpm test`
4. **审查者**:检查代码质量和规范
5. **合并**:通过所有检查后合并
## 持续改进
### 定期更新依赖
```bash
# 检查过时的依赖
pnpm outdated
# 更新依赖
pnpm update
# 更新主要版本
pnpm upgrade --latest
```
### 自定义规则
根据团队需求调整规则:
```javascript
// .eslintrc.js
rules: {
// 添加团队特定规则
'custom-rule-name': 'error'
}
```
## 参考资源
- [ESlint 官方文档](https://eslint.org/)
- [Prettier 官方文档](https://prettier.io/)
- [EditorConfig 官方文档](https://editorconfig.org/)
- [Husky 官方文档](https://typicode.github.io/husky/)
- [lint-staged 官方文档](https://github.com/okonet/lint-staged)
- [Vue ESLint 官方插件](https://eslint.vuejs.org/)
- [Conventional Commits](https://www.conventionalcommits.org/)
......@@ -27,7 +27,11 @@
"dev:qq": "NODE_ENV=development taro build --type qq --watch",
"dev:quickapp": "NODE_ENV=development taro build --type quickapp --watch",
"postinstall": "weapp-tw patch",
"lint": "eslint --ext .js,.vue src"
"lint": "eslint \"src/**/*.{js,jsx,vue}\" --fix",
"lint:no-fix": "eslint \"src/**/*.{js,jsx,vue}\"",
"format": "prettier --write \"src/**/*.{js,jsx,vue,less,css,json,md}\"",
"format:check": "prettier --check \"src/**/*.{js,jsx,vue,less,css,json,md}\"",
"prepare": "husky install"
},
"browserslist": [
"last 3 versions",
......@@ -72,17 +76,23 @@
"@types/webpack-env": "^1.13.6",
"@vue/babel-plugin-jsx": "^1.0.6",
"@vue/compiler-sfc": "^3.0.0",
"@vue/eslint-config-prettier": "^10.2.0",
"autoprefixer": "^10.4.21",
"babel-preset-taro": "4.1.9",
"css-loader": "3.4.2",
"eslint": "^8.12.0",
"eslint-config-taro": "4.1.9",
"eslint-plugin-vue": "^10.7.0",
"husky": "^9.1.7",
"less": "^4.2.0",
"lint-staged": "^16.2.7",
"postcss": "^8.5.6",
"prettier": "^3.8.1",
"sass": "^1.78.0",
"style-loader": "1.3.0",
"tailwindcss": "^3.4.0",
"unplugin-vue-components": "^0.26.0",
"vue-eslint-parser": "^10.2.0",
"vue-loader": "^17.0.0",
"weapp-tailwindcss": "^4.1.10",
"webpack": "5.91.0"
......
This diff is collapsed. Click to expand it.
#!/bin/bash
# Husky 初始化脚本
# 使用方法: bash scripts/setup-husky.sh
set -e
echo "🔧 开始设置 Husky Git Hooks..."
# 检查是否在项目根目录
if [ ! -f "package.json" ]; then
echo "❌ 错误:请在项目根目录运行此脚本"
exit 1
fi
# 安装依赖(如果未安装)
echo "📦 安装依赖..."
pnpm add -D husky lint-staged prettier || {
echo "❌ 依赖安装失败"
exit 1
}
# 初始化 Husky
echo "🪝 初始化 Husky..."
npx husky install || {
echo "❌ Husky 初始化失败"
exit 1
}
# 创建 pre-commit 钩子
echo "📝 创建 pre-commit 钩子..."
cat > .husky/pre-commit << 'EOF'
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
echo "🔍 运行代码检查..."
pnpm lint-staged
EOF
# 添加执行权限
chmod +x .husky/pre-commit
# 创建 commit-msg 钩子(可选,用于验证 commit 信息)
cat > .husky/commit-msg << 'EOF'
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
# 验证 commit 信息格式(可选)
# commit_regex='^(feat|fix|docs|style|refactor|test|chore|build|ci|perf|revert)(\(.+\))?: .{1,50}'
#
#if ! grep -qE "$commit_regex" "$1"; then
# echo "❌ Commit 信息格式不正确"
# echo "✅ 正确格式: type(scope): subject"
# echo "📝 类型: feat, fix, docs, style, refactor, test, chore, etc."
# exit 1
#fi
echo "✅ Commit 信息验证通过"
EOF
chmod +x .husky/commit-msg
# 更新 package.json scripts
echo "📦 更新 package.json scripts..."
if command -v jq > /dev/null 2>&1; then
# 如果系统有 jq,使用它更新 package.json
jq '.scripts.prepare = "husky install"' package.json > package.json.tmp && mv package.json.tmp package.json
else
echo "⚠️ 请手动在 package.json 的 scripts 中添加: \"prepare\": \"husky install\""
fi
echo "✅ Git Hooks 设置完成!"
echo ""
echo "📝 使用说明:"
echo " - 每次 commit 前会自动运行 ESLint 和 Prettier"
echo " - 如果检查失败,commit 将被中止"
echo " - 使用 'git commit --no-verify' 跳过检查(不推荐)"
echo ""
echo "🎯 现在可以开始开发了!"
......@@ -5,12 +5,12 @@
* @FilePath: /tswj/src/api/common.js
* @Description: 通用接口
*/
import { fn, fetch, uploadFn } from '@/api/fn';
import { fn, fetch, uploadFn } from '@/api/fn'
const Api = {
SMS: '/srv/?a=sms',
TOKEN: '/srv/?a=upload',
SAVE_FILE: '/srv/?a=upload&t=save_file',
SAVE_FILE: '/srv/?a=upload&t=save_file'
}
/**
......@@ -19,7 +19,7 @@ const Api = {
* @param {string} params.phone 手机号
* @returns {Promise<{code:number,data:any,msg:string}>} 标准返回
*/
export const smsAPI = (params) => fn(fetch.post(Api.SMS, params));
export const smsAPI = params => fn(fetch.post(Api.SMS, params))
/**
* @description: 获取七牛token
......@@ -28,7 +28,7 @@ export const smsAPI = (params) => fn(fetch.post(Api.SMS, params));
* @param {string} params.file 图片 base64
* @returns {Promise<{code:number,data:any,msg:string}>} 标准返回(data 为上传 token 等信息)
*/
export const qiniuTokenAPI = (params) => fn(fetch.stringifyPost(Api.TOKEN, params));
export const qiniuTokenAPI = params => fn(fetch.stringifyPost(Api.TOKEN, params))
/**
* @description: 上传七牛
......@@ -37,7 +37,7 @@ export const qiniuTokenAPI = (params) => fn(fetch.stringifyPost(Api.TOKEN, param
* @param {Object} config axios 配置
* @returns {Promise<any|false>} 成功返回七牛响应数据,失败返回 false
*/
export const qiniuUploadAPI = (url, data, config) => uploadFn(fetch.basePost(url, data, config));
export const qiniuUploadAPI = (url, data, config) => uploadFn(fetch.basePost(url, data, config))
/**
* @description: 保存图片
......@@ -49,4 +49,4 @@ export const qiniuUploadAPI = (url, data, config) => uploadFn(fetch.basePost(url
* @param {string} params.filekey 文件 key
* @returns {Promise<{code:number,data:any,msg:string}>} 标准返回
*/
export const saveFileAPI = (params) => fn(fetch.stringifyPost(Api.SAVE_FILE, params));
export const saveFileAPI = params => fn(fetch.stringifyPost(Api.SAVE_FILE, params))
......
......@@ -5,7 +5,7 @@
* @FilePath: /xyxBooking-weapp/src/api/fn.js
* @Description: 统一后端返回格式(强制 { code, data, msg })
*/
import axios from '@/utils/request';
import axios from '@/utils/request'
import Taro from '@tarojs/taro'
import qs from 'qs'
......@@ -16,7 +16,7 @@ import qs from 'qs'
* @param {Promise<any>} api axios 请求 Promise
* @returns {Promise<{code:number,data:any,msg:string,show?:boolean}>} 标准化后的返回对象
*/
export const fn = (api) => {
export const fn = api => {
return api
.then(res => {
// 约定:后端 code === 1 为成功
......@@ -26,19 +26,29 @@ export const fn = (api) => {
}
// 失败兜底:优先返回后端响应,同时做 toast 提示
console.warn('接口请求失败:', res)
if (res_data && res_data.show === false) return res_data
if (res_data && res_data.show === false) {
return res_data
}
Taro.showToast({
title: (res_data && res_data.msg) ? res_data.msg : '请求失败',
title: res_data && res_data.msg ? res_data.msg : '请求失败',
icon: 'none',
duration: 2000
})
return res_data || { code: 0, data: null, msg: '请求失败' }
})
.catch(err => {
console.error('接口请求异常:', err);
return { code: 0, data: null, msg: (err && (err.msg || err.message || err.errMsg)) ? (err.msg || err.message || err.errMsg) : '网络异常' }
console.error('接口请求异常:', err)
return {
code: 0,
data: null,
msg:
err && (err.msg || err.message || err.errMsg)
? err.msg || err.message || err.errMsg
: '网络异常'
}
})
.finally(() => { // 最终执行
.finally(() => {
// 最终执行
})
}
......@@ -47,25 +57,26 @@ export const fn = (api) => {
* @param {Promise<any>} api axios 请求 Promise
* @returns {Promise<any|false>} 成功返回七牛响应数据,失败返回 false
*/
export const uploadFn = (api) => {
export const uploadFn = api => {
return api
.then(res => {
if (res.statusText === 'OK') {
return res.data || true;
} else {
console.warn('七牛上传失败:', res);
if (!res.data.show) return false;
Taro.showToast({
title: res.data.msg,
icon: 'none',
duration: 2000
});
return false;
return res.data || true
}
console.warn('七牛上传失败:', res)
if (!res.data.show) {
return false
}
Taro.showToast({
title: res.data.msg,
icon: 'none',
duration: 2000
})
return false
})
.catch(err => {
console.error('七牛上传异常:', err);
return false;
console.error('七牛上传异常:', err)
return false
})
}
......@@ -82,7 +93,7 @@ export const fetch = {
* @param {Object} params 查询参数
* @returns {Promise<any>} axios Promise
*/
get: function (api, params) {
get(api, params) {
return axios.get(api, params)
},
/**
......@@ -91,7 +102,7 @@ export const fetch = {
* @param {Object} params 请求体
* @returns {Promise<any>} axios Promise
*/
post: function (api, params) {
post(api, params) {
return axios.post(api, params)
},
/**
......@@ -100,7 +111,7 @@ export const fetch = {
* @param {Object} params 请求体
* @returns {Promise<any>} axios Promise
*/
stringifyPost: function (api, params) {
stringifyPost(api, params) {
return axios.post(api, qs.stringify(params), {
headers: {
'content-type': 'application/x-www-form-urlencoded'
......@@ -114,7 +125,7 @@ export const fetch = {
* @param {Object} config axios 配置
* @returns {Promise<any>} axios Promise
*/
basePost: function (url, data, config) {
basePost(url, data, config) {
return axios.post(url, data, config)
}
}
......
......@@ -5,7 +5,7 @@
* @FilePath: /xyxBooking-weapp/src/api/index.js
* @Description: 文件描述
*/
import { fn, fetch } from '@/api/fn';
import { fn, fetch } from '@/api/fn'
/**
* @description 预约业务 API 聚合
......@@ -30,22 +30,22 @@ const Api = {
BILL_PREPARE: '/srv/?a=api&t=bill_person',
// BILL_PAY_STATUS: '/srv/?a=api&t=bill_pay_status',
QUERY_QR_CODE: '/srv/?a=api&t=id_number_query_qr_code',
ICBC_ORDER_QRY: '/srv/?a=icbc_orderqry',
};
ICBC_ORDER_QRY: '/srv/?a=icbc_orderqry'
}
/**
* @description: 可预约日期列表
* @param {Array} month 月份,格式yyyy-mm, reserve_full 是否可约 1=可约,0=约满,-1=没有配置预约时段, open_time 在今天,开放预约最晚可预约日期的后一天的时间点, tips 不可预约的提示信息
* @returns
*/
export const canReserveDateListAPI = (params) => fn(fetch.get(Api.CAN_RESERVE_DATE_LIST, params));
export const canReserveDateListAPI = params => fn(fetch.get(Api.CAN_RESERVE_DATE_LIST, params))
/**
* @description: 可预约时段列表
* @param {Array} month_date 日期,格式yyyy-mm-dd
* @returns
*/
export const canReserveTimeListAPI = (params) => fn(fetch.get(Api.CAN_RESERVE_TIME_LIST, params));
export const canReserveTimeListAPI = params => fn(fetch.get(Api.CAN_RESERVE_TIME_LIST, params))
/**
* @description: 参观者列表
......@@ -54,7 +54,7 @@ export const canReserveTimeListAPI = (params) => fn(fetch.get(Api.CAN_RESERVE_TI
* @param {String} end_time 时段结束时间,格式 hh:mm
* @returns
*/
export const personListAPI = (params) => fn(fetch.get(Api.PERSON_LIST, params));
export const personListAPI = params => fn(fetch.get(Api.PERSON_LIST, params))
/**
* @description: 添加参观者
......@@ -63,14 +63,14 @@ export const personListAPI = (params) => fn(fetch.get(Api.PERSON_LIST, params));
* @param {String} id_number 证件号
* @returns
*/
export const addPersonAPI = (params) => fn(fetch.post(Api.ADD_PERSON, params));
export const addPersonAPI = params => fn(fetch.post(Api.ADD_PERSON, params))
/**
* @description: 删除参观者
* @param {String} person_id 参观者id
* @returns
*/
export const delPersonAPI = (params) => fn(fetch.post(Api.DEL_PERSON, params));
export const delPersonAPI = params => fn(fetch.post(Api.DEL_PERSON, params))
/**
* @description: 提交预约
......@@ -80,7 +80,7 @@ export const delPersonAPI = (params) => fn(fetch.post(Api.DEL_PERSON, params));
* @param {String} person_id_list
* @returns {String} bill_id 预约单id
*/
export const addReserveAPI = (params) => fn(fetch.post(Api.ADD_RESERVE, params));
export const addReserveAPI = params => fn(fetch.post(Api.ADD_RESERVE, params))
/**
* @description: 支付准备(模拟)
......@@ -102,34 +102,34 @@ export const addReserveAPI = (params) => fn(fetch.post(Api.ADD_RESERVE, params))
* @param {String} bill_id 预约单id
* @returns {String}
*/
export const billInfoAPI = (params) => fn(fetch.get(Api.BILL_INFO, params));
export const billInfoAPI = params => fn(fetch.get(Api.BILL_INFO, params))
/**
* @description: 预约单详情,参观者列表 - 免授权接口
* @param {String} pay_id 订单id
* @returns {String}
*/
export const onAuthBillInfoAPI = (params) => fn(fetch.get(Api.ON_AUTH_BILL_INFO, params));
export const onAuthBillInfoAPI = params => fn(fetch.get(Api.ON_AUTH_BILL_INFO, params))
/**
* @description: 预约码列表
* @returns {String}
*/
export const qrcodeListAPI = (params) => fn(fetch.get(Api.QRCODE_LIST, params));
export const qrcodeListAPI = params => fn(fetch.get(Api.QRCODE_LIST, params))
/**
* @description: 二维码使用状态
* @param {String} qr_code 二维码编号
* @returns {String} status 二维码状态 1=未激活(未支付),3=待使用(已支付),5=被取消,7=已使用
*/
export const qrcodeStatusAPI = (params) => fn(fetch.get(Api.QRCODE_STATUS, params));
export const qrcodeStatusAPI = params => fn(fetch.get(Api.QRCODE_STATUS, params))
/**
* @description: 预约单列表
* @param {String}
* @returns {String}
*/
export const billListAPI = (params) => fn(fetch.get(Api.BILL_LIST, params));
export const billListAPI = params => fn(fetch.get(Api.BILL_LIST, params))
/**
* @description: 所有预约单的详情(用于离线缓存:列表+详情)
......@@ -149,21 +149,21 @@ export const billListAPI = (params) => fn(fetch.get(Api.BILL_LIST, params));
* @returns: {Object} data[].list 列表字段集合
* @returns: {Number} data[].list.show_cancel_reserve 显示“取消预约”按钮(1=显示)
*/
export const billOfflineAllAPI = (params) => fn(fetch.get(Api.BILL_OFFLINE_ALL, params));
export const billOfflineAllAPI = params => fn(fetch.get(Api.BILL_OFFLINE_ALL, params))
/**
* @description: 取消预约
* @param {String} pay_id
* @returns {String}
*/
export const icbcRefundAPI = (params) => fn(fetch.post(Api.ICBC_REFUND, params));
export const icbcRefundAPI = params => fn(fetch.post(Api.ICBC_REFUND, params))
/**
* @description: 预约单的参观者列表
* @param {String}
* @returns {String}
*/
export const billPersonAPI = (params) => fn(fetch.get(Api.BILL_PREPARE, params));
export const billPersonAPI = params => fn(fetch.get(Api.BILL_PREPARE, params))
/**
* 接口废弃
......@@ -179,7 +179,7 @@ export const billPersonAPI = (params) => fn(fetch.get(Api.BILL_PREPARE, params))
* @param {string} params.id_number 身份证号
* @returns {Promise<{code:number,data:any,msg:string}>} 标准返回
*/
export const queryQrCodeAPI = (params) => fn(fetch.get(Api.QUERY_QR_CODE, params));
export const queryQrCodeAPI = params => fn(fetch.get(Api.QUERY_QR_CODE, params))
/**
* @description: 查询订单号
......@@ -187,4 +187,4 @@ export const queryQrCodeAPI = (params) => fn(fetch.get(Api.QUERY_QR_CODE, params
* @param {string} params.pay_id 支付凭证/订单号
* @returns {Promise<{code:number,data:any,msg:string}>} 标准返回
*/
export const icbcOrderQryAPI = (params) => fn(fetch.get(Api.ICBC_ORDER_QRY, params));
export const icbcOrderQryAPI = params => fn(fetch.get(Api.ICBC_ORDER_QRY, params))
......
......@@ -5,12 +5,12 @@
* @FilePath: /xyxBooking-weapp/src/api/redeem.js
* @Description: 义工核销端接口
*/
import { fn, fetch } from '@/api/fn';
import { fn, fetch } from '@/api/fn'
const Api = {
REDEEM_LOGIN: '/srv/?f=reserve_admin&a=login',
REDEEM_CHECK_AUTH: '/srv/?f=reserve_admin&a=user&t=check_auth',
REDEEM_REDEEM: '/srv/?f=reserve_admin&a=bill&t=redeem',
REDEEM_REDEEM: '/srv/?f=reserve_admin&a=bill&t=redeem'
}
/**
......@@ -18,18 +18,18 @@ const Api = {
* @param {Object} params 请求参数
* @returns {Promise<{code:number,data:any,msg:string}>} 标准返回
*/
export const volunteerLoginAPI = (params) => fn(fetch.post(Api.REDEEM_LOGIN, params));
export const volunteerLoginAPI = params => fn(fetch.post(Api.REDEEM_LOGIN, params))
/**
* @description: 检查核销权限
* @param {Object} params 请求参数
* @returns {Promise<{code:number,data:{can_redeem:boolean},msg:string}>} 标准返回
*/
export const checkRedeemPermissionAPI = (params) => fn(fetch.get(Api.REDEEM_CHECK_AUTH, params));
export const checkRedeemPermissionAPI = params => fn(fetch.get(Api.REDEEM_CHECK_AUTH, params))
/**
* @description: 核销
* @param {Object} params 请求参数
* @returns {Promise<{code:number,data:any,msg:string}>} 标准返回
*/
export const verifyTicketAPI = (params) => fn(fetch.post(Api.REDEEM_REDEEM, params));
export const verifyTicketAPI = params => fn(fetch.post(Api.REDEEM_REDEEM, params))
......
......@@ -2,12 +2,12 @@
* @Date: 2026-01-10
* @Description: 从原始文档转换生成的 API 接口文件
*/
import { fn, fetch } from '@/api/fn';
import { fn, fetch } from '@/api/fn'
const Api = {
CAN_RESERVE_DATE_LIST: '/srv/?a=api&t=can_reserve_date_list',
CAN_RESERVE_TIME_LIST: '/srv/?a=api&t=can_reserve_time_list',
};
CAN_RESERVE_TIME_LIST: '/srv/?a=api&t=can_reserve_time_list'
}
/**
* @description: 可预约日期列表
......@@ -22,7 +22,7 @@ const Api = {
* }>
* }>}
*/
export const canReserveDateListAPI = (params) => fn(fetch.get(Api.CAN_RESERVE_DATE_LIST, params));
export const canReserveDateListAPI = params => fn(fetch.get(Api.CAN_RESERVE_DATE_LIST, params))
/**
* @description: 可预约时段列表
......@@ -39,4 +39,4 @@ export const canReserveDateListAPI = (params) => fn(fetch.get(Api.CAN_RESERVE_DA
* }>
* }>}
*/
export const canReserveTimeListAPI = (params) => fn(fetch.get(Api.CAN_RESERVE_TIME_LIST, params));
export const canReserveTimeListAPI = params => fn(fetch.get(Api.CAN_RESERVE_TIME_LIST, params))
......
......@@ -6,10 +6,10 @@
* @FilePath: /tswj/src/api/wx/config.js
* @Description:
*/
import { fn, fetch } from '@/api/fn';
import { fn, fetch } from '@/api/fn'
const Api = {
WX_JSAPI: '/srv/?a=wx_share',
WX_JSAPI: '/srv/?a=wx_share'
}
/**
......@@ -18,4 +18,4 @@ const Api = {
* @param {string} params.url 当前页面 URL(用于签名)
* @returns {Promise<{code:number,data:any,msg:string}>} 标准返回
*/
export const wxJsAPI = (params) => fn(fetch.get(Api.WX_JSAPI, params));
export const wxJsAPI = params => fn(fetch.get(Api.WX_JSAPI, params))
......
......@@ -11,41 +11,41 @@
* @type {Array<string>}
*/
export const apiList = [
"updateAppMessageShareData",
"updateTimelineShareData",
"onMenuShareTimeline",
"onMenuShareAppMessage",
"onMenuShareQQ",
"onMenuShareWeibo",
"onMenuShareQZone",
"startRecord",
"stopRecord",
"onVoiceRecordEnd",
"playVoice",
"pauseVoice",
"stopVoice",
"onVoicePlayEnd",
"uploadVoice",
"downloadVoice",
"chooseImage",
"previewImage",
"uploadImage",
"downloadImage",
"translateVoice",
"getNetworkType",
"openLocation",
"getLocation",
"hideOptionMenu",
"showOptionMenu",
"hideMenuItems",
"showMenuItems",
"hideAllNonBaseMenuItem",
"showAllNonBaseMenuItem",
"closeWindow",
"scanQRCode",
"chooseWXPay",
"openProductSpecificView",
"addCard",
"chooseCard",
"openCard"
'updateAppMessageShareData',
'updateTimelineShareData',
'onMenuShareTimeline',
'onMenuShareAppMessage',
'onMenuShareQQ',
'onMenuShareWeibo',
'onMenuShareQZone',
'startRecord',
'stopRecord',
'onVoiceRecordEnd',
'playVoice',
'pauseVoice',
'stopVoice',
'onVoicePlayEnd',
'uploadVoice',
'downloadVoice',
'chooseImage',
'previewImage',
'uploadImage',
'downloadImage',
'translateVoice',
'getNetworkType',
'openLocation',
'getLocation',
'hideOptionMenu',
'showOptionMenu',
'hideMenuItems',
'showMenuItems',
'hideAllNonBaseMenuItem',
'showAllNonBaseMenuItem',
'closeWindow',
'scanQRCode',
'chooseWXPay',
'openProductSpecificView',
'addCard',
'chooseCard',
'openCard'
]
......
......@@ -6,10 +6,10 @@
* @FilePath: /tswj/src/api/wx/config.js
* @Description:
*/
import { fn, fetch } from '@/api/fn';
import { fn, fetch } from '@/api/fn'
const Api = {
WX_PAY: '/srv/?a=icbc_pay_wxamp',
WX_PAY: '/srv/?a=icbc_pay_wxamp'
}
/**
......@@ -18,4 +18,4 @@ const Api = {
* @param {string} params.pay_id 预约单支付凭证
* @returns {Promise<{code:number,data:any,msg:string}>} 标准返回(data 为微信支付参数)
*/
export const wxPayAPI = (params) => fn(fetch.post(Api.WX_PAY, params));
export const wxPayAPI = params => fn(fetch.post(Api.WX_PAY, params))
......
......@@ -24,7 +24,7 @@ const pages = [
'pages/weakNetwork/index',
'pages/offlineBookingCode/index',
'pages/offlineBookingList/index',
'pages/offlineBookingDetail/index',
'pages/offlineBookingDetail/index'
]
if (process.env.NODE_ENV === 'development') {
......@@ -32,14 +32,15 @@ if (process.env.NODE_ENV === 'development') {
pages.push('pages/tailwindTest/index')
}
const subpackages = process.env.NODE_ENV === 'development'
? [
{
root: 'pages/demo',
pages: ['index'],
},
]
: []
const subpackages =
process.env.NODE_ENV === 'development'
? [
{
root: 'pages/demo',
pages: ['index']
}
]
: []
export default {
pages,
......@@ -48,6 +49,6 @@ export default {
backgroundTextStyle: 'light',
navigationBarBackgroundColor: '#fff',
navigationBarTitleText: '西园寺预约',
navigationBarTextStyle: 'black',
},
navigationBarTextStyle: 'black'
}
}
......
This diff is collapsed. Click to expand it.
......@@ -11,5 +11,5 @@
@tailwind utilities;
:root {
--nut-primary-color: #A67939;
--nut-primary-color: #a67939;
}
......
.modify-top {
z-index: 36;
position: absolute;
......
......@@ -3,9 +3,9 @@
/* ============ 颜色 ============ */
// 主色调
@base-color: #11D2B1;
@base-color: #11d2b1;
// 文字颜色
@base-font-color: #FFFFFF;
@base-font-color: #ffffff;
// 定义一个映射
#colors() {
......
......@@ -8,30 +8,24 @@
/>
</template>
<script>
import Taro from "@tarojs/taro"
import { defineComponent, onMounted, ref } from "vue"
import { drawImage, drawText, drawBlock, drawLine } from "./utils/draw.js"
import {
toPx,
toRpx,
getRandomId,
getImageInfo,
getLinearColor,
} from "./utils/tools.js"
import Taro from '@tarojs/taro'
import { defineComponent, onMounted, ref } from 'vue'
import { drawImage, drawText, drawBlock, drawLine } from './utils/draw.js'
import { toPx, toRpx, getRandomId, getImageInfo, getLinearColor } from './utils/tools.js'
export default defineComponent({
name: "PosterBuilder",
name: 'PosterBuilder',
props: {
showLoading: {
type: Boolean,
default: false,
default: false
},
config: {
type: Object,
default: () => ({}),
},
default: () => ({})
}
},
emits: ["success", "fail"],
emits: ['success', 'fail'],
setup(props, context) {
const count = ref(1)
const {
......@@ -41,7 +35,7 @@ export default defineComponent({
texts = [],
blocks = [],
lines = [],
debug = false,
debug = false
} = props.config || {}
const canvasId = getRandomId()
......@@ -51,11 +45,9 @@ export default defineComponent({
* @param {Array} images = imgTask
* @return {Promise} downloadImagePromise
*/
const initImages = (images) => {
const imagesTemp = images.filter((item) => item.url)
const drawList = imagesTemp.map((item, index) =>
getImageInfo(item, index)
)
const initImages = images => {
const imagesTemp = images.filter(item => item.url)
const drawList = imagesTemp.map((item, index) => getImageInfo(item, index))
return Promise.all(drawList)
}
......@@ -64,15 +56,15 @@ export default defineComponent({
* @return {Promise} resolve 里返回其 dom 和实例
*/
const initCanvas = () =>
new Promise((resolve) => {
new Promise(resolve => {
setTimeout(() => {
const pageInstance = Taro.getCurrentInstance()?.page || {} // 拿到当前页面实例
const query = Taro.createSelectorQuery().in(pageInstance) // 确定在当前页面内匹配子元素
query
.select(`#${canvasId}`)
.fields({ node: true, size: true, context: true }, (res) => {
.fields({ node: true, size: true, context: true }, res => {
const canvas = res.node
const ctx = canvas.getContext("2d")
const ctx = canvas.getContext('2d')
resolve({ ctx, canvas })
})
.exec()
......@@ -83,30 +75,30 @@ export default defineComponent({
* @description 保存绘制的图片
* @param { object } config
*/
const getTempFile = (canvas) => {
const getTempFile = canvas => {
Taro.canvasToTempFilePath(
{
canvas,
success: (result) => {
success: result => {
Taro.hideLoading()
context.emit("success", result)
context.emit('success', result)
},
fail: (error) => {
fail: error => {
const { errMsg } = error
if (errMsg === "canvasToTempFilePath:fail:create bitmap failed") {
if (errMsg === 'canvasToTempFilePath:fail:create bitmap failed') {
count.value += 1
if (count.value <= 3) {
getTempFile(canvas)
} else {
Taro.hideLoading()
Taro.showToast({
icon: "none",
title: errMsg || "绘制海报失败",
icon: 'none',
title: errMsg || '绘制海报失败'
})
context.emit("fail", errMsg)
context.emit('fail', errMsg)
}
}
},
}
},
context
)
......@@ -116,7 +108,7 @@ export default defineComponent({
* step2: 开始绘制任务
* @param { Array } drawTasks 待绘制任务
*/
const startDrawing = async (drawTasks) => {
const startDrawing = async drawTasks => {
// TODO: check
// const configHeight = getHeight(config)
const { ctx, canvas } = await initCanvas()
......@@ -135,22 +127,22 @@ export default defineComponent({
// 将要画的方块、文字、线条放进队列数组
const queue = drawTasks
.concat(
texts.map((item) => {
item.type = "text"
texts.map(item => {
item.type = 'text'
item.zIndex = item.zIndex || 0
return item
})
)
.concat(
blocks.map((item) => {
item.type = "block"
blocks.map(item => {
item.type = 'block'
item.zIndex = item.zIndex || 0
return item
})
)
.concat(
lines.map((item) => {
item.type = "line"
lines.map(item => {
item.type = 'line'
item.zIndex = item.zIndex || 0
return item
})
......@@ -162,15 +154,15 @@ export default defineComponent({
canvas,
ctx,
toPx,
toRpx,
toRpx
}
if (queue[i].type === "image") {
if (queue[i].type === 'image') {
await drawImage(queue[i], drawOptions)
} else if (queue[i].type === "text") {
} else if (queue[i].type === 'text') {
drawText(queue[i], drawOptions)
} else if (queue[i].type === "block") {
} else if (queue[i].type === 'block') {
drawBlock(queue[i], drawOptions)
} else if (queue[i].type === "line") {
} else if (queue[i].type === 'line') {
drawLine(queue[i], drawOptions)
}
}
......@@ -182,21 +174,22 @@ export default defineComponent({
// start: 初始化 canvas 实例 && 下载图片资源
const init = () => {
if (props.showLoading)
Taro.showLoading({ mask: true, title: "生成中..." })
if (props.showLoading) {
Taro.showLoading({ mask: true, title: '生成中...' })
}
if (props.config?.images?.length) {
initImages(props.config.images)
.then((result) => {
.then(result => {
// 1. 下载图片资源
startDrawing(result)
})
.catch((err) => {
.catch(err => {
Taro.hideLoading()
Taro.showToast({
icon: "none",
title: err.errMsg || "下载图片失败",
icon: 'none',
title: err.errMsg || '下载图片失败'
})
context.emit("fail", err)
context.emit('fail', err)
})
} else {
startDrawing([])
......@@ -211,8 +204,8 @@ export default defineComponent({
canvasId,
debug,
width,
height,
height
}
},
}
})
</script>
......
......@@ -2,7 +2,9 @@ import { getLinearColor, getTextX, toPx } from './tools'
const drawRadiusRect = ({ x, y, w, h, r }, { ctx }) => {
const minSize = Math.min(w, h)
if (r > minSize / 2) r = minSize / 2
if (r > minSize / 2) {
r = minSize / 2
}
ctx.beginPath()
ctx.moveTo(x + r, y)
ctx.arcTo(x + w, y, x + w, y + h, r)
......@@ -120,10 +122,7 @@ const drawSingleText = (drawData, drawOptions) => {
if (restWidth < 0) {
if (line === lineNum) {
if (
restWidth + ctx.measureText(text[i + 1]).width >
ctx.measureText('...').width
) {
if (restWidth + ctx.measureText(text[i + 1]).width > ctx.measureText('...').width) {
fillText = `${fillText}...`
} else {
fillText = `${fillText.substr(0, fillText.length - 1)}...`
......@@ -145,11 +144,7 @@ const drawSingleText = (drawData, drawOptions) => {
}
textArr.forEach((item, index) =>
ctx.fillText(
item,
getTextX(textAlign, x, width),
y + (lineHeight || fontSize) * index
)
ctx.fillText(item, getTextX(textAlign, x, width), y + (lineHeight || fontSize) * index)
)
ctx.restore()
......@@ -172,7 +167,7 @@ export function drawText(params, drawOptions) {
const { x = 0, y = 0, text, baseLine } = params
if (Object.prototype.toString.call(text) === '[object Array]') {
const preText = { x, y, baseLine }
text.forEach((item) => {
text.forEach(item => {
preText.x += item.marginLeft || 0
const textWidth = drawSingleText(
Object.assign(item, { ...preText, y: y + (item.marginTop || 0) }),
......@@ -188,7 +183,9 @@ export function drawText(params, drawOptions) {
export function drawLine(drawData, drawOptions) {
const { startX, startY, endX, endY, color, width } = drawData
const { ctx } = drawOptions
if (!width) return
if (!width) {
return
}
ctx.save()
ctx.beginPath()
ctx.strokeStyle = color
......@@ -225,10 +222,7 @@ export function drawBlock(data, drawOptions) {
let textY = 0
if (text) {
const textWidth = getTextWidth(
typeof text.text === 'string' ? text : text.text,
drawOptions
)
const textWidth = getTextWidth(typeof text.text === 'string' ? text : text.text, drawOptions)
blockWidth = textWidth > width ? textWidth : width
blockWidth += paddingLeft + paddingLeft
......@@ -296,7 +290,7 @@ export function drawBlock(data, drawOptions) {
}
export const drawImage = (data, drawOptions) =>
new Promise((resolve) => {
new Promise(resolve => {
const { canvas, ctx } = drawOptions
const {
x,
......
......@@ -59,8 +59,7 @@ export const getFactor = () => {
* @param {number} factor 换算系数
* @returns {number} px 值(整数)
*/
export const toPx = (rpx, factor = getFactor()) =>
parseInt(String(rpx * factor), 10)
export const toPx = (rpx, factor = getFactor()) => parseInt(String(rpx * factor), 10)
/**
* @description px 转 rpx
......@@ -68,8 +67,7 @@ export const toPx = (rpx, factor = getFactor()) =>
* @param {number} factor 换算系数
* @returns {number} rpx 值(整数)
*/
export const toRpx = (px, factor = getFactor()) =>
parseInt(String(px / factor), 10)
export const toRpx = (px, factor = getFactor()) => parseInt(String(px / factor), 10)
/**
* @description 下载图片到本地临时路径(避免跨域/协议限制)
......@@ -80,17 +78,15 @@ export const toRpx = (px, factor = getFactor()) =>
export function downImage(url) {
return new Promise((resolve, reject) => {
const wx_user_data_path =
(typeof wx !== 'undefined' && wx && wx.env && wx.env.USER_DATA_PATH)
typeof wx !== 'undefined' && wx && wx.env && wx.env.USER_DATA_PATH
? wx.env.USER_DATA_PATH
: ''
const is_local_user_path = wx_user_data_path
? new RegExp(wx_user_data_path).test(url)
: false
const is_local_user_path = wx_user_data_path ? new RegExp(wx_user_data_path).test(url) : false
if (/^http/.test(url) && !is_local_user_path) {
Taro.downloadFile({
url: mapHttpToHttps(url),
success: (res) => {
success: res => {
if (res.statusCode === 200) {
resolve(res.tempFilePath)
} else {
......@@ -116,9 +112,9 @@ export function downImage(url) {
export const getImageInfo = (item, index) =>
new Promise((resolve, reject) => {
const { x, y, width, height, url, zIndex } = item
downImage(url).then((imgPath) =>
downImage(url).then(imgPath =>
Taro.getImageInfo({ src: imgPath })
.then((imgInfo) => {
.then(imgInfo => {
let sx
let sy
const borderRadius = item.borderRadius || 0
......@@ -150,7 +146,7 @@ export const getImageInfo = (item, index) =>
}
resolve(result)
})
.catch((err) => {
.catch(err => {
reject(err)
})
)
......
<template>
<view class="index-nav" :class="[`is-${position}`]">
<view class="nav-logo is-home" :class="{ 'is-active': active === 'home' }" @tap="() => on_select('home')">
<view
class="nav-logo is-home"
:class="{ 'is-active': active === 'home' }"
@tap="() => on_select('home')"
>
<view class="nav-icon-wrap">
<image class="nav-icon" :src="icons?.home" mode="aspectFit" />
</view>
......@@ -9,7 +13,10 @@
<view
class="nav-logo is-code"
:class="[{ 'is-active': active === 'code' }, { 'is-center-raised': center_variant === 'raised' }]"
:class="[
{ 'is-active': active === 'code' },
{ 'is-center-raised': center_variant === 'raised' }
]"
@tap="() => on_select('code')"
>
<view class="nav-icon-wrap">
......@@ -23,7 +30,11 @@
<text class="nav-text">预约码</text>
</view>
<view class="nav-logo is-me" :class="{ 'is-active': active === 'me' }" @tap="() => on_select('me')">
<view
class="nav-logo is-me"
:class="{ 'is-active': active === 'me' }"
@tap="() => on_select('me')"
>
<view class="nav-icon-wrap">
<image class="nav-icon" :src="icons?.me" mode="aspectFit" />
</view>
......@@ -58,8 +69,10 @@ const props = defineProps({
const emit = defineEmits(['select'])
const on_select = (key) => {
if (!props.allow_active_tap && props.active && key === props.active) return
const on_select = key => {
if (!props.allow_active_tap && props.active && key === props.active) {
return
}
emit('select', key)
}
</script>
......@@ -74,12 +87,12 @@ const on_select = (key) => {
padding-bottom: calc(0rpx + constant(safe-area-inset-bottom));
padding-bottom: calc(0rpx + env(safe-area-inset-bottom));
box-sizing: border-box;
background: #FFFFFF;
background: #ffffff;
box-shadow: 0 -8rpx 8rpx 0 rgba(0, 0, 0, 0.1);
display: flex;
align-items: flex-end;
justify-content: space-around;
color: #A67939;
color: #a67939;
z-index: 99;
&.is-fixed {
......
......@@ -16,8 +16,8 @@
<image :src="icon_2" />
</view>
</view>
<view style="color: red; margin-top: 32rpx;">{{ userinfo.datetime }}</view>
<view style="color: #999; font-size: 24rpx; margin-top: 10rpx;">(离线模式)</view>
<view style="color: red; margin-top: 32rpx">{{ userinfo.datetime }}</view>
<view style="color: #999; font-size: 24rpx; margin-top: 10rpx">(离线模式)</view>
</view>
<view class="user-list">
<view
......@@ -27,14 +27,15 @@
:class="[
'user-item',
select_index === index ? 'checked' : '',
userList.length > 1 && item.sort ? 'border' : '',
]">
userList.length > 1 && item.sort ? 'border' : ''
]"
>
{{ item.name }}
</view>
</view>
</view>
<view v-else class="no-qrcode">
<image :src="icon_3" style="width: 320rpx; height: 320rpx;" />
<image :src="icon_3" style="width: 320rpx; height: 320rpx" />
<view class="no-qrcode-title">本地无缓存预约记录</view>
</view>
</view>
......@@ -54,101 +55,104 @@ const props = defineProps({
type: Array,
default: () => []
}
});
})
const select_index = ref(0);
const userList = ref([]);
const qrCodeImages = ref({}); // 存储生成的二维码图片 base64
const select_index = ref(0)
const userList = ref([])
const qrCodeImages = ref({}) // 存储生成的二维码图片 base64
const prevCode = () => {
select_index.value = select_index.value - 1;
select_index.value = select_index.value - 1
if (select_index.value < 0) {
select_index.value = userList.value.length - 1;
select_index.value = userList.value.length - 1
}
};
}
const nextCode = () => {
select_index.value = select_index.value + 1;
select_index.value = select_index.value + 1
if (select_index.value > userList.value.length - 1) {
select_index.value = 0;
select_index.value = 0
}
};
}
function replaceMiddleCharacters(inputString) {
if (!inputString || inputString.length < 15) {
return inputString;
return inputString
}
const start = Math.floor((inputString.length - 8) / 2);
const end = start + 8;
const replacement = '*'.repeat(8);
return inputString.substring(0, start) + replacement + inputString.substring(end);
const start = Math.floor((inputString.length - 8) / 2)
const end = start + 8
const replacement = '*'.repeat(8)
return inputString.substring(0, start) + replacement + inputString.substring(end)
}
const formatId = (id) => replaceMiddleCharacters(id);
const formatId = id => replaceMiddleCharacters(id)
const userinfo = computed(() => {
return {
name: userList.value[select_index.value]?.name,
id: formatId(userList.value[select_index.value]?.id_number),
datetime: userList.value[select_index.value]?.datetime,
};
});
datetime: userList.value[select_index.value]?.datetime
}
})
const currentQrCodeUrl = computed(() => {
const key = userList.value[select_index.value]?.qr_code;
return qrCodeImages.value[key] || '';
const key = userList.value[select_index.value]?.qr_code
return qrCodeImages.value[key] || ''
})
const selectUser = (index) => {
select_index.value = index;
const selectUser = index => {
select_index.value = index
}
const generateQrCodes = () => {
for (const item of userList.value) {
if (item.qr_code && !qrCodeImages.value[item.qr_code]) {
try {
// 使用 create + SVG 手动生成,避免 Taro 中 Canvas 依赖问题
const qr = QRCode.create(item.qr_code, { errorCorrectionLevel: 'M' });
const size = qr.modules.size;
let d = '';
for (let row = 0; row < size; row++) {
for (let col = 0; col < size; col++) {
if (qr.modules.get(col, row)) {
d += `M${col},${row}h1v1h-1z`;
}
}
}
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}"><path d="${d}" fill="#000"/></svg>`;
// 转 Base64
const buffer = new ArrayBuffer(svg.length);
const view = new Uint8Array(buffer);
for (let i = 0; i < svg.length; i++) {
view[i] = svg.charCodeAt(i);
}
const b64 = Taro.arrayBufferToBase64(buffer);
qrCodeImages.value[item.qr_code] = `data:image/svg+xml;base64,${b64}`;
} catch (err) {
console.error('QR Gen Error', err);
for (const item of userList.value) {
if (item.qr_code && !qrCodeImages.value[item.qr_code]) {
try {
// 使用 create + SVG 手动生成,避免 Taro 中 Canvas 依赖问题
const qr = QRCode.create(item.qr_code, { errorCorrectionLevel: 'M' })
const size = qr.modules.size
let d = ''
for (let row = 0; row < size; row++) {
for (let col = 0; col < size; col++) {
if (qr.modules.get(col, row)) {
d += `M${col},${row}h1v1h-1z`
}
}
}
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${size} ${size}"><path d="${d}" fill="#000"/></svg>`
// 转 Base64
const buffer = new ArrayBuffer(svg.length)
const view = new Uint8Array(buffer)
for (let i = 0; i < svg.length; i++) {
view[i] = svg.charCodeAt(i)
}
const b64 = Taro.arrayBufferToBase64(buffer)
qrCodeImages.value[item.qr_code] = `data:image/svg+xml;base64,${b64}`
} catch (err) {
console.error('QR Gen Error', err)
}
}
}
}
onMounted(() => {
if (props.list && props.list.length > 0) {
userList.value = props.list;
generateQrCodes();
}
});
if (props.list && props.list.length > 0) {
userList.value = props.list
generateQrCodes()
}
})
watch(() => props.list, (newVal) => {
watch(
() => props.list,
newVal => {
if (newVal && newVal.length > 0) {
userList.value = newVal;
generateQrCodes();
userList.value = newVal
generateQrCodes()
}
}, { deep: true });
},
{ deep: true }
)
</script>
<style lang="less">
......@@ -159,12 +163,12 @@ watch(() => props.list, (newVal) => {
flex-direction: column;
justify-content: center;
align-items: center;
background-color: #FFF;
background-color: #fff;
border-radius: 16rpx;
box-shadow: 0 0 29rpx 0 rgba(106,106,106,0.27);
box-shadow: 0 0 29rpx 0 rgba(106, 106, 106, 0.27);
.user-info {
color: #A6A6A6;
color: #a6a6a6;
font-size: 37rpx;
margin-top: 16rpx;
margin-bottom: 16rpx;
......@@ -180,21 +184,25 @@ watch(() => props.list, (newVal) => {
align-items: center;
.left {
image {
width: 56rpx; height: 56rpx; margin-right: 16rpx;
width: 56rpx;
height: 56rpx;
margin-right: 16rpx;
}
}
.center {
border: 2rpx solid #D1D1D1;
border: 2rpx solid #d1d1d1;
border-radius: 40rpx;
padding: 46rpx;
position: relative;
image {
width: 400rpx; height: 400rpx;
width: 400rpx;
height: 400rpx;
}
}
.right {
image {
width: 56rpx; height: 56rpx;
width: 56rpx;
height: 56rpx;
margin-left: 16rpx;
}
}
......@@ -208,13 +216,13 @@ watch(() => props.list, (newVal) => {
.user-item {
position: relative;
padding: 8rpx 16rpx;
border: 2rpx solid #A67939;
border: 2rpx solid #a67939;
margin: 8rpx;
border-radius: 10rpx;
color: #A67939;
color: #a67939;
&.checked {
color: #FFF;
background-color: #A67939;
color: #fff;
background-color: #a67939;
}
&.border {
margin-right: 16rpx;
......@@ -224,7 +232,7 @@ watch(() => props.list, (newVal) => {
top: calc(50% - 16rpx);
content: '';
height: 32rpx;
border-right: 2rpx solid #A67939;
border-right: 2rpx solid #a67939;
}
}
}
......@@ -238,7 +246,7 @@ watch(() => props.list, (newVal) => {
margin-bottom: 32rpx;
.no-qrcode-title {
color: #A67939;
color: #a67939;
font-size: 34rpx;
}
}
......
This diff is collapsed. Click to expand it.
......@@ -16,7 +16,10 @@
</view>
<view class="center">
<image :src="userinfo.qr_code_url" mode="aspectFit" />
<view v-if="useStatus === STATUS_CODE.CANCELED || useStatus === STATUS_CODE.USED" class="qrcode-used">
<view
v-if="useStatus === STATUS_CODE.CANCELED || useStatus === STATUS_CODE.USED"
class="qrcode-used"
>
<view class="overlay"></view>
<text class="status-text">二维码{{ qr_code_status[useStatus] }}</text>
</view>
......@@ -25,11 +28,14 @@
<!-- <image src="https://cdn.ipadbiz.cn/xys/booking/%E5%8F%B3@2x.png"> -->
</view>
</view>
<view style="color: red; margin-top: 32rpx;">{{ userinfo.datetime }}</view>
<view style="color: red; margin-top: 32rpx">{{ userinfo.datetime }}</view>
</view>
</view>
<view v-else class="no-qrcode">
<image src="https://cdn.ipadbiz.cn/xys/booking/%E6%9A%82%E6%97%A0@2x.png" style="width: 320rpx; height: 320rpx;" />
<image
src="https://cdn.ipadbiz.cn/xys/booking/%E6%9A%82%E6%97%A0@2x.png"
style="width: 320rpx; height: 320rpx"
/>
<view class="no-qrcode-title">您还没有预约过今天参观</view>
</view>
</view>
......@@ -37,9 +43,9 @@
<script setup>
import { ref, onMounted, watch, onUnmounted } from 'vue'
import { formatDatetime } from '@/utils/tools';
import { formatDatetime } from '@/utils/tools'
import { qrcodeStatusAPI, queryQrCodeAPI } from '@/api/index'
import BASE_URL from '@/utils/config';
import BASE_URL from '@/utils/config'
const props = defineProps({
id: {
......@@ -50,42 +56,44 @@ const props = defineProps({
type: Number,
default: 1
}
});
})
const userinfo = ref({});
const userinfo = ref({})
const replaceMiddleCharacters = (input_string) => {
const replaceMiddleCharacters = input_string => {
if (!input_string || input_string.length < 15) {
return input_string;
return input_string
}
const start = Math.floor((input_string.length - 8) / 2);
const end = start + 8;
const replacement = '*'.repeat(8);
return input_string.substring(0, start) + replacement + input_string.substring(end);
const start = Math.floor((input_string.length - 8) / 2)
const end = start + 8
const replacement = '*'.repeat(8)
return input_string.substring(0, start) + replacement + input_string.substring(end)
}
const formatId = (id) => replaceMiddleCharacters(id);
const formatId = id => replaceMiddleCharacters(id)
const useStatus = ref('0');
const useStatus = ref('0')
const is_loading = ref(false)
let is_destroyed = false
const qr_code_status = {
'1': '未激活',
'3': '待使用',
'5': '被取消',
'7': '已使用',
};
1: '未激活',
3: '待使用',
5: '被取消',
7: '已使用'
}
const STATUS_CODE = {
APPLY: '1',
SUCCESS: '3',
CANCELED: '5',
USED: '7',
};
USED: '7'
}
const build_qr_code_url = (qr_code) => {
if (!qr_code) return ''
const build_qr_code_url = qr_code => {
if (!qr_code) {
return ''
}
return `${BASE_URL}/admin?m=srv&a=get_qrcode&key=${encodeURIComponent(String(qr_code))}`
}
......@@ -95,8 +103,10 @@ const build_qr_code_url = (qr_code) => {
* @return {*} 格式化后的数据
*/
const normalize_item = (raw) => {
if (!raw || typeof raw !== 'object') return null
const normalize_item = raw => {
if (!raw || typeof raw !== 'object') {
return null
}
const qr_code = raw.qr_code ? String(raw.qr_code) : ''
const id_number = raw.id_number ? String(raw.id_number) : ''
return {
......@@ -104,7 +114,7 @@ const normalize_item = (raw) => {
qr_code,
qr_code_url: build_qr_code_url(qr_code),
datetime: formatDatetime({ begin_time: raw.begin_time, end_time: raw.end_time }),
id: formatId(id_number),
id: formatId(id_number)
}
}
......@@ -123,13 +133,21 @@ const reset_state = () => {
* @return {*} 状态码
*/
const load_qr_code_status = async (qr_code) => {
if (!qr_code) return
const load_qr_code_status = async qr_code => {
if (!qr_code) {
return
}
const res = await qrcodeStatusAPI({ qr_code })
if (is_destroyed) return
if (!res || res.code !== 1) return
if (is_destroyed) {
return
}
if (!res || res.code !== 1) {
return
}
const status = res?.data?.status
if (status === undefined || status === null) return
if (status === undefined || status === null) {
return
}
useStatus.value = String(status)
}
......@@ -139,7 +157,7 @@ const load_qr_code_status = async (qr_code) => {
* @return {*} 预约码卡信息
*/
const load_qr_code_info = async (id_number) => {
const load_qr_code_info = async id_number => {
const id = String(id_number || '').trim()
if (!id) {
reset_state()
......@@ -148,9 +166,13 @@ const load_qr_code_info = async (id_number) => {
is_loading.value = true
const params = { id_number: id }
if (props.id_type) params.id_type = props.id_type
if (props.id_type) {
params.id_type = props.id_type
}
const res = await queryQrCodeAPI(params)
if (is_destroyed) return
if (is_destroyed) {
return
}
is_loading.value = false
if (!res || res.code !== 1 || !res.data) {
......@@ -180,7 +202,9 @@ onMounted(() => {
watch(
() => [props.id, props.id_type],
([val]) => {
if (is_loading.value) return
if (is_loading.value) {
return
}
load_qr_code_info(val)
}
)
......@@ -194,12 +218,12 @@ watch(
flex-direction: column;
justify-content: center;
align-items: center;
background-color: #FFF;
background-color: #fff;
border-radius: 16rpx;
box-shadow: 0 0 29rpx 0 rgba(106,106,106,0.27);
box-shadow: 0 0 29rpx 0 rgba(106, 106, 106, 0.27);
.user-info {
color: #A6A6A6;
color: #a6a6a6;
font-size: 37rpx;
margin-top: 16rpx;
margin-bottom: 16rpx;
......@@ -208,41 +232,42 @@ watch(
display: flex;
align-items: center;
.center {
border: 2rpx solid #D1D1D1;
border: 2rpx solid #d1d1d1;
border-radius: 40rpx;
padding: 16rpx;
position: relative;
image {
width: 480rpx; height: 480rpx;
width: 480rpx;
height: 480rpx;
}
.qrcode-used {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 40rpx;
overflow: hidden;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-radius: 40rpx;
overflow: hidden;
.overlay {
width: 100%;
height: 100%;
background-image: url('https://cdn.ipadbiz.cn/xys/booking/southeast.jpeg');
background-size: contain;
opacity: 0.9;
}
.overlay {
width: 100%;
height: 100%;
background-image: url('https://cdn.ipadbiz.cn/xys/booking/southeast.jpeg');
background-size: contain;
opacity: 0.9;
}
.status-text {
color: #A67939;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 38rpx;
white-space: nowrap;
font-weight: bold;
z-index: 10;
}
.status-text {
color: #a67939;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 38rpx;
white-space: nowrap;
font-weight: bold;
z-index: 10;
}
}
}
}
......@@ -256,7 +281,7 @@ watch(
margin-bottom: 32rpx;
.no-qrcode-title {
color: #A67939;
color: #a67939;
font-size: 34rpx;
}
}
......
......@@ -14,13 +14,27 @@
</view>
<view class="booking-list-item-body">
<view class="booking-num">
<view class="num-body van-ellipsis">预约人数:<text>{{ reserve_info.total_qty }} 人</text>&nbsp;<text>({{ reserve_info.person_name }})</text></view>
<view v-if="(reserve_info.status === CodeStatus.SUCCESS || reserve_info.status === CodeStatus.USED || reserve_info.status === CodeStatus.CANCEL)">
<view class="num-body van-ellipsis"
>预约人数:<text>{{ reserve_info.total_qty }} 人</text>&nbsp;<text
>({{ reserve_info.person_name }})</text
></view
>
<view
v-if="
reserve_info.status === CodeStatus.SUCCESS ||
reserve_info.status === CodeStatus.USED ||
reserve_info.status === CodeStatus.CANCEL
"
>
<IconFont name="rect-right" />
</view>
</view>
<view class="booking-price">支付金额:<text>¥ {{ reserve_info.total_amt }}</text></view>
<view class="booking-time">下单时间:<text>{{ reserve_info.order_time }}</text></view>
<view class="booking-price"
>支付金额:<text>¥ {{ reserve_info.total_amt }}</text></view
>
<view class="booking-time"
>下单时间:<text>{{ reserve_info.order_time }}</text></view
>
</view>
<view v-if="is_pay_pending" class="booking-list-item-footer" @tap.stop>
<view v-if="countdown_seconds > 0" class="countdown">剩余支付时间:{{ countdown_text }}</view>
......@@ -37,26 +51,26 @@ import { IconFont } from '@nutui/icons-vue-taro'
import { useGo } from '@/hooks/useGo'
import { wechat_pay } from '@/utils/wechatPay'
const go = useGo();
const go = useGo()
const props = defineProps({
data: {
type: Object,
default: () => ({}),
default: () => ({})
},
detail_path: {
type: String,
default: '/bookingDetail',
default: '/bookingDetail'
},
is_offline: {
type: Boolean,
default: false,
},
});
default: false
}
})
const reserve_info = computed(() => props.data);
const reserve_info = computed(() => props.data)
const is_offline = computed(() => props.is_offline);
const is_offline = computed(() => props.is_offline)
/**
* @description 预约码状态枚举(与后端约定)
......@@ -78,7 +92,9 @@ const CodeStatus = {
*/
const is_pay_pending = computed(() => {
if (is_offline.value) return false
if (is_offline.value) {
return false
}
return reserve_info.value?.status === CodeStatus.APPLY && !!reserve_info.value?.pay_id
})
......@@ -89,7 +105,7 @@ const countdown_seconds = ref(0)
* @param {number|string} n 数字
* @returns {string} 两位字符串
*/
const format_two_digits = (n) => {
const format_two_digits = n => {
const num = Number(n) || 0
return num < 10 ? `0${num}` : String(num)
}
......@@ -106,9 +122,11 @@ const countdown_text = computed(() => {
* @param {string} created_time 创建时间字符串
* @returns {number} 毫秒时间戳;解析失败返回 0
*/
const parse_created_time_ms = (created_time) => {
const parse_created_time_ms = created_time => {
const raw = String(created_time || '')
if (!raw) return 0
if (!raw) {
return 0
}
const fixed = raw.replace(/-/g, '/')
const date = new Date(fixed)
const time = date.getTime()
......@@ -157,7 +175,9 @@ const update_countdown = () => {
const start_countdown = () => {
stop_countdown()
update_countdown()
if (countdown_seconds.value <= 0) return
if (countdown_seconds.value <= 0) {
return
}
countdown_timer = setInterval(update_countdown, 1000)
}
......@@ -167,8 +187,10 @@ let is_showing_pay_modal = false
* @param {string} content 弹窗内容
* @returns {Promise<boolean>} true=继续支付,false=取消
*/
const show_pay_modal = async (content) => {
if (is_showing_pay_modal) return false
const show_pay_modal = async content => {
if (is_showing_pay_modal) {
return false
}
is_showing_pay_modal = true
try {
const res = await Taro.showModal({
......@@ -176,7 +198,7 @@ const show_pay_modal = async (content) => {
content: content || '支付未完成',
showCancel: true,
cancelText: '取消',
confirmText: '继续支付',
confirmText: '继续支付'
})
return !!res?.confirm
} finally {
......@@ -190,7 +212,9 @@ const show_pay_modal = async (content) => {
*/
const onRepay = async () => {
if (!is_pay_pending.value) return
if (!is_pay_pending.value) {
return
}
if (countdown_seconds.value <= 0) {
Taro.showToast({ title: '支付已超时', icon: 'none' })
return
......@@ -213,7 +237,7 @@ const onRepay = async () => {
* @param {string} status 订单状态码
* @returns {{key:string,value:string}} 展示状态(key 用于 class)
*/
const formatStatus = (status) => {
const formatStatus = status => {
switch (status) {
case CodeStatus.APPLY:
return {
......@@ -251,12 +275,12 @@ const formatStatus = (status) => {
value: '退款中'
}
default:
return { key: '', value: '' }
return { key: '', value: '' }
}
}
const status_info = computed(() => {
return formatStatus(reserve_info.value?.status) || { key: '', value: '' }
return formatStatus(reserve_info.value?.status) || { key: '', value: '' }
})
/**
......@@ -265,10 +289,14 @@ const status_info = computed(() => {
* @param {Object} item 预约记录
* @returns {void} 无返回值
*/
const goToDetail = (item) => {
// 只有成功、已使用、已取消(退款成功)才跳转详情
if (item.status === CodeStatus.SUCCESS || item.status === CodeStatus.USED || item.status === CodeStatus.CANCEL) {
go(props.detail_path, { pay_id: item.pay_id });
const goToDetail = item => {
// 只有成功、已使用、已取消(退款成功)才跳转详情
if (
item.status === CodeStatus.SUCCESS ||
item.status === CodeStatus.USED ||
item.status === CodeStatus.CANCEL
) {
go(props.detail_path, { pay_id: item.pay_id })
}
}
......@@ -278,14 +306,18 @@ const goToDetail = (item) => {
* - 退出待支付:清空倒计时
*/
watch(is_pay_pending, (val) => {
if (val) {
start_countdown()
} else {
countdown_seconds.value = 0
stop_countdown()
}
}, { immediate: true })
watch(
is_pay_pending,
val => {
if (val) {
start_countdown()
} else {
countdown_seconds.value = 0
stop_countdown()
}
},
{ immediate: true }
)
onUnmounted(() => {
stop_countdown()
......@@ -294,71 +326,73 @@ onUnmounted(() => {
<style lang="less">
.booking-list-item {
background-color: #FFF;
background-color: #fff;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 32rpx;
box-shadow: 0 0 30rpx 0 rgba(106,106,106,0.1);
box-shadow: 0 0 30rpx 0 rgba(106, 106, 106, 0.1);
.booking-list-item-header {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: 16rpx;
border-bottom: 2rpx dashed #E6E6E6;
border-bottom: 2rpx dashed #e6e6e6;
margin-bottom: 16rpx;
font-size: 32rpx;
font-weight: bold;
color: #333;
.status {
font-size: 27rpx;
font-weight: normal;
padding: 4rpx 12rpx;
border-radius: 8rpx;
&.offline {
color: #999;
background-color: #EEE;
}
font-size: 27rpx;
font-weight: normal;
padding: 4rpx 12rpx;
border-radius: 8rpx;
&.success {
color: #A67939;
background-color: #FBEEDC;
}
&.cancel {
color: #999;
background-color: #EEE;
}
&.used {
color: #477F3D;
background-color: #E5EFE3;
}
&.offline {
color: #999;
background-color: #eee;
}
&.success {
color: #a67939;
background-color: #fbeedc;
}
&.cancel {
color: #999;
background-color: #eee;
}
&.used {
color: #477f3d;
background-color: #e5efe3;
}
}
}
.booking-list-item-body {
.booking-num {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
color: #666;
.num-body {
span, text {
color: #A67939;
font-weight: bold;
}
}
}
.booking-price, .booking-time {
color: #999;
font-size: 29rpx;
margin-bottom: 10rpx;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
color: #666;
.num-body {
span,
text {
color: #333;
color: #a67939;
font-weight: bold;
}
}
}
.booking-price,
.booking-time {
color: #999;
font-size: 29rpx;
margin-bottom: 10rpx;
text {
color: #333;
}
}
}
......@@ -369,7 +403,7 @@ onUnmounted(() => {
margin-top: 16rpx;
.countdown {
color: #A67939;
color: #a67939;
font-size: 28rpx;
&.timeout {
......@@ -380,8 +414,8 @@ onUnmounted(() => {
.repay-btn {
padding: 8rpx 20rpx;
border-radius: 12rpx;
background-color: #A67939;
color: #FFF;
background-color: #a67939;
color: #fff;
font-size: 28rpx;
}
}
......
var getDaysInOneMonth = function (year, month) {
let _month = parseInt(month, 10);
let d = new Date(year, _month, 0);
return d.getDate();
const getDaysInOneMonth = function (year, month) {
const _month = parseInt(month, 10)
const d = new Date(year, _month, 0)
return d.getDate()
}
var dateDate = function (date) {
let year = date && date.getFullYear();
let month = date && date.getMonth() + 1;
let day = date && date.getDate();
let hours = date && date.getHours();
let minutes = date && date.getMinutes();
const dateDate = function (date) {
const year = date && date.getFullYear()
const month = date && date.getMonth() + 1
const day = date && date.getDate()
const hours = date && date.getHours()
const minutes = date && date.getMinutes()
return {
year, month, day, hours, minutes
year,
month,
day,
hours,
minutes
}
}
var dateTimePicker = function (startyear, endyear) {
const dateTimePicker = function (startyear, endyear) {
// 获取date time 年份,月份,天数,小时,分钟推后30分
const years = [];
const months = [];
const hours = [];
const minutes = [];
const years = []
const months = []
const hours = []
const minutes = []
for (let i = startyear; i <= endyear; i++) {
years.push({
name: i + '年',
name: `${i}年`,
id: i
});
})
}
//获取月份
for (let i = 1; i <= 12; i++) {
if (i < 10) {
i = "0" + i;
i = `0${i}`
}
months.push({
name: i + '月',
name: `${i}月`,
id: i
});
})
}
//获取小时
for (let i = 0; i < 24; i++) {
if (i < 10) {
i = "0" + i;
i = `0${i}`
}
hours.push({
name: i + '时',
name: `${i}时`,
id: i
});
})
}
//获取分钟
for (let i = 0; i < 60; i++) {
if (i < 10) {
i = "0" + i;
i = `0${i}`
}
minutes.push({
name: i + '分',
name: `${i}分`,
id: i
});
})
}
return function (_year, _month) {
const days = [];
_year = parseInt(_year);
_month = parseInt(_month);
const days = []
_year = parseInt(_year)
_month = parseInt(_month)
//获取日期
for (let i = 1; i <= getDaysInOneMonth(_year, _month); i++) {
if (i < 10) {
i = "0" + i;
i = `0${i}`
}
days.push({
name: i + '日',
name: `${i}日`,
id: i
});
})
}
return [years, months, days, hours, minutes];
return [years, months, days, hours, minutes]
}
}
export {
dateTimePicker,
getDaysInOneMonth,
dateDate
}
export { dateTimePicker, getDaysInOneMonth, dateDate }
......
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
......@@ -5,7 +5,7 @@
* @FilePath: /xyxBooking-weapp/src/hooks/useGo.js
* @Description: 封装路由跳转方便行内调用
*/
import Taro from '@tarojs/taro';
import Taro from '@tarojs/taro'
/**
* @description 获取页面跳转方法(navigateTo)
......@@ -13,37 +13,39 @@ import Taro from '@tarojs/taro';
* - 自动补全:pages/notice/index
* @returns {(path:string, query?:Object)=>void} go 跳转函数
*/
export function useGo () {
export function useGo() {
/**
* @description 路由跳转
* @param {string} path 目标页面路径,支持 / 开头与短路径
* @param {Object} query 查询参数(键值对)
* @returns {void} 无返回值
*/
function go (path, query = {}) {
function go(path, query = {}) {
// 补全路径,如果是 / 开头,去掉 /
let url = path.startsWith('/') ? path.substring(1) : path;
let url = path.startsWith('/') ? path.substring(1) : path
// 检查是否是 tabbar 页面 (目前没有配置 tabbar,所以都是普通跳转)
// 如果是页面,加上 pages/ 前缀 (假设都在 pages 下,且目录名和 path 一致)
// H5 path 是 /notice,小程序是 pages/notice/index
if (!url.startsWith('pages/')) {
url = `pages/${url}/index`; // 适配 pages/notice/index 结构
url = `pages/${url}/index` // 适配 pages/notice/index 结构
}
// 构建 query string
let queryString = Object.keys(query).map(key => key + '=' + query[key]).join('&');
const queryString = Object.keys(query)
.map(key => `${key}=${query[key]}`)
.join('&')
if (queryString) {
url += '?' + queryString;
url += `?${queryString}`
}
Taro.navigateTo({
url: '/' + url,
fail: (err) => {
url: `/${url}`,
fail: err => {
// 如果是 tabbar 页面,尝试 switchTab
if (err.errMsg && err.errMsg.indexOf('tabbar') !== -1) {
Taro.switchTab({ url: '/' + url });
Taro.switchTab({ url: `/${url}` })
} else {
console.error('页面跳转失败:', err);
console.error('页面跳转失败:', err)
}
}
})
......@@ -57,26 +59,28 @@ export function useGo () {
* - 自动补全:pages/notice/index
* @returns {(path:string, query?:Object)=>void} replace 替换函数
*/
export function useReplace () {
export function useReplace() {
/**
* @description 路由替换
* @param {string} path 目标页面路径,支持 / 开头与短路径
* @param {Object} query 查询参数(键值对)
* @returns {void} 无返回值
*/
function replace (path, query = {}) {
let url = path.startsWith('/') ? path.substring(1) : path;
function replace(path, query = {}) {
let url = path.startsWith('/') ? path.substring(1) : path
if (!url.startsWith('pages/')) {
url = `pages/${url}/index`;
url = `pages/${url}/index`
}
let queryString = Object.keys(query).map(key => key + '=' + query[key]).join('&');
const queryString = Object.keys(query)
.map(key => `${key}=${query[key]}`)
.join('&')
if (queryString) {
url += '?' + queryString;
url += `?${queryString}`
}
Taro.redirectTo({
url: '/' + url
url: `/${url}`
})
}
return replace
......
This diff is collapsed. Click to expand it.
export default {
navigationBarTitleText: '授权页',
usingComponents: {
},
usingComponents: {}
}
......
......@@ -8,7 +8,7 @@
<template>
<view class="auth-page">
<view class="loading">
<view>正在授权登录...</view>
<view>正在授权登录...</view>
</view>
</view>
</template>
......@@ -22,41 +22,47 @@ let has_shown_fail_modal = false
let has_failed = false
useDidShow(() => {
if (has_failed) return
const now = Date.now()
if (now - last_try_at < 1200) return
last_try_at = now
if (has_failed) {
return
}
const now = Date.now()
if (now - last_try_at < 1200) {
return
}
last_try_at = now
/**
* 尝试静默授权
* - 授权成功后回跳到来源页
* - 授权失败则跳转至授权页面
*/
silentAuth()
.then(() => returnToOriginalPage())
.catch(async (error) => {
has_failed = true
if (has_shown_fail_modal) return
has_shown_fail_modal = true
await Taro.showModal({
title: '提示',
content: error?.message || '授权失败,请稍后再尝试',
showCancel: false,
confirmText: '我知道了',
})
/**
* 尝试静默授权
* - 授权成功后回跳到来源页
* - 授权失败则跳转至授权页面
*/
silentAuth()
.then(() => returnToOriginalPage())
.catch(async error => {
has_failed = true
if (has_shown_fail_modal) {
return
}
has_shown_fail_modal = true
await Taro.showModal({
title: '提示',
content: error?.message || '授权失败,请稍后再尝试',
showCancel: false,
confirmText: '我知道了'
})
})
})
</script>
<style lang="less">
.auth-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
.loading {
text-align: center;
color: #999;
}
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
.loading {
text-align: center;
color: #999;
}
}
</style>
......
This diff is collapsed. Click to expand it.
......@@ -7,12 +7,14 @@
-->
<template>
<view class="booking-code-page">
<view style="padding: 32rpx;">
<view style="padding: 32rpx">
<qrCode ref="qr_code_ref"></qrCode>
<view class="warning">
<view style="display: flex; align-items: center; justify-content: center;"><IconFont name="tips" /><text style="margin-left: 10rpx;">温馨提示</text></view>
<view style="margin-top: 16rpx;">一人一码,扫码或识别身份证成功后进入</view>
<view style="height: 256rpx;"></view>
<view style="display: flex; align-items: center; justify-content: center"
><IconFont name="tips" /><text style="margin-left: 10rpx">温馨提示</text></view
>
<view style="margin-top: 16rpx">一人一码,扫码或识别身份证成功后进入</view>
<view style="height: 256rpx"></view>
</view>
</view>
<indexNav
......@@ -28,7 +30,7 @@
<script setup>
import { ref } from 'vue'
import Taro, { useDidShow, useDidHide } from '@tarojs/taro'
import qrCode from '@/components/qrCode';
import qrCode from '@/components/qrCode'
import { IconFont } from '@nutui/icons-vue-taro'
import indexNav from '@/components/indexNav.vue'
import icon_3 from '@/assets/images/首页01@2x.png'
......@@ -42,49 +44,53 @@ import { get_weak_network_modal_no_cache_options } from '@/utils/uiText'
const qr_code_ref = ref(null)
useDidShow(() => {
qr_code_ref.value?.start_polling?.()
Taro.getNetworkType({
success: async (res) => {
const isConnected = is_usable_network(res.networkType);
if (isConnected) return
qr_code_ref.value?.start_polling?.()
Taro.getNetworkType({
success: async res => {
const isConnected = is_usable_network(res.networkType)
if (isConnected) {
return
}
if (has_offline_booking_cache()) {
Taro.redirectTo({ url: '/pages/offlineBookingList/index' })
return
}
if (has_offline_booking_cache()) {
Taro.redirectTo({ url: '/pages/offlineBookingList/index' })
return
}
try {
await Taro.showModal(get_weak_network_modal_no_cache_options())
} catch (e) {
console.error('show weak network modal failed:', e)
}
Taro.redirectTo({ url: '/pages/index/index' })
},
fail: async () => {
if (has_offline_booking_cache()) {
Taro.redirectTo({ url: '/pages/offlineBookingList/index' })
return
}
try {
await Taro.showModal(get_weak_network_modal_no_cache_options())
} catch (e) {
console.error('show weak network modal failed:', e)
}
Taro.redirectTo({ url: '/pages/index/index' })
}
});
try {
await Taro.showModal(get_weak_network_modal_no_cache_options())
} catch (e) {
console.error('show weak network modal failed:', e)
}
Taro.redirectTo({ url: '/pages/index/index' })
},
fail: async () => {
if (has_offline_booking_cache()) {
Taro.redirectTo({ url: '/pages/offlineBookingList/index' })
return
}
try {
await Taro.showModal(get_weak_network_modal_no_cache_options())
} catch (e) {
console.error('show weak network modal failed:', e)
}
Taro.redirectTo({ url: '/pages/index/index' })
}
})
})
useDidHide(() => {
qr_code_ref.value?.stop_polling?.()
qr_code_ref.value?.stop_polling?.()
})
const toMy = () => { // 跳转到我的
const toMy = () => {
// 跳转到我的
Taro.redirectTo({
url: '/pages/me/index'
})
}
const toHome = () => { // 跳转到首页
const toHome = () => {
// 跳转到首页
Taro.redirectTo({
url: '/pages/index/index'
})
......@@ -92,22 +98,25 @@ const toHome = () => { // 跳转到首页
const nav_icons = { home: icon_3, code: icon_4, me: icon_5 }
const on_nav_select = (key) => {
if (key === 'home') return toHome()
if (key === 'me') return toMy()
const on_nav_select = key => {
if (key === 'home') {
return toHome()
}
if (key === 'me') {
return toMy()
}
}
</script>
<style lang="less">
.booking-code-page {
position: relative;
min-height: 100vh;
background-color: #F6F6F6;
background-color: #f6f6f6;
.warning {
text-align: center;
color: #A67939;
color: #a67939;
margin-top: 32rpx;
}
}
......
......@@ -34,9 +34,12 @@
<view>{{ qrCodeStatusText }}</view>
</view>
</view>
<view style="height: 160rpx;"></view>
<view v-if="billInfo.status === CodeStatus.SUCCESS && billInfo.show_cancel_reserve === 1" class="cancel-wrapper">
<view @tap="cancelBooking" class="cancel-btn ">取消预约</view>
<view style="height: 160rpx"></view>
<view
v-if="billInfo.status === CodeStatus.SUCCESS && billInfo.show_cancel_reserve === 1"
class="cancel-wrapper"
>
<view @tap="cancelBooking" class="cancel-btn">取消预约</view>
</view>
</view>
</template>
......@@ -44,16 +47,16 @@
<script setup>
import { ref, computed } from 'vue'
import Taro, { useDidShow, useDidHide, useRouter as useTaroRouter } from '@tarojs/taro'
import qrCode from '@/components/qrCode';
import qrCode from '@/components/qrCode'
import { billInfoAPI, icbcRefundAPI } from '@/api/index'
import { formatDatetime, get_bill_status_text } from '@/utils/tools';
import { formatDatetime, get_bill_status_text } from '@/utils/tools'
import { refresh_offline_booking_cache } from '@/composables/useOfflineBookingCache'
const router = useTaroRouter();
const router = useTaroRouter()
const pay_id = ref('');
const qrCodeStatus = ref('');
const billInfo = ref({});
const pay_id = ref('')
const qrCodeStatus = ref('')
const billInfo = ref({})
const qr_code_ref = ref(null)
/**
......@@ -75,7 +78,7 @@ const CodeStatus = {
* @returns {string} 状态文案
*/
const qrCodeStatusText = computed(() => {
return get_bill_status_text(billInfo.value?.status)
return get_bill_status_text(billInfo.value?.status)
})
/**
......@@ -84,99 +87,99 @@ const qrCodeStatusText = computed(() => {
* @returns {Promise<void>} 无返回值
*/
const cancelBooking = async () => {
const { confirm } = await Taro.showModal({
title: '温馨提示',
content: '是否取消预约?',
confirmColor: '#A67939'
});
if (confirm) {
Taro.showLoading({ title: '取消中...' });
const { code, data } = await icbcRefundAPI({ pay_id: pay_id.value });
Taro.hideLoading();
if (code) {
Taro.showToast({ title: '取消成功' });
try {
await refresh_offline_booking_cache({ force: true })
} catch (e) {}
Taro.navigateBack();
} else {
Taro.showToast({ title: '取消失败', icon: 'none' });
}
const { confirm } = await Taro.showModal({
title: '温馨提示',
content: '是否取消预约?',
confirmColor: '#A67939'
})
if (confirm) {
Taro.showLoading({ title: '取消中...' })
const { code, data } = await icbcRefundAPI({ pay_id: pay_id.value })
Taro.hideLoading()
if (code) {
Taro.showToast({ title: '取消成功' })
try {
await refresh_offline_booking_cache({ force: true })
} catch (e) {}
Taro.navigateBack()
} else {
Taro.showToast({ title: '取消失败', icon: 'none' })
}
}
}
useDidShow(async () => {
qr_code_ref.value?.start_polling?.()
pay_id.value = router.params.pay_id;
if (pay_id.value) {
const { code, data } = await billInfoAPI({ pay_id: pay_id.value });
if (code) {
data.datetime = data && formatDatetime(data);
data.order_time = data.created_time ? data.created_time.slice(0, -3) : '';
billInfo.value = data;
}
qr_code_ref.value?.start_polling?.()
pay_id.value = router.params.pay_id
if (pay_id.value) {
const { code, data } = await billInfoAPI({ pay_id: pay_id.value })
if (code) {
data.datetime = data && formatDatetime(data)
data.order_time = data.created_time ? data.created_time.slice(0, -3) : ''
billInfo.value = data
}
}
})
useDidHide(() => {
qr_code_ref.value?.stop_polling?.()
qr_code_ref.value?.stop_polling?.()
})
</script>
<style lang="less">
.booking-detail-page {
min-height: 100vh;
background-color: #F6F6F6;
padding: 32rpx;
min-height: 100vh;
background-color: #f6f6f6;
padding: 32rpx;
.detail-wrapper {
background-color: #FFF;
border-radius: 16rpx;
padding: 32rpx;
margin-top: 32rpx;
box-shadow: 0 0 29rpx 0 rgba(106,106,106,0.1);
.detail-item {
display: flex;
justify-content: space-between;
margin-bottom: 26rpx;
color: #333;
font-size: 30rpx;
&:last-child {
margin-bottom: 0;
}
view:first-child {
color: #999;
width: 160rpx;
}
view:last-child {
flex: 1;
text-align: right;
}
}
.detail-wrapper {
background-color: #fff;
border-radius: 16rpx;
padding: 32rpx;
margin-top: 32rpx;
box-shadow: 0 0 29rpx 0 rgba(106, 106, 106, 0.1);
.detail-item {
display: flex;
justify-content: space-between;
margin-bottom: 26rpx;
color: #333;
font-size: 30rpx;
&:last-child {
margin-bottom: 0;
}
view:first-child {
color: #999;
width: 160rpx;
}
view:last-child {
flex: 1;
text-align: right;
}
}
.cancel-wrapper {
position: fixed;
bottom: 0;
left: 0;
width: 750rpx;
background-color: #FFF;
padding: 32rpx;
box-sizing: border-box;
.cancel-btn {
background-color: #FFF;
color: #A67939;
border: 2rpx solid #A67939;
text-align: center;
padding: 26rpx 0;
border-radius: 16rpx;
font-size: 35rpx;
}
}
.cancel-wrapper {
position: fixed;
bottom: 0;
left: 0;
width: 750rpx;
background-color: #fff;
padding: 32rpx;
box-sizing: border-box;
.cancel-btn {
background-color: #fff;
color: #a67939;
border: 2rpx solid #a67939;
text-align: center;
padding: 26rpx 0;
border-radius: 16rpx;
font-size: 35rpx;
}
}
}
</style>
......
......@@ -11,11 +11,18 @@
<reserveCard :data="item" />
</view>
<view v-if="loading" style="text-align: center; color: #999; padding: 20rpx;">加载中...</view>
<view v-if="finished && bookingList.length > 0" style="text-align: center; color: #999; padding: 20rpx;">没有更多了</view>
<view v-if="loading" style="text-align: center; color: #999; padding: 20rpx">加载中...</view>
<view
v-if="finished && bookingList.length > 0"
style="text-align: center; color: #999; padding: 20rpx"
>没有更多了</view
>
<view v-if="!bookingList.length && finished" class="no-qrcode">
<image src="https://cdn.ipadbiz.cn/xys/booking/%E6%9A%82%E6%97%A0@2x.png" style="width: 320rpx; height: 320rpx;" />
<image
src="https://cdn.ipadbiz.cn/xys/booking/%E6%9A%82%E6%97%A0@2x.png"
style="width: 320rpx; height: 320rpx"
/>
<view class="no-qrcode-title">您还没有预约过参观</view>
</view>
</view>
......@@ -25,14 +32,14 @@
import { ref } from 'vue'
import { useDidShow, useReachBottom } from '@tarojs/taro'
import { billListAPI } from '@/api/index'
import { formatDatetime } from '@/utils/tools';
import { formatDatetime } from '@/utils/tools'
import reserveCard from '@/components/reserveCard.vue'
const page = ref(1);
const limit = ref(5);
const bookingList = ref([]);
const loading = ref(false);
const finished = ref(false);
const page = ref(1)
const limit = ref(5)
const bookingList = ref([])
const loading = ref(false)
const finished = ref(false)
/**
* @description 加载预约记录列表(分页)
......@@ -40,52 +47,54 @@ const finished = ref(false);
* @returns {Promise<void>} 无返回值
*/
const loadData = async (isRefresh = false) => {
if (loading.value || (finished.value && !isRefresh)) return;
if (loading.value || (finished.value && !isRefresh)) {
return
}
loading.value = true;
if (isRefresh) {
page.value = 1;
finished.value = false;
}
loading.value = true
if (isRefresh) {
page.value = 1
finished.value = false
}
const { code, data } = await billListAPI({ page: page.value, row_num: limit.value });
loading.value = false;
const { code, data } = await billListAPI({ page: page.value, row_num: limit.value })
loading.value = false
if (code) {
const list = data || [];
list.forEach(item => {
item.booking_time = item && formatDatetime(item);
item.order_time = item.created_time ? item.created_time.slice(0, -3) : '';
});
if (code) {
const list = data || []
list.forEach(item => {
item.booking_time = item && formatDatetime(item)
item.order_time = item.created_time ? item.created_time.slice(0, -3) : ''
})
if (isRefresh) {
bookingList.value = list;
} else {
bookingList.value = bookingList.value.concat(list);
}
if (isRefresh) {
bookingList.value = list
} else {
bookingList.value = bookingList.value.concat(list)
}
if (list.length < limit.value) {
finished.value = true;
} else {
page.value++;
}
if (list.length < limit.value) {
finished.value = true
} else {
page.value++
}
}
}
useDidShow(() => {
loadData(true);
});
loadData(true)
})
useReachBottom(() => {
loadData();
});
loadData()
})
</script>
<style lang="less">
.booking-list-page {
padding: 32rpx;
min-height: 100vh;
background-color: #F6F6F6;
background-color: #f6f6f6;
.no-qrcode {
display: flex;
......@@ -95,9 +104,9 @@ useReachBottom(() => {
padding-top: 160rpx;
.no-qrcode-title {
color: #A67939;
font-size: 34rpx;
margin-top: 32rpx;
color: #a67939;
font-size: 34rpx;
margin-top: 32rpx;
}
}
}
......
......@@ -2,19 +2,33 @@
<view class="callback-page">
<view>
<view v-if="pay_status === PAY_STATUS.FAIL" class="text-prompts">
<image src="https://cdn.ipadbiz.cn/xys/booking/shibai.png" mode="widthFix" class="status-icon"/>
<image
src="https://cdn.ipadbiz.cn/xys/booking/shibai.png"
mode="widthFix"
class="status-icon"
/>
<view class="text">支付失败</view>
</view>
<view v-else class="text-prompts">
<image src="https://cdn.ipadbiz.cn/xys/booking/%E6%88%90%E5%8A%9F@2x.png?imageMogr2/thumbnail/200x/strip/quality/70" mode="widthFix" class="status-icon"/>
<image
src="https://cdn.ipadbiz.cn/xys/booking/%E6%88%90%E5%8A%9F@2x.png?imageMogr2/thumbnail/200x/strip/quality/70"
mode="widthFix"
class="status-icon"
/>
<!-- <view class="text">支付完成</view> -->
</view>
<view class="appointment-information">
<view class="info-item">参观人数:<text>{{ billInfo?.total_qty || 0 }} 人</text></view>
<view class="info-item">参访时间:<text>{{ billInfo?.datetime || '--' }}</text></view>
<view class="info-item">支付金额:<text>¥ {{ billInfo?.total_amt || 0 }}</text></view>
<view class="info-item"
>参观人数:<text>{{ billInfo?.total_qty || 0 }} 人</text></view
>
<view class="info-item"
>参访时间:<text>{{ billInfo?.datetime || '--' }}</text></view
>
<view class="info-item"
>支付金额:<text>¥ {{ billInfo?.total_amt || 0 }}</text></view
>
</view>
<view style="padding: 16rpx; display: flex; justify-content: center; margin-top: 32rpx;">
<view style="padding: 16rpx; display: flex; justify-content: center; margin-top: 32rpx">
<nut-button color="#A67939" size="small" @click="returnMerchant">返回首页</nut-button>
</view>
</view>
......@@ -35,26 +49,28 @@ const billInfo = ref({})
const PAY_STATUS = {
SUCCESS: '0',
FAIL: '1',
UNKNOWN: '2',
UNKNOWN: '2'
}
const pay_status = ref('0') // Default to success as per logic
const out_trade_no = router.params.out_trade_no
const getBillInfo = async () => {
if (!out_trade_no) return
if (!out_trade_no) {
return
}
try {
// Get order details
const { code, data } = await onAuthBillInfoAPI({ order_id: out_trade_no })
if (code && data) {
data.datetime = data && formatDatetime(data)
billInfo.value = data
} else {
// Handle error if needed
}
// Get order details
const { code, data } = await onAuthBillInfoAPI({ order_id: out_trade_no })
if (code && data) {
data.datetime = data && formatDatetime(data)
billInfo.value = data
} else {
// Handle error if needed
}
} catch (e) {
console.error(e)
console.error(e)
}
}
......@@ -63,51 +79,51 @@ const returnMerchant = () => {
}
onMounted(() => {
getBillInfo()
getBillInfo()
})
</script>
<style lang="less">
.callback-page {
padding: 32rpx;
background: #fff;
min-height: 100vh;
padding: 32rpx;
background: #fff;
min-height: 100vh;
.text-prompts {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 64rpx;
padding-top: 64rpx;
.text-prompts {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 64rpx;
padding-top: 64rpx;
.status-icon {
width: 200rpx;
height: 200rpx;
}
.status-icon {
width: 200rpx;
height: 200rpx;
}
.text {
margin-top: 32rpx;
font-size: 38rpx;
color: #333;
}
.text {
margin-top: 32rpx;
font-size: 38rpx;
color: #333;
}
}
.appointment-information {
background: #f8f8f8;
padding: 32rpx;
border-radius: 16rpx;
.appointment-information {
background: #f8f8f8;
padding: 32rpx;
border-radius: 16rpx;
.info-item {
margin-bottom: 16rpx;
font-size: 29rpx;
color: #666;
.info-item {
margin-bottom: 16rpx;
font-size: 29rpx;
color: #666;
text {
color: #333;
font-weight: 500;
margin-left: 16rpx;
}
}
text {
color: #333;
font-weight: 500;
margin-left: 16rpx;
}
}
}
}
</style>
......
export default {
navigationBarTitleText: 'demo',
usingComponents: {
},
usingComponents: {}
}
......
......@@ -11,8 +11,8 @@
<script setup>
import '@tarojs/taro/html.css'
import { ref } from "vue";
import "./index.less";
import { ref } from 'vue'
import './index.less'
// 定义响应式数据
const str = ref('Demo页面')
......@@ -20,6 +20,6 @@ const str = ref('Demo页面')
<script>
export default {
name: "demoPage",
};
name: 'demoPage'
}
</script>
......
......@@ -2,9 +2,9 @@
* index页面样式
*/
.index {
padding: 40rpx;
.nut-button {
margin-bottom: 40rpx;
}
padding: 40rpx;
.nut-button {
margin-bottom: 40rpx;
}
}
......
......@@ -15,10 +15,17 @@
<view class="mt-1 text-sm opacity-80">{{ weak_network_banner_desc }}</view>
</view>
<view class="index-content">
<view style="height: 28vh;">
<swiper class="my-swipe" :autoplay="true" :interval="3000" indicator-dots indicator-color="white" :circular="true">
<view style="height: 28vh">
<swiper
class="my-swipe"
:autoplay="true"
:interval="3000"
indicator-dots
indicator-color="white"
:circular="true"
>
<swiper-item>
<image style="height: 28vh; width: 100vw;" :src="banner_url" />
<image style="height: 28vh; width: 100vw" :src="banner_url" />
</swiper-item>
</swiper>
</view>
......@@ -26,8 +33,8 @@
<view ref="root" class="index-circular">
<view class="booking-wrapper">
<view class="booking" @tap="toBooking">
<view><image :src="icon_1" style="width: 96rpx; height: 96rpx;" /></view>
<view style="color: #FFF;">开始预约</view>
<view><image :src="icon_1" style="width: 96rpx; height: 96rpx" /></view>
<view style="color: #fff">开始预约</view>
</view>
</view>
</view>
......@@ -57,7 +64,7 @@ import icon_3 from '@/assets/images/首页02@2x.png'
import icon_4 from '@/assets/images/二维码icon.png'
import icon_5 from '@/assets/images/我的01@2x.png'
const go = useGo();
const go = useGo()
const is_offline = ref(false)
const weak_network_banner_desc = weak_network_text.banner_desc
// 背景图版本号, 用于刷新背景图
......@@ -66,10 +73,12 @@ const bg_version = ref(Number.isFinite(initial_t) ? initial_t : 0)
let is_reloading = false
const reload_page = () => {
if (is_reloading) return
if (is_reloading) {
return
}
is_reloading = true
Taro.reLaunch({
url: `/pages/index/index?_t=${Date.now()}`,
url: `/pages/index/index?_t=${Date.now()}`
})
}
......@@ -93,18 +102,18 @@ const page_style = computed(() => {
if (is_offline.value) {
return {
backgroundColor: '#F3EEE3',
backgroundImage: `linear-gradient(180deg, rgba(166, 121, 57, 0.10) 0%, rgba(255, 255, 255, 0.90) 60%, rgba(243, 238, 227, 1) 100%), url('${normal_bg_url.value}')`,
backgroundImage: `linear-gradient(180deg, rgba(166, 121, 57, 0.10) 0%, rgba(255, 255, 255, 0.90) 60%, rgba(243, 238, 227, 1) 100%), url('${normal_bg_url.value}')`
}
}
return {
backgroundColor: '#F3EEE3',
backgroundImage: `url('${normal_bg_url.value}')`,
backgroundImage: `url('${normal_bg_url.value}')`
}
})
const logo_style = computed(() => {
return {
backgroundImage: `url('${logo_url.value}')`,
backgroundImage: `url('${logo_url.value}')`
}
})
......@@ -114,7 +123,7 @@ const logo_style = computed(() => {
* - 更新 is_offline 状态
*/
const apply_offline_state = (next_offline) => {
const apply_offline_state = next_offline => {
if (is_offline.value === true && next_offline === false) {
reload_page()
return true
......@@ -147,9 +156,11 @@ let network_listener = null
*/
const setup_network_listener = () => {
if (has_network_listener) return
if (has_network_listener) {
return
}
has_network_listener = true
network_listener = (res) => {
network_listener = res => {
try {
const is_connected = res?.isConnected !== false
const network_type = res?.networkType
......@@ -158,7 +169,9 @@ const setup_network_listener = () => {
const next_offline = !(is_connected && is_usable_network(network_type))
// 检查是否需要刷新
const is_handled = apply_offline_state(next_offline)
if (is_handled) return
if (is_handled) {
return
}
}
// 还没有网, 再次刷新
refresh_offline_state()
......@@ -175,7 +188,9 @@ const setup_network_listener = () => {
*/
const teardown_network_listener = () => {
if (!has_network_listener) return
if (!has_network_listener) {
return
}
has_network_listener = false
if (network_listener && typeof Taro.offNetworkStatusChange === 'function') {
try {
......@@ -199,7 +214,8 @@ onUnmounted(() => {
teardown_network_listener()
})
const toBooking = () => { // 跳转到预约须知
const toBooking = () => {
// 跳转到预约须知
// 如果是离线模式,不跳转
if (is_offline.value) {
Taro.showToast({
......@@ -208,16 +224,18 @@ const toBooking = () => { // 跳转到预约须知
})
return
}
go('/notice');
go('/notice')
}
const toCode = () => { // 跳转到预约码
const toCode = () => {
// 跳转到预约码
Taro.redirectTo({
url: '/pages/bookingCode/index'
})
}
const toMy = () => { // 跳转到我的
const toMy = () => {
// 跳转到我的
Taro.redirectTo({
url: '/pages/me/index'
})
......@@ -225,9 +243,13 @@ const toMy = () => { // 跳转到我的
const nav_icons = { home: icon_3, code: icon_4, me: icon_5 }
const on_nav_select = (key) => {
if (key === 'code') return toCode()
if (key === 'me') return toMy()
const on_nav_select = key => {
if (key === 'code') {
return toCode()
}
if (key === 'me') {
return toMy()
}
}
useShareAppMessage(() => {
......@@ -236,7 +258,6 @@ useShareAppMessage(() => {
path: '/pages/index/index'
}
})
</script>
<style lang="less">
......@@ -248,7 +269,7 @@ useShareAppMessage(() => {
background-size: cover; /* 确保背景覆盖 */
&.is-offline {
background-color: #F3EEE3;
background-color: #f3eee3;
}
.offline-banner {
......@@ -258,7 +279,7 @@ useShareAppMessage(() => {
right: 24rpx;
z-index: 10;
background: rgba(255, 255, 255, 0.88);
color: #A67939;
color: #a67939;
border: 2rpx solid rgba(166, 121, 57, 0.25);
box-shadow: 0 12rpx 30rpx rgba(166, 121, 57, 0.12);
backdrop-filter: blur(6px);
......@@ -279,30 +300,30 @@ useShareAppMessage(() => {
display: flex;
justify-content: center;
align-items: center;
background-color: #A67939;
background-color: #a67939;
border-radius: 14rpx;
color: #FFFFFF;
color: #ffffff;
padding: 22rpx 128rpx;
border: 2rpx solid #A67939;
border: 2rpx solid #a67939;
}
.record {
display: flex;
justify-content: center;
align-items: center;
color: #A67939;
color: #a67939;
border-radius: 14rpx;
padding: 22rpx 128rpx;
border: 2rpx solid #A67939;
border: 2rpx solid #a67939;
margin-top: 48rpx;
}
.search {
display: flex;
justify-content: center;
align-items: center;
color: #A67939;
color: #a67939;
border-radius: 14rpx;
padding: 22rpx 128rpx;
border: 2rpx solid #A67939;
border: 2rpx solid #a67939;
margin-top: 48rpx;
}
}
......@@ -327,7 +348,7 @@ useShareAppMessage(() => {
height: 230rpx;
width: 230rpx;
border-radius: 50%;
background-color: #A67939;
background-color: #a67939;
display: flex;
align-items: center;
justify-content: center;
......@@ -348,7 +369,8 @@ useShareAppMessage(() => {
}
.my-swipe {
height: 400rpx;
swiper-item { /* Taro swiper-item 编译后 */
swiper-item {
/* Taro swiper-item 编译后 */
height: 400rpx;
width: 750rpx;
background-size: cover;
......
......@@ -2,7 +2,7 @@
<view class="my-page">
<view v-for="(item, index) in menu_list" :key="index" class="my-item" @tap="on_menu_tap(item)">
<view class="left">
<image :src="item.icon" style="width: 38rpx; height: 38rpx; margin-right: 16rpx;" />
<image :src="item.icon" style="width: 38rpx; height: 38rpx; margin-right: 16rpx" />
{{ item.name }}
</view>
<view>
......@@ -34,12 +34,17 @@ import { is_usable_network, get_network_type } from '@/utils/network'
import icon_booking from '@/assets/images/预约记录@2x.png'
import icon_visitor from '@/assets/images/我的01@2x.png'
import icon_invite from '@/assets/images/二维码@2x2.png'
import { weak_network_text, get_weak_network_modal_go_offline_records_options } from '@/utils/uiText'
import {
weak_network_text,
get_weak_network_modal_go_offline_records_options
} from '@/utils/uiText'
const go = useGo();
const go = useGo()
const on_menu_tap = async (item) => {
if (!item?.to) return
const on_menu_tap = async item => {
if (!item?.to) {
return
}
if (item.to === '/pages/bookingList/index') {
const network_type = await get_network_type()
......@@ -60,12 +65,14 @@ const on_menu_tap = async (item) => {
go(item.to)
}
const toCode = () => { // 跳转到预约码
const toCode = () => {
// 跳转到预约码
Taro.redirectTo({
url: '/pages/bookingCode/index'
})
}
const toHome = () => { // 跳转到首页
const toHome = () => {
// 跳转到首页
Taro.redirectTo({
url: '/pages/index/index'
})
......@@ -73,43 +80,51 @@ const toHome = () => { // 跳转到首页
const nav_icons = { home: icon_3, code: icon_4, me: icon_5 }
const on_nav_select = (key) => {
if (key === 'home') return toHome()
if (key === 'code') return toCode()
const on_nav_select = key => {
if (key === 'home') {
return toHome()
}
if (key === 'code') {
return toCode()
}
}
const menu_list = [{
icon: icon_booking,
name: '预约记录',
to: '/pages/bookingList/index'
}, {
icon: icon_visitor,
name: '参观者',
to: '/pages/visitorList/index'
}, {
icon: icon_invite,
name: '邀请码',
to: '/pages/search/index'
}]
const menu_list = [
{
icon: icon_booking,
name: '预约记录',
to: '/pages/bookingList/index'
},
{
icon: icon_visitor,
name: '参观者',
to: '/pages/visitorList/index'
},
{
icon: icon_invite,
name: '邀请码',
to: '/pages/search/index'
}
]
</script>
<style lang="less">
.my-page {
position: relative;
min-height: 100vh;
background-color: #F6F6F6;
background-color: #f6f6f6;
padding: 32rpx;
.my-item {
padding: 32rpx;
display: flex;
justify-content:space-between;
justify-content: space-between;
align-items: center;
margin-bottom: 32rpx;
background-color: #FFF;
background-color: #fff;
border-radius: 10rpx;
.left {
color: #A67939;
color: #a67939;
display: flex;
align-items: center;
}
......
export default {
navigationBarTitleText: 'NFC测试',
navigationBarTitleText: 'NFC测试'
}
......
This diff is collapsed. Click to expand it.
......@@ -8,18 +8,20 @@
<template>
<view class="notice-page">
<view class="content">
<view style="text-align: center; font-size: 35rpx; margin-bottom: 16rpx;">温馨提示</view>
<view style="text-align: center; font-size: 35rpx; margin-bottom: 16rpx">温馨提示</view>
<view>
为了您和他人的健康与安全,维护清净庄严的寺院环境,营造一个喜悦而祥和的节日氛围,请您留意并遵守以下注意事项:
</view>
<view v-for="(item, index) in note_text" :key="index" style="margin-top: 16rpx;">{{ item }}</view>
<view style="margin-top: 16rpx;">谢谢您的支持与配合。祝您新春吉祥、万事如意。</view>
<view v-for="(item, index) in note_text" :key="index" style="margin-top: 16rpx">{{
item
}}</view>
<view style="margin-top: 16rpx">谢谢您的支持与配合。祝您新春吉祥、万事如意。</view>
</view>
<view style="height: 256rpx"></view>
<view class="footer">
<nut-checkbox-group v-model="checked">
<nut-checkbox label="1" icon-size="32rpx">
<text style="color: #a67939; font-size: 32rpx">我已阅读并同意以上内容</text>
<text style="color: #a67939; font-size: 32rpx">我已阅读并同意以上内容</text>
</nut-checkbox>
</nut-checkbox-group>
<view @tap="confirmBtn" class="confirm-btn">确认,下一步</view>
......@@ -28,11 +30,11 @@
</template>
<script setup>
import { ref } from "vue";
import { ref } from 'vue'
import Taro, { useDidShow } from '@tarojs/taro'
import { useGo } from "@/hooks/useGo";
import { useGo } from '@/hooks/useGo'
const go = useGo();
const go = useGo()
const note_text = [
'1、敬香贵在心诚,不在数量多少。三支清香,可表心诚。请带着虔诚心、恭敬心和清净心敬香礼佛。',
'2、请不要自带香烛进寺院。山门殿两侧设有赠香处,凭香花券可免费领取三支清香。',
......@@ -43,8 +45,8 @@ const note_text = [
'7、请保管好自己随身携带的钱物,以免丢失给您带来麻烦。',
'8、您若有任何问题和困难,请向身边的法师或义工咨询、求助,或直接与客堂联系。电话:0512-65349545。',
'9、预约如需退款,请在初七之后,到客堂办理。'
];
const checked = ref([]);
]
const checked = ref([])
/**
* @description 点击确认进入下一步
......@@ -52,19 +54,19 @@ const checked = ref([]);
* @returns {void} 无返回值
*/
const confirmBtn = () => {
if (checked.value.includes("1")) {
go("/booking");
if (checked.value.includes('1')) {
go('/booking')
} else {
Taro.showToast({ title: "请勾选同意须知", icon: "none" });
Taro.showToast({ title: '请勾选同意须知', icon: 'none' })
}
};
}
</script>
<style lang="less">
.notice-page {
position: relative;
min-height: 100vh;
background-color: #F6F6F6;
background-color: #f6f6f6;
padding-top: 2rpx; // 防止 margin collapse
.content {
margin: 32rpx;
......@@ -79,16 +81,16 @@ const confirmBtn = () => {
position: fixed;
bottom: 0;
width: 750rpx;
background-color: #FFF;
background-color: #fff;
display: flex;
flex-direction: column;
padding: 32rpx;
box-sizing: border-box;
box-shadow: 0 -10rpx 8rpx 0 rgba(0,0,0,0.12);
box-shadow: 0 -10rpx 8rpx 0 rgba(0, 0, 0, 0.12);
.confirm-btn {
background-color: #A67939;
color: #FFF;
background-color: #a67939;
color: #fff;
text-align: center;
padding: 26rpx 0;
border-radius: 16rpx;
......
......@@ -6,7 +6,7 @@
* @Description: 线下预约码页面
-->
<template>
<view class="offline-booking-code-page"></view>
<view class="offline-booking-code-page"></view>
</template>
<script setup>
......@@ -14,18 +14,17 @@ import { onMounted } from 'vue'
import Taro from '@tarojs/taro'
import { useGo } from '@/hooks/useGo'
const go = useGo();
const go = useGo()
onMounted(() => {
Taro.nextTick(() => {
go('/pages/offlineBookingList/index')
})
});
Taro.nextTick(() => {
go('/pages/offlineBookingList/index')
})
})
</script>
<style lang="less">
.offline-booking-code-page {
min-height: 100vh;
min-height: 100vh;
}
</style>
......
export default {
navigationBarTitleText: '离线预约详情'
navigationBarTitleText: '离线预约详情'
}
......
<template>
<view class="offline-booking-detail-page">
<view class="header-tip">
<IconFont name="tips" size="15" color="#A67939" />&nbsp;
<text>您当前处于离线模式,仅展示本地缓存的数据</text>
</view>
<view class="offline-booking-detail-page">
<view class="header-tip">
<IconFont name="tips" size="15" color="#A67939" />&nbsp;
<text>您当前处于离线模式,仅展示本地缓存的数据</text>
</view>
<view class="content">
<offlineQrCode :list="qr_list" />
<view class="content">
<offlineQrCode :list="qr_list" />
<view v-if="bill_info && bill_info.pay_id" class="detail-wrapper">
<view class="detail-item">
<view>参访时间:</view>
<view>{{ bill_info.datetime }}</view>
</view>
<view class="detail-item">
<view>参访人数:</view>
<view>{{ bill_info.total_qty }} 人</view>
</view>
<view class="detail-item">
<view>支付金额:</view>
<view>¥ {{ bill_info.total_amt }}</view>
</view>
<view class="detail-item">
<view>下单时间:</view>
<view>{{ bill_info.order_time }}</view>
</view>
<view class="detail-item">
<view>订单编号:</view>
<view>{{ bill_info.pay_id }}</view>
</view>
<!-- <view class="detail-item">
<view v-if="bill_info && bill_info.pay_id" class="detail-wrapper">
<view class="detail-item">
<view>参访时间:</view>
<view>{{ bill_info.datetime }}</view>
</view>
<view class="detail-item">
<view>参访人数:</view>
<view>{{ bill_info.total_qty }} 人</view>
</view>
<view class="detail-item">
<view>支付金额:</view>
<view>¥ {{ bill_info.total_amt }}</view>
</view>
<view class="detail-item">
<view>下单时间:</view>
<view>{{ bill_info.order_time }}</view>
</view>
<view class="detail-item">
<view>订单编号:</view>
<view>{{ bill_info.pay_id }}</view>
</view>
<!-- <view class="detail-item">
<view>订单状态:</view>
<view>{{ status_text }}</view>
</view> -->
</view>
</view>
</view>
</view>
<view class="action-area">
<button class="back-btn" @tap="toList">返回列表</button>
</view>
<view class="action-area">
<button class="back-btn" @tap="toList">返回列表</button>
</view>
</view>
</template>
<script setup>
......@@ -47,7 +47,10 @@ import { ref, computed } from 'vue'
import Taro, { useDidShow, useRouter as useTaroRouter } from '@tarojs/taro'
import { IconFont } from '@nutui/icons-vue-taro'
import offlineQrCode from '@/components/offlineQrCode.vue'
import { get_offline_booking_by_pay_id, build_offline_qr_list } from '@/composables/useOfflineBookingCache'
import {
get_offline_booking_by_pay_id,
build_offline_qr_list
} from '@/composables/useOfflineBookingCache'
const router = useTaroRouter()
const bill_info = ref(null)
......@@ -78,95 +81,95 @@ const qr_list = ref([])
// })
const toList = () => {
Taro.navigateBack({
fail: () => {
Taro.reLaunch({ url: '/pages/offlineBookingList/index' })
}
})
Taro.navigateBack({
fail: () => {
Taro.reLaunch({ url: '/pages/offlineBookingList/index' })
}
})
}
const load_cache = () => {
const pay_id = router.params.pay_id
const data = get_offline_booking_by_pay_id(pay_id)
if (!data) {
Taro.showToast({ title: '本地无该订单缓存', icon: 'none' })
Taro.reLaunch({ url: '/pages/offlineBookingList/index' })
return
}
bill_info.value = data
qr_list.value = build_offline_qr_list(data)
const pay_id = router.params.pay_id
const data = get_offline_booking_by_pay_id(pay_id)
if (!data) {
Taro.showToast({ title: '本地无该订单缓存', icon: 'none' })
Taro.reLaunch({ url: '/pages/offlineBookingList/index' })
return
}
bill_info.value = data
qr_list.value = build_offline_qr_list(data)
}
useDidShow(() => {
load_cache()
load_cache()
})
</script>
<style lang="less">
.offline-booking-detail-page {
min-height: 100vh;
background-color: #F6F6F6;
.header-tip {
display: flex;
align-items: center;
padding: 20rpx 32rpx;
color: #A67939;
font-size: 26rpx;
background: #FFF;
min-height: 100vh;
background-color: #f6f6f6;
.header-tip {
display: flex;
align-items: center;
padding: 20rpx 32rpx;
color: #a67939;
font-size: 26rpx;
background: #fff;
}
.content {
padding: 32rpx;
padding-bottom: 180rpx;
}
.detail-wrapper {
background-color: #fff;
border-radius: 16rpx;
padding: 32rpx;
margin-top: 32rpx;
box-shadow: 0 0 29rpx 0 rgba(106, 106, 106, 0.1);
.detail-item {
display: flex;
justify-content: space-between;
margin-bottom: 26rpx;
color: #333;
font-size: 30rpx;
&:last-child {
margin-bottom: 0;
}
view:first-child {
color: #999;
width: 160rpx;
}
view:last-child {
flex: 1;
text-align: right;
}
}
.content {
padding: 32rpx;
padding-bottom: 180rpx;
}
.detail-wrapper {
background-color: #FFF;
border-radius: 16rpx;
padding: 32rpx;
margin-top: 32rpx;
box-shadow: 0 0 29rpx 0 rgba(106,106,106,0.1);
.detail-item {
display: flex;
justify-content: space-between;
margin-bottom: 26rpx;
color: #333;
font-size: 30rpx;
&:last-child {
margin-bottom: 0;
}
view:first-child {
color: #999;
width: 160rpx;
}
view:last-child {
flex: 1;
text-align: right;
}
}
}
.action-area {
position: fixed;
bottom: 0;
left: 0;
width: 750rpx;
padding: 24rpx 32rpx;
background: #FFF;
box-sizing: border-box;
box-shadow: 0 -10rpx 8rpx 0 rgba(0,0,0,0.06);
.back-btn {
background-color: #A67939;
color: #FFF;
border-radius: 16rpx;
font-size: 32rpx;
padding: 12rpx 0;
}
}
.action-area {
position: fixed;
bottom: 0;
left: 0;
width: 750rpx;
padding: 24rpx 32rpx;
background: #fff;
box-sizing: border-box;
box-shadow: 0 -10rpx 8rpx 0 rgba(0, 0, 0, 0.06);
.back-btn {
background-color: #a67939;
color: #fff;
border-radius: 16rpx;
font-size: 32rpx;
padding: 12rpx 0;
}
}
}
</style>
......
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.
This diff is collapsed. Click to expand it.