From 6b790885172e880c1b91de0d4e6c92602015ef96 Mon Sep 17 00:00:00 2001 From: Mu Yi Date: Thu, 18 Sep 2025 09:26:30 +0800 Subject: [PATCH] feat: init --- .commitlintrc.cjs | 3 + .editorconfig | 13 + .github/release.yml | 31 + .github/workflows/auto-merge.yml | 80 + .github/workflows/release-log.yml | 119 + .gitignore | 44 + .husky/commit-msg | 1 + .husky/pre-commit | 1 + .npmrc | 8 + .vscode/extensions.json | 20 + .vscode/settings.json | 93 + .vscode/vue3.code-snippets | 68 + LICENSE | 21 + README.md | 2 + env/.env | 31 + env/.env.development | 6 + env/.env.production | 6 + env/.env.test | 4 + eslint.config.mjs | 43 + favicon.ico | Bin 0 -> 14575 bytes index.html | 26 + manifest.config.ts | 129 + openapi-ts-request.config.ts | 13 + package.json | 145 + pages.config.ts | 22 + patches/@dcloudio__uni-h5.patch | 13 + pnpm-lock.yaml | 14363 ++++++++++++++++ pnpm-workspace.yaml | 6 + scripts/postupgrade.js | 35 + src/App.vue | 36 + src/api/alova-foo.ts | 17 + src/api/login.ts | 83 + src/api/types/login.ts | 58 + src/components/.gitkeep | 0 src/env.d.ts | 34 + src/hooks/.gitkeep | 0 src/hooks/usePageAuth.ts | 50 + src/hooks/useRequest.ts | 51 + src/hooks/useUpload.ts | 160 + src/interceptors/index.ts | 3 + src/interceptors/prototype.ts | 14 + src/interceptors/request.ts | 70 + src/interceptors/route.ts | 65 + src/layouts/default.vue | 9 + src/layouts/fg-tabbar/fg-tabbar.vue | 68 + src/layouts/fg-tabbar/tabbar.md | 17 + src/layouts/fg-tabbar/tabbar.ts | 11 + src/layouts/fg-tabbar/tabbarList.ts | 60 + src/layouts/tabbar.vue | 19 + src/login-sub/components/LoginMask.vue | 197 + src/login-sub/components/Overlay.vue | 54 + .../components/check-group/Checkbox.vue | 133 + .../components/check-group/CheckboxGroup.vue | 72 + src/login-sub/components/navbar/Navbar.vue | 210 + .../components/radio-group/Radio.vue | 89 + .../components/radio-group/RadioGroup.vue | 50 + src/login-sub/hooks/useUserInfo.ts | 27 + src/login-sub/index.vue | 195 + src/login-sub/privacyPolicy.vue | 7 + src/login-sub/userAgreement.vue | 7 + src/main.ts | 21 + src/manifest.json | 94 + src/pages.json | 105 + src/pages/about/about.vue | 19 + src/pages/about/components/request.vue | 84 + src/pages/about/components/upload.vue | 38 + src/pages/address/index.vue | 34 + src/pages/index/index.vue | 70 + src/pages/payment/index.vue | 63 + src/pages/temporary/index.vue | 67 + src/service/app/displayEnumLabel.ts | 13 + src/service/app/index.ts | 11 + src/service/app/pet.ts | 193 + src/service/app/pet.vuequery.ts | 151 + src/service/app/store.ts | 72 + src/service/app/store.vuequery.ts | 75 + src/service/app/types.ts | 128 + src/service/app/user.ts | 150 + src/service/app/user.vuequery.ts | 149 + src/service/index/api.ts | 17 + src/static/images/.gitkeep | 0 src/static/images/map/map-icon.png | Bin 0 -> 40383 bytes src/static/logo.svg | 33 + src/static/tabbar/example.png | Bin 0 -> 1371 bytes src/static/tabbar/exampleHL.png | Bin 0 -> 1398 bytes src/static/tabbar/home.png | Bin 0 -> 1346 bytes src/static/tabbar/homeHL.png | Bin 0 -> 1415 bytes src/static/tabbar/personal.png | Bin 0 -> 2457 bytes src/static/tabbar/personalHL.png | Bin 0 -> 2534 bytes src/store/index.ts | 17 + src/store/user.ts | 64 + src/style/iconfont.css | 28 + src/style/index.scss | 18 + src/typings.d.ts | 28 + src/typings.ts | 15 + src/uni.scss | 77 + src/uni_modules/.gitkeep | 0 src/utils/dateUtil.ts | 150 + src/utils/http.ts | 120 + src/utils/index.ts | 166 + src/utils/platform.ts | 25 + src/utils/queryString.ts | 29 + src/utils/request.ts | 78 + src/utils/request/alova.ts | 111 + src/utils/request/enum.ts | 66 + src/utils/request/types.ts | 22 + src/utils/toast.ts | 65 + src/utils/tools.ts | 25 + src/utils/uploadFile.ts | 324 + tsconfig.json | 41 + uno.config.ts | 65 + vite-plugins/copyNativeRes.ts | 41 + vite-plugins/updatePackageJson.ts | 37 + vite.config.ts | 185 + 114 files changed, 20596 insertions(+) create mode 100644 .commitlintrc.cjs create mode 100644 .editorconfig create mode 100644 .github/release.yml create mode 100644 .github/workflows/auto-merge.yml create mode 100644 .github/workflows/release-log.yml create mode 100644 .gitignore create mode 100644 .husky/commit-msg create mode 100644 .husky/pre-commit create mode 100644 .npmrc create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/vue3.code-snippets create mode 100644 LICENSE create mode 100644 README.md create mode 100644 env/.env create mode 100644 env/.env.development create mode 100644 env/.env.production create mode 100644 env/.env.test create mode 100644 eslint.config.mjs create mode 100644 favicon.ico create mode 100644 index.html create mode 100644 manifest.config.ts create mode 100644 openapi-ts-request.config.ts create mode 100644 package.json create mode 100644 pages.config.ts create mode 100644 patches/@dcloudio__uni-h5.patch create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100644 scripts/postupgrade.js create mode 100644 src/App.vue create mode 100644 src/api/alova-foo.ts create mode 100644 src/api/login.ts create mode 100644 src/api/types/login.ts create mode 100644 src/components/.gitkeep create mode 100644 src/env.d.ts create mode 100644 src/hooks/.gitkeep create mode 100644 src/hooks/usePageAuth.ts create mode 100644 src/hooks/useRequest.ts create mode 100644 src/hooks/useUpload.ts create mode 100644 src/interceptors/index.ts create mode 100644 src/interceptors/prototype.ts create mode 100644 src/interceptors/request.ts create mode 100644 src/interceptors/route.ts create mode 100644 src/layouts/default.vue create mode 100644 src/layouts/fg-tabbar/fg-tabbar.vue create mode 100644 src/layouts/fg-tabbar/tabbar.md create mode 100644 src/layouts/fg-tabbar/tabbar.ts create mode 100644 src/layouts/fg-tabbar/tabbarList.ts create mode 100644 src/layouts/tabbar.vue create mode 100644 src/login-sub/components/LoginMask.vue create mode 100644 src/login-sub/components/Overlay.vue create mode 100644 src/login-sub/components/check-group/Checkbox.vue create mode 100644 src/login-sub/components/check-group/CheckboxGroup.vue create mode 100644 src/login-sub/components/navbar/Navbar.vue create mode 100644 src/login-sub/components/radio-group/Radio.vue create mode 100644 src/login-sub/components/radio-group/RadioGroup.vue create mode 100644 src/login-sub/hooks/useUserInfo.ts create mode 100644 src/login-sub/index.vue create mode 100644 src/login-sub/privacyPolicy.vue create mode 100644 src/login-sub/userAgreement.vue create mode 100644 src/main.ts create mode 100644 src/manifest.json create mode 100644 src/pages.json create mode 100644 src/pages/about/about.vue create mode 100644 src/pages/about/components/request.vue create mode 100644 src/pages/about/components/upload.vue create mode 100644 src/pages/address/index.vue create mode 100644 src/pages/index/index.vue create mode 100644 src/pages/payment/index.vue create mode 100644 src/pages/temporary/index.vue create mode 100644 src/service/app/displayEnumLabel.ts create mode 100644 src/service/app/index.ts create mode 100644 src/service/app/pet.ts create mode 100644 src/service/app/pet.vuequery.ts create mode 100644 src/service/app/store.ts create mode 100644 src/service/app/store.vuequery.ts create mode 100644 src/service/app/types.ts create mode 100644 src/service/app/user.ts create mode 100644 src/service/app/user.vuequery.ts create mode 100644 src/service/index/api.ts create mode 100644 src/static/images/.gitkeep create mode 100644 src/static/images/map/map-icon.png create mode 100644 src/static/logo.svg create mode 100644 src/static/tabbar/example.png create mode 100644 src/static/tabbar/exampleHL.png create mode 100644 src/static/tabbar/home.png create mode 100644 src/static/tabbar/homeHL.png create mode 100644 src/static/tabbar/personal.png create mode 100644 src/static/tabbar/personalHL.png create mode 100644 src/store/index.ts create mode 100644 src/store/user.ts create mode 100644 src/style/iconfont.css create mode 100644 src/style/index.scss create mode 100644 src/typings.d.ts create mode 100644 src/typings.ts create mode 100644 src/uni.scss create mode 100644 src/uni_modules/.gitkeep create mode 100644 src/utils/dateUtil.ts create mode 100644 src/utils/http.ts create mode 100644 src/utils/index.ts create mode 100644 src/utils/platform.ts create mode 100644 src/utils/queryString.ts create mode 100644 src/utils/request.ts create mode 100644 src/utils/request/alova.ts create mode 100644 src/utils/request/enum.ts create mode 100644 src/utils/request/types.ts create mode 100644 src/utils/toast.ts create mode 100644 src/utils/tools.ts create mode 100644 src/utils/uploadFile.ts create mode 100644 tsconfig.json create mode 100644 uno.config.ts create mode 100644 vite-plugins/copyNativeRes.ts create mode 100644 vite-plugins/updatePackageJson.ts create mode 100644 vite.config.ts diff --git a/.commitlintrc.cjs b/.commitlintrc.cjs new file mode 100644 index 0000000..98ee7df --- /dev/null +++ b/.commitlintrc.cjs @@ -0,0 +1,3 @@ +module.exports = { + extends: ['@commitlint/config-conventional'], +} diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7f09864 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] # 表示所有文件适用 +charset = utf-8 # 设置文件字符集为 utf-8 +indent_style = space # 缩进风格(tab | space) +indent_size = 2 # 缩进大小 +end_of_line = lf # 控制换行类型(lf | cr | crlf) +trim_trailing_whitespace = true # 去除行首的任意空白字符 +insert_final_newline = true # 始终在文件末尾插入一个新行 + +[*.md] # 表示仅 md 文件适用以下规则 +max_line_length = off # 关闭最大行长度限制 +trim_trailing_whitespace = false # 关闭末尾空格修剪 diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..6ae23b0 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,31 @@ +categories: + - title: 🚀 新功能 + labels: [feat, feature] + - title: 🛠️ 修复 + labels: [fix, bugfix] + - title: 💅 样式 + labels: [style] + - title: 📄 文档 + labels: [docs] + - title: ⚡️ 性能 + labels: [perf] + - title: 🧪 测试 + labels: [test] + - title: ♻️ 重构 + labels: [refactor] + - title: 📦 构建 + labels: [build] + - title: 🚨 补丁 + labels: [patch, hotfix] + - title: 🌐 发布 + labels: [release, publish] + - title: 🔧 流程 + labels: [ci, cd, workflow] + - title: ⚙️ 配置 + labels: [config, chore] + - title: 📁 文件 + labels: [file] + - title: 🎨 格式化 + labels: [format] + - title: 🔀 其他 + labels: [other, misc] diff --git a/.github/workflows/auto-merge.yml b/.github/workflows/auto-merge.yml new file mode 100644 index 0000000..b1ea5c2 --- /dev/null +++ b/.github/workflows/auto-merge.yml @@ -0,0 +1,80 @@ +name: Auto Merge Main to Other Branches + +on: + push: + branches: + - main + workflow_dispatch: # 手动触发 + +jobs: + merge-to-i18n: + name: Merge main into i18n + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GH_TOKEN_AUTO_MERGE }} + + - name: Merge main into i18n + run: | + git config user.name "GitHub Actions" + git config user.email "actions@github.com" + git checkout i18n + git merge main --no-ff -m "Auto merge main into i18n" + git push origin i18n + + merge-to-base-sard-ui: + name: Merge main into base-sard-ui + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GH_TOKEN_AUTO_MERGE }} + + - name: Merge main into base-sard-ui + run: | + git config user.name "GitHub Actions" + git config user.email "actions@github.com" + git checkout base-sard-ui + git merge main --no-ff -m "Auto merge main into base-sard-ui" + git push origin base-sard-ui + + merge-to-base-uv-ui: + name: Merge main into base-uv-ui + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GH_TOKEN_AUTO_MERGE }} + + - name: Merge main into base-uv-ui + run: | + git config user.name "GitHub Actions" + git config user.email "actions@github.com" + git checkout base-uv-ui + git merge main --no-ff -m "Auto merge main into base-uv-ui" + git push origin base-uv-ui + + merge-to-base-uview-plus: + name: Merge main into base-uview-plus + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GH_TOKEN_AUTO_MERGE }} + + - name: Merge main into base-uview-plus + run: | + git config user.name "GitHub Actions" + git config user.email "actions@github.com" + git checkout base-uview-plus + git merge main --no-ff -m "Auto merge main into base-uview-plus" + git push origin base-uview-plus diff --git a/.github/workflows/release-log.yml b/.github/workflows/release-log.yml new file mode 100644 index 0000000..c2887ab --- /dev/null +++ b/.github/workflows/release-log.yml @@ -0,0 +1,119 @@ +name: Auto Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + pull-requests: read + issues: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install yq + run: sudo snap install yq + + - name: Generate changelog + id: changelog + env: + CONFIG_FILE: .github/release.yml + run: | + # 解析配置文件 + declare -A category_map + while IFS=";" read -r title labels; do + for label in $labels; do + category_map[$label]="$title" + done + done < <(yq -o=tsv '.categories[] | [.title, (.labels | join(" "))] | join(";")' $CONFIG_FILE) + # 获取版本范围 + mapfile -t tags < <(git tag -l --sort=-version:refname) + current_tag=${tags[0]} + previous_tag=${tags[1]:-} + if [[ -z "$previous_tag" ]]; then + commit_range="$current_tag" + echo "首次发布版本: $current_tag" + else + commit_range="$previous_tag..$current_tag" + echo "版本范围: $commit_range" + fi + # 获取所有符合规范的提交 + commits=$(git log --pretty=format:"%s|%h" "$commit_range") + # 生成分类日志 + declare -A log_entries + while IFS="|" read -r subject hash; do + # type=$(echo "$subject" | cut -d':' -f1 | tr -d ' ') + type=$(echo "$subject" | sed -E 's/^([[:alnum:]]+)(\(.*\))?:.*/\1/' | tr -d ' ') + found=0 + for label in "${!category_map[@]}"; do + if [[ "$type" == "$label" ]]; then + entry="- ${subject} (${hash:0:7})" + log_entries[${category_map[$label]}]+="$entry"$'\n' + found=1 + break + fi + done + if [[ $found -eq 0 ]]; then + entry="- ${subject} (${hash:0:7})" + log_entries["其他"]+="$entry"$'\n' + fi + done <<< "$commits" + + # 统计提交数量 + commit_count=$(git log --oneline "$commit_range" | wc -l) + # 统计受影响的文件数量 + file_count=$(git diff --name-only "$commit_range" | wc -l) + # 统计贡献者信息 + contributor_stats=$(git shortlog -sn "$commit_range") + contributor_notes="" + while IFS= read -r line; do + commits=$(echo "$line" | awk '{print $1}') + name=$(echo "$line" | awk '{$1=""; print $0}' | sed 's/^ //') + contributor_notes+="- @${name} (${commits} commits)\n" + done <<< "$contributor_stats" + # 构建输出内容 + release_notes="## 版本更新日志 ($current_tag)\n\n" + while IFS= read -r category; do + if [[ -n "${log_entries[$category]}" ]]; then + release_notes+="### $category\n${log_entries[$category]}\n" + fi + done < <(yq '.categories[].title' $CONFIG_FILE) + # 构建输出内容 + release_notes="## 版本更新日志 ($current_tag)\n\n" + current_date=$(date +"%Y-%m-%d") + # 添加发布日期和下载统计信息 + release_notes+=" ### 📅 发布日期: ${current_date}\n" + while IFS= read -r category; do + if [[ -n "${log_entries[$category]}" ]]; then + release_notes+="### $category\n${log_entries[$category]}\n" + fi + done < <(yq '.categories[].title' $CONFIG_FILE) + + # 添加统计信息 + release_notes+="### 📊 统计信息\n" + release_notes+="- 本次发布包含 ${commit_count} 个提交\n" + release_notes+="- 影响 ${file_count} 个文件\n\n" + # 添加贡献者信息 + release_notes+="### 👥 贡献者\n" + release_notes+="感谢这些优秀的贡献者(按提交次数排序):\n" + release_notes+="${contributor_notes}\n" + release_notes+="---\n" + # 写入文件 + echo -e "$release_notes" > changelog.md + echo "生成日志内容:" + cat changelog.md + - name: Create Release + uses: ncipollo/release-action@v1 + with: + generateReleaseNotes: false + bodyFile: changelog.md + tag: ${{ github.ref_name }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..34ddbdf --- /dev/null +++ b/.gitignore @@ -0,0 +1,44 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +.DS_Store +dist +*.local + +# Editor directories and files +.idea +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +.hbuilderx + +.stylelintcache +.eslintcache + +docs/.vitepress/dist +docs/.vitepress/cache + +src/types + +# lock 文件还是不要了,我主要的版本写死就好了 +# pnpm-lock.yaml +# package-lock.json + +# TIPS:如果某些文件已经加入了版本管理,现在重新加入 .gitignore 是不生效的,需要执行下面的操作 +# `git rm -r --cached .` 然后提交 commit 即可。 + +# git rm -r --cached file1 file2 ## 针对某些文件 +# git rm -r --cached dir1 dir2 ## 针对某些文件夹 +# git rm -r --cached . ## 针对所有文件 + +# 更新 uni-app 官方版本 +# npx @dcloudio/uvm@latest diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100644 index 0000000..36158d9 --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1 @@ +npx --no-install commitlint --edit "$1" \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..c3ec64b --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npx lint-staged --allow-empty \ No newline at end of file diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..10ecfe2 --- /dev/null +++ b/.npmrc @@ -0,0 +1,8 @@ +# registry = https://registry.npmjs.org +registry = https://registry.npmmirror.com + +strict-peer-dependencies=false +auto-install-peers=true +shamefully-hoist=true +ignore-workspace-root-check=true +install-workspace-root=true diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..178c449 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,20 @@ +{ + "recommendations": [ + "vue.volar", + "stylelint.vscode-stylelint", + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "antfu.unocss", + "antfu.iconify", + "evils.uniapp-vscode", + "uni-helper.uni-helper-vscode", + "uni-helper.uni-app-schemas-vscode", + "uni-helper.uni-highlight-vscode", + "uni-helper.uni-ui-snippets-vscode", + "uni-helper.uni-app-snippets-vscode", + "mrmlnc.vscode-json5", + "streetsidesoftware.code-spell-checker", + "foxundermoon.shell-format", + "christian-kohler.path-intellisense" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..35db1c9 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,93 @@ +{ + // 配置语言的文件关联 + "files.associations": { + "pages.json": "jsonc", // pages.json 可以写注释 + "manifest.json": "jsonc" // manifest.json 可以写注释 + }, + + "stylelint.enable": false, // 禁用 stylelint + "css.validate": false, // 禁用 CSS 内置验证 + "scss.validate": false, // 禁用 SCSS 内置验证 + "less.validate": false, // 禁用 LESS 内置验证 + + "typescript.tsdk": "node_modules\\typescript\\lib", + "explorer.fileNesting.enabled": false, + "explorer.fileNesting.expand": false, + "explorer.fileNesting.patterns": { + "README.md": "index.html,favicon.ico,robots.txt,CHANGELOG.md", + "pages.config.ts": "manifest.config.ts,openapi-ts-request.config.ts", + "package.json": "tsconfig.json,pnpm-lock.yaml,pnpm-workspace.yaml,LICENSE,.gitattributes,.gitignore,.gitpod.yml,CNAME,.npmrc,.browserslistrc", + "eslint.config.mjs": ".commitlintrc.*,.prettier*,.editorconfig,.commitlint.cjs,.eslint*" + }, + + // Disable the default formatter, use eslint instead + "prettier.enable": false, + "editor.formatOnSave": false, + + // Auto fix + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit", + "source.organizeImports": "never" + }, + + // Silent the stylistic rules in you IDE, but still auto fix them + "eslint.rules.customizations": [ + // { "rule": "style/*", "severity": "off", "fixable": true }, + // { "rule": "format/*", "severity": "off", "fixable": true }, + // { "rule": "*-indent", "severity": "off", "fixable": true }, + // { "rule": "*-spacing", "severity": "off", "fixable": true }, + // { "rule": "*-spaces", "severity": "off", "fixable": true }, + // { "rule": "*-order", "severity": "off", "fixable": true }, + // { "rule": "*-dangle", "severity": "off", "fixable": true }, + // { "rule": "*-newline", "severity": "off", "fixable": true }, + // { "rule": "*quotes", "severity": "off", "fixable": true }, + // { "rule": "*semi", "severity": "off", "fixable": true } + ], + + // Enable eslint for all supported languages + "eslint.validate": [ + "javascript", + "javascriptreact", + "typescript", + "typescriptreact", + "vue", + "html", + "markdown", + "json", + "json5", + "jsonc", + "yaml", + "toml", + "xml", + "gql", + "graphql", + "astro", + "svelte", + "css", + "less", + "scss", + "pcss", + "postcss" + ], + "cSpell.words": [ + "alova", + "Aplipay", + "climblee", + "commitlint", + "dcloudio", + "iconfont", + "oxlint", + "qrcode", + "refresherrefresh", + "scrolltolower", + "tabbar", + "Toutiao", + "uniapp", + "unibest", + "uview", + "uvui", + "Wechat", + "WechatMiniprogram", + "Weixin" + ] +} diff --git a/.vscode/vue3.code-snippets b/.vscode/vue3.code-snippets new file mode 100644 index 0000000..51bab9f --- /dev/null +++ b/.vscode/vue3.code-snippets @@ -0,0 +1,68 @@ +{ + // Place your unibest 工作区 snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and + // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope + // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is + // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: + // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. + // Placeholders with the same ids are connected. + // Example: + // "Print to console": { + // "scope": "javascript,typescript", + // "prefix": "log", + // "body": [ + // "console.log('$1');", + // "$2" + // ], + // "description": "Log output to console" + // } + "Print unibest Vue3 SFC": { + "scope": "vue", + "prefix": "v3", + "body": [ + "", + "{", + " layout: 'default',", + " style: {", + " navigationBarTitleText: '$1',", + " },", + "}", + "\n", + "\n", + "\n", + "\n", + ], + }, + "Print unibest style": { + "scope": "vue", + "prefix": "st", + "body": [ + "\n" + ], + }, + "Print unibest script": { + "scope": "vue", + "prefix": "sc", + "body": [ + "\n" + ], + }, + "Print unibest template": { + "scope": "vue", + "prefix": "te", + "body": [ + "\n" + ], + }, +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9e91d10 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 菲鸽 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..dde5c9c --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +## 康养小程序端(外壳) + diff --git a/env/.env b/env/.env new file mode 100644 index 0000000..fbce61c --- /dev/null +++ b/env/.env @@ -0,0 +1,31 @@ +VITE_APP_TITLE = '康乐云家' +VITE_APP_PORT = 9000 + +VITE_UNI_APPID = '__UNI__D1E5001' +VITE_WX_APPID = 'wxe173e798fc5a9f02' + +# h5部署网站的base,配置到 manifest.config.ts 里的 h5.router.base +VITE_APP_PUBLIC_BASE=/ + +# 登录页面 +VITE_LOGIN_URL = '/pages/login/index' +# 第一个请求地址 +VITE_SERVER_BASEURL = 'https://ukw0y1.laf.run' +# 第二个请求地址 +VITE_API_SECONDARY_URL = 'https://ukw0y1.laf.run' + +VITE_UPLOAD_BASEURL = 'https://ukw0y1.laf.run/upload' + +# 有些同学可能需要在微信小程序里面根据 develop、trial、release 分别设置上传地址,参考代码如下。 +# 下面的变量如果没有设置,会默认使用 VITE_SERVER_BASEURL or VITE_UPLOAD_BASEURL +VITE_SERVER_BASEURL__WEIXIN_DEVELOP = 'https://ukw0y1.laf.run' +VITE_SERVER_BASEURL__WEIXIN_TRIAL = 'https://ukw0y1.laf.run' +VITE_SERVER_BASEURL__WEIXIN_RELEASE = 'https://ukw0y1.laf.run' + +VITE_UPLOAD_BASEURL__WEIXIN_DEVELOP = 'https://ukw0y1.laf.run/upload' +VITE_UPLOAD_BASEURL__WEIXIN_TRIAL = 'https://ukw0y1.laf.run/upload' +VITE_UPLOAD_BASEURL__WEIXIN_RELEASE = 'https://ukw0y1.laf.run/upload' + +# h5是否需要配置代理 +VITE_APP_PROXY=false +VITE_APP_PROXY_PREFIX = '/api' diff --git a/env/.env.development b/env/.env.development new file mode 100644 index 0000000..04fa273 --- /dev/null +++ b/env/.env.development @@ -0,0 +1,6 @@ +# 变量必须以 VITE_ 为前缀才能暴露给外部读取 +NODE_ENV = 'development' +# 是否去除console 和 debugger +VITE_DELETE_CONSOLE = false +# 是否开启sourcemap +VITE_SHOW_SOURCEMAP = true diff --git a/env/.env.production b/env/.env.production new file mode 100644 index 0000000..8a1b50c --- /dev/null +++ b/env/.env.production @@ -0,0 +1,6 @@ +# 变量必须以 VITE_ 为前缀才能暴露给外部读取 +NODE_ENV = 'development' +# 是否去除console 和 debugger +VITE_DELETE_CONSOLE = true +# 是否开启sourcemap +VITE_SHOW_SOURCEMAP = false diff --git a/env/.env.test b/env/.env.test new file mode 100644 index 0000000..e22f765 --- /dev/null +++ b/env/.env.test @@ -0,0 +1,4 @@ +# 变量必须以 VITE_ 为前缀才能暴露给外部读取 +NODE_ENV = 'development' +# 是否去除console 和 debugger +VITE_DELETE_CONSOLE = false diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..54ce246 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,43 @@ +import uniHelper from '@uni-helper/eslint-config' + +export default uniHelper({ + unocss: true, + vue: true, + markdown: false, + ignores: [ + 'src/uni_modules/', + 'dist', + // unplugin-auto-import 生成的类型文件,每次提交都改变,所以加入这里吧,与 .gitignore 配合使用 + 'auto-import.d.ts', + // vite-plugin-uni-pages 生成的类型文件,每次切换分支都一堆不同的,所以直接 .gitignore + 'uni-pages.d.ts', + // 插件生成的文件 + 'src/pages.json', + 'src/manifest.json', + // 忽略自动生成文件 + 'src/service/app/**', + ], + rules: { + 'no-console': 'off', + 'no-unused-vars': 'off', + 'vue/no-unused-refs': 'off', + 'unused-imports/no-unused-vars': 'off', + 'eslint-comments/no-unlimited-disable': 'off', + 'jsdoc/check-param-names': 'off', + 'jsdoc/require-returns-description': 'off', + 'ts/no-empty-object-type': 'off', + 'no-extend-native': 'off', + }, + formatters: { + /** + * Format CSS, LESS, SCSS files, also the ` diff --git a/src/api/alova-foo.ts b/src/api/alova-foo.ts new file mode 100644 index 0000000..93a9eb4 --- /dev/null +++ b/src/api/alova-foo.ts @@ -0,0 +1,17 @@ +import { API_DOMAINS, http } from '@/utils/request/alova' + +export interface IFoo { + id: number + name: string +} + +export function foo() { + return http.Get('/foo', { + params: { + name: '菲鸽', + page: 1, + pageSize: 10, + }, + meta: { domain: API_DOMAINS.SECONDARY }, // 用于切换请求地址 + }) +} diff --git a/src/api/login.ts b/src/api/login.ts new file mode 100644 index 0000000..9732650 --- /dev/null +++ b/src/api/login.ts @@ -0,0 +1,83 @@ +import type { ICaptcha, IUpdateInfo, IUpdatePassword, IUserInfoVo, IUserLogin } from './types/login' +import { http } from '@/utils/http' + +/** + * 登录表单 + */ +export interface ILoginForm { + username: string + password: string + code: string + uuid: string +} + +/** + * 获取验证码 + * @returns ICaptcha 验证码 + */ +export function getCode() { + return http.get('/user/getCode') +} + +/** + * 用户登录 + * @param loginForm 登录表单 + */ +export function login(loginForm: ILoginForm) { + return http.post('/user/login', loginForm) +} + +/** + * 获取用户信息 + */ +export function getUserInfo() { + return http.get('/user/info') +} + +/** + * 退出登录 + */ +export function logout() { + return http.get('/user/logout') +} + +/** + * 修改用户信息 + */ +export function updateInfo(data: IUpdateInfo) { + return http.post('/user/updateInfo', data) +} + +/** + * 修改用户密码 + */ +export function updateUserPassword(data: IUpdatePassword) { + return http.post('/user/updatePassword', data) +} + +/** + * 获取微信登录凭证 + * @returns Promise 包含微信登录凭证(code) + */ +export function getWxCode() { + return new Promise((resolve, reject) => { + uni.login({ + provider: 'weixin', + success: res => resolve(res), + fail: err => reject(new Error(err)), + }) + }) +} + +/** + * 微信登录参数 + */ + +/** + * 微信登录 + * @param params 微信登录参数,包含code + * @returns Promise 包含登录结果 + */ +export function wxLogin(data: { code: string }) { + return http.post('/user/wxLogin', data) +} diff --git a/src/api/types/login.ts b/src/api/types/login.ts new file mode 100644 index 0000000..aee49a7 --- /dev/null +++ b/src/api/types/login.ts @@ -0,0 +1,58 @@ +/** + * 用户信息 + */ +export interface IUserInfoVo { + id: number + username: string + avatar: string + token: string + openId: string +} + +/** + * 登录返回的信息 + */ +export interface IUserLogin { + id: string + username: string + token: string +} + +/** + * 获取验证码 + */ +export interface ICaptcha { + captchaEnabled: boolean + uuid: string + image: string +} +/** + * 上传成功的信息 + */ +export interface IUploadSuccessInfo { + fileId: number + originalName: string + fileName: string + storagePath: string + fileHash: string + fileType: string + fileBusinessType: string + fileSize: number +} +/** + * 更新用户信息 + */ +export interface IUpdateInfo { + id: number + name: string + sex: string +} +/** + * 更新用户信息 + */ +export interface IUpdatePassword { + id: number + oldPassword: string + newPassword: string + confirmPassword: string +} diff --git a/src/components/.gitkeep b/src/components/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 0000000..b4a2c97 --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1,34 @@ +/// +/// + +declare module '*.vue' { + import type { DefineComponent } from 'vue' + + const component: DefineComponent<{}, {}, any> + export default component +} + +interface ImportMetaEnv { + /** 网站标题,应用名称 */ + readonly VITE_APP_TITLE: string + /** 服务端口号 */ + readonly VITE_SERVER_PORT: string + /** 后台接口地址 */ + readonly VITE_SERVER_BASEURL: string + /** H5是否需要代理 */ + readonly VITE_APP_PROXY: 'true' | 'false' + /** H5是否需要代理,需要的话有个前缀 */ + readonly VITE_APP_PROXY_PREFIX: string // 一般是/api + /** 上传图片地址 */ + readonly VITE_UPLOAD_BASEURL: string + /** 是否清除console */ + readonly VITE_DELETE_CONSOLE: string + // 更多环境变量... +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} + +declare const __VITE_APP_PROXY__: 'true' | 'false' +declare const __UNI_PLATFORM__: 'app' | 'h5' | 'mp-alipay' | 'mp-baidu' | 'mp-kuaishou' | 'mp-lark' | 'mp-qq' | 'mp-tiktok' | 'mp-weixin' | 'mp-xiaochengxu' diff --git a/src/hooks/.gitkeep b/src/hooks/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/hooks/usePageAuth.ts b/src/hooks/usePageAuth.ts new file mode 100644 index 0000000..fd006c8 --- /dev/null +++ b/src/hooks/usePageAuth.ts @@ -0,0 +1,50 @@ +import { onLoad } from '@dcloudio/uni-app' +import { useUserStore } from '@/store' +import { needLoginPages as _needLoginPages, getNeedLoginPages } from '@/utils' + +const loginRoute = import.meta.env.VITE_LOGIN_URL +const isDev = import.meta.env.DEV +function isLogined() { + const userStore = useUserStore() + return !!userStore.userInfo.username +} +// 检查当前页面是否需要登录 +export function usePageAuth() { + onLoad((options) => { + // 获取当前页面路径 + const pages = getCurrentPages() + const currentPage = pages[pages.length - 1] + const currentPath = `/${currentPage.route}` + + // 获取需要登录的页面列表 + let needLoginPages: string[] = [] + if (isDev) { + needLoginPages = getNeedLoginPages() + } + else { + needLoginPages = _needLoginPages + } + + // 检查当前页面是否需要登录 + const isNeedLogin = needLoginPages.includes(currentPath) + if (!isNeedLogin) { + return + } + + const hasLogin = isLogined() + if (hasLogin) { + return true + } + + // 构建重定向URL + const queryString = Object.entries(options || {}) + .map(([key, value]) => `${key}=${encodeURIComponent(String(value))}`) + .join('&') + + const currentFullPath = queryString ? `${currentPath}?${queryString}` : currentPath + const redirectRoute = `${loginRoute}?redirect=${encodeURIComponent(currentFullPath)}` + + // 重定向到登录页 + uni.redirectTo({ url: redirectRoute }) + }) +} diff --git a/src/hooks/useRequest.ts b/src/hooks/useRequest.ts new file mode 100644 index 0000000..017a710 --- /dev/null +++ b/src/hooks/useRequest.ts @@ -0,0 +1,51 @@ +import type { Ref } from 'vue' + +interface IUseRequestOptions { + /** 是否立即执行 */ + immediate?: boolean + /** 初始化数据 */ + initialData?: T +} + +interface IUseRequestReturn { + loading: Ref + error: Ref + data: Ref + run: () => Promise +} + +/** + * useRequest是一个定制化的请求钩子,用于处理异步请求和响应。 + * @param func 一个执行异步请求的函数,返回一个包含响应数据的Promise。 + * @param options 包含请求选项的对象 {immediate, initialData}。 + * @param options.immediate 是否立即执行请求,默认为false。 + * @param options.initialData 初始化数据,默认为undefined。 + * @returns 返回一个对象{loading, error, data, run},包含请求的加载状态、错误信息、响应数据和手动触发请求的函数。 + */ +export default function useRequest( + func: () => Promise>, + options: IUseRequestOptions = { immediate: false }, +): IUseRequestReturn { + const loading = ref(false) + const error = ref(false) + const data = ref(options.initialData) as Ref + const run = async () => { + loading.value = true + return func() + .then((res) => { + data.value = res.data + error.value = false + return data.value + }) + .catch((err) => { + error.value = err + throw err + }) + .finally(() => { + loading.value = false + }) + } + + options.immediate && run() + return { loading, error, data, run } +} diff --git a/src/hooks/useUpload.ts b/src/hooks/useUpload.ts new file mode 100644 index 0000000..3080d5a --- /dev/null +++ b/src/hooks/useUpload.ts @@ -0,0 +1,160 @@ +import { ref } from 'vue' +import { getEnvBaseUploadUrl } from '@/utils' + +const VITE_UPLOAD_BASEURL = `${getEnvBaseUploadUrl()}` + +type TfileType = 'image' | 'file' +type TImage = 'png' | 'jpg' | 'jpeg' | 'webp' | '*' +type TFile = 'doc' | 'docx' | 'ppt' | 'zip' | 'xls' | 'xlsx' | 'txt' | TImage + +interface TOptions { + formData?: Record + maxSize?: number + accept?: T extends 'image' ? TImage[] : TFile[] + fileType?: T + success?: (params: any) => void + error?: (err: any) => void +} + +export default function useUpload(options: TOptions = {} as TOptions) { + const { + formData = {}, + maxSize = 5 * 1024 * 1024, + accept = ['*'], + fileType = 'image', + success, + error: onError, + } = options + + const loading = ref(false) + const error = ref(null) + const data = ref(null) + + const handleFileChoose = ({ tempFilePath, size }: { tempFilePath: string, size: number }) => { + if (size > maxSize) { + uni.showToast({ + title: `文件大小不能超过 ${maxSize / 1024 / 1024}MB`, + icon: 'none', + }) + return + } + + // const fileExtension = file?.tempFiles?.name?.split('.').pop()?.toLowerCase() + // const isTypeValid = accept.some((type) => type === '*' || type.toLowerCase() === fileExtension) + + // if (!isTypeValid) { + // uni.showToast({ + // title: `仅支持 ${accept.join(', ')} 格式的文件`, + // icon: 'none', + // }) + // return + // } + + loading.value = true + uploadFile({ + tempFilePath, + formData, + onSuccess: (res) => { + const { data: _data } = JSON.parse(res) + data.value = _data + // console.log('上传成功', res) + success?.(_data) + }, + onError: (err) => { + error.value = err + onError?.(err) + }, + onComplete: () => { + loading.value = false + }, + }) + } + + const run = () => { + // 微信小程序从基础库 2.21.0 开始, wx.chooseImage 停止维护,请使用 uni.chooseMedia 代替。 + // 微信小程序在2023年10月17日之后,使用本API需要配置隐私协议 + const chooseFileOptions = { + count: 1, + success: (res: any) => { + console.log('File selected successfully:', res) + // 小程序中res:{errMsg: "chooseImage:ok", tempFiles: [{fileType: "image", size: 48976, tempFilePath: "http://tmp/5iG1WpIxTaJf3ece38692a337dc06df7eb69ecb49c6b.jpeg"}]} + // h5中res:{errMsg: "chooseImage:ok", tempFilePaths: "blob:http://localhost:9000/f74ab6b8-a14d-4cb6-a10d-fcf4511a0de5", tempFiles: [File]} + // h5的File有以下字段:{name: "girl.jpeg", size: 48976, type: "image/jpeg"} + // App中res:{errMsg: "chooseImage:ok", tempFilePaths: "file:///Users/feige/xxx/gallery/1522437259-compressed-IMG_0006.jpg", tempFiles: [File]} + // App的File有以下字段:{path: "file:///Users/feige/xxx/gallery/1522437259-compressed-IMG_0006.jpg", size: 48976} + let tempFilePath = '' + let size = 0 + // #ifdef MP-WEIXIN + tempFilePath = res.tempFiles[0].tempFilePath + size = res.tempFiles[0].size + // #endif + // #ifndef MP-WEIXIN + tempFilePath = res.tempFilePaths[0] + size = res.tempFiles[0].size + // #endif + handleFileChoose({ tempFilePath, size }) + }, + fail: (err: any) => { + console.error('File selection failed:', err) + error.value = err + onError?.(err) + }, + } + + if (fileType === 'image') { + // #ifdef MP-WEIXIN + uni.chooseMedia({ + ...chooseFileOptions, + mediaType: ['image'], + }) + // #endif + + // #ifndef MP-WEIXIN + uni.chooseImage(chooseFileOptions) + // #endif + } + else { + uni.chooseFile({ + ...chooseFileOptions, + type: 'all', + }) + } + } + + return { loading, error, data, run } +} + +async function uploadFile({ + tempFilePath, + formData, + onSuccess, + onError, + onComplete, +}: { + tempFilePath: string + formData: Record + onSuccess: (data: any) => void + onError: (err: any) => void + onComplete: () => void +}) { + uni.uploadFile({ + url: VITE_UPLOAD_BASEURL, + filePath: tempFilePath, + name: 'file', + formData, + success: (uploadFileRes) => { + try { + const data = uploadFileRes.data + onSuccess(data) + } + catch (err) { + onError(err) + } + }, + fail: (err) => { + console.error('Upload failed:', err) + onError(err) + }, + complete: onComplete, + }) +} diff --git a/src/interceptors/index.ts b/src/interceptors/index.ts new file mode 100644 index 0000000..786c44f --- /dev/null +++ b/src/interceptors/index.ts @@ -0,0 +1,3 @@ +export { prototypeInterceptor } from './prototype' +export { requestInterceptor } from './request' +export { routeInterceptor } from './route' diff --git a/src/interceptors/prototype.ts b/src/interceptors/prototype.ts new file mode 100644 index 0000000..647e6bd --- /dev/null +++ b/src/interceptors/prototype.ts @@ -0,0 +1,14 @@ +export const prototypeInterceptor = { + install() { + // 解决低版本手机不识别 array.at() 导致运行报错的问题 + if (typeof Array.prototype.at !== 'function') { + Array.prototype.at = function (index: number) { + if (index < 0) + return this[this.length + index] + if (index >= this.length) + return undefined + return this[index] + } + } + }, +} diff --git a/src/interceptors/request.ts b/src/interceptors/request.ts new file mode 100644 index 0000000..48cbf09 --- /dev/null +++ b/src/interceptors/request.ts @@ -0,0 +1,70 @@ +import { useUserStore } from '@/store' +import { getEnvBaseUrl } from '@/utils' +import { platform } from '@/utils/platform' +import { stringifyQuery } from '@/utils/queryString' + +export type CustomRequestOptions = UniApp.RequestOptions & { + query?: Record + /** 出错时是否隐藏错误提示 */ + hideErrorToast?: boolean +} & IUniUploadFileOptions // 添加uni.uploadFile参数类型 + +// 请求基准地址 +const baseUrl = getEnvBaseUrl() + +// 拦截器配置 +const httpInterceptor = { + // 拦截前触发 + invoke(options: CustomRequestOptions) { + // 接口请求支持通过 query 参数配置 queryString + if (options.query) { + const queryStr = stringifyQuery(options.query) + if (options.url.includes('?')) { + options.url += `&${queryStr}` + } + else { + options.url += `?${queryStr}` + } + } + // 非 http 开头需拼接地址 + if (!options.url.startsWith('http')) { + // #ifdef H5 + // console.log(__VITE_APP_PROXY__) + if (JSON.parse(__VITE_APP_PROXY__)) { + // 自动拼接代理前缀 + options.url = import.meta.env.VITE_APP_PROXY_PREFIX + options.url + } + else { + options.url = baseUrl + options.url + } + // #endif + // 非H5正常拼接 + // #ifndef H5 + options.url = baseUrl + options.url + // #endif + // TIPS: 如果需要对接多个后端服务,也可以在这里处理,拼接成所需要的地址 + } + // 1. 请求超时 + options.timeout = 10000 // 10s + // 2. (可选)添加小程序端请求头标识 + options.header = { + platform, // 可选,与 uniapp 定义的平台一致,告诉后台来源 + ...options.header, + } + // 3. 添加 token 请求头标识 + const userStore = useUserStore() + const { token } = userStore.userInfo as unknown as IUserInfo + if (token) { + options.header.Authorization = `Bearer ${token}` + } + }, +} + +export const requestInterceptor = { + install() { + // 拦截 request 请求 + uni.addInterceptor('request', httpInterceptor) + // 拦截 uploadFile 文件上传 + uni.addInterceptor('uploadFile', httpInterceptor) + }, +} diff --git a/src/interceptors/route.ts b/src/interceptors/route.ts new file mode 100644 index 0000000..70eff89 --- /dev/null +++ b/src/interceptors/route.ts @@ -0,0 +1,65 @@ +/** + * by 菲鸽 on 2024-03-06 + * 路由拦截,通常也是登录拦截 + * 可以设置路由白名单,或者黑名单,看业务需要选哪一个 + * 我这里应为大部分都可以随便进入,所以使用黑名单 + */ +import { useUserStore } from '@/store' +import { needLoginPages as _needLoginPages, getLastPage, getNeedLoginPages } from '@/utils' + +// TODO Check +const loginRoute = import.meta.env.VITE_LOGIN_URL + +function isLogined() { + const userStore = useUserStore() + return !!userStore.userInfo.username +} + +const isDev = import.meta.env.DEV + +// 黑名单登录拦截器 - (适用于大部分页面不需要登录,少部分页面需要登录) +const navigateToInterceptor = { + // 注意,这里的url是 '/' 开头的,如 '/pages/index/index',跟 'pages.json' 里面的 path 不同 + // 增加对相对路径的处理,BY 网友 @ideal + invoke({ url }: { url: string }) { + // console.log(url) // /pages/route-interceptor/index?name=feige&age=30 + let path = url.split('?')[0] + + // 处理相对路径 + if (!path.startsWith('/')) { + const currentPath = getLastPage().route + const normalizedCurrentPath = currentPath.startsWith('/') ? currentPath : `/${currentPath}` + const baseDir = normalizedCurrentPath.substring(0, normalizedCurrentPath.lastIndexOf('/')) + path = `${baseDir}/${path}` + } + + let needLoginPages: string[] = [] + // 为了防止开发时出现BUG,这里每次都获取一下。生产环境可以移到函数外,性能更好 + if (isDev) { + needLoginPages = getNeedLoginPages() + } + else { + needLoginPages = _needLoginPages + } + const isNeedLogin = needLoginPages.includes(path) + if (!isNeedLogin) { + return true + } + const hasLogin = isLogined() + if (hasLogin) { + return true + } + const redirectRoute = `${loginRoute}?redirect=${encodeURIComponent(url)}` + uni.navigateTo({ url: redirectRoute }) + return false + }, +} + +export const routeInterceptor = { + install() { + uni.addInterceptor('navigateTo', navigateToInterceptor) + uni.addInterceptor('reLaunch', navigateToInterceptor) + uni.addInterceptor('redirectTo', navigateToInterceptor) + uni.addInterceptor('switchTab', navigateToInterceptor) + }, +} diff --git a/src/layouts/default.vue b/src/layouts/default.vue new file mode 100644 index 0000000..98c5644 --- /dev/null +++ b/src/layouts/default.vue @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/src/layouts/fg-tabbar/fg-tabbar.vue b/src/layouts/fg-tabbar/fg-tabbar.vue new file mode 100644 index 0000000..819aa6f --- /dev/null +++ b/src/layouts/fg-tabbar/fg-tabbar.vue @@ -0,0 +1,68 @@ + + + diff --git a/src/layouts/fg-tabbar/tabbar.md b/src/layouts/fg-tabbar/tabbar.md new file mode 100644 index 0000000..2485b06 --- /dev/null +++ b/src/layouts/fg-tabbar/tabbar.md @@ -0,0 +1,17 @@ +# tabbar 说明 + +`tabbar` 分为 `4 种` 情况: + +- 0 `无 tabbar`,只有一个页面入口,底部无 `tabbar` 显示;常用语临时活动页。 +- 1 `原生 tabbar`,使用 `switchTab` 切换 tabbar,`tabbar` 页面有缓存。 + - 优势:原生自带的 tabbar,最先渲染,有缓存。 + - 劣势:只能使用 2 组图片来切换选中和非选中状态,修改颜色只能重新换图片(或者用 iconfont)。 +- 2 `有缓存自定义 tabbar`,使用 `switchTab` 切换 tabbar,`tabbar` 页面有缓存。使用了第三方 UI 库的 `tabbar` 组件,并隐藏了原生 `tabbar` 的显示。 + - 优势:可以随意配置自己想要的 `svg icon`,切换字体颜色方便。有缓存。可以实现各种花里胡哨的动效等。 + - 劣势:首次点击 tababr 会闪烁。 +- 3 `无缓存自定义 tabbar`,使用 `navigateTo` 切换 `tabbar`,`tabbar` 页面无缓存。使用了第三方 UI 库的 `tabbar` 组件。 + - 优势:可以随意配置自己想要的 svg icon,切换字体颜色方便。可以实现各种花里胡哨的动效等。 + - 劣势:首次点击 `tababr` 会闪烁,无缓存。 + + +> 注意:花里胡哨的效果需要自己实现,本模版不提供。 diff --git a/src/layouts/fg-tabbar/tabbar.ts b/src/layouts/fg-tabbar/tabbar.ts new file mode 100644 index 0000000..03be03f --- /dev/null +++ b/src/layouts/fg-tabbar/tabbar.ts @@ -0,0 +1,11 @@ +/** + * tabbar 状态,增加 storageSync 保证刷新浏览器时在正确的 tabbar 页面 + * 使用reactive简单状态,而不是 pinia 全局状态 + */ +export const tabbarStore = reactive({ + curIdx: uni.getStorageSync('app-tabbar-index') || 0, + setCurIdx(idx: number) { + this.curIdx = idx + uni.setStorageSync('app-tabbar-index', idx) + }, +}) diff --git a/src/layouts/fg-tabbar/tabbarList.ts b/src/layouts/fg-tabbar/tabbarList.ts new file mode 100644 index 0000000..a1e23da --- /dev/null +++ b/src/layouts/fg-tabbar/tabbarList.ts @@ -0,0 +1,60 @@ +/** + * tabbar 选择的策略,更详细的介绍见 tabbar.md 文件 + * 0: 'NO_TABBAR' `无 tabbar` + * 1: 'NATIVE_TABBAR' `完全原生 tabbar` + * 2: 'CUSTOM_TABBAR_WITH_CACHE' `有缓存自定义 tabbar` + * 3: 'CUSTOM_TABBAR_WITHOUT_CACHE' `无缓存自定义 tabbar` + * + * 温馨提示:本文件的任何代码更改了之后,都需要重新运行,否则 pages.json 不会更新导致错误 + */ +export const TABBAR_MAP = { + NO_TABBAR: 0, + NATIVE_TABBAR: 1, + CUSTOM_TABBAR_WITH_CACHE: 2, + CUSTOM_TABBAR_WITHOUT_CACHE: 3, +} +// TODO:通过这里切换使用tabbar的策略 +export const selectedTabbarStrategy = TABBAR_MAP.NATIVE_TABBAR + +// selectedTabbarStrategy==NATIVE_TABBAR(1) 时,需要填 iconPath 和 selectedIconPath +// selectedTabbarStrategy==CUSTOM_TABBAR(2,3) 时,需要填 icon 和 iconType +// selectedTabbarStrategy==NO_TABBAR(0) 时,tabbarList 不生效 +export const tabbarList = [ + { + iconPath: 'static/tabbar/home.png', + selectedIconPath: 'static/tabbar/homeHL.png', + pagePath: 'pages/index/index', + text: '', + icon: '', + // 选用 UI 框架自带的 icon时,iconType 为 uiLib + iconType: 'uiLib', + }, + { + iconPath: 'static/tabbar/example.png', + selectedIconPath: 'static/tabbar/exampleHL.png', + pagePath: 'pages/about/about', + text: '', + icon: '', + // 注意 unocss 的图标需要在 页面上引入一下,或者配置到 unocss.config.ts 的 safelist 中 + iconType: 'unocss', + }, +] + +// NATIVE_TABBAR(1) 和 CUSTOM_TABBAR_WITH_CACHE(2) 时,需要tabbar缓存 +export const cacheTabbarEnable = selectedTabbarStrategy === TABBAR_MAP.NATIVE_TABBAR + || selectedTabbarStrategy === TABBAR_MAP.CUSTOM_TABBAR_WITH_CACHE + +const _tabbar = { + color: '#999999', + selectedColor: '#018d71', + backgroundColor: '#F8F8F8', + borderStyle: 'black', + height: '50px', + fontSize: '10px', + iconWidth: '24px', + spacing: '3px', + list: tabbarList, +} + +// 0和1 需要显示底部的tabbar的各种配置,以利用缓存 +export const tabBar = cacheTabbarEnable ? _tabbar : undefined diff --git a/src/layouts/tabbar.vue b/src/layouts/tabbar.vue new file mode 100644 index 0000000..dc69bc0 --- /dev/null +++ b/src/layouts/tabbar.vue @@ -0,0 +1,19 @@ + + + diff --git a/src/login-sub/components/LoginMask.vue b/src/login-sub/components/LoginMask.vue new file mode 100644 index 0000000..b0c5255 --- /dev/null +++ b/src/login-sub/components/LoginMask.vue @@ -0,0 +1,197 @@ + + + + + diff --git a/src/login-sub/components/Overlay.vue b/src/login-sub/components/Overlay.vue new file mode 100644 index 0000000..bb9c6a3 --- /dev/null +++ b/src/login-sub/components/Overlay.vue @@ -0,0 +1,54 @@ + + + + + diff --git a/src/login-sub/components/check-group/Checkbox.vue b/src/login-sub/components/check-group/Checkbox.vue new file mode 100644 index 0000000..b629a93 --- /dev/null +++ b/src/login-sub/components/check-group/Checkbox.vue @@ -0,0 +1,133 @@ + + + + + diff --git a/src/login-sub/components/check-group/CheckboxGroup.vue b/src/login-sub/components/check-group/CheckboxGroup.vue new file mode 100644 index 0000000..1c8e836 --- /dev/null +++ b/src/login-sub/components/check-group/CheckboxGroup.vue @@ -0,0 +1,72 @@ + + + + + diff --git a/src/login-sub/components/navbar/Navbar.vue b/src/login-sub/components/navbar/Navbar.vue new file mode 100644 index 0000000..909fb23 --- /dev/null +++ b/src/login-sub/components/navbar/Navbar.vue @@ -0,0 +1,210 @@ + + + + + diff --git a/src/login-sub/components/radio-group/Radio.vue b/src/login-sub/components/radio-group/Radio.vue new file mode 100644 index 0000000..c4dcc25 --- /dev/null +++ b/src/login-sub/components/radio-group/Radio.vue @@ -0,0 +1,89 @@ + + + + + diff --git a/src/login-sub/components/radio-group/RadioGroup.vue b/src/login-sub/components/radio-group/RadioGroup.vue new file mode 100644 index 0000000..fa22b5d --- /dev/null +++ b/src/login-sub/components/radio-group/RadioGroup.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/src/login-sub/hooks/useUserInfo.ts b/src/login-sub/hooks/useUserInfo.ts new file mode 100644 index 0000000..c6ee25e --- /dev/null +++ b/src/login-sub/hooks/useUserInfo.ts @@ -0,0 +1,27 @@ +import { setWxInfo } from '@/service/index/api' + +//uniapp 登陆获取用户信息 +export const useLogin = () => { + return new Promise(function (resolve, reject) { + uni.login({ + success: function (res) { + if (res.code) { + resolve(res) + } else { + reject(res) + } + }, + fail: function (err) { + reject(err) + }, + }) + }) +} + +export const useWxInfo = ({ code, openId }) => { + setWxInfo({ code, openId }).then((res) => { + if (res.code === 200) { + console.log(res.result) + } + }) +} diff --git a/src/login-sub/index.vue b/src/login-sub/index.vue new file mode 100644 index 0000000..d4a10f6 --- /dev/null +++ b/src/login-sub/index.vue @@ -0,0 +1,195 @@ + +{ + style: { + navigationBarTitleText: '康乐云家', + }, +} + + + + + + + diff --git a/src/login-sub/privacyPolicy.vue b/src/login-sub/privacyPolicy.vue new file mode 100644 index 0000000..20babbf --- /dev/null +++ b/src/login-sub/privacyPolicy.vue @@ -0,0 +1,7 @@ + + + + + diff --git a/src/login-sub/userAgreement.vue b/src/login-sub/userAgreement.vue new file mode 100644 index 0000000..9fca83e --- /dev/null +++ b/src/login-sub/userAgreement.vue @@ -0,0 +1,7 @@ + + + + + diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..b86a5fd --- /dev/null +++ b/src/main.ts @@ -0,0 +1,21 @@ +import { VueQueryPlugin } from '@tanstack/vue-query' +import { createSSRApp } from 'vue' +import App from './App.vue' +import { prototypeInterceptor, requestInterceptor, routeInterceptor } from './interceptors' + +import store from './store' +import '@/style/index.scss' +import 'virtual:uno.css' + +export function createApp() { + const app = createSSRApp(App) + app.use(store) + app.use(routeInterceptor) + app.use(requestInterceptor) + app.use(prototypeInterceptor) + app.use(VueQueryPlugin) + + return { + app, + } +} diff --git a/src/manifest.json b/src/manifest.json new file mode 100644 index 0000000..873e55c --- /dev/null +++ b/src/manifest.json @@ -0,0 +1,94 @@ +{ + "name": "康乐云家", + "appid": "__UNI__D1E5001", + "description": "", + "versionName": "1.0.0", + "versionCode": "100", + "transformPx": false, + "app-plus": { + "usingComponents": true, + "nvueStyleCompiler": "uni-app", + "compilerVersion": 3, + "splashscreen": { + "alwaysShowBeforeRender": true, + "waiting": true, + "autoclose": true, + "delay": 0 + }, + "modules": {}, + "distribute": { + "android": { + "permissions": [ + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "" + ], + "minSdkVersion": 30, + "targetSdkVersion": 30, + "abiFilters": [ + "armeabi-v7a", + "arm64-v8a" + ] + }, + "ios": {}, + "sdkConfigs": {}, + "icons": { + "android": {}, + "ios": {} + } + }, + "compatible": { + "ignoreVersion": true + } + }, + "quickapp": {}, + "mp-weixin": { + "appid": "wxe173e798fc5a9f02", + "setting": { + "urlCheck": false, + "es6": true, + "minified": true + }, + "usingComponents": true, + "optimization": { + "subPackages": true + }, + "permission": { + "scope.userLocation": { + "desc": "你的位置信息将用于小程序位置接口的效果展示" + } + }, + "requiredPrivateInfos": [ + "getLocation" + ] + }, + "mp-alipay": { + "usingComponents": true, + "styleIsolation": "shared" + }, + "mp-baidu": { + "usingComponents": true + }, + "mp-toutiao": { + "usingComponents": true + }, + "uniStatistics": { + "enable": false + }, + "vueVersion": "3", + "h5": { + "router": {} + } +} \ No newline at end of file diff --git a/src/pages.json b/src/pages.json new file mode 100644 index 0000000..1d380a3 --- /dev/null +++ b/src/pages.json @@ -0,0 +1,105 @@ +{ + "globalStyle": { + "navigationBarTitleText": "康乐云家", + "navigationBarBackgroundColor": "#ffffff", + "navigationBarTextStyle": "black", + "backgroundColor": "#FFFFFF" + }, + "easycom": { + "autoscan": true, + "custom": { + "^fg-(.*)": "@/components/fg-$1/fg-$1.vue", + "^wd-(.*)": "wot-design-uni/components/wd-$1/wd-$1.vue", + "^(?!z-paging-refresh|z-paging-load-more)z-paging(.*)": "z-paging/components/z-paging$1/z-paging$1.vue" + } + }, + "tabBar": { + "color": "#999999", + "selectedColor": "#018d71", + "backgroundColor": "#F8F8F8", + "borderStyle": "black", + "height": "50px", + "fontSize": "10px", + "iconWidth": "24px", + "spacing": "3px", + "list": [ + { + "iconPath": "static/tabbar/home.png", + "selectedIconPath": "static/tabbar/homeHL.png", + "pagePath": "pages/index/index", + "text": "", + "icon": "", + "iconType": "uiLib" + }, + { + "iconPath": "static/tabbar/example.png", + "selectedIconPath": "static/tabbar/exampleHL.png", + "pagePath": "pages/about/about", + "text": "", + "icon": "", + "iconType": "unocss" + } + ] + }, + "pages": [ + { + "path": "pages/index/index", + "type": "home", + "style": { + "navigationBarTitleText": "康乐云家", + "enableShareAppMessage": true, + "enableShareTimeline": true + } + }, + { + "path": "pages/about/about", + "type": "page", + "style": { + "navigationBarTitleText": "" + } + }, + { + "path": "pages/address/index", + "type": "page", + "style": { + "navigationBarTitleText": "地址" + }, + "layout": false + }, + { + "path": "pages/payment/index", + "type": "page" + }, + { + "path": "pages/temporary/index", + "type": "page", + "style": { + "navigationBarTitleText": "康乐云家", + "enableShareAppMessage": true, + "enableShareTimeline": true + } + } + ], + "subPackages": [ + { + "root": "login-sub", + "pages": [ + { + "path": "index", + "type": "page", + "style": { + "navigationBarTitleText": "康乐云家" + } + }, + { + "path": "privacyPolicy", + "type": "page" + }, + { + "path": "userAgreement", + "type": "page" + } + ] + } + ] +} diff --git a/src/pages/about/about.vue b/src/pages/about/about.vue new file mode 100644 index 0000000..1ffd1ff --- /dev/null +++ b/src/pages/about/about.vue @@ -0,0 +1,19 @@ + +{ + style: { + navigationBarTitleText: '', + }, +} + + + + + + + diff --git a/src/pages/about/components/request.vue b/src/pages/about/components/request.vue new file mode 100644 index 0000000..60e21f4 --- /dev/null +++ b/src/pages/about/components/request.vue @@ -0,0 +1,84 @@ + +{ + layout: 'demo', + style: { + navigationBarTitleText: '请求', + }, +} + + + + + diff --git a/src/pages/about/components/upload.vue b/src/pages/about/components/upload.vue new file mode 100644 index 0000000..d23625f --- /dev/null +++ b/src/pages/about/components/upload.vue @@ -0,0 +1,38 @@ + +{ + layout: 'default', + style: { + navigationBarTitleText: '上传-状态一体化', + }, +} + + + + + + + diff --git a/src/pages/address/index.vue b/src/pages/address/index.vue new file mode 100644 index 0000000..89cc704 --- /dev/null +++ b/src/pages/address/index.vue @@ -0,0 +1,34 @@ + +{ + style: { + navigationBarTitleText: '地址', + }, + "layout": false +} + + + + + \ No newline at end of file diff --git a/src/pages/index/index.vue b/src/pages/index/index.vue new file mode 100644 index 0000000..b3d43db --- /dev/null +++ b/src/pages/index/index.vue @@ -0,0 +1,70 @@ + + +{ + style: { + navigationBarTitleText: '康乐云家', + enableShareAppMessage: true, + enableShareTimeline: true, + }, +} + + + + + + + + diff --git a/src/pages/payment/index.vue b/src/pages/payment/index.vue new file mode 100644 index 0000000..87d6173 --- /dev/null +++ b/src/pages/payment/index.vue @@ -0,0 +1,63 @@ + + \ No newline at end of file diff --git a/src/pages/temporary/index.vue b/src/pages/temporary/index.vue new file mode 100644 index 0000000..d95c26b --- /dev/null +++ b/src/pages/temporary/index.vue @@ -0,0 +1,67 @@ + + { + style: { + navigationBarTitleText: '康乐云家', + enableShareAppMessage: true, + enableShareTimeline: true, + }, + } + + + + + diff --git a/src/service/app/displayEnumLabel.ts b/src/service/app/displayEnumLabel.ts new file mode 100644 index 0000000..4974815 --- /dev/null +++ b/src/service/app/displayEnumLabel.ts @@ -0,0 +1,13 @@ +/* eslint-disable */ +// @ts-ignore +import * as API from './types'; + +export function displayStatusEnum(field: API.IStatusEnum) { + return { available: 'available', pending: 'pending', sold: 'sold' }[field]; +} + +export function displayStatusEnum2(field: API.IStatusEnum2) { + return { placed: 'placed', approved: 'approved', delivered: 'delivered' }[ + field + ]; +} diff --git a/src/service/app/index.ts b/src/service/app/index.ts new file mode 100644 index 0000000..45b6e53 --- /dev/null +++ b/src/service/app/index.ts @@ -0,0 +1,11 @@ +/* eslint-disable */ +// @ts-ignore +export * from './types'; +export * from './displayEnumLabel'; + +export * from './pet'; +export * from './pet.vuequery'; +export * from './store'; +export * from './store.vuequery'; +export * from './user'; +export * from './user.vuequery'; diff --git a/src/service/app/pet.ts b/src/service/app/pet.ts new file mode 100644 index 0000000..70b95ef --- /dev/null +++ b/src/service/app/pet.ts @@ -0,0 +1,193 @@ +/* eslint-disable */ +// @ts-ignore +import request from '@/utils/request'; +import { CustomRequestOptions } from '@/interceptors/request'; + +import * as API from './types'; + +/** Update an existing pet PUT /pet */ +export async function updatePet({ + body, + options, +}: { + body: API.Pet; + options?: CustomRequestOptions; +}) { + return request('/pet', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + data: body, + ...(options || {}), + }); +} + +/** Add a new pet to the store POST /pet */ +export async function addPet({ + body, + options, +}: { + body: API.Pet; + options?: CustomRequestOptions; +}) { + return request('/pet', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + data: body, + ...(options || {}), + }); +} + +/** Find pet by ID Returns a single pet GET /pet/${param0} */ +export async function getPetById({ + params, + options, +}: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.getPetByIdParams; + options?: CustomRequestOptions; +}) { + const { petId: param0, ...queryParams } = params; + + return request(`/pet/${param0}`, { + method: 'GET', + params: { ...queryParams }, + ...(options || {}), + }); +} + +/** Updates a pet in the store with form data POST /pet/${param0} */ +export async function updatePetWithForm({ + params, + body, + options, +}: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.updatePetWithFormParams; + body: { + /** Updated name of the pet */ + name?: string; + /** Updated status of the pet */ + status?: string; + }; + options?: CustomRequestOptions; +}) { + const { petId: param0, ...queryParams } = params; + + return request(`/pet/${param0}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + params: { ...queryParams }, + data: body, + ...(options || {}), + }); +} + +/** Deletes a pet DELETE /pet/${param0} */ +export async function deletePet({ + params, + options, +}: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.deletePetParams; + options?: CustomRequestOptions; +}) { + const { petId: param0, ...queryParams } = params; + + return request(`/pet/${param0}`, { + method: 'DELETE', + params: { ...queryParams }, + ...(options || {}), + }); +} + +/** uploads an image POST /pet/${param0}/uploadImage */ +export async function uploadFile({ + params, + body, + file, + options, +}: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.uploadFileParams; + body: { + /** Additional data to pass to server */ + additionalMetadata?: string; + }; + file?: File; + options?: CustomRequestOptions; +}) { + const { petId: param0, ...queryParams } = params; + const formData = new FormData(); + + if (file) { + formData.append('file', file); + } + + Object.keys(body).forEach((ele) => { + const item = (body as { [key: string]: any })[ele]; + + if (item !== undefined && item !== null) { + if (typeof item === 'object' && !(item instanceof File)) { + if (item instanceof Array) { + item.forEach((f) => formData.append(ele, f || '')); + } else { + formData.append(ele, JSON.stringify(item)); + } + } else { + formData.append(ele, item); + } + } + }); + + return request(`/pet/${param0}/uploadImage`, { + method: 'POST', + headers: { + 'Content-Type': 'multipart/form-data', + }, + params: { ...queryParams }, + data: formData, + ...(options || {}), + }); +} + +/** Finds Pets by status Multiple status values can be provided with comma separated strings GET /pet/findByStatus */ +export async function findPetsByStatus({ + params, + options, +}: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.findPetsByStatusParams; + options?: CustomRequestOptions; +}) { + return request('/pet/findByStatus', { + method: 'GET', + params: { + ...params, + }, + ...(options || {}), + }); +} + +/** Finds Pets by tags Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. GET /pet/findByTags */ +export async function findPetsByTags({ + params, + options, +}: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.findPetsByTagsParams; + options?: CustomRequestOptions; +}) { + return request('/pet/findByTags', { + method: 'GET', + params: { + ...params, + }, + ...(options || {}), + }); +} diff --git a/src/service/app/pet.vuequery.ts b/src/service/app/pet.vuequery.ts new file mode 100644 index 0000000..c6c0b7d --- /dev/null +++ b/src/service/app/pet.vuequery.ts @@ -0,0 +1,151 @@ +/* eslint-disable */ +// @ts-ignore +import { queryOptions, useMutation } from '@tanstack/vue-query'; +import type { DefaultError } from '@tanstack/vue-query'; +import request from '@/utils/request'; +import { CustomRequestOptions } from '@/interceptors/request'; + +import * as apis from './pet'; +import * as API from './types'; + +/** Update an existing pet PUT /pet */ +export function useUpdatePetMutation(options?: { + onSuccess?: (value?: unknown) => void; + onError?: (error?: DefaultError) => void; +}) { + const { onSuccess, onError } = options || {}; + + const response = useMutation({ + mutationFn: apis.updatePet, + onSuccess(data: unknown) { + onSuccess?.(data); + }, + onError(error) { + onError?.(error); + }, + }); + + return response; +} + +/** Add a new pet to the store POST /pet */ +export function useAddPetMutation(options?: { + onSuccess?: (value?: unknown) => void; + onError?: (error?: DefaultError) => void; +}) { + const { onSuccess, onError } = options || {}; + + const response = useMutation({ + mutationFn: apis.addPet, + onSuccess(data: unknown) { + onSuccess?.(data); + }, + onError(error) { + onError?.(error); + }, + }); + + return response; +} + +/** Find pet by ID Returns a single pet GET /pet/${param0} */ +export function getPetByIdQueryOptions(options: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.getPetByIdParams; + options?: CustomRequestOptions; +}) { + return queryOptions({ + queryFn: async ({ queryKey }) => { + return apis.getPetById(queryKey[1] as typeof options); + }, + queryKey: ['getPetById', options], + }); +} + +/** Updates a pet in the store with form data POST /pet/${param0} */ +export function useUpdatePetWithFormMutation(options?: { + onSuccess?: (value?: unknown) => void; + onError?: (error?: DefaultError) => void; +}) { + const { onSuccess, onError } = options || {}; + + const response = useMutation({ + mutationFn: apis.updatePetWithForm, + onSuccess(data: unknown) { + onSuccess?.(data); + }, + onError(error) { + onError?.(error); + }, + }); + + return response; +} + +/** Deletes a pet DELETE /pet/${param0} */ +export function useDeletePetMutation(options?: { + onSuccess?: (value?: unknown) => void; + onError?: (error?: DefaultError) => void; +}) { + const { onSuccess, onError } = options || {}; + + const response = useMutation({ + mutationFn: apis.deletePet, + onSuccess(data: unknown) { + onSuccess?.(data); + }, + onError(error) { + onError?.(error); + }, + }); + + return response; +} + +/** uploads an image POST /pet/${param0}/uploadImage */ +export function useUploadFileMutation(options?: { + onSuccess?: (value?: API.ApiResponse) => void; + onError?: (error?: DefaultError) => void; +}) { + const { onSuccess, onError } = options || {}; + + const response = useMutation({ + mutationFn: apis.uploadFile, + onSuccess(data: API.ApiResponse) { + onSuccess?.(data); + }, + onError(error) { + onError?.(error); + }, + }); + + return response; +} + +/** Finds Pets by status Multiple status values can be provided with comma separated strings GET /pet/findByStatus */ +export function findPetsByStatusQueryOptions(options: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.findPetsByStatusParams; + options?: CustomRequestOptions; +}) { + return queryOptions({ + queryFn: async ({ queryKey }) => { + return apis.findPetsByStatus(queryKey[1] as typeof options); + }, + queryKey: ['findPetsByStatus', options], + }); +} + +/** Finds Pets by tags Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing. GET /pet/findByTags */ +export function findPetsByTagsQueryOptions(options: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.findPetsByTagsParams; + options?: CustomRequestOptions; +}) { + return queryOptions({ + queryFn: async ({ queryKey }) => { + return apis.findPetsByTags(queryKey[1] as typeof options); + }, + queryKey: ['findPetsByTags', options], + }); +} diff --git a/src/service/app/store.ts b/src/service/app/store.ts new file mode 100644 index 0000000..0d87f52 --- /dev/null +++ b/src/service/app/store.ts @@ -0,0 +1,72 @@ +/* eslint-disable */ +// @ts-ignore +import request from '@/utils/request'; +import { CustomRequestOptions } from '@/interceptors/request'; + +import * as API from './types'; + +/** Returns pet inventories by status Returns a map of status codes to quantities GET /store/inventory */ +export async function getInventory({ + options, +}: { + options?: CustomRequestOptions; +}) { + return request>('/store/inventory', { + method: 'GET', + ...(options || {}), + }); +} + +/** Place an order for a pet POST /store/order */ +export async function placeOrder({ + body, + options, +}: { + body: API.Order; + options?: CustomRequestOptions; +}) { + return request('/store/order', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + data: body, + ...(options || {}), + }); +} + +/** Find purchase order by ID For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions GET /store/order/${param0} */ +export async function getOrderById({ + params, + options, +}: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.getOrderByIdParams; + options?: CustomRequestOptions; +}) { + const { orderId: param0, ...queryParams } = params; + + return request(`/store/order/${param0}`, { + method: 'GET', + params: { ...queryParams }, + ...(options || {}), + }); +} + +/** Delete purchase order by ID For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors DELETE /store/order/${param0} */ +export async function deleteOrder({ + params, + options, +}: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.deleteOrderParams; + options?: CustomRequestOptions; +}) { + const { orderId: param0, ...queryParams } = params; + + return request(`/store/order/${param0}`, { + method: 'DELETE', + params: { ...queryParams }, + ...(options || {}), + }); +} diff --git a/src/service/app/store.vuequery.ts b/src/service/app/store.vuequery.ts new file mode 100644 index 0000000..dd6d660 --- /dev/null +++ b/src/service/app/store.vuequery.ts @@ -0,0 +1,75 @@ +/* eslint-disable */ +// @ts-ignore +import { queryOptions, useMutation } from '@tanstack/vue-query'; +import type { DefaultError } from '@tanstack/vue-query'; +import request from '@/utils/request'; +import { CustomRequestOptions } from '@/interceptors/request'; + +import * as apis from './store'; +import * as API from './types'; + +/** Returns pet inventories by status Returns a map of status codes to quantities GET /store/inventory */ +export function getInventoryQueryOptions(options: { + options?: CustomRequestOptions; +}) { + return queryOptions({ + queryFn: async ({ queryKey }) => { + return apis.getInventory(queryKey[1] as typeof options); + }, + queryKey: ['getInventory', options], + }); +} + +/** Place an order for a pet POST /store/order */ +export function usePlaceOrderMutation(options?: { + onSuccess?: (value?: API.Order) => void; + onError?: (error?: DefaultError) => void; +}) { + const { onSuccess, onError } = options || {}; + + const response = useMutation({ + mutationFn: apis.placeOrder, + onSuccess(data: API.Order) { + onSuccess?.(data); + }, + onError(error) { + onError?.(error); + }, + }); + + return response; +} + +/** Find purchase order by ID For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions GET /store/order/${param0} */ +export function getOrderByIdQueryOptions(options: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.getOrderByIdParams; + options?: CustomRequestOptions; +}) { + return queryOptions({ + queryFn: async ({ queryKey }) => { + return apis.getOrderById(queryKey[1] as typeof options); + }, + queryKey: ['getOrderById', options], + }); +} + +/** Delete purchase order by ID For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors DELETE /store/order/${param0} */ +export function useDeleteOrderMutation(options?: { + onSuccess?: (value?: unknown) => void; + onError?: (error?: DefaultError) => void; +}) { + const { onSuccess, onError } = options || {}; + + const response = useMutation({ + mutationFn: apis.deleteOrder, + onSuccess(data: unknown) { + onSuccess?.(data); + }, + onError(error) { + onError?.(error); + }, + }); + + return response; +} diff --git a/src/service/app/types.ts b/src/service/app/types.ts new file mode 100644 index 0000000..4691b64 --- /dev/null +++ b/src/service/app/types.ts @@ -0,0 +1,128 @@ +/* eslint-disable */ +// @ts-ignore + +export type ApiResponse = { + code?: number; + type?: string; + message?: string; +}; + +export type Category = { + id?: number; + name?: string; +}; + +export type deleteOrderParams = { + /** ID of the order that needs to be deleted */ + orderId: number; +}; + +export type deletePetParams = { + /** Pet id to delete */ + petId: number; +}; + +export type deleteUserParams = { + /** The name that needs to be deleted */ + username: string; +}; + +export type findPetsByStatusParams = { + /** Status values that need to be considered for filter */ + status: ('available' | 'pending' | 'sold')[]; +}; + +export type findPetsByTagsParams = { + /** Tags to filter by */ + tags: string[]; +}; + +export type getOrderByIdParams = { + /** ID of pet that needs to be fetched */ + orderId: number; +}; + +export type getPetByIdParams = { + /** ID of pet to return */ + petId: number; +}; + +export type getUserByNameParams = { + /** The name that needs to be fetched. Use user1 for testing. */ + username: string; +}; + +export type loginUserParams = { + /** The user name for login */ + username: string; + /** The password for login in clear text */ + password: string; +}; + +export type Order = { + id?: number; + petId?: number; + quantity?: number; + shipDate?: string; + /** Order Status */ + status?: 'placed' | 'approved' | 'delivered'; + complete?: boolean; +}; + +export type Pet = { + id?: number; + category?: Category; + name: string; + photoUrls: string[]; + tags?: Tag[]; + /** pet status in the store */ + status?: 'available' | 'pending' | 'sold'; +}; + +export enum StatusEnum { + available = 'available', + pending = 'pending', + sold = 'sold', +} + +export type IStatusEnum = keyof typeof StatusEnum; + +export enum StatusEnum2 { + placed = 'placed', + approved = 'approved', + delivered = 'delivered', +} + +export type IStatusEnum2 = keyof typeof StatusEnum2; + +export type Tag = { + id?: number; + name?: string; +}; + +export type updatePetWithFormParams = { + /** ID of pet that needs to be updated */ + petId: number; +}; + +export type updateUserParams = { + /** name that need to be updated */ + username: string; +}; + +export type uploadFileParams = { + /** ID of pet to update */ + petId: number; +}; + +export type User = { + id?: number; + username?: string; + firstName?: string; + lastName?: string; + email?: string; + password?: string; + phone?: string; + /** User Status */ + userStatus?: number; +}; diff --git a/src/service/app/user.ts b/src/service/app/user.ts new file mode 100644 index 0000000..2474272 --- /dev/null +++ b/src/service/app/user.ts @@ -0,0 +1,150 @@ +/* eslint-disable */ +// @ts-ignore +import request from '@/utils/request'; +import { CustomRequestOptions } from '@/interceptors/request'; + +import * as API from './types'; + +/** Create user This can only be done by the logged in user. 返回值: successful operation POST /user */ +export async function createUser({ + body, + options, +}: { + body: API.User; + options?: CustomRequestOptions; +}) { + return request('/user', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + data: body, + ...(options || {}), + }); +} + +/** Get user by user name GET /user/${param0} */ +export async function getUserByName({ + params, + options, +}: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.getUserByNameParams; + options?: CustomRequestOptions; +}) { + const { username: param0, ...queryParams } = params; + + return request(`/user/${param0}`, { + method: 'GET', + params: { ...queryParams }, + ...(options || {}), + }); +} + +/** Updated user This can only be done by the logged in user. PUT /user/${param0} */ +export async function updateUser({ + params, + body, + options, +}: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.updateUserParams; + body: API.User; + options?: CustomRequestOptions; +}) { + const { username: param0, ...queryParams } = params; + + return request(`/user/${param0}`, { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + params: { ...queryParams }, + data: body, + ...(options || {}), + }); +} + +/** Delete user This can only be done by the logged in user. DELETE /user/${param0} */ +export async function deleteUser({ + params, + options, +}: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.deleteUserParams; + options?: CustomRequestOptions; +}) { + const { username: param0, ...queryParams } = params; + + return request(`/user/${param0}`, { + method: 'DELETE', + params: { ...queryParams }, + ...(options || {}), + }); +} + +/** Creates list of users with given input array 返回值: successful operation POST /user/createWithArray */ +export async function createUsersWithArrayInput({ + body, + options, +}: { + body: API.User[]; + options?: CustomRequestOptions; +}) { + return request('/user/createWithArray', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + data: body, + ...(options || {}), + }); +} + +/** Creates list of users with given input array 返回值: successful operation POST /user/createWithList */ +export async function createUsersWithListInput({ + body, + options, +}: { + body: API.User[]; + options?: CustomRequestOptions; +}) { + return request('/user/createWithList', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + data: body, + ...(options || {}), + }); +} + +/** Logs user into the system GET /user/login */ +export async function loginUser({ + params, + options, +}: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.loginUserParams; + options?: CustomRequestOptions; +}) { + return request('/user/login', { + method: 'GET', + params: { + ...params, + }, + ...(options || {}), + }); +} + +/** Logs out current logged in user session 返回值: successful operation GET /user/logout */ +export async function logoutUser({ + options, +}: { + options?: CustomRequestOptions; +}) { + return request('/user/logout', { + method: 'GET', + ...(options || {}), + }); +} diff --git a/src/service/app/user.vuequery.ts b/src/service/app/user.vuequery.ts new file mode 100644 index 0000000..0e13636 --- /dev/null +++ b/src/service/app/user.vuequery.ts @@ -0,0 +1,149 @@ +/* eslint-disable */ +// @ts-ignore +import { queryOptions, useMutation } from '@tanstack/vue-query'; +import type { DefaultError } from '@tanstack/vue-query'; +import request from '@/utils/request'; +import { CustomRequestOptions } from '@/interceptors/request'; + +import * as apis from './user'; +import * as API from './types'; + +/** Create user This can only be done by the logged in user. 返回值: successful operation POST /user */ +export function useCreateUserMutation(options?: { + onSuccess?: (value?: unknown) => void; + onError?: (error?: DefaultError) => void; +}) { + const { onSuccess, onError } = options || {}; + + const response = useMutation({ + mutationFn: apis.createUser, + onSuccess(data: unknown) { + onSuccess?.(data); + }, + onError(error) { + onError?.(error); + }, + }); + + return response; +} + +/** Get user by user name GET /user/${param0} */ +export function getUserByNameQueryOptions(options: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.getUserByNameParams; + options?: CustomRequestOptions; +}) { + return queryOptions({ + queryFn: async ({ queryKey }) => { + return apis.getUserByName(queryKey[1] as typeof options); + }, + queryKey: ['getUserByName', options], + }); +} + +/** Updated user This can only be done by the logged in user. PUT /user/${param0} */ +export function useUpdateUserMutation(options?: { + onSuccess?: (value?: unknown) => void; + onError?: (error?: DefaultError) => void; +}) { + const { onSuccess, onError } = options || {}; + + const response = useMutation({ + mutationFn: apis.updateUser, + onSuccess(data: unknown) { + onSuccess?.(data); + }, + onError(error) { + onError?.(error); + }, + }); + + return response; +} + +/** Delete user This can only be done by the logged in user. DELETE /user/${param0} */ +export function useDeleteUserMutation(options?: { + onSuccess?: (value?: unknown) => void; + onError?: (error?: DefaultError) => void; +}) { + const { onSuccess, onError } = options || {}; + + const response = useMutation({ + mutationFn: apis.deleteUser, + onSuccess(data: unknown) { + onSuccess?.(data); + }, + onError(error) { + onError?.(error); + }, + }); + + return response; +} + +/** Creates list of users with given input array 返回值: successful operation POST /user/createWithArray */ +export function useCreateUsersWithArrayInputMutation(options?: { + onSuccess?: (value?: unknown) => void; + onError?: (error?: DefaultError) => void; +}) { + const { onSuccess, onError } = options || {}; + + const response = useMutation({ + mutationFn: apis.createUsersWithArrayInput, + onSuccess(data: unknown) { + onSuccess?.(data); + }, + onError(error) { + onError?.(error); + }, + }); + + return response; +} + +/** Creates list of users with given input array 返回值: successful operation POST /user/createWithList */ +export function useCreateUsersWithListInputMutation(options?: { + onSuccess?: (value?: unknown) => void; + onError?: (error?: DefaultError) => void; +}) { + const { onSuccess, onError } = options || {}; + + const response = useMutation({ + mutationFn: apis.createUsersWithListInput, + onSuccess(data: unknown) { + onSuccess?.(data); + }, + onError(error) { + onError?.(error); + }, + }); + + return response; +} + +/** Logs user into the system GET /user/login */ +export function loginUserQueryOptions(options: { + // 叠加生成的Param类型 (非body参数openapi默认没有生成对象) + params: API.loginUserParams; + options?: CustomRequestOptions; +}) { + return queryOptions({ + queryFn: async ({ queryKey }) => { + return apis.loginUser(queryKey[1] as typeof options); + }, + queryKey: ['loginUser', options], + }); +} + +/** Logs out current logged in user session 返回值: successful operation GET /user/logout */ +export function logoutUserQueryOptions(options: { + options?: CustomRequestOptions; +}) { + return queryOptions({ + queryFn: async ({ queryKey }) => { + return apis.logoutUser(queryKey[1] as typeof options); + }, + queryKey: ['logoutUser', options], + }); +} diff --git a/src/service/index/api.ts b/src/service/index/api.ts new file mode 100644 index 0000000..5388cdc --- /dev/null +++ b/src/service/index/api.ts @@ -0,0 +1,17 @@ +import { http } from '@/utils/http' + +export const getSessionKey = (params: { JsCode: string }) => { + return http.get('/api/sysWxOpen/v2/wxOpenId', params) +} + +export const setWxInfo = (params: { code: string; openId: string }) => { + return http.get('/api/sysWxOpen/wxPhone', params) +} + +export const getWxUserInfo = () => { + return http.get('/api/weChatUserEx/userInfo') +} + +export const payTransaction = (params: { total: number; openId: string; description: string }) => { + return http.post('/api/sysWechatPay/payTransaction', params) +} diff --git a/src/static/images/.gitkeep b/src/static/images/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/static/images/map/map-icon.png b/src/static/images/map/map-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..64533dfcca9ab7792c587418421efbe8bdfb587c GIT binary patch literal 40383 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 literal 0 HcmV?d00001 diff --git a/src/static/logo.svg b/src/static/logo.svg new file mode 100644 index 0000000..eaee669 --- /dev/null +++ b/src/static/logo.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/static/tabbar/example.png b/src/static/tabbar/example.png new file mode 100644 index 0000000000000000000000000000000000000000..fd1e942bcf691d6d3711535d9419417cabd5d781 GIT binary patch literal 1371 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 Eg2>i=Q~&?~ literal 0 HcmV?d00001 diff --git a/src/static/tabbar/home.png b/src/static/tabbar/home.png new file mode 100644 index 0000000000000000000000000000000000000000..8f82e213c78bd38fc9e7a6eee31c05de0cd3363d GIT binary patch literal 1346 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 V*RY*zoyhZ! literal 0 HcmV?d00001 diff --git a/src/static/tabbar/personal.png b/src/static/tabbar/personal.png new file mode 100644 index 0000000000000000000000000000000000000000..0a569a2f441a1b64342cab45d0b5d85a13891962 GIT binary patch literal 2457 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 { + // 定义用户信息 + const userInfo = ref({ ...userInfoState }) + // 设置用户信息 + const setUserInfo = (val: IUserInfoVo) => { + console.log('设置用户信息', val) + // 若头像为空 则使用默认头像 + if (!val.avatar) { + val.avatar = userInfoState.avatar + } + else { + val.avatar = 'https://oss.laf.run/ukw0y1-site/avatar.jpg?feige' + } + userInfo.value = val + } + const setUserAvatar = (avatar: string) => { + userInfo.value.avatar = avatar + console.log('设置用户头像', avatar) + console.log('userInfo', userInfo.value) + } + // 删除用户信息 + const removeUserInfo = () => { + userInfo.value = { ...userInfoState } + uni.removeStorageSync('userInfo') + uni.removeStorageSync('token') + } + + const setUserOpenId = (openId: string) => { + userInfo.value.openId = openId + } + + const setUserToken = (token: string) => { + userInfo.value.token = token + } + + return { + userInfo, + setUserAvatar, + setUserOpenId, + setUserToken + + } + }, + { + persist: true, + }, +) diff --git a/src/style/iconfont.css b/src/style/iconfont.css new file mode 100644 index 0000000..35da86c --- /dev/null +++ b/src/style/iconfont.css @@ -0,0 +1,28 @@ +@font-face { + font-family: 'iconfont'; /* Project id 4543091 */ + src: + url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAAOwAAsAAAAAB9AAAANjAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHFQGYACDHAqDBIJqATYCJAMQCwoABCAFhGcHPRvnBsgusG3kMyE15/44PsBX09waBHv0REDt97oHAQDFrOIyPirRiULQ+TJcXV0hCYTuVFcBC915/2vX/32Q80hkZ5PZGZ9snvwruVLloidKqYN6iKC53bOtbKwVLSIi3W6zCWZbs3VbER3j9JpGX3ySYcc94IQRTK5s4epS/jSqIgvg37qlY2/jwQN7D9ADpfRCmIknQByTscVZPTBr+hnnCKg2o4bjakvXEPjuY65DJGeJNtBUhn1JxOBuB2UZmUpBOXdsFp4oxOv4GHgs3h/+wRDcicqSZJG1q9kK1z/Af9NpqxjpC2QaAdpHlCFh4spcYXs5sMWpSk5wUj31G2dLQKVKkZ/w7f/8/i/A3JVUSZK9f7xIKJeU14IFpBI/Qfkkz46GT/CuaGREfCtKJUougWeQWHvVC5Lcz2BGS+SePR99vj3yjJx7h574tp7uWcOh4yfaTjS/245TT/vkQrN+a7RLkK8+Vd+bz+FSGh+9srDQKPeJ2s29z7ah4+efdoxefRbbGwfy7ht+SuIWukzsu1b6ePP+6kN1aamb47qsPim1Ia3xdEpDcl1dckPKGYnneI23+57r2W1Mmkqs6ajrChRCs5qyQ66rTVWhgZaG7toOeHm5cxn0sSQuNDEgcUTdNTSupKI1JRZih/JssAUKezPeOJJzbNozF6zWJuuVavVU5Tgtkop/SDzHa7ytvnCTq0PhkEfi4xLLtb0PuwyOAYqmrYQApFJyoJjTnfz+ve94vvv2f/yWgxl8Jd8Di2DRDPuob59mU/+VfDCROQyR8xSnmP9fXm7liagmN39OlmbvjqG0sMsJKrU0EFXogaRSH5bNY1CmxhyUq7QC1cY1T67RwuQk5CoM2RUQNLoEUb03kDS6h2XzcyjT7iOUa/QXqq1Hn6/GUBAaGcGcWJFlGUmCoVOp8kLvABHnVczGYiOE2SVEUH5OXj/TSnTCDjHAviAWcE4RZYaGWszNiKoayGSGTASeY+PcrMjNpVMvyREMDRoxBMYRVojFMkQiMOhohubdzxtAiOapMMbERpKMnQT9SL4ceQysVdJZVa9kEbsFogIcRyEUE2kN0mL7CDVIGhBzupWMEHA5bDvipgq5hKJcKef8ivbx1kC15KgcYkghhzLxYNntxoKCReJ82jAHAAA=') + format('woff2'), + url('//at.alicdn.com/t/c/font_4543091_njpo5b95nl.woff?t=1715485842402') format('woff'), + url('//at.alicdn.com/t/c/font_4543091_njpo5b95nl.ttf?t=1715485842402') format('truetype'); +} + +.iconfont { + font-family: 'iconfont' !important; + font-size: 16px; + font-style: normal; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-my:before { + content: '\e78c'; +} + +.icon-package:before { + content: '\e9c2'; +} + +.icon-chat:before { + content: '\e600'; +} diff --git a/src/style/index.scss b/src/style/index.scss new file mode 100644 index 0000000..86184d9 --- /dev/null +++ b/src/style/index.scss @@ -0,0 +1,18 @@ +@import './iconfont.css'; + +.test { + // 可以通过 @apply 多个样式封装整体样式 + @apply mt-4 ml-4; + + padding-top: 4px; + color: red; +} + +:root, +page { + // 修改按主题色 + // --wot-color-theme: #37c2bc; + + // 修改按钮背景色 + // --wot-button-primary-bg-color: green; +} diff --git a/src/typings.d.ts b/src/typings.d.ts new file mode 100644 index 0000000..d3a2422 --- /dev/null +++ b/src/typings.d.ts @@ -0,0 +1,28 @@ +// 全局要用的类型放到这里 + +declare global { + interface IResData { + code: number + message: string + result: T + } + + // uni.uploadFile文件上传参数 + interface IUniUploadFileOptions { + file?: File + files?: UniApp.UploadFileOptionFiles[] + filePath?: string + name?: string + formData?: any + } + + interface IUserInfo { + nickname?: string + avatar?: string + /** 微信的 openid,非微信没有这个字段 */ + openid?: string + token?: string + } +} + +export {} // 防止模块污染 diff --git a/src/typings.ts b/src/typings.ts new file mode 100644 index 0000000..b48b630 --- /dev/null +++ b/src/typings.ts @@ -0,0 +1,15 @@ +// 枚举定义 + +export enum TestEnum { + A = '1', + B = '2', +} + +// uni.uploadFile文件上传参数 +export interface IUniUploadFileOptions { + file?: File + files?: UniApp.UploadFileOptionFiles[] + filePath?: string + name?: string + formData?: any +} diff --git a/src/uni.scss b/src/uni.scss new file mode 100644 index 0000000..21b9e5f --- /dev/null +++ b/src/uni.scss @@ -0,0 +1,77 @@ +/* stylelint-disable comment-empty-line-before */ +/** + * 这里是uni-app内置的常用样式变量 + * + * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量 + * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App + * + */ + +/** + * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能 + * + * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件 + */ + +/* 颜色变量 */ + +/* 行为相关颜色 */ +$uni-color-primary: #007aff; +$uni-color-success: #4cd964; +$uni-color-warning: #f0ad4e; +$uni-color-error: #dd524d; + +/* 文字基本颜色 */ +$uni-text-color: #333; // 基本色 +$uni-text-color-inverse: #fff; // 反色 +$uni-text-color-grey: #999; // 辅助灰色,如加载更多的提示信息 +$uni-text-color-placeholder: #808080; +$uni-text-color-disable: #c0c0c0; + +/* 背景颜色 */ +$uni-bg-color: #fff; +$uni-bg-color-grey: #f8f8f8; +$uni-bg-color-hover: #f1f1f1; // 点击状态颜色 +$uni-bg-color-mask: rgb(0 0 0 / 40%); // 遮罩颜色 + +/* 边框颜色 */ +$uni-border-color: #c8c7cc; + +/* 尺寸变量 */ + +/* 文字尺寸 */ +$uni-font-size-sm: 12px; +$uni-font-size-base: 14px; +$uni-font-size-lg: 16; + +/* 图片尺寸 */ +$uni-img-size-sm: 20px; +$uni-img-size-base: 26px; +$uni-img-size-lg: 40px; + +/* Border Radius */ +$uni-border-radius-sm: 2px; +$uni-border-radius-base: 3px; +$uni-border-radius-lg: 6px; +$uni-border-radius-circle: 50%; + +/* 水平间距 */ +$uni-spacing-row-sm: 5px; +$uni-spacing-row-base: 10px; +$uni-spacing-row-lg: 15px; + +/* 垂直间距 */ +$uni-spacing-col-sm: 4px; +$uni-spacing-col-base: 8px; +$uni-spacing-col-lg: 12px; + +/* 透明度 */ +$uni-opacity-disabled: 0.3; // 组件禁用态的透明度 + +/* 文章场景相关 */ +$uni-color-title: #2c405a; // 文章标题颜色 +$uni-font-size-title: 20px; +$uni-color-subtitle: #555; // 二级标题颜色 +$uni-font-size-subtitle: 18px; +$uni-color-paragraph: #3f536e; // 文章段落颜色 +$uni-font-size-paragraph: 15px; diff --git a/src/uni_modules/.gitkeep b/src/uni_modules/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/dateUtil.ts b/src/utils/dateUtil.ts new file mode 100644 index 0000000..78e3178 --- /dev/null +++ b/src/utils/dateUtil.ts @@ -0,0 +1,150 @@ +import dayjs from 'dayjs' +import calendar from 'dayjs/plugin/calendar' +import quarterOfYear from 'dayjs/plugin/quarterOfYear' +import relativeTime from 'dayjs/plugin/relativeTime' +import updateLocale from 'dayjs/plugin/updateLocale' +import utc from 'dayjs/plugin/utc' +import weekday from 'dayjs/plugin/weekday' +import 'dayjs/locale/zh-cn' + +dayjs.extend(calendar) +dayjs.extend(quarterOfYear) +dayjs.extend(relativeTime) +dayjs.extend(updateLocale) +dayjs.extend(utc) +dayjs.extend(weekday) + +dayjs.locale('zh-cn') + +dayjs.updateLocale('zh-cn', { + calendar: { + sameDay: 'HH:mm', + nextDay: '[明天]', + nextWeek: 'dddd', + lastDay: '[昨天] HH:mm', + lastWeek: 'dddd HH:mm', + sameElse: 'YYYY年M月D日 HH:mm', + }, + relativeTime: { + future: '%s后', + past: '%s前', + s: '几秒', + m: '1分钟', + mm: '%d分钟', + h: '1小时', + hh: '%d小时', + d: '1天', + dd: '%d天', + M: '1个月', + MM: '%d个月', + y: '1年', + yy: '%d年', + }, +}) + +/** 时间工具 */ +export const dateUtil = dayjs + +export const DATETIME_FORMAT = 'YYYY-MM-DD HH:mm:ss' +export const DATE_FORMAT = 'YYYY-MM-DD' +export const TIME_FORMAT = 'HH:mm' + +/** + * 格式化日期 + * @param _date 日期对象、时间戳或字符串 + * @param format 格式字符串 + * @returns 格式化后的日期字符串 + */ +function _format(_date: dayjs.ConfigType, format: string): string { + if (!_date) { + return _date as any + } + const date = dateUtil(_date) + return date.isValid() ? date.format(format) : (_date as string) +} +/** + * 格式化为日期时间字符串 + * @param date 日期对象、时间戳或字符串 + * @param format 格式字符串,默认为 DATETIME_FORMAT + * @returns 格式化后的日期时间字符串 + */ +export function formatToDatetime(date: dayjs.ConfigType = undefined, format: string = DATETIME_FORMAT): string { + return _format(date, format) +} + +/** + * 格式化为日期字符串 + * @param date 日期对象、时间戳或字符串 + * @param format 格式字符串,默认为 DATE_FORMAT + * @returns 格式化后的日期字符串 + */ +export function formatToDate(date: dayjs.ConfigType = undefined, format: string = DATE_FORMAT): string { + return _format(date, format) +} + +/** + * 格式化为日期字符串 + * @param date 日期对象、时间戳或字符串 + * @param format 格式字符串,默认为 TIME_FORMAT + * @returns 格式化后的日期字符串 + */ +export function formatToTime(date: dayjs.ConfigType = undefined, format: string = TIME_FORMAT): string { + return _format(date, format) +} + +/** + * 时间人性化显示 + * @param date 要格式化的日期 + * @param oppositeDate 参考日期,默认为当前时间 + * @returns 人性化的时间字符串 + */ +export function humanizedDate(date: dayjs.ConfigType, oppositeDate: dayjs.ConfigType = undefined): string { + if (!date || !dateUtil(date).isValid()) { + return '' + } + + const now = oppositeDate ? dateUtil(oppositeDate) : dateUtil() + const diffSeconds = now.diff(date, 'second') + const diffMinutes = now.diff(date, 'minute') + const diffHours = now.diff(date, 'hour') + const diffDays = now.diff(date, 'day') + + if (diffSeconds < 60) { + return `${diffSeconds}秒前` + } + else if (diffMinutes < 60) { + return `${diffMinutes}分钟前` + } + else if (diffHours < 24) { + return `${diffHours}小时前` + } + else if (diffDays < 7) { + return `${diffDays}天前` + } + else { + return formatToDatetime(date) + } +} + +/** + * 获取时辰问候语 + * @returns 根据当前时间返回相应的问候语 + */ +export function getGreeting(): string { + const currentHour = dateUtil().hour() + if (currentHour >= 5 && currentHour < 12) { + return '早上好' + } + else if (currentHour >= 12 && currentHour < 14) { + return '中午好' + } + else if (currentHour >= 14 && currentHour < 18) { + return '下午好' + } + else if (currentHour >= 18 && currentHour < 24) { + return '晚上好' + } + else { + return '深夜了' + } +} diff --git a/src/utils/http.ts b/src/utils/http.ts new file mode 100644 index 0000000..47abba7 --- /dev/null +++ b/src/utils/http.ts @@ -0,0 +1,120 @@ +import type { CustomRequestOptions } from '@/interceptors/request' +import { staticBaseUrl, baseUrl } from '@/utils/index' + +export function http(options: CustomRequestOptions) { + // 1. 返回 Promise 对象 + return new Promise>((resolve, reject) => { + if (options.query?.staticType === 'static') { + options.url = `${staticBaseUrl}${options.url}` + } else if (options.query?.hasPrefix) { + console.log('hasPrefix', options.url) + } else { + options.url = `${baseUrl}${options.url}` + } + uni.request({ + ...options, + dataType: 'json', + // #ifndef MP-WEIXIN + responseType: 'json', + // #endif + // 响应成功 + success(res) { + // 状态码 2xx,参考 axios 的设计 + if (res.statusCode >= 200 && res.statusCode < 300) { + // 2.1 提取核心数据 res.data + resolve(res.data as IResData) + } + else if (res.statusCode === 401) { + // 401错误 -> 清理用户信息,跳转到登录页 + // userStore.clearUserInfo() + // uni.navigateTo({ url: '/pages/login/login' }) + reject(res) + } + else { + // 其他错误 -> 根据后端错误信息轻提示 + !options.hideErrorToast + && uni.showToast({ + icon: 'none', + title: (res.data as IResData).message || '请求错误', + }) + reject(res) + } + }, + // 响应失败 + fail(err) { + uni.showToast({ + icon: 'none', + title: '网络错误,换个网络试试', + }) + reject(err) + }, + }) + }) +} + +/** + * GET 请求 + * @param url 后台地址 + * @param query 请求query参数 + * @param header 请求头,默认为json格式 + * @returns + */ +export function httpGet(url: string, query?: Record, header?: Record, options?: Partial) { + return http({ + url, + query, + method: 'GET', + header, + ...options, + }) +} + +/** + * POST 请求 + * @param url 后台地址 + * @param data 请求body参数 + * @param query 请求query参数,post请求也支持query,很多微信接口都需要 + * @param header 请求头,默认为json格式 + * @returns + */ +export function httpPost(url: string, data?: Record, query?: Record, header?: Record, options?: Partial) { + return http({ + url, + query, + data, + method: 'POST', + header, + ...options, + }) +} +/** + * PUT 请求 + */ +export function httpPut(url: string, data?: Record, query?: Record, header?: Record, options?: Partial) { + return http({ + url, + data, + query, + method: 'PUT', + header, + ...options, + }) +} + +/** + * DELETE 请求(无请求体,仅 query) + */ +export function httpDelete(url: string, query?: Record, header?: Record, options?: Partial) { + return http({ + url, + query, + method: 'DELETE', + header, + ...options, + }) +} + +http.get = httpGet +http.post = httpPost +http.put = httpPut +http.delete = httpDelete diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..2d1565a --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,166 @@ +import { pages, subPackages } from '@/pages.json' +import { isMpWeixin } from './platform' + +export function getLastPage() { + // getCurrentPages() 至少有1个元素,所以不再额外判断 + // const lastPage = getCurrentPages().at(-1) + // 上面那个在低版本安卓中打包会报错,所以改用下面这个【虽然我加了 src/interceptions/prototype.ts,但依然报错】 + const pages = getCurrentPages() + return pages[pages.length - 1] +} + +/** + * 获取当前页面路由的 path 路径和 redirectPath 路径 + * path 如 '/pages/login/index' + * redirectPath 如 '/pages/demo/base/route-interceptor' + */ +export function currRoute() { + const lastPage = getLastPage() + const currRoute = (lastPage as any).$page + // console.log('lastPage.$page:', currRoute) + // console.log('lastPage.$page.fullpath:', currRoute.fullPath) + // console.log('lastPage.$page.options:', currRoute.options) + // console.log('lastPage.options:', (lastPage as any).options) + // 经过多端测试,只有 fullPath 靠谱,其他都不靠谱 + const { fullPath } = currRoute as { fullPath: string } + // console.log(fullPath) + // eg: /pages/login/index?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor (小程序) + // eg: /pages/login/index?redirect=%2Fpages%2Froute-interceptor%2Findex%3Fname%3Dfeige%26age%3D30(h5) + return getUrlObj(fullPath) +} + +function ensureDecodeURIComponent(url: string) { + if (url.startsWith('%')) { + return ensureDecodeURIComponent(decodeURIComponent(url)) + } + return url +} +/** + * 解析 url 得到 path 和 query + * 比如输入url: /pages/login/index?redirect=%2Fpages%2Fdemo%2Fbase%2Froute-interceptor + * 输出: {path: /pages/login/index, query: {redirect: /pages/demo/base/route-interceptor}} + */ +export function getUrlObj(url: string) { + const [path, queryStr] = url.split('?') + // console.log(path, queryStr) + + if (!queryStr) { + return { + path, + query: {}, + } + } + const query: Record = {} + queryStr.split('&').forEach((item) => { + const [key, value] = item.split('=') + // console.log(key, value) + query[key] = ensureDecodeURIComponent(value) // 这里需要统一 decodeURIComponent 一下,可以兼容h5和微信y + }) + return { path, query } +} +/** + * 得到所有的需要登录的 pages,包括主包和分包的 + * 这里设计得通用一点,可以传递 key 作为判断依据,默认是 needLogin, 与 route-block 配对使用 + * 如果没有传 key,则表示所有的 pages,如果传递了 key, 则表示通过 key 过滤 + */ +export function getAllPages(key = 'needLogin') { + // 这里处理主包 + const mainPages = pages + .filter(page => !key || page[key]) + .map(page => ({ + ...page, + path: `/${page.path}`, + })) + + // 这里处理分包 + const subPages: any[] = [] + subPackages.forEach((subPageObj) => { + // console.log(subPageObj) + const { root } = subPageObj + + subPageObj.pages + .filter(page => !key || page[key]) + .forEach((page: { path: string } & Record) => { + subPages.push({ + ...page, + path: `/${root}/${page.path}`, + }) + }) + }) + const result = [...mainPages, ...subPages] + // console.log(`getAllPages by ${key} result: `, result) + return result +} + +/** + * 得到所有的需要登录的 pages,包括主包和分包的 + * 只得到 path 数组 + */ +export const getNeedLoginPages = (): string[] => getAllPages('needLogin').map(page => page.path) + +/** + * 得到所有的需要登录的 pages,包括主包和分包的 + * 只得到 path 数组 + */ +export const needLoginPages: string[] = getAllPages('needLogin').map(page => page.path) + +/** + * 根据微信小程序当前环境,判断应该获取的 baseUrl + */ +export function getEnvBaseUrl() { + // 请求基准地址 + let baseUrl = import.meta.env.VITE_SERVER_BASEURL + + // 微信小程序端环境区分 + if (isMpWeixin) { + const { + miniProgram: { envVersion }, + } = uni.getAccountInfoSync() + + switch (envVersion) { + case 'develop': + baseUrl = import.meta.env.VITE_SERVER_BASEURL__WEIXIN_DEVELOP || baseUrl + break + case 'trial': + baseUrl = import.meta.env.VITE_SERVER_BASEURL__WEIXIN_TRIAL || baseUrl + break + case 'release': + baseUrl = import.meta.env.VITE_SERVER_BASEURL__WEIXIN_RELEASE || baseUrl + break + } + } + + return baseUrl +} + +/** + * 根据微信小程序当前环境,判断应该获取的 UPLOAD_BASEURL + */ +export function getEnvBaseUploadUrl() { + // 请求基准地址 + let baseUploadUrl = import.meta.env.VITE_UPLOAD_BASEURL + + // 微信小程序端环境区分 + if (isMpWeixin) { + const { + miniProgram: { envVersion }, + } = uni.getAccountInfoSync() + + switch (envVersion) { + case 'develop': + baseUploadUrl = import.meta.env.VITE_UPLOAD_BASEURL__WEIXIN_DEVELOP || baseUploadUrl + break + case 'trial': + baseUploadUrl = import.meta.env.VITE_UPLOAD_BASEURL__WEIXIN_TRIAL || baseUploadUrl + break + case 'release': + baseUploadUrl = import.meta.env.VITE_UPLOAD_BASEURL__WEIXIN_RELEASE || baseUploadUrl + break + } + } + + return baseUploadUrl +} + +export const baseUrl = 'https://api.zz.jinzejk.com' +export const staticBaseUrl = 'https://api.static.ycymedu.com' \ No newline at end of file diff --git a/src/utils/platform.ts b/src/utils/platform.ts new file mode 100644 index 0000000..bf82bda --- /dev/null +++ b/src/utils/platform.ts @@ -0,0 +1,25 @@ +/* + * @Author: 菲鸽 + * @Date: 2024-03-28 19:13:55 + * @Last Modified by: 菲鸽 + * @Last Modified time: 2024-03-28 19:24:55 + */ +export const platform = __UNI_PLATFORM__ +export const isH5 = __UNI_PLATFORM__ === 'h5' +export const isApp = __UNI_PLATFORM__ === 'app' +export const isMp = __UNI_PLATFORM__.startsWith('mp-') +export const isMpWeixin = __UNI_PLATFORM__.startsWith('mp-weixin') +export const isMpAplipay = __UNI_PLATFORM__.startsWith('mp-alipay') +export const isMpToutiao = __UNI_PLATFORM__.startsWith('mp-toutiao') + +const PLATFORM = { + platform, + isH5, + isApp, + isMp, + isMpWeixin, + isMpAplipay, + isMpToutiao, +} +export default PLATFORM + diff --git a/src/utils/queryString.ts b/src/utils/queryString.ts new file mode 100644 index 0000000..edf973e --- /dev/null +++ b/src/utils/queryString.ts @@ -0,0 +1,29 @@ +/** + * 将对象序列化为URL查询字符串,用于替代第三方的 qs 库,节省宝贵的体积 + * 支持基本类型值和数组,不支持嵌套对象 + * @param obj 要序列化的对象 + * @returns 序列化后的查询字符串 + */ +export function stringifyQuery(obj: Record): string { + if (!obj || typeof obj !== 'object' || Array.isArray(obj)) + return '' + + return Object.entries(obj) + .filter(([_, value]) => value !== undefined && value !== null) + .map(([key, value]) => { + // 对键进行编码 + const encodedKey = encodeURIComponent(key) + + // 处理数组类型 + if (Array.isArray(value)) { + return value + .filter(item => item !== undefined && item !== null) + .map(item => `${encodedKey}=${encodeURIComponent(item)}`) + .join('&') + } + + // 处理基本类型 + return `${encodedKey}=${encodeURIComponent(value)}` + }) + .join('&') +} diff --git a/src/utils/request.ts b/src/utils/request.ts new file mode 100644 index 0000000..9879f25 --- /dev/null +++ b/src/utils/request.ts @@ -0,0 +1,78 @@ +import type { CustomRequestOptions } from '@/interceptors/request' + +/** + * 请求方法: 主要是对 uni.request 的封装,去适配 openapi-ts-request 的 request 方法 + * @param options 请求参数 + * @returns 返回 Promise 对象 + */ +function http(options: CustomRequestOptions) { + // 1. 返回 Promise 对象 + return new Promise((resolve, reject) => { + uni.request({ + ...options, + dataType: 'json', + // #ifndef MP-WEIXIN + responseType: 'json', + // #endif + // 响应成功 + success(res) { + // 状态码 2xx,参考 axios 的设计 + if (res.statusCode >= 200 && res.statusCode < 300) { + // 2.1 提取核心数据 res.data + resolve(res.data as T) + } + else if (res.statusCode === 401) { + // 401错误 -> 清理用户信息,跳转到登录页 + // userStore.clearUserInfo() + // uni.navigateTo({ url: '/pages/login/login' }) + reject(res) + } + else { + // 其他错误 -> 根据后端错误信息轻提示 + !options.hideErrorToast + && uni.showToast({ + icon: 'none', + title: (res.data as T & { msg?: string })?.msg || '请求错误', + }) + reject(res) + } + }, + // 响应失败 + fail(err) { + uni.showToast({ + icon: 'none', + title: '网络错误,换个网络试试', + }) + reject(err) + }, + }) + }) +} + +/* + * openapi-ts-request 工具的 request 跨客户端适配方法 + */ +export default function request( + url: string, + options: Omit & { + params?: Record + headers?: Record + }, +) { + const requestOptions = { + url, + ...options, + } + + if (options.params) { + requestOptions.query = requestOptions.params + delete requestOptions.params + } + + if (options.headers) { + requestOptions.header = options.headers + delete requestOptions.headers + } + + return http(requestOptions) +} diff --git a/src/utils/request/alova.ts b/src/utils/request/alova.ts new file mode 100644 index 0000000..039326d --- /dev/null +++ b/src/utils/request/alova.ts @@ -0,0 +1,111 @@ +import type { uniappRequestAdapter } from '@alova/adapter-uniapp' +import type { IResponse } from './types' +import AdapterUniapp from '@alova/adapter-uniapp' +import { createAlova } from 'alova' +import { createServerTokenAuthentication } from 'alova/client' +import VueHook from 'alova/vue' +import { toast } from '@/utils/toast' +import { ContentTypeEnum, ResultEnum, ShowMessage } from './enum' + +// 配置动态Tag +export const API_DOMAINS = { + DEFAULT: import.meta.env.VITE_SERVER_BASEURL, + SECONDARY: import.meta.env.VITE_API_SECONDARY_URL, +} + +/** + * 创建请求实例 + */ +const { onAuthRequired, onResponseRefreshToken } = createServerTokenAuthentication< + typeof VueHook, + typeof uniappRequestAdapter +>({ + refreshTokenOnError: { + isExpired: (error) => { + return error.response?.status === ResultEnum.Unauthorized + }, + handler: async () => { + try { + // await authLogin(); + } + catch (error) { + // 切换到登录页 + await uni.reLaunch({ url: '/pages/common/login/index' }) + throw error + } + }, + }, +}) + +/** + * alova 请求实例 + */ +const alovaInstance = createAlova({ + baseURL: import.meta.env.VITE_API_BASE_URL, + ...AdapterUniapp(), + timeout: 5000, + statesHook: VueHook, + + beforeRequest: onAuthRequired((method) => { + // 设置默认 Content-Type + method.config.headers = { + ContentType: ContentTypeEnum.JSON, + Accept: 'application/json, text/plain, */*', + ...method.config.headers, + } + + const { config } = method + const ignoreAuth = !config.meta?.ignoreAuth + console.log('ignoreAuth===>', ignoreAuth) + // 处理认证信息 自行处理认证问题 + if (ignoreAuth) { + const token = 'getToken()' + if (!token) { + throw new Error('[请求错误]:未登录') + } + // method.config.headers.token = token; + } + + // 处理动态域名 + if (config.meta?.domain) { + method.baseURL = config.meta.domain + console.log('当前域名', method.baseURL) + } + }), + + responded: onResponseRefreshToken((response, method) => { + const { config } = method + const { requestType } = config + const { + statusCode, + data: rawData, + errMsg, + } = response as UniNamespace.RequestSuccessCallbackResult + + // 处理特殊请求类型(上传/下载) + if (requestType === 'upload' || requestType === 'download') { + return response + } + + // 处理 HTTP 状态码错误 + if (statusCode !== 200) { + const errorMessage = ShowMessage(statusCode) || `HTTP请求错误[${statusCode}]` + console.error('errorMessage===>', errorMessage) + toast.error(errorMessage) + throw new Error(`${errorMessage}:${errMsg}`) + } + + // 处理业务逻辑错误 + const { code, message, data } = rawData as IResponse + if (code !== ResultEnum.Success) { + if (config.meta?.toast !== false) { + toast.warning(message) + } + throw new Error(`请求错误[${code}]:${message}`) + } + // 处理成功响应,返回业务数据 + return data + }), +}) + +export const http = alovaInstance diff --git a/src/utils/request/enum.ts b/src/utils/request/enum.ts new file mode 100644 index 0000000..1868fe0 --- /dev/null +++ b/src/utils/request/enum.ts @@ -0,0 +1,66 @@ +export enum ResultEnum { + Success = 0, // 成功 + Error = 400, // 错误 + Unauthorized = 401, // 未授权 + Forbidden = 403, // 禁止访问(原为forbidden) + NotFound = 404, // 未找到(原为notFound) + MethodNotAllowed = 405, // 方法不允许(原为methodNotAllowed) + RequestTimeout = 408, // 请求超时(原为requestTimeout) + InternalServerError = 500, // 服务器错误(原为internalServerError) + NotImplemented = 501, // 未实现(原为notImplemented) + BadGateway = 502, // 网关错误(原为badGateway) + ServiceUnavailable = 503, // 服务不可用(原为serviceUnavailable) + GatewayTimeout = 504, // 网关超时(原为gatewayTimeout) + HttpVersionNotSupported = 505, // HTTP版本不支持(原为httpVersionNotSupported) +} +export enum ContentTypeEnum { + JSON = 'application/json;charset=UTF-8', + FORM_URLENCODED = 'application/x-www-form-urlencoded;charset=UTF-8', + FORM_DATA = 'multipart/form-data;charset=UTF-8', +} +/** + * 根据状态码,生成对应的错误信息 + * @param {number|string} status 状态码 + * @returns {string} 错误信息 + */ +export function ShowMessage(status: number | string): string { + let message: string + switch (status) { + case 400: + message = '请求错误(400)' + break + case 401: + message = '未授权,请重新登录(401)' + break + case 403: + message = '拒绝访问(403)' + break + case 404: + message = '请求出错(404)' + break + case 408: + message = '请求超时(408)' + break + case 500: + message = '服务器错误(500)' + break + case 501: + message = '服务未实现(501)' + break + case 502: + message = '网络错误(502)' + break + case 503: + message = '服务不可用(503)' + break + case 504: + message = '网络超时(504)' + break + case 505: + message = 'HTTP版本不受支持(505)' + break + default: + message = `连接出错(${status})!` + } + return `${message},请检查网络或联系管理员!` +} diff --git a/src/utils/request/types.ts b/src/utils/request/types.ts new file mode 100644 index 0000000..2a4154a --- /dev/null +++ b/src/utils/request/types.ts @@ -0,0 +1,22 @@ +// 通用响应格式 +export interface IResponse { + code: number | string + data: T + message: string + status: string | number +} + +// 分页请求参数 +export interface PageParams { + page: number + pageSize: number + [key: string]: any +} + +// 分页响应数据 +export interface PageResult { + list: T[] + total: number + page: number + pageSize: number +} diff --git a/src/utils/toast.ts b/src/utils/toast.ts new file mode 100644 index 0000000..e524b00 --- /dev/null +++ b/src/utils/toast.ts @@ -0,0 +1,65 @@ +/** + * toast 弹窗组件 + * 支持 success/error/warning/info 四种状态 + * 可配置 duration, position 等参数 + */ + +type ToastType = 'success' | 'error' | 'warning' | 'info' + +interface ToastOptions { + type?: ToastType + duration?: number + position?: 'top' | 'middle' | 'bottom' + icon?: 'success' | 'error' | 'none' | 'loading' | 'fail' | 'exception' + message: string +} + +export function showToast(options: ToastOptions | string) { + const defaultOptions: ToastOptions = { + type: 'info', + duration: 2000, + position: 'middle', + message: '', + } + const mergedOptions + = typeof options === 'string' + ? { ...defaultOptions, message: options } + : { ...defaultOptions, ...options } + // 映射position到uniapp支持的格式 + const positionMap: Record = { + top: 'top', + middle: 'center', + bottom: 'bottom', + } + + // 映射图标类型 + const iconMap: Record< + ToastType, + 'success' | 'error' | 'none' | 'loading' | 'fail' | 'exception' + > = { + success: 'success', + error: 'error', + warning: 'fail', + info: 'none', + } + + // 调用uni.showToast显示提示 + uni.showToast({ + title: mergedOptions.message, + duration: mergedOptions.duration, + position: positionMap[mergedOptions.position], + icon: mergedOptions.icon || iconMap[mergedOptions.type], + mask: true, + }) +} + +export const toast = { + success: (message: string, options?: Omit) => + showToast({ ...options, type: 'success', message }), + error: (message: string, options?: Omit) => + showToast({ ...options, type: 'error', message }), + warning: (message: string, options?: Omit) => + showToast({ ...options, type: 'warning', message }), + info: (message: string, options?: Omit) => + showToast({ ...options, type: 'info', message }), +} diff --git a/src/utils/tools.ts b/src/utils/tools.ts new file mode 100644 index 0000000..859992c --- /dev/null +++ b/src/utils/tools.ts @@ -0,0 +1,25 @@ +/** + * 获取设备基础信息 + * + * @see [uni.getDeviceInfo](https://uniapp.dcloud.net.cn/api/system/getDeviceInfo.html) + */ +export function getDeviceInfo() { + if (uni.getDeviceInfo || uni.canIUse('getDeviceInfo')) { + return uni.getDeviceInfo() + } else { + return uni.getSystemInfoSync() + } + } + + /** + * 获取窗口信息 + * + * @see [uni.getWindowInfo](https://uniapp.dcloud.net.cn/api/system/getWindowInfo.html) + */ + export function getWindowInfo() { + if (uni.getWindowInfo || uni.canIUse('getWindowInfo')) { + return uni.getWindowInfo() + } else { + return uni.getSystemInfoSync() + } + } \ No newline at end of file diff --git a/src/utils/uploadFile.ts b/src/utils/uploadFile.ts new file mode 100644 index 0000000..416d39c --- /dev/null +++ b/src/utils/uploadFile.ts @@ -0,0 +1,324 @@ +import { toast } from './toast' + +/** + * 文件上传钩子函数使用示例 + * @example + * const { loading, error, data, progress, run } = useUpload( + * uploadUrl, + * {}, + * { + * maxSize: 5, // 最大5MB + * sourceType: ['album'], // 仅支持从相册选择 + * onProgress: (p) => console.log(`上传进度:${p}%`), + * onSuccess: (res) => console.log('上传成功', res), + * onError: (err) => console.error('上传失败', err), + * }, + * ) + */ + +/** + * 上传文件的URL配置 + */ +export const uploadFileUrl = { + /** 用户头像上传地址 */ + USER_AVATAR: `${import.meta.env.VITE_SERVER_BASEURL}/user/avatar`, +} + +/** + * 通用文件上传函数(支持直接传入文件路径) + * @param url 上传地址 + * @param filePath 本地文件路径 + * @param formData 额外表单数据 + * @param options 上传选项 + */ +export function useFileUpload(url: string, filePath: string, formData: Record = {}, options: Omit = {}) { + return useUpload( + url, + formData, + { + ...options, + sourceType: ['album'], + sizeType: ['original'], + }, + filePath, + ) +} + +export interface UploadOptions { + /** 最大可选择的图片数量,默认为1 */ + count?: number + /** 所选的图片的尺寸,original-原图,compressed-压缩图 */ + sizeType?: Array<'original' | 'compressed'> + /** 选择图片的来源,album-相册,camera-相机 */ + sourceType?: Array<'album' | 'camera'> + /** 文件大小限制,单位:MB */ + maxSize?: number // + /** 上传进度回调函数 */ + onProgress?: (progress: number) => void + /** 上传成功回调函数 */ + onSuccess?: (res: Record) => void + /** 上传失败回调函数 */ + onError?: (err: Error | UniApp.GeneralCallbackResult) => void + /** 上传完成回调函数(无论成功失败) */ + onComplete?: () => void +} + +/** + * 文件上传钩子函数 + * @template T 上传成功后返回的数据类型 + * @param url 上传地址 + * @param formData 额外的表单数据 + * @param options 上传选项 + * @returns 上传状态和控制对象 + */ +export function useUpload(url: string, formData: Record = {}, options: UploadOptions = {}, + /** 直接传入文件路径,跳过选择器 */ + directFilePath?: string) { + /** 上传中状态 */ + const loading = ref(false) + /** 上传错误状态 */ + const error = ref(false) + /** 上传成功后的响应数据 */ + const data = ref() + /** 上传进度(0-100) */ + const progress = ref(0) + + /** 解构上传选项,设置默认值 */ + const { + /** 最大可选择的图片数量 */ + count = 1, + /** 所选的图片的尺寸 */ + sizeType = ['original', 'compressed'], + /** 选择图片的来源 */ + sourceType = ['album', 'camera'], + /** 文件大小限制(MB) */ + maxSize = 10, + /** 进度回调 */ + onProgress, + /** 成功回调 */ + onSuccess, + /** 失败回调 */ + onError, + /** 完成回调 */ + onComplete, + } = options + + /** + * 检查文件大小是否超过限制 + * @param size 文件大小(字节) + * @returns 是否通过检查 + */ + const checkFileSize = (size: number) => { + const sizeInMB = size / 1024 / 1024 + if (sizeInMB > maxSize) { + toast.warning(`文件大小不能超过${maxSize}MB`) + return false + } + return true + } + /** + * 触发文件选择和上传 + * 根据平台使用不同的选择器: + * - 微信小程序使用 chooseMedia + * - 其他平台使用 chooseImage + */ + const run = () => { + if (directFilePath) { + // 直接使用传入的文件路径 + loading.value = true + progress.value = 0 + uploadFile({ + url, + tempFilePath: directFilePath, + formData, + data, + error, + loading, + progress, + onProgress, + onSuccess, + onError, + onComplete, + }) + return + } + + // #ifdef MP-WEIXIN + // 微信小程序环境下使用 chooseMedia API + uni.chooseMedia({ + count, + mediaType: ['image'], // 仅支持图片类型 + sourceType, + success: (res) => { + const file = res.tempFiles[0] + // 检查文件大小是否符合限制 + if (!checkFileSize(file.size)) + return + + // 开始上传 + loading.value = true + progress.value = 0 + uploadFile({ + url, + tempFilePath: file.tempFilePath, + formData, + data, + error, + loading, + progress, + onProgress, + onSuccess, + onError, + onComplete, + }) + }, + fail: (err) => { + console.error('选择媒体文件失败:', err) + error.value = true + onError?.(err) + }, + }) + // #endif + + // #ifndef MP-WEIXIN + // 非微信小程序环境下使用 chooseImage API + uni.chooseImage({ + count, + sizeType, + sourceType, + success: (res) => { + console.log('选择图片成功:', res) + + // 开始上传 + loading.value = true + progress.value = 0 + uploadFile({ + url, + tempFilePath: res.tempFilePaths[0], + formData, + data, + error, + loading, + progress, + onProgress, + onSuccess, + onError, + onComplete, + }) + }, + fail: (err) => { + console.error('选择图片失败:', err) + error.value = true + onError?.(err) + }, + }) + // #endif + } + + return { loading, error, data, progress, run } +} + +/** + * 文件上传选项接口 + * @template T 上传成功后返回的数据类型 + */ +interface UploadFileOptions { + /** 上传地址 */ + url: string + /** 临时文件路径 */ + tempFilePath: string + /** 额外的表单数据 */ + formData: Record + /** 上传成功后的响应数据 */ + data: Ref + /** 上传错误状态 */ + error: Ref + /** 上传中状态 */ + loading: Ref + /** 上传进度(0-100) */ + progress: Ref + /** 上传进度回调 */ + onProgress?: (progress: number) => void + /** 上传成功回调 */ + onSuccess?: (res: Record) => void + /** 上传失败回调 */ + onError?: (err: Error | UniApp.GeneralCallbackResult) => void + /** 上传完成回调 */ + onComplete?: () => void +} + +/** + * 执行文件上传 + * @template T 上传成功后返回的数据类型 + * @param options 上传选项 + */ +function uploadFile({ + url, + tempFilePath, + formData, + data, + error, + loading, + progress, + onProgress, + onSuccess, + onError, + onComplete, +}: UploadFileOptions) { + try { + // 创建上传任务 + const uploadTask = uni.uploadFile({ + url, + filePath: tempFilePath, + name: 'file', // 文件对应的 key + formData, + header: { + // H5环境下不需要手动设置Content-Type,让浏览器自动处理multipart格式 + // #ifndef H5 + 'Content-Type': 'multipart/form-data', + // #endif + }, + // 确保文件名称合法 + success: (uploadFileRes) => { + console.log('上传文件成功:', uploadFileRes) + try { + // 解析响应数据 + const { data: _data } = JSON.parse(uploadFileRes.data) + // 上传成功 + data.value = _data as T + onSuccess?.(_data) + } + catch (err) { + // 响应解析错误 + console.error('解析上传响应失败:', err) + error.value = true + onError?.(new Error('上传响应解析失败')) + } + }, + fail: (err) => { + // 上传请求失败 + console.error('上传文件失败:', err) + error.value = true + onError?.(err) + }, + complete: () => { + // 无论成功失败都执行 + loading.value = false + onComplete?.() + }, + }) + + // 监听上传进度 + uploadTask.onProgressUpdate((res) => { + progress.value = res.progress + onProgress?.(res.progress) + }) + } + catch (err) { + // 创建上传任务失败 + console.error('创建上传任务失败:', err) + error.value = true + loading.value = false + onError?.(new Error('创建上传任务失败')) + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..22a48c5 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,41 @@ +{ + "compilerOptions": { + "composite": true, + "lib": ["esnext", "dom"], + "baseUrl": ".", + "module": "ESNext", + "moduleResolution": "Node", + "paths": { + "@/*": ["./src/*"], + "@img/*": ["./src/static/*"] + }, + "resolveJsonModule": true, + "types": [ + "@dcloudio/types", + "@uni-helper/uni-types", + "@types/wechat-miniprogram", + "wot-design-uni/global.d.ts", + "z-paging/types", + "./src/typings.d.ts" + ], + "allowJs": true, + "noImplicitThis": true, + "outDir": "dist", + "sourceMap": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true + }, + "vueCompilerOptions": { + "plugins": ["@uni-helper/uni-types/volar-plugin"] + }, + "include": [ + "src/**/*.ts", + "src/**/*.js", + "src/**/*.d.ts", + "src/**/*.tsx", + "src/**/*.jsx", + "src/**/*.vue", + "src/**/*.json" + ], + "exclude": ["node_modules"] +} diff --git a/uno.config.ts b/uno.config.ts new file mode 100644 index 0000000..2ddbd83 --- /dev/null +++ b/uno.config.ts @@ -0,0 +1,65 @@ +// https://www.npmjs.com/package/@uni-helper/unocss-preset-uni +import { presetUni } from '@uni-helper/unocss-preset-uni' +import { + defineConfig, + presetAttributify, + presetIcons, + transformerDirectives, + transformerVariantGroup, +} from 'unocss' + +export default defineConfig({ + presets: [ + presetUni({ + attributify: { + // prefix: 'fg-', // 如果加前缀,则需要在代码里面使用 `fg-` 前缀,如:
+ prefixedOnly: true, + }, + }), + presetIcons({ + scale: 1.2, + warn: true, + extraProperties: { + 'display': 'inline-block', + 'vertical-align': 'middle', + }, + }), + // 支持css class属性化 + presetAttributify(), + ], + transformers: [ + // 启用指令功能:主要用于支持 @apply、@screen 和 theme() 等 CSS 指令 + transformerDirectives(), + // 启用 () 分组功能 + // 支持css class组合,eg: `
测试 unocss
` + transformerVariantGroup(), + ], + shortcuts: [ + { + center: 'flex justify-center items-center', + }, + ], + safelist: [], + rules: [ + [ + 'p-safe', + { + padding: + 'env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left)', + }, + ], + ['pt-safe', { 'padding-top': 'env(safe-area-inset-top)' }], + ['pb-safe', { 'padding-bottom': 'env(safe-area-inset-bottom)' }], + ], + theme: { + colors: { + /** 主题色,用法如: text-primary */ + primary: 'var(--wot-color-theme,#0957DE)', + }, + fontSize: { + /** 提供更小号的字体,用法如:text-2xs */ + '2xs': ['20rpx', '28rpx'], + '3xs': ['18rpx', '26rpx'], + }, + }, +}) diff --git a/vite-plugins/copyNativeRes.ts b/vite-plugins/copyNativeRes.ts new file mode 100644 index 0000000..5a461d2 --- /dev/null +++ b/vite-plugins/copyNativeRes.ts @@ -0,0 +1,41 @@ +import path from 'node:path' +import process from 'node:process' +import fs from 'fs-extra' + +export function copyNativeRes() { + const waitPath = path.resolve(__dirname, '../src/nativeResources') + const buildPath = path.resolve( + __dirname, + '../dist', + process.env.NODE_ENV === 'production' ? 'build' : 'dev', + process.env.UNI_PLATFORM!, + 'nativeResources', + ) + + return { + enforce: 'post', + async writeBundle() { + try { + // 检查源目录是否存在 + const sourceExists = await fs.pathExists(waitPath) + if (!sourceExists) { + console.warn(`[copyNativeRes] 警告:源目录 "${waitPath}" 不存在,跳过复制操作。`) + return + } + + // 确保目标目录及中间目录存在 + await fs.ensureDir(buildPath) + console.log(`[copyNativeRes] 确保目标目录存在:${buildPath}`) + + // 执行文件夹复制 + await fs.copy(waitPath, buildPath) + console.log( + `[copyNativeRes] 成功将 nativeResources 目录中的资源移动到构建目录:${buildPath}`, + ) + } + catch (error) { + console.error(`[copyNativeRes] 复制资源失败:`, error) + } + }, + } +} diff --git a/vite-plugins/updatePackageJson.ts b/vite-plugins/updatePackageJson.ts new file mode 100644 index 0000000..bd93d6a --- /dev/null +++ b/vite-plugins/updatePackageJson.ts @@ -0,0 +1,37 @@ +// src/plugins/updatePackageJson.ts +import type { Plugin } from 'vite' +import fs from 'node:fs/promises' +import path from 'node:path' +import process from 'node:process' + +function updatePackageJson(): Plugin { + return { + name: 'update-package-json', + async buildStart() { + // 只在生产环境构建时执行 + if (process.env.NODE_ENV !== 'production') + return + + const packageJsonPath = path.resolve(process.cwd(), 'package.json') + + try { + // 读取并解析 package.json + const content = await fs.readFile(packageJsonPath, 'utf-8') + const packageJson = JSON.parse(content) + + // 更新时间戳(使用 ISO 格式或自定义格式) + packageJson['update-time'] = new Date().toISOString().split('T')[0] // YYYY-MM-DD + + // 写回文件(保持 2 空格缩进) + await fs.writeFile(packageJsonPath, `${JSON.stringify(packageJson, null, 2)}\n`, 'utf-8') + + console.log(`[update-package-json] 更新时间戳: ${packageJson['update-time']}`) + } + catch (error) { + console.error('[update-package-json] 插件执行失败:', error) + } + }, + } +} + +export default updatePackageJson diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..905acf6 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,185 @@ +import path from 'node:path' +import process from 'node:process' +import Uni from '@dcloudio/vite-plugin-uni' +import Components from '@uni-helper/vite-plugin-uni-components' +// @see https://uni-helper.js.org/vite-plugin-uni-layouts +import UniLayouts from '@uni-helper/vite-plugin-uni-layouts' +// @see https://github.com/uni-helper/vite-plugin-uni-manifest +import UniManifest from '@uni-helper/vite-plugin-uni-manifest' +// @see https://uni-helper.js.org/vite-plugin-uni-pages +import UniPages from '@uni-helper/vite-plugin-uni-pages' +// @see https://github.com/uni-helper/vite-plugin-uni-platform +// 需要与 @uni-helper/vite-plugin-uni-pages 插件一起使用 +import UniPlatform from '@uni-helper/vite-plugin-uni-platform' +/** + * 分包优化、模块异步跨包调用、组件异步跨包引用 + * @see https://github.com/uni-ku/bundle-optimizer + */ +import Optimization from '@uni-ku/bundle-optimizer' +import dayjs from 'dayjs' +import { visualizer } from 'rollup-plugin-visualizer' +import AutoImport from 'unplugin-auto-import/vite' +import { defineConfig, loadEnv } from 'vite' +import ViteRestart from 'vite-plugin-restart' +import updatePackageJson from './vite-plugins/updatePackageJson' + +// https://vitejs.dev/config/ +export default async ({ command, mode }) => { + // @see https://unocss.dev/ + const UnoCSS = (await import('unocss/vite')).default + // console.log(mode === process.env.NODE_ENV) // true + + // mode: 区分生产环境还是开发环境 + console.log('command, mode -> ', command, mode) + // pnpm dev:h5 时得到 => serve development + // pnpm build:h5 时得到 => build production + // pnpm dev:mp-weixin 时得到 => build development (注意区别,command为build) + // pnpm build:mp-weixin 时得到 => build production + // pnpm dev:app 时得到 => build development (注意区别,command为build) + // pnpm build:app 时得到 => build production + // dev 和 build 命令可以分别使用 .env.development 和 .env.production 的环境变量 + + const { UNI_PLATFORM } = process.env + console.log('UNI_PLATFORM -> ', UNI_PLATFORM) // 得到 mp-weixin, h5, app 等 + + const env = loadEnv(mode, path.resolve(process.cwd(), 'env')) + const { + VITE_APP_PORT, + VITE_SERVER_BASEURL, + VITE_DELETE_CONSOLE, + VITE_SHOW_SOURCEMAP, + VITE_APP_PUBLIC_BASE, + VITE_APP_PROXY, + VITE_APP_PROXY_PREFIX, + } = env + console.log('环境变量 env -> ', env) + + return defineConfig({ + envDir: './env', // 自定义env目录 + base: VITE_APP_PUBLIC_BASE, + plugins: [ + UniPages({ + exclude: ['**/components/**/**.*'], + routeBlockLang: 'json5', // 虽然设了默认值,但是vue文件还是要加上 lang="json5", 这样才能很好地格式化 + // homePage 通过 vue 文件的 route-block 的type="home"来设定 + // pages 目录为 src/pages,分包目录不能配置在pages目录下 + subPackages: ['src/login-sub'], // 是个数组,可以配置多个,但是不能为pages里面的目录 + dts: 'src/types/uni-pages.d.ts', + }), + UniLayouts(), + UniPlatform(), + UniManifest(), + // UniXXX 需要在 Uni 之前引入 + { + // 临时解决 dcloudio 官方的 @dcloudio/uni-mp-compiler 出现的编译 BUG + // 参考 github issue: https://github.com/dcloudio/uni-app/issues/4952 + // 自定义插件禁用 vite:vue 插件的 devToolsEnabled,强制编译 vue 模板时 inline 为 true + name: 'fix-vite-plugin-vue', + configResolved(config) { + const plugin = config.plugins.find(p => p.name === 'vite:vue') + if (plugin && plugin.api && plugin.api.options) { + plugin.api.options.devToolsEnabled = false + } + }, + }, + UnoCSS(), + AutoImport({ + imports: ['vue', 'uni-app'], + dts: 'src/types/auto-import.d.ts', + dirs: ['src/hooks'], // 自动导入 hooks + vueTemplate: true, // default false + }), + // Optimization 插件需要 page.json 文件,故应在 UniPages 插件之后执行 + Optimization({ + enable: { + 'optimization': true, + 'async-import': true, + 'async-component': true, + }, + dts: { + base: 'src/types', + }, + logger: false, + }), + + ViteRestart({ + // 通过这个插件,在修改vite.config.js文件则不需要重新运行也生效配置 + restart: ['vite.config.js'], + }), + // h5环境增加 BUILD_TIME 和 BUILD_BRANCH + UNI_PLATFORM === 'h5' && { + name: 'html-transform', + transformIndexHtml(html) { + return html.replace('%BUILD_TIME%', dayjs().format('YYYY-MM-DD HH:mm:ss')) + }, + }, + // 打包分析插件,h5 + 生产环境才弹出 + UNI_PLATFORM === 'h5' + && mode === 'production' + && visualizer({ + filename: './node_modules/.cache/visualizer/stats.html', + open: true, + gzipSize: true, + brotliSize: true, + }), + // 只有在 app 平台时才启用 copyNativeRes 插件 + // UNI_PLATFORM === 'app' && copyNativeRes(), + Components({ + extensions: ['vue'], + deep: true, // 是否递归扫描子目录, + directoryAsNamespace: false, // 是否把目录名作为命名空间前缀,true 时组件名为 目录名+组件名, + dts: 'src/types/components.d.ts', // 自动生成的组件类型声明文件路径(用于 TypeScript 支持) + }), + Uni(), + updatePackageJson(), + ], + define: { + __UNI_PLATFORM__: JSON.stringify(UNI_PLATFORM), + __VITE_APP_PROXY__: JSON.stringify(VITE_APP_PROXY), + }, + css: { + postcss: { + plugins: [ + // autoprefixer({ + // // 指定目标浏览器 + // overrideBrowserslist: ['> 1%', 'last 2 versions'], + // }), + ], + }, + }, + + resolve: { + alias: { + '@': path.join(process.cwd(), './src'), + '@img': path.join(process.cwd(), './src/static/images'), + }, + }, + server: { + host: '0.0.0.0', + hmr: true, + port: Number.parseInt(VITE_APP_PORT, 10), + // 仅 H5 端生效,其他端不生效(其他端走build,不走devServer) + proxy: JSON.parse(VITE_APP_PROXY) + ? { + [VITE_APP_PROXY_PREFIX]: { + target: VITE_SERVER_BASEURL, + changeOrigin: true, + rewrite: path => path.replace(new RegExp(`^${VITE_APP_PROXY_PREFIX}`), ''), + }, + } + : undefined, + }, + esbuild: { + drop: VITE_DELETE_CONSOLE === 'true' ? ['console', 'debugger'] : ['debugger'], + }, + build: { + sourcemap: false, + // 方便非h5端调试 + // sourcemap: VITE_SHOW_SOURCEMAP === 'true', // 默认是false + target: 'es6', + // 开发环境不用压缩 + minify: mode === 'development' ? false : 'esbuild', + + }, + }) +}