This commit is contained in:
2024-02-02 22:31:01 +08:00
parent 06a5f041bf
commit 486ea5013a
77 changed files with 17507 additions and 122 deletions

22
src/App.vue Normal file
View File

@@ -0,0 +1,22 @@
<script setup lang="ts">
import { mpUpdate } from '@/utils';
onLaunch(() => {
console.log('App Launch');
// #ifdef MP
mpUpdate();
// #endif
});
onShow(() => {
console.log('App Show');
});
onHide(() => {
console.log('App Hide');
});
</script>
<style lang="scss">
/* 每个页面公共css */
@import 'uview-plus/index.scss';
@import '@/static/styles/common.scss';
</style>

17
src/api/common/index.ts Normal file
View File

@@ -0,0 +1,17 @@
/**
* 通用接口
*/
import type { SendCodeParams, SendCodeResult, UploadImageResult } from './types';
import { post, upload } from '@/utils/request';
enum URL {
upload = '/common/upload',
sendCode = '/sendCode',
}
// 图片上传
export const uploadImage = (imagePath: string) =>
upload<UploadImageResult>({ url: URL.upload, filePath: imagePath, name: 'file' });
// 发送验证码
export const sendCode = (data: SendCodeParams) => post<SendCodeResult>({ url: URL.sendCode, data });

13
src/api/common/types.ts Normal file
View File

@@ -0,0 +1,13 @@
export interface UploadImageResult {
file: string;
url: string;
}
export interface SendCodeParams {
phone: number;
code: number;
}
export interface SendCodeResult {
code: number;
}

4
src/api/index.ts Normal file
View File

@@ -0,0 +1,4 @@
import * as CommonApi from './common';
import * as UserApi from './user';
export { CommonApi, UserApi };

18
src/api/user/index.ts Normal file
View File

@@ -0,0 +1,18 @@
/**
* 用户信息相关接口
*/
import type { LoginByCodeParams, LoginParams, LoginResult } from './types';
import { get, post } from '@/utils/request';
import type { UserState } from '@/store/modules/user/types';
enum URL {
login = '/user/login',
loginByCode = '/user/loginByCode',
logout = '/user/logout',
profile = '/user/profile',
}
export const getUserProfile = () => get<UserState>({ url: URL.profile });
export const login = (data: LoginParams) => post<LoginResult>({ url: URL.login, data });
export const loginByCode = (data: LoginByCodeParams) => post<any>({ url: URL.loginByCode, data });
export const logout = () => post<any>({ url: URL.logout });

15
src/api/user/types.ts Normal file
View File

@@ -0,0 +1,15 @@
export interface LoginParams {
phone: string;
code: string;
}
export interface LoginByCodeParams {
code: string;
}
export interface LoginResult {
token: string;
user_id: number;
user_name: string;
avatar: string;
}

View File

@@ -0,0 +1,103 @@
<template>
<view class="nav-wrap">
<view class="nav-title">
<u--image :show-loading="true" src="./static/logo.png" width="70px" height="70px" />
<view class="nav-info">
<view class="nav-info__title">
<text class="nav-info__title__text">uview-plus3</text>
</view>
<text class="nav-slogan">多平台快速开发的UI框架</text>
</view>
</view>
<text class="nav-desc">{{ desc }}</text>
</view>
</template>
<script>
export default {
name: 'PageNav',
props: {
desc: String,
title: String,
},
data() {
return {};
},
};
</script>
<style lang="scss" scoped>
.nav-wrap {
position: relative;
padding: 15px;
}
.lang {
position: absolute;
top: 15px;
right: 15px;
}
.nav-title {
/* #ifndef APP-NVUE */
display: flex;
justify-content: flex-start;
align-items: center;
/* #endif */
flex-direction: row;
}
.nav-info {
margin-left: 15px;
&__title {
/* #ifndef APP-NVUE */
display: flex;
/* #endif */
flex-direction: row;
align-items: center;
&__text {
/* #ifndef APP-NVUE */
display: flex;
font-size: 25px;
text-align: left;
/* #endif */
color: $u-main-color;
font-weight: bold;
}
&__jump {
margin-left: 20px;
font-size: 12px;
color: $u-primary;
font-weight: normal;
}
}
}
.logo {
width: 70px;
height: 70px;
/* #ifndef APP-NVUE */
height: auto;
/* #endif */
}
.nav-slogan {
font-size: 14px;
color: $u-tips-color;
}
.nav-desc {
margin-top: 10px;
font-size: 14px;
color: $u-content-color;
line-height: 20px;
}
</style>

