feat: init

This commit is contained in:
Mu Yi 2025-09-18 09:26:30 +08:00
commit 6b79088517
114 changed files with 20596 additions and 0 deletions

3
.commitlintrc.cjs Normal file
View File

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

13
.editorconfig Normal file
View File

@ -0,0 +1,13 @@
root = true
[*] # 表示所有文件适用
charset = utf-8 # 设置文件字符集为 utf-8
indent_style = space # 缩进风格(tab | space)
indent_size = 2 # 缩进大小
end_of_line = lf # 控制换行类型(lf | cr | crlf)
trim_trailing_whitespace = true # 去除行首的任意空白字符
insert_final_newline = true # 始终在文件末尾插入一个新行
[*.md] # 表示仅 md 文件适用以下规则
max_line_length = off # 关闭最大行长度限制
trim_trailing_whitespace = false # 关闭末尾空格修剪

31
.github/release.yml vendored Normal file
View File

@ -0,0 +1,31 @@
categories:
- title: 🚀 新功能
labels: [feat, feature]
- title: 🛠️ 修复
labels: [fix, bugfix]
- title: 💅 样式
labels: [style]
- title: 📄 文档
labels: [docs]
- title: ⚡️ 性能
labels: [perf]
- title: 🧪 测试
labels: [test]
- title: ♻️ 重构
labels: [refactor]
- title: 📦 构建
labels: [build]
- title: 🚨 补丁
labels: [patch, hotfix]
- title: 🌐 发布
labels: [release, publish]
- title: 🔧 流程
labels: [ci, cd, workflow]
- title: ⚙️ 配置
labels: [config, chore]
- title: 📁 文件
labels: [file]
- title: 🎨 格式化
labels: [format]
- title: 🔀 其他
labels: [other, misc]

80
.github/workflows/auto-merge.yml vendored Normal file
View File

@ -0,0 +1,80 @@
name: Auto Merge Main to Other Branches
on:
push:
branches:
- main
workflow_dispatch: # 手动触发
jobs:
merge-to-i18n:
name: Merge main into i18n
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GH_TOKEN_AUTO_MERGE }}
- name: Merge main into i18n
run: |
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git checkout i18n
git merge main --no-ff -m "Auto merge main into i18n"
git push origin i18n
merge-to-base-sard-ui:
name: Merge main into base-sard-ui
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GH_TOKEN_AUTO_MERGE }}
- name: Merge main into base-sard-ui
run: |
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git checkout base-sard-ui
git merge main --no-ff -m "Auto merge main into base-sard-ui"
git push origin base-sard-ui
merge-to-base-uv-ui:
name: Merge main into base-uv-ui
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GH_TOKEN_AUTO_MERGE }}
- name: Merge main into base-uv-ui
run: |
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git checkout base-uv-ui
git merge main --no-ff -m "Auto merge main into base-uv-ui"
git push origin base-uv-ui
merge-to-base-uview-plus:
name: Merge main into base-uview-plus
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GH_TOKEN_AUTO_MERGE }}
- name: Merge main into base-uview-plus
run: |
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git checkout base-uview-plus
git merge main --no-ff -m "Auto merge main into base-uview-plus"
git push origin base-uview-plus

119
.github/workflows/release-log.yml vendored Normal file
View File

@ -0,0 +1,119 @@
name: Auto Release
on:
push:
tags:
- 'v*'
permissions:
contents: write
pull-requests: read
issues: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Install yq
run: sudo snap install yq
- name: Generate changelog
id: changelog
env:
CONFIG_FILE: .github/release.yml
run: |
# 解析配置文件
declare -A category_map
while IFS=";" read -r title labels; do
for label in $labels; do
category_map[$label]="$title"
done
done < <(yq -o=tsv '.categories[] | [.title, (.labels | join(" "))] | join(";")' $CONFIG_FILE)
# 获取版本范围
mapfile -t tags < <(git tag -l --sort=-version:refname)
current_tag=${tags[0]}
previous_tag=${tags[1]:-}
if [[ -z "$previous_tag" ]]; then
commit_range="$current_tag"
echo "首次发布版本: $current_tag"
else
commit_range="$previous_tag..$current_tag"
echo "版本范围: $commit_range"
fi
# 获取所有符合规范的提交
commits=$(git log --pretty=format:"%s|%h" "$commit_range")
# 生成分类日志
declare -A log_entries
while IFS="|" read -r subject hash; do
# type=$(echo "$subject" | cut -d':' -f1 | tr -d ' ')
type=$(echo "$subject" | sed -E 's/^([[:alnum:]]+)(\(.*\))?:.*/\1/' | tr -d ' ')
found=0
for label in "${!category_map[@]}"; do
if [[ "$type" == "$label" ]]; then
entry="- ${subject} (${hash:0:7})"
log_entries[${category_map[$label]}]+="$entry"$'\n'
found=1
break
fi
done
if [[ $found -eq 0 ]]; then
entry="- ${subject} (${hash:0:7})"
log_entries["其他"]+="$entry"$'\n'
fi
done <<< "$commits"
# 统计提交数量
commit_count=$(git log --oneline "$commit_range" | wc -l)
# 统计受影响的文件数量
file_count=$(git diff --name-only "$commit_range" | wc -l)
# 统计贡献者信息
contributor_stats=$(git shortlog -sn "$commit_range")
contributor_notes=""
while IFS= read -r line; do
commits=$(echo "$line" | awk '{print $1}')
name=$(echo "$line" | awk '{$1=""; print $0}' | sed 's/^ //')
contributor_notes+="- @${name} (${commits} commits)\n"
done <<< "$contributor_stats"
# 构建输出内容
release_notes="## 版本更新日志 ($current_tag)\n\n"
while IFS= read -r category; do
if [[ -n "${log_entries[$category]}" ]]; then
release_notes+="### $category\n${log_entries[$category]}\n"
fi
done < <(yq '.categories[].title' $CONFIG_FILE)
# 构建输出内容
release_notes="## 版本更新日志 ($current_tag)\n\n"
current_date=$(date +"%Y-%m-%d")
# 添加发布日期和下载统计信息
release_notes+=" ### 📅 发布日期: ${current_date}\n"
while IFS= read -r category; do
if [[ -n "${log_entries[$category]}" ]]; then
release_notes+="### $category\n${log_entries[$category]}\n"
fi
done < <(yq '.categories[].title' $CONFIG_FILE)
# 添加统计信息
release_notes+="### 📊 统计信息\n"
release_notes+="- 本次发布包含 ${commit_count} 个提交\n"
release_notes+="- 影响 ${file_count} 个文件\n\n"
# 添加贡献者信息
release_notes+="### 👥 贡献者\n"
release_notes+="感谢这些优秀的贡献者(按提交次数排序):\n"
release_notes+="${contributor_notes}\n"
release_notes+="---\n"
# 写入文件
echo -e "$release_notes" > changelog.md
echo "生成日志内容:"
cat changelog.md
- name: Create Release
uses: ncipollo/release-action@v1
with:
generateReleaseNotes: false
bodyFile: changelog.md
tag: ${{ github.ref_name }}

44
.gitignore vendored Normal file
View File

@ -0,0 +1,44 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
*.local
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.hbuilderx
.stylelintcache
.eslintcache
docs/.vitepress/dist
docs/.vitepress/cache
src/types
# lock 文件还是不要了,我主要的版本写死就好了
# pnpm-lock.yaml
# package-lock.json
# TIPS:如果某些文件已经加入了版本管理,现在重新加入 .gitignore 是不生效的,需要执行下面的操作
# `git rm -r --cached .` 然后提交 commit 即可。
# git rm -r --cached file1 file2 ## 针对某些文件
# git rm -r --cached dir1 dir2 ## 针对某些文件夹
# git rm -r --cached . ## 针对所有文件
# 更新 uni-app 官方版本
# npx @dcloudio/uvm@latest

1
.husky/commit-msg Normal file
View File

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

1
.husky/pre-commit Normal file
View File

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

8
.npmrc Normal file
View File

@ -0,0 +1,8 @@
# registry = https://registry.npmjs.org
registry = https://registry.npmmirror.com
strict-peer-dependencies=false
auto-install-peers=true
shamefully-hoist=true
ignore-workspace-root-check=true
install-workspace-root=true

20
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,20 @@
{
"recommendations": [
"vue.volar",
"stylelint.vscode-stylelint",
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"antfu.unocss",
"antfu.iconify",
"evils.uniapp-vscode",
"uni-helper.uni-helper-vscode",
"uni-helper.uni-app-schemas-vscode",
"uni-helper.uni-highlight-vscode",
"uni-helper.uni-ui-snippets-vscode",
"uni-helper.uni-app-snippets-vscode",
"mrmlnc.vscode-json5",
"streetsidesoftware.code-spell-checker",
"foxundermoon.shell-format",
"christian-kohler.path-intellisense"
]
}

93
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,93 @@
{
// 配置语言的文件关联
"files.associations": {
"pages.json": "jsonc", // pages.json 可以写注释
"manifest.json": "jsonc" // manifest.json 可以写注释
},
"stylelint.enable": false, // 禁用 stylelint
"css.validate": false, // 禁用 CSS 内置验证
"scss.validate": false, // 禁用 SCSS 内置验证
"less.validate": false, // 禁用 LESS 内置验证
"typescript.tsdk": "node_modules\\typescript\\lib",
"explorer.fileNesting.enabled": false,
"explorer.fileNesting.expand": false,
"explorer.fileNesting.patterns": {
"README.md": "index.html,favicon.ico,robots.txt,CHANGELOG.md",
"pages.config.ts": "manifest.config.ts,openapi-ts-request.config.ts",
"package.json": "tsconfig.json,pnpm-lock.yaml,pnpm-workspace.yaml,LICENSE,.gitattributes,.gitignore,.gitpod.yml,CNAME,.npmrc,.browserslistrc",
"eslint.config.mjs": ".commitlintrc.*,.prettier*,.editorconfig,.commitlint.cjs,.eslint*"
},
// Disable the default formatter, use eslint instead
"prettier.enable": false,
"editor.formatOnSave": false,
// Auto fix
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
// Silent the stylistic rules in you IDE, but still auto fix them
"eslint.rules.customizations": [
// { "rule": "style/*", "severity": "off", "fixable": true },
// { "rule": "format/*", "severity": "off", "fixable": true },
// { "rule": "*-indent", "severity": "off", "fixable": true },
// { "rule": "*-spacing", "severity": "off", "fixable": true },
// { "rule": "*-spaces", "severity": "off", "fixable": true },
// { "rule": "*-order", "severity": "off", "fixable": true },
// { "rule": "*-dangle", "severity": "off", "fixable": true },
// { "rule": "*-newline", "severity": "off", "fixable": true },
// { "rule": "*quotes", "severity": "off", "fixable": true },
// { "rule": "*semi", "severity": "off", "fixable": true }
],
// Enable eslint for all supported languages
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"json5",
"jsonc",
"yaml",
"toml",
"xml",
"gql",
"graphql",
"astro",
"svelte",
"css",
"less",
"scss",
"pcss",
"postcss"
],
"cSpell.words": [
"alova",
"Aplipay",
"climblee",
"commitlint",
"dcloudio",
"iconfont",
"oxlint",
"qrcode",
"refresherrefresh",
"scrolltolower",
"tabbar",
"Toutiao",
"uniapp",
"unibest",
"uview",
"uvui",
"Wechat",
"WechatMiniprogram",
"Weixin"
]
}

