Compare commits

..

No commits in common. "main" and "v2.10.1" have entirely different histories.

72 changed files with 2818 additions and 4172 deletions

View File

@ -1,3 +0,0 @@
module.exports = {
extends: ['@commitlint/config-conventional'],
}

60
.github/release.yml vendored
View File

@ -1,31 +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]
- 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']

View File

@ -1,14 +1,13 @@
name: Auto Merge Main to Other Branches
name: Auto Merge Base to Other Branches
on:
push:
branches:
- main
- base
workflow_dispatch: # 手动触发
jobs:
merge-to-i18n:
name: Merge main into i18n
auto-merge:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
@ -17,64 +16,34 @@ jobs:
fetch-depth: 0
token: ${{ secrets.GH_TOKEN_AUTO_MERGE }}
- name: Merge main into i18n
- name: Merge base into main
run: |
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git checkout main
git merge base --no-ff -m "Auto merge base into main"
git push origin main
- name: Merge base 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 merge base --no-ff -m "Auto merge base 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
- name: Merge base into tabbar
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
git checkout tabbar
git merge base --no-ff -m "Auto merge base into tabbar"
git push origin tabbar
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
- name: Merge base into spa
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
git checkout spa
git merge base --no-ff -m "Auto merge base into spa"
git push origin spa

2
.gitignore vendored
View File

@ -27,7 +27,7 @@ dist
docs/.vitepress/dist
docs/.vitepress/cache
src/types
types
# lock 文件还是不要了,我主要的版本写死就好了
# pnpm-lock.yaml

View File

@ -1 +0,0 @@
npx --no-install commitlint --edit "$1"

View File

@ -1 +0,0 @@
npx lint-staged --allow-empty

4
.npmrc
View File

@ -1,5 +1,5 @@
# registry = https://registry.npmjs.org
registry = https://registry.npmmirror.com
registry = https://registry.npmjs.org
# registry = https://registry.npmmirror.com
strict-peer-dependencies=false
auto-install-peers=true

View File

@ -1,5 +1,3 @@
node_modules
# unplugin-auto-import 生成的类型文件,每次提交都改变,所以加入这里吧,与 .gitignore 配合使用
auto-import.d.ts

View File

@ -10,7 +10,7 @@ module.exports = {
htmlWhitespaceSensitivity: 'ignore',
overrides: [
{
files: '*.{json,jsonc}',
files: '*.json',
options: {
trailingComma: 'none',
},

View File

@ -13,8 +13,6 @@
"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"
"streetsidesoftware.code-spell-checker"
]
}

79
.vscode/settings.json vendored
View File

@ -1,7 +1,14 @@
{
// prettier
"editor.defaultFormatter": "esbenp.prettier-vscode",
//
"editor.formatOnSave": true,
//
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.fixAll.eslint": "explicit",
"source.fixAll.stylelint": "explicit"
},
// stylelint
"stylelint.validate": ["css", "scss", "vue", "html"], // package.jsonscripts
"stylelint.enable": true,
@ -34,14 +41,12 @@
"commitlint",
"dcloudio",
"iconfont",
"oxlint",
"qrcode",
"refresherrefresh",
"scrolltolower",
"tabbar",
"Toutiao",
"unibest",
"uview",
"uvui",
"Wechat",
"WechatMiniprogram",
@ -51,69 +56,7 @@
"explorer.fileNesting.enabled": true,
"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*"
},
// //
// "prettier.enable": true,
// "editor.formatOnSave": true,
// //
// "editor.codeActionsOnSave": {
// "source.fixAll": "explicit",
// "source.fixAll.eslint": "explicit",
// "source.fixAll.stylelint": "explicit"
// },
// 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"
]
"package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,LICENSE,.gitattributes,.gitignore,.gitpod.yml,CNAME,.npmrc,.browserslistrc",
".eslintrc.cjs": ".eslintignore,.prettierignore,.stylelintignore,.commitlintrc.*,.prettierrc.*,.stylelintrc.*,.eslintrc-auto-import.json,.editorconfig,.commitlint.cjs"
}
}

View File

@ -27,12 +27,12 @@
" },",
"}",
"</route>\n",
"<script lang=\"ts\" setup>",
"//$3",
"</script>\n",
"<template>",
" <view class=\"\">$2</view>",
"</template>\n",
"<script lang=\"ts\" setup>",
"//$3",
"</script>\n",
"<style lang=\"scss\" scoped>",
"//$4",
"</style>\n",
@ -41,28 +41,16 @@
"Print unibest style": {
"scope": "vue",
"prefix": "st",
"body": [
"<style lang=\"scss\" scoped>",
"//",
"</style>\n"
],
"body": ["<style lang=\"scss\" scoped>", "//", "</style>\n"],
},
"Print unibest script": {
"scope": "vue",
"prefix": "sc",
"body": [
"<script lang=\"ts\" setup>",
"//$3",
"</script>\n"
],
"body": ["<script lang=\"ts\" setup>", "//$3", "</script>\n"],
},
"Print unibest template": {
"scope": "vue",
"prefix": "te",
"body": [
"<template>",
" <view class=\"\">$1</view>",
"</template>\n"
],
"body": ["<template>", " <view class=\"\">$1</view>", "</template>\n"],
},
}
}

View File

@ -63,20 +63,21 @@
## &#x1F4C2; 快速开始
执行 `pnpm create unibest` 创建项目
执行 `pnpm i` 安装依赖
执行 `pnpm dev` 运行 `H5`
执行 `pnpm dev:mp` 运行 `微信小程序`
## 📦 运行(支持热更新)
- web平台 `pnpm dev:h5`, 然后打开 [http://localhost:9000/](http://localhost:9000/)。
- weixin平台`pnpm dev:mp` 然后打开微信开发者工具,导入本地文件夹,选择本项目的`dist/dev/mp-weixin` 文件。
- weixin平台`pnpm dev:mp-weixin` 然后打开微信开发者工具,导入本地文件夹,选择本项目的`dist/dev/mp-weixin` 文件。
- APP平台`pnpm dev:app`, 然后打开 `HBuilderX`,导入刚刚生成的`dist/dev/app` 文件夹,选择运行到模拟器(开发时优先使用),或者运行的安卓/ios基座。
## 🔗 发布
- web平台 `pnpm build:h5`,打包后的文件在 `dist/build/h5`可以放到web服务器如nginx运行。如果最终不是放在根目录可以在 `manifest.config.ts` 文件的 `h5.router.base` 属性进行修改。
- weixin平台`pnpm build:mp`, 打包后的文件在 `dist/build/mp-weixin`,然后通过微信开发者工具导入,并点击右上角的“上传”按钮进行上传。
- weixin平台`pnpm build:mp-weixin`, 打包后的文件在 `dist/build/mp-weixin`,然后通过微信开发者工具导入,并点击右上角的“上传”按钮进行上传。
- APP平台`pnpm build:app`, 然后打开 `HBuilderX`,导入刚刚生成的`dist/build/app` 文件夹,选择发行 - APP云打包。
## 📄 License

5
env/.env vendored
View File

@ -1,15 +1,12 @@
VITE_APP_TITLE = 'unibest'
VITE_APP_PORT = 9000
VITE_UNI_APPID = '__UNI__D1E5001'
VITE_UNI_APPID = 'H57F2ACE4'
VITE_WX_APPID = 'wxa2abb91f64032a2b'
# 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_UPLOAD_BASEURL = 'https://ukw0y1.laf.run/upload'

View File

@ -1,22 +0,0 @@
import uniHelper from '@uni-helper/eslint-config'
export default uniHelper({
unocss: true,
vue: true,
markdown: false,
ignores: [
'src/uni_modules/',
'dist',
],
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',
},
})

View File

