commit 6b790885172e880c1b91de0d4e6c92602015ef96 Author: Mu Yi Date: Thu Sep 18 09:26:30 2025 +0800 feat: init 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 0000000..64533df Binary files /dev/null and b/src/static/images/map/map-icon.png differ 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 0000000..fd1e942 Binary files /dev/null and b/src/static/tabbar/example.png differ diff --git a/src/static/tabbar/exampleHL.png b/src/static/tabbar/exampleHL.png new file mode 100644 index 0000000..7501011 Binary files /dev/null and b/src/static/tabbar/exampleHL.png differ diff --git a/src/static/tabbar/home.png b/src/static/tabbar/home.png new file mode 100644 index 0000000..8f82e21 Binary files /dev/null and b/src/static/tabbar/home.png differ diff --git a/src/static/tabbar/homeHL.png b/src/static/tabbar/homeHL.png new file mode 100644 index 0000000..26d3761 Binary files /dev/null and b/src/static/tabbar/homeHL.png differ diff --git a/src/static/tabbar/personal.png b/src/static/tabbar/personal.png new file mode 100644 index 0000000..0a569a2 Binary files /dev/null and b/src/static/tabbar/personal.png differ diff --git a/src/static/tabbar/personalHL.png b/src/static/tabbar/personalHL.png new file mode 100644 index 0000000..8c3e66e Binary files /dev/null and b/src/static/tabbar/personalHL.png differ diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 0000000..74b1b2f --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1,17 @@ +import { createPinia } from 'pinia' +import { createPersistedState } from 'pinia-plugin-persistedstate' // 数据持久化 + +const store = createPinia() +store.use( + createPersistedState({ + storage: { + getItem: uni.getStorageSync, + setItem: uni.setStorageSync, + }, + }), +) + +export default store + +// 模块统一导出 +export * from './user' diff --git a/src/store/user.ts b/src/store/user.ts new file mode 100644 index 0000000..af95438 --- /dev/null +++ b/src/store/user.ts @@ -0,0 +1,64 @@ +import type { IUserInfoVo } from '@/api/types/login' +import { defineStore } from 'pinia' +import { ref } from 'vue' + +import { toast } from '@/utils/toast' + +// 初始化状态 +const userInfoState: IUserInfoVo = { + id: 0, + username: '', + avatar: '/static/images/default-avatar.png', + token: '', + openId:'', +} + +export const useUserStore = defineStore( + 'user', + () => { + // 定义用户信息 + 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', + + }, + }) +}