68
.vscode/vue3.code-snippets vendored Normal file
View File

@ -0,0 +1,68 @@
{
// Place your unibest 工作区 snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
// description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
// is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
// used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
// $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
// Placeholders with the same ids are connected.
// Example:
// "Print to console": {
// "scope": "javascript,typescript",
// "prefix": "log",
// "body": [
// "console.log('$1');",
// "$2"
// ],
// "description": "Log output to console"
// }
"Print unibest Vue3 SFC": {
"scope": "vue",
"prefix": "v3",
"body": [
"<route lang=\"json5\" type=\"page\">",
"{",
" layout: 'default',",
" style: {",
" navigationBarTitleText: '$1',",
" },",
"}",
"</route>\n",
"<script lang=\"ts\" setup>",
"//$3",
"</script>\n",
"<template>",
" <view class=\"\">$2</view>",
"</template>\n",
"<style lang=\"scss\" scoped>",
"//$4",
"</style>\n",
],
},
"Print unibest style": {
"scope": "vue",
"prefix": "st",
"body": [
"<style lang=\"scss\" scoped>",
"//",
"</style>\n"
],
},
"Print unibest script": {
"scope": "vue",
"prefix": "sc",
"body": [
"<script lang=\"ts\" setup>",
"//$3",
"</script>\n"
],
},
"Print unibest template": {
"scope": "vue",
"prefix": "te",
"body": [
"<template>",
" <view class=\"\">$1</view>",
"</template>\n"
],
},
}

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 菲鸽
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

2
README.md Normal file
View File

@ -0,0 +1,2 @@
## 康养小程序端(外壳)

31
env/.env vendored Normal file
View File

@ -0,0 +1,31 @@
VITE_APP_TITLE = '康乐云家'
VITE_APP_PORT = 9000
VITE_UNI_APPID = '__UNI__D1E5001'
VITE_WX_APPID = 'wxe173e798fc5a9f02'
# h5部署网站的base,配置到 manifest.config.ts 里的 h5.router.base
VITE_APP_PUBLIC_BASE=/
# 登录页面
VITE_LOGIN_URL = '/pages/login/index'
# 第一个请求地址
VITE_SERVER_BASEURL = 'https://ukw0y1.laf.run'
# 第二个请求地址
VITE_API_SECONDARY_URL = 'https://ukw0y1.laf.run'
VITE_UPLOAD_BASEURL = 'https://ukw0y1.laf.run/upload'
# 有些同学可能需要在微信小程序里面根据 develop、trial、release 分别设置上传地址,参考代码如下。
# 下面的变量如果没有设置,会默认使用 VITE_SERVER_BASEURL or VITE_UPLOAD_BASEURL
VITE_SERVER_BASEURL__WEIXIN_DEVELOP = 'https://ukw0y1.laf.run'
VITE_SERVER_BASEURL__WEIXIN_TRIAL = 'https://ukw0y1.laf.run'
VITE_SERVER_BASEURL__WEIXIN_RELEASE = 'https://ukw0y1.laf.run'
VITE_UPLOAD_BASEURL__WEIXIN_DEVELOP = 'https://ukw0y1.laf.run/upload'
VITE_UPLOAD_BASEURL__WEIXIN_TRIAL = 'https://ukw0y1.laf.run/upload'
VITE_UPLOAD_BASEURL__WEIXIN_RELEASE = 'https://ukw0y1.laf.run/upload'
# h5是否需要配置代理
VITE_APP_PROXY=false
VITE_APP_PROXY_PREFIX = '/api'

6
env/.env.development vendored Normal file
View File

@ -0,0 +1,6 @@
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'development'
# 是否去除console 和 debugger
VITE_DELETE_CONSOLE = false
# 是否开启sourcemap
VITE_SHOW_SOURCEMAP = true

6
env/.env.production vendored Normal file
View File

@ -0,0 +1,6 @@
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'development'
# 是否去除console 和 debugger
VITE_DELETE_CONSOLE = true
# 是否开启sourcemap
VITE_SHOW_SOURCEMAP = false

4
env/.env.test vendored Normal file
View File

@ -0,0 +1,4 @@
# 变量必须以 VITE_ 为前缀才能暴露给外部读取
NODE_ENV = 'development'
# 是否去除console 和 debugger
VITE_DELETE_CONSOLE = false

43
eslint.config.mjs Normal file
View File

@ -0,0 +1,43 @@
import uniHelper from '@uni-helper/eslint-config'
export default uniHelper({
unocss: true,
vue: true,
markdown: false,
ignores: [
'src/uni_modules/',
'dist',
// unplugin-auto-import 生成的类型文件,每次提交都改变,所以加入这里吧,与 .gitignore 配合使用
'auto-import.d.ts',
// vite-plugin-uni-pages 生成的类型文件,每次切换分支都一堆不同的,所以直接 .gitignore
'uni-pages.d.ts',
// 插件生成的文件
'src/pages.json',
'src/manifest.json',
// 忽略自动生成文件
'src/service/app/**',
],
rules: {
'no-console': 'off',
'no-unused-vars': 'off',
'vue/no-unused-refs': 'off',
'unused-imports/no-unused-vars': 'off',
'eslint-comments/no-unlimited-disable': 'off',
'jsdoc/check-param-names': 'off',
'jsdoc/require-returns-description': 'off',
'ts/no-empty-object-type': 'off',
'no-extend-native': 'off',
},
formatters: {
/**
* Format CSS, LESS, SCSS files, also the `<style>` blocks in Vue
* By default uses Prettier
*/
css: true,
/**
* Format HTML files
* By default uses Prettier
*/
html: true,
},
})

BIN
favicon.ico Normal file

Binary file not shown.

26
index.html Normal file
View File

@ -0,0 +1,26 @@
<!doctype html>
<html build-time="%BUILD_TIME%">
<head>
<meta charset="UTF-8" />
<link rel="shortcut icon" href="favicon.ico" type="image/x-icon" />
<script>
var coverSupport =
'CSS' in window &&
typeof CSS.supports === 'function' &&
(CSS.supports('top: env(a)') || CSS.supports('top: constant(a)'))
document.write(
'<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
(coverSupport ? ', viewport-fit=cover' : '') +
'" />',
)
</script>
<title>康乐云家</title>
<!--preload-links-->
<!--app-context-->
</head>
<body>
<div id="app"><!--app-html--></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

129
manifest.config.ts Normal file
View File

@ -0,0 +1,129 @@
import path from 'node:path'
import process from 'node:process'
// manifest.config.ts
import { defineManifestConfig } from '@uni-helper/vite-plugin-uni-manifest'
import { loadEnv } from 'vite'
// 手动解析命令行参数获取 mode
function getMode() {
const args = process.argv.slice(2)
const modeFlagIndex = args.findIndex(arg => arg === '--mode')
return modeFlagIndex !== -1 ? args[modeFlagIndex + 1] : args[0] === 'build' ? 'production' : 'development' // 默认 development
}
// 获取环境变量的范例
const env = loadEnv(getMode(), path.resolve(process.cwd(), 'env'))
const {
VITE_APP_TITLE,
VITE_UNI_APPID,
VITE_WX_APPID,
VITE_APP_PUBLIC_BASE,
VITE_FALLBACK_LOCALE,
} = 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': {
router: {
// base: VITE_APP_PUBLIC_BASE,
},
},
/* 5+App特有相关 */
'app-plus': {
usingComponents: true,
nvueStyleCompiler: 'uni-app',
compilerVersion: 3,
compatible: {
ignoreVersion: true,
},
splashscreen: {
alwaysShowBeforeRender: true,
waiting: true,
autoclose: true,
delay: 0,
},
/* 模块配置 */
modules: {},
/* 应用发布信息 */
distribute: {
/* android打包配置 */
android: {
minSdkVersion: 30,
targetSdkVersion: 30,
abiFilters: ['armeabi-v7a', 'arm64-v8a'],
permissions: [
'<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/>',
'<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>',
'<uses-permission android:name="android.permission.VIBRATE"/>',
'<uses-permission android:name="android.permission.READ_LOGS"/>',
'<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>',
'<uses-feature android:name="android.hardware.camera.autofocus"/>',
'<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>',
'<uses-permission android:name="android.permission.CAMERA"/>',
'<uses-permission android:name="android.permission.GET_ACCOUNTS"/>',
'<uses-permission android:name="android.permission.READ_PHONE_STATE"/>',
'<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>',
'<uses-permission android:name="android.permission.WAKE_LOCK"/>',
'<uses-permission android:name="android.permission.FLASHLIGHT"/>',
'<uses-feature android:name="android.hardware.camera"/>',
'<uses-permission android:name="android.permission.WRITE_SETTINGS"/>',
],
},
/* ios打包配置 */
ios: {},
/* SDK配置 */
sdkConfigs: {},
/* 图标配置 */
icons: {
android: {
},
ios: {
},
},
},
},
/* 快应用特有相关 */
'quickapp': {},
/* 小程序特有相关 */
'mp-weixin': {
appid: VITE_WX_APPID,
setting: {
urlCheck: false,
// 是否启用 ES6 转 ES5
es6: true,
minified: true,
},
optimization: {
subPackages: true,
},
usingComponents: true,
permission: {
'scope.userLocation': {
desc: '你的位置信息将用于小程序位置接口的效果展示',
},
},
requiredPrivateInfos: ['getLocation'],
// __usePrivacyCheck__: true,
},
'mp-alipay': {
usingComponents: true,
styleIsolation: 'shared',
},
'mp-baidu': {
usingComponents: true,
},
'mp-toutiao': {
usingComponents: true,
},
'uniStatistics': {
enable: false,
},
'vueVersion': '3',
})

View File

@ -0,0 +1,13 @@
import type { GenerateServiceProps } from 'openapi-ts-request'
export default [
{
schemaPath: 'http://petstore.swagger.io/v2/swagger.json',
serversPath: './src/service/app',
requestLibPath: `import request from '@/utils/request';\n import { CustomRequestOptions } from '@/interceptors/request';`,
requestOptionsType: 'CustomRequestOptions',
isGenReactQuery: true,
reactQueryMode: 'vue',
isGenJavaScript: false,
},
] as GenerateServiceProps[]

145
package.json Normal file
View File

