init
22
src/App.vue
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
103
src/components/page-nav/page-nav.vue
Normal 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
@@ -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 };
|
33
src/hooks/use-clipboard/index.ts
Normal 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,
|
||||
};
|
||||
}
|
19
src/hooks/use-loading/index.ts
Normal 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,
|
||||
};
|
||||
}
|
21
src/hooks/use-modal/index.ts
Normal 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,
|
||||
}
|
||||
}
|
32
src/hooks/use-share/index.ts
Normal 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
@@ -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
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
167
src/pages/common/login/index.vue
Normal 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>
|
12
src/pages/common/webview/index.vue
Normal 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
@@ -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
@@ -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
@@ -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/qrcode/index.vue
Normal 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
@@ -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);
|
||||
},
|
||||
});
|
||||
});
|
BIN
src/static/images/ic_tab_home_active.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
src/static/images/ic_tab_home_normal.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
src/static/images/ic_tab_mall_active.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
src/static/images/ic_tab_mall_normal.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
BIN
src/static/images/ic_tab_mine_active.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
src/static/images/ic_tab_mine_normal.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
src/static/images/ic_tab_qrcode_active.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
src/static/images/ic_tab_qrcode_normal.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
src/static/images/logo.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
4
src/static/styles/common.scss
Normal file
@@ -0,0 +1,4 @@
|
||||
page {
|
||||
font-size: 28rpx;
|
||||
background-color: #f9f9f8;
|
||||
}
|
20
src/store/index.ts
Normal 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;
|
58
src/store/modules/app/index.ts
Normal 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;
|
3
src/store/modules/app/types.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface AppState {
|
||||
systemInfo: UniApp.GetSystemInfoResult;
|
||||
}
|
82
src/store/modules/user/index.ts
Normal 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;
|
16
src/store/modules/user/types.ts
Normal 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
@@ -0,0 +1 @@
|
||||
@import 'uview-plus/theme.scss';
|
15
src/utils/auth/index.ts
Normal 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
@@ -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
@@ -0,0 +1,4 @@
|
||||
export * from './auth';
|
||||
export * from './common';
|
||||
export * from './modals';
|
||||
export * from './request';
|
81
src/utils/modals/index.ts
Normal 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,
|
||||
});
|
||||
});
|
||||
}
|
42
src/utils/request/index.ts
Normal 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;
|
104
src/utils/request/interceptors.ts
Normal 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 };
|
41
src/utils/request/status.ts
Normal 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},请检查网络或联系管理员!`;
|
||||
};
|
7
src/utils/request/type.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
// 返回res.data的interface
|
||||
export interface IResponse<T = any> {
|
||||
code: number | string;
|
||||
result: T;
|
||||
message: string;
|
||||
status: string | number;
|
||||
}
|