20
env/.env
vendored
@ -1,15 +1,25 @@
|
||||
VITE_APP_TITLE = 'unibest'
|
||||
VITE_APP_TITLE = '芋道管理系统'
|
||||
VITE_APP_PORT = 9000
|
||||
|
||||
VITE_UNI_APPID = 'H57F2ACE4'
|
||||
VITE_WX_APPID = 'wxa2abb91f64032a2b'
|
||||
VITE_WX_APPID = 'wx90bcb2127c1d8720'
|
||||
|
||||
# h5部署网站的base,配置到 manifest.config.ts 里的 h5.router.base
|
||||
VITE_APP_PUBLIC_BASE=/unibest/
|
||||
|
||||
VITE_SERVER_BASEURL = 'https://ukw0y1.laf.run'
|
||||
VITE_UPLOAD_BASEURL = 'https://ukw0y1.laf.run/upload'
|
||||
VITE_SERVER_BASEURL = 'http://localhost:48080/admin-api'
|
||||
VITE_UPLOAD_BASEURL = 'http://localhost:48080/upload'
|
||||
|
||||
# h5是否需要配置代理
|
||||
VITE_APP_PROXY=false
|
||||
VITE_APP_PROXY_PREFIX = '/api'
|
||||
VITE_APP_PROXY_PREFIX = '/admin-api'
|
||||
|
||||
|
||||
# 租户开关
|
||||
VITE_APP_TENANT_ENABLE=true
|
||||
# 验证码的开关
|
||||
VITE_APP_CAPTCHA_ENABLE=true
|
||||
# 默认账户密码
|
||||
VITE_APP_DEFAULT_LOGIN_TENANT = 芋道源码
|
||||
VITE_APP_DEFAULT_LOGIN_USERNAME = admin
|
||||
VITE_APP_DEFAULT_LOGIN_PASSWORD = admin123
|
@ -107,6 +107,7 @@
|
||||
"@esbuild/darwin-arm64": "0.20.2",
|
||||
"@esbuild/darwin-x64": "0.20.2",
|
||||
"@iconify-json/carbon": "^1.1.35",
|
||||
"@iconify-json/ic": "^1.2.1",
|
||||
"@rollup/rollup-darwin-x64": "^4.18.0",
|
||||
"@types/node": "^20.14.2",
|
||||
"@types/wechat-miniprogram": "^3.4.7",
|
||||
|
@ -30,28 +30,34 @@ export default defineUniPages({
|
||||
list: [
|
||||
// 注意tabbar路由需要使用 layout:tabbar 布局
|
||||
{
|
||||
pagePath: 'pages/index/index',
|
||||
text: '首页',
|
||||
icon: 'home',
|
||||
iconType: 'wot',
|
||||
},
|
||||
{
|
||||
pagePath: 'pages/about/about',
|
||||
text: '关于',
|
||||
icon: 'i-carbon-code',
|
||||
pagePath: 'pages/message/index',
|
||||
text: '消息',
|
||||
icon: 'i-ic-outline-message',
|
||||
iconType: 'unocss',
|
||||
},
|
||||
{
|
||||
pagePath: 'pages/colab/index',
|
||||
text: '协作',
|
||||
icon: 'i-ic-outline-handshake',
|
||||
iconType: 'unocss',
|
||||
},
|
||||
{
|
||||
pagePath: 'pages/work/index',
|
||||
text: '工作台',
|
||||
icon: 'i-ic-baseline-apps',
|
||||
iconType: 'unocss',
|
||||
},
|
||||
{
|
||||
pagePath: 'pages/contacts/index',
|
||||
text: '通讯录',
|
||||
icon: 'i-ic-baseline-contact-page',
|
||||
iconType: 'unocss',
|
||||
},
|
||||
// {
|
||||
// pagePath: 'pages/my/index',
|
||||
// text: '我的',
|
||||
// icon: '/static/logo.svg',
|
||||
// iconType: 'local',
|
||||
// },
|
||||
{
|
||||
pagePath: 'pages/my/index',
|
||||
text: '我的',
|
||||
icon: 'iconfont icon-my',
|
||||
iconType: 'iconfont',
|
||||
icon: 'i-ic-baseline-person',
|
||||
iconType: 'unocss',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
10
pnpm-lock.yaml
generated
@ -78,6 +78,9 @@ importers:
|
||||
'@iconify-json/carbon':
|
||||
specifier: ^1.1.35
|
||||
version: 1.1.35
|
||||
'@iconify-json/ic':
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1
|
||||
'@rollup/rollup-darwin-x64':
|
||||
specifier: ^4.18.0
|
||||
version: 4.18.0
|
||||
@ -1185,6 +1188,9 @@ packages:
|
||||
'@iconify-json/carbon@1.1.35':
|
||||
resolution: {integrity: sha512-zKqioWceqFRiLJvxpjcCpVP3j2YcokYshlbwSAHBhOih5XNUymUS3hm1kpV4KljMI1xWH96UcozHaaf6x4YzdA==}
|
||||
|
||||
'@iconify-json/ic@1.2.1':
|
||||
resolution: {integrity: sha512-UjL/bjJP/T5EV881+hTzcfTKVo0KEUjhnMiJcLtPzNgPtU2KZZmRx8BSKKR61H4CN/5FTEbyawGyG0aEt3SwGQ==}
|
||||
|
||||
'@iconify/types@2.0.0':
|
||||
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
|
||||
|
||||
@ -6887,6 +6893,10 @@ snapshots:
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
|
||||
'@iconify-json/ic@1.2.1':
|
||||
dependencies:
|
||||
'@iconify/types': 2.0.0
|
||||
|
||||
'@iconify/types@2.0.0': {}
|
||||
|
||||
'@iconify/utils@2.1.24':
|
||||
|
@ -13,6 +13,10 @@ onHide(() => {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
page {
|
||||
// 默认背景颜色
|
||||
background: #f2f3f7;
|
||||
}
|
||||
/* stylelint-disable selector-type-no-unknown */
|
||||
button::after {
|
||||
border: none;
|
||||
|
@ -36,7 +36,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
// unocss icon 默认不生效,需要在这里写一遍才能生效!注释掉也是生效的,但是必须要有!
|
||||
// i-carbon-code
|
||||
// i-ic-outline-message i-ic-outline-handshake i-ic-baseline-apps i-ic-baseline-contact-page i-ic-baseline-person
|
||||
import { tabBar } from '@/pages.json'
|
||||
import { tabbarStore } from './tabbar'
|
||||
|
||||
|
@ -1,8 +1,11 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import qs from 'qs'
|
||||
import { useUserStore } from '@/store'
|
||||
import { platform } from '@/utils/platform'
|
||||
import { getEvnBaseUrl } from '@/utils'
|
||||
import { getAccessToken, getTenantId } from '@/utils/auth'
|
||||
const tenantEnable = import.meta.env.VITE_APP_TENANT_ENABLE
|
||||
|
||||
const whiteList: string[] = ['/login', '/refresh-token', '/system/tenant/get-id-by-name']
|
||||
|
||||
export type CustomRequestOptions = UniApp.RequestOptions & {
|
||||
query?: Record<string, any>
|
||||
@ -50,10 +53,24 @@ const httpInterceptor = {
|
||||
...options.header,
|
||||
}
|
||||
// 3. 添加 token 请求头标识
|
||||
const userStore = useUserStore()
|
||||
const { token } = userStore.userInfo as unknown as IUserInfo
|
||||
if (token) {
|
||||
options.header.Authorization = `Bearer ${token}`
|
||||
// const userStore = useUserStore()
|
||||
// const { token } = userStore.userInfo as unknown as IUserInfo
|
||||
// if (token) {
|
||||
// options.header.Authorization = `Bearer ${token}`
|
||||
// }
|
||||
|
||||
let isToken = (options!.header || {}).isToken === false
|
||||
isToken = whiteList.some((allowUrl) => options!.url.indexOf(allowUrl) !== -1)
|
||||
|
||||
if (!isToken && getAccessToken()) {
|
||||
// 能够获取的到accessToken并且不是白名单
|
||||
options.header.Authorization = `Bearer ${getAccessToken()}`
|
||||
}
|
||||
|
||||
// 4. 添加租户标识
|
||||
if (tenantEnable && tenantEnable === 'true') {
|
||||
const tenantId = getTenantId()
|
||||
if (tenantId) options.header['tenant-id'] = tenantId
|
||||
}
|
||||
},
|
||||
}
|
||||
|
@ -3,24 +3,46 @@
|
||||
* 路由拦截,通常也是登录拦截
|
||||
* 可以设置路由白名单,或者黑名单,看业务需要选哪一个
|
||||
* 我这里应为大部分都可以随便进入,所以使用黑名单
|
||||
*
|
||||
* update 2024-10-09
|
||||
* 登录之后,会进行页面跳转。在完成页面跳转之前,就需要获取到用户信息
|
||||
* 并设置到 userStore 中。
|
||||
*
|
||||
* 如何设置当前页面需要校验登录状态?
|
||||
* 在 router 块中设置
|
||||
*
|
||||
* <route lang="json5">
|
||||
{
|
||||
layout: 'tabbar',
|
||||
style: {
|
||||
navigationStyle: 'custom',
|
||||
navigationBarTitleText: '首页',
|
||||
},
|
||||
needLogin: true
|
||||
}
|
||||
</route>
|
||||
*/
|
||||
import { useUserStore } from '@/store'
|
||||
import { getNeedLoginPages, needLoginPages as _needLoginPages } from '@/utils'
|
||||
import { getAccessToken } from '@/utils/auth'
|
||||
|
||||
// TODO Check
|
||||
const loginRoute = '/pages/login/index'
|
||||
|
||||
const isLogined = () => {
|
||||
const userStore = useUserStore()
|
||||
return userStore.isLogined
|
||||
}
|
||||
// const isSetUserInfo = () => {
|
||||
// const userStore = useUserStore()
|
||||
// return userStore.userInfo.isSetUser
|
||||
// }
|
||||
|
||||
const isDev = import.meta.env.DEV
|
||||
|
||||
// 黑名单登录拦截器 - (适用于大部分页面不需要登录,少部分页面需要登录)
|
||||
const navigateToInterceptor = {
|
||||
// 注意,这里的url是 '/' 开头的,如 '/pages/index/index',跟 'pages.json' 里面的 path 不同
|
||||
invoke({ url }: { url: string }) {
|
||||
// 拦截前触发
|
||||
async invoke(option) {
|
||||
const { url } = option
|
||||
console.log('navigateToInterceptor invoke')
|
||||
// console.log(url) // /pages/route-interceptor/index?name=feige&age=30
|
||||
const path = url.split('?')[0]
|
||||
let needLoginPages: string[] = []
|
||||
@ -31,16 +53,36 @@ const navigateToInterceptor = {
|
||||
needLoginPages = _needLoginPages
|
||||
}
|
||||
const isNeedLogin = needLoginPages.includes(path)
|
||||
// console.log('the path: {} is needLogin? {}', path, isNeedLogin)
|
||||
if (!isNeedLogin) {
|
||||
return true
|
||||
// 当前页面不需要登录
|
||||
console.log('the path: {} is not needLogin', url)
|
||||
return option
|
||||
}
|
||||
const hasLogin = isLogined()
|
||||
if (hasLogin) {
|
||||
return true
|
||||
|
||||
// 下面的逻辑跟 PC 端差不多
|
||||
if (getAccessToken()) {
|
||||
if (path === '/pages/login/index') {
|
||||
// 这里写死,避免在 login 页面中设置了 needLogin,导致死循环
|
||||
return option
|
||||
} else {
|
||||
// todo 获取字典数据并保存到本地
|
||||
|
||||
// 获取用户的相关信息
|
||||
const userStore = useUserStore()
|
||||
if (!userStore.userInfo.isSetUser) {
|
||||
// todo 显示加载状态
|
||||
await userStore.setUserInfoAction()
|
||||
// todo 菜单权限应该也需从这里进行逻辑上的设置
|
||||
} else {
|
||||
return option
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 当前页面需要登录,但是获取不到token,这个时候跳转到登录页面
|
||||
uni.navigateTo({ url: '/pages/login/index' })
|
||||
return option
|
||||
}
|
||||
const redirectRoute = `${loginRoute}?redirect=${encodeURIComponent(url)}`
|
||||
uni.navigateTo({ url: redirectRoute })
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "unibest",
|
||||
"name": "芋道管理系统",
|
||||
"appid": "H57F2ACE4",
|
||||
"description": "",
|
||||
"versionName": "1.0.0",
|
||||
@ -83,7 +83,7 @@
|
||||
},
|
||||
"quickapp": {},
|
||||
"mp-weixin": {
|
||||
"appid": "wxa2abb91f64032a2b",
|
||||
"appid": "wx90bcb2127c1d8720",
|
||||
"setting": {
|
||||
"urlCheck": false
|
||||
},
|
||||
|
@ -25,41 +25,76 @@
|
||||
"spacing": "3px",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/index/index",
|
||||
"text": "首页",
|
||||
"icon": "home",
|
||||
"iconType": "wot"
|
||||
"pagePath": "pages/message/index",
|
||||
"text": "消息",
|
||||
"icon": "i-ic-outline-message",
|
||||
"iconType": "unocss"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/about/about",
|
||||
"text": "关于",
|
||||
"icon": "i-carbon-code",
|
||||
"pagePath": "pages/colab/index",
|
||||
"text": "协作",
|
||||
"icon": "i-ic-outline-handshake",
|
||||
"iconType": "unocss"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/work/index",
|
||||
"text": "工作台",
|
||||
"icon": "i-ic-baseline-apps",
|
||||
"iconType": "unocss"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/contacts/index",
|
||||
"text": "通讯录",
|
||||
"icon": "i-ic-baseline-contact-page",
|
||||
"iconType": "unocss"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/my/index",
|
||||
"text": "我的",
|
||||
"icon": "iconfont icon-my",
|
||||
"iconType": "iconfont"
|
||||
"icon": "i-ic-baseline-person",
|
||||
"iconType": "unocss"
|
||||
}
|
||||
]
|
||||
},
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"path": "pages/work/index",
|
||||
"type": "home",
|
||||
"layout": "tabbar",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "首页"
|
||||
"navigationBarTitleText": "工作台"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/about/about",
|
||||
"path": "pages/colab/index",
|
||||
"type": "page",
|
||||
"layout": "tabbar",
|
||||
"style": {
|
||||
"navigationBarTitleText": "关于"
|
||||
"navigationBarTitleText": "协作"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/contacts/index",
|
||||
"type": "page",
|
||||
"layout": "tabbar",
|
||||
"style": {
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "通讯录"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/login/index",
|
||||
"type": "page",
|
||||
"style": {
|
||||
"navigationBarTitleText": "登录页面"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/message/index",
|
||||
"type": "page",
|
||||
"layout": "tabbar",
|
||||
"style": {
|
||||
"navigationBarTitleText": "消息"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -1,37 +0,0 @@
|
||||
<route lang="json5">
|
||||
{
|
||||
layout: 'tabbar',
|
||||
style: {
|
||||
navigationBarTitleText: '关于',
|
||||
},
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<view
|
||||
class="bg-white overflow-hidden pt-2 px-4"
|
||||
:style="{ marginTop: safeAreaInsets?.top + 'px' }"
|
||||
>
|
||||
<view class="text-center text-3xl mt-8">
|
||||
鸽友们好,我是
|
||||
<text class="text-red-500">菲鸽</text>
|
||||
</view>
|
||||
<RequestComp />
|
||||
<UploadComp />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import RequestComp from './components/request.vue'
|
||||
import UploadComp from './components/upload.vue'
|
||||
|
||||
// 获取屏幕边界到安全区域距离
|
||||
const { safeAreaInsets } = uni.getSystemInfoSync()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.test-css {
|
||||
// mt-4=>1rem=>16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
@ -1,56 +0,0 @@
|
||||
<route lang="json5">
|
||||
{
|
||||
layout: 'demo',
|
||||
style: {
|
||||
navigationBarTitleText: '请求',
|
||||
},
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<view class="p-6 text-center">
|
||||
<view class="my-2">使用的是 laf 云后台</view>
|
||||
<view class="text-green-400">我的推荐码,可以获得佣金</view>
|
||||
|
||||
<!-- #ifdef H5 -->
|
||||
<view class="my-2">
|
||||
<a class="my-2" :href="recommendUrl" target="_blank">{{ recommendUrl }}</a>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- #ifndef H5 -->
|
||||
<view class="my-2 text-left text-sm">{{ recommendUrl }}</view>
|
||||
<!-- #endif -->
|
||||
|
||||
<!-- http://localhost:9000/#/pages/index/request -->
|
||||
<wd-button @click="run" class="my-6">发送请求</wd-button>
|
||||
<view class="h-12">
|
||||
<view v-if="loading">loading...</view>
|
||||
<block v-else>
|
||||
<view class="text-xl">请求数据如下</view>
|
||||
<view class="text-green leading-8">{{ JSON.stringify(data) }}</view>
|
||||
</block>
|
||||
</view>
|
||||
<wd-button type="error" @click="reset" class="my-6" :disabled="!data">重置数据</wd-button>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { getFooAPI, postFooAPI, IFooItem } from '@/service/index/foo'
|
||||
|
||||
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,
|
||||
})
|
||||
const reset = () => {
|
||||
data.value = initialData
|
||||
}
|
||||
</script>
|
@ -1,30 +0,0 @@
|
||||
<route lang="json5" type="page">
|
||||
{
|
||||
layout: 'default',
|
||||
style: {
|
||||
navigationBarTitleText: '上传-状态一体化',
|
||||
},
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<view class="p-4 text-center">
|
||||
<wd-button @click="run">选择图片并上传</wd-button>
|
||||
<view v-if="loading" class="text-blue h-10">上传...</view>
|
||||
<template v-else>
|
||||
<view class="m-2">上传后返回的接口数据:</view>
|
||||
<view class="m-2">{{ data }}</view>
|
||||
<view class="h-80 w-full">
|
||||
<image v-if="data" :src="data || data" mode="scaleToFill" />
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
const { loading, data, run } = useUpload({ user: '菲鸽' })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
//
|
||||
</style>
|
23
src/pages/colab/index.vue
Normal file
@ -0,0 +1,23 @@
|
||||
<!-- 协作界面:用于 bpm 工作流 -->
|
||||
<route lang="json5" type="page">
|
||||
{
|
||||
layout: 'tabbar',
|
||||
style: {
|
||||
navigationBarTitleText: '协作',
|
||||
},
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<view class="h-full w-full flex justify-center items-center">
|
||||
<wd-status-tip image="/static/images/empty.png" tip="功能暂未开放" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
//
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
//
|
||||
</style>
|
41
src/pages/contacts/components/ContactsItem.vue
Normal file
@ -0,0 +1,41 @@
|
||||
<!-- 通讯录 item -->
|
||||
<template>
|
||||
<view class="flex justify-between w-full">
|
||||
<!-- 头像 -->
|
||||
<view class="flex justify-center items-center w-100rpx">
|
||||
<image
|
||||
class="w-80rpx h-80rpx rounded-5px overflow-hidden"
|
||||
:src="props.item.avatar ? props.item.avatar : '/static/images/contacts.png'"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
</view>
|
||||
<!-- 右侧信息 -->
|
||||
<view
|
||||
class="h-100rpx grow-1 flex flex-col justify-center border-solid border-b-[0.5px] border-x-0 border-t-0 border-[#E3E2E2]"
|
||||
>
|
||||
<!-- 如果是个人 -->
|
||||
<view v-if="props.item.isLeaf">
|
||||
<view class="text-24rpx">{{ props.item.name }}</view>
|
||||
<view class="text-22rpx">
|
||||
{{ props.item.post }}
|
||||
</view>
|
||||
</view>
|
||||
<view v-else>
|
||||
<view class="text-24rpx">
|
||||
{{ props.item.name }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
//
|
||||
import { defineProps } from 'vue'
|
||||
|
||||
const props = defineProps(['item'])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
//
|
||||
</style>
|
114
src/pages/contacts/index.vue
Normal file
@ -0,0 +1,114 @@
|
||||
<!-- 通讯录界面:用于 im 聊天 -->
|
||||
<route lang="json5" type="page">
|
||||
{
|
||||
layout: 'tabbar',
|
||||
style: {
|
||||
navigationStyle: 'custom',
|
||||
navigationBarTitleText: '通讯录',
|
||||
},
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<!-- 全部的背景 -->
|
||||
<view class="">
|
||||
<!-- 安全位置插入 -->
|
||||
<view class="overflow-hidden" :style="{ marginTop: safeAreaInsets?.top + 'px' }">
|
||||
<!-- 自定义顶部的导航栏 -->
|
||||
<view class="h-80rpx flex justify-between items-center px-2 bg-[#F8F8F8]">
|
||||
<view class="w-50rpx">
|
||||
<view
|
||||
class="i-ic-baseline-keyboard-arrow-left"
|
||||
v-show="historyStack.length > 1"
|
||||
@click="handleLeftArrow"
|
||||
></view>
|
||||
</view>
|
||||
<view class="text-22rpx grow-1 text-center">通讯录</view>
|
||||
<view class="w-50rpx"></view>
|
||||
</view>
|
||||
|
||||
<!-- 自定义间距 -->
|
||||
<view class="bg-[#f2f3f7] h-2 w-full"></view>
|
||||
<!-- 消息列表 -->
|
||||
<view class="flex flex-col bg-white p-2">
|
||||
<view v-for="(item, index) in treeList" :key="index" @click="handleClick(item)">
|
||||
<ContactsItem :item="item" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
//
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import ContactsItem from './components/ContactsItem.vue'
|
||||
import contactsTree from './mock'
|
||||
// 提示组件
|
||||
const toast = useToast()
|
||||
|
||||
defineOptions({
|
||||
name: 'Contacts',
|
||||
})
|
||||
|
||||
// 获取屏幕边界到安全区域距离
|
||||
const { safeAreaInsets } = uni.getSystemInfoSync()
|
||||
|
||||
// 搜索树的子节点
|
||||
const searchTree = (tree, targetId) => {
|
||||
console.log('targetId', targetId)
|
||||
console.log('tree', tree)
|
||||
if (!targetId) {
|
||||
return tree
|
||||
}
|
||||
|
||||
let queue = [...tree]
|
||||
let result = []
|
||||
while (queue.length) {
|
||||
const node = queue.shift()
|
||||
if (node.id === targetId) {
|
||||
result = node.children
|
||||
break
|
||||
}
|
||||
if (node.children) {
|
||||
queue = queue.concat(node.children)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const treeList = ref({})
|
||||
|
||||
const handleClick = (item) => {
|
||||
console.log('展示当前的item', item.id)
|
||||
|
||||
if (item.isLeaf) {
|
||||
toast.info('功能尚未开放')
|
||||
} else {
|
||||
historyStack.value.push(item.id)
|
||||
treeList.value = searchTree(contactsTree, item.id)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLeftArrow = () => {
|
||||
if (historyStack.value.length > 1) {
|
||||
historyStack.value.pop()
|
||||
}
|
||||
const id = historyStack.value.at(-1)
|
||||
treeList.value = searchTree(contactsTree, id)
|
||||
}
|
||||
|
||||
// 历史栈
|
||||
const historyStack = ref([])
|
||||
|
||||
onLoad((_option) => {
|
||||
// 默认展示第一层级
|
||||
historyStack.value.push(undefined)
|
||||
treeList.value = searchTree(contactsTree, undefined)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
//
|
||||
</style>
|
70
src/pages/contacts/mock.js
Normal file
@ -0,0 +1,70 @@
|
||||
export default [
|
||||
{
|
||||
id: 1,
|
||||
name: '芋道集团',
|
||||
isLeaf: false,
|
||||
children: [
|
||||
{
|
||||
id: 11,
|
||||
parentId: 1,
|
||||
name: 'IT中心',
|
||||
isLeaf: false,
|
||||
children: [
|
||||
{
|
||||
id: 111,
|
||||
parentId: 11,
|
||||
name: '马青楷',
|
||||
post: 'CTO',
|
||||
avatar: '/static/images/avatar3.jpg',
|
||||
isLeaf: true,
|
||||
},
|
||||
{
|
||||
id: 112,
|
||||
parentId: 11,
|
||||
name: '王伟',
|
||||
post: '前端开发',
|
||||
avatar: '/static/images/avatar2.jpg',
|
||||
isLeaf: true,
|
||||
},
|
||||
{
|
||||
id: 113,
|
||||
parentId: 11,
|
||||
name: '王博',
|
||||
post: '技术主管',
|
||||
avatar: '/static/images/avatar1.jpg',
|
||||
isLeaf: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 12,
|
||||
parentId: 1,
|
||||
name: '行政部门',
|
||||
isLeaf: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '中软集团',
|
||||
isLeaf: false,
|
||||
children: [
|
||||
{
|
||||
id: 21,
|
||||
parentId: 2,
|
||||
name: '中软IT',
|
||||
isLeaf: false,
|
||||
children: [
|
||||
{
|
||||
id: 211,
|
||||
parentId: 21,
|
||||
name: '马青楷',
|
||||
post: 'CTO',
|
||||
isLeaf: true,
|
||||
avatar: '/static/images/avatar3.jpg',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
@ -1,57 +0,0 @@
|
||||
<!-- 使用 type="home" 属性设置首页,其他页面不需要设置,默认为page;推荐使用json5,更强大,且允许注释 -->
|
||||
<route lang="json5" type="home">
|
||||
{
|
||||
layout: 'tabbar',
|
||||
style: {
|
||||
navigationStyle: 'custom',
|
||||
navigationBarTitleText: '首页',
|
||||
},
|
||||
}
|
||||
</route>
|
||||
<template>
|
||||
<view
|
||||
class="bg-white overflow-hidden pt-2 px-4"
|
||||
:style="{ marginTop: safeAreaInsets?.top + 'px' }"
|
||||
>
|
||||
<view class="mt-12">
|
||||
<image src="/static/logo.svg" alt="" class="w-28 h-28 block mx-auto" />
|
||||
</view>
|
||||
<view class="text-center text-4xl main-title-color mt-4">unibest</view>
|
||||
<view class="text-center text-2xl mt-2 mb-8">最好用的 uniapp 开发模板</view>
|
||||
|
||||
<view class="text-justify max-w-100 m-auto text-4 indent mb-2">{{ description }}</view>
|
||||
<view class="text-center mt-8">
|
||||
当前平台是:
|
||||
<text class="text-green-500">{{ PLATFORM.platform }}</text>
|
||||
</view>
|
||||
<view class="text-center mt-4">
|
||||
模板分支是:
|
||||
<text class="text-green-500">tabbar</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import PLATFORM from '@/utils/platform'
|
||||
|
||||
defineOptions({
|
||||
name: 'Home',
|
||||
})
|
||||
|
||||
// 获取屏幕边界到安全区域距离
|
||||
const { safeAreaInsets } = uni.getSystemInfoSync()
|
||||
const author = ref('菲鸽')
|
||||
const description = ref(
|
||||
'unibest 是一个集成了多种工具和技术的 uniapp 开发模板,由 uniapp + Vue3 + Ts + Vite4 + UnoCss + UniUI + VSCode 构建,模板具有代码提示、自动格式化、统一配置、代码片段等功能,并内置了许多常用的基本组件和基本功能,让你编写 uniapp 拥有 best 体验。',
|
||||
)
|
||||
// 测试 uni API 自动引入
|
||||
onLoad(() => {
|
||||
console.log(author)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.main-title-color {
|
||||
color: #d14328;
|
||||
}
|
||||
</style>
|
146
src/pages/login/components/LoginForm.vue
Normal file
@ -0,0 +1,146 @@
|
||||
<!-- 账号密码登录 -->
|
||||
<template>
|
||||
<view class="mt-4">
|
||||
<view>
|
||||
<wd-form ref="gennerateForm" :model="loginData.loginForm" class="">
|
||||
<view class="flex flex-col items-center gap-4">
|
||||
<view
|
||||
class="rounded-xl p-10rpx border-2rpx border-blue-400 border-solid"
|
||||
:class="{
|
||||
'border-4rpx border-blue-500 p-8rpx': focusInput === 'tenantName',
|
||||
}"
|
||||
>
|
||||
<wd-input
|
||||
v-if="loginData.tenantEnable"
|
||||
type="text"
|
||||
placeholder="请输入租户名称"
|
||||
v-model="loginData.loginForm.tenantName"
|
||||
custom-class="w-620rpx"
|
||||
:no-border="true"
|
||||
@focus="() => handleInputFocus('tenantName')"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="rounded-xl p-10rpx border-2rpx border-blue-400 border-solid"
|
||||
:class="{
|
||||
'border-4rpx border-blue-500 p-8rpx': focusInput === 'username',
|
||||
}"
|
||||
>
|
||||
<wd-input
|
||||
type="text"
|
||||
v-model="loginData.loginForm.username"
|
||||
placeholder="请输入账号"
|
||||
custom-class="w-620rpx"
|
||||
:no-border="true"
|
||||
@focus="() => handleInputFocus('username')"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="rounded-xl p-10rpx border-2rpx border-blue-400 border-solid"
|
||||
:class="{
|
||||
'border-4rpx border-blue-500 p-8rpx': focusInput === 'password',
|
||||
}"
|
||||
>
|
||||
<wd-input
|
||||
type="password"
|
||||
v-model="loginData.loginForm.password"
|
||||
placeholder="请输入密码"
|
||||
custom-class="w-620rpx"
|
||||
:no-border="true"
|
||||
@focus="() => handleInputFocus('password')"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</wd-form>
|
||||
</view>
|
||||
<view class="w-620rpx mx-auto">
|
||||
<wd-button custom-class="mt-8" type="primary" block @click="handleLogin">
|
||||
<text>登录</text>
|
||||
</wd-button>
|
||||
</view>
|
||||
|
||||
<!-- 提示组件 -->
|
||||
<wd-toast />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useToast } from 'wot-design-uni'
|
||||
import { login, getTenantIdByName } from '@/service/login/LoginAPI'
|
||||
import { useUserStore } from '@/store'
|
||||
import * as authUtil from '@/utils/auth'
|
||||
|
||||
// 提示组件 TODO @ Qiksy 讨论:// 提示组件 这种,我们要不放在 useToast(),作为尾注释,简洁一点。
|
||||
const toast = useToast()
|
||||
|
||||
const focusInput = ref('')
|
||||
const handleInputFocus = (type: string) => {
|
||||
focusInput.value = type
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
agree: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const loginData = reactive({
|
||||
isShowPassword: false,
|
||||
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
|
||||
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
|
||||
loginForm: {
|
||||
tenantName: import.meta.env.VITE_APP_DEFAULT_LOGIN_TENANT || '',
|
||||
username: import.meta.env.VITE_APP_DEFAULT_LOGIN_USERNAME || '',
|
||||
password: import.meta.env.VITE_APP_DEFAULT_LOGIN_PASSWORD || '',
|
||||
captchaVerification: '',
|
||||
rememberMe: true, // 默认记录我。如果不需要,可手动修改
|
||||
},
|
||||
})
|
||||
|
||||
// 获取租户 ID // TODO @ Qiksy 讨论:这个方法注释,要不用 /** 获取租户 ID */ 高亮的更明显哈。
|
||||
const getTenantId = async () => {
|
||||
if (loginData.tenantEnable === 'true') {
|
||||
const res = (await getTenantIdByName(loginData.loginForm.tenantName)) as string
|
||||
authUtil.setTenantId(res)
|
||||
}
|
||||
}
|
||||
|
||||
const handleLogin = async () => {
|
||||
console.log('账号密码登录')
|
||||
focusInput.value = ''
|
||||
if (!props.agree) {
|
||||
toast.warning('请阅读并同意《用户协议》和《隐私政策》')
|
||||
return
|
||||
}
|
||||
|
||||
// 1. 先根据租户名,获取 tenantId
|
||||
await getTenantId()
|
||||
|
||||
// todo 校验form数据
|
||||
const res = await login(loginData.loginForm)
|
||||
if (!res) {
|
||||
return
|
||||
}
|
||||
|
||||
authUtil.setToken(res)
|
||||
|
||||
// 获取用户信息,保存到 store
|
||||
const userStore = useUserStore()
|
||||
await userStore.setUserInfoAction()
|
||||
|
||||
// 暂时先跳到首页
|
||||
uni.switchTab({
|
||||
url: '/pages/work/index',
|
||||
})
|
||||
console.log('登录成功')
|
||||
|
||||
// todo 判断重定向
|
||||
|
||||
// todo 处理页面loading展示
|
||||
}
|
||||
</script>
|
74
src/pages/login/index.vue
Normal file
@ -0,0 +1,74 @@
|
||||
<route lang="json5" type="page">
|
||||
{
|
||||
style: {
|
||||
navigationBarTitleText: '登录页面',
|
||||
},
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<view class="relative h-full">
|
||||
<!-- banner区域 -->
|
||||
<view class="absolute w-full h-480rpx">
|
||||
<view class="color-white pt-[116rpx] pl-[68rpx] z-10">
|
||||
<view><text class="text-[64rpx] font-700">你好</text></view>
|
||||
<view class="mt-[36rpx]"><text class="text-[36rpx]">欢迎登录芋道快速开发平台</text></view>
|
||||
</view>
|
||||
<!-- TODO @芋艿:后续把静态资源,放到 CDN -->
|
||||
<image
|
||||
class="absolute w-full top-0 -z-1"
|
||||
src="/static/images/login-bg.png"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
</view>
|
||||
<!-- 主要容器区域 -->
|
||||
<!-- 430rpx 是上面banner的高度,50rpx是留给底部的空间 -->
|
||||
<view
|
||||
class="top-[430rpx] h-[calc(100vh-430rpx-50rpx)] w-full rounded-t-2xl bg-white flex flex-col items-center absolute px-4 box-border"
|
||||
>
|
||||
<!-- tabs 切换 -->
|
||||
<wd-tabs v-model="tab" custom-class="h-full">
|
||||
<block :key="0">
|
||||
<wd-tab title="手机号登录">手机号登录</wd-tab>
|
||||
</block>
|
||||
<block :key="1">
|
||||
<wd-tab title="密码登录">
|
||||
<LoginForm :agree="agree" />
|
||||
</wd-tab>
|
||||
</block>
|
||||
</wd-tabs>
|
||||
|
||||
<!-- 隐私与用户条款 -->
|
||||
<view class="text-[14px] text-[#999] mt-4 h-50rpx flex justify-center">
|
||||
<wd-checkbox class="inline-block" v-model="agree"></wd-checkbox>
|
||||
<text>已阅读并同意</text>
|
||||
<text class="text-[#0B5EFF]">《用户协议》</text>
|
||||
<text>和</text>
|
||||
<text class="text-[#0B5EFF]">《隐私政策》</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import LoginForm from '@/pages/login/components/LoginForm.vue'
|
||||
import { ref, reactive } from 'vue'
|
||||
|
||||
//
|
||||
|
||||
const tab = ref(1) // tabs的索引
|
||||
const agree = ref(false) // 是否同意协议
|
||||
|
||||
onLoad(() => {
|
||||
//
|
||||
})
|
||||
|
||||
onMounted(() => {})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
//
|
||||
.login-bg {
|
||||
background-image: url('/static/images/login-bg.png');
|
||||
}
|
||||
</style>
|
44
src/pages/message/components/MessageItem.vue
Normal file
@ -0,0 +1,44 @@
|
||||
<!-- 消息列表组件 -->
|
||||
<template>
|
||||
<view class="w-full flex justify-between">
|
||||
<view class="w-100rpx h-100rpx flex items-center justify-center">
|
||||
<image
|
||||
class="w-80rpx h-80rpx rounded-5px overflow-hidden"
|
||||
:src="props.item.avatar"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
</view>
|
||||
<view
|
||||
class="h-100rpx grow-1 ml-2 flex justify-between py-1 border-b-[0.5px] border-solid border-[#E3E2E2] border-x-0 border-t-0 box-border"
|
||||
>
|
||||
<view class="flex flex-col justify-between grow-1">
|
||||
<view>
|
||||
{{ props.item.title }}
|
||||
</view>
|
||||
<view class="overflow-hidden truncate text-20rpx w-500rpx">
|
||||
{{ props.item.content }}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="flex flex-col justify-between">
|
||||
<view class="text-20rpx text-[#999] truncate">
|
||||
{{ props.item.time }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
//
|
||||
const props = defineProps({
|
||||
item: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
//
|
||||
</style>
|
41
src/pages/message/index.vue
Normal file
@ -0,0 +1,41 @@
|
||||
<!-- 消息界面:用于 im 聊天 -->
|
||||
<route lang="json5" type="page">
|
||||
{
|
||||
layout: 'tabbar',
|
||||
style: {
|
||||
navigationBarTitleText: '消息',
|
||||
},
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<!-- 搜索框 -->
|
||||
<wd-search
|
||||
v-model="searchValue"
|
||||
@focus="searchFocus"
|
||||
@blur="searchBlur"
|
||||
@search="search"
|
||||
maxlength="10"
|
||||
hide-cancel
|
||||
light
|
||||
/>
|
||||
<view class="px-4 pb-4 pt-0 flex flex-col gap-2">
|
||||
<MessageItem v-for="(item, index) in messageList" :key="index" :item="item" />
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
//
|
||||
import { ref } from 'vue'
|
||||
import MessageItem from './components/MessageItem.vue'
|
||||
import messageList from './mock'
|
||||
const searchValue = ref('')
|
||||
/// /////// 搜索栏 事件////////////
|
||||
const searchFocus = () => {}
|
||||
const searchBlur = () => {}
|
||||
const search = () => {}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
//
|
||||
</style>
|
92
src/pages/message/mock.js
Normal file
@ -0,0 +1,92 @@
|
||||
export default [
|
||||
{
|
||||
title: 'IT中心工作群',
|
||||
content: '今天下午2点到5点,开部门会议,请各位提前准备好汇报材料。',
|
||||
time: '昨天 15:30',
|
||||
avatar: '/static/images/avatar1.jpg',
|
||||
},
|
||||
{
|
||||
title: '产品设计讨论组',
|
||||
content: '关于新功能的设计稿,大家有什么建议可以在群里提出来。',
|
||||
time: '昨天 18:45',
|
||||
avatar: '/static/images/avatar2.jpg',
|
||||
},
|
||||
{
|
||||
title: '前端开发小组',
|
||||
content: '明天开始进行项目代码review,请各位准备好自己的代码。',
|
||||
time: '今天 09:00',
|
||||
avatar: '/static/images/avatar3.jpg',
|
||||
},
|
||||
{
|
||||
title: '公司年会筹备组',
|
||||
content: '年会节目征集开始了,欢迎大家积极报名参加!',
|
||||
time: '今天 10:20',
|
||||
avatar: '/static/images/avatar4.jpg',
|
||||
},
|
||||
{
|
||||
title: '技术分享会',
|
||||
content: '本周五下午的技术分享会主题是“Vue 3的新特性”,不要错过哦。',
|
||||
time: '今天 14:30',
|
||||
avatar: '/static/images/avatar5.jpg',
|
||||
},
|
||||
{
|
||||
title: 'IT中心工作群',
|
||||
content: '今天下午2点到5点,开部门会议,请各位提前准备好汇报材料。',
|
||||
time: '昨天 15:30',
|
||||
avatar: '/static/images/avatar1.jpg',
|
||||
},
|
||||
{
|
||||
title: '产品设计讨论组',
|
||||
content: '关于新功能的设计稿,大家有什么建议可以在群里提出来。',
|
||||
time: '昨天 18:45',
|
||||
avatar: '/static/images/avatar2.jpg',
|
||||
},
|
||||
{
|
||||
title: '前端开发小组',
|
||||
content: '明天开始进行项目代码review,请各位准备好自己的代码。',
|
||||
time: '今天 09:00',
|
||||
avatar: '/static/images/avatar3.jpg',
|
||||
},
|
||||
{
|
||||
title: '公司年会筹备组',
|
||||
content: '年会节目征集开始了,欢迎大家积极报名参加!',
|
||||
time: '今天 10:20',
|
||||
avatar: '/static/images/avatar4.jpg',
|
||||
},
|
||||
{
|
||||
title: '技术分享会',
|
||||
content: '本周五下午的技术分享会主题是“Vue 3的新特性”,不要错过哦。',
|
||||
time: '今天 14:30',
|
||||
avatar: '/static/images/avatar5.jpg',
|
||||
},
|
||||
{
|
||||
title: 'IT中心工作群',
|
||||
content: '今天下午2点到5点,开部门会议,请各位提前准备好汇报材料。',
|
||||
time: '昨天 15:30',
|
||||
avatar: '/static/images/avatar2.jpg',
|
||||
},
|
||||
{
|
||||
title: '产品设计讨论组',
|
||||
content: '关于新功能的设计稿,大家有什么建议可以在群里提出来。',
|
||||
time: '昨天 18:45',
|
||||
avatar: '/static/images/avatar1.jpg',
|
||||
},
|
||||
{
|
||||
title: '前端开发小组',
|
||||
content: '明天开始进行项目代码review,请各位准备好自己的代码。',
|
||||
time: '今天 09:00',
|
||||
avatar: '/static/images/avatar3.jpg',
|
||||
},
|
||||
{
|
||||
title: '公司年会筹备组',
|
||||
content: '年会节目征集开始了,欢迎大家积极报名参加!',
|
||||
time: '今天 10:20',
|
||||
avatar: '/static/images/avatar2.jpg',
|
||||
},
|
||||
{
|
||||
title: '技术分享会',
|
||||
content: '本周五下午的技术分享会主题是“Vue 3的新特性”,不要错过哦。',
|
||||
time: '今天 14:30',
|
||||
avatar: '/static/images/avatar4.jpg',
|
||||
},
|
||||
]
|
@ -8,11 +8,121 @@
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<view class="pt-40 text-xl text-center text-green-500">我的页面</view>
|
||||
<view class="p-4 flex flex-col gap-4">
|
||||
<!-- 个人信息-登陆后 -->
|
||||
<view
|
||||
class="flex justify-between w-full items-center gap-4 h-150rpx bg-white rounded-xl py-15rpx px-10rpx box-border"
|
||||
v-if="getAccessToken()"
|
||||
>
|
||||
<view class="rounded-full w-120rpx h-120rpx">
|
||||
<image
|
||||
:src="user ? user.avatar : '/static/images/user.png'"
|
||||
class="h-120rpx w-120rpx"
|
||||
mode="scaleToFill"
|
||||
/>
|
||||
</view>
|
||||
<view class="grow-1 flex flex-col justify-between h-120rpx py-2 box-border">
|
||||
<view>
|
||||
<text class="text-2xl font-600">{{ user?.nickname }}</text>
|
||||
</view>
|
||||
<view class="text-xs flex items-center">
|
||||
<text>芋道集团</text>
|
||||
<view class="i-ic-twotone-shield text-emerald"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="h-full">
|
||||
<!-- todo 设置按钮 -->
|
||||
<view class="i-ic-outline-settings mr-15rpx"></view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 个人信息-登录前 -->
|
||||
<view v-else class="flex justify-between w-full items-start gap-4">
|
||||
<view class="text-2xl font-600" @click="handleLogin">点击登录</view>
|
||||
</view>
|
||||
|
||||
<!-- // todo 日程安排 -->
|
||||
<!-- 应用设置 -->
|
||||
<view class="flex flex-col gap-2">
|
||||
<view>
|
||||
<text class="text-lg font-600">应用设置</text>
|
||||
</view>
|
||||
<view class="rounded-xl bg-white overflow-hidden">
|
||||
<wd-cell title="修改密码" is-link v-if="getAccessToken()">
|
||||
<template #icon>
|
||||
<view class="self-center flex item-center justify-center mr-2">
|
||||
<view class="i-ic-outline-lock"></view>
|
||||
</view>
|
||||
</template>
|
||||
</wd-cell>
|
||||
<wd-cell title="隐私协议" is-link>
|
||||
<template #icon>
|
||||
<view class="self-center flex item-center justify-center mr-2">
|
||||
<view class="i-ic-outline-policy"></view>
|
||||
</view>
|
||||
</template>
|
||||
</wd-cell>
|
||||
<wd-cell title="关于我们" is-link>
|
||||
<template #icon>
|
||||
<view class="self-center flex item-center justify-center mr-2">
|
||||
<view class="i-ic-outline-info"></view>
|
||||
</view>
|
||||
</template>
|
||||
</wd-cell>
|
||||
<wd-cell title="投诉与建议" is-link>
|
||||
<template #icon>
|
||||
<view class="self-center flex item-center justify-center mr-2">
|
||||
<view class="i-ic-outline-feedback"></view>
|
||||
</view>
|
||||
</template>
|
||||
</wd-cell>
|
||||
<!-- 退出登录,红色的警告字体 -->
|
||||
<wd-cell @click="handleLogout" clickable v-if="getAccessToken()">
|
||||
<template #icon>
|
||||
<view class="self-center flex item-center justify-center mr-2">
|
||||
<view class="i-ic-outline-logout text-red-500"></view>
|
||||
</view>
|
||||
</template>
|
||||
<template #title>
|
||||
<view>
|
||||
<text class="text-red-500">退出登录</text>
|
||||
</view>
|
||||
</template>
|
||||
</wd-cell>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useUserStore } from '@/store'
|
||||
import { useMessage } from 'wot-design-uni'
|
||||
import { getAccessToken } from '@/utils/auth'
|
||||
const message = useMessage()
|
||||
//
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const handleLogin = () => {
|
||||
uni.redirectTo({ url: '/pages/login/index' })
|
||||
}
|
||||
const handleLogout = async () => {
|
||||
message
|
||||
.confirm({
|
||||
msg: '确定退出登录?',
|
||||
title: '退出登录',
|
||||
})
|
||||
.then(async () => {
|
||||
await userStore.LogOut()
|
||||
uni.redirectTo({ url: '/pages/login/index' })
|
||||
})
|
||||
}
|
||||
|
||||
const user = ref<any>({})
|
||||
|
||||
onLoad((_option) => {
|
||||
//
|
||||
user.value = userStore.userInfo.user
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
37
src/pages/work/components/AppItems.vue
Normal file
@ -0,0 +1,37 @@
|
||||
<!-- 工作台图标 -->
|
||||
<template>
|
||||
<view class="flex flex-col items-center gap-1" @click="handleClick">
|
||||
<view class="bg-blue-500 rounded-5px w-80rpx h-80rpx flex flex-col items-center justify-center">
|
||||
<view :class="props.icon" class="color-white w-60rpx h-60rpx"></view>
|
||||
</view>
|
||||
<view class="text-22rpx">{{ props.title }}</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
//
|
||||
const props = defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
link: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const handleClick = () => {
|
||||
uni.navigateTo({
|
||||
url: props.link,
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
//
|
||||
</style>
|
49
src/pages/work/index.vue
Normal file
@ -0,0 +1,49 @@
|
||||
<!-- 工作台界面:用于各种 crud 管理操作 -->
|
||||
<route lang="json5" type="home">
|
||||
{
|
||||
layout: 'tabbar',
|
||||
style: {
|
||||
navigationBarTitleText: '工作台',
|
||||
},
|
||||
}
|
||||
</route>
|
||||
|
||||
<template>
|
||||
<!-- 搜索框 -->
|
||||
<wd-search
|
||||
v-model="searchValue"
|
||||
@focus="searchFocus"
|
||||
@blur="searchBlur"
|
||||
@search="search"
|
||||
maxlength="10"
|
||||
hide-cancel
|
||||
light
|
||||
/>
|
||||
<view class="px-4 box-border">
|
||||
<!-- 系统管理相关的 -->
|
||||
<view class="bg-white rounded-5px flex flex-col box-border p-2">
|
||||
<view class="text-22rpx box-border">系统管理</view>
|
||||
<view class="flex flex-wrap gap-6 mt-2">
|
||||
<AppItems title="用户管理" icon="i-ic-baseline-account-box" />
|
||||
<AppItems title="角色管理" icon="i-ic-round-person-pin" />
|
||||
<AppItems title="菜单管理" icon="i-ic-twotone-list-alt" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
//
|
||||
import { ref } from 'vue'
|
||||
import AppItems from './components/AppItems.vue'
|
||||
|
||||
const searchValue = ref('')
|
||||
/// /////// 搜索栏 事件////////////
|
||||
const searchFocus = () => {}
|
||||
const searchBlur = () => {}
|
||||
const search = () => {}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
//
|
||||
</style>
|
21
src/service/login/LoginAPI.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { http, httpGet, httpPost } from '@/utils/http'
|
||||
|
||||
export const login = (data) => {
|
||||
return http({
|
||||
url: '/system/auth/login',
|
||||
method: 'POST',
|
||||
data,
|
||||
})
|
||||
}
|
||||
|
||||
export const loginOut = () => {
|
||||
return httpPost('/system/auth/logout')
|
||||
}
|
||||
|
||||
export const getInfo = (): Promise<UserInfoVO> => {
|
||||
return httpGet<UserInfoVO>('/system/auth/get-permission-info')
|
||||
}
|
||||
|
||||
export const getTenantIdByName = (name: string) => {
|
||||
return httpGet('/system/tenant/get-id-by-name', { name })
|
||||
}
|
BIN
src/static/images/avatar1.jpg
Normal file
After Width: | Height: | Size: 7.2 KiB |
BIN
src/static/images/avatar2.jpg
Normal file
After Width: | Height: | Size: 4.6 KiB |
BIN
src/static/images/avatar3.jpg
Normal file
After Width: | Height: | Size: 6.8 KiB |
BIN
src/static/images/avatar4.jpg
Normal file
After Width: | Height: | Size: 6.5 KiB |
BIN
src/static/images/avatar5.jpg
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
src/static/images/contacts.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
src/static/images/empty.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
src/static/images/login-bg.png
Normal file
After Width: | Height: | Size: 68 KiB |
@ -1,35 +1,74 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { getAccessToken, removeToken } from '@/utils/auth'
|
||||
import { getInfo, loginOut } from '@/service/login/LoginAPI'
|
||||
|
||||
const initState = { nickname: '', avatar: '' }
|
||||
const initState = {
|
||||
permissions: [],
|
||||
roles: [],
|
||||
isSetUser: false,
|
||||
user: {
|
||||
id: 0,
|
||||
avatar: '',
|
||||
nickname: '',
|
||||
deptId: 0,
|
||||
},
|
||||
}
|
||||
|
||||
export const useUserStore = defineStore(
|
||||
'user',
|
||||
() => {
|
||||
const userInfo = ref<IUserInfo>({ ...initState })
|
||||
// state
|
||||
|
||||
const setUserInfo = (val: IUserInfo) => {
|
||||
userInfo.value = val
|
||||
const userInfo = ref<UserInfoVO>({ ...initState })
|
||||
|
||||
// actions methods
|
||||
const setUserInfoAction = async () => {
|
||||
if (!getAccessToken()) {
|
||||
// 获取不到accessToken,直接返回
|
||||
resetState()
|
||||
return
|
||||
}
|
||||
|
||||
const data = await getInfo()
|
||||
userInfo.value = {
|
||||
...data,
|
||||
isSetUser: true,
|
||||
}
|
||||
}
|
||||
|
||||
const clearUserInfo = () => {
|
||||
userInfo.value = { ...initState }
|
||||
const setUserAvatarAction = async (avatar: string) => {
|
||||
userInfo.value.user.avatar = avatar
|
||||
}
|
||||
// 一般没有reset需求,不需要的可以删除
|
||||
const reset = () => {
|
||||
userInfo.value = { ...initState }
|
||||
}
|
||||
const isLogined = computed(() => !!userInfo.value.token)
|
||||
|
||||
const setUserNicknameAction = async (nickname: string) => {
|
||||
userInfo.value.user.nickname = nickname
|
||||
}
|
||||
|
||||
const LogOut = async () => {
|
||||
await loginOut()
|
||||
removeToken()
|
||||
resetState()
|
||||
}
|
||||
|
||||
const resetState = () => {
|
||||
console.log('initState', initState)
|
||||
userInfo.value = initState
|
||||
console.log('重置userInfo', userInfo.value)
|
||||
}
|
||||
|
||||
// 暴露到外面的方法
|
||||
return {
|
||||
userInfo,
|
||||
setUserInfo,
|
||||
clearUserInfo,
|
||||
isLogined,
|
||||
reset,
|
||||
setUserInfoAction,
|
||||
setUserAvatarAction,
|
||||
setUserNicknameAction,
|
||||
LogOut,
|
||||
resetState,
|
||||
}
|
||||
},
|
||||
{
|
||||
// 持久化
|
||||
persist: true,
|
||||
},
|
||||
)
|
||||
|
@ -15,4 +15,8 @@ page {
|
||||
|
||||
// 修改按钮背景色
|
||||
// --wot-button-primary-bg-color: green;
|
||||
|
||||
//
|
||||
--wot-search-light-bg: #f2f3f7;
|
||||
--wot-search-input-radius: 5px;
|
||||
}
|
||||
|
20
src/types/system.d.ts
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
type UserVO = {
|
||||
id: number
|
||||
avatar: string
|
||||
nickname: string
|
||||
deptId: number
|
||||
}
|
||||
|
||||
// USER 缓存
|
||||
type UserInfoVO = {
|
||||
permissions: string[]
|
||||
roles: string[]
|
||||
isSetUser: boolean
|
||||
user: UserVO
|
||||
}
|
||||
|
||||
declare namespace JSX {
|
||||
interface IntrinsicElements {
|
||||
block: any // 或者更具体的类型定义
|
||||
}
|
||||
}
|
9
src/types/uni-pages.d.ts
vendored
@ -4,14 +4,17 @@
|
||||
// Generated by vite-plugin-uni-pages
|
||||
|
||||
interface NavigateToOptions {
|
||||
url: "/pages/index/index" |
|
||||
"/pages/about/about" |
|
||||
url: "/pages/work/index" |
|
||||
"/pages/colab/index" |
|
||||
"/pages/contacts/index" |
|
||||
"/pages/login/index" |
|
||||
"/pages/message/index" |
|
||||
"/pages/my/index";
|
||||
}
|
||||
interface RedirectToOptions extends NavigateToOptions {}
|
||||
|
||||
interface SwitchTabOptions {
|
||||
url: "/pages/index/index" | "/pages/about/about" | "/pages/my/index"
|
||||
url: "/pages/message/index" | "/pages/colab/index" | "/pages/work/index" | "/pages/contacts/index" | "/pages/my/index"
|
||||
}
|
||||
|
||||
type ReLaunchOptions = NavigateToOptions | SwitchTabOptions;
|
||||
|
37
src/utils/auth.ts
Normal file
@ -0,0 +1,37 @@
|
||||
const AccessTokenKey = 'ACCESS_TOKEN'
|
||||
const RefreshTokenKey = 'REFRESH_TOKEN'
|
||||
const TenantIdKey = 'tenantId'
|
||||
|
||||
// ========== Token 相关 ==========
|
||||
|
||||
// 获取 Token
|
||||
export function getAccessToken() {
|
||||
return uni.getStorageSync(AccessTokenKey)
|
||||
}
|
||||
|
||||
// 获取 RefreshToken
|
||||
export function getRefreshToken() {
|
||||
return uni.getStorageSync(RefreshTokenKey)
|
||||
}
|
||||
|
||||
// 设置 Token
|
||||
export function setToken(token) {
|
||||
uni.setStorageSync(AccessTokenKey, token.accessToken)
|
||||
uni.setStorageSync(RefreshTokenKey, token.refreshToken)
|
||||
}
|
||||
|
||||
// 移除 Token
|
||||
export function removeToken() {
|
||||
uni.removeStorageSync(AccessTokenKey)
|
||||
uni.removeStorageSync(RefreshTokenKey)
|
||||
}
|
||||
|
||||
// ========== 租户相关 ==========
|
||||
|
||||
export const getTenantId = () => {
|
||||
return uni.getStorageSync(TenantIdKey)
|
||||
}
|
||||
|
||||
export const setTenantId = (username: string) => {
|
||||
uni.setStorageSync(TenantIdKey, username)
|
||||
}
|
30
src/utils/common.ts
Normal file
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 显示模态弹窗
|
||||
*
|
||||
* @param content 提示的标题
|
||||
*/
|
||||
export function showConfirm(content: string) {
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
content,
|
||||
cancelText: '取消',
|
||||
confirmText: '确定',
|
||||
success: function (res) {
|
||||
resolve(res)
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示消息提示框
|
||||
*
|
||||
* @param content 提示的标题
|
||||
*/
|
||||
export function toast(content: string) {
|
||||
uni.showToast({
|
||||
title: content,
|
||||
icon: 'none',
|
||||
})
|
||||
}
|
7
src/utils/errorCode.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export default {
|
||||
'401': '认证失败,无法访问系统资源',
|
||||
'403': '当前操作没有权限',
|
||||
'404': '访问资源不存在',
|
||||
'500': '服务器错误',
|
||||
default: '系统未知错误,请反馈给管理员',
|
||||
}
|
@ -1,8 +1,27 @@
|
||||
import { CustomRequestOptions } from '@/interceptors/request'
|
||||
import { useUserStore } from '@/store'
|
||||
import { getAccessToken, getRefreshToken, setToken } from '@/utils/auth'
|
||||
import { showConfirm, toast } from '@/utils/common'
|
||||
import { getEvnBaseUrl } from '@/utils'
|
||||
import errorCode from './errorCode'
|
||||
|
||||
/*
|
||||
双 token 刷新说明:
|
||||
1. token 和 tenantId 都是在拦截器中赋值
|
||||
2. 后端并不直接更改 http statusCode,而是返回一个重新包装后的对象,所以不能使用失败回调函数就行判断token是否过期需要重新获取
|
||||
3. 应该在 success 中,解析 res,如果 code 是 401,则说明 access token 过期,需要重新获取
|
||||
*/
|
||||
|
||||
// 是否正在刷新中
|
||||
let isRefreshToken = false
|
||||
// 请求的方法栈
|
||||
let requestList = []
|
||||
// 请求基准地址
|
||||
const baseUrl = getEvnBaseUrl()
|
||||
|
||||
export const http = <T>(options: CustomRequestOptions) => {
|
||||
// 1. 返回 Promise 对象
|
||||
return new Promise<IResData<T>>((resolve, reject) => {
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
uni.request({
|
||||
...options,
|
||||
dataType: 'json',
|
||||
@ -10,25 +29,97 @@ export const http = <T>(options: CustomRequestOptions) => {
|
||||
responseType: 'json',
|
||||
// #endif
|
||||
// 响应成功
|
||||
success(res) {
|
||||
// 状态码 2xx,参考 axios 的设计
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
// 2.1 提取核心数据 res.data
|
||||
resolve(res.data as IResData<T>)
|
||||
} else if (res.statusCode === 401) {
|
||||
// 401错误 -> 清理用户信息,跳转到登录页
|
||||
// userStore.clearUserInfo()
|
||||
// uni.navigateTo({ url: '/pages/login/login' })
|
||||
reject(res)
|
||||
} else {
|
||||
// 其他错误 -> 根据后端错误信息轻提示
|
||||
!options.hideErrorToast &&
|
||||
uni.showToast({
|
||||
icon: 'none',
|
||||
title: (res.data as IResData<T>).msg || '请求错误',
|
||||
async success(res) {
|
||||
const resp = res.data as IResData<T>
|
||||
const { data, code, msg } = resp
|
||||
|
||||
if (code === 401) {
|
||||
// 未授权
|
||||
const userStore = useUserStore()
|
||||
|
||||
if (!isRefreshToken) {
|
||||
// 设置为正在刷新
|
||||
isRefreshToken = true
|
||||
|
||||
// 1. 如果获取不到刷新令牌,则只能执行登出操作
|
||||
if (!getRefreshToken()) {
|
||||
showConfirm('登录状态已过期,您可以继续留在该页面,或者重新登录?').then(
|
||||
(res: any) => {
|
||||
if (res.confirm) {
|
||||
// 清除缓存起来的用户信息
|
||||
userStore.LogOut().then((res) => {
|
||||
uni.reLaunch({ url: '/pages/login/index' })
|
||||
})
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// 2. 刷新accesstoken
|
||||
try {
|
||||
const refreshTokenRes: any = await refreshToken()
|
||||
if (refreshTokenRes.data.code !== 0) {
|
||||
// 如果获取不到refresh token,就直接跳转到登录页面
|
||||
showConfirm('登录状态已过期,您可以继续留在该页面,或者重新登录?').then(
|
||||
(res: any) => {
|
||||
if (res.confirm) {
|
||||
// 清除缓存起来的用户信息
|
||||
userStore.LogOut().then((res) => {
|
||||
uni.reLaunch({ url: '/pages/login/index' })
|
||||
})
|
||||
}
|
||||
},
|
||||
)
|
||||
const rejMsg = '无效的会话,或者会话已过期,请重新登录。'
|
||||
reject(rejMsg)
|
||||
}
|
||||
|
||||
// 2.1 刷新成功,则回放队列的请求 + 当前请求
|
||||
setToken(refreshTokenRes.data.data)
|
||||
options.header.Authorization = 'Bearer ' + getAccessToken()
|
||||
// 将所有的请求方法都进行调用
|
||||
requestList.forEach((cb) => {
|
||||
cb()
|
||||
})
|
||||
// 调用之后就清空
|
||||
requestList = []
|
||||
|
||||
// 到达这里,属于是请求已经失败了,需要重新请求的
|
||||
// 所以这里是一个递归调用
|
||||
resolve(http(options))
|
||||
} catch (e) {
|
||||
// 为什么需要 catch 异常呢?刷新失败时,请求因为 Promise.reject 触发异常。
|
||||
// 2.2 刷新失败,只回放队列的请求
|
||||
requestList.forEach((cb) => {
|
||||
cb()
|
||||
})
|
||||
const rejMsg = '无效的会话,或者会话已过期,请重新登录。'
|
||||
reject(rejMsg)
|
||||
} finally {
|
||||
requestList = []
|
||||
isRefreshToken = false
|
||||
}
|
||||
} else {
|
||||
// 添加到队列,等待刷新获取到新的令牌
|
||||
return new Promise((resolve) => {
|
||||
requestList.push(() => {
|
||||
options.header.Authorization = 'Bearer ' + getAccessToken()
|
||||
resolve(http(options))
|
||||
})
|
||||
})
|
||||
reject(res)
|
||||
}
|
||||
} else if (code === 500) {
|
||||
// 服务器错误
|
||||
toast(msg)
|
||||
reject(errorCode['500'])
|
||||
} else if (code !== 0) {
|
||||
// 其他的错误
|
||||
// ps:yudao 的 success 的 code,默认是 0 而不是 200
|
||||
toast(msg)
|
||||
reject(code)
|
||||
}
|
||||
|
||||
resolve(data)
|
||||
},
|
||||
// 响应失败
|
||||
fail(err) {
|
||||
@ -78,3 +169,13 @@ export const httpPost = <T>(
|
||||
|
||||
http.get = httpGet
|
||||
http.post = httpPost
|
||||
|
||||
const refreshToken = async () => {
|
||||
return await uni.request({
|
||||
method: 'POST',
|
||||
url: baseUrl + '/system/auth/refresh-token?refreshToken=' + getRefreshToken(),
|
||||
header: {
|
||||
'tenant-id': 1,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
@ -131,16 +131,16 @@ export const getEvnBaseUrl = () => {
|
||||
const {
|
||||
miniProgram: { envVersion },
|
||||
} = uni.getAccountInfoSync()
|
||||
|
||||
// 开发、体验、正式版 三种不同的小程序都可以配置不同的后端 url
|
||||
switch (envVersion) {
|
||||
case 'develop':
|
||||
baseUrl = 'https://ukw0y1.laf.run'
|
||||
baseUrl = 'http://localhost:48080/admin-api'
|
||||
break
|
||||
case 'trial':
|
||||
baseUrl = 'https://ukw0y1.laf.run'
|
||||
baseUrl = 'http://localhost:48080/admin-api'
|
||||
break
|
||||
case 'release':
|
||||
baseUrl = 'https://ukw0y1.laf.run'
|
||||
baseUrl = 'http://localhost:48080/admin-api'
|
||||
break
|
||||
}
|
||||
}
|
||||
|