@ -0,0 +1,145 @@
{
"name": "pension-wx",
"type": "commonjs",
"version": "3.2.0",
"packageManager": "pnpm@10.10.0",
"update-time": "2025-06-21",
"engines": {
"node": ">=18",
"pnpm": ">=7.30"
},
"scripts": {
"preinstall": "npx only-allow pnpm",
"uvm": "npx @dcloudio/uvm@latest",
"uvm-rm": "node ./scripts/postupgrade.js",
"postuvm": "echo upgrade uni-app success!",
"dev:app": "uni -p app",
"dev:app-android": "uni -p app-android",
"dev:app-ios": "uni -p app-ios",
"dev:custom": "uni -p",
"dev": "uni",
"dev:h5": "uni",
"dev:h5:ssr": "uni --ssr",
"dev:mp": "uni -p mp-weixin",
"dev:mp-alipay": "uni -p mp-alipay",
"dev:mp-baidu": "uni -p mp-baidu",
"dev:mp-jd": "uni -p mp-jd",
"dev:mp-kuaishou": "uni -p mp-kuaishou",
"dev:mp-lark": "uni -p mp-lark",
"dev:mp-qq": "uni -p mp-qq",
"dev:mp-toutiao": "uni -p mp-toutiao",
"dev:mp-weixin": "uni -p mp-weixin",
"dev:mp-xhs": "uni -p mp-xhs",
"dev:quickapp-webview": "uni -p quickapp-webview",
"dev:quickapp-webview-huawei": "uni -p quickapp-webview-huawei",
"dev:quickapp-webview-union": "uni -p quickapp-webview-union",
"build:app": "uni build -p app",
"build:app-android": "uni build -p app-android",
"build:app-ios": "uni build -p app-ios",
"build:custom": "uni build -p",
"build:h5": "uni build",
"build": "uni build",
"build:h5:ssr": "uni build --ssr",
"build:mp-alipay": "uni build -p mp-alipay",
"build:mp": "uni build -p mp-weixin",
"build:mp-baidu": "uni build -p mp-baidu",
"build:mp-jd": "uni build -p mp-jd",
"build:mp-kuaishou": "uni build -p mp-kuaishou",
"build:mp-lark": "uni build -p mp-lark",
"build:mp-qq": "uni build -p mp-qq",
"build:mp-toutiao": "uni build -p mp-toutiao",
"build:mp-weixin": "uni build -p mp-weixin",
"build:mp-xhs": "uni build -p mp-xhs",
"build:quickapp-webview": "uni build -p quickapp-webview",
"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"
},
"dependencies": {
"@alova/adapter-uniapp": "^2.0.14",
"@alova/shared": "^1.3.1",
"@dcloudio/uni-app": "3.0.0-4060620250520001",
"@dcloudio/uni-app-harmony": "3.0.0-4060620250520001",
"@dcloudio/uni-app-plus": "3.0.0-4060620250520001",
"@dcloudio/uni-components": "3.0.0-4060620250520001",
"@dcloudio/uni-h5": "3.0.0-4060620250520001",
"@dcloudio/uni-mp-alipay": "3.0.0-4060620250520001",
"@dcloudio/uni-mp-baidu": "3.0.0-4060620250520001",
"@dcloudio/uni-mp-harmony": "3.0.0-4060620250520001",
"@dcloudio/uni-mp-jd": "3.0.0-4060620250520001",
"@dcloudio/uni-mp-kuaishou": "3.0.0-4060620250520001",
"@dcloudio/uni-mp-lark": "3.0.0-4060620250520001",
"@dcloudio/uni-mp-qq": "3.0.0-4060620250520001",
"@dcloudio/uni-mp-toutiao": "3.0.0-4060620250520001",
"@dcloudio/uni-mp-weixin": "3.0.0-4060620250520001",
"@dcloudio/uni-mp-xhs": "3.0.0-4060620250520001",
"@dcloudio/uni-quickapp-webview": "3.0.0-4060620250520001",
"@tanstack/vue-query": "^5.62.16",
"abortcontroller-polyfill": "^1.7.8",
"alova": "^3.3.3",
"dayjs": "1.11.10",
"js-cookie": "^3.0.5",
"pinia": "2.0.36",
"pinia-plugin-persistedstate": "3.2.1",
"vue": "^3.4.21",
"wot-design-uni": "^1.9.1",
"z-paging": "2.8.7"
},
"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",
"@dcloudio/uni-stacktracey": "3.0.0-4060620250520001",
"@dcloudio/vite-plugin-uni": "3.0.0-4060620250520001",
"@esbuild/darwin-arm64": "0.20.2",
"@esbuild/darwin-x64": "0.20.2",
"@iconify-json/carbon": "^1.2.4",
"@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-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",
"eslint-plugin-format": "^1.0.1",
"husky": "^9.1.7",
"lint-staged": "^15.2.10",
"openapi-ts-request": "^1.1.2",
"postcss": "^8.4.49",
"postcss-html": "^1.7.0",
"postcss-scss": "^4.0.9",
"rollup-plugin-visualizer": "^5.12.0",
"sass": "1.77.8",
"terser": "^5.36.0",
"typescript": "^5.7.2",
"unocss": "65.4.2",
"unplugin-auto-import": "^0.17.8",
"vite": "5.2.8",
"vite-plugin-restart": "^0.4.2",
"vue-tsc": "^2.2.10"
},
"resolutions": {
"bin-wrapper": "npm:bin-wrapper-china"
},
"lint-staged": {
"*": "eslint --fix"
}
}

22
pages.config.ts Normal file
View File

@ -0,0 +1,22 @@
import { defineUniPages } from '@uni-helper/vite-plugin-uni-pages'
import { tabBar } from './src/layouts/fg-tabbar/tabbarList'
export default defineUniPages({
globalStyle: {
navigationBarTitleText: '康乐云家',
navigationBarBackgroundColor: '#ffffff',
navigationBarTextStyle: 'black',
backgroundColor: '#FFFFFF',
},
easycom: {
autoscan: true,
custom: {
'^fg-(.*)': '@/components/fg-$1/fg-$1.vue',
'^wd-(.*)': 'wot-design-uni/components/wd-$1/wd-$1.vue',
'^(?!z-paging-refresh|z-paging-load-more)z-paging(.*)':
'z-paging/components/z-paging$1/z-paging$1.vue',
},
},
// tabbar 的配置统一在 “./src/layouts/fg-tabbar/tabbarList.ts” 文件中
tabBar: tabBar as any,
})

View File

@ -0,0 +1,13 @@
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;
}

14363
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

6
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,6 @@
packages:
- '**'
- '!node_modules'
patchedDependencies:
'@dcloudio/uni-h5': patches/@dcloudio__uni-h5.patch

35
scripts/postupgrade.js Normal file
View File

@ -0,0 +1,35 @@
// # 执行 `pnpm upgrade` 后会升级 `uniapp` 相关依赖
// # 在升级完后,会自动添加很多无用依赖,这需要删除以减小依赖包体积
// # 只需要执行下面的命令即可
const { exec } = require('node:child_process')
// 定义要执行的命令
const dependencies = [
'@dcloudio/uni-app-harmony',
// TODO: 如果需要某个平台的小程序,请手动删除或注释掉
'@dcloudio/uni-mp-alipay',
'@dcloudio/uni-mp-baidu',
'@dcloudio/uni-mp-jd',
'@dcloudio/uni-mp-kuaishou',
'@dcloudio/uni-mp-lark',
'@dcloudio/uni-mp-qq',
'@dcloudio/uni-mp-toutiao',
'@dcloudio/uni-mp-xhs',
'@dcloudio/uni-quickapp-webview',
// i18n模板要注释掉下面的
'vue-i18n',
]
// 使用exec执行命令
exec(`pnpm un ${dependencies.join(' ')}`, (error, stdout, stderr) => {
if (error) {
// 如果有错误,打印错误信息
console.error(`执行出错: ${error}`)
return
}
// 打印正常输出
console.log(`stdout: ${stdout}`)
// 如果有错误输出,也打印出来
console.error(`stderr: ${stderr}`)
})

36
src/App.vue Normal file
View File

@ -0,0 +1,36 @@
<script setup lang="ts">
import { onHide, onLaunch, onShow } from '@dcloudio/uni-app'
import { usePageAuth } from '@/hooks/usePageAuth'
import 'abortcontroller-polyfill/dist/abortcontroller-polyfill-only'
usePageAuth()
onLaunch(() => {
console.log('App Launch')
})
onShow(() => {
console.log('App Show')
})
onHide(() => {
console.log('App Hide')
})
</script>
<style lang="scss">
button::after {
border: none;
}
swiper,
scroll-view {
flex: 1;
height: 100%;
overflow: hidden;
}
image {
width: 100%;
height: 100%;
vertical-align: middle;
}
</style>

17
src/api/alova-foo.ts Normal file
View File

@ -0,0 +1,17 @@
import { API_DOMAINS, http } from '@/utils/request/alova'
export interface IFoo {
id: number
name: string
}
export function foo() {
return http.Get<IFoo>('/foo', {
params: {
name: '菲鸽',
page: 1,
pageSize: 10,
},
meta: { domain: API_DOMAINS.SECONDARY }, // 用于切换请求地址
})
}

83
src/api/login.ts Normal file
View File

@ -0,0 +1,83 @@
import type { ICaptcha, IUpdateInfo, IUpdatePassword, IUserInfoVo, IUserLogin } from './types/login'
import { http } from '@/utils/http'
/**
* 登录表单
*/
export interface ILoginForm {
username: string
password: string
code: string
uuid: string
}
/**
* 获取验证码
* @returns ICaptcha 验证码
*/
export function getCode() {
return http.get<ICaptcha>('/user/getCode')
}
/**
* 用户登录
* @param loginForm 登录表单
*/
export function login(loginForm: ILoginForm) {
return http.post<IUserLogin>('/user/login', loginForm)
}
/**
* 获取用户信息
*/
export function getUserInfo() {
return http.get<IUserInfoVo>('/user/info')
}
/**
* 退出登录
*/
export function logout() {
return http.get<void>('/user/logout')
}
/**
* 修改用户信息
*/
export function updateInfo(data: IUpdateInfo) {
return http.post('/user/updateInfo', data)
}
/**
* 修改用户密码
*/
export function updateUserPassword(data: IUpdatePassword) {
return http.post('/user/updatePassword', data)
}
/**
* 获取微信登录凭证
* @returns Promise 包含微信登录凭证(code)
*/
export function getWxCode() {
return new Promise<UniApp.LoginRes>((resolve, reject) => {
uni.login({
provider: 'weixin',
success: res => resolve(res),
fail: err => reject(new Error(err)),
})
})
}
/**
* 微信登录参数
*/
/**
* 微信登录
* @param params 微信登录参数,包含code
* @returns Promise 包含登录结果
*/
export function wxLogin(data: { code: string }) {
return http.post<IUserLogin>('/user/wxLogin', data)
}