6
src/hooks/index.ts Normal file
View File

@@ -0,0 +1,6 @@
import useShare from './use-share';
import useLoading from './use-loading';
import useModal from './use-modal';
import useClipboard from './use-clipboard';
export { useShare, useLoading, useModal, useClipboard };

View File

@@ -0,0 +1,33 @@
/**
* 剪切板
*/
interface SetClipboardDataOptions {
data: string;
showToast?: boolean;
}
export default function useClipboard() {
const setClipboardData = ({ data, showToast = true }: SetClipboardDataOptions) => {
return new Promise<string>((resolve, reject) => {
uni.setClipboardData({
data,
showToast,
success: ({ data }) => resolve(data),
fail: error => reject(error),
});
});
};
const getClipboardData = () => {
return new Promise<string>((resolve, reject) => {
uni.getClipboardData({
success: ({ data }) => resolve(data),
fail: error => reject(error),
});
});
};
return {
setClipboardData,
getClipboardData,
};
}

View File

@@ -0,0 +1,19 @@
/**
* loading 提示框
*/
export default function useLoading() {
const showLoading = (content = '加载中') => {
uni.showLoading({
title: content,
mask: true,
});
};
const hideLoading = () => {
uni.hideLoading();
};
return {
showLoading,
hideLoading,
};
}

View File

@@ -0,0 +1,21 @@
/**
* Dialog 提示框
*/
export default function useModal() {
const showModal = (content: string, options: UniApp.ShowModalOptions) => {
return new Promise((resolve, reject) => {
uni.showModal({
title: '温馨提示',
content,
showCancel: false,
confirmColor: '#1677FF',
success: res => resolve(res),
fail: () => reject(new Error('Alert 调用失败 !')),
...options,
})
})
}
return {
showModal,
}
}

View File

@@ -0,0 +1,32 @@
/**
* 小程序分享
*/
interface UseShareOptions {
title?: string;
path?: string;
query?: string;
imageUrl?: string;
}
export default function useShare(options?: UseShareOptions) {
// #ifdef MP-WEIXIN
const title = options?.title ?? '';
const path = options?.path ?? '';
const query = options?.query ?? '';
const imageUrl = options?.imageUrl ?? '';
onShareAppMessage(() => {
return {
title,
path: path ? `${path}${query ? `?${query}` : ''}` : '',
imageUrl,
};
});
onShareTimeline(() => {
return {
title,
query: options?.query ?? '',
imageUrl,
};
});
// #endif
}

32
src/main.ts Normal file
View File

@@ -0,0 +1,32 @@
import { createSSRApp } from 'vue';
// 引入UnoCSS
import 'uno.css';
// 引入uview-plus
import uviewPlus from 'uview-plus';
import App from '@/App.vue';
// 引入状态管理
import setupStore from '@/store';
// 引入请求封装
import setupRequest from '@/utils/request';
// 权限管理
import '@/permission';
// #ifdef VUE3
export function createApp() {
const app = createSSRApp(App);
app.use(uviewPlus);
// 状态管理
setupStore(app);
// 网络请求
setupRequest();
return {
app,
};
}
// #endif

47
src/manifest.json Normal file
View File

