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 3648 additions and 1601 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"
......
......@@ -108,6 +108,9 @@ importers:
'@vue/compiler-sfc':
specifier: ^3.0.0
version: 3.5.26
'@vue/eslint-config-prettier':
specifier: ^10.2.0
version: 10.2.0(@types/eslint@9.6.1)(eslint@8.57.1)(prettier@3.8.1)
autoprefixer:
specifier: ^10.4.21
version: 10.4.23(postcss@8.5.6)
......@@ -122,13 +125,25 @@ importers:
version: 8.57.1
eslint-config-taro:
specifier: 4.1.9
version: 4.1.9(@babel/core@7.28.5)(eslint@8.57.1)(typescript@5.9.3)
version: 4.1.9(@babel/core@7.28.5)(eslint-plugin-vue@10.7.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(vue-eslint-parser@10.2.0(eslint@8.57.1)))(eslint@8.57.1)(typescript@5.9.3)
eslint-plugin-vue:
specifier: ^10.7.0
version: 10.7.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(vue-eslint-parser@10.2.0(eslint@8.57.1))
husky:
specifier: ^9.1.7
version: 9.1.7
less:
specifier: ^4.2.0
version: 4.5.1
lint-staged:
specifier: ^16.2.7
version: 16.2.7
postcss:
specifier: ^8.5.6
version: 8.5.6
prettier:
specifier: ^3.8.1
version: 3.8.1
sass:
specifier: ^1.78.0
version: 1.97.2
......@@ -141,6 +156,9 @@ importers:
unplugin-vue-components:
specifier: ^0.26.0
version: 0.26.0(@babel/parser@7.28.5)(rollup@3.29.5)(vue@3.5.26(typescript@5.9.3))
vue-eslint-parser:
specifier: ^10.2.0
version: 10.2.0(eslint@8.57.1)
vue-loader:
specifier: ^17.0.0
version: 17.4.2(@vue/compiler-sfc@3.5.26)(vue@3.5.26(typescript@5.9.3))(webpack@5.91.0(@swc/core@1.3.96))
......@@ -1535,6 +1553,10 @@ packages:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
'@pkgr/core@0.2.9':
resolution: {integrity: sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==}
engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
'@rnx-kit/babel-preset-metro-react-native@1.1.8':
resolution: {integrity: sha512-8DotuBK1ZgV0H/tmCmtW/3ofA7JR/8aPqSu9lKnuqwBfq4bxz+w1sMyfFl89m4teWlkhgyczWBGD6NCLqTgi9A==}
peerDependencies:
......@@ -2225,6 +2247,12 @@ packages:
'@vue/devtools-shared@7.7.9':
resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==}
'@vue/eslint-config-prettier@10.2.0':
resolution: {integrity: sha512-GL3YBLwv/+b86yHcNNfPJxOTtVFJ4Mbc9UU3zR+KVoG7SwGTjPT+32fXamscNumElhcpXW3mT0DgzS9w32S7Bw==}
peerDependencies:
eslint: '>= 8.21.0'
prettier: '>= 3.0.0'
'@vue/reactivity@3.5.26':
resolution: {integrity: sha512-9EnYB1/DIiUYYnzlnUBgwU32NNvLp/nhxLXeWRhHUEeWNTn1ECxX8aGO7RTXeX6PPcxe3LLuNBFoJbV4QZ+CFQ==}
......@@ -2385,6 +2413,10 @@ packages:
resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==}
engines: {node: '>=8'}
ansi-escapes@7.2.0:
resolution: {integrity: sha512-g6LhBsl+GBPRWGWsBtutpzBYuIIdBkLEvad5C/va/74Db018+5TZiyA26cZJAr3Rft5lprVqOIPxf5Vid6tqAw==}
engines: {node: '>=18'}
ansi-html-community@0.0.8:
resolution: {integrity: sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==}
engines: {'0': node >= 0.8.0}
......@@ -2741,6 +2773,10 @@ packages:
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
engines: {node: '>=8'}
cli-cursor@5.0.0:
resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
engines: {node: '>=18'}
cli-highlight@2.1.11:
resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==}
engines: {node: '>=8.0.0', npm: '>=5.0.0'}
......@@ -2750,6 +2786,10 @@ packages:
resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==}
engines: {node: '>=6'}
cli-truncate@5.1.1:
resolution: {integrity: sha512-SroPvNHxUnk+vIW/dOSfNqdy1sPEFkrTk6TUtqLCnBlo3N7TNYYkzzN7uSD6+jVjrdO4+p8nH7JzH6cIvUem6A==}
engines: {node: '>=20'}
cli-width@3.0.0:
resolution: {integrity: sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==}
engines: {node: '>= 10'}
......@@ -2791,6 +2831,10 @@ packages:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
commander@14.0.2:
resolution: {integrity: sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==}
engines: {node: '>=20'}
commander@2.20.3:
resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==}
......@@ -3285,6 +3329,9 @@ packages:
electron-to-chromium@1.5.267:
resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==}
emoji-regex@10.6.0:
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
......@@ -3330,6 +3377,10 @@ packages:
engines: {node: '>=4'}
hasBin: true
environment@1.1.0:
resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==}
engines: {node: '>=18'}
errno@0.1.8:
resolution: {integrity: sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==}
hasBin: true
......@@ -3401,6 +3452,12 @@ packages:
resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
engines: {node: '>=10'}
eslint-config-prettier@10.1.8:
resolution: {integrity: sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==}
hasBin: true
peerDependencies:
eslint: '>=7.0.0'
eslint-config-taro@4.1.9:
resolution: {integrity: sha512-vPtiWIIb4P2RxuGpTQvRiWNrxuKZvhVl/eSHCyxa6IXo26bTOLcg3Hv5imaDi8NRshqLT0jqcf85dWWBYxBxpw==}
engines: {node: '>= 18'}
......@@ -3451,6 +3508,34 @@ packages:
'@typescript-eslint/parser':
optional: true
eslint-plugin-prettier@5.5.5:
resolution: {integrity: sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==}
engines: {node: ^14.18.0 || >=16.0.0}
peerDependencies:
'@types/eslint': '>=8.0.0'
eslint: '>=8.0.0'
eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0'
prettier: '>=3.0.0'
peerDependenciesMeta:
'@types/eslint':
optional: true
eslint-config-prettier:
optional: true
eslint-plugin-vue@10.7.0:
resolution: {integrity: sha512-r2XFCK4qlo1sxEoAMIoTTX0PZAdla0JJDt1fmYiworZUX67WeEGqm+JbyAg3M+pGiJ5U6Mp5WQbontXWtIW7TA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
'@stylistic/eslint-plugin': ^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0
'@typescript-eslint/parser': ^7.0.0 || ^8.0.0
eslint: ^8.57.0 || ^9.0.0
vue-eslint-parser: ^10.0.0
peerDependenciesMeta:
'@stylistic/eslint-plugin':
optional: true
'@typescript-eslint/parser':
optional: true
eslint-scope@5.1.1:
resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==}
engines: {node: '>=8.0.0'}
......@@ -3459,6 +3544,10 @@ packages:
resolution: {integrity: sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
eslint-scope@8.4.0:
resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
eslint-visitor-keys@2.1.0:
resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==}
engines: {node: '>=10'}
......@@ -3467,6 +3556,10 @@ packages:
resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
eslint-visitor-keys@4.2.1:
resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
eslint@8.41.0:
resolution: {integrity: sha512-WQDQpzGBOP5IrXPo4Hc0814r4/v2rrIsB0rhT7jtunIalgg6gYXWhRMOejVO8yH21T/FGaxjmFjBMNqcIlmH1Q==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
......@@ -3479,6 +3572,10 @@ packages:
deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
hasBin: true
espree@10.4.0:
resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
espree@9.6.1:
resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
......@@ -3513,6 +3610,9 @@ packages:
eventemitter3@4.0.7:
resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
eventemitter3@5.0.4:
resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
events@3.3.0:
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
engines: {node: '>=0.8.x'}
......@@ -3542,6 +3642,9 @@ packages:
fast-deep-equal@3.1.3:
resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
fast-diff@1.3.0:
resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==}
fast-glob@3.3.3:
resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
engines: {node: '>=8.6.0'}
......@@ -3731,6 +3834,10 @@ packages:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*}
get-east-asian-width@1.4.0:
resolution: {integrity: sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==}
engines: {node: '>=18'}
get-intrinsic@1.3.0:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
......@@ -3989,6 +4096,11 @@ packages:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
engines: {node: '>=10.17.0'}
husky@9.1.7:
resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
engines: {node: '>=18'}
hasBin: true
iconv-lite@0.4.24:
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
engines: {node: '>=0.10.0'}
......@@ -4140,6 +4252,10 @@ packages:
resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
engines: {node: '>=8'}
is-fullwidth-code-point@5.1.0:
resolution: {integrity: sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ==}
engines: {node: '>=18'}
is-generator-function@1.1.2:
resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==}
engines: {node: '>= 0.4'}
......@@ -4501,6 +4617,15 @@ packages:
lines-and-columns@1.2.4:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
lint-staged@16.2.7:
resolution: {integrity: sha512-lDIj4RnYmK7/kXMya+qJsmkRFkGolciXjrsZ6PC25GdTfWOAWetR0ZbsNXRAj1EHHImRSalc+whZFg56F5DVow==}
engines: {node: '>=20.17'}
hasBin: true
listr2@9.0.5:
resolution: {integrity: sha512-ME4Fb83LgEgwNw96RKNvKV4VTLuXfoKudAmm2lP8Kk87KaMK0/Xrx/aAkMWmT8mDb+3MlFDspfbCs7adjRxA2g==}
engines: {node: '>=20.0.0'}
loader-runner@4.3.1:
resolution: {integrity: sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==}
engines: {node: '>=6.11.5'}
......@@ -4562,6 +4687,10 @@ packages:
resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
engines: {node: '>=10'}
log-update@6.1.0:
resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==}
engines: {node: '>=18'}
loglevel-plugin-prefix@0.8.4:
resolution: {integrity: sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==}
......@@ -4676,6 +4805,10 @@ packages:
resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==}
engines: {node: '>=6'}
mimic-function@5.0.1:
resolution: {integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==}
engines: {node: '>=18'}
mimic-response@1.0.1:
resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==}
engines: {node: '>=4'}
......@@ -4755,6 +4888,10 @@ packages:
mz@2.7.0:
resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==}
nano-spawn@2.0.0:
resolution: {integrity: sha512-tacvGzUY5o2D8CBh2rrwxyNojUsZNU2zjNTzKQrkgGJQTbGAfArVWXSKMBokBeeg6C7OLRGUEyoFlYbfeWQIqw==}
engines: {node: '>=20.17'}
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
......@@ -4885,6 +5022,10 @@ packages:
resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==}
engines: {node: '>=6'}
onetime@7.0.0:
resolution: {integrity: sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==}
engines: {node: '>=18'}
open@8.4.2:
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
engines: {node: '>=12'}
......@@ -5063,6 +5204,11 @@ packages:
resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
engines: {node: '>=12'}
pidtree@0.6.0:
resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==}
engines: {node: '>=0.10'}
hasBin: true
pify@2.3.0:
resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==}
engines: {node: '>=0.10.0'}
......@@ -5593,6 +5739,15 @@ packages:
resolution: {integrity: sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==}
engines: {node: '>=4'}
prettier-linter-helpers@1.0.1:
resolution: {integrity: sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==}
engines: {node: '>=6.0.0'}
prettier@3.8.1:
resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==}
engines: {node: '>=14'}
hasBin: true
pretty-bytes@5.6.0:
resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==}
engines: {node: '>=6'}
......@@ -5800,6 +5955,10 @@ packages:
resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==}
engines: {node: '>=8'}
restore-cursor@5.1.0:
resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
engines: {node: '>=18'}
retry@0.13.1:
resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}
engines: {node: '>= 4'}
......@@ -6033,6 +6192,10 @@ packages:
resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==}
engines: {node: '>=14.16'}
slice-ansi@7.1.2:
resolution: {integrity: sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w==}
engines: {node: '>=18'}
snake-case@3.0.4:
resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==}
......@@ -6110,6 +6273,10 @@ packages:
resolution: {integrity: sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==}
engines: {node: '>=0.10.0'}
string-argv@0.3.2:
resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==}
engines: {node: '>=0.6.19'}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
......@@ -6118,6 +6285,14 @@ packages:
resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
engines: {node: '>=12'}
string-width@7.2.0:
resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==}
engines: {node: '>=18'}
string-width@8.1.0:
resolution: {integrity: sha512-Kxl3KJGb/gxkaUMOjRsQ8IrXiGW75O4E3RPjFIINOVH8AMl2SQ/yWdTzWwF3FevIX9LcMAjJW+GRwAlAbTSXdg==}
engines: {node: '>=20'}
string.fromcodepoint@0.2.1:
resolution: {integrity: sha512-n69H31OnxSGSZyZbgBlvYIXlrMhJQ0dQAX1js1QDhpaUH6zmU3QYlj07bCwCNlPOu3oRXIubGPl2gDGnHsiCqg==}
......@@ -6243,6 +6418,10 @@ packages:
symbol-tree@3.2.4:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
synckit@0.11.12:
resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==}
engines: {node: ^14.18.0 || >=16.0.0}
tailwindcss-config@1.1.3:
resolution: {integrity: sha512-7AN01Cwz2vd/vl6nKwoR0y/KlgpbKBREp1Q+MHJw8QF53AueVRFgU2Cqq0yhIQ4nC2wGvEJtlBCTeKYPX3TCXQ==}
......@@ -6558,6 +6737,12 @@ packages:
engines: {node: '>=6.0'}
hasBin: true
vue-eslint-parser@10.2.0:
resolution: {integrity: sha512-CydUvFOQKD928UzZhTp4pr2vWz1L+H99t7Pkln2QSPdvmURT0MoC4wUccfCnuEaihNsu9aYYyk+bep8rlfkUXw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
vue-loader@17.4.2:
resolution: {integrity: sha512-yTKOA4R/VN4jqjw4y5HrynFL8AK0Z3/Jt7eOJXEitsm0GMRHDBjCfCiuTiLP7OESvsZYo2pATCWhDqxC5ZrM6w==}
peerDependencies:
......@@ -6726,6 +6911,10 @@ packages:
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
engines: {node: '>=12'}
wrap-ansi@9.0.2:
resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==}
engines: {node: '>=18'}
wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==}
......@@ -6741,6 +6930,10 @@ packages:
utf-8-validate:
optional: true
xml-name-validator@4.0.0:
resolution: {integrity: sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==}
engines: {node: '>=12'}
xml-name-validator@5.0.0:
resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==}
engines: {node: '>=18'}
......@@ -8220,6 +8413,8 @@ snapshots:
'@pkgjs/parseargs@0.11.0':
optional: true
'@pkgr/core@0.2.9': {}
'@rnx-kit/babel-preset-metro-react-native@1.1.8(@babel/core@7.28.5)(@babel/plugin-transform-typescript@7.28.5(@babel/core@7.28.5))(@babel/runtime@7.28.4)':
dependencies:
'@babel/core': 7.28.5
......@@ -9113,6 +9308,15 @@ snapshots:
dependencies:
rfdc: 1.4.1
'@vue/eslint-config-prettier@10.2.0(@types/eslint@9.6.1)(eslint@8.57.1)(prettier@3.8.1)':
dependencies:
eslint: 8.57.1
eslint-config-prettier: 10.1.8(eslint@8.57.1)
eslint-plugin-prettier: 5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@8.57.1))(eslint@8.57.1)(prettier@3.8.1)
prettier: 3.8.1
transitivePeerDependencies:
- '@types/eslint'
'@vue/reactivity@3.5.26':
dependencies:
'@vue/shared': 3.5.26
......@@ -9311,6 +9515,10 @@ snapshots:
dependencies:
type-fest: 0.21.3
ansi-escapes@7.2.0:
dependencies:
environment: 1.1.0
ansi-html-community@0.0.8: {}
ansi-regex@5.0.1: {}
......@@ -9765,6 +9973,10 @@ snapshots:
dependencies:
restore-cursor: 3.1.0
cli-cursor@5.0.0:
dependencies:
restore-cursor: 5.1.0
cli-highlight@2.1.11:
dependencies:
chalk: 4.1.2
......@@ -9776,6 +9988,11 @@ snapshots:
cli-spinners@2.9.2: {}
cli-truncate@5.1.1:
dependencies:
slice-ansi: 7.1.2
string-width: 8.1.0
cli-width@3.0.0: {}
cliui@6.0.0:
......@@ -9820,6 +10037,8 @@ snapshots:
dependencies:
delayed-stream: 1.0.0
commander@14.0.2: {}
commander@2.20.3: {}
commander@4.1.1: {}
......@@ -10343,6 +10562,8 @@ snapshots:
electron-to-chromium@1.5.267: {}
emoji-regex@10.6.0: {}
emoji-regex@8.0.0: {}
emoji-regex@9.2.2: {}
......@@ -10372,6 +10593,8 @@ snapshots:
envinfo@7.21.0: {}
environment@1.1.0: {}
errno@0.1.8:
dependencies:
prr: 1.0.1
......@@ -10538,13 +10761,19 @@ snapshots:
escape-string-regexp@4.0.0: {}
eslint-config-taro@4.1.9(@babel/core@7.28.5)(eslint@8.57.1)(typescript@5.9.3):
eslint-config-prettier@10.1.8(eslint@8.57.1):
dependencies:
eslint: 8.57.1
eslint-config-taro@4.1.9(@babel/core@7.28.5)(eslint-plugin-vue@10.7.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(vue-eslint-parser@10.2.0(eslint@8.57.1)))(eslint@8.57.1)(typescript@5.9.3):
dependencies:
'@babel/eslint-parser': 7.28.5(@babel/core@7.28.5)(eslint@8.57.1)
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)
'@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3)
eslint: 8.57.1
eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)
optionalDependencies:
eslint-plugin-vue: 10.7.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(vue-eslint-parser@10.2.0(eslint@8.57.1))
transitivePeerDependencies:
- '@babel/core'
- eslint-import-resolver-typescript
......@@ -10599,6 +10828,29 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
eslint-plugin-prettier@5.5.5(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@8.57.1))(eslint@8.57.1)(prettier@3.8.1):
dependencies:
eslint: 8.57.1
prettier: 3.8.1
prettier-linter-helpers: 1.0.1
synckit: 0.11.12
optionalDependencies:
'@types/eslint': 9.6.1
eslint-config-prettier: 10.1.8(eslint@8.57.1)
eslint-plugin-vue@10.7.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(vue-eslint-parser@10.2.0(eslint@8.57.1)):
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@8.57.1)
eslint: 8.57.1
natural-compare: 1.4.0
nth-check: 2.1.1
postcss-selector-parser: 7.1.1
semver: 7.7.3
vue-eslint-parser: 10.2.0(eslint@8.57.1)
xml-name-validator: 4.0.0
optionalDependencies:
'@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.9.3)
eslint-scope@5.1.1:
dependencies:
esrecurse: 4.3.0
......@@ -10609,10 +10861,17 @@ snapshots:
esrecurse: 4.3.0
estraverse: 5.3.0
eslint-scope@8.4.0:
dependencies:
esrecurse: 4.3.0
estraverse: 5.3.0
eslint-visitor-keys@2.1.0: {}
eslint-visitor-keys@3.4.3: {}
eslint-visitor-keys@4.2.1: {}
eslint@8.41.0:
dependencies:
'@eslint-community/eslint-utils': 4.9.1(eslint@8.41.0)
......@@ -10700,6 +10959,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
espree@10.4.0:
dependencies:
acorn: 8.15.0
acorn-jsx: 5.3.2(acorn@8.15.0)
eslint-visitor-keys: 4.2.1
espree@9.6.1:
dependencies:
acorn: 8.15.0
......@@ -10726,6 +10991,8 @@ snapshots:
eventemitter3@4.0.7: {}
eventemitter3@5.0.4: {}
events@3.3.0: {}
execa@5.1.1:
......@@ -10791,6 +11058,8 @@ snapshots:
fast-deep-equal@3.1.3: {}
fast-diff@1.3.0: {}
fast-glob@3.3.3:
dependencies:
'@nodelib/fs.stat': 2.0.5
......@@ -10969,6 +11238,8 @@ snapshots:
get-caller-file@2.0.5: {}
get-east-asian-width@1.4.0: {}
get-intrinsic@1.3.0:
dependencies:
call-bind-apply-helpers: 1.0.2
......@@ -11311,6 +11582,8 @@ snapshots:
human-signals@2.1.0: {}
husky@9.1.7: {}
iconv-lite@0.4.24:
dependencies:
safer-buffer: 2.1.2
......@@ -11465,6 +11738,10 @@ snapshots:
is-fullwidth-code-point@3.0.0: {}
is-fullwidth-code-point@5.1.0:
dependencies:
get-east-asian-width: 1.4.0
is-generator-function@1.1.2:
dependencies:
call-bound: 1.0.4
......@@ -11816,6 +12093,25 @@ snapshots:
lines-and-columns@1.2.4: {}
lint-staged@16.2.7:
dependencies:
commander: 14.0.2
listr2: 9.0.5
micromatch: 4.0.8
nano-spawn: 2.0.0
pidtree: 0.6.0
string-argv: 0.3.2
yaml: 2.8.2
listr2@9.0.5:
dependencies:
cli-truncate: 5.1.1
colorette: 2.0.20
eventemitter3: 5.0.4
log-update: 6.1.0
rfdc: 1.4.1
wrap-ansi: 9.0.2
loader-runner@4.3.1: {}
loader-utils@1.4.2:
......@@ -11872,6 +12168,14 @@ snapshots:
chalk: 4.1.2
is-unicode-supported: 0.1.0
log-update@6.1.0:
dependencies:
ansi-escapes: 7.2.0
cli-cursor: 5.0.0
slice-ansi: 7.1.2
strip-ansi: 7.1.2
wrap-ansi: 9.0.2
loglevel-plugin-prefix@0.8.4: {}
loglevel@1.9.2: {}
......@@ -11958,6 +12262,8 @@ snapshots:
mimic-fn@2.1.0: {}
mimic-function@5.0.1: {}
mimic-response@1.0.1: {}
mini-css-extract-plugin@2.9.4(webpack@5.91.0(@swc/core@1.3.96)):
......@@ -12040,6 +12346,8 @@ snapshots:
object-assign: 4.1.1
thenify-all: 1.6.0
nano-spawn@2.0.0: {}
nanoid@3.3.11: {}
native-request@1.1.2:
......@@ -12165,6 +12473,10 @@ snapshots:
dependencies:
mimic-fn: 2.1.0
onetime@7.0.0:
dependencies:
mimic-function: 5.0.1
open@8.4.2:
dependencies:
define-lazy-prop: 2.0.0
......@@ -12335,6 +12647,8 @@ snapshots:
picomatch@4.0.3: {}
pidtree@0.6.0: {}
pify@2.3.0: {}
pify@3.0.0: {}
......@@ -12892,6 +13206,12 @@ snapshots:
prepend-http@2.0.0: {}
prettier-linter-helpers@1.0.1:
dependencies:
fast-diff: 1.3.0
prettier@3.8.1: {}
pretty-bytes@5.6.0: {}
pretty-error@4.0.0:
......@@ -13122,6 +13442,11 @@ snapshots:
onetime: 5.1.2
signal-exit: 3.0.7
restore-cursor@5.1.0:
dependencies:
onetime: 7.0.0
signal-exit: 4.1.0
retry@0.13.1: {}
reusify@1.1.0: {}
......@@ -13388,6 +13713,11 @@ snapshots:
slash@5.1.0: {}
slice-ansi@7.1.2:
dependencies:
ansi-styles: 6.2.3
is-fullwidth-code-point: 5.1.0
snake-case@3.0.4:
dependencies:
dot-case: 3.0.4
......@@ -13470,6 +13800,8 @@ snapshots:
strict-uri-encode@1.1.0: {}
string-argv@0.3.2: {}
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
......@@ -13482,6 +13814,17 @@ snapshots:
emoji-regex: 9.2.2
strip-ansi: 7.1.2
string-width@7.2.0:
dependencies:
emoji-regex: 10.6.0
get-east-asian-width: 1.4.0
strip-ansi: 7.1.2
string-width@8.1.0:
dependencies:
get-east-asian-width: 1.4.0
strip-ansi: 7.1.2
string.fromcodepoint@0.2.1: {}
string.prototype.trim@1.2.10:
......@@ -13616,6 +13959,10 @@ snapshots:
symbol-tree@3.2.4: {}
synckit@0.11.12:
dependencies:
'@pkgr/core': 0.2.9
tailwindcss-config@1.1.3:
dependencies:
'@weapp-tailwindcss/shared': 1.1.1
......@@ -13957,6 +14304,18 @@ snapshots:
acorn: 8.15.0
acorn-walk: 8.3.4
vue-eslint-parser@10.2.0(eslint@8.57.1):
dependencies:
debug: 4.4.3
eslint: 8.57.1
eslint-scope: 8.4.0
eslint-visitor-keys: 4.2.1
espree: 10.4.0
esquery: 1.7.0
semver: 7.7.3
transitivePeerDependencies:
- supports-color
vue-loader@17.4.2(@vue/compiler-sfc@3.5.26)(vue@3.5.26(typescript@5.9.3))(webpack@5.91.0(@swc/core@1.3.96)):
dependencies:
chalk: 4.1.2
......@@ -14229,10 +14588,18 @@ snapshots:
string-width: 5.1.2
strip-ansi: 7.1.2
wrap-ansi@9.0.2:
dependencies:
ansi-styles: 6.2.3
string-width: 7.2.0
strip-ansi: 7.1.2
wrappy@1.0.2: {}
ws@8.19.0: {}
xml-name-validator@4.0.0: {}
xml-name-validator@5.0.0: {}
xmlchars@2.2.0: {}
......
#!/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;
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;
}
})
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,12 +32,13 @@ if (process.env.NODE_ENV === 'development') {
pages.push('pages/tailwindTest/index')
}
const subpackages = process.env.NODE_ENV === 'development'
const subpackages =
process.env.NODE_ENV === 'development'
? [
{
root: 'pages/demo',
pages: ['index'],
},
pages: ['index']
}
]
: []
......@@ -48,6 +49,6 @@ export default {
backgroundTextStyle: 'light',
navigationBarBackgroundColor: '#fff',
navigationBarTitleText: '西园寺预约',
navigationBarTextStyle: 'black',
},
navigationBarTextStyle: 'black'
}
}
......
......@@ -11,7 +11,10 @@ import './utils/polyfill'
import './app.less'
import { saveCurrentPagePath, hasAuth, silentAuth, navigateToAuth } from '@/utils/authRedirect'
import Taro from '@tarojs/taro'
import { refresh_offline_booking_cache, has_offline_booking_cache } from '@/composables/useOfflineBookingCache'
import {
refresh_offline_booking_cache,
has_offline_booking_cache
} from '@/composables/useOfflineBookingCache'
import { is_usable_network, get_network_type } from '@/utils/network'
import { enable_offline_booking_cache_polling } from '@/composables/useOfflineBookingCachePolling'
import { weak_network_text, get_weak_network_modal_use_cache_options } from '@/utils/uiText'
......@@ -28,7 +31,7 @@ const App = createApp({
const query = options?.query || {}
const query_string = Object.keys(query)
.map((key) => `${key}=${encodeURIComponent(query[key])}`)
.map(key => `${key}=${encodeURIComponent(query[key])}`)
.join('&')
const full_path = query_string ? `${path}?${query_string}` : path
......@@ -61,10 +64,18 @@ const App = createApp({
const pages = Taro.getCurrentPages ? Taro.getCurrentPages() : []
const current_page = pages && pages.length ? pages[pages.length - 1] : null
const current_route = String(current_page?.route || '')
if (!current_route) return false
if (current_route.includes('pages/offlineBookingList/index')) return true
if (current_route.includes('pages/offlineBookingDetail/index')) return true
if (current_route.includes('pages/offlineBookingCode/index')) return true
if (!current_route) {
return false
}
if (current_route.includes('pages/offlineBookingList/index')) {
return true
}
if (current_route.includes('pages/offlineBookingDetail/index')) {
return true
}
if (current_route.includes('pages/offlineBookingCode/index')) {
return true
}
return false
}
......@@ -77,13 +88,19 @@ const App = createApp({
* @returns {Promise<boolean>} true=需要中断后续启动流程,false=继续
*/
const handle_bad_network = async (network_type) => {
if (has_shown_network_modal) return false
if (should_skip_network_prompt()) return false
const handle_bad_network = async network_type => {
if (has_shown_network_modal) {
return false
}
if (should_skip_network_prompt()) {
return false
}
const is_none_network = network_type === 'none'
const is_weak_network = !is_usable_network(network_type)
if (!is_weak_network) return false
if (!is_weak_network) {
return false
}
has_shown_network_modal = true
......@@ -99,7 +116,11 @@ const App = createApp({
}
} else {
try {
await Taro.showToast({ title: weak_network_text.toast_title, icon: 'none', duration: 2000 })
await Taro.showToast({
title: weak_network_text.toast_title,
icon: 'none',
duration: 2000
})
} catch (e) {
return is_none_network
}
......@@ -112,7 +133,7 @@ const App = createApp({
* 监听网络状态变化
* - 当网络连接且有授权时,预加载离线预约记录数据
*/
Taro.onNetworkStatusChange((res) => {
Taro.onNetworkStatusChange(res => {
const is_connected = res?.isConnected !== false
const network_type = res?.networkType || 'none'
const network_usable = is_connected && is_usable_network(network_type)
......@@ -120,7 +141,9 @@ const App = createApp({
if (network_usable) {
has_shown_network_modal = false
last_network_usable = true
if (hasAuth()) preloadBookingData()
if (hasAuth()) {
preloadBookingData()
}
return
}
......@@ -129,7 +152,6 @@ const App = createApp({
if (should_prompt) {
handle_bad_network(network_type)
}
return
})
/**
......@@ -144,7 +166,9 @@ const App = createApp({
* - 仅在首次启动时检查网络情况
* - 如果用户已展示过提示弹窗,则直接返回 false
*/
if (has_shown_network_modal) return false
if (has_shown_network_modal) {
return false
}
const network_type = await get_network_type()
last_network_usable = is_usable_network(network_type)
......@@ -157,9 +181,11 @@ const App = createApp({
* @returns {void} 无返回值
*/
const try_preload_when_online = () => {
if (!hasAuth()) return
if (!hasAuth()) {
return
}
Taro.getNetworkType({
success: (res) => {
success: res => {
if (is_usable_network(res.networkType)) {
preloadBookingData()
}
......@@ -182,7 +208,9 @@ const App = createApp({
// 处理在启动时出现的不良网络情况
const should_stop = await handle_bad_network_on_launch()
// 如果用户选择进入离线模式,则直接返回
if (should_stop) return
if (should_stop) {
return
}
/**
* 尝试在有授权时预加载离线预约记录数据
......@@ -197,7 +225,9 @@ const App = createApp({
return
}
if (path === 'pages/auth/index') return
if (path === 'pages/auth/index') {
return
}
try {
// 尝试静默授权
......@@ -209,12 +239,9 @@ const App = createApp({
// 授权失败则跳转至授权页面
navigateToAuth(full_path || undefined)
}
return
},
onShow() {
},
});
onShow() {}
})
App.use(createPinia())
......
......@@ -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,52 +55,52 @@ 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 = () => {
......@@ -107,29 +108,29 @@ const generateQrCodes = () => {
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 = '';
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`;
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>`;
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);
const buffer = new ArrayBuffer(svg.length)
const view = new Uint8Array(buffer)
for (let i = 0; i < svg.length; i++) {
view[i] = svg.charCodeAt(i);
view[i] = svg.charCodeAt(i)
}
const b64 = Taro.arrayBufferToBase64(buffer);
const b64 = Taro.arrayBufferToBase64(buffer)
qrCodeImages.value[item.qr_code] = `data:image/svg+xml;base64,${b64}`;
qrCodeImages.value[item.qr_code] = `data:image/svg+xml;base64,${b64}`
} catch (err) {
console.error('QR Gen Error', err);
console.error('QR Gen Error', err)
}
}
}
......@@ -137,18 +138,21 @@ const generateQrCodes = () => {
onMounted(() => {
if (props.list && props.list.length > 0) {
userList.value = props.list;
generateQrCodes();
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;
}
}
......
......@@ -16,7 +16,10 @@
</view>
<view class="center">
<image :src="currentQrCodeUrl" 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">二维码{{ get_qrcode_status_text(useStatus) }}</text>
</view>
......@@ -25,7 +28,7 @@
<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 class="user-list">
<view
......@@ -35,16 +38,24 @@
: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="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 style="text-align: center; color: #A67939; margin-top: 16rpx;">查看我的“<text @tap="toRecord" style="text-decoration: underline; color: #ED9820;">预约记录</text>”</view>
<view style="text-align: center; color: #a67939; margin-top: 16rpx"
>查看我的“<text @tap="toRecord" style="text-decoration: underline; color: #ed9820"
>预约记录</text
>”</view
>
</view>
</view>
</template>
......@@ -52,12 +63,12 @@
<script setup>
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
import Taro from '@tarojs/taro'
import { formatDatetime, mask_id_number, get_qrcode_status_text } from '@/utils/tools';
import { formatDatetime, mask_id_number, get_qrcode_status_text } from '@/utils/tools'
import { qrcodeListAPI, qrcodeStatusAPI, billPersonAPI } from '@/api/index'
import { useGo } from '@/hooks/useGo'
import BASE_URL from '@/utils/config';
import BASE_URL from '@/utils/config'
const go = useGo();
const go = useGo()
const props = defineProps({
status: {
......@@ -68,80 +79,81 @@ const props = defineProps({
type: String,
default: ''
},
payId: { // 接收 payId
payId: {
// 接收 payId
type: String,
default: ''
}
});
})
const select_index = ref(0);
const userList = ref([]);
const select_index = ref(0)
const userList = ref([])
/**
* @description 切换到上一张二维码(循环)
* @returns {void} 无返回值
*/
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
}
};
}
/**
* @description 切换到下一张二维码(循环)
* @returns {void} 无返回值
*/
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
}
};
}
watch(
() => select_index.value,
() => {
refreshBtn();
refreshBtn()
}
)
watch(
() => props.payId,
(val) => {
val => {
if (val) {
init();
init()
}
},
{ immediate: true }
)
const formatId = (id) => mask_id_number(id)
const formatId = id => mask_id_number(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 url = userList.value[select_index.value]?.qr_code_url;
const url = userList.value[select_index.value]?.qr_code_url
if (url && url.startsWith('/')) {
return BASE_URL + url;
return BASE_URL + url
}
return url;
return url
})
const useStatus = ref('0');
const useStatus = ref('0')
const STATUS_CODE = {
APPLY: '1',
SUCCESS: '3',
CANCELED: '5',
USED: '7',
};
USED: '7'
}
/**
* @description 刷新当前选中二维码状态
......@@ -149,10 +161,14 @@ const STATUS_CODE = {
* @returns {Promise<void>} 无返回值
*/
const refreshBtn = async () => {
if (!userList.value[select_index.value]) return;
const { code, data } = await qrcodeStatusAPI({ qr_code: userList.value[select_index.value].qr_code });
if (!userList.value[select_index.value]) {
return
}
const { code, data } = await qrcodeStatusAPI({
qr_code: userList.value[select_index.value].qr_code
})
if (code) {
useStatus.value = data.status;
useStatus.value = data.status
}
}
......@@ -161,8 +177,8 @@ const refreshBtn = async () => {
* @param {number} index 下标
* @returns {void} 无返回值
*/
const selectUser = (index) => {
select_index.value = index;
const selectUser = index => {
select_index.value = index
}
/**
......@@ -171,17 +187,17 @@ const selectUser = (index) => {
* @param {Array<Object>} data 二维码列表
* @returns {Array<Object>} 处理后的列表
*/
const formatGroup = (data) => {
let lastPayId = null;
const formatGroup = data => {
let lastPayId = null
for (let i = 0; i < data.length; i++) {
if (data[i].pay_id !== lastPayId) {
data[i].sort = 1;
lastPayId = data[i].pay_id;
data[i].sort = 1
lastPayId = data[i].pay_id
} else {
data[i].sort = 0;
data[i].sort = 0
}
}
return data;
return data
}
/**
......@@ -193,55 +209,55 @@ const formatGroup = (data) => {
const init = async () => {
if (!props.type) {
try {
const { code, data } = await qrcodeListAPI();
const { code, data } = await qrcodeListAPI()
if (code) {
data.forEach(item => {
item.qr_code_url = '/admin?m=srv&a=get_qrcode&key=' + item.qr_code;
item.qr_code_url = `/admin?m=srv&a=get_qrcode&key=${item.qr_code}`
item.datetime = formatDatetime({ begin_time: item.begin_time, end_time: item.end_time })
item.sort = 0;
});
item.sort = 0
})
// 剔除qr_code为空的二维码
const validData = data.filter(item => item.qr_code !== '');
const validData = data.filter(item => item.qr_code !== '')
if (validData.length > 0) {
userList.value = formatGroup(validData);
refreshBtn();
userList.value = formatGroup(validData)
refreshBtn()
} else {
userList.value = [];
userList.value = []
}
}
} catch (err) {
console.error('Fetch QR List Failed:', err);
console.error('Fetch QR List Failed:', err)
}
} else {
if (props.payId) {
const { code, data } = await billPersonAPI({ pay_id: props.payId });
const { code, data } = await billPersonAPI({ pay_id: props.payId })
if (code) {
data.forEach(item => {
item.qr_code_url = '/admin?m=srv&a=get_qrcode&key=' + item.qr_code;
item.sort = 0;
item.qr_code_url = `/admin?m=srv&a=get_qrcode&key=${item.qr_code}`
item.sort = 0
// billPersonAPI 返回的数据可能没有 datetime 字段,需要检查
// 如果没有,可能需要从外部传入或者假设是当天的?
// H5 代码没有处理 datetime,但在 template 里用了。
// 这里暂且不做处理,如果没有 datetime 就不显示
});
const validData = data.filter(item => item.qr_code !== '');
})
const validData = data.filter(item => item.qr_code !== '')
if (validData.length > 0) {
userList.value = validData;
refreshBtn();
userList.value = validData
refreshBtn()
} else {
userList.value = [];
userList.value = []
}
}
}
}
};
}
onMounted(() => {
init();
start_polling();
});
init()
start_polling()
})
/**
* @description 轮询刷新二维码状态
......@@ -251,13 +267,15 @@ onMounted(() => {
const poll = async () => {
if (userList.value.length && useStatus.value === STATUS_CODE.SUCCESS) {
if (userList.value[select_index.value]) {
const { code, data } = await qrcodeStatusAPI({ qr_code: userList.value[select_index.value].qr_code });
const { code, data } = await qrcodeStatusAPI({
qr_code: userList.value[select_index.value].qr_code
})
if (code) {
useStatus.value = data.status;
useStatus.value = data.status
}
}
}
};
}
const interval_id = ref(null)
/**
......@@ -267,7 +285,9 @@ const interval_id = ref(null)
*/
const start_polling = () => {
if (interval_id.value) return
if (interval_id.value) {
return
}
interval_id.value = setInterval(poll, 3000)
}
......@@ -278,14 +298,16 @@ const start_polling = () => {
*/
const stop_polling = () => {
if (!interval_id.value) return
if (!interval_id.value) {
return
}
clearInterval(interval_id.value)
interval_id.value = null
}
onUnmounted(() => {
stop_polling();
});
stop_polling()
})
defineExpose({ start_polling, stop_polling })
......@@ -294,7 +316,7 @@ defineExpose({ start_polling, stop_polling })
* @returns {void} 无返回值
*/
const toRecord = () => {
go('/bookingList');
go('/bookingList')
}
</script>
......@@ -306,12 +328,12 @@ const toRecord = () => {
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;
......@@ -321,16 +343,19 @@ const toRecord = () => {
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;
}
.qrcode-used {
position: absolute;
......@@ -350,7 +375,7 @@ const toRecord = () => {
}
.status-text {
color: #A67939;
color: #a67939;
position: absolute;
top: 50%;
left: 50%;
......@@ -364,7 +389,8 @@ const toRecord = () => {
}
.right {
image {
width: 56rpx; height: 56rpx;
width: 56rpx;
height: 56rpx;
margin-left: 16rpx;
}
}
......@@ -378,13 +404,13 @@ const toRecord = () => {
.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;
......@@ -394,7 +420,7 @@ const toRecord = () => {
top: calc(50% - 16rpx);
content: '';
height: 32rpx;
border-right: 2rpx solid #A67939;
border-right: 2rpx solid #a67939;
}
}
}
......@@ -408,7 +434,7 @@ const toRecord = () => {
margin-bottom: 32rpx;
.no-qrcode-title {
color: #A67939;
color: #a67939;
font-size: 34rpx;
}
}
......
......@@ -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,12 +232,13 @@ 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;
......@@ -233,7 +258,7 @@ watch(
}
.status-text {
color: #A67939;
color: #a67939;
position: absolute;
top: 50%;
left: 50%;
......@@ -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 {
......@@ -265,10 +289,14 @@ const status_info = computed(() => {
* @param {Object} item 预约记录
* @returns {void} 无返回值
*/
const goToDetail = (item) => {
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 });
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) => {
watch(
is_pay_pending,
val => {
if (val) {
start_countdown()
} else {
countdown_seconds.value = 0
stop_countdown()
}
}, { immediate: true })
},
{ immediate: true }
)
onUnmounted(() => {
stop_countdown()
......@@ -294,18 +326,18 @@ 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;
......@@ -319,20 +351,20 @@ onUnmounted(() => {
&.offline {
color: #999;
background-color: #EEE;
background-color: #eee;
}
&.success {
color: #A67939;
background-color: #FBEEDC;
color: #a67939;
background-color: #fbeedc;
}
&.cancel {
color: #999;
background-color: #EEE;
background-color: #eee;
}
&.used {
color: #477F3D;
background-color: #E5EFE3;
color: #477f3d;
background-color: #e5efe3;
}
}
}
......@@ -346,13 +378,15 @@ onUnmounted(() => {
color: #666;
.num-body {
span, text {
color: #A67939;
span,
text {
color: #a67939;
font-weight: bold;
}
}
}
.booking-price, .booking-time {
.booking-price,
.booking-time {
color: #999;
font-size: 29rpx;
margin-bottom: 10rpx;
......@@ -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 }
......
<template>
<picker mode="multiSelector" :range-key="'name'" :value="timeIndex" :range="activityArray" :disabled="disabled"
@change="bindMultiPickerChange" @columnChange="bindMultiPickerColumnChange">
<picker
mode="multiSelector"
:range-key="'name'"
:value="timeIndex"
:range="activityArray"
:disabled="disabled"
@change="bindMultiPickerChange"
@columnChange="bindMultiPickerColumnChange"
>
<slot />
</picker>
</template>
<script>
import { dateTimePicker, dateDate } from "./dateTimePicker.js";
import { dateTimePicker, dateDate } from './dateTimePicker.js'
export default {
name: "TimePickerDataPicker",
name: 'TimePickerDataPicker',
props: {
startTime: {
type: [Object, Date],
default: new Date(),
default: new Date()
},
endTime: {
type: [Object, Date],
default: new Date(),
default: new Date()
},
defaultTime: {
type: [Object, Date],
default: new Date(),
default: new Date()
},
disabled: {
type: Boolean,
default: false,
},
default: false
}
},
data() {
return {
......@@ -35,150 +42,139 @@ export default {
day: 1,
hour: 0,
minute: 0,
datePicker: "",
datePicker: '',
defaultIndex: [0, 0, 0, 0, 0],
startIndex: [0, 0, 0, 0, 0],
endIndex: [0, 0, 0, 0, 0],
};
endIndex: [0, 0, 0, 0, 0]
}
},
computed: {
timeDate() {
const { startTime, endTime } = this;
return { startTime, endTime };
},
const { startTime, endTime } = this
return { startTime, endTime }
}
},
watch: {
timeDate() {
this.initData();
this.initData()
},
defaultTime () {
this.initData();
defaultTime() {
this.initData()
}
},
created() {
this.initData();
this.initData()
},
methods: {
initData() {
let startTime = this.startTime;
let endTime = this.endTime;
this.datePicker = dateTimePicker(
startTime.getFullYear(),
endTime.getFullYear()
);
this.setDateData(this.defaultTime);
this.getKeyIndex(this.startTime, "startIndex");
const startTime = this.startTime
const endTime = this.endTime
this.datePicker = dateTimePicker(startTime.getFullYear(), endTime.getFullYear())
this.setDateData(this.defaultTime)
this.getKeyIndex(this.startTime, 'startIndex')
// 截止时间索引
this.getKeyIndex(this.endTime, "endIndex");
this.getKeyIndex(this.endTime, 'endIndex')
// 默认索引
this.getKeyIndex(this.defaultTime, "defaultIndex");
this.timeIndex = this.defaultIndex;
this.getKeyIndex(this.defaultTime, 'defaultIndex')
this.timeIndex = this.defaultIndex
// 初始时间
this.initTime();
this.initTime()
},
getKeyIndex(time, key) {
let Arr = dateDate(time);
let _index = this.getIndex(Arr);
this[key] = _index;
const Arr = dateDate(time)
const _index = this.getIndex(Arr)
this[key] = _index
},
getIndex(arr) {
let timeIndex = [];
let indexKey = ["year", "month", "day", "hours", "minutes"];
const timeIndex = []
const indexKey = ['year', 'month', 'day', 'hours', 'minutes']
this.activityArray.forEach((element, index) => {
let _index = element.findIndex(
(item) => parseInt(item.id) === parseInt(arr[indexKey[index]])
);
timeIndex[index] = _index >= 0 ? _index : 0;
});
return timeIndex;
const _index = element.findIndex(
item => parseInt(item.id) === parseInt(arr[indexKey[index]])
)
timeIndex[index] = _index >= 0 ? _index : 0
})
return timeIndex
},
initTime() {
let _index = this.timeIndex;
this.year = this.activityArray[0][_index[0]].id;
this.month = this.activityArray[1].length && this.activityArray[1][_index[1]].id;
this.day = this.activityArray[2].length && this.activityArray[2][_index[2]].id;
this.hour = this.activityArray[3].length && this.activityArray[3][_index[3]].id;
this.minute = this.activityArray[4].length && this.activityArray[4][_index[4]].id;
const _index = this.timeIndex
this.year = this.activityArray[0][_index[0]].id
this.month = this.activityArray[1].length && this.activityArray[1][_index[1]].id
this.day = this.activityArray[2].length && this.activityArray[2][_index[2]].id
this.hour = this.activityArray[3].length && this.activityArray[3][_index[3]].id
this.minute = this.activityArray[4].length && this.activityArray[4][_index[4]].id
},
setDateData(_date) {
let _data = dateDate(_date);
this.activityArray = this.datePicker(_data.year, _data.month);
const _data = dateDate(_date)
this.activityArray = this.datePicker(_data.year, _data.month)
},
bindMultiPickerChange(e) {
console.log("picker发送选择改变,携带值为", e.detail.value);
let activityArray = JSON.parse(JSON.stringify(this.activityArray)),
console.log('picker发送选择改变,携带值为', e.detail.value)
const activityArray = JSON.parse(JSON.stringify(this.activityArray)),
{ value } = e.detail,
_result = [];
_result = []
for (let i = 0; i < value.length; i++) {
_result[i] = activityArray[i][value[i]].id;
_result[i] = activityArray[i][value[i]].id
}
this.$emit("result", _result);
this.$emit('result', _result)
},
bindMultiPickerColumnChange(e) {
console.log("修改的列为", e.detail.column, ",值为", e.detail.value);
console.log('修改的列为', e.detail.column, ',值为', e.detail.value)
let _data = JSON.parse(JSON.stringify(this.activityArray)),
timeIndex = JSON.parse(JSON.stringify(this.timeIndex)),
{ startIndex, endIndex } = this,
{ column, value } = e.detail,
_value = _data[column][value].id,
_start = dateDate(this.startTime),
_end = dateDate(this.endTime);
_end = dateDate(this.endTime)
switch (e.detail.column) {
case 0:
if (_value <= _start.year) {
timeIndex = startIndex;
this.year = _start.year;
this.setDateData(this.startTime);
timeIndex = startIndex
this.year = _start.year
this.setDateData(this.startTime)
} else if (_value >= _end.year) {
this.year = _end.year;
timeIndex = [endIndex[0], 0, 0, 0, 0];
this.setDateData(this.endTime);
this.year = _end.year
timeIndex = [endIndex[0], 0, 0, 0, 0]
this.setDateData(this.endTime)
} else {
this.year = _value;
timeIndex = [value, 0, 0, 0, 0];
this.activityArray = this.datePicker(_value, 1);
this.year = _value
timeIndex = [value, 0, 0, 0, 0]
this.activityArray = this.datePicker(_value, 1)
}
timeIndex = this.timeIndex = JSON.parse(JSON.stringify(timeIndex));
this.timeIndex = timeIndex;
break;
timeIndex = this.timeIndex = JSON.parse(JSON.stringify(timeIndex))
this.timeIndex = timeIndex
break
case 1:
if (this.year == _start.year && value <= startIndex[1]) {
timeIndex = startIndex;
this.month = _start.month;
this.setDateData(this.startTime);
timeIndex = startIndex
this.month = _start.month
this.setDateData(this.startTime)
} else if (this.year == _end.year && value >= endIndex[1]) {
timeIndex = endIndex;
this.month = _end.month;
this.setDateData(this.endTime);
timeIndex = endIndex
this.month = _end.month
this.setDateData(this.endTime)
} else {
this.month = _value;
_data[2] = this.datePicker(this.year, this.month)[2];
timeIndex = [timeIndex[0], value, 0, 0, 0];
this.activityArray = _data;
this.month = _value
_data[2] = this.datePicker(this.year, this.month)[2]
timeIndex = [timeIndex[0], value, 0, 0, 0]
this.activityArray = _data
}
this.timeIndex = JSON.parse(JSON.stringify(timeIndex));
break;
this.timeIndex = JSON.parse(JSON.stringify(timeIndex))
break
case 2:
if (
this.year == _start.year &&
this.month == _start.month &&
value <= startIndex[2]
) {
this.day = _start.day;
timeIndex = startIndex;
} else if (
this.year == _end.year &&
this.month == _end.month &&
value >= endIndex[2]
) {
this.day = _end.day;
timeIndex = endIndex;
if (this.year == _start.year && this.month == _start.month && value <= startIndex[2]) {
this.day = _start.day
timeIndex = startIndex
} else if (this.year == _end.year && this.month == _end.month && value >= endIndex[2]) {
this.day = _end.day
timeIndex = endIndex
} else {
this.day = _value;
timeIndex = [timeIndex[0], timeIndex[1], value, 0, 0];
this.day = _value
timeIndex = [timeIndex[0], timeIndex[1], value, 0, 0]
}
this.timeIndex = JSON.parse(JSON.stringify(timeIndex));
break;
this.timeIndex = JSON.parse(JSON.stringify(timeIndex))
break
case 3:
if (
this.year == _start.year &&
......@@ -186,25 +182,25 @@ export default {
this.day == _start.day &&
value <= startIndex[3]
) {
this.hour = _start.hours;
timeIndex = startIndex;
this.hour = _start.hours
timeIndex = startIndex
} else if (
this.year == _end.year &&
this.month == _end.month &&
this.day == _end.day &&
value >= endIndex[3]
) {
this.hour = _end.hours;
timeIndex = endIndex;
this.hour = _end.hours
timeIndex = endIndex
} else {
this.hour = _value;
timeIndex[3] = value;
timeIndex[4] = 0;
this.hour = _value
timeIndex[3] = value
timeIndex[4] = 0
}
this.timeIndex = JSON.parse(JSON.stringify(timeIndex));
break;
this.timeIndex = JSON.parse(JSON.stringify(timeIndex))
break
case 4:
timeIndex[4] = value;
timeIndex[4] = value
if (
this.year == _start.year &&
this.month == _start.month &&
......@@ -212,7 +208,7 @@ export default {
this.hour == _start.hours &&
value <= startIndex[4]
) {
timeIndex = startIndex;
timeIndex = startIndex
} else if (
this.year == _end.year &&
this.month == _end.month &&
......@@ -220,12 +216,12 @@ export default {
this.hour == _end.hours &&
value >= endIndex[4]
) {
timeIndex = endIndex;
timeIndex = endIndex
}
this.timeIndex = JSON.parse(JSON.stringify(timeIndex));
break;
this.timeIndex = JSON.parse(JSON.stringify(timeIndex))
break
}
},
},
};
}
}
}
</script>
......
......@@ -22,8 +22,10 @@ let refresh_promise = null
* @param {Object} bill 原始预约记录
* @returns {Object} 扁平化后的预约记录对象
*/
const extract_bill_payload = (bill) => {
if (!bill) return {}
const extract_bill_payload = bill => {
if (!bill) {
return {}
}
const data = { ...bill }
const list = data.list
......@@ -42,8 +44,10 @@ const extract_bill_payload = (bill) => {
* @param {Object} bill 预约记录
* @returns {Array} 人员列表
*/
const extract_person_list = (bill) => {
if (!bill) return []
const extract_person_list = bill => {
if (!bill) {
return []
}
/**
* 从预约记录中提取人员列表
......@@ -69,7 +73,7 @@ const extract_person_list = (bill) => {
* @param {Object} item 原始预约记录项
* @returns {Object} 格式化后的预约记录项
*/
const normalize_bill_item = (item) => {
const normalize_bill_item = item => {
const data = extract_bill_payload(item)
data.datetime = data.datetime || formatDatetime(data)
......@@ -80,7 +84,9 @@ const normalize_bill_item = (item) => {
const person_list = extract_person_list(item)
const first = person_list[0]
const name = first?.name || first?.person_name
if (name) data.person_name = name
if (name) {
data.person_name = name
}
}
return data
......@@ -113,10 +119,10 @@ export const has_offline_booking_cache = () => {
* @param {*} pay_id 支付ID
* @returns {Object|null} 匹配的预约记录项或 null
*/
export const get_offline_booking_by_pay_id = (pay_id) => {
export const get_offline_booking_by_pay_id = pay_id => {
const list = get_offline_booking_cache()
const target_pay_id = String(pay_id || '')
return list.find((item) => String(item?.pay_id || '') === target_pay_id) || null
return list.find(item => String(item?.pay_id || '') === target_pay_id) || null
}
/**
......@@ -124,7 +130,7 @@ export const get_offline_booking_by_pay_id = (pay_id) => {
* @param {Object} bill - 预约记录项
* @returns {Array} 人员列表(包含姓名、身份证号、二维码等信息)
*/
export const get_offline_bill_person_list = (bill) => {
export const get_offline_bill_person_list = bill => {
return extract_person_list(bill)
}
......@@ -133,13 +139,18 @@ export const get_offline_bill_person_list = (bill) => {
* @param {Object} bill - 预约记录项
* @returns {Array} 二维码列表(包含姓名、身份证号、二维码、预约时间等信息)
*/
export const build_offline_qr_list = (bill) => {
export const build_offline_qr_list = bill => {
const list = get_offline_bill_person_list(bill)
const datetime = bill?.datetime || formatDatetime(bill || {})
return list
.filter((item) => item && (item.qr_code || item.qrcode || item.qrCode) && (item.qr_code || item.qrcode || item.qrCode) !== '')
.map((item) => {
.filter(
item =>
item &&
(item.qr_code || item.qrcode || item.qrCode) &&
(item.qr_code || item.qrcode || item.qrCode) !== ''
)
.map(item => {
const begin_time = item.begin_time || bill?.begin_time
const end_time = item.end_time || bill?.end_time
const qr_code = item.qr_code || item.qrcode || item.qrCode
......@@ -151,9 +162,11 @@ export const build_offline_qr_list = (bill) => {
qr_code,
begin_time,
end_time,
datetime: item.datetime || (begin_time && end_time ? formatDatetime({ begin_time, end_time }) : datetime),
datetime:
item.datetime ||
(begin_time && end_time ? formatDatetime({ begin_time, end_time }) : datetime),
pay_id: bill?.pay_id,
sort: 0,
sort: 0
}
})
}
......@@ -175,9 +188,13 @@ export const refresh_offline_booking_cache = async ({ force = false } = {}) => {
// 4. 刷新完成后,将结果存储到本地缓存(key: OFFLINE_BOOKING_CACHE_KEY)
// 5. 返回刷新结果 Promise
if (!hasAuth()) return { code: 0, data: null, msg: '未授权' }
if (!hasAuth()) {
return { code: 0, data: null, msg: '未授权' }
}
if (refresh_promise && !force) return refresh_promise
if (refresh_promise && !force) {
return refresh_promise
}
// 核心逻辑:
// 1. 立刻触发异步逻辑,同时捕获 Promise 状态
......@@ -193,7 +210,9 @@ export const refresh_offline_booking_cache = async ({ force = false } = {}) => {
const { code, data, msg } = await billOfflineAllAPI()
if (code && Array.isArray(data)) {
// 过滤出状态为3(已完成)的记录
const normalized = data.map(normalize_bill_item).filter((item) => item && item.pay_id && item.status == 3)
const normalized = data
.map(normalize_bill_item)
.filter(item => item && item.pay_id && item.status == 3)
if (normalized.length > 0) {
// TAG: 核心逻辑:将过滤后的记录存储到本地缓存
Taro.setStorageSync(OFFLINE_BOOKING_CACHE_KEY, normalized)
......
......@@ -40,7 +40,7 @@ const polling_state = {
network_usable: null, // 网络可用性
has_network_listener: false, // 是否已注册网络监听器
network_listener: null, // 网络监听器
network_listener_promise: null, // 网络监听器Promise
network_listener_promise: null // 网络监听器Promise
}
/**
......@@ -48,7 +48,7 @@ const polling_state = {
* @param {Object} options 选项
* @return {Object} 规范化后的选项
*/
const normalize_options = (options) => {
const normalize_options = options => {
return options || {}
}
......@@ -57,8 +57,10 @@ const normalize_options = (options) => {
* @param {Object} options 选项
* @return {Object} 保存后的选项
*/
const save_last_options = (options) => {
if (options) polling_state.last_options = options
const save_last_options = options => {
if (options) {
polling_state.last_options = options
}
return polling_state.last_options
}
......@@ -73,11 +75,15 @@ const save_last_options = (options) => {
* @param {Object} options 选项
* @param {Boolean} options.force 是否强制刷新
*/
const run_refresh_once = async (options) => {
const run_refresh_once = async options => {
// 前置检查:不满足轮询条件时直接返回(网络不可用或无引用)
if (!should_run_polling()) return
if (!should_run_polling()) {
return
}
// 核心防重复——如果正在刷新,直接返回
if (polling_state.in_flight) return
if (polling_state.in_flight) {
return
}
// 标记为"正在刷新"
polling_state.in_flight = true
try {
......@@ -111,9 +117,15 @@ const update_network_usable = async () => {
* 2. network_usable === true:网络可用
*/
const should_run_polling = () => {
if (polling_state.ref_count <= 0) return false
if (polling_state.network_usable === false) return false
if (polling_state.network_usable === null) return false
if (polling_state.ref_count <= 0) {
return false
}
if (polling_state.network_usable === false) {
return false
}
if (polling_state.network_usable === null) {
return false
}
return true
}
......@@ -147,7 +159,7 @@ const ensure_network_listener = async () => {
await update_network_usable()
// 网络状态变化监听器, 网络状态变化时的处理逻辑,此时只是定义,不会立即执行
polling_state.network_listener = (res) => {
polling_state.network_listener = res => {
const is_connected = res?.isConnected !== false
const type = res?.networkType || 'unknown'
polling_state.network_usable = is_connected && is_usable_network(type)
......@@ -204,9 +216,13 @@ const ensure_network_listener = async () => {
const teardown_network_listener = () => {
// 1. 前置校验:避免无效执行
// 如果没有注册网络监听器,直接返回
if (!polling_state.has_network_listener) return
if (!polling_state.has_network_listener) {
return
}
// 如果有引用计数,说明有其他地方在使用轮询,不能注销监听器
if (polling_state.ref_count > 0) return
if (polling_state.ref_count > 0) {
return
}
// 标记监听器已注销(核心状态更新)
polling_state.has_network_listener = false
// 解绑框架层面的监听器
......@@ -239,9 +255,11 @@ const teardown_network_listener = () => {
* @param {Boolean} options.force 是否强制刷新(透传给 refresh_offline_booking_cache)
* @param {Boolean} options.restart 是否为重启操作(网络恢复时调用)
*/
const start_offline_booking_cache_polling = (options) => {
const start_offline_booking_cache_polling = options => {
options = normalize_options(options)
if (!should_run_polling()) return // 不满足轮询条件直接返回
if (!should_run_polling()) {
return
} // 不满足轮询条件直接返回
const interval_ms = Number(options?.interval_ms || 60000)
const is_restart = options?.restart === true
......@@ -249,7 +267,9 @@ const start_offline_booking_cache_polling = (options) => {
// 改进:区分首次启动和重启的防重逻辑
// 首次启动时,如果已经在轮询则直接返回(防重复启动)
// 重启时,需要清除旧定时器并重新建立(支持网络恢复时重启)
if (polling_state.running && !is_restart) return
if (polling_state.running && !is_restart) {
return
}
// 如果是重启或定时器已存在,先清除旧定时器
if (is_restart && polling_state.timer_id) {
......@@ -296,11 +316,11 @@ const stop_offline_booking_cache_polling = () => {
* 核心动作:将全局的 ref_count 加 1,代表 "又多了一个场景需要使用轮询功能"。
* @param {Object} options 选项
*/
const acquire_polling_ref = (options) => {
const acquire_polling_ref = options => {
save_last_options(options)
polling_state.ref_count += 1
// 改进:检查网络监听器注册结果,只有成功后才启动轮询
ensure_network_listener().then((success) => {
ensure_network_listener().then(success => {
if (success && polling_state.last_options) {
start_offline_booking_cache_polling(polling_state.last_options)
}
......@@ -328,7 +348,7 @@ const release_polling_ref = () => {
* @param {Boolean} options.immediate 是否立即刷新一次
* @param {Boolean} options.force 是否强制刷新(透传给 refresh_offline_booking_cache)
*/
export const enable_offline_booking_cache_polling = (options) => {
export const enable_offline_booking_cache_polling = options => {
save_last_options(options)
/**
* 核心目的:对 app_enabled=true 的场景做兜底,确保轮询在 "已启用但异常停止" 时能被主动恢复,而非被动等待网络变化;
......@@ -336,7 +356,7 @@ export const enable_offline_booking_cache_polling = (options) => {
* 设计思维:体现了 "主动调用需即时生效" 的用户体验考量,以及 "依赖前置检查" 的工程化思维 —— 先保证依赖(监听器)就绪,再执行核心操作(启动轮询)。
*/
if (polling_state.app_enabled) {
ensure_network_listener().then((success) => {
ensure_network_listener().then(success => {
if (success && polling_state.last_options) {
start_offline_booking_cache_polling(polling_state.last_options)
}
......@@ -352,7 +372,9 @@ export const enable_offline_booking_cache_polling = (options) => {
*/
export const disable_offline_booking_cache_polling = () => {
if (!polling_state.app_enabled) return
if (!polling_state.app_enabled) {
return
}
polling_state.app_enabled = false
release_polling_ref()
}
......
......@@ -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
......
......@@ -11,7 +11,14 @@
<view class="form-card">
<view class="form-row">
<view class="label">姓名</view>
<nut-input v-model="name" class="field-input" placeholder="请输入参观者真实姓名" type="text" input-align="right" :border="false" />
<nut-input
v-model="name"
class="field-input"
placeholder="请输入参观者真实姓名"
type="text"
input-align="right"
:border="false"
/>
</view>
<view class="form-row">
<view class="label">证件类型</view>
......@@ -22,7 +29,14 @@
</view>
<view class="form-row">
<view class="label">证件号码</view>
<nut-input v-model="id_number" class="field-input" placeholder="请输入证件号码" :type="id_number_type" input-align="right" :border="false" />
<nut-input
v-model="id_number"
class="field-input"
placeholder="请输入证件号码"
:type="id_number_type"
input-align="right"
:border="false"
/>
</view>
</view>
......@@ -54,125 +68,160 @@ import Taro from '@tarojs/taro'
import { addPersonAPI } from '@/api/index'
import { IconFont } from '@nutui/icons-vue-taro'
const name = ref('');
const id_number = ref('');
const show_id_type_picker = ref(false);
const name = ref('')
const id_number = ref('')
const show_id_type_picker = ref(false)
const id_type_options = [
{ label: '身份证', value: 1 },
{ label: '其他', value: 3 }
];
const id_type = ref(id_type_options[0].value);
const id_type_picker_value = ref([String(id_type.value)]);
]
const id_type = ref(id_type_options[0].value)
const id_type_picker_value = ref([String(id_type.value)])
const id_type_columns = computed(() => {
return id_type_options.map(item => ({
text: item.label,
value: String(item.value)
}));
});
}))
})
const id_type_label = computed(() => {
return id_type_options.find(item => item.value === id_type.value)?.label || id_type_options[0].label;
});
const id_number_type = computed(() => (id_type.value === 1 ? 'idcard' : 'text'));
return (
id_type_options.find(item => item.value === id_type.value)?.label || id_type_options[0].label
)
})
const id_number_type = computed(() => (id_type.value === 1 ? 'idcard' : 'text'))
const open_id_type_picker = () => {
id_type_picker_value.value = [String(id_type.value)];
show_id_type_picker.value = true;
id_type_picker_value.value = [String(id_type.value)]
show_id_type_picker.value = true
}
const on_id_type_confirm = ({ selectedValue }) => {
const value = selectedValue?.[0];
id_type.value = Number(value) || 1;
show_id_type_picker.value = false;
const value = selectedValue?.[0]
id_type.value = Number(value) || 1
show_id_type_picker.value = false
}
// 身份证校验
const checkIDCard = (idcode) => {
const checkIDCard = idcode => {
// 1. 基础格式校验 (18位)
if (!idcode || !/^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/.test(idcode)) {
return false;
if (
!idcode ||
!/^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/.test(
idcode
)
) {
return false
}
// 2. 地区码校验
const cityMap = {
11: "北京", 12: "天津", 13: "河北", 14: "山西", 15: "内蒙古",
21: "辽宁", 22: "吉林", 23: "黑龙江",
31: "上海", 32: "江苏", 33: "浙江", 34: "安徽", 35: "福建", 36: "江西", 37: "山东",
41: "河南", 42: "湖北", 43: "湖南", 44: "广东", 45: "广西", 46: "海南",
50: "重庆", 51: "四川", 52: "贵州", 53: "云南", 54: "西藏",
61: "陕西", 62: "甘肃", 63: "青海", 64: "宁夏", 65: "新疆",
71: "台湾", 81: "香港", 82: "澳门", 91: "国外"
};
11: '北京',
12: '天津',
13: '河北',
14: '山西',
15: '内蒙古',
21: '辽宁',
22: '吉林',
23: '黑龙江',
31: '上海',
32: '江苏',
33: '浙江',
34: '安徽',
35: '福建',
36: '江西',
37: '山东',
41: '河南',
42: '湖北',
43: '湖南',
44: '广东',
45: '广西',
46: '海南',
50: '重庆',
51: '四川',
52: '贵州',
53: '云南',
54: '西藏',
61: '陕西',
62: '甘肃',
63: '青海',
64: '宁夏',
65: '新疆',
71: '台湾',
81: '香港',
82: '澳门',
91: '国外'
}
if (!cityMap[idcode.substr(0, 2)]) {
return false;
return false
}
// 3. 出生日期校验
const birthday = idcode.substr(6, 8);
const year = parseInt(birthday.substr(0, 4));
const month = parseInt(birthday.substr(4, 2));
const day = parseInt(birthday.substr(6, 2));
const date = new Date(year, month - 1, day);
const birthday = idcode.substr(6, 8)
const year = parseInt(birthday.substr(0, 4))
const month = parseInt(birthday.substr(4, 2))
const day = parseInt(birthday.substr(6, 2))
const date = new Date(year, month - 1, day)
if (date.getFullYear() !== year || date.getMonth() + 1 !== month || date.getDate() !== day) {
return false;
return false
}
// 校验日期不能超过当前时间
if (date > new Date()) {
return false;
return false
}
// 4. 校验码计算
// 加权因子
const factor = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2];
const factor = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
// 校验位对应值
const parity = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'];
const parity = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']
let sum = 0;
const codeArr = idcode.split("");
let sum = 0
const codeArr = idcode.split('')
// 计算加权和
for (let i = 0; i < 17; i++) {
sum += codeArr[i] * factor[i];
sum += codeArr[i] * factor[i]
}
// 取模
const mod = sum % 11;
const mod = sum % 11
// 获取校验位
const last = parity[mod];
const last = parity[mod]
// 对比最后一位 (统一转大写比较)
return last === codeArr[17].toUpperCase();
return last === codeArr[17].toUpperCase()
}
const save = async () => {
if (!name.value) {
Taro.showToast({ title: '请输入姓名', icon: 'none' });
return;
Taro.showToast({ title: '请输入姓名', icon: 'none' })
return
}
if (!id_number.value) {
Taro.showToast({ title: '请输入证件号码', icon: 'none' });
return;
Taro.showToast({ title: '请输入证件号码', icon: 'none' })
return
}
if (id_type.value === 1 && !checkIDCard(id_number.value)) {
Taro.showToast({ title: '请输入正确的身份证号', icon: 'none' });
return;
Taro.showToast({ title: '请输入正确的身份证号', icon: 'none' })
return
}
Taro.showLoading({ title: '保存中' });
Taro.showLoading({ title: '保存中' })
const { code, msg } = await addPersonAPI({
name: name.value,
id_type: id_type.value,
id_number: id_number.value
});
Taro.hideLoading();
})
Taro.hideLoading()
if (code) {
Taro.showToast({ title: '添加成功' });
name.value = '';
id_number.value = '';
Taro.navigateBack();
Taro.showToast({ title: '添加成功' })
name.value = ''
id_number.value = ''
Taro.navigateBack()
} else {
Taro.showToast({ title: msg || '添加失败', icon: 'none' });
Taro.showToast({ title: msg || '添加失败', icon: 'none' })
}
}
</script>
......@@ -180,7 +229,7 @@ const save = async () => {
<style lang="less">
.add-visitor-page {
min-height: 100vh;
background-color: #F6F6F6;
background-color: #f6f6f6;
padding-top: 2rpx;
.content {
......@@ -189,7 +238,7 @@ const save = async () => {
}
.form-card {
background-color: #FFF;
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
}
......@@ -201,7 +250,7 @@ const save = async () => {
height: 112rpx;
&:not(:last-child) {
border-bottom: 2rpx solid #F2F2F2;
border-bottom: 2rpx solid #f2f2f2;
}
.label {
......@@ -230,7 +279,7 @@ const save = async () => {
.picker-arrow {
margin-left: 10rpx;
color: #BBB;
color: #bbb;
font-size: 28rpx;
}
}
......@@ -239,7 +288,7 @@ const save = async () => {
margin-top: 28rpx;
display: flex;
align-items: center;
color: #C7A46D;
color: #c7a46d;
font-size: 24rpx;
.tip-text {
......@@ -253,18 +302,18 @@ const save = async () => {
right: 0;
bottom: 0;
padding: 24rpx 32rpx calc(24rpx + env(safe-area-inset-bottom));
background-color: #F6F6F6;
background-color: #f6f6f6;
}
.save-btn {
width: 686rpx;
height: 96rpx;
background-color: #A67939;
background-color: #a67939;
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
color: #FFF;
color: #fff;
font-size: 34rpx;
font-weight: 600;
}
......
export default {
navigationBarTitleText: '授权页',
usingComponents: {
},
usingComponents: {}
}
......
......@@ -22,9 +22,13 @@ let has_shown_fail_modal = false
let has_failed = false
useDidShow(() => {
if (has_failed) return
if (has_failed) {
return
}
const now = Date.now()
if (now - last_try_at < 1200) return
if (now - last_try_at < 1200) {
return
}
last_try_at = now
/**
......@@ -34,15 +38,17 @@ useDidShow(() => {
*/
silentAuth()
.then(() => returnToOriginalPage())
.catch(async (error) => {
.catch(async error => {
has_failed = true
if (has_shown_fail_modal) return
if (has_shown_fail_modal) {
return
}
has_shown_fail_modal = true
await Taro.showModal({
title: '提示',
content: error?.message || '授权失败,请稍后再尝试',
showCancel: false,
confirmText: '我知道了',
confirmText: '我知道了'
})
})
})
......
......@@ -18,24 +18,44 @@
</view>
<view class="weeks-wrapper">
<view v-for="(week, index) in weeks" :key="index" class="weeks">
<view v-for="(date, dateIndex) in week" :key="dateIndex"
<view
v-for="(date, dateIndex) in week"
:key="dateIndex"
@tap="chooseDay(date)"
:class="[ 'item',
:class="[
'item',
date && checked_day === findDatesInfo(date).date ? 'checked' : '',
date && (findDatesInfo(date).reserve_full === ReserveStatus.FULL || findDatesInfo(date).reserve_full === ReserveStatus.OVERDUE) ? 'disabled' : ''
date &&
(findDatesInfo(date).reserve_full === ReserveStatus.FULL ||
findDatesInfo(date).reserve_full === ReserveStatus.OVERDUE)
? 'disabled'
: ''
]"
>
<view v-if="findDatesInfo(date).date">
<view class="day-lunar">{{ findDatesInfo(date).lunar_date.IDayCn }}</view>
<view class="day-text">{{ findDatesInfo(date).text }}</view>
<view v-if="findDatesInfo(date).reserve_full === ReserveStatus.INFINITY || findDatesInfo(date).reserve_full === ReserveStatus.OVERDUE" class="day-price"></view>
<view v-else-if="findDatesInfo(date).reserve_full === ReserveStatus.FULL" class="day-no-booking">约满</view>
<view
v-if="
findDatesInfo(date).reserve_full === ReserveStatus.INFINITY ||
findDatesInfo(date).reserve_full === ReserveStatus.OVERDUE
"
class="day-price"
></view>
<view
v-else-if="findDatesInfo(date).reserve_full === ReserveStatus.FULL"
class="day-no-booking"
>约满</view
>
</view>
</view>
</view>
</view>
</view>
<view v-if="checked_day && checked_day_reserve_full === ReserveStatus.AVAILABLE" class="choose-time">
<view
v-if="checked_day && checked_day_reserve_full === ReserveStatus.AVAILABLE"
class="choose-time"
>
<view class="title">
<view class="text">选择参访时间段</view>
</view>
......@@ -64,14 +84,14 @@
<view class="title">
<view class="text">选择参访时间段</view>
</view>
<view style="padding: 48rpx 24rpx; color: #A67939; text-align: center;">
<view style="padding: 48rpx 24rpx; color: #a67939; text-align: center">
{{ infinity_tips_text }}
</view>
</view>
</view>
<view style="height: 160rpx;"></view>
<view style="height: 160rpx"></view>
<view v-if="checked_day && checked_day_reserve_full === ReserveStatus.AVAILABLE" class="next">
<view @tap="nextBtn" class="button" style="background-color: #A67939;">下一步</view>
<view @tap="nextBtn" class="button" style="background-color: #a67939">下一步</view>
</view>
<!-- NutUI Popup + DatePicker -->
......@@ -92,31 +112,33 @@
<script setup>
import { ref, computed } from 'vue'
import Taro, { useDidShow } from '@tarojs/taro'
import dayjs from 'dayjs';
import dayjs from 'dayjs'
import { useGo } from '@/hooks/useGo'
import icon_select1 from '@/assets/images/单选01@2x.png'
import icon_select2 from '@/assets/images/单选02@2x.png'
import { canReserveDateListAPI, canReserveTimeListAPI } from '@/api/index'
import calendar from 'xst-solar2lunar'
const go = useGo();
const go = useGo()
const dates_list = ref([]); // 当月日期列表信息
const dates = ref([]); // 当月日期集合
const dates_list = ref([]) // 当月日期列表信息
const dates = ref([]) // 当月日期集合
useDidShow(async () => {
const raw_date = new Date();
const { code, data } = await canReserveDateListAPI({ month: `${raw_date.getFullYear()}-${(raw_date.getMonth() + 1).toString().padStart(2, '0')}` });
const raw_date = new Date()
const { code, data } = await canReserveDateListAPI({
month: `${raw_date.getFullYear()}-${(raw_date.getMonth() + 1).toString().padStart(2, '0')}`
})
if (code) {
// 日期列表
dates_list.value = data || [];
dates_list.value = data || []
// 今日之前都不可约
dates_list.value.forEach((date) => {
dates_list.value.forEach(date => {
if (dayjs(date.month_date).isBefore(dayjs())) {
date.reserve_full = ReserveStatus.OVERDUE;
date.reserve_full = ReserveStatus.OVERDUE
}
});
dates.value = dates_list.value.map(item => item.month_date);
})
dates.value = dates_list.value.map(item => item.month_date)
}
})
......@@ -125,22 +147,24 @@ useDidShow(async () => {
* @param {string} date
* @return {object} {text: 日期, date: 日期, reserve_full: 是否可约 1=可约,0=约满,-1=无需预约 overdue=过期日期 }
*/
const findDatesInfo = (date) => {
if (!date) return { text: '', date: '', reserve_full: '', lunar_date: {} };
const result = dates_list.value.find((item) => item.month_date === date);
const currentDate = new Date(date);
const findDatesInfo = date => {
if (!date) {
return { text: '', date: '', reserve_full: '', lunar_date: {} }
}
const result = dates_list.value.find(item => item.month_date === date)
const currentDate = new Date(date)
// calendar.solar2lunar 需要年,月,日 (数字)
// dayjs(date).format('YYYY-MM-DD') -> 2024-01-01
const d = dayjs(date);
const lunarDate = calendar.solar2lunar(d.year(), d.month() + 1, d.date());
const d = dayjs(date)
const lunarDate = calendar.solar2lunar(d.year(), d.month() + 1, d.date())
return {
text: currentDate.getDate().toString().padStart(2, '0'),
date: result?.month_date,
reserve_full: result?.reserve_full,
tips: result?.tips || '',
lunar_date: lunarDate
};
};
}
}
/**
* @description: 预约状态
......@@ -150,59 +174,61 @@ const ReserveStatus = {
INFINITY: -1, // 无需预约
FULL: 0, // 约满
AVAILABLE: 1, // 可约
OVERDUE: 'overdue', // 过期日期
OVERDUE: 'overdue' // 过期日期
}
const daysOfWeek = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"];
const daysOfWeek = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
/**
* @description: 每周日期列表
* @return {array} [array]
*/
const weeks = computed(() => {
if (dates.value.length === 0) return [];
const result = [];
let currentWeek = [];
let currentDate = new Date(dates.value[0]);
if (dates.value.length === 0) {
return []
}
const result = []
let currentWeek = []
let currentDate = new Date(dates.value[0])
// 确定第一个日期是星期几
const firstDayOfWeek = currentDate.getDay() === 0 ? 7 : currentDate.getDay();
const firstDayOfWeek = currentDate.getDay() === 0 ? 7 : currentDate.getDay()
// 添加空白的日期,直到第一个日期的星期一
for (let i = 1; i < firstDayOfWeek; i++) {
currentWeek.push('');
currentWeek.push('')
}
// 添加日期
for (const date of dates.value) {
currentDate = new Date(date);
const dayOfWeek = currentDate.getDay() === 0 ? 7 : currentDate.getDay();
currentDate = new Date(date)
const dayOfWeek = currentDate.getDay() === 0 ? 7 : currentDate.getDay()
// 如果当前星期一,开始新的一行
if (dayOfWeek === 1 && currentWeek.length > 0) {
result.push(currentWeek);
currentWeek = [];
result.push(currentWeek)
currentWeek = []
}
currentWeek.push(date); // 仅将日期部分作为字符串添加到当前星期数组
currentWeek.push(date) // 仅将日期部分作为字符串添加到当前星期数组
}
// 添加最后一行
if (currentWeek.length > 0) {
while (currentWeek.length < 7) {
currentWeek.push('');
currentWeek.push('')
}
result.push(currentWeek);
result.push(currentWeek)
}
return result;
});
return result
})
const checked_day = ref('');
const checked_day_price = ref(0);
const checked_day_reserve_full = ref(null);
const checked_time = ref(-1);
const timePeriod = ref([]); // 当前时间段信息
const checked_day = ref('')
const checked_day_price = ref(0)
const checked_day_reserve_full = ref(null)
const checked_time = ref(-1)
const timePeriod = ref([]) // 当前时间段信息
/**
* @description: 无预约提示
......@@ -210,21 +236,27 @@ const timePeriod = ref([]); // 当前时间段信息
*/
const infinity_tips_text = computed(() => {
if (!checked_day.value || checked_day_reserve_full.value !== ReserveStatus.INFINITY) return '';
const info = findDatesInfo(checked_day.value);
if (!checked_day.value || checked_day_reserve_full.value !== ReserveStatus.INFINITY) {
return ''
}
const info = findDatesInfo(checked_day.value)
if (dayjs(checked_day.value).isAfter(dayjs(), 'day')) {
const tips = (info.tips || '').trim();
if (tips) return tips;
const tips = (info.tips || '').trim()
if (tips) {
return tips
}
return '暂未开启预约';
});
}
return '暂未开启预约'
})
const chooseTime = (item, index) => { // 选择时间段回调
if (item.rest_qty || item.rest_qty === QtyStatus.INFINITY) { // 余量等于-1为不限制数量
checked_time.value = index;
checked_day_price.value = item.price; // 当前价格
const chooseTime = (item, index) => {
// 选择时间段回调
if (item.rest_qty || item.rest_qty === QtyStatus.INFINITY) {
// 余量等于-1为不限制数量
checked_time.value = index
checked_day_price.value = item.price // 当前价格
}
};
}
/**
* @description: 数量状态
......@@ -232,7 +264,7 @@ const chooseTime = (item, index) => { // 选择时间段回调
*/
const QtyStatus = {
FULL: 0, // 无余量
INFINITY: -1, // 无限制
INFINITY: -1 // 无限制
}
/**
......@@ -240,77 +272,90 @@ const QtyStatus = {
* @param {string} date
* @return {void}
*/
const chooseDay = async (date) => { // 点击日期回调
if (!date) return;
const info = findDatesInfo(date);
const chooseDay = async date => {
// 点击日期回调
if (!date) {
return
}
const info = findDatesInfo(date)
//
if (info.reserve_full === ReserveStatus.AVAILABLE || info.reserve_full === ReserveStatus.INFINITY) { // 状态 1可约 || -1不限制
checked_day.value = date; // 当前日期
checked_day_reserve_full.value = info.reserve_full; // 当前状态
if (
info.reserve_full === ReserveStatus.AVAILABLE ||
info.reserve_full === ReserveStatus.INFINITY
) {
// 状态 1可约 || -1不限制
checked_day.value = date // 当前日期
checked_day_reserve_full.value = info.reserve_full // 当前状态
// 如果可约,查询时间段信息
if (info.reserve_full === ReserveStatus.AVAILABLE) {
// 选择日期后,查询时间段信息
const { code, data } = await canReserveTimeListAPI({ month_date: checked_day.value });
const { code, data } = await canReserveTimeListAPI({ month_date: checked_day.value })
if (code) {
// rest_qty >0表示有余量,可约;=0表示没有余量,不可约;<0表示不限,可约;
// period_type 时段类型 REGULAR=日常预约,SPRING_FESTIVAL=春节预约
timePeriod.value = data;
checked_time.value = -1; // 重置已选择的时间段
timePeriod.value = data
checked_time.value = -1 // 重置已选择的时间段
}
}
}
};
}
const showPicker = ref(false);
const showPicker = ref(false)
const chooseDate = () => {
showPicker.value = true;
showPicker.value = true
}
const raw_date = new Date();
const currentDate = ref(new Date()); // NutUI DatePicker v-model 绑定的是 Date 对象
const minDate = new Date();
const maxDate = new Date(2050, 11, 1);
const currentDateText = ref((raw_date.getMonth() + 1).toString().padStart(2, '0'));
const raw_date = new Date()
const currentDate = ref(new Date()) // NutUI DatePicker v-model 绑定的是 Date 对象
const minDate = new Date()
const maxDate = new Date(2050, 11, 1)
const currentDateText = ref((raw_date.getMonth() + 1).toString().padStart(2, '0'))
const onConfirm = async ({ selectedValue, selectedOptions }) => { // 选择日期回调
const onConfirm = async ({ selectedValue, selectedOptions }) => {
// 选择日期回调
// selectedValue 可能是数组或对象,NutUI 文档
// selectedOptions 是选项对象数组
// year-month 模式下 selectedValue 可能是 [year, month]
// 实际上 NutUI DatePicker confirm 事件参数:{ selectedValue, selectedOptions }
showPicker.value = false;
showPicker.value = false
// selectedValue: ['2024', '02']
const [year, month] = selectedValue;
currentDateText.value = month;
const [year, month] = selectedValue
currentDateText.value = month
// 清空选择
checked_day.value = '';
checked_time.value = -1;
checked_day_reserve_full.value = null;
checked_day.value = ''
checked_time.value = -1
checked_day_reserve_full.value = null
// 选择日期后,查询月份信息
const { code, data } = await canReserveDateListAPI({ month: `${year}-${month}` });
const { code, data } = await canReserveDateListAPI({ month: `${year}-${month}` })
if (code) {
// 日期列表
dates_list.value = data || [];
dates_list.value = data || []
// 今日之前都不可约
dates_list.value.forEach((date) => {
dates_list.value.forEach(date => {
if (dayjs(date.month_date).isBefore(dayjs())) {
date.reserve_full = ReserveStatus.OVERDUE;
date.reserve_full = ReserveStatus.OVERDUE
}
});
dates.value = dates_list.value.map(item => item.month_date);
})
dates.value = dates_list.value.map(item => item.month_date)
}
}
const onCancel = () => {
showPicker.value = false;
showPicker.value = false
}
const nextBtn = () => {
if (!checked_day.value || checked_time.value === -1) {
Taro.showToast({ title: '请选择日期和时间段', icon: 'none' });
Taro.showToast({ title: '请选择日期和时间段', icon: 'none' })
} else {
go('/submit', { date: checked_day.value, time: `${timePeriod.value[checked_time.value]['begin_time'].slice(0, -3)}-${timePeriod.value[checked_time.value]['end_time'].slice(0, -3)}`, price: checked_day_price.value, period_type: timePeriod.value[checked_time.value].period_type });
go('/submit', {
date: checked_day.value,
time: `${timePeriod.value[checked_time.value]['begin_time'].slice(0, -3)}-${timePeriod.value[checked_time.value]['end_time'].slice(0, -3)}`,
price: checked_day_price.value,
period_type: timePeriod.value[checked_time.value].period_type
})
}
}
</script>
......@@ -319,13 +364,12 @@ const nextBtn = () => {
.booking-page {
position: relative;
min-height: 100vh;
background-color: #F6F6F6;
background-color: #f6f6f6;
.calendar {
padding: 32rpx 16rpx;
.choose-date {
border-radius: 10rpx;
background-color: #FFFFFF;
background-color: #ffffff;
.title {
padding: 16rpx 24rpx;
......@@ -335,20 +379,20 @@ const nextBtn = () => {
.text {
&::before {
content: '';
border: 4rpx solid #A67939;
border: 4rpx solid #a67939;
margin-right: 16rpx;
}
}
.day {
background-color: #FFFBF3;
background-color: #fffbf3;
border-radius: 14rpx;
border: 2rpx solid #A67939;
border: 2rpx solid #a67939;
padding: 6rpx 16rpx;
color: #A67939;
color: #a67939;
}
}
.days-of-week {
background-color: #F6F6F6;
background-color: #f6f6f6;
display: flex;
padding: 24rpx 1%;
font-size: 27rpx;
......@@ -369,39 +413,39 @@ const nextBtn = () => {
text-align: center;
margin: 0 10rpx;
padding: 16rpx 0;
border: 2rpx solid #FFF;
border: 2rpx solid #fff;
.day-lunar {
color: #1E1E1E;
color: #1e1e1e;
font-size: 27rpx;
margin-bottom: 10rpx;
}
.day-text {
color: #1E1E1E;
color: #1e1e1e;
font-weight: bold;
font-size: 34rpx;
}
.day-price {
color: #A67939;
color: #a67939;
font-size: 27rpx;
}
&.checked {
border: 2rpx solid #A67939;
border: 2rpx solid #a67939;
border-radius: 10rpx;
background-color: #FFFBF3;
background-color: #fffbf3;
}
&.disabled {
.day-lunar {
color: #C7C7C7;
color: #c7c7c7;
margin-bottom: 10rpx;
}
.day-text {
color: #C7C7C7;
color: #c7c7c7;
}
.day-price {
color: #C7C7C7;
color: #c7c7c7;
}
.day-no-booking {
color: #C7C7C7;
color: #c7c7c7;
font-size: 24rpx;
}
}
......@@ -418,7 +462,7 @@ const nextBtn = () => {
.text {
&::before {
content: '';
border: 4rpx solid #A67939;
border: 4rpx solid #a67939;
margin-right: 16rpx;
}
}
......@@ -428,37 +472,37 @@ const nextBtn = () => {
display: flex;
align-items: center;
justify-content: space-between;
background-color: #FFF;
background-color: #fff;
border-radius: 10rpx;
padding: 27rpx;
margin: 32rpx 0;
.left {
display: flex;
align-items: center;
color: #1E1E1E;
color: #1e1e1e;
.icon {
width: 38rpx;
height: 38rpx;
margin-right: 16rpx;
}
.price {
color:#A67939;
color: #a67939;
margin-left: 16rpx;
}
}
.right {
color: #A67939;
color: #a67939;
}
&.disabled {
background-color: #E0E0E0;
background-color: #e0e0e0;
.left {
color: #C7C7C7;
color: #c7c7c7;
.price {
color:#C7C7C7;
color: #c7c7c7;
}
}
.right {
color: #C7C7C7;
color: #c7c7c7;
}
}
}
......@@ -472,12 +516,12 @@ const nextBtn = () => {
width: 750rpx;
display: flex;
left: 0;
background-color: #FFF;
background-color: #fff;
align-items: center;
justify-content: center;
box-shadow: 0 -10rpx 8rpx 0 rgba(0,0,0,0.12);
box-shadow: 0 -10rpx 8rpx 0 rgba(0, 0, 0, 0.12);
.button {
color: #FFF;
color: #fff;
padding: 27rpx 0;
border-radius: 16rpx;
font-size: 35rpx;
......
......@@ -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'
......@@ -44,9 +46,11 @@ 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
success: async res => {
const isConnected = is_usable_network(res.networkType)
if (isConnected) {
return
}
if (has_offline_booking_cache()) {
Taro.redirectTo({ url: '/pages/offlineBookingList/index' })
......@@ -72,19 +76,21 @@ useDidShow(() => {
}
Taro.redirectTo({ url: '/pages/index/index' })
}
});
})
})
useDidHide(() => {
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)
/**
......@@ -88,33 +91,33 @@ const cancelBooking = async () => {
title: '温馨提示',
content: '是否取消预约?',
confirmColor: '#A67939'
});
})
if (confirm) {
Taro.showLoading({ title: '取消中...' });
const { code, data } = await icbcRefundAPI({ pay_id: pay_id.value });
Taro.hideLoading();
Taro.showLoading({ title: '取消中...' })
const { code, data } = await icbcRefundAPI({ pay_id: pay_id.value })
Taro.hideLoading()
if (code) {
Taro.showToast({ title: '取消成功' });
Taro.showToast({ title: '取消成功' })
try {
await refresh_offline_booking_cache({ force: true })
} catch (e) {}
Taro.navigateBack();
Taro.navigateBack()
} else {
Taro.showToast({ title: '取消失败', icon: 'none' });
Taro.showToast({ title: '取消失败', icon: 'none' })
}
}
}
useDidShow(async () => {
qr_code_ref.value?.start_polling?.()
pay_id.value = router.params.pay_id;
pay_id.value = router.params.pay_id
if (pay_id.value) {
const { code, data } = await billInfoAPI({ pay_id: 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;
data.datetime = data && formatDatetime(data)
data.order_time = data.created_time ? data.created_time.slice(0, -3) : ''
billInfo.value = data
}
}
})
......@@ -127,15 +130,15 @@ useDidHide(() => {
<style lang="less">
.booking-detail-page {
min-height: 100vh;
background-color: #F6F6F6;
background-color: #f6f6f6;
padding: 32rpx;
.detail-wrapper {
background-color: #FFF;
background-color: #fff;
border-radius: 16rpx;
padding: 32rpx;
margin-top: 32rpx;
box-shadow: 0 0 29rpx 0 rgba(106,106,106,0.1);
box-shadow: 0 0 29rpx 0 rgba(106, 106, 106, 0.1);
.detail-item {
display: flex;
......@@ -164,14 +167,14 @@ useDidHide(() => {
bottom: 0;
left: 0;
width: 750rpx;
background-color: #FFF;
background-color: #fff;
padding: 32rpx;
box-sizing: border-box;
.cancel-btn {
background-color: #FFF;
color: #A67939;
border: 2rpx solid #A67939;
background-color: #fff;
color: #a67939;
border: 2rpx solid #a67939;
text-align: center;
padding: 26rpx 0;
border-radius: 16rpx;
......
......@@ -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;
loading.value = true
if (isRefresh) {
page.value = 1;
finished.value = false;
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 || [];
const list = data || []
list.forEach(item => {
item.booking_time = item && formatDatetime(item);
item.order_time = item.created_time ? item.created_time.slice(0, -3) : '';
});
item.booking_time = item && formatDatetime(item)
item.order_time = item.created_time ? item.created_time.slice(0, -3) : ''
})
if (isRefresh) {
bookingList.value = list;
bookingList.value = list
} else {
bookingList.value = bookingList.value.concat(list);
bookingList.value = bookingList.value.concat(list)
}
if (list.length < limit.value) {
finished.value = true;
finished.value = true
} else {
page.value++;
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,7 +104,7 @@ useReachBottom(() => {
padding-top: 160rpx;
.no-qrcode-title {
color: #A67939;
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,14 +49,16 @@ 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
......
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>
......
......@@ -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 = [{
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测试'
}
......
......@@ -6,18 +6,32 @@
</view>
<view class="action-area">
<nut-button type="primary" size="large" @click="startScan" :disabled="isScanning" :loading="isScanning">
<nut-button
type="primary"
size="large"
@click="startScan"
:disabled="isScanning"
:loading="isScanning"
>
{{ isScanning ? '正在扫描...' : '开始 NFC 扫描' }}
</nut-button>
<nut-button v-if="isScanning" type="danger" size="large" @click="stopScan" style="margin-top: 40rpx;">
<nut-button
v-if="isScanning"
type="danger"
size="large"
@click="stopScan"
style="margin-top: 40rpx"
>
停止扫描
</nut-button>
</view>
<view class="status-area">
<text class="status-label">当前状态:</text>
<text class="status-text" :class="{ 'scanning': isScanning, 'error': !!error }">{{ status }}</text>
<text class="status-text" :class="{ scanning: isScanning, error: !!error }">{{
status
}}</text>
</view>
<view class="result-area" v-if="result">
......@@ -41,18 +55,18 @@
</template>
<script setup>
import { ref, onUnmounted } from 'vue';
import Taro from '@tarojs/taro';
import { ref, onUnmounted } from 'vue'
import Taro from '@tarojs/taro'
const isScanning = ref(false);
const status = ref('等待操作');
const result = ref('');
const error = ref('');
const debugInfo = ref('');
const isScanning = ref(false)
const status = ref('等待操作')
const result = ref('')
const error = ref('')
const debugInfo = ref('')
let nfcAdapter = null;
let nfcAdapter = null
const safe_stringify = (data) => {
const safe_stringify = data => {
try {
return JSON.stringify(
data,
......@@ -60,63 +74,63 @@ const safe_stringify = (data) => {
if (value instanceof ArrayBuffer) {
return {
__type: 'ArrayBuffer',
hex: bufferToHex(value),
};
hex: bufferToHex(value)
}
}
if (value && value.buffer instanceof ArrayBuffer) {
return {
__type: 'TypedArray',
hex: bufferToHex(value.buffer),
};
hex: bufferToHex(value.buffer)
}
}
return value;
return value
},
2
);
)
} catch (e) {
return String(data);
return String(data)
}
};
}
const startScan = async () => {
error.value = '';
result.value = '';
debugInfo.value = '';
error.value = ''
result.value = ''
debugInfo.value = ''
let systemInfo = null;
let systemInfo = null
try {
systemInfo = Taro.getSystemInfoSync();
systemInfo = Taro.getSystemInfoSync()
} catch (e) {}
const envType = Taro.getEnv && Taro.getEnv();
const platform = systemInfo && systemInfo.platform ? systemInfo.platform : '';
const system = systemInfo && systemInfo.system ? systemInfo.system : '';
const model = systemInfo && systemInfo.model ? systemInfo.model : '';
const SDKVersion = systemInfo && systemInfo.SDKVersion ? systemInfo.SDKVersion : '';
const version = systemInfo && systemInfo.version ? systemInfo.version : '';
debugInfo.value = `env: ${envType}\nplatform: ${platform}\nsystem: ${system}\nmodel: ${model}\nSDKVersion: ${SDKVersion}\nversion: ${version}`;
const envType = Taro.getEnv && Taro.getEnv()
const platform = systemInfo && systemInfo.platform ? systemInfo.platform : ''
const system = systemInfo && systemInfo.system ? systemInfo.system : ''
const model = systemInfo && systemInfo.model ? systemInfo.model : ''
const SDKVersion = systemInfo && systemInfo.SDKVersion ? systemInfo.SDKVersion : ''
const version = systemInfo && systemInfo.version ? systemInfo.version : ''
debugInfo.value = `env: ${envType}\nplatform: ${platform}\nsystem: ${system}\nmodel: ${model}\nSDKVersion: ${SDKVersion}\nversion: ${version}`
if (platform === 'ios') {
error.value = 'iOS 端微信小程序通常不支持 NFC(该能力主要在 Android 可用)';
status.value = '启动失败';
return;
error.value = 'iOS 端微信小程序通常不支持 NFC(该能力主要在 Android 可用)'
status.value = '启动失败'
return
}
if (!Taro.getNFCAdapter) {
error.value = '当前环境不支持 NFC 接口';
status.value = '启动失败';
return;
error.value = '当前环境不支持 NFC 接口'
status.value = '启动失败'
return
}
if (envType && Taro.ENV_TYPE && envType !== Taro.ENV_TYPE.WEAPP) {
error.value = '当前不是微信小程序环境,无法使用 NFC';
status.value = '启动失败';
return;
error.value = '当前不是微信小程序环境,无法使用 NFC'
status.value = '启动失败'
return
}
try {
nfcAdapter = Taro.getNFCAdapter();
nfcAdapter = Taro.getNFCAdapter()
status.value = '正在初始化 NFC...';
status.value = '正在初始化 NFC...'
await nfcAdapter.startDiscovery({
techs: [
......@@ -127,102 +141,101 @@ const startScan = async () => {
'ISO-DEP',
'MIFARE-CLASSIC',
'MIFARE-ULTRALIGHT',
'NDEF',
'NDEF'
],
success: () => {
status.value = '请将手机背面靠近 NFC 标签';
isScanning.value = true;
status.value = '请将手机背面靠近 NFC 标签'
isScanning.value = true
},
fail: (err) => {
console.error('NFC start error:', err);
fail: err => {
console.error('NFC start error:', err)
// 错误码参考微信文档
debugInfo.value = `${debugInfo.value}\n\nstartDiscovery fail:\n${err && (err.errMsg || JSON.stringify(err))}`;
debugInfo.value = `${debugInfo.value}\n\nstartDiscovery fail:\n${err && (err.errMsg || JSON.stringify(err))}`
if (err.errCode === 13000) {
error.value = '设备不支持 NFC';
error.value = '设备不支持 NFC'
} else if (err.errCode === 13001) {
error.value = '系统 NFC 开关未开启';
error.value = '系统 NFC 开关未开启'
} else if (err.errMsg && err.errMsg.includes('platform is not supported')) {
error.value = '开发者工具不支持 NFC,请使用真机调试';
error.value = '开发者工具不支持 NFC,请使用真机调试'
} else {
error.value = 'NFC 启动失败: ' + (err.errMsg || JSON.stringify(err));
error.value = `NFC 启动失败: ${err.errMsg || JSON.stringify(err)}`
}
status.value = '启动失败';
status.value = '启动失败'
}
});
})
nfcAdapter.onDiscovered((res) => {
console.log('NFC Discovered:', res);
status.value = '发现标签,正在读取...';
Taro.vibrateShort();
nfcAdapter.onDiscovered(res => {
console.log('NFC Discovered:', res)
status.value = '发现标签,正在读取...'
Taro.vibrateShort()
handleNfcMessage(res);
handleNfcMessage(res)
// 扫描成功后,通常可以选择是否停止。这里我们保持扫描状态,或者提供停止按钮。
// 用户需求是“扫描成功后显示信息内容”,为了防止重复读取造成刷屏,可以考虑读取成功后自动停止。
// 但如果是测试页,可能想连续测多个。我选择不自动停止,但更新结果。
});
})
} catch (e) {
console.error('NFC Adapter error:', e);
debugInfo.value = `${debugInfo.value}\n\ngetNFCAdapter error:\n${e && (e.errMsg || e.message || JSON.stringify(e))}`;
error.value = 'NFC 初始化失败(可能是设备/系统不支持,或不在可用环境)';
status.value = '错误';
console.error('NFC Adapter error:', e)
debugInfo.value = `${debugInfo.value}\n\ngetNFCAdapter error:\n${e && (e.errMsg || e.message || JSON.stringify(e))}`
error.value = 'NFC 初始化失败(可能是设备/系统不支持,或不在可用环境)'
status.value = '错误'
}
};
}
const stopScan = () => {
if (nfcAdapter) {
nfcAdapter.stopDiscovery({
success: () => {
status.value = '已停止扫描';
isScanning.value = false;
status.value = '已停止扫描'
isScanning.value = false
},
fail: (err) => {
console.error('Stop NFC fail', err);
fail: err => {
console.error('Stop NFC fail', err)
},
complete: () => {
// 确保状态更新
isScanning.value = false;
isScanning.value = false
if (nfcAdapter && nfcAdapter.offDiscovered) {
nfcAdapter.offDiscovered();
nfcAdapter.offDiscovered()
}
}
});
})
} else {
isScanning.value = false;
isScanning.value = false
}
};
}
// 辅助函数:将 ArrayBuffer 转为 16 进制字符串
const bufferToHex = (buffer) => {
const bufferToHex = buffer => {
return Array.from(new Uint8Array(buffer))
.map(b => b.toString(16).padStart(2, '0'))
.join(':')
.toUpperCase();
};
.toUpperCase()
}
const handleNfcMessage = (res) => {
let content = '';
const handleNfcMessage = res => {
let content = ''
// 1. 获取 UID
if (res.id) {
content += `UID: ${bufferToHex(res.id)}\n`;
content += `UID: ${bufferToHex(res.id)}\n`
}
// 2. 获取 Tech Types
if (res.techs && res.techs.length) {
content += `Techs: ${res.techs.join(', ')}\n`;
content += `Techs: ${res.techs.join(', ')}\n`
}
// 3. 解析 NDEF 消息
if (res.messages && res.messages.length > 0) {
content += '\n--- NDEF Records ---\n';
content += '\n--- NDEF Records ---\n'
try {
// res.messages 是一个数组,通常取第一个 NDEF Message
const records = res.messages[0].records || [];
const records = res.messages[0].records || []
records.forEach((record, index) => {
content += `[Record ${index + 1}]\n`;
content += `[Record ${index + 1}]\n`
// Type Name Format (TNF) - bits 0-2 of first byte (not directly exposed in simple objects usually,
// but we might need to parse payload if it's raw.
......@@ -230,49 +243,48 @@ const handleNfcMessage = (res) => {
if (record.type) {
// record.type is ArrayBuffer
const typeStr = new TextDecoder().decode(record.type);
content += `Type: ${typeStr}\n`;
const typeStr = new TextDecoder().decode(record.type)
content += `Type: ${typeStr}\n`
// Text Record Parsing (Type = 'T')
if (typeStr === 'T') {
const payload = new Uint8Array(record.payload);
const payload = new Uint8Array(record.payload)
if (payload.length > 0) {
const statusByte = payload[0];
const langCodeLen = statusByte & 0x3F;
const statusByte = payload[0]
const langCodeLen = statusByte & 0x3f
// const isUtf16 = (statusByte & 0x80) !== 0; // bit 7
// 提取文本内容
const textBytes = payload.slice(1 + langCodeLen);
const text = new TextDecoder().decode(textBytes);
content += `Content: ${text}\n`;
const textBytes = payload.slice(1 + langCodeLen)
const text = new TextDecoder().decode(textBytes)
content += `Content: ${text}\n`
}
} else {
// 其他类型,尝试直接转码显示,或者显示 HEX
const text = new TextDecoder().decode(record.payload);
const text = new TextDecoder().decode(record.payload)
// 简单的过滤,如果看起来像乱码则显示 Hex
if (/[\x00-\x08\x0E-\x1F]/.test(text)) {
content += `Payload (Hex): ${bufferToHex(record.payload)}\n`;
content += `Payload (Hex): ${bufferToHex(record.payload)}\n`
} else {
content += `Payload: ${text}\n`;
content += `Payload: ${text}\n`
}
}
}
});
})
} catch (parseErr) {
console.error(parseErr);
content += '解析 NDEF 数据出错\n';
console.error(parseErr)
content += '解析 NDEF 数据出错\n'
}
} else {
content += '\n(无 NDEF 消息)\n';
content += '\n(无 NDEF 消息)\n'
}
result.value = `--- 原始数据 ---\n${safe_stringify(res)}\n\n--- 解析结果 ---\n${content}`;
};
result.value = `--- 原始数据 ---\n${safe_stringify(res)}\n\n--- 解析结果 ---\n${content}`
}
onUnmounted(() => {
stopScan();
});
stopScan()
})
</script>
<style lang="less">
......
......@@ -8,12 +8,14 @@
<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">
......@@ -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;
......
......@@ -14,14 +14,13 @@ 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')
})
});
})
</script>
<style lang="less">
......
export default {
navigationBarTitleText: '离线预约详情'
}
......
......@@ -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)
......@@ -105,15 +108,15 @@ useDidShow(() => {
<style lang="less">
.offline-booking-detail-page {
min-height: 100vh;
background-color: #F6F6F6;
background-color: #f6f6f6;
.header-tip {
display: flex;
align-items: center;
padding: 20rpx 32rpx;
color: #A67939;
color: #a67939;
font-size: 26rpx;
background: #FFF;
background: #fff;
}
.content {
......@@ -122,11 +125,11 @@ useDidShow(() => {
}
.detail-wrapper {
background-color: #FFF;
background-color: #fff;
border-radius: 16rpx;
padding: 32rpx;
margin-top: 32rpx;
box-shadow: 0 0 29rpx 0 rgba(106,106,106,0.1);
box-shadow: 0 0 29rpx 0 rgba(106, 106, 106, 0.1);
.detail-item {
display: flex;
......@@ -156,13 +159,13 @@ useDidShow(() => {
left: 0;
width: 750rpx;
padding: 24rpx 32rpx;
background: #FFF;
background: #fff;
box-sizing: border-box;
box-shadow: 0 -10rpx 8rpx 0 rgba(0,0,0,0.06);
box-shadow: 0 -10rpx 8rpx 0 rgba(0, 0, 0, 0.06);
.back-btn {
background-color: #A67939;
color: #FFF;
background-color: #a67939;
color: #fff;
border-radius: 16rpx;
font-size: 32rpx;
padding: 12rpx 0;
......
export default {
navigationBarTitleText: '离线预约记录'
}
......
......@@ -53,15 +53,15 @@ useDidShow(() => {
<style lang="less">
.offline-booking-list-page {
min-height: 100vh;
background-color: #F6F6F6;
background-color: #f6f6f6;
.header-tip {
display: flex;
align-items: center;
padding: 20rpx 32rpx;
color: #A67939;
color: #a67939;
font-size: 26rpx;
background: #FFF;
background: #fff;
}
.list-wrapper {
......@@ -71,7 +71,7 @@ useDidShow(() => {
.empty {
padding: 120rpx 0;
text-align: center;
color: #A67939;
color: #a67939;
font-size: 32rpx;
}
......@@ -81,13 +81,13 @@ useDidShow(() => {
left: 0;
width: 750rpx;
padding: 24rpx 32rpx;
background: #FFF;
background: #fff;
box-sizing: border-box;
box-shadow: 0 -10rpx 8rpx 0 rgba(0,0,0,0.06);
box-shadow: 0 -10rpx 8rpx 0 rgba(0, 0, 0, 0.06);
.home-btn {
background-color: #A67939;
color: #FFF;
background-color: #a67939;
color: #fff;
border-radius: 16rpx;
font-size: 32rpx;
padding: 12rpx 0;
......
......@@ -25,7 +25,7 @@
placeholder="请输入证件号码"
@blur="checkIdCode"
:maxlength="id_type === 1 ? 18 : 30"
>
/>
</view>
<view class="tip-block">
<view class="tip-title">
......@@ -65,72 +65,78 @@
import { ref, computed } from 'vue'
import Taro from '@tarojs/taro'
import { IconFont } from '@nutui/icons-vue-taro'
import qrCodeSearch from '@/components/qrCodeSearch';
import qrCodeSearch from '@/components/qrCodeSearch'
import { useGo } from '@/hooks/useGo'
const go = useGo();
const is_search = ref(false);
const idCode = ref('');
const id_number = ref('');
const show_id_type_picker = ref(false);
const go = useGo()
const is_search = ref(false)
const idCode = ref('')
const id_number = ref('')
const show_id_type_picker = ref(false)
const id_type_options = [
{ label: '身份证', value: 1 },
{ label: '其他', value: 3 }
];
const id_type = ref(id_type_options[0].value);
const id_type_picker_value = ref([String(id_type.value)]);
]
const id_type = ref(id_type_options[0].value)
const id_type_picker_value = ref([String(id_type.value)])
const id_type_columns = computed(() => {
return id_type_options.map(item => ({
text: item.label,
value: String(item.value)
}));
});
}))
})
const id_type_label = computed(() => {
return id_type_options.find(item => item.value === id_type.value)?.label || id_type_options[0].label;
});
return (
id_type_options.find(item => item.value === id_type.value)?.label || id_type_options[0].label
)
})
const open_id_type_picker = () => {
id_type_picker_value.value = [String(id_type.value)];
show_id_type_picker.value = true;
id_type_picker_value.value = [String(id_type.value)]
show_id_type_picker.value = true
}
const on_id_type_confirm = ({ selectedValue }) => {
const value = selectedValue?.[0];
id_type.value = Number(value) || 1;
show_id_type_picker.value = false;
const value = selectedValue?.[0]
id_type.value = Number(value) || 1
show_id_type_picker.value = false
}
// 简单的身份证校验
const validateCIN = (id) => {
return /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/.test(id);
const validateCIN = id => {
return /(^\d{15}$)|(^\d{18}$)|(^\d{17}(\d|X|x)$)/.test(id)
}
const checkIdCode = () => { // 检查身份证号是否为空
if (id_type.value !== 1) return true;
const checkIdCode = () => {
// 检查身份证号是否为空
if (id_type.value !== 1) {
return true
}
let flag = true;
if (idCode.value.length === 15) { // 15位身份证号码不校验
flag = true;
let flag = true
if (idCode.value.length === 15) {
// 15位身份证号码不校验
flag = true
} else {
if (!validateCIN(idCode.value)) {
Taro.showToast({ title: '请检查身份证号码', icon: 'none' });
flag = false;
Taro.showToast({ title: '请检查身份证号码', icon: 'none' })
flag = false
}
}
return flag;
return flag
}
const searchBtn = async () => {
// 查询用户信息
if (checkIdCode() && idCode.value) {
is_search.value = true;
id_number.value = idCode.value;
is_search.value = true
id_number.value = idCode.value
idCode.value = ''
}
}
const goBack = () => {
is_search.value = false;
is_search.value = false
}
const goToHome = () => {
go('/index')
......@@ -149,7 +155,7 @@ const goToHome = () => {
background-size: cover;
.input-card {
background-color: #FFF;
background-color: #fff;
border-radius: 16rpx;
border: 2rpx solid rgba(166, 121, 57, 0.45);
margin-bottom: 32rpx;
......@@ -185,14 +191,14 @@ const goToHome = () => {
.picker-arrow {
margin-left: 10rpx;
color: #BBB;
color: #bbb;
font-size: 28rpx;
}
}
.tip-block {
margin-top: 64rpx;
color: #A67939;
color: #a67939;
text-align: center;
font-size: 30rpx;
......@@ -222,8 +228,8 @@ const goToHome = () => {
}
.footer-btn {
background-color: #A67939;
color: #FFF;
background-color: #a67939;
color: #fff;
text-align: center;
height: 96rpx;
display: flex;
......@@ -256,14 +262,14 @@ const goToHome = () => {
}
.btn-left {
border: 2rpx solid #A67939;
color: #A67939;
background-color: #FFF;
border: 2rpx solid #a67939;
color: #a67939;
background-color: #fff;
}
.btn-right {
background-color: #A67939;
color: #FFF;
background-color: #a67939;
color: #fff;
}
}
......
......@@ -10,7 +10,7 @@
<view @tap="goToBooking" class="visit-time">
<view>参访时间</view>
<view class="flex items-center">
<text style="font-size: 30rpx;">{{ date }} {{ time }}</text>
<text style="font-size: 30rpx">{{ date }} {{ time }}</text>
<IconFont name="rect-right" class="ml-1" />
</view>
</view>
......@@ -20,37 +20,53 @@
</view>
</view>
<view v-if="visitorList.length" class="visitors-list">
<view v-for="(item, index) in visitorList" :key="index" @tap="addVisitor(item)" class="visitor-item">
<view style="margin-right: 32rpx;">
<image v-if="!checked_visitors.includes(item.id)" :src="icon_check1" style="width: 38rpx; height: 38rpx;" />
<image v-else :src="icon_check2" style="width: 38rpx; height: 38rpx;" />
<view
v-for="(item, index) in visitorList"
:key="index"
@tap="addVisitor(item)"
class="visitor-item"
>
<view style="margin-right: 32rpx">
<image
v-if="!checked_visitors.includes(item.id)"
:src="icon_check1"
style="width: 38rpx; height: 38rpx"
/>
<image v-else :src="icon_check2" style="width: 38rpx; height: 38rpx" />
</view>
<view>
<view style="color: #A67939;">{{ item.name }}</view>
<view style="color: #a67939">{{ item.name }}</view>
<view>证件号:{{ formatId(item.id_number) }}</view>
<view v-if="item.is_reserve === RESERVE_STATUS.ENABLE" style="color: #9C9A9A; font-size: 26rpx;">*已预约过{{ date
}}参观,请不要重复预约</view>
<view
v-if="item.is_reserve === RESERVE_STATUS.ENABLE"
style="color: #9c9a9a; font-size: 26rpx"
>*已预约过{{ date }}参观,请不要重复预约</view
>
</view>
</view>
</view>
<view v-else class="no-visitors-list">
<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-visitors-list-title">您还没有添加过参观者</view>
</view>
<view style="height: 160rpx;"></view>
<view style="height: 160rpx"></view>
<view class="submit-wrapper">
<view class="control-wrapper">
<view class="left">
<view style="margin-left: 32rpx; display: flex;align-items: center;">
订单金额&nbsp;&nbsp;<view style="color: #FF1919;display: inline-block;">¥<view
style="font-size: 48rpx;display: inline-block;">&nbsp;{{ total }}</view>
<view style="margin-left: 32rpx; display: flex; align-items: center">
订单金额&nbsp;&nbsp;<view style="color: #ff1919; display: inline-block"
>¥<view style="font-size: 48rpx; display: inline-block">&nbsp;{{ total }}</view>
</view>
</view>
</view>
<view @tap="submitBtn" class="right">提交订单</view>
</view>
<view style="font-size: 27rpx;margin-left: 32rpx;color: #FF1919; margin-bottom: 32rpx;">提交后请在10分钟内完成支付</view>
<view style="font-size: 27rpx; margin-left: 32rpx; color: #ff1919; margin-bottom: 32rpx"
>提交后请在10分钟内完成支付</view
>
</view>
</view>
</template>
......@@ -66,16 +82,16 @@ import { personListAPI, addReserveAPI } from '@/api/index'
import { wechat_pay } from '@/utils/wechatPay'
import { mask_id_number } from '@/utils/tools'
const router = useTaroRouter();
const go = useGo();
const replace = useReplace();
const router = useTaroRouter()
const go = useGo()
const replace = useReplace()
const visitorList = ref([]);
const date = ref('');
const time = ref('');
const price = ref(0);
const period_type = ref('');
const formatId = (id) => mask_id_number(id)
const visitorList = ref([])
const date = ref('')
const time = ref('')
const price = ref(0)
const period_type = ref('')
const formatId = id => mask_id_number(id)
/**
* @description 当天预约标记
......@@ -86,8 +102,8 @@ const RESERVE_STATUS = {
ENABLE: '1'
}
const checked_visitors = ref([]);
const is_submitting = ref(false); // 是否正在提交订单
const checked_visitors = ref([])
const is_submitting = ref(false) // 是否正在提交订单
/**
* @description 选择/取消选择参观者
......@@ -95,20 +111,21 @@ const is_submitting = ref(false); // 是否正在提交订单
* @param {Object} item 参观者数据
* @returns {void} 无返回值
*/
const addVisitor = (item) => {
if (item.is_reserve === RESERVE_STATUS.ENABLE) { // 今天已经预约
const addVisitor = item => {
if (item.is_reserve === RESERVE_STATUS.ENABLE) {
// 今天已经预约
Taro.showToast({ title: '已预约过参观,请不要重复预约', icon: 'none' })
return;
return
}
if (checked_visitors.value.includes(item.id)) {
checked_visitors.value = checked_visitors.value.filter((v) => v !== item.id);
checked_visitors.value = checked_visitors.value.filter(v => v !== item.id)
} else {
checked_visitors.value.push(item.id);
checked_visitors.value.push(item.id)
}
}
const total = computed(() => {
return price.value * checked_visitors.value.length;
return price.value * checked_visitors.value.length
})
/**
......@@ -116,7 +133,7 @@ const total = computed(() => {
* @returns {void} 无返回值
*/
const goToBooking = () => {
go('/booking');
go('/booking')
}
/**
......@@ -124,13 +141,13 @@ const goToBooking = () => {
* @returns {void} 无返回值
*/
const goToVisitor = () => {
go('/addVisitor');
go('/addVisitor')
}
// 待支付订单ID
const pending_pay_id = ref(null);
const pending_pay_id = ref(null)
// 待支付订单是否需要支付
const pending_need_pay = ref(null);
const pending_need_pay = ref(null)
/**
* @description 刷新参观者列表(并同步“当天已预约”标记)
......@@ -139,43 +156,47 @@ const pending_need_pay = ref(null);
* @returns {Promise<void>} 无返回值
*/
const refreshVisitorList = async (options) => {
if (!date.value || !time.value) return;
const refreshVisitorList = async options => {
if (!date.value || !time.value) {
return
}
const res = await personListAPI({
reserve_date: date.value,
begin_time: time.value.split('-')[0],
end_time: time.value.split('-')[1],
period_type: period_type.value
});
})
if (res && res.code) {
visitorList.value = res.data || [];
visitorList.value = res.data || []
if (options?.reset_checked) {
checked_visitors.value = [];
checked_visitors.value = []
}
}
}
let is_showing_pay_modal = false;
let is_showing_pay_modal = false
/**
* @description 支付未完成弹窗(防并发)
* @param {string} content 弹窗内容
* @returns {Promise<boolean>} true=继续支付,false=离开
*/
const showPayErrorModal = async (content) => {
if (is_showing_pay_modal) return;
is_showing_pay_modal = true;
const showPayErrorModal = async content => {
if (is_showing_pay_modal) {
return
}
is_showing_pay_modal = true
try {
const res = await Taro.showModal({
title: '提示',
content: content || '支付失败,请稍后再试',
showCancel: true,
cancelText: '离开',
confirmText: '继续支付',
});
return !!res?.confirm;
confirmText: '继续支付'
})
return !!res?.confirm
} finally {
is_showing_pay_modal = false;
is_showing_pay_modal = false
}
}
......@@ -186,20 +207,23 @@ const showPayErrorModal = async (content) => {
* @returns {Promise<void>} 无返回值
*/
const submitBtn = async () => {
if (is_submitting.value) return;
if (is_submitting.value) {
return
}
if (!checked_visitors.value.length) {
Taro.showToast({ title: '请先添加参观者', icon: 'none' })
return;
return
}
is_submitting.value = true;
is_submitting.value = true
try {
let pay_id = pending_pay_id.value;
let need_pay = pending_need_pay.value;
let pay_id = pending_pay_id.value
let need_pay = pending_need_pay.value
if (!pay_id) { // TAG: 提交订单, 如果没有待支付订单ID, 则创建一个新的订单
Taro.showLoading({ title: '提交中...' });
let reserve_res = null;
if (!pay_id) {
// TAG: 提交订单, 如果没有待支付订单ID, 则创建一个新的订单
Taro.showLoading({ title: '提交中...' })
let reserve_res = null
try {
reserve_res = await addReserveAPI({
reserve_date: date.value,
......@@ -207,60 +231,61 @@ const submitBtn = async () => {
end_time: time.value.split('-')[1],
person_id_list: JSON.stringify(checked_visitors.value),
period_type: period_type.value
});
})
} finally {
Taro.hideLoading();
Taro.hideLoading()
}
if (!reserve_res || reserve_res.code != 1) {
return;
return
}
pay_id = reserve_res.data.pay_id;
pending_pay_id.value = pay_id;
need_pay = reserve_res.data?.need_pay;
pending_need_pay.value = need_pay;
await refreshVisitorList({ reset_checked: true });
pay_id = reserve_res.data.pay_id
pending_pay_id.value = pay_id
need_pay = reserve_res.data?.need_pay
pending_need_pay.value = need_pay
await refreshVisitorList({ reset_checked: true })
}
// 以接口返回的 need_pay 为准:1=需要支付,0=不需要支付
if (Number(need_pay) === 1 || need_pay === true) {
// 初始化循环
let should_continue = true;
let should_continue = true
// 循环支付直到支付成功或用户取消支付
while (should_continue) {
const pay_res = await wechat_pay({ pay_id })
if (pay_res && pay_res.code == 1) {
pending_pay_id.value = null;
pending_need_pay.value = null;
go('/success', { pay_id });
pending_pay_id.value = null
pending_need_pay.value = null
go('/success', { pay_id })
return
}
// 刷新参观者列表, 清除已预约标记
refreshVisitorList({ reset_checked: true }).catch(() => {});
should_continue = await showPayErrorModal(pay_res?.msg || '支付未完成,可再次点击提交订单继续支付')
refreshVisitorList({ reset_checked: true }).catch(() => {})
should_continue = await showPayErrorModal(
pay_res?.msg || '支付未完成,可再次点击提交订单继续支付'
)
}
replace('/bookingList')
return
} else {
pending_pay_id.value = null;
pending_need_pay.value = null;
go('/success', { pay_id });
pending_pay_id.value = null
pending_need_pay.value = null
go('/success', { pay_id })
}
} finally {
is_submitting.value = false;
is_submitting.value = false
}
}
useDidShow(async () => {
const params = router.params;
date.value = params.date || '';
time.value = params.time || '';
price.value = params.price || 0;
period_type.value = params.period_type || '';
await refreshVisitorList();
});
const params = router.params
date.value = params.date || ''
time.value = params.time || ''
price.value = params.price || 0
period_type.value = params.period_type || ''
await refreshVisitorList()
})
</script>
<style lang="less">
......@@ -269,7 +294,7 @@ useDidShow(async () => {
position: relative;
.visit-time {
background-color: #FFF;
background-color: #fff;
display: flex;
align-items: center;
justify-content: space-between;
......@@ -278,8 +303,8 @@ useDidShow(async () => {
}
.add-visitors {
border: 2rpx dashed #A67939;
color: #A67939;
border: 2rpx dashed #a67939;
color: #a67939;
border-radius: 10rpx;
text-align: center;
padding: 21rpx 0;
......@@ -289,7 +314,7 @@ useDidShow(async () => {
.visitors-list {
.visitor-item {
background-color: #FFF;
background-color: #fff;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 32rpx;
......@@ -311,7 +336,7 @@ useDidShow(async () => {
}
.no-visitors-list-title {
color: #A67939;
color: #a67939;
font-size: 34rpx;
}
}
......@@ -322,7 +347,7 @@ useDidShow(async () => {
left: 0;
width: 750rpx;
display: flex;
background-color: #FFF;
background-color: #fff;
// padding: 32rpx;
justify-content: space-between;
flex-direction: column;
......@@ -342,8 +367,8 @@ useDidShow(async () => {
}
.right {
background-color: #A67939;
color: #FFF;
background-color: #a67939;
color: #fff;
margin: 32rpx;
padding: 26rpx 96rpx;
border-radius: 5px;
......
......@@ -13,14 +13,23 @@
<view class="text">预约成功</view>
</view>
<view class="appointment-information">
<view class="number-of-visitors">参观人数:<text>{{ billInfo?.total_qty }} 人</text></view>
<view class="visit-time">参访时间:<text>{{ billInfo?.datetime }}</text></view>
<view class="payment-amount">支付金额:<text>¥ {{ billInfo?.total_amt }}</text></view>
<view class="number-of-visitors"
>参观人数:<text>{{ billInfo?.total_qty }} 人</text></view
>
<view class="visit-time"
>参访时间:<text>{{ billInfo?.datetime }}</text></view
>
<view class="payment-amount"
>支付金额:<text>¥ {{ billInfo?.total_amt }}</text></view
>
</view>
<view class="appointment-notice">
<view style="margin-bottom: 8rpx; display: flex; align-items: center; justify-content: center;"><IconFont name="tips" />&nbsp;温馨提示</view>
<view style="font-size: 27rpx;">1. 一人一码,或拿身份证,扫码或识别身份证成功后进入</view>
<view style="font-size: 27rpx;">2. 若您无法按时参观,请提前在预约记录中取消您的预约</view>
<view
style="margin-bottom: 8rpx; display: flex; align-items: center; justify-content: center"
><IconFont name="tips" />&nbsp;温馨提示</view
>
<view style="font-size: 27rpx">1. 一人一码,或拿身份证,扫码或识别身份证成功后进入</view>
<view style="font-size: 27rpx">2. 若您无法按时参观,请提前在预约记录中取消您的预约</view>
</view>
</view>
<view class="success-btn">
......@@ -36,27 +45,27 @@ import Taro, { useDidShow, useRouter as useTaroRouter } from '@tarojs/taro'
import { IconFont } from '@nutui/icons-vue-taro'
import { useGo } from '@/hooks/useGo'
import { billInfoAPI } from '@/api/index'
import { formatDatetime } from '@/utils/tools';
import { formatDatetime } from '@/utils/tools'
import { refresh_offline_booking_cache } from '@/composables/useOfflineBookingCache'
const router = useTaroRouter();
const go = useGo();
const router = useTaroRouter()
const go = useGo()
const goToHome = () => {
go('/index')
}
const goToDetail = () => {
go('/bookingDetail', { pay_id: router.params.pay_id });
go('/bookingDetail', { pay_id: router.params.pay_id })
}
const billInfo = ref({});
const billInfo = ref({})
useDidShow(async () => {
// 获取订单详情
const { code, data } = await billInfoAPI({ pay_id: router.params.pay_id });
const { code, data } = await billInfoAPI({ pay_id: router.params.pay_id })
if (code) {
data.datetime = data && formatDatetime(data);
billInfo.value = data;
data.datetime = data && formatDatetime(data)
billInfo.value = data
}
// 刷新离线预约缓存
refresh_offline_booking_cache({ force: true })
......@@ -66,7 +75,7 @@ useDidShow(async () => {
<style lang="less">
.success-page {
position: relative;
background-color: #FFF;
background-color: #fff;
min-height: 100vh;
.text-prompts {
......@@ -79,28 +88,28 @@ useDidShow(async () => {
width: 60vw;
}
.text {
color: #A67939;
color: #a67939;
font-size: 40rpx;
margin-top: 32rpx;
}
}
.appointment-information {
padding: 64rpx 32rpx;
border-bottom: 2rpx dashed #A67939;
border-bottom: 2rpx dashed #a67939;
line-height: 2;
.number-of-visitors {
text {
color: #A67939;
color: #a67939;
}
}
.visit-time {
text {
color: #A67939;
color: #a67939;
}
}
.payment-amount {
text {
color: #A67939;
color: #a67939;
}
}
}
......@@ -126,13 +135,13 @@ useDidShow(async () => {
}
.btn-left {
border: 2rpx solid #A67939;
color: #A67939;
border: 2rpx solid #a67939;
color: #a67939;
}
.btn-right {
background-color: #A67939;
color: #FFF;
background-color: #a67939;
color: #fff;
}
}
}
......
......@@ -28,27 +28,41 @@
<view class="text-sm text-gray-500 mb-4">核销记录信息</view>
<template v-if="verify_info && Object.keys(verify_info).length > 0">
<view class="flex justify-between items-center py-3 border-b border-gray-50 border-solid last:border-0">
<view
class="flex justify-between items-center py-3 border-b border-gray-50 border-solid last:border-0"
>
<view class="text-gray-500 text-base">姓名</view>
<view class="text-gray-900 text-lg font-medium">{{ verify_info.person_name || '-' }}</view>
<view class="text-gray-900 text-lg font-medium">{{
verify_info.person_name || '-'
}}</view>
</view>
<view class="flex justify-between items-center py-3 border-b border-gray-50 border-solid last:border-0">
<view
class="flex justify-between items-center py-3 border-b border-gray-50 border-solid last:border-0"
>
<view class="text-gray-500 text-base">证件号码</view>
<view class="text-gray-900 text-lg font-medium">{{ formatIdNumber(verify_info.id_number) }}</view>
<view class="text-gray-900 text-lg font-medium">{{
formatIdNumber(verify_info.id_number)
}}</view>
</view>
<view class="flex justify-between items-center py-3 border-b border-gray-50 border-solid last:border-0">
<view
class="flex justify-between items-center py-3 border-b border-gray-50 border-solid last:border-0"
>
<view class="text-gray-500 text-base">状态</view>
<view class="text-amber-600 text-lg font-medium">{{ verify_info.status || '-' }}</view>
</view>
<view class="flex justify-between items-center py-3 border-b border-gray-50 border-solid last:border-0">
<view
class="flex justify-between items-center py-3 border-b border-gray-50 border-solid last:border-0"
>
<view class="text-gray-500 text-base">预约开始</view>
<view class="text-gray-900 text-lg font-medium">{{ verify_info.begin_time || '-' }}</view>
</view>
<view class="flex justify-between items-center py-3 border-b border-gray-50 border-solid last:border-0">
<view
class="flex justify-between items-center py-3 border-b border-gray-50 border-solid last:border-0"
>
<view class="text-gray-500 text-base">预约结束</view>
<view class="text-gray-900 text-lg font-medium">{{ verify_info.end_time || '-' }}</view>
</view>
......@@ -94,24 +108,38 @@ const msg = ref('请点击下方按钮进行核销')
const store = mainStore()
const replace = useReplace()
const formatIdNumber = (id) => mask_id_number(id, { keep_start: 6, keep_end: 4 })
const formatIdNumber = id => mask_id_number(id, { keep_start: 6, keep_end: 4 })
const status_title = computed(() => {
if (verify_status.value === 'verifying') return '核销中'
if (verify_status.value === 'success') return '核销成功'
if (verify_status.value === 'fail') return '核销失败'
if (verify_status.value === 'verifying') {
return '核销中'
}
if (verify_status.value === 'success') {
return '核销成功'
}
if (verify_status.value === 'fail') {
return '核销失败'
}
return '核销'
})
const status_icon_type = computed(() => {
if (verify_status.value === 'verifying') return 'waiting'
if (verify_status.value === 'success') return 'success'
if (verify_status.value === 'fail') return 'cancel'
if (verify_status.value === 'verifying') {
return 'waiting'
}
if (verify_status.value === 'success') {
return 'success'
}
if (verify_status.value === 'fail') {
return 'cancel'
}
return 'info'
})
const status_icon_color = computed(() => {
if (verify_status.value === 'fail') return '#E24A4A'
if (verify_status.value === 'fail') {
return '#E24A4A'
}
return '#A67939'
})
......@@ -126,9 +154,13 @@ const status_icon_color = computed(() => {
* @return {void}
*/
const verify_ticket = async (code) => {
if (!code) return
if (verify_status.value === 'verifying') return
const verify_ticket = async code => {
if (!code) {
return
}
if (verify_status.value === 'verifying') {
return
}
verify_code.value = code
verify_status.value = 'verifying'
......@@ -164,7 +196,9 @@ useDidShow(async () => {
return
}
if (permission_res?.data) store.changeUserInfo(permission_res.data)
if (permission_res?.data) {
store.changeUserInfo(permission_res.data)
}
if (permission_res?.data?.can_redeem !== true) {
replace('volunteerLogin')
return
......@@ -178,11 +212,10 @@ useDidShow(async () => {
const start_scan_and_verify = () => {
Taro.scanCode({
success: (res) => {
success: res => {
verify_ticket(res?.result || '')
},
fail: () => {
}
fail: () => {}
})
}
</script>
......@@ -199,9 +232,9 @@ const start_scan_and_verify = () => {
bottom: 0;
width: 750rpx;
padding: 24rpx 32rpx calc(24rpx + env(safe-area-inset-bottom));
background-color: #FFFFFF;
background-color: #ffffff;
box-sizing: border-box;
box-shadow: 0 -10rpx 8rpx 0 rgba(0,0,0,0.08);
box-shadow: 0 -10rpx 8rpx 0 rgba(0, 0, 0, 0.08);
}
.verify-btn {
......
......@@ -4,7 +4,14 @@
<view class="title">
<view class="text">参观者信息</view>
</view>
<view @tap="() => { go('/pages/addVisitor/index') }" class="add-visitors">
<view
@tap="
() => {
go('/pages/addVisitor/index')
}
"
class="add-visitors"
>
<view class="add-btn flex items-center justify-center">
<IconFont name="plus" class="mr-1" /> 添加参观者
</view>
......@@ -12,20 +19,26 @@
<view v-if="visitorList.length" class="visitors-list">
<view v-for="(item, index) in visitorList" :key="index" class="visitor-item">
<view>
<view style="color: #A67939;">{{ item.name }}</view>
<view style="color: #a67939">{{ item.name }}</view>
<view>证件号:{{ formatId(item.id_number) }}</view>
</view>
<view @tap="removeItem(item)" style="margin-left: 32rpx;">
<image src="https://cdn.ipadbiz.cn/xys/booking/%E5%88%A0%E9%99%A4@2x.png" style="width: 38rpx; height: 38rpx;" />
<view @tap="removeItem(item)" style="margin-left: 32rpx">
<image
src="https://cdn.ipadbiz.cn/xys/booking/%E5%88%A0%E9%99%A4@2x.png"
style="width: 38rpx; height: 38rpx"
/>
</view>
</view>
</view>
<view v-else class="no-visitors-list">
<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-visitors-list-title">您还没有添加过参观者</view>
</view>
</view>
<view style="height: 256rpx;"></view>
<view style="height: 256rpx"></view>
<indexNav
:icons="nav_icons"
active="me"
......@@ -49,28 +62,37 @@ import icon_4 from '@/assets/images/二维码icon.png'
import icon_5 from '@/assets/images/我的02@2x.png'
import { mask_id_number } from '@/utils/tools'
const go = useGo();
const go = useGo()
const toCode = () => { // 跳转到预约码
go('/pages/bookingCode/index');
const toCode = () => {
// 跳转到预约码
go('/pages/bookingCode/index')
}
const toHome = () => { // 跳转到首页
go('/pages/index/index');
const toHome = () => {
// 跳转到首页
go('/pages/index/index')
}
const toMy = () => { // 跳转到我的
go('/pages/me/index');
const toMy = () => {
// 跳转到我的
go('/pages/me/index')
}
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()
if (key === 'me') return toMy()
const on_nav_select = key => {
if (key === 'home') {
return toHome()
}
if (key === 'code') {
return toCode()
}
if (key === 'me') {
return toMy()
}
}
const visitorList = ref([]);
const formatId = (id) => mask_id_number(id)
const visitorList = ref([])
const formatId = id => mask_id_number(id)
/**
* @description 加载参观者列表
......@@ -78,13 +100,13 @@ const formatId = (id) => mask_id_number(id)
*/
const loadList = async () => {
try {
const { code, data } = await personListAPI({});
const { code, data } = await personListAPI({})
if (code) {
visitorList.value = data || [];
visitorList.value = data || []
}
} catch (err) {
console.error(err);
Taro.showToast({ title: '加载失败', icon: 'none' });
console.error(err)
Taro.showToast({ title: '加载失败', icon: 'none' })
}
}
......@@ -93,31 +115,31 @@ const loadList = async () => {
* @param {Object} item 参观者对象
* @returns {Promise<void>} 无返回值
*/
const removeItem = async (item) => {
const { confirm } = await Taro.showModal({ title: '提示', content: '确定删除该参观者吗?' });
const removeItem = async item => {
const { confirm } = await Taro.showModal({ title: '提示', content: '确定删除该参观者吗?' })
if (confirm) {
try {
const res = await delPersonAPI({ person_id: item.id });
const res = await delPersonAPI({ person_id: item.id })
if (res && res.code) {
Taro.showToast({ title: '删除成功' });
loadList();
Taro.showToast({ title: '删除成功' })
loadList()
}
} catch (error) {
console.error(error);
Taro.showToast({ title: '删除出错', icon: 'none' });
console.error(error)
Taro.showToast({ title: '删除出错', icon: 'none' })
}
}
}
useDidShow(() => {
loadList();
loadList()
})
</script>
<style lang="less">
.visitor-list-page {
min-height: 100vh;
background-color: #F6F6F6;
background-color: #f6f6f6;
padding: 32rpx;
.visitor-content {
......@@ -126,14 +148,14 @@ useDidShow(() => {
font-size: 35rpx;
font-weight: bold;
margin-bottom: 32rpx;
border-left: 6rpx solid #A67939;
border-left: 6rpx solid #a67939;
padding-left: 16rpx;
}
}
.add-visitors {
border: 2rpx dashed #A67939;
color: #A67939;
border: 2rpx dashed #a67939;
color: #a67939;
border-radius: 10rpx;
text-align: center;
padding: 21rpx 0;
......@@ -148,7 +170,7 @@ useDidShow(() => {
.visitors-list {
.visitor-item {
background-color: #FFF;
background-color: #fff;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 32rpx;
......@@ -165,7 +187,7 @@ useDidShow(() => {
flex-direction: column;
.no-visitors-list-title {
color: #A67939;
color: #a67939;
font-size: 34rpx;
margin-top: 32rpx;
}
......
......@@ -17,13 +17,23 @@
<view class="input-group">
<text class="label">账号</text>
<input v-model="username" placeholder="请输入账号" placeholder-class="input-placeholder" cursorSpacing="40rpx" />
<input
v-model="username"
placeholder="请输入账号"
placeholder-class="input-placeholder"
cursorSpacing="40rpx"
/>
</view>
<view class="input-group">
<text class="label">密码</text>
<input v-model="password" password placeholder="请输入密码" placeholder-class="input-placeholder"
cursorSpacing="40rpx" />
<input
v-model="password"
password
placeholder="请输入密码"
placeholder-class="input-placeholder"
cursorSpacing="40rpx"
/>
</view>
<button class="login-btn" @tap="handleLogin">立即登录</button>
......@@ -51,10 +61,16 @@ const password = ref('')
const check_permission_and_redirect = async () => {
try {
const permission_res = await checkRedeemPermissionAPI()
if (permission_res?.code !== 1) return
if (permission_res?.data) store.changeUserInfo(permission_res.data)
if (permission_res?.data?.can_redeem === true) replace('verificationResult')
} catch (e) { }
if (permission_res?.code !== 1) {
return
}
if (permission_res?.data) {
store.changeUserInfo(permission_res.data)
}
if (permission_res?.data?.can_redeem === true) {
replace('verificationResult')
}
} catch (e) {}
}
useDidShow(() => {
......@@ -85,7 +101,9 @@ const handleLogin = async () => {
return
}
if (permission_res?.data) store.changeUserInfo(permission_res.data)
if (permission_res?.data) {
store.changeUserInfo(permission_res.data)
}
if (permission_res?.data?.can_redeem === true) {
Taro.showToast({ title: permission_res?.msg || login_res?.msg || '登录成功', icon: 'success' })
......@@ -100,7 +118,7 @@ const handleLogin = async () => {
<style lang="less">
.login-page {
min-height: 100vh;
background-color: #F6F6F6;
background-color: #f6f6f6;
display: flex;
flex-direction: column;
align-items: center;
......@@ -146,7 +164,7 @@ const handleLogin = async () => {
}
.input-group {
background-color: #F7F8FA;
background-color: #f7f8fa;
border-radius: 12rpx;
padding: 28rpx 30rpx;
margin-bottom: 32rpx;
......@@ -171,13 +189,13 @@ const handleLogin = async () => {
}
.input-placeholder {
color: #C0C4CC;
color: #c0c4cc;
}
}
.login-btn {
margin-top: 80rpx;
background: #A67939;
background: #a67939;
color: #fff;
height: 96rpx;
line-height: 96rpx;
......
......@@ -4,9 +4,17 @@
<view>
<IconFont name="clock" size="80rpx" color="#A67939" />
</view>
<view style="margin: 32rpx 0;">支付中</view>
<view style="margin: 32rpx 0">支付中</view>
<view>{{ current.seconds }} s</view>
<view style="margin: 48rpx 0; font-size: 27rpx; color: #A67939; text-align: center; line-height: 2;">
<view
style="
margin: 48rpx 0;
font-size: 27rpx;
color: #a67939;
text-align: center;
line-height: 2;
"
>
温馨提示:{{ pay_msg }}<br />
</view>
</view>
......@@ -56,7 +64,9 @@ const startCountdown = () => {
}
const checkStatus = async () => {
if (!pay_id) return
if (!pay_id) {
return
}
try {
const { code, data } = await billPayStatusAPI({ pay_id })
// TAG:轮询支付回调
......@@ -96,8 +106,12 @@ onMounted(() => {
})
onUnmounted(() => {
if(timer) clearInterval(timer)
if(countdownTimer) clearInterval(countdownTimer)
if (timer) {
clearInterval(timer)
}
if (countdownTimer) {
clearInterval(countdownTimer)
}
})
const goBackBtn = () => {
......
......@@ -16,7 +16,7 @@
<view class="offline-entry" @tap="toOfflineCode">
<view class="circle-btn">
<image :src="icon_invite" style="width: 60rpx; height: 60rpx; margin-bottom: 16rpx;" />
<image :src="icon_invite" style="width: 60rpx; height: 60rpx; margin-bottom: 16rpx" />
<text>预约记录</text>
</view>
</view>
......@@ -38,12 +38,14 @@ import { weak_network_text, get_weak_network_modal_no_cache_options } from '@/ut
import icon_invite from '@/assets/images/二维码@2x2.png'
const go = useGo();
const go = useGo()
const weak_network_title = weak_network_text.title
const weak_network_desc = weak_network_text.offline_page_desc
onMounted(async () => {
if (has_offline_booking_cache()) return
if (has_offline_booking_cache()) {
return
}
try {
await Taro.showModal(get_weak_network_modal_no_cache_options())
} catch (e) {
......@@ -53,13 +55,13 @@ onMounted(async () => {
})
const toOfflineCode = () => {
go('/pages/offlineBookingList/index');
go('/pages/offlineBookingList/index')
}
const retry = () => {
// 尝试重新加载当前页或者是返回上一页重试
// 这里简单做成返回首页
Taro.reLaunch({ url: '/pages/index/index' });
Taro.reLaunch({ url: '/pages/index/index' })
}
</script>
......@@ -103,7 +105,7 @@ const retry = () => {
width: 240rpx;
height: 240rpx;
border-radius: 50%;
background: #FFFFFF;
background: #ffffff;
display: flex;
flex-direction: column;
align-items: center;
......@@ -111,7 +113,7 @@ const retry = () => {
box-shadow: 0 10rpx 30rpx rgba(166, 121, 57, 0.4);
text {
color: #A67939;
color: #a67939;
font-size: 32rpx;
font-weight: bold;
letter-spacing: 2rpx;
......@@ -126,7 +128,7 @@ const retry = () => {
.sub-action {
padding: 20rpx;
text {
color: #A67939;
color: #a67939;
font-size: 28rpx;
text-decoration: underline;
}
......
......@@ -16,8 +16,8 @@ export const useCounterStore = defineStore('counter', {
*/
increment() {
this.count++
},
},
}
}
})
// 也可以用函数式(类似组件 setup)定义 Store,适合更复杂场景:
......
......@@ -24,7 +24,7 @@ export const hostStore = defineStore('host', {
* @param {string} id 主办方 id
* @returns {void} 无返回值
*/
add (id) {
add(id) {
this.id = id
},
/**
......@@ -32,8 +32,8 @@ export const hostStore = defineStore('host', {
* @param {string} id join_id
* @returns {void} 无返回值
*/
addJoin (id) {
addJoin(id) {
this.join_id = id
},
},
}
}
})
......
......@@ -5,7 +5,7 @@
* @FilePath: /xyxBooking-weapp/src/stores/main.js
* @Description: 文件描述
*/
import { defineStore } from 'pinia';
import { defineStore } from 'pinia'
/**
* @description 全局主状态
......@@ -18,17 +18,17 @@ export const mainStore = defineStore('main', {
count: 0,
auth: false,
// keepPages: ['default'], // 小程序不支持这种 keep-alive 机制
appUserInfo: null, // 用户信息
};
appUserInfo: null // 用户信息
}
},
getters: {
/**
* @description 是否具备义工核销权限
* @returns {boolean} true=义工,false=非义工
*/
isVolunteer: (state) => {
return !!(state.appUserInfo && (state.appUserInfo.can_redeem === true));
},
isVolunteer: state => {
return !!(state.appUserInfo && state.appUserInfo.can_redeem === true)
}
},
actions: {
/**
......@@ -36,8 +36,8 @@ export const mainStore = defineStore('main', {
* @param {boolean} state 是否已授权
* @returns {void} 无返回值
*/
changeState (state) {
this.auth = state;
changeState(state) {
this.auth = state
},
// setVolunteerStatus(status) {
// this.isVolunteer = status;
......@@ -55,8 +55,8 @@ export const mainStore = defineStore('main', {
* @param {Object|null} info 用户信息对象
* @returns {void} 无返回值
*/
changeUserInfo (info) {
this.appUserInfo = info;
changeUserInfo(info) {
this.appUserInfo = info
}
},
});
}
})
......
......@@ -15,7 +15,7 @@ import { defineStore } from 'pinia'
export const routerStore = defineStore('router', {
state: () => {
return {
url: '',
url: ''
}
},
actions: {
......@@ -24,15 +24,15 @@ export const routerStore = defineStore('router', {
* @param {string} path 页面路径(可带 query)
* @returns {void} 无返回值
*/
add (path) {
add(path) {
this.url = path
},
/**
* @description 清空回跳路径
* @returns {void} 无返回值
*/
remove () {
remove() {
this.url = ''
},
},
}
}
})
......
......@@ -28,7 +28,9 @@ let navigating_to_auth = false
*/
export const getCurrentPageFullPath = () => {
const pages = Taro.getCurrentPages()
if (!pages || pages.length === 0) return ''
if (!pages || pages.length === 0) {
return ''
}
const current_page = pages[pages.length - 1]
const route = current_page.route
......@@ -36,7 +38,7 @@ export const getCurrentPageFullPath = () => {
// 改进:key 也需要编码,避免特殊字符导致 URL 解析错误
const query_params = Object.keys(options)
.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(options[key])}`)
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(options[key])}`)
.join('&')
return query_params ? `${route}?${query_params}` : route
......@@ -47,7 +49,7 @@ export const getCurrentPageFullPath = () => {
* @param {string} custom_path 自定义路径,不传则取当前页完整路径
* @returns {void} 无返回值
*/
export const saveCurrentPagePath = (custom_path) => {
export const saveCurrentPagePath = custom_path => {
const router = routerStore()
const path = custom_path || getCurrentPageFullPath()
router.add(path)
......@@ -75,12 +77,16 @@ let auth_promise = null
* @param {object} response Taro.request 响应对象
* @returns {string|null} cookie 字符串或 null
*/
const extractCookie = (response) => {
const extractCookie = response => {
// 小程序端优先从 response.cookies 取
if (response.cookies?.[0]) return response.cookies[0]
if (response.cookies?.[0]) {
return response.cookies[0]
}
// H5 端从 header 取(兼容不同大小写)
const cookie = response.header?.['Set-Cookie'] || response.header?.['set-cookie']
if (Array.isArray(cookie)) return cookie[0]
if (Array.isArray(cookie)) {
return cookie[0]
}
return cookie || null
}
......@@ -92,18 +98,20 @@ const extractCookie = (response) => {
* @param {boolean} options.show_loading 是否展示 loading,默认 true
* @returns {Promise<{code:number,msg?:string,data?:any,cookie?:string}>} 授权结果(会把 cookie 写入 storage 的 sessionid)
*/
export const refreshSession = async (options) => {
export const refreshSession = async options => {
const show_loading = options?.show_loading !== false
// 已有授权进行中时,直接复用同一个 Promise
if (auth_promise) return auth_promise
if (auth_promise) {
return auth_promise
}
auth_promise = (async () => {
try {
if (show_loading) {
Taro.showLoading({
title: '加载中...',
mask: true,
mask: true
})
}
......@@ -111,7 +119,7 @@ export const refreshSession = async (options) => {
const login_result = await new Promise((resolve, reject) => {
Taro.login({
success: resolve,
fail: reject,
fail: reject
})
})
......@@ -120,14 +128,14 @@ export const refreshSession = async (options) => {
}
const request_data = {
code: login_result.code,
code: login_result.code
}
// 换取后端会话(服务端通过 Set-Cookie 返回会话信息)
const response = await Taro.request({
url: buildApiUrl('openid_wxapp'),
method: 'POST',
data: request_data,
data: request_data
})
if (!response?.data || response.data.code !== 1) {
......@@ -150,7 +158,7 @@ export const refreshSession = async (options) => {
return {
...response.data,
cookie,
cookie
}
} finally {
if (show_loading) {
......@@ -171,7 +179,7 @@ export const refreshSession = async (options) => {
*
* 改进:使用下划线前缀表示私有函数,仅供 silentAuth 内部使用
*/
const _do_silent_auth = async (show_loading) => {
const _do_silent_auth = async show_loading => {
// 已有 sessionid 时直接视为已授权
if (hasAuth()) {
return { code: 1, msg: '已授权' }
......@@ -206,13 +214,14 @@ export const silentAuth = async (on_success, on_error, options) => {
* 后续再调用 silentAuth() 会复用这个失败的 Promise,导致永远失败、且永远不会重新发起授权。
* 用 finally :保证成功/失败都会清空,下一次调用才有机会重新走授权流程。
*/
auth_promise = _do_silent_auth(show_loading)
.finally(() => {
auth_promise = _do_silent_auth(show_loading).finally(() => {
auth_promise = null
})
}
const result = await auth_promise
if (on_success) on_success(result)
if (on_success) {
on_success(result)
}
/**
* 当前返回值 没有实际消费点 :全项目只在 3 处调用,全部都 不使用返回值 。
......@@ -230,7 +239,9 @@ export const silentAuth = async (on_success, on_error, options) => {
message: error?.message || '授权失败,请稍后重试',
original: error
}
if (on_error) on_error(error_obj)
if (on_error) {
on_error(error_obj)
}
throw error
}
}
......@@ -253,7 +264,7 @@ const NAVIGATING_RESET_DELAY_MS = 300
* @param {string} return_path 指定回跳路径(可选)
* @returns {Promise<void>} 无返回值
*/
export const navigateToAuth = async (return_path) => {
export const navigateToAuth = async return_path => {
const pages = Taro.getCurrentPages()
const current_page = pages[pages.length - 1]
const current_route = current_page?.route
......@@ -262,8 +273,12 @@ export const navigateToAuth = async (return_path) => {
}
const now = Date.now()
if (navigating_to_auth) return
if (now - last_navigate_auth_at < NAVIGATE_AUTH_COOLDOWN_MS) return
if (navigating_to_auth) {
return
}
if (now - last_navigate_auth_at < NAVIGATE_AUTH_COOLDOWN_MS) {
return
}
last_navigate_auth_at = now
navigating_to_auth = true
......@@ -339,7 +354,7 @@ export const returnToOriginalPage = async (default_path = '/pages/index/index')
* @param {object} options 页面 options
* @returns {boolean} true=来自分享场景,false=非分享场景
*/
export const isFromShare = (options) => {
export const isFromShare = options => {
return options && (options.from_share === '1' || options.scene)
}
......@@ -353,7 +368,9 @@ export const isFromShare = (options) => {
*/
export const handleSharePageAuth = async (options, callback) => {
if (hasAuth()) {
if (typeof callback === 'function') callback()
if (typeof callback === 'function') {
callback()
}
return true
}
......@@ -364,7 +381,9 @@ export const handleSharePageAuth = async (options, callback) => {
try {
await silentAuth(
() => {
if (typeof callback === 'function') callback()
if (typeof callback === 'function') {
callback()
}
},
() => {
navigateToAuth()
......@@ -382,7 +401,7 @@ export const handleSharePageAuth = async (options, callback) => {
* @param {string} path 原路径
* @returns {string} 追加后的路径
*/
export const addShareFlag = (path) => {
export const addShareFlag = path => {
const separator = path.includes('?') ? '&' : '?'
return `${path}${separator}from_share=1`
}
......
......@@ -14,13 +14,14 @@
* - 线上/测试环境按需切换
* @type {string}
*/
const BASE_URL = process.env.NODE_ENV === 'production'
// ? 'https://oa.onwall.cn'
? 'https://oa-dev.onwall.cn'
// ?'https://oa.jcedu.org'
: 'https://oa-dev.onwall.cn'
// : 'https://oa.jcedu.org'
;
const BASE_URL =
process.env.NODE_ENV === 'production'
? // ? 'https://oa.onwall.cn'
'https://oa-dev.onwall.cn'
: // ?'https://oa.jcedu.org'
'https://oa-dev.onwall.cn'
// : 'https://oa.jcedu.org'
/**
* 接口默认公共参数(避免在多个文件里硬编码)
* - f:业务模块标识
......@@ -28,7 +29,7 @@ const BASE_URL = process.env.NODE_ENV === 'production'
*/
export const REQUEST_DEFAULT_PARAMS = {
f: 'reserve',
client_name: '智慧西园寺',
client_name: '智慧西园寺'
}
export default BASE_URL
......
......@@ -5,7 +5,7 @@
* @FilePath: /xyxBooking-weapp/src/utils/mixin.js
* @Description: 全局 mixin(兼容保留)
*/
import { getSessionId, setSessionId, clearSessionId } from './request';
import { getSessionId, setSessionId, clearSessionId } from './request'
/**
* @description 全局 mixin(兼容保留)
......@@ -16,12 +16,12 @@ import { getSessionId, setSessionId, clearSessionId } from './request';
export default {
// 初始化入口(如需全局混入逻辑可写在这里)
init: {
created () {
created() {
// 说明:sessionid 现在由 request.js 的拦截器自动管理
// 如需在组件创建时做通用初始化,可在此补充
}
}
};
}
/**
* @description 导出 sessionid 管理工具(供极端场景手动处理)
......
......@@ -12,7 +12,7 @@ import Taro from '@tarojs/taro'
* @param {string} network_type - 网络类型
* @returns {boolean} 是否可用
*/
export const is_usable_network = (network_type) => {
export const is_usable_network = network_type => {
return ['wifi', '4g', '5g', '3g'].includes(network_type)
}
......@@ -25,7 +25,7 @@ export const get_network_type = async () => {
const result = await new Promise((resolve, reject) => {
Taro.getNetworkType({
success: resolve,
fail: reject,
fail: reject
})
})
return result?.networkType || 'unknown'
......
......@@ -20,39 +20,39 @@ if (typeof TextEncoder === 'undefined') {
* @returns {Uint8Array} UTF-8 字节数组
*/
encode(str) {
const len = str.length;
const res = [];
const len = str.length
const res = []
for (let i = 0; i < len; i++) {
let point = str.charCodeAt(i);
let point = str.charCodeAt(i)
if (point <= 0x007f) {
res.push(point);
res.push(point)
} else if (point <= 0x07ff) {
res.push(0xc0 | (point >>> 6));
res.push(0x80 | (0x3f & point));
res.push(0xc0 | (point >>> 6))
res.push(0x80 | (0x3f & point))
} else if (point <= 0xffff) {
res.push(0xe0 | (point >>> 12));
res.push(0x80 | (0x3f & (point >>> 6)));
res.push(0x80 | (0x3f & point));
res.push(0xe0 | (point >>> 12))
res.push(0x80 | (0x3f & (point >>> 6)))
res.push(0x80 | (0x3f & point))
} else {
point = 0x10000 + ((point - 0xd800) << 10) + (str.charCodeAt(++i) - 0xdc00);
res.push(0xf0 | (point >>> 18));
res.push(0x80 | (0x3f & (point >>> 12)));
res.push(0x80 | (0x3f & (point >>> 6)));
res.push(0x80 | (0x3f & point));
point = 0x10000 + ((point - 0xd800) << 10) + (str.charCodeAt(++i) - 0xdc00)
res.push(0xf0 | (point >>> 18))
res.push(0x80 | (0x3f & (point >>> 12)))
res.push(0x80 | (0x3f & (point >>> 6)))
res.push(0x80 | (0x3f & point))
}
}
return new Uint8Array(res);
return new Uint8Array(res)
}
}
if (typeof globalThis !== 'undefined') {
globalThis.TextEncoder = TextEncoder;
globalThis.TextEncoder = TextEncoder
}
if (typeof global !== 'undefined') {
global.TextEncoder = TextEncoder;
global.TextEncoder = TextEncoder
}
if (typeof window !== 'undefined') {
window.TextEncoder = TextEncoder;
window.TextEncoder = TextEncoder
}
}
......@@ -65,30 +65,30 @@ if (typeof TextDecoder === 'undefined') {
* @returns {string} 解码后的字符串
*/
decode(view, options) {
void options;
void options
if (!view) {
return '';
return ''
}
let string = '';
const arr = new Uint8Array(view);
let string = ''
const arr = new Uint8Array(view)
for (let i = 0; i < arr.length; i++) {
string += String.fromCharCode(arr[i]);
string += String.fromCharCode(arr[i])
}
try {
// 简单的 UTF-8 解码尝试
return decodeURIComponent(escape(string));
return decodeURIComponent(escape(string))
} catch (e) {
return string;
return string
}
}
}
if (typeof globalThis !== 'undefined') {
globalThis.TextDecoder = TextDecoder;
globalThis.TextDecoder = TextDecoder
}
if (typeof global !== 'undefined') {
global.TextDecoder = TextDecoder;
global.TextDecoder = TextDecoder
}
if (typeof window !== 'undefined') {
window.TextDecoder = TextDecoder;
window.TextDecoder = TextDecoder
}
}
......
......@@ -6,7 +6,7 @@
* @Description: 简单axios封装,后续按实际处理
*/
// import axios from 'axios'
import axios from 'axios-miniprogram';
import axios from 'axios-miniprogram'
import Taro from '@tarojs/taro'
// import qs from 'qs'
// import { strExist } from './tools'
......@@ -18,7 +18,7 @@ import { parseQueryString } from './tools'
// import { ProgressStart, ProgressEnd } from '@/components/axios-progress/progress';
// import store from '@/store'
// import { getToken } from '@/utils/auth'
import BASE_URL, { REQUEST_DEFAULT_PARAMS } from './config';
import BASE_URL, { REQUEST_DEFAULT_PARAMS } from './config'
/**
* @description 获取 sessionid 的工具函数
......@@ -28,12 +28,12 @@ import BASE_URL, { REQUEST_DEFAULT_PARAMS } from './config';
*/
export const getSessionId = () => {
try {
return Taro.getStorageSync("sessionid") || null;
return Taro.getStorageSync('sessionid') || null
} catch (error) {
console.error('获取sessionid失败:', error);
return null;
console.error('获取sessionid失败:', error)
return null
}
};
}
/**
* @description 设置 sessionid(一般不需要手动调用)
......@@ -42,9 +42,11 @@ export const getSessionId = () => {
* @param {string} sessionid cookie 字符串
* @returns {void} 无返回值
*/
export const setSessionId = (sessionid) => {
export const setSessionId = sessionid => {
try {
if (!sessionid) return
if (!sessionid) {
return
}
Taro.setStorageSync('sessionid', sessionid)
} catch (error) {
console.error('设置sessionid失败:', error)
......@@ -76,7 +78,7 @@ export const clearSessionId = () => {
const service = axios.create({
baseURL: BASE_URL, // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests
timeout: 5000, // request timeout
timeout: 5000 // request timeout
})
// service.defaults.params = {
......@@ -91,9 +93,11 @@ let has_shown_timeout_modal = false
* @returns {boolean} true=超时,false=非超时
*/
const is_timeout_error = (error) => {
const is_timeout_error = error => {
const msg = String(error?.message || error?.errMsg || '')
if (error?.code === 'ECONNABORTED') return true
if (error?.code === 'ECONNABORTED') {
return true
}
return msg.toLowerCase().includes('timeout')
}
......@@ -102,7 +106,7 @@ const is_timeout_error = (error) => {
* @param {Error} error 请求错误对象
* @returns {boolean} true=网络错误,false=非网络错误
*/
const is_network_error = (error) => {
const is_network_error = error => {
const msg = String(error?.message || error?.errMsg || '')
const raw = (() => {
try {
......@@ -111,14 +115,28 @@ const is_network_error = (error) => {
return ''
}
})()
const lower = (msg + ' ' + raw).toLowerCase()
if (lower.includes('request:fail')) return true
if (lower.includes('request fail')) return true
if (lower.includes('network error')) return true
if (lower.includes('failed to fetch')) return true
if (lower.includes('the internet connection appears to be offline')) return true
if (lower.includes('err_blocked_by_client')) return true
if (lower.includes('blocked_by_client')) return true
const lower = `${msg} ${raw}`.toLowerCase()
if (lower.includes('request:fail')) {
return true
}
if (lower.includes('request fail')) {
return true
}
if (lower.includes('network error')) {
return true
}
if (lower.includes('failed to fetch')) {
return true
}
if (lower.includes('the internet connection appears to be offline')) {
return true
}
if (lower.includes('err_blocked_by_client')) {
return true
}
if (lower.includes('blocked_by_client')) {
return true
}
return false
}
......@@ -129,8 +147,10 @@ const is_network_error = (error) => {
* @param {Error} error 请求错误对象
* @returns {Promise<boolean>} true=需要降级,false=不需要
*/
const should_handle_bad_network = async (error) => {
if (is_timeout_error(error)) return true
const should_handle_bad_network = async error => {
if (is_timeout_error(error)) {
return true
}
return is_network_error(error)
}
......@@ -141,13 +161,17 @@ const should_handle_bad_network = async (error) => {
* @returns {Promise<void>} 无返回值
*/
const handle_request_timeout = async () => {
if (has_shown_timeout_modal) return
if (has_shown_timeout_modal) {
return
}
has_shown_timeout_modal = true
const pages = Taro.getCurrentPages ? Taro.getCurrentPages() : []
const current_page = pages && pages.length ? pages[pages.length - 1] : null
const current_route = current_page?.route || ''
if (String(current_route).includes('pages/offlineBookingList/index')) return
if (String(current_route).includes('pages/offlineBookingList/index')) {
return
}
// 若有离线预约记录缓存,则跳转至离线预约列表页
if (has_offline_booking_cache()) {
......@@ -193,15 +217,15 @@ service.interceptors.request.use(
* - 确保每个请求都带上最新的 sessionid
* - 注意:axios-miniprogram 的 headers 可能不存在,需要先兜底
*/
const sessionid = getSessionId();
const sessionid = getSessionId()
if (sessionid) {
config.headers = config.headers || {}
config.headers.cookie = sessionid;
config.headers.cookie = sessionid
}
// 增加时间戳
if (config.method === 'get') {
config.params = { ...config.params, timestamp: (new Date()).valueOf() }
config.params = { ...config.params, timestamp: new Date().valueOf() }
}
// if ((config.method || '').toLowerCase() === 'post') {
......@@ -253,8 +277,8 @@ service.interceptors.response.use(
* 记录来源页:用于授权成功后回跳
* - 避免死循环:如果已经在 auth 页则不重复记录/跳转
*/
const pages = Taro.getCurrentPages();
const currentPage = pages[pages.length - 1];
const pages = Taro.getCurrentPages()
const currentPage = pages[pages.length - 1]
if (currentPage && currentPage.route !== 'pages/auth/index') {
saveCurrentPagePath()
}
......@@ -266,8 +290,8 @@ service.interceptors.response.use(
return await service(retry_config)
} catch (error) {
// 静默续期失败:降级跳转到授权页(由授权页完成授权并回跳)
const pages_retry = Taro.getCurrentPages();
const current_page_retry = pages_retry[pages_retry.length - 1];
const pages_retry = Taro.getCurrentPages()
const current_page_retry = pages_retry[pages_retry.length - 1]
if (current_page_retry && current_page_retry.route !== 'pages/auth/index') {
navigateToAuth()
}
......@@ -276,7 +300,7 @@ service.interceptors.response.use(
}
if (['预约ID不存在'].includes(res.msg)) {
res.show = false;
res.show = false
}
return response
......
......@@ -6,12 +6,12 @@
* @description 主办方默认用户类型
* @type {Array<string>}
*/
const DEFAULT_HOST_TYPE = ['首次参与', '老用户'];
const DEFAULT_HOST_TYPE = ['首次参与', '老用户']
/**
* @description 主办方默认用户状态
* @type {Array<string>}
*/
const DEFAULT_HOST_STATUS = ['跟踪', '引导'];
const DEFAULT_HOST_STATUS = ['跟踪', '引导']
export { DEFAULT_HOST_TYPE, DEFAULT_HOST_STATUS }
......
......@@ -5,8 +5,8 @@
* @FilePath: /git/xyxBooking-weapp/src/utils/tools.js
* @Description: 工具函数库
*/
import dayjs from 'dayjs';
import Taro from '@tarojs/taro';
import dayjs from 'dayjs'
import Taro from '@tarojs/taro'
import BASE_URL, { REQUEST_DEFAULT_PARAMS } from './config'
/**
......@@ -14,25 +14,25 @@ import BASE_URL, { REQUEST_DEFAULT_PARAMS } from './config'
* @param {string|number|Date} date 时间入参
* @returns {string} 格式化后的时间字符串(YYYY-MM-DD HH:mm)
*/
const formatDate = (date) => {
return dayjs(date).format('YYYY-MM-DD HH:mm');
};
const formatDate = date => {
return dayjs(date).format('YYYY-MM-DD HH:mm')
}
/**
* @description 判断设备信息
* @returns {Object} 设备信息对象,包含是否为 Android、iOS、是否为平板等属性
*/
const wxInfo = () => {
const info = Taro.getSystemInfoSync();
const isAndroid = info.platform === 'android';
const isiOS = info.platform === 'ios';
const info = Taro.getSystemInfoSync()
const isAndroid = info.platform === 'android'
const isiOS = info.platform === 'ios'
// 说明:当前项目只用到 Android/iOS 区分;平板能力按需补充
return {
isAndroid,
isiOS,
isTable: false // 小程序通常不是 tablet 模式,或者可以根据 screenWidth 判断
};
};
}
}
/**
* @description 解析 URL 参数
......@@ -40,14 +40,16 @@ const wxInfo = () => {
* @returns {Object} URL 参数对象(键值对)
*/
const parseQueryString = url => {
if (!url) return {};
var json = {};
var arr = url.indexOf('?') >= 0 ? url.substr(url.indexOf('?') + 1).split('&') : [];
if (!url) {
return {}
}
const json = {}
const arr = url.indexOf('?') >= 0 ? url.substr(url.indexOf('?') + 1).split('&') : []
arr.forEach(item => {
var tmp = item.split('=');
json[tmp[0]] = tmp[1];
});
return json;
const tmp = item.split('=')
json[tmp[0]] = tmp[1]
})
return json
}
/**
......@@ -57,9 +59,13 @@ const parseQueryString = url => {
* @returns {boolean} true=包含任意一个子串,false=都不包含
*/
const strExist = (array, str) => {
if (!str) return false;
if (!str) {
return false
}
const exist = array.filter(arr => {
if (str.indexOf(arr) >= 0) return str;
if (str.indexOf(arr) >= 0) {
return str
}
})
return exist.length > 0
}
......@@ -90,32 +96,35 @@ const strExist = (array, str) => {
* 2. 若 begin_time/end_time 缺失,拼接后可能出现 "undefined-undefined" 等异常,需保证入参完整性;
* 3. 该函数默认截取时间字符串前19位(slice(0, -6)),需根据实际时间格式调整截取长度
*/
const formatDatetime = (data) => {
if (!data || !data.begin_time || !data.end_time) return '';
const formatDatetime = data => {
if (!data || !data.begin_time || !data.end_time) {
return ''
}
const normalize = (timeStr) => {
if (!timeStr) return '';
let clean = timeStr.split('+')[0];
clean = clean.split('Z')[0];
clean = clean.trim().replace(/\s+/, 'T');
return clean;
};
const normalize = timeStr => {
if (!timeStr) {
return ''
}
let clean = timeStr.split('+')[0]
clean = clean.split('Z')[0]
clean = clean.trim().replace(/\s+/, 'T')
return clean
}
const start = dayjs(normalize(data.begin_time));
const end = dayjs(normalize(data.end_time));
const start = dayjs(normalize(data.begin_time))
const end = dayjs(normalize(data.end_time))
if (!start.isValid() || !end.isValid()) return '';
if (!start.isValid() || !end.isValid()) {
return ''
}
const isNextDayMidnight =
end.diff(start, 'day') === 1 &&
end.hour() === 0 &&
end.minute() === 0 &&
end.second() === 0;
end.diff(start, 'day') === 1 && end.hour() === 0 && end.minute() === 0 && end.second() === 0
const endTimeText = isNextDayMidnight ? '24:00' : end.format('HH:mm');
const endTimeText = isNextDayMidnight ? '24:00' : end.format('HH:mm')
return `${start.format('YYYY-MM-DD')} ${start.format('HH:mm')}-${endTimeText}`;
};
return `${start.format('YYYY-MM-DD')} ${start.format('HH:mm')}-${endTimeText}`
}
/**
* @description 证件号脱敏
......@@ -128,7 +137,9 @@ const formatDatetime = (data) => {
*/
const mask_id_number = (id_number, options = {}) => {
const raw = String(id_number || '')
if (!raw) return ''
if (!raw) {
return ''
}
const has_keep_start = Number.isFinite(options.keep_start)
const has_keep_end = Number.isFinite(options.keep_end)
......@@ -137,19 +148,25 @@ const mask_id_number = (id_number, options = {}) => {
const mask_count = Number.isFinite(options.mask_count) ? options.mask_count : 8
if (has_keep_start && has_keep_end) {
if (raw.length <= keep_start + keep_end) return raw
if (raw.length <= keep_start + keep_end) {
return raw
}
const prefix = raw.slice(0, keep_start)
const suffix = raw.slice(raw.length - keep_end)
const middle_len = Math.max(1, raw.length - keep_start - keep_end)
return `${prefix}${'*'.repeat(middle_len)}${suffix}`
}
if (raw.length < 15) return raw
if (raw.length < 15) {
return raw
}
const safe_mask_count = Math.min(Math.max(1, mask_count), raw.length)
const start = Math.floor((raw.length - safe_mask_count) / 2)
const end = start + safe_mask_count
if (start < 0 || end > raw.length) return raw
if (start < 0 || end > raw.length) {
return raw
}
return raw.substring(0, start) + '*'.repeat(safe_mask_count) + raw.substring(end)
}
......@@ -159,12 +176,20 @@ const mask_id_number = (id_number, options = {}) => {
* @param {string|number} status 状态值
* @returns {string} 状态文案
*/
const get_qrcode_status_text = (status) => {
const get_qrcode_status_text = status => {
const key = String(status || '')
if (key === '1') return '未激活'
if (key === '3') return '待使用'
if (key === '5') return '被取消'
if (key === '7') return '已使用'
if (key === '1') {
return '未激活'
}
if (key === '3') {
return '待使用'
}
if (key === '5') {
return '被取消'
}
if (key === '7') {
return '已使用'
}
return '未知状态'
}
......@@ -173,12 +198,20 @@ const get_qrcode_status_text = (status) => {
* @param {string|number} status 状态值
* @returns {string} 状态文案
*/
const get_bill_status_text = (status) => {
const get_bill_status_text = status => {
const key = String(status || '')
if (key === '3') return '预约成功'
if (key === '5') return '已取消'
if (key === '9') return '已使用'
if (key === '11') return '退款中'
if (key === '3') {
return '预约成功'
}
if (key === '5') {
return '已取消'
}
if (key === '9') {
return '已使用'
}
if (key === '11') {
return '退款中'
}
return '未知状态'
}
......@@ -193,9 +226,19 @@ const buildApiUrl = (action, params = {}) => {
a: action,
f: REQUEST_DEFAULT_PARAMS.f,
client_name: REQUEST_DEFAULT_PARAMS.client_name,
...params,
...params
})
return `${BASE_URL}/srv/?${queryParams.toString()}`
}
export { formatDate, wxInfo, parseQueryString, strExist, formatDatetime, mask_id_number, get_qrcode_status_text, get_bill_status_text, buildApiUrl };
export {
formatDate,
wxInfo,
parseQueryString,
strExist,
formatDatetime,
mask_id_number,
get_qrcode_status_text,
get_bill_status_text,
buildApiUrl
}
......
......@@ -14,13 +14,14 @@ export const weak_network_text = {
toast_title: '网络连接不畅',
banner_desc: '网络开小差啦!请检查网络设置,或更换位置后重新进入小程序~',
offline_page_desc: '当前网络信号较弱,已自动为您切换至离线模式',
modal_no_cache_content: '当前网络信号较弱,暂无法使用小程序,请检查网络设置,或更换位置后重新进入小程序~',
modal_no_cache_content:
'当前网络信号较弱,暂无法使用小程序,请检查网络设置,或更换位置后重新进入小程序~',
modal_use_cache_content: '当前网络信号较弱,可使用已缓存的预约记录进入离线模式',
modal_go_offline_records_content: '当前网络信号较弱,是否进入离线预约记录?',
offline_mode_no_booking_toast: '当前为离线模式,无法预约',
confirm_ok: '知道了',
confirm_booking_records: '预约记录',
confirm_offline_records: '离线记录',
confirm_offline_records: '离线记录'
}
/**
......@@ -32,7 +33,7 @@ export const get_weak_network_modal_no_cache_options = () => {
title: weak_network_text.title,
content: weak_network_text.modal_no_cache_content,
confirmText: weak_network_text.confirm_ok,
showCancel: false,
showCancel: false
}
}
......@@ -45,7 +46,7 @@ export const get_weak_network_modal_use_cache_options = () => {
title: weak_network_text.title,
content: weak_network_text.modal_use_cache_content,
confirmText: weak_network_text.confirm_booking_records,
cancelText: weak_network_text.confirm_ok,
cancelText: weak_network_text.confirm_ok
}
}
......@@ -58,6 +59,6 @@ export const get_weak_network_modal_go_offline_records_options = () => {
title: weak_network_text.title,
content: weak_network_text.modal_go_offline_records_content,
confirmText: weak_network_text.confirm_offline_records,
cancelText: weak_network_text.confirm_ok,
cancelText: weak_network_text.confirm_ok
}
}
......
......@@ -4,11 +4,11 @@
*/
const getCurrentPageUrl = () => {
// 获取加载的页面栈
let pages = getCurrentPages()
const pages = getCurrentPages()
// 获取当前页面对象
let currentPage = pages[pages.length - 1]
const currentPage = pages[pages.length - 1]
// 当前页面 route(不含 query)
let url = currentPage.route
const url = currentPage.route
return url
}
/**
......@@ -17,15 +17,12 @@ const getCurrentPageUrl = () => {
*/
const getCurrentPageParam = () => {
// 获取加载的页面栈
let pages = getCurrentPages()
const pages = getCurrentPages()
// 获取当前页面对象
let currentPage = pages[pages.length - 1]
const currentPage = pages[pages.length - 1]
// 当前页面 query 参数对象
let options = currentPage.options
const options = currentPage.options
return options
}
export {
getCurrentPageUrl,
getCurrentPageParam
}
export { getCurrentPageUrl, getCurrentPageParam }
......
......@@ -34,15 +34,15 @@ export const wechat_pay = async ({ pay_id }) => {
const pay_params = pay_params_res?.data || {}
const pay_result = await new Promise((resolve) => {
const pay_result = await new Promise(resolve => {
Taro.requestPayment({
timeStamp: pay_params.timeStamp,
nonceStr: pay_params.nonceStr,
package: pay_params.package,
signType: pay_params.signType,
paySign: pay_params.paySign,
success: (res) => resolve({ ok: true, res }),
fail: (err) => resolve({ ok: false, err }),
success: res => resolve({ ok: true, res }),
fail: err => resolve({ ok: false, err })
})
})
......