58
src/api/types/login.ts Normal file
View File

@ -0,0 +1,58 @@
/**
* 用户信息
*/
export interface IUserInfoVo {
id: number
username: string
avatar: string
token: string
openId: string
}
/**
* 登录返回的信息
*/
export interface IUserLogin {
id: string
username: string
token: string
}
/**
* 获取验证码
*/
export interface ICaptcha {
captchaEnabled: boolean
uuid: string
image: string
}
/**
* 上传成功的信息
*/
export interface IUploadSuccessInfo {
fileId: number
originalName: string
fileName: string
storagePath: string
fileHash: string
fileType: string
fileBusinessType: string
fileSize: number
}
/**
* 更新用户信息
*/
export interface IUpdateInfo {
id: number
name: string
sex: string
}
/**
* 更新用户信息
*/
export interface IUpdatePassword {
id: number
oldPassword: string
newPassword: string
confirmPassword: string
}

0
src/components/.gitkeep Normal file
View File

34
src/env.d.ts vendored Normal file
View File

@ -0,0 +1,34 @@
/// <reference types="vite/client" />
/// <reference types="vite-svg-loader" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
interface ImportMetaEnv {
/** 网站标题,应用名称 */
readonly VITE_APP_TITLE: string
/** 服务端口号 */
readonly VITE_SERVER_PORT: string
/** 后台接口地址 */
readonly VITE_SERVER_BASEURL: string
/** H5是否需要代理 */
readonly VITE_APP_PROXY: 'true' | 'false'
/** H5是否需要代理,需要的话有个前缀 */
readonly VITE_APP_PROXY_PREFIX: string // 一般是/api
/** 上传图片地址 */
readonly VITE_UPLOAD_BASEURL: string
/** 是否清除console */
readonly VITE_DELETE_CONSOLE: string
// 更多环境变量...
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
declare const __VITE_APP_PROXY__: 'true' | 'false'
declare const __UNI_PLATFORM__: 'app' | 'h5' | 'mp-alipay' | 'mp-baidu' | 'mp-kuaishou' | 'mp-lark' | 'mp-qq' | 'mp-tiktok' | 'mp-weixin' | 'mp-xiaochengxu'

0
src/hooks/.gitkeep Normal file
View File

50
src/hooks/usePageAuth.ts Normal file
View File

@ -0,0 +1,50 @@
import { onLoad } from '@dcloudio/uni-app'
import { useUserStore } from '@/store'
import { needLoginPages as _needLoginPages, getNeedLoginPages } from '@/utils'
const loginRoute = import.meta.env.VITE_LOGIN_URL
const isDev = import.meta.env.DEV
function isLogined() {
const userStore = useUserStore()
return !!userStore.userInfo.username
}
// 检查当前页面是否需要登录
export function usePageAuth() {
onLoad((options) => {
// 获取当前页面路径
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
const currentPath = `/${currentPage.route}`
// 获取需要登录的页面列表
let needLoginPages: string[] = []
if (isDev) {
needLoginPages = getNeedLoginPages()
}
else {
needLoginPages = _needLoginPages
}
// 检查当前页面是否需要登录
const isNeedLogin = needLoginPages.includes(currentPath)
if (!isNeedLogin) {
return
}
const hasLogin = isLogined()
if (hasLogin) {
return true
}
// 构建重定向URL
const queryString = Object.entries(options || {})
.map(([key, value]) => `${key}=${encodeURIComponent(String(value))}`)
.join('&')
const currentFullPath = queryString ? `${currentPath}?${queryString}` : currentPath
const redirectRoute = `${loginRoute}?redirect=${encodeURIComponent(currentFullPath)}`
// 重定向到登录页
uni.redirectTo({ url: redirectRoute })
})
}

51
src/hooks/useRequest.ts Normal file
View File

@ -0,0 +1,51 @@
import type { Ref } from 'vue'
interface IUseRequestOptions<T> {
/** 是否立即执行 */
immediate?: boolean
/** 初始化数据 */
initialData?: T
}
interface IUseRequestReturn<T> {
loading: Ref<boolean>
error: Ref<boolean | Error>
data: Ref<T | undefined>
run: () => Promise<T | undefined>
}
/**
* useRequest是一个定制化的请求钩子,用于处理异步请求和响应。
* @param func 一个执行异步请求的函数,返回一个包含响应数据的Promise。
* @param options 包含请求选项的对象 {immediate, initialData}。
* @param options.immediate 是否立即执行请求,默认为false。
* @param options.initialData 初始化数据,默认为undefined。
* @returns 返回一个对象{loading, error, data, run},包含请求的加载状态、错误信息、响应数据和手动触发请求的函数。
*/
export default function useRequest<T>(
func: () => Promise<IResData<T>>,
options: IUseRequestOptions<T> = { immediate: false },
): IUseRequestReturn<T> {
const loading = ref(false)
const error = ref(false)
const data = ref<T | undefined>(options.initialData) as Ref<T | undefined>
const run = async () => {
loading.value = true
return func()
.then((res) => {
data.value = res.data
error.value = false
return data.value
})
.catch((err) => {
error.value = err
throw err
})
.finally(() => {
loading.value = false
})
}
options.immediate && run()
return { loading, error, data, run }
}

160
src/hooks/useUpload.ts Normal file
View File

@ -0,0 +1,160 @@
import { ref } from 'vue'
import { getEnvBaseUploadUrl } from '@/utils'
const VITE_UPLOAD_BASEURL = `${getEnvBaseUploadUrl()}`
type TfileType = 'image' | 'file'
type TImage = 'png' | 'jpg' | 'jpeg' | 'webp' | '*'
type TFile = 'doc' | 'docx' | 'ppt' | 'zip' | 'xls' | 'xlsx' | 'txt' | TImage
interface TOptions<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
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 run = () => {
// 微信小程序从基础库 2.21.0 开始, wx.chooseImage 停止维护,请使用 uni.chooseMedia 代替。
// 微信小程序在2023年10月17日之后,使用本API需要配置隐私协议
const chooseFileOptions = {
count: 1,
success: (res: any) => {
console.log('File selected successfully:', res)
// 小程序中res:{errMsg: "chooseImage:ok", tempFiles: [{fileType: "image", size: 48976, tempFilePath: "http://tmp/5iG1WpIxTaJf3ece38692a337dc06df7eb69ecb49c6b.jpeg"}]}
// h5中res:{errMsg: "chooseImage:ok", tempFilePaths: "blob:http://localhost:9000/f74ab6b8-a14d-4cb6-a10d-fcf4511a0de5", tempFiles: [File]}
// h5的File有以下字段:{name: "girl.jpeg", size: 48976, type: "image/jpeg"}
// App中res:{errMsg: "chooseImage:ok", tempFilePaths: "file:///Users/feige/xxx/gallery/1522437259-compressed-IMG_0006.jpg", tempFiles: [File]}
// App的File有以下字段:{path: "file:///Users/feige/xxx/gallery/1522437259-compressed-IMG_0006.jpg", size: 48976}
let tempFilePath = ''
let size = 0
// #ifdef MP-WEIXIN
tempFilePath = res.tempFiles[0].tempFilePath
size = res.tempFiles[0].size
// #endif
// #ifndef MP-WEIXIN
tempFilePath = res.tempFilePaths[0]
size = res.tempFiles[0].size
// #endif
handleFileChoose({ tempFilePath, size })
},
fail: (err: any) => {
console.error('File selection failed:', err)
error.value = err
onError?.(err)
},
}
if (fileType === 'image') {
// #ifdef MP-WEIXIN
uni.chooseMedia({
...chooseFileOptions,
mediaType: ['image'],
})
// #endif
// #ifndef MP-WEIXIN
uni.chooseImage(chooseFileOptions)
// #endif
}
else {
uni.chooseFile({
...chooseFileOptions,
type: 'all',
})
}
}
return { loading, error, data, run }
}
async function uploadFile({
tempFilePath,
formData,
onSuccess,
onError,
onComplete,
}: {
tempFilePath: string
formData: Record<string, any>
onSuccess: (data: any) => void
onError: (err: any) => void
onComplete: () => void
}) {
uni.uploadFile({
url: VITE_UPLOAD_BASEURL,
filePath: tempFilePath,
name: 'file',
formData,
success: (uploadFileRes) => {
try {
const data = uploadFileRes.data
onSuccess(data)
}
catch (err) {
onError(err)
}
},
fail: (err) => {
console.error('Upload failed:', err)
onError(err)
},
complete: onComplete,
})
}

View File

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

View File

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

View File

@ -0,0 +1,70 @@
import { useUserStore } from '@/store'
import { getEnvBaseUrl } from '@/utils'
import { platform } from '@/utils/platform'
import { stringifyQuery } from '@/utils/queryString'
export type CustomRequestOptions = UniApp.RequestOptions & {
query?: Record<string, any>
/** 出错时是否隐藏错误提示 */
hideErrorToast?: boolean
} & IUniUploadFileOptions // 添加uni.uploadFile参数类型
// 请求基准地址
const baseUrl = getEnvBaseUrl()
// 拦截器配置
const httpInterceptor = {
// 拦截前触发
invoke(options: CustomRequestOptions) {
// 接口请求支持通过 query 参数配置 queryString
if (options.query) {
const queryStr = stringifyQuery(options.query)
if (options.url.includes('?')) {
options.url += `&${queryStr}`
}
else {
options.url += `?${queryStr}`
}
}
// 非 http 开头需拼接地址
if (!options.url.startsWith('http')) {
// #ifdef H5
// console.log(__VITE_APP_PROXY__)
if (JSON.parse(__VITE_APP_PROXY__)) {
// 自动拼接代理前缀
options.url = import.meta.env.VITE_APP_PROXY_PREFIX + options.url
}
else {
options.url = baseUrl + options.url
}
// #endif
// 非H5正常拼接
// #ifndef H5
options.url = baseUrl + options.url
// #endif
// TIPS: 如果需要对接多个后端服务,也可以在这里处理,拼接成所需要的地址
}
// 1. 请求超时
options.timeout = 10000 // 10s
// 2. (可选)添加小程序端请求头标识
options.header = {
platform, // 可选,与 uniapp 定义的平台一致,告诉后台来源
...options.header,
}
// 3. 添加 token 请求头标识
const userStore = useUserStore()
const { token } = userStore.userInfo as unknown as IUserInfo
if (token) {
options.header.Authorization = `Bearer ${token}`
}
},
}
export const requestInterceptor = {
install() {
// 拦截 request 请求
uni.addInterceptor('request', httpInterceptor)
// 拦截 uploadFile 文件上传
uni.addInterceptor('uploadFile', httpInterceptor)
},
}

65
src/interceptors/route.ts Normal file
View File