@ -1,7 +1,6 @@
import path from 'node:path'
import process from 'node:process'
// manifest.config.ts
import { defineManifestConfig } from '@uni-helper/vite-plugin-uni-manifest'
import path from 'node:path'
import { loadEnv } from 'vite'
// 获取环境变量的范例
@ -15,14 +14,14 @@ const {
} = env
export default defineManifestConfig({
'name': VITE_APP_TITLE,
'appid': VITE_UNI_APPID,
'description': '',
'versionName': '1.0.0',
'versionCode': '100',
'transformPx': false,
'locale': VITE_FALLBACK_LOCALE, // 'zh-Hans'
'h5': {
name: VITE_APP_TITLE,
appid: VITE_UNI_APPID,
description: '',
versionName: '1.0.0',
versionCode: '100',
transformPx: false,
locale: VITE_FALLBACK_LOCALE, // 'zh-Hans'
h5: {
router: {
base: VITE_APP_PUBLIC_BASE,
},
@ -83,14 +82,14 @@ export default defineManifestConfig({
ios: {
appstore: 'static/app/icons/1024x1024.png',
ipad: {
'app': 'static/app/icons/76x76.png',
app: 'static/app/icons/76x76.png',
'app@2x': 'static/app/icons/152x152.png',
'notification': 'static/app/icons/20x20.png',
notification: 'static/app/icons/20x20.png',
'notification@2x': 'static/app/icons/40x40.png',
'proapp@2x': 'static/app/icons/167x167.png',
'settings': 'static/app/icons/29x29.png',
settings: 'static/app/icons/29x29.png',
'settings@2x': 'static/app/icons/58x58.png',
'spotlight': 'static/app/icons/40x40.png',
spotlight: 'static/app/icons/40x40.png',
'spotlight@2x': 'static/app/icons/80x80.png',
},
iphone: {
@ -108,15 +107,12 @@ export default defineManifestConfig({
},
},
/* 快应用特有相关 */
'quickapp': {},
quickapp: {},
/* 小程序特有相关 */
'mp-weixin': {
appid: VITE_WX_APPID,
setting: {
urlCheck: false,
// 是否启用 ES6 转 ES5
es6: true,
minified: true,
},
usingComponents: true,
// __usePrivacyCheck__: true,
@ -131,8 +127,8 @@ export default defineManifestConfig({
'mp-toutiao': {
usingComponents: true,
},
'uniStatistics': {
uniStatistics: {
enable: false,
},
'vueVersion': '3',
vueVersion: '3',
})

View File

@ -1,9 +1,8 @@
{
"name": "unibest",
"type": "commonjs",
"version": "3.1.0",
"version": "2.10.1",
"description": "unibest - 最好的 uniapp 开发模板",
"update-time": "2025-06-21",
"author": {
"name": "feige996",
"zhName": "菲鸽",
@ -12,14 +11,13 @@
"gitee": "https://gitee.com/feige996"
},
"license": "MIT",
"homepage": "https://unibest.tech",
"repository": "https://github.com/feige996/unibest",
"repository-gitee": "https://gitee.com/feige996/unibest",
"repository-old": "https://github.com/codercup/unibest",
"repository-deprecated": "https://github.com/codercup/unibest",
"bugs": {
"url": "https://github.com/feige996/unibest/issues",
"url-old": "https://github.com/codercup/unibest/issues"
"url": "https://github.com/feige996/unibest/issues"
},
"homepage": "https://feige996.github.io/unibest/",
"engines": {
"node": ">=18",
"pnpm": ">=7.30"
@ -70,10 +68,18 @@
"build:quickapp-webview-huawei": "uni build -p quickapp-webview-huawei",
"build:quickapp-webview-union": "uni build -p quickapp-webview-union",
"type-check": "vue-tsc --noEmit",
"openapi-ts-request": "openapi-ts",
"prepare": "git init && husky",
"lint": "eslint",
"lint:fix": "eslint --fix"
"openapi-ts-request": "openapi-ts"
},
"lint-staged": {
"**/*.{html,vue,ts,cjs,json,md}": [
"prettier --write"
],
"**/*.{vue,js,ts,jsx,tsx,vue,css,scss,html}": [
"oxlint --fix"
]
},
"resolutions": {
"bin-wrapper": "npm:bin-wrapper-china"
},
"dependencies": {
"@dcloudio/uni-app": "3.0.0-4060620250520001",
@ -98,14 +104,12 @@
"js-cookie": "^3.0.5",
"pinia": "2.0.36",
"pinia-plugin-persistedstate": "3.2.1",
"vue": "^3.4.21",
"qs": "6.5.3",
"vue": "^3.5.15",
"wot-design-uni": "^1.9.1",
"z-paging": "2.8.7"
"z-paging": "^2.8.4"
},
"devDependencies": {
"@antfu/eslint-config": "^4.15.0",
"@commitlint/cli": "^19.8.1",
"@commitlint/config-conventional": "^19.8.1",
"@dcloudio/types": "^3.4.8",
"@dcloudio/uni-automator": "3.0.0-4060620250520001",
"@dcloudio/uni-cli-shared": "3.0.0-4060620250520001",
@ -117,42 +121,32 @@
"@rollup/rollup-darwin-x64": "^4.28.0",
"@types/node": "^20.17.9",
"@types/wechat-miniprogram": "^3.4.8",
"@uni-helper/eslint-config": "^0.4.0",
"@uni-helper/uni-types": "1.0.0-alpha.3",
"@uni-helper/unocss-preset-uni": "^0.2.11",
"@uni-helper/vite-plugin-uni-components": "0.2.0",
"@uni-helper/vite-plugin-uni-layouts": "0.1.10",
"@uni-helper/vite-plugin-uni-manifest": "0.2.8",
"@uni-helper/vite-plugin-uni-pages": "0.2.28",
"@uni-helper/vite-plugin-uni-platform": "0.0.4",
"@uni-helper/vite-plugin-uni-components": "^0.2.0",
"@uni-helper/vite-plugin-uni-layouts": "^0.1.10",
"@uni-helper/vite-plugin-uni-manifest": "^0.2.8",
"@uni-helper/vite-plugin-uni-pages": "0.2.20",
"@uni-helper/vite-plugin-uni-platform": "^0.0.4",
"@uni-ku/bundle-optimizer": "^1.3.3",
"@unocss/eslint-plugin": "^66.2.3",
"@unocss/preset-legacy-compat": "^0.59.4",
"@vue/runtime-core": "^3.4.21",
"@vue/tsconfig": "^0.1.3",
"autoprefixer": "^10.4.20",
"eslint": "^9.29.0",
"husky": "^9.1.7",
"lint-staged": "^15.2.10",
"openapi-ts-request": "^1.1.2",
"oxlint": "^0.1.0",
"postcss": "^8.4.49",
"postcss-html": "^1.7.0",
"postcss-scss": "^4.0.9",
"prettier": "^3.5.3",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "1.77.8",
"terser": "^5.36.0",
"typescript": "^5.7.2",
"unocss": "65.4.2",
"unocss": "^66.0.0",
"unplugin-auto-import": "^0.17.8",
"vite": "5.2.8",
"vite": "6.3.5",
"vite-plugin-restart": "^0.4.2",
"vue-tsc": "^2.2.10"
},
"resolutions": {
"bin-wrapper": "npm:bin-wrapper-china"
},
"lint-staged": {
"*": "eslint --fix"
}
}

View File

@ -1,5 +1,4 @@
import { defineUniPages } from '@uni-helper/vite-plugin-uni-pages'
import { tabBar } from './src/layouts/fg-tabbar/tabbarList'
export default defineUniPages({
globalStyle: {
@ -18,6 +17,35 @@ export default defineUniPages({
'z-paging/components/z-paging$1/z-paging$1.vue',
},
},
// tabbar 的配置统一在 “./src/layouts/fg-tabbar/tabbarList.ts” 文件中
tabBar: tabBar as any,
// 如果不需要tabBar可以注释掉这个配置或者直接删除
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: '首页',
},
{
iconPath: 'static/tabbar/example.png',
selectedIconPath: 'static/tabbar/exampleHL.png',
pagePath: 'pages/about/about',
text: '关于',
},
{
iconPath: 'static/tabbar/personal.png',
selectedIconPath: 'static/tabbar/personalHL.png',
pagePath: 'pages/mine/index',
text: '我的',
},
],
},
})

View File

@ -1,13 +0,0 @@
diff --git a/dist/uni-h5.es.js b/dist/uni-h5.es.js
index 7421bad97d94ad34a3d4d94292a9ee9071430662..19c6071ee4036ceb8d1cfa09030e471c002d2cda 100644
--- a/dist/uni-h5.es.js
+++ b/dist/uni-h5.es.js
@@ -23410,7 +23410,7 @@ function useShowTabBar(emit2) {
const tabBar2 = useTabBar();
const showTabBar2 = computed(() => route.meta.isTabBar && tabBar2.shown);
updateCssVar({
- "--tab-bar-height": tabBar2.height
+ "--tab-bar-height": tabBar2?.height || 0
});
return showTabBar2;
}

3568
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,2 +0,0 @@
patchedDependencies:
'@dcloudio/uni-h5': patches/@dcloudio__uni-h5.patch

View File

@ -2,7 +2,8 @@
// # 在升级完后,会自动添加很多无用依赖,这需要删除以减小依赖包体积
// # 只需要执行下面的命令即可
const { exec } = require('node:child_process')
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { exec } = require('child_process')
// 定义要执行的命令
const dependencies = [

View File

@ -1,10 +1,7 @@
<script setup lang="ts">
import { onHide, onLaunch, onShow } from '@dcloudio/uni-app'
import { usePageAuth } from '@/hooks/usePageAuth'
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'
usePageAuth()
onLaunch(() => {
console.log('App Launch')
})
@ -17,6 +14,7 @@ onHide(() => {
</script>
<style lang="scss">
/* stylelint-disable selector-type-no-unknown */
button::after {
border: none;
}

View File

@ -1,4 +1,4 @@
import type { ICaptcha, IUpdateInfo, IUpdatePassword, IUserInfoVo, IUserLogin } from './types/login'
import { ICaptcha, IUpdateInfo, IUpdatePassword, IUserInfoVo, IUserLogin } from './login.typings'
import { http } from '@/utils/http'
/**
@ -15,7 +15,7 @@ export interface ILoginForm {
*
* @returns ICaptcha
*/
export function getCode() {
export const getCode = () => {
return http.get<ICaptcha>('/user/getCode')
}
@ -23,35 +23,35 @@ export function getCode() {
*
* @param loginForm
*/
export function login(loginForm: ILoginForm) {
export const login = (loginForm: ILoginForm) => {
return http.post<IUserLogin>('/user/login', loginForm)
}
/**
*
*/
export function getUserInfo() {
export const getUserInfo = () => {
return http.get<IUserInfoVo>('/user/info')
}
/**
* 退
*/
export function logout() {
export const logout = () => {
return http.get<void>('/user/logout')
}
/**
*
*/
export function updateInfo(data: IUpdateInfo) {
export const updateInfo = (data: IUpdateInfo) => {
return http.post('/user/updateInfo', data)
}
/**
*
*/
export function updateUserPassword(data: IUpdatePassword) {
export const updateUserPassword = (data: IUpdatePassword) => {
return http.post('/user/updatePassword', data)
}
@ -59,12 +59,12 @@ export function updateUserPassword(data: IUpdatePassword) {
*
* @returns Promise (code)
*/
export function getWxCode() {
export const getWxCode = () => {
return new Promise<UniApp.LoginRes>((resolve, reject) => {
uni.login({
provider: 'weixin',
success: res => resolve(res),
fail: err => reject(new Error(err)),
success: (res) => resolve(res),
fail: (err) => reject(new Error(err)),
})
})
}
@ -78,6 +78,6 @@ export function getWxCode() {
* @param params code
* @returns Promise
*/
export function wxLogin(data: { code: string }) {
export const wxLogin = (data: { code: string }) => {
return http.post<IUserLogin>('/user/wxLogin', data)
}

View File

@ -1,7 +1,7 @@
/**
*
*/
export interface IUserInfoVo {
export type IUserInfoVo = {
id: number
username: string
avatar: string
@ -11,7 +11,7 @@ export interface IUserInfoVo {
/**
*
*/
export interface IUserLogin {
export type IUserLogin = {
id: string
username: string
token: string
@ -20,7 +20,7 @@ export interface IUserLogin {
/**
*
*/
export interface ICaptcha {
export type ICaptcha = {
captchaEnabled: boolean
uuid: string
image: string
@ -28,7 +28,7 @@ export interface ICaptcha {
/**
*
*/
export interface IUploadSuccessInfo {
export type IUploadSuccessInfo = {
fileId: number
originalName: string
fileName: string
@ -41,7 +41,7 @@ export interface IUploadSuccessInfo {
/**
*
*/
export interface IUpdateInfo {
export type IUpdateInfo = {
id: number
name: string
sex: string
@ -49,7 +49,7 @@ export interface IUpdateInfo {
/**
*
*/
export interface IUpdatePassword {
export type IUpdatePassword = {
id: number
oldPassword: string
newPassword: string

View File

@ -0,0 +1,47 @@
@import 'wot-design-uni/components/wd-button/index.scss';
:deep(.wd-privacy-popup) {
width: 600rpx;
padding: 0 24rpx;
box-sizing: border-box;
border-radius: 32rpx;
overflow: hidden;
}
.wd-privacy-popup {
&__header {
width: 100%;
height: 128rpx;
line-height: 128rpx;
color: rgba(0, 0, 0, 0.85);
font-size: 30rpx;
padding: 0 12rpx;
box-sizing: border-box;
}
&__container {
width: 100%;
box-sizing: border-box;
padding: 0 12rpx;
margin-bottom: 32rpx;
font-size: 28rpx;
line-height: 1.8;
color: #3e3e3e;
text-align: left;
font-weight: 550;
&-protocol {
color: #4d80f0;
}
}
&__footer {
display: flex;
justify-content: space-between;
padding-bottom: 36rpx;
button {
border: none;
outline: none;
}
}
}

View File

@ -0,0 +1,144 @@
<template>
<view>
<wd-popup
v-model="showPopup"
:close-on-click-modal="false"
custom-class="wd-privacy-popup"
@close="handleClose"
>
<view class="wd-privacy-popup__header">
<!--标题-->
<view class="wd-picker__title">{{ title }}</view>
</view>
<view class="wd-privacy-popup__container">
<text>{{ desc }}</text>
<text class="wd-privacy-popup__container-protocol" @click="openPrivacyContract">
{{ protocol }}
</text>
<text>{{ subDesc }}</text>
</view>
<view class="wd-privacy-popup__footer">
<button
class="wd-privacy-popup__footer-disagree wd-button is-block is-round is-medium is-plain"
id="disagree-btn"
@click="handleDisagree"
>
拒绝
</button>
<button
class="wd-privacy-popup__footer-agree wd-button is-primary is-block is-round is-medium"
id="agree-btn"
open-type="agreePrivacyAuthorization"
@agreeprivacyauthorization="handleAgree"
>
同意
</button>
</view>
</wd-popup>
</view>
</template>
<script lang="ts">
export default {
name: 'privacy-popup',
options: {
virtualHost: true,
addGlobalClass: true,
styleIsolation: 'shared',
},
}
</script>
<script lang="ts" setup>
import { onBeforeMount, ref } from 'vue'
type Props = {
title?: string //
desc?: string //
subDesc?: string //
protocol?: string //
}
const props = withDefaults(defineProps<Props>(), {
title: '用户隐私保护提示',
desc: '感谢您使用本应用,您使用本应用的服务之前请仔细阅读并同意',
subDesc:
'。当您点击同意并开始时用产品服务时,即表示你已理解并同息该条款内容,该条款将对您产生法律约束力。如您拒绝,将无法使用相应服务。',
protocol: '《用户隐私保护指引》',
})
const showPopup = ref<boolean>(false) // popup
const privacyResolves = ref(new Set()) // onNeedPrivacyAuthorizationreslove
const privacyHandler = (resolve: any) => {
showPopup.value = true
privacyResolves.value.add(resolve)
}
const emit = defineEmits(['agree', 'disagree'])
onBeforeMount(() => {
//
if ((wx as any).onNeedPrivacyAuthorization) {
;(wx as any).onNeedPrivacyAuthorization((resolve: any) => {
if (typeof privacyHandler === 'function') {
privacyHandler(resolve)
}
})
}
})
/**
* 同意隐私协议
*/
function handleAgree() {
showPopup.value = false
privacyResolves.value.forEach((resolve: any) => {
resolve({
event: 'agree',
buttonId: 'agree-btn',
})
})
privacyResolves.value.clear()
emit('agree')
}
/**
* 拒绝隐私协议
*/
function handleDisagree() {
showPopup.value = false
privacyResolves.value.forEach((resolve: any) => {
resolve({
event: 'disagree',
})
})
privacyResolves.value.clear()
}
/**
* 打开隐私协议
*/
function openPrivacyContract() {
;(wx as any).openPrivacyContract({
success: (res) => {
console.log('openPrivacyContract success')
},
fail: (res) => {
console.error('openPrivacyContract fail', res)
},
})
}
/**
* 弹出框关闭时清空
*/
function handleClose() {
privacyResolves.value.clear()
}
</script>
<style lang="scss" scoped>
@import './index.scss';
</style>

7
src/env.d.ts vendored
View File

@ -2,8 +2,8 @@
/// <reference types="vite-svg-loader" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
import { DefineComponent } from 'vue'
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
}
@ -29,6 +29,3 @@ interface ImportMetaEnv {
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'

View File

@ -1,50 +0,0 @@
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 })
})
}

View File

@ -1,6 +1,6 @@
import type { UnwrapRef } from 'vue'
import { UnwrapRef } from 'vue'
interface IUseRequestOptions<T> {
type IUseRequestOptions<T> = {
/** 是否立即执行 */
immediate?: boolean
/** 初始化数据 */

View File

@ -1,160 +1,69 @@
import { ref } from 'vue'
// TODO: 别忘加更改环境变量的 VITE_UPLOAD_BASEURL 地址。
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<T extends TfileType> {
formData?: Record<string, any>
maxSize?: number
accept?: T extends 'image' ? TImage[] : TFile[]
fileType?: T
success?: (params: any) => void
error?: (err: any) => void
}
export default function useUpload<T extends TfileType>(options: TOptions<T> = {} as TOptions<T>) {
const {
formData = {},
maxSize = 5 * 1024 * 1024,
accept = ['*'],
fileType = 'image',
success,
error: onError,
} = options
/**
* useUpload
* @param formData {name: '菲鸽'}
* @returns {loading, error, data, run}
*/
export default function useUpload<T = string>(formData: Record<string, any> = {}) {
const loading = ref(false)
const error = ref<Error | null>(null)
const data = ref<any>(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 error = ref(false)
const data = ref<T>()
const run = () => {
// #ifdef MP-WEIXIN
// 微信小程序从基础库 2.21.0 开始, wx.chooseImage 停止维护,请使用 uni.chooseMedia 代替。
// 微信小程序在2023年10月17日之后使用本API需要配置隐私协议
const chooseFileOptions = {
uni.chooseMedia({
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 })
mediaType: ['image'],
success: (res) => {
loading.value = true
const tempFilePath = res.tempFiles[0].tempFilePath
uploadFile<T>({ tempFilePath, formData, data, error, loading })
},
fail: (err: any) => {
console.error('File selection failed:', err)
error.value = err
onError?.(err)
fail: (err) => {
console.error('uni.chooseMedia err->', err)
error.value = true
},
}
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',
})
}
})
// #endif
// #ifndef MP-WEIXIN
uni.chooseImage({
count: 1,
success: (res) => {
loading.value = true
const tempFilePath = res.tempFilePaths[0]
uploadFile<T>({ tempFilePath, formData, data, error, loading })
},
fail: (err) => {
console.error('uni.chooseImage err->', err)
error.value = true
},
})
// #endif
}
return { loading, error, data, run }
}
async function uploadFile({
tempFilePath,
formData,
onSuccess,
onError,
onComplete,
}: {
tempFilePath: string
formData: Record<string, any>
onSuccess: (data: any) => void
onError: (err: any) => void
onComplete: () => void
}) {
function uploadFile<T>({ tempFilePath, formData, data, error, loading }) {
uni.uploadFile({
url: VITE_UPLOAD_BASEURL,
filePath: tempFilePath,
name: 'file',
formData,
success: (uploadFileRes) => {
try {
const data = uploadFileRes.data
onSuccess(data)
}
catch (err) {
onError(err)
}
data.value = uploadFileRes.data as T
},
fail: (err) => {
console.error('Upload failed:', err)
onError(err)
console.error('uni.uploadFile err->', err)
error.value = true
},
complete: () => {
loading.value = false
},
complete: onComplete,
})
}

View File

@ -1,3 +1,3 @@
export { prototypeInterceptor } from './prototype'
export { requestInterceptor } from './request'
export { routeInterceptor } from './route'
export { requestInterceptor } from './request'
export { prototypeInterceptor } from './prototype'

View File

@ -2,11 +2,10 @@ export const prototypeInterceptor = {
install() {
// 解决低版本手机不识别 array.at() 导致运行报错的问题
if (typeof Array.prototype.at !== 'function') {
// eslint-disable-next-line no-extend-native
Array.prototype.at = function (index: number) {
if (index < 0)
return this[this.length + index]
if (index >= this.length)
return undefined
if (index < 0) return this[this.length + index]
if (index >= this.length) return undefined
return this[index]
}
}

View File

@ -1,7 +1,8 @@
/* eslint-disable no-param-reassign */
import qs from 'qs'
import { useUserStore } from '@/store'
import { getEnvBaseUrl } from '@/utils'
import { platform } from '@/utils/platform'
import { stringifyQuery } from '@/utils/queryString'
import { getEnvBaseUrl } from '@/utils'
export type CustomRequestOptions = UniApp.RequestOptions & {
query?: Record<string, any>
@ -18,11 +19,10 @@ const httpInterceptor = {
invoke(options: CustomRequestOptions) {
// 接口请求支持通过 query 参数配置 queryString
if (options.query) {
const queryStr = stringifyQuery(options.query)
const queryStr = qs.stringify(options.query)
if (options.url.includes('?')) {
options.url += `&${queryStr}`
}
else {
} else {
options.url += `?${queryStr}`
}
}
@ -33,8 +33,7 @@ const httpInterceptor = {
if (JSON.parse(__VITE_APP_PROXY__)) {
// 自动拼接代理前缀
options.url = import.meta.env.VITE_APP_PROXY_PREFIX + options.url
}
else {
} else {
options.url = baseUrl + options.url
}
// #endif

View File

@ -5,12 +5,12 @@
* 便使
*/
import { useUserStore } from '@/store'
import { needLoginPages as _needLoginPages, getLastPage, getNeedLoginPages } from '@/utils'
import { needLoginPages as _needLoginPages, getNeedLoginPages, getLastPage } from '@/utils'
// TODO Check
const loginRoute = import.meta.env.VITE_LOGIN_URL
const loginRoute = '/pages/login/index'
function isLogined() {
const isLogined = () => {
const userStore = useUserStore()
return !!userStore.userInfo.username
}
@ -37,8 +37,7 @@ const navigateToInterceptor = {
// 为了防止开发时出现BUG这里每次都获取一下。生产环境可以移到函数外性能更好
if (isDev) {
needLoginPages = getNeedLoginPages()
}
else {
} else {
needLoginPages = _needLoginPages
}
const isNeedLogin = needLoginPages.includes(path)

View File

@ -1,3 +1,12 @@
<template>
<wd-config-provider :themeVars="themeVars">
<slot />
<wd-toast />
<wd-message-box />
<privacy-popup />
</wd-config-provider>
</template>
<script lang="ts" setup>
import type { ConfigProviderThemeVars } from 'wot-design-uni'
@ -7,11 +16,3 @@ const themeVars: ConfigProviderThemeVars = {
// buttonPrimaryColor: '#07c160',
}
</script>
<template>
<wd-config-provider :theme-vars="themeVars">
<slot />
<wd-toast />
<wd-message-box />
</wd-config-provider>
</template>

View File

@ -1,3 +1,11 @@
<template>
<wd-config-provider :themeVars="themeVars">
<slot />
<wd-toast />
<wd-message-box />
</wd-config-provider>
</template>
<script lang="ts" setup>
import type { ConfigProviderThemeVars } from 'wot-design-uni'
@ -7,11 +15,3 @@ const themeVars: ConfigProviderThemeVars = {
// buttonPrimaryColor: '#07c160',
}
</script>
<template>
<wd-config-provider :theme-vars="themeVars">
<slot />
<wd-toast />
<wd-message-box />
</wd-config-provider>
</template>

View File

@ -1,67 +0,0 @@
<script setup lang="ts">
import { tabbarStore } from './tabbar'
// 'i-carbon-code',
import { tabbarList as _tabBarList, cacheTabbarEnable, selectedTabbarStrategy } from './tabbarList'
// @ts-expect-error
const customTabbarEnable = selectedTabbarStrategy === 1 || selectedTabbarStrategy === 2
/** tabbarList 里面的 path 从 pages.config.ts 得到 */
const tabbarList = _tabBarList.map(item => ({ ...item, path: `/${item.pagePath}` }))
function selectTabBar({ value: index }: { value: number }) {
const url = tabbarList[index].path
tabbarStore.setCurIdx(index)
if (cacheTabbarEnable) {
uni.switchTab({ url })
}
else {
uni.navigateTo({ url })
}
}
onLoad(() => {
// tabBar 2 tabBar
// @ts-expect-error
const hideRedundantTabbarEnable = selectedTabbarStrategy === 1
hideRedundantTabbarEnable
&& uni.hideTabBar({
fail(err) {
console.log('hideTabBar fail: ', err)
},
success(res) {
console.log('hideTabBar success: ', res)
},
})
})
</script>
<template>
<wd-tabbar
v-if="customTabbarEnable"
v-model="tabbarStore.curIdx"
bordered
safeareainsetbottom
placeholder
fixed
@change="selectTabBar"
>
<block v-for="(item, idx) in tabbarList" :key="item.path">
<wd-tabbar-item v-if="item.iconType === 'uiLib'" :title="item.text" :icon="item.icon" />
<wd-tabbar-item
v-else-if="item.iconType === 'unocss' || item.iconType === 'iconfont'"
:title="item.text"
>
<template #icon>
<view
h-40rpx
w-40rpx
:class="[item.icon, idx === tabbarStore.curIdx ? 'is-active' : 'is-inactive']"
/>
</template>
</wd-tabbar-item>
<wd-tabbar-item v-else-if="item.iconType === 'local'" :title="item.text">
<template #icon>
<image :src="item.icon" h-40rpx w-40rpx />
</template>
</wd-tabbar-item>
</block>
</wd-tabbar>
</template>

View File

@ -1,16 +0,0 @@
# tabbar 说明
`tabbar` 分为 `4 种` 情况:
- `完全原生 tabbar`,使用 `switchTab` 切换 tabbar`tabbar` 页面有缓存。
- 优势:原生自带的 tabbar最先渲染有缓存。
- 劣势:只能使用 2 组图片来切换选中和非选中状态,修改颜色只能重新换图片(或者用 iconfont
- `半自定义 tabbar`,使用 `switchTab` 切换 tabbar`tabbar` 页面有缓存。使用了第三方 UI 库的 `tabbar` 组件,并隐藏了原生 `tabbar` 的显示。
- 优势:可以随意配置自己想要的 `svg icon`,切换字体颜色方便。有缓存。可以实现各种花里胡哨的动效等。
- 劣势:首次点击 tababr 会闪烁。
- `全自定义 tabbar`,使用 `navigateTo` 切换 `tabbar``tabbar` 页面无缓存。使用了第三方 UI 库的 `tabbar` 组件。
- 优势:可以随意配置自己想要的 svg icon切换字体颜色方便。可以实现各种花里胡哨的动效等。
- 劣势:首次点击 `tababr` 会闪烁,无缓存。
- `无 tabbar`,只有一个页面入口,底部无 `tabbar` 显示;常用语临时活动页。
> 注意:花里胡哨的效果需要自己实现,本模版不提供。

View File

@ -1,11 +0,0 @@
/**
* 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)
},
})

View File

@ -1,65 +0,0 @@
/**
* tabbar tabbar.md
* 0: 'NATIVE_TABBAR' `完全原生 tabbar`
* 2: 'FULL_CUSTOM_TABBAR' `全自定义 tabbar`
* 1: 'HALF_CUSTOM_TABBAR' `半自定义 tabbar`
* 3: 'NO_TABBAR' `无 tabbar`
*
* pages.json
*/
// TODO通过这里切换使用tabbar的策略
export const selectedTabbarStrategy = 0
// 0 和 1 时需要tabbar缓存
export const cacheTabbarEnable = selectedTabbarStrategy < 2
// selectedTabbarStrategy==0 时,需要填 iconPath 和 selectedIconPath
// selectedTabbarStrategy==1 or 2 时,需要填 icon 和 iconType
// selectedTabbarStrategy==3 时tabbarList 不生效
export const tabbarList = [
{
iconPath: 'static/tabbar/home.png',
selectedIconPath: 'static/tabbar/homeHL.png',
pagePath: 'pages/index/index',
text: '首页',
icon: 'home',
iconType: 'uiLib',
},
{
iconPath: 'static/tabbar/example.png',
selectedIconPath: 'static/tabbar/exampleHL.png',
pagePath: 'pages/about/about',
text: '关于',
icon: 'i-carbon-code',
// 注意 unocss 的图标需要在 页面上引入一下,或者配置到 unocss.config.ts 的 safelist 中
iconType: 'unocss',
},
// {
// pagePath: 'pages/my/index',
// text: '我的',
// icon: '/static/logo.svg',
// iconType: 'local',
// },
// {
// pagePath: 'pages/mine/index',
// text: '我的',
// icon: 'iconfont icon-my',
// iconType: 'iconfont',
// },
]
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

View File

@ -1,19 +0,0 @@
<script lang="ts" setup>
import type { ConfigProviderThemeVars } from 'wot-design-uni'
import FgTabbar from './fg-tabbar/fg-tabbar.vue'
const themeVars: ConfigProviderThemeVars = {
// colorTheme: 'red',
// buttonPrimaryBgColor: '#07c160',
// buttonPrimaryColor: '#07c160',
}
</script>
<template>
<wd-config-provider :theme-vars="themeVars">
<slot />
<FgTabbar />
<wd-toast />
<wd-message-box />
</wd-config-provider>
</template>

View File

@ -1,11 +1,11 @@
import '@/style/index.scss'
import { VueQueryPlugin } from '@tanstack/vue-query'
import 'uno.css'
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)

View File

@ -1,6 +1,6 @@
{
"name": "unibest",
"appid": "__UNI__D1E5001",
"appid": "H57F2ACE4",
"description": "",
"versionName": "1.0.0",
"versionCode": "100",
@ -85,9 +85,7 @@
"mp-weixin": {
"appid": "wxa2abb91f64032a2b",
"setting": {
"urlCheck": false,
"es6": true,
"minified": true
"urlCheck": false
},
"usingComponents": true
},

View File

@ -1,27 +1,20 @@
<route lang="json5" type="page">
{
style: {
navigationStyle: 'default',
navigationBarTitleText: '分包页面 标题',
},
style: { navigationBarTitleText: '分包页面 标题' },
}
</route>
<template>
<view class="text-center">
<view class="m-8">http://localhost:9000/#/pages-sub/demo/index</view>
<view class="text-green-500">分包页面demo</view>
</view>
</template>
<script lang="ts" setup>
// code here
</script>
<template>
<view class="text-center">
<view class="m-8">
http://localhost:9000/#/pages-sub/demo/index
</view>
<view class="text-green-500">
分包页面demo
</view>
</view>
</template>
<style lang="scss" scoped>
//
</style>

View File

@ -28,17 +28,19 @@
"iconPath": "static/tabbar/home.png",
"selectedIconPath": "static/tabbar/homeHL.png",
"pagePath": "pages/index/index",
"text": "首页",
"icon": "home",
"iconType": "uiLib"
"text": "首页"
},
{
"iconPath": "static/tabbar/example.png",
"selectedIconPath": "static/tabbar/exampleHL.png",
"pagePath": "pages/about/about",
"text": "关于",
"icon": "i-carbon-code",
"iconType": "unocss"
"text": "关于"
},
{
"iconPath": "static/tabbar/personal.png",
"selectedIconPath": "static/tabbar/personalHL.png",
"pagePath": "pages/mine/index",
"text": "我的"
}
]
},
@ -46,7 +48,6 @@
{
"path": "pages/index/index",
"type": "home",
"layout": "tabbar",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "首页"
@ -55,11 +56,47 @@
{
"path": "pages/about/about",
"type": "page",
"layout": "tabbar",
"style": {
"navigationBarTitleText": "关于"
"navigationBarTitleText": "关于",
"navigationStyle": "custom"
}
},
{
"path": "pages/login/index",
"type": "page",
"style": {
"navigationBarTitleText": "登录",
"navigationStyle": "custom"
}
},
{
"path": "pages/mine/index",
"type": "page",
"style": {
"navigationBarTitleText": "我的"
}
},
{
"path": "pages/mine/about/index",
"type": "page",
"style": {
"navigationBarTitleText": "关于我们"
}
},
{
"path": "pages/mine/info/index",
"type": "page",
"style": {
"navigationBarTitleText": "个人资料"
}
},
{
"path": "pages/mine/password/index",
"type": "page",
"style": {
"navigationBarTitleText": "修改密码"
}
}
],
"subPackages": []
}
}

View File

@ -1,40 +1,38 @@
<route lang="json5">
{
layout: 'tabbar',
style: {
navigationBarTitleText: '关于',
navigationStyle: 'custom', //
},
}
</route>
<template>
<view>
<fg-navbar>关于</fg-navbar>
<view
class="bg-white overflow-hidden pt-2 px-4"
:style="{ marginTop: safeAreaInsets?.top + 'px' }"
>
<view class="text-center text-3xl mt-8">
鸽友们好我是
<text class="text-red-500">菲鸽</text>
</view>
<view class="test-css">测试 scss 样式</view>
<RequestComp />
<UploadComp />
</view>
</view>
</template>
<script lang="ts" setup>
import RequestComp from './components/request.vue'
import UploadComp from './components/upload.vue'
//
const { safeAreaInsets } = uni.getSystemInfoSync()
// vue .ts
// const testOxlint = (name: string) => {
// console.log('oxlint')
// }
// testOxlint('oxlint')
console.log('about')
</script>
<template>
<view>
<view class="mt-8 text-center text-3xl">
鸽友们好我是
<text class="text-red-500">
菲鸽
</text>
</view>
<RequestComp />
<UploadComp />
</view>
</template>
<style lang="scss" scoped>
.test-css {
// 16rpx=>0.5rem

View File

@ -7,9 +7,36 @@
}
</route>
<template>
<view class="p-6 text-center">
<view class="my-2">使用的是 laf 云后台</view>
<view class="text-green-400">我的推荐码可以获得佣金</view>
<!-- #ifdef H5 -->
<view class="my-2">
<a class="my-2" :href="recommendUrl" target="_blank">{{ recommendUrl }}</a>
</view>
<!-- #endif -->
<!-- #ifndef H5 -->
<view class="my-2 text-left text-sm">{{ recommendUrl }}</view>
<!-- #endif -->
<!-- http://localhost:9000/#/pages/index/request -->
<wd-button @click="run" class="my-6">发送请求</wd-button>
<view class="h-16">
<view v-if="loading">loading...</view>
<block v-else>
<view class="text-xl">请求数据如下</view>
<view class="text-green leading-8">{{ JSON.stringify(data) }}</view>
</block>
</view>
<wd-button type="error" @click="reset" class="my-6" :disabled="!data">重置数据</wd-button>
</view>
</template>
<script lang="ts" setup>
import type { IFooItem } from '@/service/index/foo'
import { getFooAPI } from '@/service/index/foo'
import { getFooAPI, postFooAPI, IFooItem } from '@/service/index/foo'
// import { findPetsByStatusQueryOptions } from '@/service/app'
// import { useQuery } from '@tanstack/vue-query'
@ -34,51 +61,7 @@ const { loading, error, data, run } = useRequest<IFooItem>(() => getFooAPI('菲
// refetch,
// } = useQuery(findPetsByStatusQueryOptions({ params: { status: ['available'] } }))
function reset() {
const reset = () => {
data.value = initialData
}
</script>
<template>
<view class="p-6 text-center">
<view class="my-2">
使用的是 laf 云后台
</view>
<view class="text-green-400">
我的推荐码可以获得佣金
</view>
<!-- #ifdef H5 -->
<view class="my-2">
<a class="my-2" :href="recommendUrl" target="_blank">{{ recommendUrl }}</a>
</view>
<!-- #endif -->
<!-- #ifndef H5 -->
<view class="my-2 text-left text-sm">
{{ recommendUrl }}
</view>
<!-- #endif -->
<!-- http://localhost:9000/#/pages/index/request -->
<wd-button class="my-6" @click="run">
发送请求
</wd-button>
<view class="h-16">
<view v-if="loading">
loading...
</view>
<block v-else>
<view class="text-xl">
请求数据如下
</view>
<view class="text-green leading-8">
{{ JSON.stringify(data) }}
</view>
</block>
</view>
<wd-button type="error" class="my-6" :disabled="!data" @click="reset">
重置数据
</wd-button>
</view>
</template>

View File

@ -7,32 +7,24 @@
}
</route>
<script lang="ts" setup>
const { loading, data, run } = useUpload()
</script>
<template>
<view class="p-4 text-center">
<wd-button @click="run">
选择图片并上传
</wd-button>
<view v-if="loading" class="h-10 text-blue">
上传...
</view>
<wd-button @click="run">选择图片并上传</wd-button>
<view v-if="loading" class="text-blue h-10">上传...</view>
<template v-else>
<view class="m-2">
上传后返回的接口数据
</view>
<view class="m-2">
{{ data }}
</view>
<view v-if="data" class="h-80 w-full">
<image :src="data.url" mode="scaleToFill" />
<view class="m-2">上传后返回的接口数据</view>
<view class="m-2">{{ data }}</view>
<view class="h-80 w-full">
<image v-if="data" :src="data || data" mode="scaleToFill" />
</view>
</template>
</view>
</template>
<script lang="ts" setup>
const { loading, data, run } = useUpload({ user: '菲鸽' })
</script>
<style lang="scss" scoped>
//
</style>

View File

@ -1,14 +1,34 @@
<!-- 使用 type="home" 属性设置首页其他页面不需要设置默认为page推荐使用json5更强大且允许注释 -->
<route lang="json5" type="home">
{
layout: 'tabbar',
style: {
// 'custom' 'default'
navigationStyle: 'custom',
navigationBarTitleText: '首页',
},
}
</route>
<template>
<view
class="bg-white overflow-hidden pt-2 px-4"
:style="{ marginTop: safeAreaInsets?.top + 'px' }"
>
<view class="mt-12">
<image src="/static/logo.svg" alt="" class="w-28 h-28 block mx-auto" />
</view>
<view class="text-center text-4xl main-title-color mt-4">unibest</view>
<view class="text-center text-2xl mt-2 mb-8">最好用的 uniapp 开发模板</view>
<view class="text-justify max-w-100 m-auto text-4 indent mb-2">{{ description }}</view>
<view class="text-center mt-8">
当前平台是
<text class="text-green-500">{{ PLATFORM.platform }}</text>
</view>
<view class="text-center mt-4">
模板分支是
<text class="text-green-500">base</text>
</view>
</view>
</template>
<script lang="ts" setup>
import PLATFORM from '@/utils/platform'
@ -18,78 +38,19 @@ defineOptions({
})
//
let safeAreaInsets
let systemInfo
// #ifdef MP-WEIXIN
// 使API
systemInfo = uni.getWindowInfo()
safeAreaInsets = systemInfo.safeArea
? {
top: systemInfo.safeArea.top,
right: systemInfo.windowWidth - systemInfo.safeArea.right,
bottom: systemInfo.windowHeight - systemInfo.safeArea.bottom,
left: systemInfo.safeArea.left,
}
: null
// #endif
// #ifndef MP-WEIXIN
// 使uni API
systemInfo = uni.getSystemInfoSync()
safeAreaInsets = systemInfo.safeAreaInsets
// #endif
const { safeAreaInsets } = uni.getSystemInfoSync()
const author = ref('菲鸽')
const description = ref(
'unibest 是一个集成了多种工具和技术的 uniapp 开发模板,由 uniapp + Vue3 + Ts + Vite5 + UnoCss + VSCode 构建,模板具有代码提示、自动格式化、统一配置、代码片段等功能,并内置了许多常用的基本组件和基本功能,让你编写 uniapp 拥有 best 体验。',
'unibest 是一个集成了多种工具和技术的 uniapp 开发模板,由 uniapp + Vue3 + Ts + Vite6 + UnoCss + VSCode 构建,模板具有代码提示、自动格式化、统一配置、代码片段等功能,并内置了许多常用的基本组件和基本功能,让你编写 uniapp 拥有 best 体验。',
)
// uni API
onLoad(() => {
console.log('项目作者:', author.value)
})
console.log('index')
</script>
<template>
<view class="bg-white px-4 pt-2" :style="{ marginTop: `${safeAreaInsets?.top}px` }">
<view class="mt-10">
<image src="/static/logo.svg" alt="" class="mx-auto block h-28 w-28" />
</view>
<view class="mt-4 text-center text-4xl text-[#d14328]">
unibest
</view>
<view class="mb-8 mt-2 text-center text-2xl">
最好用的 uniapp 开发模板
</view>
<view class="m-auto mb-2 max-w-100 text-justify indent text-4">
{{ description }}
</view>
<view class="mt-4 text-center">
作者
<text class="text-green-500">
菲鸽
</text>
</view>
<view class="mt-4 text-center">
官网地址
<text class="text-green-500">
https://unibest.tech
</text>
</view>
<view class="mt-6 h-1px bg-#eee" />
<view class="mt-8 text-center">
当前平台是
<text class="text-green-500">
{{ PLATFORM.platform }}
</text>
</view>
<view class="mt-4 text-center">
模板分支是
<text class="text-green-500">
base
</text>
</view>
</view>
</template>
<style>
.main-title-color {
color: #d14328;
}
</style>

584
src/pages/login/index.vue Normal file
View File

@ -0,0 +1,584 @@
<route lang="json5" type="page">
{
style: {
navigationBarTitleText: '登录',
navigationStyle: 'custom',
},
}
</route>
<template>
<view class="login-container">
<!-- 背景装饰元素 -->
<view class="bg-decoration bg-circle-1"></view>
<view class="bg-decoration bg-circle-2"></view>
<view class="bg-decoration bg-circle-3"></view>
<view class="login-header">
<image class="login-logo" :src="appLogo" mode="aspectFit"></image>
<view class="login-title">{{ appTitle }}</view>
</view>
<view class="login-form">
<view class="welcome-text">欢迎登录</view>
<view class="login-desc">请输入您的账号和密码</view>
<view class="login-input-group">
<view class="input-wrapper">
<wd-input
v-model="loginForm.username"
prefix-icon="user"
placeholder="请输入用户名"
clearable
class="login-input"
:border="false"
required
></wd-input>
<view class="input-bottom-line"></view>
</view>
<view class="input-wrapper">
<wd-input
v-model="loginForm.password"
prefix-icon="lock-on"
placeholder="请输入密码"
clearable
show-password
class="login-input"
:border="false"
required
></wd-input>
<view class="input-bottom-line"></view>
</view>
<!-- 验证码区域 -->
<view class="input-wrapper captcha-wrapper">
<wd-input
v-if="captcha.captchaEnabled"
v-model="loginForm.code"
prefix-icon="secured"
placeholder="请输入验证码"
clearable
class="login-input captcha-input"
:border="false"
required
>
<template #suffix>
<image
class="captcha-image"
:src="'data:image/gif;base64,' + captcha.image"
mode="aspectFit"
@click="refreshCaptcha"
></image>
</template>
</wd-input>
<view class="input-bottom-line"></view>
</view>
</view>
<!-- 登录按钮组 -->
<view class="login-buttons">
<!-- 账号密码登录按钮 -->
<wd-button
type="primary"
size="large"
block
@click="handleAccountLogin"
class="account-login-btn"
>
<wd-icon name="right" size="18px" class="login-icon"></wd-icon>
登录
</wd-button>
<!-- 微信小程序一键登录按钮 -->
<!-- #ifdef MP-WEIXIN -->
<view class="divider">
<view class="divider-line"></view>
<view class="divider-text"></view>
<view class="divider-line"></view>
</view>
<wd-button
type="info"
size="large"
block
plain
@click="handleWechatLogin"
class="wechat-login-btn"
>
微信一键登录
</wd-button>
<!-- #endif -->
</view>
</view>
<!-- 隐私协议勾选 -->
<view class="privacy-agreement">
<wd-checkbox
v-model="agreePrivacy"
shape="square"
class="privacy-checkbox"
active-color="var(--wot-color-theme, #1989fa)"
>
<view class="agreement-text">
我已阅读并同意
<text class="agreement-link" @click.stop="handleAgreement('user')">用户协议</text>
<text class="agreement-link" @click.stop="handleAgreement('privacy')">隐私政策</text>
</view>
</wd-checkbox>
</view>
<view class="login-footer"></view>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useUserStore } from '@/store/user'
import { isMpWeixin } from '@/utils/platform'
import { getCode, ILoginForm } from '@/api/login'
import { toast } from '@/utils/toast'
import { isTableBar } from '@/utils/index'
import { ICaptcha } from '@/api/login.typings'
const redirectRoute = ref('')
//
const appTitle = ref(import.meta.env.VITE_APP_TITLE || 'Unibest Login')
const appLogo = ref(import.meta.env.VITE_APP_LOGO || '/static/logo.svg')
// store
const userStore = useUserStore()
//
//
const captcha = ref<ICaptcha>({
captchaEnabled: false,
uuid: '',
image: '',
})
//
const loginForm = ref<ILoginForm>({
username: 'admin',
password: '123456',
code: '',
uuid: '',
})
//
const agreePrivacy = ref(true)
//
onLoad((option) => {
//
captcha.value.captchaEnabled && refreshCaptcha()
//
if (option.redirect) {
redirectRoute.value = option.redirect
}
})
//
const handleAccountLogin = async () => {
if (!agreePrivacy.value) {
toast.error('请阅读同意协议')
return
}
//
if (!loginForm.value.username) {
toast.error('请输入用户名')
return
}
if (!loginForm.value.password) {
toast.error('请输入密码')
return
}
if (captcha.value.captchaEnabled && !loginForm.value.code) {
toast.error('请输入验证码')
return
}
//
await userStore.login(loginForm.value)
//
const targetUrl = redirectRoute.value || '/pages/index/index'
if (isTableBar(targetUrl)) {
uni.switchTab({ url: targetUrl })
} else {
uni.redirectTo({ url: targetUrl })
}
}
//
const handleWechatLogin = async () => {
if (!isMpWeixin) {
toast.info('请在微信小程序中使用此功能')
return
}
//
if (!agreePrivacy.value) {
toast.error('请先阅读并同意用户协议和隐私政策')
return
}
//
await userStore.wxLogin()
//
const targetUrl = redirectRoute.value || '/pages/index/index'
if (isTableBar(targetUrl)) {
uni.switchTab({ url: targetUrl })
} else {
uni.redirectTo({ url: targetUrl })
}
}
//
const refreshCaptcha = () => {
//
getCode().then((res) => {
const { data } = res
loginForm.value.uuid = data.uuid
captcha.value = data
})
}
//
const handleAgreement = (type: 'user' | 'privacy') => {
const title = type === 'user' ? '用户协议' : '隐私政策'
// showToast(`${title}`)
//
// uni.navigateTo({
// url: `/pages/agreement/${type}`
// })
}
</script>
<style lang="scss" scoped>
/* 验证码输入框样式 */
.captcha-wrapper {
.captcha-input {
:deep(.wd-input__suffix) {
margin-right: 0;
padding-right: 0;
}
}
.captcha-image {
width: 100px;
height: 36px;
margin-left: 10px;
border-radius: 8px;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
position: relative;
overflow: hidden;
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.1), transparent);
pointer-events: none;
}
&:active {
opacity: 0.8;
transform: scale(0.96);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
}
}
}
.login-container {
box-sizing: border-box;
display: flex;
flex-direction: column;
min-height: 100vh;
padding: 0 70rpx;
background-color: #ffffff;
background-image: linear-gradient(
135deg,
rgba(25, 137, 250, 0.05) 0%,
rgba(255, 255, 255, 0) 100%
);
position: relative;
overflow: hidden;
}
/* 背景装饰元素 */
.bg-decoration {
position: absolute;
border-radius: 50%;
background: linear-gradient(135deg, rgba(25, 137, 250, 0.05), rgba(25, 137, 250, 0.1));
z-index: 0;
pointer-events: none;
}
.bg-circle-1 {
width: 500rpx;
height: 500rpx;
top: -200rpx;
right: -200rpx;
opacity: 0.6;
}
.bg-circle-2 {
width: 400rpx;
height: 400rpx;
bottom: 10%;
left: -200rpx;
opacity: 0.4;
}
.bg-circle-3 {
width: 300rpx;
height: 300rpx;
bottom: -100rpx;
right: 10%;
opacity: 0.3;
background: linear-gradient(135deg, rgba(7, 193, 96, 0.05), rgba(7, 193, 96, 0.1));
}
.login-header {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-top: 120rpx;
animation: fadeInDown 0.8s ease-out;
.login-logo {
width: 200rpx;
height: 200rpx;
border-radius: 36rpx;
box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.12);
transition: all 0.3s ease;
&:active {
transform: scale(0.95);
box-shadow: 0 6rpx 15rpx rgba(0, 0, 0, 0.1);
}
}
.login-title {
margin-top: 30rpx;
font-size: 46rpx;
font-weight: bold;
color: #333333;
letter-spacing: 3rpx;
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.05);
}
}
.login-form {
flex: 1;
margin-top: 70rpx;
animation: fadeIn 0.8s ease-out 0.2s both;
.welcome-text {
margin-bottom: 16rpx;
font-size: 48rpx;
font-weight: bold;
color: #333333;
text-align: center;
letter-spacing: 1rpx;
}
.login-desc {
margin-bottom: 70rpx;
font-size: 28rpx;
color: #888888;
text-align: center;
}
.login-input-group {
margin-bottom: 60rpx;
position: relative;
z-index: 1;
.input-wrapper {
position: relative;
margin-bottom: 50rpx;
transition: all 0.3s ease;
border-radius: 16rpx;
overflow: hidden;
&:last-child {
margin-bottom: 0;
}
.login-input {
padding: 12rpx 20rpx;
background-color: rgba(245, 247, 250, 0.7);
border-radius: 16rpx;
transition: all 0.3s ease;
:deep(.wd-input__inner) {
font-size: 30rpx;
color: #333333;
}
:deep(.wd-input__placeholder) {
font-size: 28rpx;
color: #aaaaaa;
}
&:focus-within {
background-color: rgba(245, 247, 250, 0.95);
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.06);
transform: translateY(-3rpx);
}
}
.input-bottom-line {
position: absolute;
bottom: -2rpx;
left: 5%;
width: 90%;
height: 2rpx;
background: linear-gradient(
to right,
transparent,
var(--wot-color-theme, #1989fa),
transparent
);
transition: transform 0.4s ease;
transform: scaleX(0);
opacity: 0.8;
}
&:focus-within .input-bottom-line {
transform: scaleX(1);
}
.input-icon {
margin-right: 16rpx;
color: #666666;
transition: color 0.3s ease;
}
&:focus-within .input-icon {
color: var(--wot-color-theme, #1989fa);
}
}
}
.login-buttons {
display: flex;
flex-direction: column;
gap: 36rpx;
.account-login-btn {
height: 96rpx;
margin-top: 20rpx;
font-size: 32rpx;
font-weight: 500;
letter-spacing: 2rpx;
border-radius: 48rpx;
box-shadow: 0 10rpx 20rpx rgba(25, 137, 250, 0.25);
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
.login-icon {
margin-right: 8rpx;
opacity: 0.8;
transition: all 0.3s ease;
}
&:active {
box-shadow: 0 5rpx 10rpx rgba(25, 137, 250, 0.2);
transform: scale(0.98);
.login-icon {
transform: translateX(3rpx);
}
}
}
.divider {
display: flex;
align-items: center;
margin: 24rpx 0;
.divider-line {
flex: 1;
height: 1px;
background-color: #eeeeee;
}
.divider-text {
padding: 0 24rpx;
font-size: 24rpx;
color: #999999;
}
}
.wechat-login-btn {
height: 96rpx;
font-size: 32rpx;
color: #07c160;
border-color: #07c160;
border-radius: 48rpx;
transition: all 0.3s ease;
.wechat-icon {
margin-right: 12rpx;
}
&:active {
background-color: rgba(7, 193, 96, 0.08);
transform: scale(0.98);
}
}
}
}
.privacy-agreement {
display: flex;
justify-content: center;
margin: 30rpx 0 40rpx;
animation: fadeIn 0.8s ease-out 0.4s both;
.privacy-checkbox {
display: flex;
align-items: center;
}
.agreement-text {
font-size: 26rpx;
line-height: 1.6;
color: #666666;
.agreement-link {
padding: 0 4rpx;
font-weight: 500;
color: var(--wot-color-theme, #1989fa);
transition: all 0.3s ease;
&:active {
opacity: 0.8;
transform: scale(0.98);
}
}
}
}
.login-footer {
padding: 50rpx 0;
margin-top: auto;
}
/* 添加动画效果 */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>

View File

@ -0,0 +1,173 @@
<route lang="json5">
{
style: {
navigationBarTitleText: '关于我们',
},
}
</route>
<template>
<view class="about-container">
<view class="about-card">
<!-- 应用信息 -->
<view class="app-info">
<view class="logo-wrapper">
<wd-img :src="appLogo" width="120px" height="120px" radius="24rpx"></wd-img>
</view>
<view class="app-name">{{ appTitle }}</view>
<view class="app-version">版本 {{ packageJson.version }}</view>
</view>
<!-- 联系方式 -->
<view class="info-section">
<view class="section-title">联系我们</view>
<view class="section-content">
<view class="contact-item">
<wd-icon name="phone" size="20px" class="contact-icon"></wd-icon>
<text class="contact-text">客服电话400-XXX-XXXX</text>
</view>
<view class="contact-item">
<wd-icon name="mail" size="20px" class="contact-icon"></wd-icon>
<text class="contact-text">邮箱support@unibest.tech</text>
</view>
<view class="contact-item">
<wd-icon name="location" size="20px" class="contact-icon"></wd-icon>
<text class="contact-text">地址中国·深圳</text>
</view>
</view>
</view>
<!-- 版权信息 -->
<view class="copyright">
<text>Copyright © 2025-{{ currentYear }} {{ appTitle }}</text>
<text>All Rights Reserved</text>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import packageJson from '@/../package.json'
const appTitle = ref(import.meta.env.VITE_APP_TITLE || 'unibest')
const appLogo = ref(import.meta.env.VITE_APP_LOGO || '/static/logo.svg')
//
const currentYear = computed(() => new Date().getFullYear())
</script>
<style lang="scss" scoped>
.about-container {
background-color: #f5f7fa;
padding: 30rpx;
}
.about-card {
background-color: #ffffff;
border-radius: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
overflow: hidden;
padding: 40rpx 30rpx;
}
/* 应用信息 */
.app-info {
display: flex;
flex-direction: column;
align-items: center;
padding: 30rpx 0 50rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.logo-wrapper {
margin-bottom: 20rpx;
box-shadow: 0 8rpx 16rpx rgba(0, 0, 0, 0.08);
border-radius: 24rpx;
}
.app-name {
font-size: 40rpx;
font-weight: 600;
color: #333;
margin-bottom: 10rpx;
}
.app-version {
font-size: 28rpx;
color: #999;
}
/* 信息区块 */
.info-section {
padding: 40rpx 0;
border-bottom: 2rpx solid #f0f0f0;
}
.section-title {
font-size: 34rpx;
font-weight: 600;
color: #333;
margin-bottom: 20rpx;
position: relative;
padding-left: 24rpx;
&::before {
content: '';
position: absolute;
left: 0;
top: 50%;
transform: translateY(-50%);
width: 8rpx;
height: 32rpx;
background: linear-gradient(135deg, #4a7bff, #6a5acd);
border-radius: 4rpx;
}
}
.section-content {
padding: 0 10rpx;
}
.content-text {
font-size: 30rpx;
color: #666;
line-height: 1.6;
text-align: justify;
}
/* 联系方式 */
.contact-item {
display: flex;
align-items: center;
margin-bottom: 20rpx;
&:last-child {
margin-bottom: 0;
}
}
.contact-icon {
margin-right: 20rpx;
color: #4a7bff;
}
.contact-text {
font-size: 30rpx;
color: #666;
}
/* 版权信息 */
.copyright {
padding-top: 40rpx;
display: flex;
flex-direction: column;
align-items: center;
text {
font-size: 26rpx;
color: #999;
line-height: 1.6;
}
}
</style>

367
src/pages/mine/index.vue Normal file
View File

@ -0,0 +1,367 @@
<route lang="json5">
{
style: {
navigationBarTitleText: '我的',
},
}
</route>
<template>
<view class="profile-container">
{{ JSON.stringify(userStore.userInfo) }}
<!-- 用户信息区域 -->
<view class="user-info-section">
<!-- #ifdef MP-WEIXIN -->
<button class="avatar-button" open-type="chooseAvatar" @chooseavatar="onChooseAvatar">
<wd-img :src="userStore.userInfo.avatar" width="80px" height="80px" radius="50%"></wd-img>
</button>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<view class="avatar-wrapper" @click="run">
<wd-img :src="userStore.userInfo.avatar" width="100%" height="100%" radius="50%"></wd-img>
</view>
<!-- #endif -->
<view class="user-details">
<!-- #ifdef MP-WEIXIN -->
<input
type="nickname"
class="weui-input"
placeholder="请输入昵称"
v-model="userStore.userInfo.username"
/>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<view class="username">{{ userStore.userInfo.username }}</view>
<!-- #endif -->
<view class="user-id">ID: {{ userStore.userInfo.id }}</view>
</view>
</view>
<!-- 功能区块 -->
<view class="function-section">
<view class="cell-group">
<view class="group-title">账号管理</view>
<wd-cell title="个人资料" is-link @click="handleProfileInfo">
<template #icon>
<wd-icon name="user" size="20px"></wd-icon>
</template>
</wd-cell>
<wd-cell title="账号安全" is-link @click="handlePassword">
<template #icon>
<wd-icon name="lock-on" size="20px"></wd-icon>
</template>
</wd-cell>
</view>
<view class="cell-group">
<view class="group-title">通用设置</view>
<wd-cell title="消息通知" is-link @click="handleInform">
<template #icon>
<wd-icon name="notification" size="20px"></wd-icon>
</template>
</wd-cell>
<wd-cell title="清理缓存" is-link @click="handleClearCache">
<template #icon>
<wd-icon name="clear" size="20px"></wd-icon>
</template>
</wd-cell>
<wd-cell title="应用更新" is-link @click="handleAppUpdate">
<template #icon>
<wd-icon name="refresh1" size="20px"></wd-icon>
</template>
</wd-cell>
<wd-cell title="关于我们" is-link @click="handleAbout">
<template #icon>
<wd-icon name="info-circle" size="20px"></wd-icon>
</template>
</wd-cell>
</view>
<view class="logout-button-wrapper">
<wd-button type="error" v-if="hasLogin" block @click="handleLogout">退出登录</wd-button>
<wd-button type="primary" v-else block @click="handleLogin">登录</wd-button>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { useUserStore } from '@/store'
import { useToast } from 'wot-design-uni'
import { uploadFileUrl, useUpload } from '@/utils/uploadFile'
import { storeToRefs } from 'pinia'
import { IUploadSuccessInfo } from '@/api/login.typings'
const userStore = useUserStore()
const toast = useToast()
const hasLogin = ref(false)
onShow((options) => {
hasLogin.value = !!uni.getStorageSync('token')
console.log('个人中心onShow', hasLogin.value, options)
hasLogin.value && useUserStore().getUserInfo()
})
// #ifndef MP-WEIXIN
//
const { run } = useUpload<IUploadSuccessInfo>(
uploadFileUrl.USER_AVATAR,
{},
{
onSuccess: (res) => useUserStore().getUserInfo(),
},
)
// #endif
//
const handleLogin = async () => {
// #ifdef MP-WEIXIN
//
await userStore.wxLogin()
hasLogin.value = true
// #endif
// #ifndef MP-WEIXIN
uni.navigateTo({ url: '/pages/login/index' })
// #endif
}
// #ifdef MP-WEIXIN
//
const onChooseAvatar = (e: any) => {
console.log('选择头像', e.detail)
const { avatarUrl } = e.detail
const { run } = useUpload<IUploadSuccessInfo>(
uploadFileUrl.USER_AVATAR,
{},
{
onSuccess: (res) => useUserStore().getUserInfo(),
},
avatarUrl,
)
run()
}
// #endif
// #ifdef MP-WEIXIN
//
const getUserInfo = (e: any) => {
console.log(e.detail)
}
// #endif
//
const handleProfileInfo = () => {
uni.navigateTo({ url: `/pages/mine/info/index` })
}
//
const handlePassword = () => {
uni.navigateTo({ url: `/pages/mine/password/index` })
}
//
const handleInform = () => {
// uni.navigateTo({ url: `/pages/mine/inform/index` })
toast.success('功能开发中')
}
//
const handleAppUpdate = () => {
// #ifdef MP
// #ifndef MP-HARMONY
const updateManager = uni.getUpdateManager()
updateManager.onCheckForUpdate(function (res) {
//
// console.log(res.hasUpdate)
if (res.hasUpdate) {
toast.success('检测到新版本,正在下载中...')
} else {
toast.success('已是最新版本')
}
})
updateManager.onUpdateReady(function (res) {
uni.showModal({
title: '更新提示',
content: '新版本已经准备好,是否重启应用?',
success(res) {
if (res.confirm) {
// applyUpdate
updateManager.applyUpdate()
}
},
})
})
updateManager.onUpdateFailed(function (res) {
//
toast.error('新版本下载失败')
})
// #endif
// #endif
// #ifndef MP
toast.success('功能开发中')
// #endif
}
//
const handleAbout = () => {
uni.navigateTo({ url: `/pages/mine/about/index` })
}
//
const handleClearCache = () => {
uni.showModal({
title: '清除缓存',
content: '确定要清除所有缓存吗?\n清除后需要重新登录',
success: (res) => {
if (res.confirm) {
try {
//
uni.clearStorageSync()
//
useUserStore().logout()
toast.success('清除缓存成功')
} catch (err) {
console.error('清除缓存失败:', err)
toast.error('清除缓存失败')
}
}
},
})
}
// 退
const handleLogout = () => {
uni.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
//
useUserStore().logout()
hasLogin.value = false
// 退
toast.success('退出登录成功')
// #ifdef MP-WEIXIN
//
// uni.reLaunch({ url: '/pages/index/index' })
// #endif
// #ifndef MP-WEIXIN
//
// uni.reLaunch({ url: '/pages/login/index' })
// #endif
}
},
})
}
</script>
<style lang="scss" scoped>
/* 基础样式 */
.profile-container {
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, 'Helvetica Neue', sans-serif;
background-color: #f7f8fa;
}
/* 用户信息区域 */
.user-info-section {
display: flex;
align-items: center;
padding: 40rpx;
margin: 30rpx 30rpx 20rpx;
background-color: #fff;
border-radius: 24rpx;
box-shadow: 0 6rpx 20rpx rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
}
.avatar-wrapper {
width: 160rpx;
height: 160rpx;
margin-right: 40rpx;
overflow: hidden;
border: 4rpx solid #f5f5f5;
border-radius: 50%;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
}
.avatar-button {
height: 160rpx;
padding: 0;
margin-right: 40rpx;
overflow: hidden;
border: 4rpx solid #f5f5f5;
border-radius: 50%;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
}
.user-details {
flex: 1;
}
.username {
margin-bottom: 12rpx;
font-size: 38rpx;
font-weight: 600;
color: #333;
letter-spacing: 0.5rpx;
}
.user-id {
font-size: 28rpx;
color: #666;
}
.user-created {
margin-top: 8rpx;
font-size: 24rpx;
color: #999;
}
/* 功能区块 */
.function-section {
padding: 0 20rpx;
margin-top: 20rpx;
}
.cell-group {
margin-bottom: 20rpx;
overflow: hidden;
background-color: #fff;
border-radius: 16rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.05);
}
.group-title {
padding: 24rpx 30rpx 16rpx;
font-size: 30rpx;
font-weight: 500;
color: #999;
background-color: #fafafa;
}
:deep(.wd-cell) {
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.wd-cell__title {
margin-left: 5px;
font-size: 32rpx;
color: #333;
}
.cell-icon {
margin-right: 20rpx;
font-size: 36rpx;
}
}
/* 退出登录按钮 */
.logout-button-wrapper {
padding: 40rpx 30rpx;
}
:deep(.wd-button--danger) {
height: 88rpx;
font-size: 32rpx;
line-height: 88rpx;
color: #fff;
background-color: #f53f3f;
border-radius: 44rpx;
}
</style>

View File

@ -0,0 +1,190 @@
<route lang="json5">
{
style: {
navigationBarTitleText: '个人资料',
},
}
</route>
<template>
<view class="profile-info-container">
<view class="profile-card">
<view class="form-wrapper">
<wd-form ref="formRef" :model="formData" label-width="160rpx" class="profile-form">
<wd-cell-group class="form-group">
<!-- 昵称 -->
<view class="sex-field">
<text class="field-label">昵称</text>
<wd-input
prop="name"
clearable
v-model="formData.name"
placeholder="请输入昵称"
:rules="[{ required: true, message: '请填写昵称' }]"
class="form-input"
/>
</view>
<!-- 性别 -->
<view class="sex-field">
<text class="field-label">性别</text>
<wd-radio-group
v-model="formData.sex"
shape="button"
:rules="[{ required: true, message: '请选择性别' }]"
>
<wd-radio :value="'1'"></wd-radio>
<wd-radio :value="'0'"></wd-radio>
</wd-radio-group>
</view>
</wd-cell-group>
</wd-form>
<!-- 操作按钮 -->
<view class="form-actions">
<wd-button type="primary" size="large" @click="handleSubmit">保存修改</wd-button>
</view>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { useUserStore } from '@/store'
import { storeToRefs } from 'pinia'
import { toast } from '@/utils/toast'
import { updateInfo } from '@/api/login'
//
const formRef = ref()
//
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
//
const formData = ref({
id: userInfo.value.id,
name: userInfo.value.name,
sex: userInfo.value.sex,
})
//
const handleSubmit = async () => {
//
const valid = await formRef.value.validate()
if (!valid) return
const { message } = await updateInfo(formData.value)
await useUserStore().getUserInfo()
toast.success(message)
}
</script>
<style lang="scss" scoped>
.profile-info-container {
min-height: 100vh;
background-color: #f5f7fa;
padding: 30rpx;
}
.profile-card {
background-color: #ffffff;
border-radius: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
overflow: hidden;
}
.card-header {
padding: 40rpx 30rpx 20rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.card-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
position: relative;
display: inline-block;
padding-bottom: 16rpx;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 60rpx;
height: 6rpx;
background: linear-gradient(90deg, #4a7bff, #6a5acd);
border-radius: 6rpx;
}
}
.form-wrapper {
padding: 30rpx;
}
.form-group {
border-radius: 16rpx;
overflow: hidden;
margin-bottom: 40rpx;
}
.form-input {
font-size: 30rpx;
}
.sex-field {
display: flex;
align-items: center;
padding: 24rpx 30rpx;
background-color: #ffffff;
}
.field-label {
width: 160rpx;
font-size: 30rpx;
color: #333;
}
.radio-group {
flex: 1;
display: flex;
gap: 20rpx;
}
.radio-btn {
flex: 1;
height: 80rpx;
line-height: 80rpx;
text-align: center;
font-size: 30rpx;
border-radius: 12rpx;
background-color: #f5f7fa;
&:active {
opacity: 0.8;
}
}
.form-actions {
display: flex;
flex-direction: row;
gap: 20rpx;
}
.submit-btn {
height: 90rpx;
border-radius: 45rpx;
font-size: 32rpx;
font-weight: 500;
background: linear-gradient(135deg, #4a7bff, #6a5acd);
box-shadow: 0 8rpx 16rpx rgba(74, 123, 255, 0.2);
transition: all 0.3s ease;
&:active {
transform: translateY(2rpx);
box-shadow: 0 4rpx 8rpx rgba(74, 123, 255, 0.15);
}
}
</style>

View File

@ -0,0 +1,203 @@
<route lang="json5">
{
style: {
navigationBarTitleText: '修改密码',
},
}
</route>
<template>
<view class="profile-info-container">
<view class="profile-card">
<view class="form-wrapper">
<wd-form ref="formRef" :model="formData" label-width="160rpx" class="profile-form">
<wd-cell-group class="form-group">
<!-- 昵称 -->
<view class="sex-field">
<text class="field-label">旧密码</text>
<wd-input
prop="oldPassword"
clearable
v-model="formData.oldPassword"
placeholder="请输入旧密码"
show-password
:rules="[{ required: true, message: '请填写旧密码' }]"
class="form-input"
/>
</view>
<view class="sex-field">
<text class="field-label">新密码</text>
<wd-input
prop="newPassword"
clearable
v-model="formData.newPassword"
placeholder="请输入新密码"
show-password
:rules="[{ required: true, message: '请填写新密码' }]"
class="form-input"
/>
</view>
<view class="sex-field">
<text class="field-label">确认密码</text>
<wd-input
prop="confirmPassword"
clearable
v-model="formData.confirmPassword"
placeholder="请输入新密码"
show-password
:rules="[{ required: true, message: '请填写新密码' }]"
class="form-input"
/>
</view>
</wd-cell-group>
</wd-form>
<!-- 操作按钮 -->
<view class="form-actions">
<wd-button type="primary" size="large" @click="handleSubmit">保存修改</wd-button>
</view>
</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { useUserStore } from '@/store'
import { storeToRefs } from 'pinia'
import { toast } from '@/utils/toast'
import { updateInfo, updateUserPassword } from '@/api/login'
//
const formRef = ref()
//
const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore)
//
const formData = ref({
id: userInfo.value.id,
oldPassword: '',
newPassword: '',
confirmPassword: '',
})
//
const handleSubmit = async () => {
//
const valid = await formRef.value.validate()
if (!valid) return
const { message } = await updateUserPassword(formData.value)
await useUserStore().logout()
toast.success('修改成功,请重新登录')
}
</script>
<style lang="scss" scoped>
.profile-info-container {
min-height: 100vh;
background-color: #f5f7fa;
padding: 30rpx;
}
.profile-card {
background-color: #ffffff;
border-radius: 24rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
overflow: hidden;
}
.card-header {
padding: 40rpx 30rpx 20rpx;
border-bottom: 2rpx solid #f0f0f0;
}
.card-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
position: relative;
display: inline-block;
padding-bottom: 16rpx;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 60rpx;
height: 6rpx;
background: linear-gradient(90deg, #4a7bff, #6a5acd);
border-radius: 6rpx;
}
}
.form-wrapper {
padding: 30rpx;
}
.form-group {
border-radius: 16rpx;
overflow: hidden;
margin-bottom: 40rpx;
}
.form-input {
font-size: 30rpx;
}
.sex-field {
display: flex;
align-items: center;
padding: 24rpx 30rpx;
background-color: #ffffff;
}
.field-label {
width: 160rpx;
font-size: 30rpx;
color: #333;
}
.radio-group {
flex: 1;
display: flex;
gap: 20rpx;
}
.radio-btn {
flex: 1;
height: 80rpx;
line-height: 80rpx;
text-align: center;
font-size: 30rpx;
border-radius: 12rpx;
background-color: #f5f7fa;
&:active {
opacity: 0.8;
}
}
.form-actions {
display: flex;
flex-direction: row;
gap: 20rpx;
}
.submit-btn {
height: 90rpx;
border-radius: 45rpx;
font-size: 32rpx;
font-weight: 500;
background: linear-gradient(135deg, #4a7bff, #6a5acd);
box-shadow: 0 8rpx 16rpx rgba(74, 123, 255, 0.2);
transition: all 0.3s ease;
&:active {
transform: translateY(2rpx);
box-shadow: 0 4rpx 8rpx rgba(74, 123, 255, 0.15);
}
}
</style>

View File

@ -1,28 +1,27 @@
import { http } from '@/utils/http'
export interface IFooItem {
id: string
name: string
}
/** GET 请求 */
export function getFooAPI(name: string) {
export const getFooAPI = (name: string) => {
return http.get<IFooItem>('/foo', { name })
}
/** GET 请求;支持 传递 header 的范例 */
export function getFooAPI2(name: string) {
export const getFooAPI2 = (name: string) => {
return http.get<IFooItem>('/foo', { name }, { 'Content-Type-100': '100' })
}
/** POST 请求 */
export function postFooAPI(name: string) {
export const postFooAPI = (name: string) => {
return http.post<IFooItem>('/foo', { name })
}
/** POST 请求;需要传递 query 参数的范例微信小程序经常有同时需要query参数和body参数的场景 */
export function postFooAPI2(name: string) {
export const postFooAPI2 = (name: string) => {
return http.post<IFooItem>('/foo', { name })
}
/** POST 请求;支持 传递 header 的范例 */
export function postFooAPI3(name: string) {
export const postFooAPI3 = (name: string) => {
return http.post<IFooItem>('/foo', { name }, { name }, { 'Content-Type-100': '100' })
}

View File

@ -1,14 +1,14 @@
import type { IUserInfoVo } from '@/api/types/login'
import { defineStore } from 'pinia'
import { ref } from 'vue'
import {
getUserInfo as _getUserInfo,
login as _login,
logout as _logout,
getUserInfo as _getUserInfo,
wxLogin as _wxLogin,
logout as _logout,
getWxCode,
} from '@/api/login'
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { toast } from '@/utils/toast'
import { IUserInfoVo } from '@/api/login.typings'
// 初始化状态
const userInfoState: IUserInfoVo = {
@ -29,35 +29,17 @@ export const useUserStore = defineStore(
// 若头像为空 则使用默认头像
if (!val.avatar) {
val.avatar = userInfoState.avatar
}
else {
} 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 getUserInfo = async () => {
const res = await _getUserInfo()
const userInfo = res.data
setUserInfo(userInfo)
uni.setStorageSync('userInfo', userInfo)
uni.setStorageSync('token', userInfo.token)
// TODO 这里可以增加获取用户路由的方法 根据用户的角色动态生成路由
return res
}
/**
*
* @param credentials
@ -72,10 +54,21 @@ export const useUserStore = defineStore(
const res = await _login(credentials)
console.log('登录信息', res)
toast.success('登录成功')
await getUserInfo()
getUserInfo()
return res
}
/**
*
*/
const getUserInfo = async () => {
const res = await _getUserInfo()
const userInfo = res.data
setUserInfo(userInfo)
uni.setStorageSync('userInfo', userInfo)
uni.setStorageSync('token', userInfo.token)
// TODO 这里可以增加获取用户路由的方法 根据用户的角色动态生成路由
return res
}
/**
* 退
*/
@ -92,7 +85,7 @@ export const useUserStore = defineStore(
console.log('微信登录code', data)
const res = await _wxLogin(data)
await getUserInfo()
getUserInfo()
return res
}
@ -101,7 +94,6 @@ export const useUserStore = defineStore(
login,
wxLogin,
getUserInfo,
setUserAvatar,
logout,
}
},

View File

@ -1,4 +1,4 @@
@import './iconfont.css';
// @import './iconfont.css';
.test {
// 可以通过 @apply 多个样式封装整体样式

6
src/typings.d.ts vendored
View File

@ -1,14 +1,14 @@
// 全局要用的类型放到这里
declare global {
interface IResData<T> {
type IResData<T> = {
code: number
msg: string
data: T
}
// uni.uploadFile文件上传参数
interface IUniUploadFileOptions {
type IUniUploadFileOptions = {
file?: File
files?: UniApp.UploadFileOptionFiles[]
filePath?: string
@ -16,7 +16,7 @@ declare global {
formData?: any
}
interface IUserInfo {
type IUserInfo = {
nickname?: string
avatar?: string
/** 微信的 openid非微信没有这个字段 */

View File

@ -6,7 +6,7 @@ export enum TestEnum {
}
// uni.uploadFile文件上传参数
export interface IUniUploadFileOptions {
export type IUniUploadFileOptions = {
file?: File
files?: UniApp.UploadFileOptionFiles[]
filePath?: string

View File

@ -1,6 +1,6 @@
import type { CustomRequestOptions } from '@/interceptors/request'
import { CustomRequestOptions } from '@/interceptors/request'
export function http<T>(options: CustomRequestOptions) {
export const http = <T>(options: CustomRequestOptions) => {
// 1. 返回 Promise 对象
return new Promise<IResData<T>>((resolve, reject) => {
uni.request({
@ -15,20 +15,18 @@ export function http<T>(options: CustomRequestOptions) {
if (res.statusCode >= 200 && res.statusCode < 300) {
// 2.1 提取核心数据 res.data
resolve(res.data as IResData<T>)
}
else if (res.statusCode === 401) {
} else if (res.statusCode === 401) {
// 401错误 -> 清理用户信息,跳转到登录页
// userStore.clearUserInfo()
// uni.navigateTo({ url: '/pages/login/login' })
reject(res)
}
else {
} else {
// 其他错误 -> 根据后端错误信息轻提示
!options.hideErrorToast
&& uni.showToast({
icon: 'none',
title: (res.data as IResData<T>).msg || '请求错误',
})
!options.hideErrorToast &&
uni.showToast({
icon: 'none',
title: (res.data as IResData<T>).msg || '请求错误',
})
reject(res)
}
},
@ -51,13 +49,16 @@ export function http<T>(options: CustomRequestOptions) {
* @param header json格式
* @returns
*/
export function httpGet<T>(url: string, query?: Record<string, any>, header?: Record<string, any>, options?: Partial<CustomRequestOptions>) {
export const httpGet = <T>(
url: string,
query?: Record<string, any>,
header?: Record<string, any>,
) => {
return http<T>({
url,
query,
method: 'GET',
header,
...options,
})
}
@ -69,40 +70,51 @@ export function httpGet<T>(url: string, query?: Record<string, any>, header?: Re
* @param header json格式
* @returns
*/
export function httpPost<T>(url: string, data?: Record<string, any>, query?: Record<string, any>, header?: Record<string, any>, options?: Partial<CustomRequestOptions>) {
export const httpPost = <T>(
url: string,
data?: Record<string, any>,
query?: Record<string, any>,
header?: Record<string, any>,
) => {
return http<T>({
url,
query,
data,
method: 'POST',
header,
...options,
})
}
/**
* PUT
*/
export function httpPut<T>(url: string, data?: Record<string, any>, query?: Record<string, any>, header?: Record<string, any>, options?: Partial<CustomRequestOptions>) {
export const httpPut = <T>(
url: string,
data?: Record<string, any>,
query?: Record<string, any>,
header?: Record<string, any>,
) => {
return http<T>({
url,
data,
query,
method: 'PUT',
header,
...options,
})
}
/**
* DELETE query
*/
export function httpDelete<T>(url: string, query?: Record<string, any>, header?: Record<string, any>, options?: Partial<CustomRequestOptions>) {
export const httpDelete = <T>(
url: string,
query?: Record<string, any>,
header?: Record<string, any>,
) => {
return http<T>({
url,
query,
method: 'DELETE',
header,
...options,
})
}

View File

@ -1,7 +1,9 @@
import { pages, subPackages } from '@/pages.json'
import pagesConfig from '@/pages.json'
import { isMpWeixin } from './platform'
export function getLastPage() {
const { pages, subPackages, tabBar = { list: [] } } = { ...pagesConfig }
export const getLastPage = () => {
// getCurrentPages() 至少有1个元素所以不再额外判断
// const lastPage = getCurrentPages().at(-1)
// 上面那个在低版本安卓中打包会报错,所以改用下面这个【虽然我加了 src/interceptions/prototype.ts但依然报错】
@ -9,12 +11,44 @@ export function getLastPage() {
return pages[pages.length - 1]
}
/** 判断当前页面是否是 tabbar 页 */
export const getIsTabbar = () => {
try {
const lastPage = getLastPage()
const currPath = lastPage?.route
return Boolean(tabBar?.list?.some((item) => item.pagePath === currPath))
} catch {
return false
}
}
/**
* tabbar
* @param path
* @returns true: tabbar false: tabbar
*/
export const isTableBar = (path: string) => {
if (!tabBar) {
return false
}
if (!tabBar.list.length) {
// 通常有 tabBar 的话list 不能有空且至少有2个元素这里其实不用处理
return false
}
// 这里需要处理一下 path因为 tabBar 中的 pagePath 是不带 /pages 前缀的
if (path.startsWith('/')) {
path = path.substring(1)
}
return !!tabBar.list.find((e) => e.pagePath === path)
}
/**
* path redirectPath
* path '/pages/login/index'
* redirectPath '/pages/demo/base/route-interceptor'
*/
export function currRoute() {
export const currRoute = () => {
const lastPage = getLastPage()
const currRoute = (lastPage as any).$page
// console.log('lastPage.$page:', currRoute)
@ -29,7 +63,7 @@ export function currRoute() {
return getUrlObj(fullPath)
}
function ensureDecodeURIComponent(url: string) {
const ensureDecodeURIComponent = (url: string) => {
if (url.startsWith('%')) {
return ensureDecodeURIComponent(decodeURIComponent(url))
}
@ -40,7 +74,7 @@ function ensureDecodeURIComponent(url: string) {
* 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) {
export const getUrlObj = (url: string) => {
const [path, queryStr] = url.split('?')
// console.log(path, queryStr)
@ -63,15 +97,16 @@ export function getUrlObj(url: string) {
* key needLogin, route-block 使
* key pages key, key
*/
export function getAllPages(key = 'needLogin') {
export const getAllPages = (key = 'needLogin') => {
// 这里处理主包
const mainPages = pages
.filter(page => !key || page[key])
.map(page => ({
...page,
path: `/${page.path}`,
}))
const mainPages = [
...pages
.filter((page) => !key || page[key])
.map((page) => ({
...page,
path: `/${page.path}`,
})),
]
// 这里处理分包
const subPages: any[] = []
subPackages.forEach((subPageObj) => {
@ -79,7 +114,7 @@ export function getAllPages(key = 'needLogin') {
const { root } = subPageObj
subPageObj.pages
.filter(page => !key || page[key])
.filter((page) => !key || page[key])
.forEach((page: { path: string } & Record<string, any>) => {
subPages.push({
...page,
@ -96,18 +131,18 @@ export function getAllPages(key = 'needLogin') {
* pages
* path
*/
export const getNeedLoginPages = (): string[] => getAllPages('needLogin').map(page => page.path)
export const getNeedLoginPages = (): string[] => getAllPages('needLogin').map((page) => page.path)
/**
* pages
* path
*/
export const needLoginPages: string[] = getAllPages('needLogin').map(page => page.path)
export const needLoginPages: string[] = getAllPages('needLogin').map((page) => page.path)
/**
* baseUrl
*/
export function getEnvBaseUrl() {
export const getEnvBaseUrl = () => {
// 请求基准地址
let baseUrl = import.meta.env.VITE_SERVER_BASEURL
@ -136,7 +171,7 @@ export function getEnvBaseUrl() {
/**
* UPLOAD_BASEURL
*/
export function getEnvBaseUploadUrl() {
export const getEnvBaseUploadUrl = () => {
// 请求基准地址
let baseUploadUrl = import.meta.env.VITE_UPLOAD_BASEURL

View File

@ -1,29 +0,0 @@
/**
* URL查询字符串 qs
*
* @param obj
* @returns
*/
export function stringifyQuery(obj: Record<string, any>): 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('&')
}

View File

@ -1,11 +1,11 @@
import type { CustomRequestOptions } from '@/interceptors/request'
import { CustomRequestOptions } from '@/interceptors/request'
/**
* 请求方法: 主要是对 uni.request openapi-ts-request request
* @param options
* @returns Promise
*/
function http<T>(options: CustomRequestOptions) {
const http = <T>(options: CustomRequestOptions) => {
// 1. 返回 Promise 对象
return new Promise<T>((resolve, reject) => {
uni.request({
@ -20,20 +20,18 @@ function http<T>(options: CustomRequestOptions) {
if (res.statusCode >= 200 && res.statusCode < 300) {
// 2.1 提取核心数据 res.data
resolve(res.data as T)
}
else if (res.statusCode === 401) {
} else if (res.statusCode === 401) {
// 401错误 -> 清理用户信息,跳转到登录页
// userStore.clearUserInfo()
// uni.navigateTo({ url: '/pages/login/login' })
reject(res)
}
else {
} else {
// 其他错误 -> 根据后端错误信息轻提示
!options.hideErrorToast
&& uni.showToast({
icon: 'none',
title: (res.data as T & { msg?: string })?.msg || '请求错误',
})
!options.hideErrorToast &&
uni.showToast({
icon: 'none',
title: (res.data as T & { msg?: string })?.msg || '请求错误',
})
reject(res)
}
},

View File

@ -21,8 +21,8 @@ export function showToast(options: ToastOptions | string) {
position: 'middle',
message: '',
}
const mergedOptions
= typeof options === 'string'
const mergedOptions =
typeof options === 'string'
? { ...defaultOptions, message: options }
: { ...defaultOptions, ...options }
// 映射position到uniapp支持的格式

View File

@ -21,7 +21,7 @@ import { toast } from './toast'
*/
export const uploadFileUrl = {
/** 用户头像上传地址 */
USER_AVATAR: `${import.meta.env.VITE_SERVER_BASEURL}/user/avatar`,
USER_AVATAR: import.meta.env.VITE_SERVER_BASEURL + '/user/avatar',
}
/**
@ -31,7 +31,12 @@ export const uploadFileUrl = {
* @param formData
* @param options
*/
export function useFileUpload<T = string>(url: string, filePath: string, formData: Record<string, any> = {}, options: Omit<UploadOptions, 'sourceType' | 'sizeType' | 'count'> = {}) {
export const useFileUpload = <T = string>(
url: string,
filePath: string,
formData: Record<string, any> = {},
options: Omit<UploadOptions, 'sourceType' | 'sizeType' | 'count'> = {},
) => {
return useUpload<T>(
url,
formData,
@ -56,7 +61,7 @@ export interface UploadOptions {
/** 上传进度回调函数 */
onProgress?: (progress: number) => void
/** 上传成功回调函数 */
onSuccess?: (res: Record<string, any>) => void
onSuccess?: (res: UniApp.UploadFileSuccessCallbackResult) => void
/** 上传失败回调函数 */
onError?: (err: Error | UniApp.GeneralCallbackResult) => void
/** 上传完成回调函数(无论成功失败) */
@ -71,9 +76,13 @@ export interface UploadOptions {
* @param options
* @returns
*/
export function useUpload<T = string>(url: string, formData: Record<string, any> = {}, options: UploadOptions = {},
export const useUpload = <T = string>(
url: string,
formData: Record<string, any> = {},
options: UploadOptions = {},
/** 直接传入文件路径,跳过选择器 */
directFilePath?: string) {
directFilePath?: string,
) => {
/** 上传中状态 */
const loading = ref(false)
/** 上传错误状态 */
@ -152,8 +161,7 @@ export function useUpload<T = string>(url: string, formData: Record<string, any>
success: (res) => {
const file = res.tempFiles[0]
// 检查文件大小是否符合限制
if (!checkFileSize(file.size))
return
if (!checkFileSize(file.size)) return
// 开始上传
loading.value = true
@ -240,7 +248,7 @@ interface UploadFileOptions<T> {
/** 上传进度回调 */
onProgress?: (progress: number) => void
/** 上传成功回调 */
onSuccess?: (res: Record<string, any>) => void
onSuccess?: (res: UniApp.UploadFileSuccessCallbackResult) => void
/** 上传失败回调 */
onError?: (err: Error | UniApp.GeneralCallbackResult) => void
/** 上传完成回调 */
@ -280,15 +288,20 @@ function uploadFile<T>({
},
// 确保文件名称合法
success: (uploadFileRes) => {
console.log('上传文件成功:', uploadFileRes)
try {
// 解析响应数据
const { data: _data } = JSON.parse(uploadFileRes.data)
// 上传成功
data.value = _data as T
onSuccess?.(_data)
}
catch (err) {
const result = JSON.parse(uploadFileRes.data)
if (result.code === 1) {
// 上传成功
data.value = result.data as T
onSuccess?.(uploadFileRes)
} else {
// 业务错误
const err = new Error(result.message || '上传失败')
error.value = true
onError?.(err)
}
} catch (err) {
// 响应解析错误
console.error('解析上传响应失败:', err)
error.value = true
@ -313,8 +326,7 @@ function uploadFile<T>({
progress.value = res.progress
onProgress?.(res.progress)
})
}
catch (err) {
} catch (err) {
// 创建上传任务失败
console.error('创建上传任务失败:', err)
error.value = true

View File

@ -1,15 +1,20 @@
{
"compilerOptions": {
"composite": true,
"lib": ["esnext", "dom"],
"baseUrl": ".",
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "Node",
"paths": {
"@/*": ["./src/*"],
"@img/*": ["./src/static/*"]
},
"resolveJsonModule": true,
"noImplicitThis": true,
"allowSyntheticDefaultImports": true,
"allowJs": true,
"sourceMap": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"outDir": "dist",
"lib": ["esnext", "dom"],
"types": [
"@dcloudio/types",
"@uni-helper/uni-types",
@ -17,17 +22,12 @@
"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"]
},
"exclude": ["node_modules"],
"include": [
"src/**/*.ts",
"src/**/*.js",
@ -36,6 +36,5 @@
"src/**/*.jsx",
"src/**/*.vue",
"src/**/*.json"
],
"exclude": ["node_modules"]
]
}

View File

@ -1,26 +1,20 @@
// https://www.npmjs.com/package/@uni-helper/unocss-preset-uni
import { presetUni } from '@uni-helper/unocss-preset-uni'
import {
defineConfig,
presetAttributify,
presetIcons,
presetAttributify,
transformerDirectives,
transformerVariantGroup,
} from 'unocss'
export default defineConfig({
presets: [
presetUni({
attributify: {
// prefix: 'fg-', // 如果加前缀,则需要在代码里面使用 `fg-` 前缀,如:<div fg-border="1px solid #000"></div>
prefixedOnly: true,
},
}),
presetUni(),
presetIcons({
scale: 1.2,
warn: true,
extraProperties: {
'display': 'inline-block',
display: 'inline-block',
'vertical-align': 'middle',
},
}),
@ -39,7 +33,6 @@ export default defineConfig({
center: 'flex justify-center items-center',
},
],
safelist: [],
rules: [
[
'p-safe',

View File

@ -1,6 +1,5 @@
import path from 'node:path'
import process from 'node:process'
import fs from 'fs-extra'
import path from 'path'
export function copyNativeRes() {
const waitPath = path.resolve(__dirname, '../src/nativeResources')
@ -32,8 +31,7 @@ export function copyNativeRes() {
console.log(
`[copyNativeRes] 成功将 nativeResources 目录中的资源移动到构建目录:${buildPath}`,
)
}
catch (error) {
} catch (error) {
console.error(`[copyNativeRes] 复制资源失败:`, error)
}
},

View File

@ -1,37 +0,0 @@
// 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

View File

@ -1,27 +1,26 @@
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'
import dayjs from 'dayjs'
import path from 'node:path'
import { defineConfig, loadEnv } from 'vite'
// @see https://uni-helper.js.org/vite-plugin-uni-pages
import UniPages from '@uni-helper/vite-plugin-uni-pages'
// @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-platform
// 需要与 @uni-helper/vite-plugin-uni-pages 插件一起使用
import UniPlatform from '@uni-helper/vite-plugin-uni-platform'
// @see https://github.com/uni-helper/vite-plugin-uni-manifest
import UniManifest from '@uni-helper/vite-plugin-uni-manifest'
/**
*
* @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'
import { copyNativeRes } from './vite-plugins/copyNativeRes'
import Components from '@uni-helper/vite-plugin-uni-components'
// https://vitejs.dev/config/
export default async ({ command, mode }) => {
@ -75,7 +74,7 @@ export default async ({ command, mode }) => {
// 自定义插件禁用 vite:vue 插件的 devToolsEnabled强制编译 vue 模板时 inline 为 true
name: 'fix-vite-plugin-vue',
configResolved(config) {
const plugin = config.plugins.find(p => p.name === 'vite:vue')
const plugin = config.plugins.find((p) => p.name === 'vite:vue')
if (plugin && plugin.api && plugin.api.options) {
plugin.api.options.devToolsEnabled = false
}
@ -91,7 +90,7 @@ export default async ({ command, mode }) => {
// Optimization 插件需要 page.json 文件,故应在 UniPages 插件之后执行
Optimization({
enable: {
'optimization': true,
optimization: true,
'async-import': true,
'async-component': true,
},
@ -113,16 +112,16 @@ export default async ({ command, mode }) => {
},
},
// 打包分析插件h5 + 生产环境才弹出
UNI_PLATFORM === 'h5'
&& mode === 'production'
&& visualizer({
filename: './node_modules/.cache/visualizer/stats.html',
open: true,
gzipSize: true,
brotliSize: true,
}),
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(),
UNI_PLATFORM === 'app' && copyNativeRes(),
Components({
extensions: ['vue'],
deep: true, // 是否递归扫描子目录,
@ -130,7 +129,6 @@ export default async ({ command, mode }) => {
dts: 'src/types/components.d.ts', // 自动生成的组件类型声明文件路径(用于 TypeScript 支持)
}),
Uni(),
updatePackageJson(),
],
define: {
__UNI_PLATFORM__: JSON.stringify(UNI_PLATFORM),
@ -163,15 +161,14 @@ export default async ({ command, mode }) => {
[VITE_APP_PROXY_PREFIX]: {
target: VITE_SERVER_BASEURL,
changeOrigin: true,
rewrite: path => path.replace(new RegExp(`^${VITE_APP_PROXY_PREFIX}`), ''),
rewrite: (path) => path.replace(new RegExp(`^${VITE_APP_PROXY_PREFIX}`), ''),
},
}
: undefined,
},
build: {
sourcemap: false,
// 方便非h5端调试
// sourcemap: VITE_SHOW_SOURCEMAP === 'true', // 默认是false
sourcemap: VITE_SHOW_SOURCEMAP === 'true', // 默认是false
target: 'es6',
// 开发环境不用压缩
minify: mode === 'development' ? false : 'terser',