@@ -0,0 +1,47 @@
{
"name": "SUKE-MP",
"appid": "",
"description": "",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
/* 5+App */
"app-plus":
{
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"splashscreen":
{
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
/* */
"modules": {}
},
/* */
"mp-weixin":
{
"appid": "wx67a750d0ceed4d88",
"setting":
{
"urlCheck": false
},
"usingComponents": true
},
"uniStatistics":
{
"enable": false
},
"vueVersion": "3",
"h5":
{
"router":
{
"mode": "hash",
"base": "/uniapp-vue3-template/"
}
}
}

97
src/pages.json Normal file
View File

@@ -0,0 +1,97 @@
{
"easycom": {
"custom": {
"^u--(.*)": "uview-plus/components/u-$1/u-$1.vue",
"^up-(.*)": "uview-plus/components/u-$1/u-$1.vue",
"^u-([^-].*)": "uview-plus/components/u-$1/u-$1.vue",
"^(?!z-paging-refresh|z-paging-load-more)z-paging(.*)": "z-paging/components/z-paging$1/z-paging$1.vue"
}
},
"pages": [
{
"path": "pages/home/index",
"style": {
"navigationBarTitleText": "首页"
}
},
{
"path": "pages/qrcode/index",
"style": {
"navigationBarTitleText": "支付码"
}
},
{
"path": "pages/mall/index",
"style": {
"navigationBarTitleText": "商城"
}
},
{
"path": "pages/mine/index",
"style": {
"navigationBarTitleText": "我的"
}
}
],
"subPackages": [
{
"root": "pages/common",
"pages": [
{
"path": "login/index",
"navigationStyle": "custom"
},
{
"path": "webview/index",
"navigationBarTitleText": "网页"
}
]
}
],
"preloadRule": {
"pages/home/index": {
"network": "all",
"packages": [
"pages/common"
]
}
},
"tabBar": {
"color": "#ABABAB",
"selectedColor": "#333333",
"borderStyle": "black",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/home/index",
"iconPath": "static/images/ic_tab_home_normal.png",
"selectedIconPath": "static/images/ic_tab_home_active.png",
"text": "首页"
},
{
"pagePath": "pages/qrcode/index",
"iconPath": "static/images/ic_tab_qrcode_normal.png",
"selectedIconPath": "static/images/ic_tab_qrcode_active.png",
"text": "支付码"
},
{
"pagePath": "pages/mall/index",
"iconPath": "static/images/ic_tab_mall_normal.png",
"selectedIconPath": "static/images/ic_tab_mall_active.png",
"text": "商城"
},
{
"pagePath": "pages/mine/index",
"iconPath": "static/images/ic_tab_mine_normal.png",
"selectedIconPath": "static/images/ic_tab_mine_active.png",
"text": "我的"
}
]
},
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
}
}

View File

@@ -0,0 +1,167 @@
<template>
<view>
<view class="login-form-wrap">
<view class="title">
欢迎登录
</view>
<input v-model="tel" class="u-border-bottom" type="number" placeholder="请输入手机号">
<view class="u-border-bottom my-40rpx flex">
<input v-model="code" class="flex-1" type="number" placeholder="请输入验证码">
<view>
<u-code ref="uCodeRef" @change="codeChange" />
<u-button :text="tips" type="success" size="mini" @click="getCode" />
</view>
</view>
<button :style="[inputStyle]" class="login-btn" @tap="submit">
登录
</button>
<view class="alternative">
<view class="password">
密码登录
</view>
<view class="issue">
遇到问题
</view>
</view>
</view>
<view class="login-type-wrap">
<view class="item wechat">
<view class="icon">
<u-icon size="35" name="weixin-fill" color="rgb(83,194,64)" />
</view>
微信
</view>
<view class="item QQ">
<view class="icon">
<u-icon size="35" name="qq-fill" color="rgb(17,183,233)" />
</view>
QQ
</view>
</view>
<view class="hint">
登录代表同意
<text class="link">
用户协议隐私政策
</text>
并授权使用您的账号信息如昵称头像收获地址以便您统一管理
</view>
</view>
</template>
<script setup lang="ts">
import uCode from 'uview-plus/components/u-code/u-code.vue';
import { setToken } from '@/utils/auth';
const tel = ref<string>('18502811111');
const code = ref<string>('1234');
const tips = ref<string>();
const uCodeRef = ref<InstanceType<typeof uCode> | null>(null);
const inputStyle = computed<CSSStyleDeclaration>(() => {
const style = {} as CSSStyleDeclaration;
if (tel.value && code.value) {
style.color = '#fff';
style.backgroundColor = uni.$u.color.warning;
}
return style;
});
function codeChange(text: string) {
tips.value = text;
}
function getCode() {
if (uCodeRef.value?.canGetCode) {
// 模拟向后端请求验证码
uni.showLoading({
title: '正在获取验证码',
});
setTimeout(() => {
uni.hideLoading();
uni.$u.toast('验证码已发送');
// 通知验证码组件内部开始倒计时
uCodeRef.value?.start();
}, 1000);
}
else {
uni.$u.toast('倒计时结束后再发送');
}
}
function submit() {
if (uni.$u.test.mobile(tel.value)) {
setToken('1234567890');
uni.reLaunch({ url: '/pages/home/index' });
}
}
</script>
<style lang="scss" scoped>
.login-form-wrap {
margin: 80rpx auto 0;
width: 600rpx;
.title {
margin-bottom: 100rpx;
font-size: 60rpx;
text-align: left;
font-weight: 500;
}
input {
padding-bottom: 6rpx;
margin-bottom: 10rpx;
text-align: left;
}
.tips {
margin-top: 8rpx;
margin-bottom: 60rpx;
color: $u-info;
}
.login-btn {
padding: 12rpx 0;
font-size: 30rpx;
color: $u-tips-color;
background-color: rgb(253 243 208);
border: none;
&::after {
border: none;
}
}
.alternative {
display: flex;
justify-content: space-between;
margin-top: 30rpx;
color: $u-tips-color;
}
}
.login-type-wrap {
display: flex;
justify-content: space-between;
padding: 350rpx 150rpx 150rpx;
.item {
display: flex;
align-items: center;
font-size: 28rpx;
color: $u-content-color;
flex-direction: column;
}
}
.hint {
padding: 20rpx 40rpx;
font-size: 20rpx;
color: $u-tips-color;
.link {
color: $u-warning;
}
}
</style>