@ -0,0 +1,65 @@
/**
* by 菲鸽 on 2024-03-06
* 路由拦截,通常也是登录拦截
* 可以设置路由白名单,或者黑名单,看业务需要选哪一个
* 我这里应为大部分都可以随便进入,所以使用黑名单
*/
import { useUserStore } from '@/store'
import { needLoginPages as _needLoginPages, getLastPage, getNeedLoginPages } from '@/utils'
// TODO Check
const loginRoute = import.meta.env.VITE_LOGIN_URL
function isLogined() {
const userStore = useUserStore()
return !!userStore.userInfo.username
}
const isDev = import.meta.env.DEV
// 黑名单登录拦截器 - (适用于大部分页面不需要登录,少部分页面需要登录)
const navigateToInterceptor = {
// 注意,这里的url是 '/' 开头的,如 '/pages/index/index',跟 'pages.json' 里面的 path 不同
// 增加对相对路径的处理,BY 网友 @ideal
invoke({ url }: { url: string }) {
// console.log(url) // /pages/route-interceptor/index?name=feige&age=30
let path = url.split('?')[0]
// 处理相对路径
if (!path.startsWith('/')) {
const currentPath = getLastPage().route
const normalizedCurrentPath = currentPath.startsWith('/') ? currentPath : `/${currentPath}`
const baseDir = normalizedCurrentPath.substring(0, normalizedCurrentPath.lastIndexOf('/'))
path = `${baseDir}/${path}`
}
let needLoginPages: string[] = []
// 为了防止开发时出现BUG,这里每次都获取一下。生产环境可以移到函数外,性能更好
if (isDev) {
needLoginPages = getNeedLoginPages()
}
else {
needLoginPages = _needLoginPages
}
const isNeedLogin = needLoginPages.includes(path)
if (!isNeedLogin) {
return true
}
const hasLogin = isLogined()
if (hasLogin) {
return true
}
const redirectRoute = `${loginRoute}?redirect=${encodeURIComponent(url)}`
uni.navigateTo({ url: redirectRoute })
return false
},
}
export const routeInterceptor = {
install() {
uni.addInterceptor('navigateTo', navigateToInterceptor)
uni.addInterceptor('reLaunch', navigateToInterceptor)
uni.addInterceptor('redirectTo', navigateToInterceptor)
uni.addInterceptor('switchTab', navigateToInterceptor)
},
}

9
src/layouts/default.vue Normal file
View File

@ -0,0 +1,9 @@
<template>
<view class="h-screen w-screen">
<slot />
</view>
</template>
<script lang="ts" setup>
</script>

View File

@ -0,0 +1,68 @@
<script setup lang="ts">
import { tabbarStore } from './tabbar'
// 'i-carbon-code',
import { tabbarList as _tabBarList, cacheTabbarEnable, selectedTabbarStrategy, TABBAR_MAP } from './tabbarList'
const customTabbarEnable
= selectedTabbarStrategy === TABBAR_MAP.CUSTOM_TABBAR_WITH_CACHE
|| selectedTabbarStrategy === TABBAR_MAP.CUSTOM_TABBAR_WITHOUT_CACHE
/** 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 的问题
const hideRedundantTabbarEnable = selectedTabbarStrategy === TABBAR_MAP.CUSTOM_TABBAR_WITH_CACHE
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
safe-area-inset-bottom
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

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

View File

@ -0,0 +1,11 @@
/**
* tabbar 状态,增加 storageSync 保证刷新浏览器时在正确的 tabbar 页面
* 使用reactive简单状态,而不是 pinia 全局状态
*/
export const tabbarStore = reactive({
curIdx: uni.getStorageSync('app-tabbar-index') || 0,
setCurIdx(idx: number) {
this.curIdx = idx
uni.setStorageSync('app-tabbar-index', idx)
},
})

View File

@ -0,0 +1,60 @@
/**
* tabbar 选择的策略,更详细的介绍见 tabbar.md 文件
* 0: 'NO_TABBAR' `无 tabbar`
* 1: 'NATIVE_TABBAR' `完全原生 tabbar`
* 2: 'CUSTOM_TABBAR_WITH_CACHE' `有缓存自定义 tabbar`
* 3: 'CUSTOM_TABBAR_WITHOUT_CACHE' `无缓存自定义 tabbar`
*
* 温馨提示:本文件的任何代码更改了之后,都需要重新运行,否则 pages.json 不会更新导致错误
*/
export const TABBAR_MAP = {
NO_TABBAR: 0,
NATIVE_TABBAR: 1,
CUSTOM_TABBAR_WITH_CACHE: 2,
CUSTOM_TABBAR_WITHOUT_CACHE: 3,
}
// TODO:通过这里切换使用tabbar的策略
export const selectedTabbarStrategy = TABBAR_MAP.NATIVE_TABBAR
// selectedTabbarStrategy==NATIVE_TABBAR(1) 时,需要填 iconPath 和 selectedIconPath
// selectedTabbarStrategy==CUSTOM_TABBAR(2,3) 时,需要填 icon 和 iconType
// selectedTabbarStrategy==NO_TABBAR(0) 时,tabbarList 不生效
export const tabbarList = [
{
iconPath: 'static/tabbar/home.png',
selectedIconPath: 'static/tabbar/homeHL.png',
pagePath: 'pages/index/index',
text: '',
icon: '',
// 选用 UI 框架自带的 icon时,iconType 为 uiLib
iconType: 'uiLib',
},
{
iconPath: 'static/tabbar/example.png',
selectedIconPath: 'static/tabbar/exampleHL.png',
pagePath: 'pages/about/about',
text: '',
icon: '',
// 注意 unocss 的图标需要在 页面上引入一下,或者配置到 unocss.config.ts 的 safelist 中
iconType: 'unocss',
},
]
// NATIVE_TABBAR(1) 和 CUSTOM_TABBAR_WITH_CACHE(2) 时,需要tabbar缓存
export const cacheTabbarEnable = selectedTabbarStrategy === TABBAR_MAP.NATIVE_TABBAR
|| selectedTabbarStrategy === TABBAR_MAP.CUSTOM_TABBAR_WITH_CACHE
const _tabbar = {
color: '#999999',
selectedColor: '#018d71',
backgroundColor: '#F8F8F8',
borderStyle: 'black',
height: '50px',
fontSize: '10px',
iconWidth: '24px',
spacing: '3px',
list: tabbarList,
}
// 0和1 需要显示底部的tabbar的各种配置,以利用缓存
export const tabBar = cacheTabbarEnable ? _tabbar : undefined

19
src/layouts/tabbar.vue Normal file
View File

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

@ -0,0 +1,197 @@
<template>
<Overlay :show="show" @update:show="handleClose">
<view
class="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 flex flex-col items-center bg-white p-[40rpx] rounded-[32rpx]"
@click.stop
>
<view class="w-[200rpx] h-[200rpx]">
<image
class="w-[200rpx] h-[200rpx]"
src="https://api.static.ycymedu.com/images/logo.png"
mode="aspectFit"
></image>
</view>
<view class="flex flex-col items-center">
<text class="text-[26rpx] mt-[20rpx] mb-[40rpx]" :selectable="false">
{{ phone ? '申请使用您的手机号' : '申请获取您的个人信息' }}
</text>
<button
class="w-[493rpx]! mb-[40rpx] h-[88rpx]! rounded-[44rpx] text-[32rpx] text-white flex items-center justify-center"
:class="checked.length > 0 ? 'bg-[#1580FF]' : 'bg-[#BFBFBF]'"
@click.stop="handleClick"
open-type="getPhoneNumber"
@getphonenumber="getPhoneNumber"
:disabled="checked.length === 0"
>
手机号快捷登录
</button>
<view class="flex items-center flex-nowrap">
<CheckboxGroup v-model="checked" class="check-class mr-[10rpx]">
<Checkbox name="1" cell shape="button" class="custom-checkbox"></Checkbox>
</CheckboxGroup>
<view class="flex items-center">
<text class="text-[24rpx] whitespace-nowrap">
已阅读并同意
<text class="text-[#1580FF]" @click.stop="handleClickUserAgreement">
<text>《用户协议》</text>
</text>
和
<text class="text-[#1580FF]" @click.stop="handleClickPrivacyPolicy">
<text>《隐私条款》</text>
</text>
</text>
</view>
</view>
</view>
</view>
</Overlay>
</template>
<script lang="ts" setup>
import { useLogin } from '@/login-sub/hooks/useUserInfo'
import Overlay from './Overlay.vue'
import Checkbox from './check-group/Checkbox.vue'
import CheckboxGroup from './check-group/CheckboxGroup.vue'
import {
getSessionKey,
getWxUserInfo,
setWxInfo,
} from '@/service/index/api'
import { useUserStore } from '@/store/user'
defineProps({
show: {
type: Boolean,
default: false,
},
})
defineOptions({
options: {
styleIsolation: 'shared',
},
})
const emits = defineEmits(['update:show', 'authReady'])
const userStore = useUserStore()
const handleClose = () => {
emits('update:show', false)
}
const phone = ref(true) // 手机号登陆
const checked = ref([]) // 是否同意条款
const getPhoneInfo = ref(null)
const handleClickUserAgreement = () => {
uni.navigateTo({
url: '/login-sub/userAgreement',
})
}
const handleClickPrivacyPolicy = () => {
uni.navigateTo({
url: '/login-sub/privacyPolicy',
})
}
const getPhoneNumber = async (e: any) => {
if (e.detail.errMsg == 'getPhoneNumber:ok') {
const detail = e.detail
let _getPhoneInfo = {
iv: detail.iv,
encryptedData: detail.encryptedData,
code: detail.code,
}
getPhoneInfo.value = _getPhoneInfo
await getUserInfo(detail.code)
} else if (e.detail.errMsg == 'getPhoneNumber:fail not login') {
uni.showToast({
title: '请先登录',
icon: 'none',
})
} else {
uni.showToast({
title: '获取手机号失败',
icon: 'none',
})
}
}
const handleClick = () => {
if (!checked.value) {
uni.showToast({
title: '您需先同意《服务条款》和《隐私条款》',
icon: 'none',
})
return
}
}
const getUserInfo = async (_code: string) => {
let userInfo = (await useLogin()) as { code: string; errMsg: string }
if (userInfo.errMsg == 'login:ok') {
const resp = await getSessionKey({ JsCode: userInfo.code })
if (resp.code == 200) {
const result = resp.result as { accessToken: string; openId: string }
userStore.setUserToken(result.accessToken)
userStore.setUserOpenId(result.openId)
setWxInfo({ code: _code, openId: result.openId })
// 根据微信的信息获取用户信息
getWxUserInfo().then((resp) => {
const infoData = resp.result as unknown as {
userExtend: { provinceCode: string }
zyBatches: any[]
batchDataUrl: string
batchName: string
avatar: string
nickName: string
}
if (resp.code === 200) {
// 根据用户信息中的城市设置对应城市的分数等信息
}
})
}
} else {
uni.showToast({
title: '您需先授权',
icon: 'none',
})
}
}
</script>
<style lang="scss" scoped>
:deep(.custom-checkbox) {
display: flex;
align-items: center;
justify-content: center;
.checkbox__icon {
border-radius: 50%;
height: 32rpx;
width: 32rpx;
margin: 0;
}
.custom-box {
width: 32rpx;
height: 32rpx;
border: 1px solid #ddd;
border-radius: 50%;
}
}
:deep(.checkbox-active) {
border-color: #fff !important;
background-color: #fff !important;
}
</style>

View File

@ -0,0 +1,54 @@
<template>
<view
v-if="show"
class="overlay"
:class="{ 'overlay-show': show }"
@click="handleClick"
:style="{ zIndex }"
>
<slot></slot>
</view>
</template>
<script lang="ts" setup>
const props = defineProps({
show: {
type: Boolean,
default: false,
},
zIndex: {
type: Number,
default: 10,
},
closeOnClickOverlay: {
type: Boolean,
default: true,
},
})
const emit = defineEmits(['click', 'update:show'])
const handleClick = (event: Event) => {
emit('click', event)
if (props.closeOnClickOverlay) {
emit('update:show', false)
}
}
</script>
<style scoped>
.overlay {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.7);
transition: all 0.3s ease;
}
.overlay-show {
opacity: 1;
visibility: visible;
}
</style>

View File

@ -0,0 +1,133 @@
<template>
<view
class="checkbox"
:class="{
'checkbox--disabled': isDisabled,
'checkbox-active': isChecked,
'checkbox-disabled': isDisabled,
}"
@click="handleClick"
>
<view class="checkbox__icon" :class="{ 'checkbox__icon--checked': isChecked }">
<text v-show="isChecked" class="i-carbon-checkmark checkbox__icon-check"></text>
</view>
<view class="checkbox__label">
<slot>{{ label }}</slot>
</view>
</view>
</template>
<script lang="ts" setup>
import { computed, inject } from 'vue'
const props = defineProps({
name: {
type: [String, Number],
required: true,
},
label: {
type: String,
default: '',
},
disabled: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['change'])
interface CheckboxGroupContext {
modelValue: ComputedRef<any[]>
disabled: ComputedRef<boolean>
max: ComputedRef<number>
selectedCount: ComputedRef<number>
toggleOption: (option: { value: string | number }) => void
}
// 注入checkbox group的上下文
const checkboxGroup = inject<CheckboxGroupContext>('checkboxGroup', {
modelValue: computed(() => []),
disabled: computed(() => false),
max: computed(() => 0),
selectedCount: computed(() => 0),
toggleOption: () => {},
})
// 是否选中
const isChecked = computed(() => {
const modelValue = checkboxGroup.modelValue.value
return modelValue.includes(props.name)
})
// 是否禁用
const isDisabled = computed(() => {
const max = checkboxGroup.max.value
const selectedCount = checkboxGroup.selectedCount.value
// 如果有原始禁用属性,或者group禁用,或者达到最大选择数且未选中,则禁用
return (
props.disabled ||
checkboxGroup.disabled.value ||
(max > 1 && !isChecked.value && selectedCount >= max)
)
})
// 处理点击事件
const handleClick = () => {
if (isDisabled.value) return
checkboxGroup.toggleOption({
value: props.name,
})
}
</script>
<style scoped>
.checkbox {
display: inline-flex;
align-items: center;
cursor: pointer;
font-size: 28rpx;
}
.checkbox--disabled {
cursor: not-allowed;
opacity: 0.5;
}
.checkbox__icon {
width: 40rpx;
height: 40rpx;
border: 2rpx solid #dcdfe6;
border-radius: 4rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 8rpx;
transition: all 0.2s;
}
.checkbox__icon--checked {
background-color: #0083ff;
border-color: #0083ff;
}
.checkbox__icon-check {
color: #fff;
font-size: 32rpx;
}
.checkbox__label {
line-height: 1;
}
.checkbox-active {
background-color: #0083ff;
border-color: #0083ff;
}
.checkbox-disabled {
opacity: 0.5;
cursor: not-allowed !important;
}
</style>

View File

@ -0,0 +1,72 @@
<template>
<view class="checkbox-group">
<slot></slot>
</view>
</template>
<script lang="ts" setup>
import { provide, computed } from 'vue'
const props = defineProps({
modelValue: {
type: Array,
default: () => [],
},
max: {
type: Number,
default: 0,
},
disabled: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['update:modelValue', 'change'])
// 内部维护一个响应式的值
const innerValue = computed(() => props.modelValue)
// 切换选项
const toggleOption = (option: { label: string; value: string | number }) => {
const currentValue = innerValue.value
const index = currentValue.indexOf(option.value)
let newValue = [...currentValue]
if (index === -1) {
if (props.max === 1) {
newValue = [option.value]
} else if (props.max && currentValue.length >= props.max) {
uni.showToast({
title: `最多只能选择${props.max}项`,
icon: 'none',
})
return
} else {
newValue.push(option.value)
}
} else {
newValue.splice(index, 1)
}
emit('update:modelValue', newValue)
emit('change', newValue)
}
// 提供给checkbox的上下文
provide('checkboxGroup', {
modelValue: computed(() => props.modelValue),
disabled: computed(() => props.disabled),
max: computed(() => props.max),
selectedCount: computed(() => props.modelValue.length),
toggleOption,
})
</script>
<style scoped lang="scss">
.checkbox-group {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
</style>

View File

@ -0,0 +1,210 @@
<template>
<view class="navbar">
<!-- 状态栏占位 -->
<view
v-if="safeAreaInsetTop"
class="status-bar"
:style="{ height: statusBarHeight + 'px', backgroundColor: bgColor }"
></view>
<!-- 导航栏主体 -->
<view
class="navbar-content"
:class="[contentClass, fixed ? 'navbar-fixed' : '', bordered ? 'navbar-border' : '']"
:style="{
backgroundColor: bgColor,
height: navHeight + 'px',
top: fixed ? (safeAreaInsetTop ? statusBarHeight : 0) + 'px' : '0',
}"
>
<!-- 左侧区域 -->
<view class="navbar-left" @click="handleClickLeft">
<view v-if="leftArrow" class="back-icon">
<view class="i-carbon-chevron-left text-[40rpx] text-[#333] font-semibold" />
</view>
<slot name="left"></slot>
</view>
<!-- 中间标题区域 -->
<view class="navbar-title">
<slot name="title">
<text class="title-text">{{ title }}</text>
</slot>
</view>
<!-- 右侧区域 -->
<view class="navbar-right">
<slot name="right"></slot>
</view>
</view>
<!-- 占位元素 -->
<view
v-if="placeholder && fixed"
:style="{
height: `${navHeight}px`,
backgroundColor: bgColor,
}"
></view>
<slot name="background"></slot>
</view>
</template>
<script lang="ts" setup>
import { getDeviceInfo, getWindowInfo } from '@/utils/tools'
import { computed } from 'vue'
defineProps({
title: {
type: String,
default: '',
},
leftArrow: {
type: Boolean,
default: false,
},
fixed: {
type: Boolean,
default: false,
},
placeholder: {
type: Boolean,
default: false,
},
bordered: {
type: Boolean,
default: true,
},
safeAreaInsetTop: {
type: Boolean,
default: true,
},
bgColor: {
type: String,
default: '#ffffff',
},
contentClass: {
type: String,
default: 'justify-between',
},
})
const emit = defineEmits(['clickLeft'])
// 获取系统信息
const systemInfo = getWindowInfo()
const deviceInfo = getDeviceInfo()
const statusBarHeight = systemInfo.statusBarHeight || 0
// 动态计算导航栏高度
const navHeight = computed(() => {
// 获取屏幕信息
const { screenWidth } = systemInfo
const { platform } = deviceInfo
// 将px转换为rpx的比例
const ratio = 750 / screenWidth
// 根据平台设置不同的导航栏高度
if (platform === 'ios') {
return 88 / ratio // iOS 一般是 44pt,转换为px
} else if (platform === 'android') {
return 96 / ratio // Android 一般是 48dp,转换为px
} else {
return 88 / ratio // 默认值
}
})
const handleClickLeft = () => {
emit('clickLeft')
}
</script>
<style scoped>
.navbar {
width: 100%;
}
.status-bar {
width: 100%;
background-color: inherit;
}
.navbar-content {
box-sizing: border-box;
display: flex;
align-items: center;
width: 100%;
/* justify-content: space-between; */
padding: 0 16rpx;
background-color: #fff;
}
.navbar-fixed {
position: fixed;
left: 0;
z-index: 999;
width: 100%;
}
.navbar-border {
border-bottom: 1rpx solid #eee;
}
.navbar-left {
display: flex;
align-items: center;
min-width: 100rpx;
height: 100%;
}
.back-icon {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.navbar-title {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
overflow: hidden;
/* flex: 1; */
text-align: center;
}
.title-text {
overflow: hidden;
font-size: 34rpx;
font-weight: 500;
color: #333;
text-overflow: ellipsis;
white-space: nowrap;
}
.navbar-right {
display: flex;
align-items: center;
justify-content: flex-end;
min-width: 52rpx;
height: 100%;
}
@font-face {
font-family: 'iconfont';
src: url('data:font/woff2;charset=utf-8;base64,d09GMgABAAAAAAKYAAsAAAAABlAAAAJMAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHFQGYACCcApcdgE2AiQDCAsGAAQgBYRnBzYbmQXIHpIkBQQKkYCABBEPz2/t/XN3twEbowBkQTxQEQ1RKaKSxEOi0agkJKF5Qvj/f037IFKwlZ2dWU2tJu0EhPwHkBwgOVAclKcvAQpI/v/fz/08XECy+YBymmPQiwIcSmhAY4uSFcgJ+IaxC1zCYwLtRjWSnZ2rGgQWBowLxCPrVBBYllQqNTQ0VISaBXEHtTRNUwW4jb4f/xYEC0kqMzDx6CGrQuKXxKc6Zf7POYQgQHs5kIwjYwEoxK3G/DpRwbi0dlNwKKjAL4lf6vw/R2zVWvTPIwuiCnp2wCRUZ3yJX5pJFVDfByyAFR2AblMAX/OR3t7+zOJi8GyyfzC1uQXLZvtnk/0zyfTy+PvH0/Xp5OzR98/H797/+/fDu3d/3739+/fd+/+nmxvLc5vrS+sry2vz84tLs9Mzc4vzs9NTM/Ozc1OzM3MzU/Mz0wvTU4vTk0tTE8uTEyuT4yv/G0E3XUxv7wwNbu/s9G8fbO9v7+3sb+3ubW4dbO4dbO3vbu4dbO3JzqPFtRE4gEGAX0NBkL+hpCZALkEp5FKUQqE0NHlXJIGrDNAOcEQBCHU+kXT5QNblC7kEv1EK9Y9SB/8o7YYu2m0YXrJLouNIjQJhH+QbVkVZrUQ+YuqzUJdzxPMHhdIj0+hg4o0D8ogj5r5bSoQUxjADz+A8hBDQFEYwh3mommXTul7Vm5ZtqAqJHIdoKCDYDyQ3mCqUG1YKn5+C0s0yiJ/qKVAQedKAhg6Y3mEHJBQaWKnvLVMiiEIxGAY8Aw6HIAhAJmEIzIIOUjLTTAB1taL1QvNq+fYN7QDjcc2okeioaOmy5LFXt3QAAAAA')
format('woff2');
}
.back-text {
font-family: 'iconfont' !important;
font-size: 48rpx;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #333;
}
</style>

View File

@ -0,0 +1,89 @@
<template>
<label
class="radio-wrapper"
:class="{ 'radio-wrapper--disabled': isDisabled }"
@click.stop="handleClick"
>
<radio
class="radio"
:value="String(name)"
:checked="isChecked"
:disabled="isDisabled"
:color="isChecked ? '#0083ff' : ''"
:name="String(name)"
/>
<view class="radio-label" :class="{ 'radio-label--active': isChecked }">
<slot>{{ label }}</slot>
</view>
</label>
</template>
<script lang="ts" setup>
import { computed, inject } from 'vue'
const props = defineProps({
name: {
type: [String, Number],
required: true,
},
label: {
type: String,
default: '',
},
disabled: {
type: Boolean,
default: false,
},
})
interface RadioGroupContext {
modelValue: ComputedRef<string | number>
disabled: ComputedRef<boolean>
toggleOption: (value: string | number) => void
}
// 注入radio group的上下文
const radioGroup = inject<RadioGroupContext>('radioGroup', {
modelValue: computed(() => ''),
disabled: computed(() => false),
toggleOption: () => {},
})
// 是否选中
const isChecked = computed(() => {
return radioGroup.modelValue.value === props.name
})
// 是否禁用
const isDisabled = computed(() => {
return props.disabled || radioGroup.disabled.value
})
// 处理点击事件
const handleClick = () => {
if (isDisabled.value) return
radioGroup.toggleOption(props.name)
}
</script>
<style scoped lang="scss">
.radio-wrapper {
display: inline-flex;
align-items: center;
font-size: 28rpx;
padding: 8rpx 0;
&--disabled {
opacity: 0.5;
}
}
.radio-label {
margin-left: 10rpx;
line-height: 1;
&--active {
color: #0083ff;
}
}
</style>

View File

@ -0,0 +1,50 @@
<template>
<radio-group class="radio-group" :value="modelValue" @change="handleChange">
<slot></slot>
</radio-group>
</template>
<script lang="ts" setup>
import { provide, computed } from 'vue'
const props = defineProps({
modelValue: {
type: [String, Number],
default: '',
},
disabled: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['update:modelValue', 'change'])
// 处理变更
const handleChange = (e: any) => {
const value = e.detail.value
emit('update:modelValue', value)
emit('change', value)
}
// 切换选项
const toggleOption = (value: string | number) => {
emit('update:modelValue', value)
emit('change', value)
}
// 提供给radio的上下文
provide('radioGroup', {
modelValue: computed(() => props.modelValue),
disabled: computed(() => props.disabled),
toggleOption,
})
</script>
<style scoped lang="scss">
.radio-group {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
</style>

View File

@ -0,0 +1,27 @@
import { setWxInfo } from '@/service/index/api'
//uniapp 登陆获取用户信息
export const useLogin = () => {
return new Promise(function (resolve, reject) {
uni.login({
success: function (res) {
if (res.code) {
resolve(res)
} else {
reject(res)
}
},
fail: function (err) {
reject(err)
},
})
})
}
export const useWxInfo = ({ code, openId }) => {
setWxInfo({ code, openId }).then((res) => {
if (res.code === 200) {
console.log(res.result)
}
})
}

195
src/login-sub/index.vue Normal file
View File

@ -0,0 +1,195 @@
<route lang="json5" type="page">
{
style: {
navigationBarTitleText: '康乐云家',
},
}
</route>
<template>
<view class="h-screen flex flex-col bg-white">
<view class="flex flex-col justify-center items-center flex-1 pb-safe mt-[-100px]">
<view class="w-[424rpx] h-[424rpx]">
<image
class="w-[424rpx] h-[424rpx]"
src="https://api.static.ycymedu.com/images/logo.png"
mode="aspectFit"
></image>
</view>
<button
class="w-[493rpx]! mb-[40rpx] h-[88rpx]! rounded-[44rpx] text-[32rpx] text-white flex items-center justify-center"
:class="checked.length > 0 ? 'bg-[#1580FF]' : 'bg-[#BFBFBF]'"
@click.stop="handleClick"
open-type="getPhoneNumber"
@getphonenumber="getPhoneNumber"
:disabled="checked.length === 0"
>
一键登录
</button>
<view class="flex items-center flex-nowrap">
<CheckboxGroup v-model="checked" class="check-class mr-[10rpx]">
<Checkbox name="1" cell shape="button" class="custom-checkbox"></Checkbox>
</CheckboxGroup>
<view class="flex items-center">
<text class="text-[24rpx] whitespace-nowrap">
已阅读并同意
<text class="text-[#1580FF]" @click.stop="handleClickUserAgreement">
<text>《用户协议》</text>
</text>
和
<text class="text-[#1580FF]" @click.stop="handleClickPrivacyPolicy">
<text>《隐私条款》</text>
</text>
</text>
</view>
</view>
</view>
<LoginMask v-model:show="show" @auth-ready="handleAuthReady" />
</view>
</template>
<script setup lang="ts">
import LoginMask from './components/LoginMask.vue'
import { useUserStore } from '@/store/user'
import {
getSessionKey,
getWxUserInfo,
setWxInfo,
} from '@/service/index/api'
import Checkbox from './components/check-group/Checkbox.vue'
import CheckboxGroup from './components/check-group/CheckboxGroup.vue'
import { useLogin } from '@/login-sub/hooks/useUserInfo'
const show = ref(false)
const checked = ref([]) // 是否同意条款
const getPhoneInfo = ref(null)
const getPhoneNumber = async (e: any) => {
if (e.detail.errMsg == 'getPhoneNumber:ok') {
const detail = e.detail
let _getPhoneInfo = {
iv: detail.iv,
encryptedData: detail.encryptedData,
code: detail.code,
}
getPhoneInfo.value = _getPhoneInfo
await getUserInfo(detail.code)
} else if (e.detail.errMsg == 'getPhoneNumber:fail not login') {
uni.showToast({
title: '请先登录',
icon: 'none',
})
} else {
uni.showToast({
title: '获取手机号失败',
icon: 'none',
})
}
}
const handleClick = () => {
if (!checked.value) {
uni.showToast({
title: '您需先同意《服务条款》和《隐私条款》',
icon: 'none',
})
return
}
}
// 授权完成之后返回到上一个目录去
const handleAuthReady = () => {
uni.navigateBack()
}
const handleClickLeft = () => {
uni.navigateBack()
}
const handleClickUserAgreement = () => {
uni.navigateTo({
url: '/login-sub/userAgreement',
})
}
const handleClickPrivacyPolicy = () => {
uni.navigateTo({
url: '/login-sub/privacyPolicy',
})
}
const userStore = useUserStore()
const getUserInfo = async (_code: string) => {
let userInfo = (await useLogin()) as { code: string; errMsg: string }
if (userInfo.errMsg == 'login:ok') {
const resp = await getSessionKey({ JsCode: userInfo.code })
if (resp.code == 200) {
const result = resp.result as { accessToken: string; openId: string }
setWxInfo({ code: _code, openId: result.openId })
userStore.setUserToken(result.accessToken)
userStore.setUserOpenId(result.openId)
uni.navigateBack()
// // 根据微信的信息获取用户信息
// getWxUserInfo().then((resp) => {
// const infoData = resp.result as unknown as {
// userExtend: { provinceCode: string; init: boolean }
// zyBatches: any[]
// batchDataUrl: string
// batchName: string
// avatar: string
// nickName: string
// mobile: string
// sex: number
// }
// if (resp.code === 200) {
// // 根据用户信息中的城市设置对应城市的分数等信息
// }
// })
}
} else {
uni.showToast({
title: '您需先授权',
icon: 'none',
})
}
}
</script>
<style lang="scss" scoped>
:deep(.custom-checkbox) {
display: flex;
align-items: center;
justify-content: center;
.checkbox__icon {
border-radius: 50%;
height: 32rpx;
width: 32rpx;
margin: 0;
}
.custom-box {
width: 32rpx;
height: 32rpx;
border: 1px solid #ddd;
border-radius: 50%;
}
}
:deep(.checkbox-active) {
border-color: #fff !important;
background-color: #fff !important;
}
</style>

View File

@ -0,0 +1,7 @@
<template>
<web-view src="https://api.static.ycymedu.com/lwxy.html" />
</template>
<script lang="ts" setup></script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,7 @@
<template>
<web-view src="https://api.static.ycymedu.com/lwuser.html" />
</template>
<script lang="ts" setup></script>
<style scoped lang="scss"></style>

21
src/main.ts Normal file
View File

@ -0,0 +1,21 @@
import { VueQueryPlugin } from '@tanstack/vue-query'
import { createSSRApp } from 'vue'
import App from './App.vue'
import { prototypeInterceptor, requestInterceptor, routeInterceptor } from './interceptors'
import store from './store'
import '@/style/index.scss'
import 'virtual:uno.css'
export function createApp() {
const app = createSSRApp(App)
app.use(store)
app.use(routeInterceptor)
app.use(requestInterceptor)
app.use(prototypeInterceptor)
app.use(VueQueryPlugin)
return {
app,
}
}

94
src/manifest.json Normal file
View File

@ -0,0 +1,94 @@
{
"name": "康乐云家",
"appid": "__UNI__D1E5001",
"description": "",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
"app-plus": {
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
"modules": {},
"distribute": {
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
],
"minSdkVersion": 30,
"targetSdkVersion": 30,
"abiFilters": [
"armeabi-v7a",
"arm64-v8a"
]
},
"ios": {},
"sdkConfigs": {},
"icons": {
"android": {},
"ios": {}
}
},
"compatible": {
"ignoreVersion": true
}
},
"quickapp": {},
"mp-weixin": {
"appid": "wxe173e798fc5a9f02",
"setting": {
"urlCheck": false,
"es6": true,
"minified": true
},
"usingComponents": true,
"optimization": {
"subPackages": true
},
"permission": {
"scope.userLocation": {
"desc": "你的位置信息将用于小程序位置接口的效果展示"
}
},
"requiredPrivateInfos": [
"getLocation"
]
},
"mp-alipay": {
"usingComponents": true,
"styleIsolation": "shared"
},
"mp-baidu": {
"usingComponents": true
},
"mp-toutiao": {
"usingComponents": true
},
"uniStatistics": {
"enable": false
},
"vueVersion": "3",
"h5": {
"router": {}
}
}

105
src/pages.json Normal file
View File

@ -0,0 +1,105 @@
{
"globalStyle": {
"navigationBarTitleText": "康乐云家",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"backgroundColor": "#FFFFFF"
},
"easycom": {
"autoscan": true,
"custom": {
"^fg-(.*)": "@/components/fg-$1/fg-$1.vue",
"^wd-(.*)": "wot-design-uni/components/wd-$1/wd-$1.vue",
"^(?!z-paging-refresh|z-paging-load-more)z-paging(.*)": "z-paging/components/z-paging$1/z-paging$1.vue"
}
},
"tabBar": {
"color": "#999999",
"selectedColor": "#018d71",
"backgroundColor": "#F8F8F8",
"borderStyle": "black",
"height": "50px",
"fontSize": "10px",
"iconWidth": "24px",
"spacing": "3px",
"list": [
{
"iconPath": "static/tabbar/home.png",
"selectedIconPath": "static/tabbar/homeHL.png",
"pagePath": "pages/index/index",
"text": "",
"icon": "",
"iconType": "uiLib"
},
{
"iconPath": "static/tabbar/example.png",
"selectedIconPath": "static/tabbar/exampleHL.png",
"pagePath": "pages/about/about",
"text": "",
"icon": "",
"iconType": "unocss"
}
]
},
"pages": [
{
"path": "pages/index/index",
"type": "home",
"style": {
"navigationBarTitleText": "康乐云家",
"enableShareAppMessage": true,
"enableShareTimeline": true
}
},
{
"path": "pages/about/about",
"type": "page",
"style": {
"navigationBarTitleText": ""
}
},
{
"path": "pages/address/index",
"type": "page",
"style": {
"navigationBarTitleText": "地址"
},
"layout": false
},
{
"path": "pages/payment/index",
"type": "page"
},
{
"path": "pages/temporary/index",
"type": "page",
"style": {
"navigationBarTitleText": "康乐云家",
"enableShareAppMessage": true,
"enableShareTimeline": true
}
}
],
"subPackages": [
{
"root": "login-sub",
"pages": [
{
"path": "index",
"type": "page",
"style": {
"navigationBarTitleText": "康乐云家"
}
},
{
"path": "privacyPolicy",
"type": "page"
},
{
"path": "userAgreement",
"type": "page"
}
]
}
]
}

19
src/pages/about/about.vue Normal file
View File

@ -0,0 +1,19 @@
<route lang="json5">
{
style: {
navigationBarTitleText: '',
},
}
</route>
<script lang="ts" setup>
</script>
<template>
<view />
</template>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,84 @@
<route lang="json5">
{
layout: 'demo',
style: {
navigationBarTitleText: '请求',
},
}
</route>
<script lang="ts" setup>
import type { IFooItem } from '@/service/index/foo'
import { getFooAPI } from '@/service/index/foo'
// import { findPetsByStatusQueryOptions } from '@/service/app'
// import { useQuery } from '@tanstack/vue-query'
const recommendUrl = ref('http://laf.run/signup?code=ohaOgIX')
// const initialData = {
// name: 'initialData',
// id: '1234',
// }
const initialData = undefined
// 适合少部分全局性的接口————多个页面都需要的请求接口,额外编写一个 Service 层
const { loading, error, data, run } = useRequest<IFooItem>(() => getFooAPI('菲鸽'), {
immediate: true,
initialData,
})
// 使用 vue-query 的 useQuery 来请求数据,只做参考,是否使用请根据实际情况而定
// const {
// data: data2,
// error: error2,
// isLoading: isLoading2,
// refetch,
// } = useQuery(findPetsByStatusQueryOptions({ params: { status: ['available'] } }))
function 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

@ -0,0 +1,38 @@
<route lang="json5" type="page">
{
layout: 'default',
style: {
navigationBarTitleText: '上传-状态一体化',
},
}
</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>
<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>
</template>
</view>
</template>
<style lang="scss" scoped>
//
</style>

View File

@ -0,0 +1,34 @@
<route lang="json5" type="page">
{
style: {
navigationBarTitleText: '地址',
},
"layout": false
}
</route>
<template>
<view class="flex flex-col bg-[#F5F5F5] pt-[30rpx]">
<view class="mb-[40rpx] px-[30rpx]">
<view class="text-[32rpx] font-400 text-[#666] mb-[20rpx]">当前定位</view>
<view class="rounded-[20rpx] bg-white flex items-center">
<view class="py-[30rpx] pl-[30rpx] flex-1 flex flex-col min-w-0">
<view class="text-[36rpx] font-600 truncate">盐山路298</view>
<view class="flex items-center text-[32rpx] text-[#666] font-400 mt-[20rpx]">
<view>上海市</view>
<view>-</view>
<view>齐齐哈尔路</view>
<view>-</view>
<view>杨浦区</view>
</view>
</view>
<image src="/static/images/map/map-icon.png" alt="" class="h-[148rpx] w-[192rpx] shrink-0" />
</view>
</view>
<view class="flex-auto flex flex-col min-h-0">
<view class="text-[32rpx] font-400 mb-[20rpx] text-[#666] px-[30rpx]">我的地址</view>
</view>
</view>
</template>
<script lang="ts" setup></script>

70
src/pages/index/index.vue Normal file
View File

@ -0,0 +1,70 @@
<!-- 使用 type="home" 属性设置首页,其他页面不需要设置,默认为page;推荐使用json5,更强大,且允许注释 -->
<route lang="json5" type="home">
{
style: {
navigationBarTitleText: '康乐云家',
enableShareAppMessage: true,
enableShareTimeline: true,
},
}
</route>
<template>
<view>
<web-view :src="webUrl" @message="handlePostMessage" />
</view>
</template>
<script lang="ts" setup>
defineOptions({
name: 'Home',
})
// https://mobile.jinzejk.com/home
const webUrl = ref(`http://192.168.100.121:3001/home`)
// 测试 uni API 自动引入
onLoad(() => {
uni.hideTabBar()
uni.showShareMenu({
withShareTicket: true,
menus: ['shareAppMessage', 'shareTimeline']
})
uni.getLocation({
type: 'wgs84',
success: ({ longitude, latitude }) => {
webUrl.value = `${webUrl.value}?longitude=${longitude}&latitude=${latitude}&time=${new Date().getTime()}`
},
fail: (error) => {
webUrl.value += `?longitude=121.5097620985243&latitude=31.30658013237847&time=${new Date().getTime()}`
console.log(error)
},
})
})
onShareAppMessage(() => {
return {
title: '康乐云家',
path: `/pages/index/index`,
}
})
onShareTimeline(() => {
return {
title: '康乐云家',
path:'/pages/index/index',
}
})
const handlePostMessage = (evt: any) => {
const actionsData = evt.detail.data as any[]
actionsData.forEach((item) => {
console.log(item)
})
}
</script>

View File

@ -0,0 +1,63 @@
<template>
<view></view>
</template>
<script lang="ts" setup>
import {payTransaction} from "@/service/index/api"
import { useUserStore } from "@/store"
const userStore = useUserStore()
const price = ref(0)
const productionName = ref("")
const handleOrder = () => {
payTransaction({
total: price.value * 100,
openId: userStore.userInfo.openId,
description: productionName.value,
}).then((res) => {
if (res.code === 200) {
requestPay(res.result)
} else {
uni.showToast({
title: res.message,
icon: 'none',
})
}
})
}
const requestPay = (res) => {
uni.requestPayment({
provider: 'wxpay',
timeStamp: res.timeStamp,
orderInfo: '',
nonceStr: res.nonceStr, //随机字符串,长度为32个字符以下。
package: res.package, //prepay_id 参数值,提交格式如:prepay_id=xx
signType: res.signType, //MD5类型
paySign: res.paySign, //签名
success: (result) => {
uni.showToast({
title: '支付成功!',
icon: 'success',
})
uni.navigateBack()
},
fail: (error) => {
console.log(error)
},
})
}
onBeforeMount(() => {
handleOrder()
})
onLoad((options) => {
console.log(options);
price.value = options.total || 0;
productionName.value = options.description || ''
handleOrder()
})
</script>

View File

@ -0,0 +1,67 @@
<route lang="json5">
{
style: {
navigationBarTitleText: '康乐云家',
enableShareAppMessage: true,
enableShareTimeline: true,
},
}
</route>
<template>
<web-view :src="webUrl" />
</template>
<script setup lang="ts">
// https://mobile.jinzejk.com
let webUrl = ref('http://192.168.100.121:3001')
const webFullPath = ref("")
const getLocation = (webFullPath:string) => {
if(!webFullPath.includes('/home/address')){
return
}
uni.getLocation({
type: 'gcj02',
success: ({ longitude, latitude }) => {
console.log(longitude,latitude)
},
fail: (error) => {
console.log(error)
},
})
}
onLoad((options) => {
webFullPath.value = decodeURIComponent(options.webFullPath)
getLocation(webFullPath.value)
webUrl.value = webUrl.value + webFullPath.value
uni.setStorageSync('webUrl', webUrl)
uni.showShareMenu({
withShareTicket: true,
menus: ['shareAppMessage', 'shareTimeline']
})
})
onShareAppMessage(() => {
return {
title: '康乐云家',
path: `/pages/temporary/index?webFullPath=${webFullPath.value}`,
query: 'webFullPath=' + webUrl
}
})
onShareTimeline(() => {
return {
title: '康乐云家',
query: 'webFullPath=' + webUrl
}
})
</script>

View File

@ -0,0 +1,13 @@
/* eslint-disable */
// @ts-ignore
import * as API from './types';
export function displayStatusEnum(field: API.IStatusEnum) {
return { available: 'available', pending: 'pending', sold: 'sold' }[field];
}
export function displayStatusEnum2(field: API.IStatusEnum2) {
return { placed: 'placed', approved: 'approved', delivered: 'delivered' }[
field
];
}

11
src/service/app/index.ts Normal file
View File

@ -0,0 +1,11 @@
/* eslint-disable */
// @ts-ignore
export * from './types';
export * from './displayEnumLabel';
export * from './pet';
export * from './pet.vuequery';
export * from './store';
export * from './store.vuequery';
export * from './user';
export * from './user.vuequery';

22
src/service/app/pet.ts Normal file
View File

@ -0,0 +1,193 @@
/* eslint-disable */
// @ts-ignore
import request from '@/utils/request';
import { CustomRequestOptions } from '@/interceptors/request';
import * as API from './types';
/** Update an existing pet PUT /pet */
export async function updatePet({
body,
options,
}: {
body: API.Pet;
options?: CustomRequestOptions;
}) {
return request<unknown>('/pet', {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
data: body,
...(options || {}),