View File

@@ -0,0 +1,12 @@
<template>
<web-view class="h-full" :src="url" />
</template>
<script setup lang="ts">
const url = ref<string>('');
onLoad((params: any) => {
if (params.url)
url.value = params.url;
});
</script>

18
src/pages/home/index.vue Normal file
View File

@@ -0,0 +1,18 @@
<template>
<view class='flex flex-col items-center justify-center'>
<swiper class='swiper'>
<swiper-item v-for='(item, index) in bannerList' :key='index'>
<image :src='item' mode='aspectFill' />
</swiper-item>
</swiper>
</view>
</template>
<script setup lang='ts'>
import { useUserStore } from '@/store';
const store = useUserStore();
const bannerList = ref<string[]>(['1', '2', '3', '4']);
console.log('store.user_name', store.user_name);
</script>

25
src/pages/mall/index.vue Normal file
View File

@@ -0,0 +1,25 @@
<template>
<view class="flex flex-col items-center justify-center">
<image
class="mb-50rpx mt-200rpx h-200rpx w-200rpx"
src="@/static/images/logo.png"
width="200rpx"
height="200rpx"
/>
<view class="flex justify-center">
<text class="font-size-36rpx color-gray-700">
{{ title }}
</text>
</view>
</view>
</template>
<script setup lang="ts">
import { useUserStore } from '@/store';
const title = ref<string>();
title.value = import.meta.env.VITE_APP_TITLE;
const store = useUserStore();
console.log('store.user_name', store.user_name);
</script>

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

@@ -0,0 +1,25 @@
<template>
<view class="flex flex-col items-center justify-center">
<image
class="mb-50rpx mt-200rpx h-200rpx w-200rpx"
src="@/static/images/logo.png"
width="200rpx"
height="200rpx"
/>
<view class="flex justify-center">
<text class="font-size-36rpx color-gray-700">
{{ title }}
</text>
</view>
</view>
</template>
<script setup lang="ts">
import { useUserStore } from '@/store';
const title = ref<string>();
title.value = import.meta.env.VITE_APP_TITLE;
const store = useUserStore();
console.log('store.user_name', store.user_name);
</script>

View File

@@ -0,0 +1,25 @@
<template>
<view class="flex flex-col items-center justify-center">
<image
class="mb-50rpx mt-200rpx h-200rpx w-200rpx"
src="@/static/images/logo.png"
width="200rpx"
height="200rpx"
/>
<view class="flex justify-center">
<text class="font-size-36rpx color-gray-700">
{{ title }}
</text>
</view>
</view>
</template>
<script setup lang="ts">
import { useUserStore } from '@/store';
const title = ref<string>();
title.value = import.meta.env.VITE_APP_TITLE;
const store = useUserStore();
console.log('store.user_name', store.user_name);
</script>

37
src/permission.ts Normal file
View File

@@ -0,0 +1,37 @@
import { getToken } from '@/utils/auth';
// 登录页面
const loginPage = '/pages/common/login/index';
// 页面白名单
const whiteList = ['/', '/pages/common/login/index', '/pages/home/index'];
// 检查地址白名单
function checkWhite(url: string) {
const path = url.split('?')[0];
return whiteList.includes(path);
}
// 页面跳转验证拦截器
const list = ['navigateTo', 'redirectTo', 'reLaunch', 'switchTab'];
list.forEach((item) => {
uni.addInterceptor(item, {
invoke(to) {
if (getToken()) {
if (to.url === loginPage)
uni.reLaunch({ url: '/' });
return true;
}
else {
if (checkWhite(to.url))
return true;
uni.reLaunch({ url: loginPage });
return false;
}
},
fail(err) {
console.log(err);
},
});
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
src/static/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -0,0 +1,4 @@
page {
font-size: 28rpx;
background-color: #f9f9f8;
}

20
src/store/index.ts Normal file
View File

@@ -0,0 +1,20 @@
import type { App } from 'vue';
import { createPinia } from 'pinia';
// 导入子模块
import useAppStore from './modules/app';
import useUserStore from './modules/user';
// import piniaPersist from 'pinia-plugin-persist-uni';
// 安装pinia状态管理插件
function setupStore(app: App) {
const store = createPinia();
// store.use(piniaPersist);
app.use(store);
}
// 导出模块
export { useAppStore, useUserStore };
export default setupStore;

View File

@@ -0,0 +1,58 @@
import { defineStore } from 'pinia';
import type { AppState } from './types';
const useAppStore = defineStore('app', {
state: (): AppState => ({
systemInfo: {} as UniApp.GetSystemInfoResult,
}),
getters: {
getSystemInfo(): UniApp.GetSystemInfoResult {
return this.systemInfo;
},
},
actions: {
setSystemInfo(info: UniApp.GetSystemInfoResult) {
this.systemInfo = info;
},
initSystemInfo() {
uni.getSystemInfo({
success: (res: UniApp.GetSystemInfoResult) => {
this.setSystemInfo(res);
},
fail: (err: any) => {
console.error(err);
},
});
},
checkUpdate() {
const updateManager = uni.getUpdateManager();
updateManager.onCheckForUpdate((res: UniApp.OnCheckForUpdateResult) => {
// 请求完新版本信息的回调
console.log(res.hasUpdate);
});
updateManager.onUpdateReady(() => {
uni.showModal({
title: '更新提示',
content: '新版本已经准备好,是否重启应用?',
success(res) {
if (res.confirm) {
// 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
updateManager.applyUpdate();
}
},
});
});
updateManager.onUpdateFailed((res: any) => {
console.error(res);
// 新的版本下载失败
uni.showToast({
title: '更新失败',
icon: 'error',
});
});
},
},
});
export default useAppStore;

View File

@@ -0,0 +1,3 @@
export interface AppState {
systemInfo: UniApp.GetSystemInfoResult;
}

View File

@@ -0,0 +1,82 @@
import { defineStore } from 'pinia';
import type { UserState, providerType } from './types';
import {
getUserProfile,
loginByCode,
login as userLogin,
logout as userLogout,
} from '@/api/user/index';
import { clearToken, setToken } from '@/utils/auth';
import type { LoginParams } from '@/api/user/types';
const useUserStore = defineStore('user', {
state: (): UserState => ({
user_id: '',
user_name: '江阳小道',
avatar: '',
token: '',
}),
getters: {
userInfo(state: UserState): UserState {
return { ...state };
},
},
actions: {
// 设置用户的信息
setInfo(partial: Partial<UserState>) {
this.$patch(partial);
},
// 重置用户信息
resetInfo() {
this.$reset();
},
// 获取用户信息
async info() {
const result = await getUserProfile();
this.setInfo(result);
},
// 异步登录并存储token
login(loginForm: LoginParams) {
return new Promise(async (resolve, reject) => {
try {
const result = await userLogin(loginForm);
const token = result?.token;
if (token) {
setToken(token);
}
resolve(result);
} catch (error) {
reject(error)
}
});
},
// Logout
async logout() {
await userLogout();
this.resetInfo();
clearToken();
},
// 小程序授权登录
authLogin(provider: providerType = 'weixin') {
return new Promise((resolve, reject) => {
uni.login({
provider,
success: async (result: UniApp.LoginRes) => {
if (result.code) {
const res = await loginByCode({ code: result.code });
resolve(res);
} else {
reject(new Error(result.errMsg));
}
},
fail: (err: any) => {
console.error(`login error: ${err}`);
reject(err);
},
});
});
},
},
});
export default useUserStore;

View File

@@ -0,0 +1,16 @@
export type RoleType = '' | '*' | 'user';
export interface UserState {
user_id?: string;
user_name?: string;
avatar?: string;
token?: string;
}
export type providerType =
| 'weixin'
| 'qq'
| 'sinaweibo'
| 'xiaomi'
| 'apple'
| 'univerify'
| undefined;

1
src/uni.scss Normal file
View File

@@ -0,0 +1 @@
@import 'uview-plus/theme.scss';

15
src/utils/auth/index.ts Normal file
View File

@@ -0,0 +1,15 @@
const TokenKey = 'admin-token';
const TokenPrefix = 'Bearer ';
function isLogin() {
return !!uni.getStorageSync(TokenKey);
}
function getToken() {
return uni.getStorageSync(TokenKey);
}
function setToken(token: string) {
uni.setStorageSync(TokenKey, token);
}
function clearToken() {
uni.removeStorageSync(TokenKey);
}
export { TokenPrefix, isLogin, getToken, setToken, clearToken };

28
src/utils/common/index.ts Normal file
View File

@@ -0,0 +1,28 @@
// 小程序更新检测
export function mpUpdate() {
const updateManager = uni.getUpdateManager();
updateManager.onCheckForUpdate((res) => {
// 请求完新版本信息的回调
console.log(res.hasUpdate);
});
updateManager.onUpdateReady(() => {
uni.showModal({
title: '更新提示',
content: '检测到新版本,是否下载新版本并重启小程序?',
success(res) {
if (res.confirm) {
// 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
updateManager.applyUpdate();
}
},
});
});
updateManager.onUpdateFailed(() => {
// 新的版本下载失败
uni.showModal({
title: '已经有新版本了哟~',
content: '新版本已经上线啦~,请您删除当前小程序,重新搜索打开哟~',
showCancel: false,
});
});
}

4
src/utils/index.ts Normal file
View File

@@ -0,0 +1,4 @@
export * from './auth';
export * from './common';
export * from './modals';
export * from './request';

81
src/utils/modals/index.ts Normal file
View File

@@ -0,0 +1,81 @@
interface IShowToastOptions {
title?: string
icon?: 'success' | 'loading' | 'error' | 'none'
image?: string
duration?: number
position?: 'top' | 'center' | 'bottom'
mask?: boolean
}
interface ILoadingOptions {
show?: (content?: string) => void
hide?: () => void
}
interface IShowModalOptions {
title?: string
content?: string
showCancel?: boolean
cancelText?: string
cancelColor?: string
confirmText?: string
confirmColor?: string
editable?: boolean
placeholderText?: string
}
/**
* 轻提示
* @param {string} content 提示内容
* @param {object} option 配置
*/
export function Toast(content: string, option: IShowToastOptions = {}) {
uni.showToast({
title: content,
icon: 'none',
mask: true,
duration: 1500,
...option,
});
}
/**
* Loading 提示框
* @param {string} content 提示内容
*/
export const Loading: ILoadingOptions = {
show: (content = '加载中') => {
uni.showLoading({
title: content,
mask: true,
});
},
hide: () => {
uni.hideLoading();
},
};
/**
* Dialog 提示框
* @param {string} content 提示内容
* @param {object} option 配置
*/
export function Dialog(content: string, option: IShowModalOptions = {}) {
option.showCancel = false;
return new Promise((resolve, reject) => {
uni.showModal({
title: '温馨提示',
content,
showCancel: false,
confirmColor: '#1677FF',
success(res) {
if (res.confirm)
resolve(res);
},
fail() {
reject(new Error('Alert 调用失败 !'));
},
...option,
});
});
}

View File

@@ -0,0 +1,42 @@
// 引入配置
import type { HttpRequestConfig } from 'uview-plus/libs/luch-request/index';
import { requestInterceptors, responseInterceptors } from './interceptors';
import type { IResponse } from './type';
// 引入拦截器配置
export function setupRequest() {
uni.$u.http.setConfig((defaultConfig: HttpRequestConfig) => {
/* defaultConfig 为默认全局配置 */
defaultConfig.baseURL = import.meta.env.VITE_APP_BASE_API;
return defaultConfig;
});
requestInterceptors();
responseInterceptors();
}
export function request<T = any>(config: HttpRequestConfig): Promise<T> {
return new Promise((resolve) => {
uni.$u.http.request(config).then((res: IResponse) => {
const { result } = res;
resolve(result as T);
});
});
}
export function get<T = any>(config: HttpRequestConfig): Promise<T> {
return request({ ...config, method: 'GET' });
}
export function post<T = any>(config: HttpRequestConfig): Promise<T> {
return request({ ...config, method: 'POST' });
}
export function upload<T = any>(config: HttpRequestConfig): Promise<T> {
return request({ ...config, method: 'UPLOAD' });
}
export function download<T = any>(config: HttpRequestConfig): Promise<T> {
return request({ ...config, method: 'DOWNLOAD' });
}
export default setupRequest;

View File

@@ -0,0 +1,104 @@
import type {
HttpError,
HttpRequestConfig,
HttpResponse,
} from 'uview-plus/libs/luch-request/index';
import { showMessage } from './status';
import { getToken } from '@/utils/auth';
import useUserStore from '@/store/modules/user';
// 是否正在刷新token的标记
let isRefreshing: boolean = false;
// 重试队列,每一项将是一个待执行的函数形式
let requestQueue: (() => void)[] = [];
function requestInterceptors() {
/**
* 请求拦截
* @param {Object} http
*/
uni.$u.http.interceptors.request.use(
(config: HttpRequestConfig) => {
// 可使用async await 做异步操作
// 初始化请求拦截器时会执行此方法此时data为undefined赋予默认{}
config.data = config.data || {};
// token设置
const token = getToken();
if (token && config.header) {
config.header.token = token;
}
return config;
},
(
config: any, // 可使用async await 做异步操作
) => Promise.reject(config),
);
}
function responseInterceptors() {
/**
* 响应拦截
* @param {Object} http
*/
uni.$u.http.interceptors.response.use(
async (response: HttpResponse) => {
/* 对响应成功做点什么 可使用async await 做异步操作 */
const data = response.data;
// 配置参数
const config = response.config;
// 自定义参数
const custom = config?.custom;
// 请求成功则返回结果
if (data.code === 200) {
return data || {};
}
// 登录状态失效,重新登录
if (data.code === 401) {
// 是否在获取token中,防止重复获取
if (!isRefreshing) {
// 修改登录状态为true
isRefreshing = true;
await useUserStore().authLogin();
// 登录完成之后,开始执行队列请求
requestQueue.forEach(cb => cb());
// 重试完了清空这个队列
requestQueue = [];
isRefreshing = false;
// 重新执行本次请求
return uni.$u.http.request(config);
} else {
return new Promise(resolve => {
// 将resolve放进队列用一个函数形式来保存等登录后直接执行
requestQueue.push(() => {
resolve(uni.$u.http.request(config));
});
});
}
}
// 如果没有显式定义custom的toast参数为false的话默认对报错进行toast弹出提示
if (custom?.toast !== false) {
uni.$u.toast(data.message);
}
// 如果需要catch返回则进行reject
if (custom?.catch) {
return Promise.reject(data);
} else {
// 否则返回一个pending中的promise
return new Promise(() => {});
}
},
(response: HttpError) => {
if (response.statusCode) {
// 请求已发出但是不在2xx的范围
showMessage(response.statusCode);
return Promise.reject(response.data);
}
showMessage('网络连接异常,请稍后再试!');
},
);
}
export { requestInterceptors, responseInterceptors };

View File

@@ -0,0 +1,41 @@
export const showMessage = (status: number | string): string => {
let message = '';
switch (status) {
case 400:
message = '请求错误(400)';
break;
case 401:
message = '未授权,请重新登录(401)';
break;
case 403:
message = '拒绝访问(403)';
break;
case 404:
message = '请求出错(404)';
break;
case 408:
message = '请求超时(408)';
break;
case 500:
message = '服务器错误(500)';
break;
case 501:
message = '服务未实现(501)';
break;
case 502:
message = '网络错误(502)';
break;
case 503:
message = '服务不可用(503)';
break;
case 504:
message = '网络超时(504)';
break;
case 505:
message = 'HTTP版本不受支持(505)';
break;
default:
message = `连接出错(${status})!`;
}
return `${message},请检查网络或联系管理员!`;
};

View File

@@ -0,0 +1,7 @@
// 返回res.data的interface
export interface IResponse<T = any> {
code: number | string;
result: T;
message: string;
status: string | number;
}