feat: Add typings for additional uni-components

- Added typings for additional uni-components to improve type safety and code clarity.
- Created a new file `type.d.ts` to declare the `ROUTES` constant.

npmrc: Add .npmrc file

- Added a new `.npmrc` file to configure hoisting patterns for public packages.
- The file includes patterns for hoisting `@vue*` packages and an optional setting for `shamefully-hoist`.

refactor: Remove unused user store

- Deleted the `src/stores/user.ts` file as it is no longer needed.
- The functionality provided by the user store has been replaced by other stores.

tsconfig.json: Add wot-design-uni/global to types

- Updated the `tsconfig.json` file to include the `wot-design-uni/global` type definition.
- This allows for better type checking and autocompletion when using the `wot-design-uni` library.

feat: Add useTabbar store

- Created a new store `useTabbar` in the `src/stores/useTabbar.ts` file.
- The store provides reactive state for managing the active tab in the tab bar.

refactor: Update App.vue

- Updated the `src/App.vue` file to import and use the `useTabbar` store.
- Set the initial active tab to 'home' when the app is launched.

feat: Add useConfig store

- Created a new store `useConfig` in the `src/composables/useConfig.ts` file.
- The store provides reactive state for managing the base URL used in API requests.

feat: Add router configuration

- Created a new file `src/router/index.ts` to configure the app's router.
- Imported the `createRouter` function from the `uni-mini-router` library.
- Imported the `pages.json` file and used `uni-parse-pages` to generate the routes.
- Exported the created router instance.

feat: Add persist plugin

- Created a new file `src/stores/persist.ts` to implement a plugin for persisting store state.
- The plugin uses `uni.getStorageSync` and `uni.setStorageSync` to store and retrieve store state from local storage.

feat: Add page-wrapper component

- Created a new file `src/components/page-wrapper.vue` to define a reusable page wrapper component.
- The component includes a slot for content and adds a tab bar at the bottom of the page.

feat: Add BussApi and user types

- Created a new file `src/api/BussApi.ts` to define API methods for the business-related functionality.
- Added types for the API request and response objects.
- Created a new file `src/types/api/user.ts` to define types for the user-related API responses.

chore: Update dependencies

- Updated various dependencies in the `package.json` file.
- Added `wot-design-uni` as a new dependency.
- Updated `sass-loader` to version 10.

feat: Add my page

- Created a new file `src/pages/my/index.vue` to implement the "My" page.
- Added API calls to fetch user profile information.
- Added a logout button to log out the user and redirect to the login page.

chore: Update pages.json

- Updated the `pages.json` file to include the new "my" page and configure the tab bar.
- Set the "home" page as the initial page.
This commit is contained in:
Timothy Yin 2024-09-19 00:26:40 +08:00
parent 0fe6fc4b1d
commit 8f0994e0b2
25 changed files with 1595 additions and 160 deletions

4
.npmrc Normal file
View File

@ -0,0 +1,4 @@
// .npmrc
public-hoist-pattern[]=@vue*
// or
// shamefully-hoist = true

1
components.d.ts vendored
View File

@ -7,6 +7,7 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
PageWrapper: typeof import('./src/components/page-wrapper.vue')['default']
Tabbar: typeof import('./src/components/Tabbar.vue')['default']
TabBar: typeof import('./src/components/TabBar.vue')['default']
WdButton: typeof import('wot-design-uni/components/wd-button/wd-button.vue')['default']

View File

@ -60,7 +60,8 @@
"@dcloudio/uni-quickapp-webview": "3.0.0-4020420240722002",
"pinia": "^2.2.2",
"vue": "^3.4.21",
"vue-i18n": "^9.1.9"
"vue-i18n": "^9.1.9",
"wot-design-uni": "^1.3.10"
},
"devDependencies": {
"@dcloudio/types": "^3.4.8",
@ -68,9 +69,17 @@
"@dcloudio/uni-cli-shared": "3.0.0-4020420240722002",
"@dcloudio/uni-stacktracey": "3.0.0-4020420240722002",
"@dcloudio/vite-plugin-uni": "3.0.0-4020420240722002",
"@uni-helper/vite-plugin-uni-components": "^0.1.0",
"@vue/runtime-core": "^3.4.21",
"@vue/tsconfig": "^0.1.3",
"add": "^2.0.6",
"axios": "^1.7.7",
"fant-axios-adapter": "^0.0.6",
"sass": "^1.78.0",
"sass-loader": "10",
"typescript": "^4.9.4",
"uni-mini-router": "^0.1.6",
"uni-parse-pages": "^0.0.1",
"unocss": "^0.62.3",
"unocss-preset-weapp": "^0.62.2",
"vite": "5.2.8",

1190
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,12 @@
<script setup lang="ts">
import { onLaunch, onShow, onHide } from "@dcloudio/uni-app";
import { useTabbar } from "./stores/useTabbar";
const tab = useTabbar()
onLaunch(() => {
console.log("App Launch");
tab.activeTab = 'home'
});
onShow(() => {
console.log("App Show");

28
src/api/BussApi.ts Normal file
View File

@ -0,0 +1,28 @@
import http from "@/http/HttpClient";
import type { User } from "@/types/api/user";
export interface LoginRequest extends Record<string, string> {
email: string;
password: string;
remember: string;
}
export default class BussApi {
static login(params: LoginRequest): Promise<{ token: string }> {
return http
.server()
.post("/login", params)
.then((res) => res.data);
}
static profile(token: string): Promise<User> {
return http
.server()
.get("/user/online", {
headers: {
Authorization: `Bearer ${token}`,
},
})
.then((res) => res.data);
}
}

View File

@ -1,13 +1,61 @@
<script lang="ts" setup>
import { nextTick, ref } from 'vue';
import { useRouter, useRoute } from 'uni-mini-router'
import { onMounted, computed, watch } from 'vue';
import { useTabbar } from '@/stores/useTabbar';
const props = defineProps({
currentName: {
type: String,
default: 'home'
}
})
const router = useRouter()
const tab = useTabbar()
// const isFirstTime = ref(true)
// onMounted(() => {
// if (isFirstTime.value) {
// tab.activeTab = props.currentName
// isFirstTime.value = false
// }
// })
const tabWhitelist = ['home', 'progress', 'my']
const nameLabelIconMap = {
home: {
title: '进度查看',
icon: 'dashboard'
},
progress: {
title: '进度管理',
icon: 'transfer'
},
my: {
title: '我的',
icon: 'user'
}
}
const tabList = computed(() => router.routes.filter((r: { name: string }) => tabWhitelist.includes(r.name)).map((route: { name: keyof typeof nameLabelIconMap }) => {
return {
name: route.name,
title: nameLabelIconMap[route.name]?.title,
icon: nameLabelIconMap[route.name]?.icon
}
}))
</script>
<template>
<div>
tabbar
<wd-tabbar v-model="tab.activeTab" fixed safe-area-inset-bottom bordered placeholder>
<wd-tabbar-item v-for="(tab, i) in tabList" :name="tab.name" :title="tab.title" :icon="tab.icon" :key="i"
@tap="router.pushTab({ name: tab.name })" />
</wd-tabbar>
</div>
</template>
<style scoped>
</style>
<style scoped></style>

View File

@ -0,0 +1,27 @@
<script lang="ts" setup>
import TabBar from '@/components/Tabbar.vue';
import { useUser } from '@/stores/useUser';
import { useRouter } from 'uni-mini-router';
import { onMounted } from 'vue';
// const router = useRouter()
// const user = useUser()
// onMounted(() => {
// if (!user.userinfo) {
// router.replaceAll('/pages/login/index')
// }
// })
</script>
<template>
<div>
<slot></slot>
<wd-toast />
<tab-bar current-name="home" />
</div>
</template>
<style scoped>
</style>

View File

@ -0,0 +1,10 @@
import { defineStore } from "pinia";
import { ref } from "vue";
export const useConfig = defineStore('config', () => {
const BASE_URL = ref<string>("https://ppmp.fenshenzhike.com/api");
return {
BASE_URL
}
})

114
src/http/HttpClient.ts Normal file
View File

@ -0,0 +1,114 @@
import axios from "axios";
import { uniAdapter } from "fant-axios-adapter";
export default class ApiClient {
public static server() {
const BASE_URL = "https://ppmp.fenshenzhike.com/api";
return ApiClient.create(BASE_URL);
}
public static create(baseUrl: string) {
const instance = axios.create({
withCredentials: true,
baseURL: baseUrl,
adapter: uniAdapter,
});
instance.interceptors.request.use(
(request) => {
if (request.headers) {
request.headers.set(
"Content-Type",
"application/x-www-form-urlencoded"
);
} else {
request.headers = new axios.AxiosHeaders();
request.headers.set(
"Content-Type",
"application/x-www-form-urlencoded"
);
}
request.headers.trace_id = new Date().getTime();
return request;
},
(error) => Promise.reject(error)
);
instance.interceptors.response.use(
(response) => {
if (response.data.code === 10001) {
const pages = getCurrentPages() as any[];
setTimeout(() => {
uni.showToast({ title: "登录已过期,请重新登录", icon: "none" });
}, 300);
if (
!pages[pages.length - 1].$page ||
(pages[pages.length - 1].$page &&
pages[pages.length - 1].$page.fullPath !== "/pages/login/index")
) {
uni.reLaunch({ url: "/pages/login/index" });
}
}
if (!response.data.code || response.data.code === 10000) {
return response;
} else {
const error: Record<string, any> = {};
if (response.data.code) {
error.code = response.data.code;
}
if (response.data.message) {
error.message = response.data.message;
} else {
error.message = `服务器内部错误:${response.status}`;
}
// error.response = response.data;
return Promise.reject(error);
}
},
(error) => {
if (error.status !== 0 && !error.status) {
const newError = error as any;
newError.msg = newError.errMsg || "网络错误";
return Promise.reject(newError);
}
const pages = getCurrentPages() as any[];
switch (error.status) {
case 1:
error.msg = "网络超时";
break;
case 401:
// todo 401 logout
error.msg = "请先登录";
setTimeout(() => {
uni.showToast({ title: "登录已过期,请重新登录", icon: "none" });
}, 300);
if (
!pages[pages.length - 1].$page ||
(pages[pages.length - 1].$page &&
pages[pages.length - 1].$page.fullPath !== "/pages/login/index")
) {
uni.reLaunch({ url: "/pages/login/index" });
}
break;
case 403:
error.msg = `${error.status} 禁止访问!`;
break;
case 500:
error.msg = `${error.status} 服务内部异常!`;
break;
case 502:
error.msg = `${error.status} 服务器暂不可用!`;
break;
case 503:
error.msg = `${error.status} 服务器升级中!`;
break;
case 404:
error.msg = `${error.status} 服务器无回应!`;
break;
default:
error.msg = `${error.status} 未知错误!`;
}
return Promise.reject(error);
}
);
return instance;
}
}

View File

@ -1,14 +1,19 @@
import { createSSRApp } from "vue";
import { createPinia } from "pinia";
import App from "./App.vue";
import router from "./router";
import "uno.css";
import { persist } from "./stores/persist";
export function createApp() {
const pinia = createPinia();
pinia.use(persist);
const app = createSSRApp(App);
app.use(pinia);
app.use(router);
return {
app,

View File

@ -1,15 +1,46 @@
{
"pages": [ //pageshttps://uniapp.dcloud.io/collocation/pages
"pages": [
{
"name": "home",
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "uni-app"
"navigationBarTitleText": "进度查看"
}
},
{
"name": "my",
"path": "pages/my/index",
"style": {
"navigationBarTitleText": "我的"
}
},
{
"name": "login",
"path": "pages/login/index",
"style": {
"navigationBarTitleText": "登录"
}
}
],
"tabBar": {
"custom": true,
"color": "#bfbfbf",
"selectedColor": "#0165FF",
"backgroundColor": "#ffffff",
"list": [
{
"pagePath": "pages/index/index",
"text": "进度查看"
},
{
"pagePath": "pages/my/index",
"text": "我的"
}
]
},
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
"navigationBarTitleText": "XSH PPMP",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
}

View File

@ -1,27 +1,18 @@
<template>
<div class="content">
<img class="logo" src="/static/logo.png" />
<div class="flex flex-col items-center gap-4">
<p class="title text-red-500">
{{ title }}
</p>
<button @click="onClick">increment</button>
<page-wrapper>
<div class="content">
<img class="logo" src="/static/logo.png" />
<div class="flex flex-col items-center gap-4">
<p class="title text-4xl text-neutral-300 font-bold">
XSH PPMS
</p>
</div>
</div>
</div>
</page-wrapper>
</template>
<script setup lang="ts">
import { useUserStore } from '@/stores/user';
import { computed } from 'vue';
import { ref } from 'vue'
const user = useUserStore()
const onClick = () => {
user.count++
}
const title = computed(() => `Counter: ${user.count}`)
import pageWrapper from '@/components/page-wrapper.vue';
</script>
<style>

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

@ -0,0 +1,77 @@
<script lang="ts" setup>
import BussApi from '@/api/BussApi';
import { useUser } from '@/stores/useUser';
import { useRouter } from 'uni-mini-router';
import { reactive, ref } from 'vue';
import { useToast } from 'wot-design-uni';
const router = useRouter()
const toast = useToast()
const user = useUser()
const model = reactive<{
email: string
password: string
remember: boolean
}>({
email: '',
password: '',
remember: true
})
const form = ref()
const handleSubmit = () => {
form.value.validate()
.then(({ valid, errors }: { valid: boolean; errors: any }) => {
if (valid) {
toast.loading({
msg: '登录中...'
})
BussApi.login({
email: model.email,
password: model.password,
remember: model.remember + '',
}).then(res => {
user.token = res.token
toast.loading({
msg: '加载资料...'
})
BussApi.profile(user.token).then(res => {
user.userinfo = res
toast.success({ msg: '登录成功' })
setTimeout(() => {
router.pushTab('/pages/index/index')
}, 1000)
}).catch(err => {
toast.error({ msg: err.message })
})
}).catch(err => {
toast.error({ msg: err.message })
})
}
})
.catch((error: any) => {
console.log(error, 'error')
})
}
</script>
<template>
<div>
<wd-form ref="form" class="p-4" :model="model">
<wd-cell-group border>
<wd-input label="邮箱" label-width="100px" prop="email" clearable v-model="model.email" placeholder="请输入邮箱"
:rules="[{ required: true, message: '请填写邮箱' }]" />
<wd-input label="密码" label-width="100px" prop="password" show-password clearable v-model="model.password"
placeholder="请输入密码" :rules="[{ required: true, message: '请填写密码' }]" />
</wd-cell-group>
<view class="p-4">
<wd-button type="primary" size="large" @click="handleSubmit" block>登录</wd-button>
</view>
</wd-form>
<wd-toast />
</div>
</template>
<style scoped></style>

39
src/pages/my/index.vue Normal file
View File

@ -0,0 +1,39 @@
<script lang="ts" setup>
import BussApi from '@/api/BussApi';
import pageWrapper from '@/components/page-wrapper.vue';
import { useUser } from '@/stores/useUser';
import { useRouter } from 'uni-mini-router';
import { onMounted } from 'vue';
const router = useRouter()
const user = useUser()
const logout = () => {
user.logout()
router.replaceAll('/pages/login/index')
}
onMounted(() => {
BussApi.profile(user.token!).then(res => {
user.userinfo = res
})
})
</script>
<template>
<page-wrapper>
<div class="p-4 flex flex-col gap-4">
<WdCellGroup :border="true">
<WdCell title="用户名" :value="user.userinfo?.username || '-'" />
<WdCell title="邮箱" :value="user.userinfo?.email || '-'" />
<WdCell title="单位" :value="user.userinfo?.department_id || '-'" />
<WdCell title="角色" :value="user.userinfo?.roles.join('') || '-'" />
</WdCellGroup>
<div class="px-4">
<wd-button plain hairline block type="error" @click="logout">退出账号</wd-button>
</div>
</div>
</page-wrapper>
</template>
<style scoped></style>

11
src/router/index.ts Normal file
View File

@ -0,0 +1,11 @@
import { createRouter } from "uni-mini-router";
// 导入pages.json
import pagesJson from "../pages.json";
// 引入uni-parse-pages
import pagesJsonToRoutes from "uni-parse-pages";
// 生成路由表
const routes = pagesJsonToRoutes(pagesJson);
const router = createRouter({
routes: [...routes], // 路由表信息
});
export default router;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 200 KiB

17
src/stores/persist.ts Normal file
View File

@ -0,0 +1,17 @@
import type { PiniaPluginContext } from "pinia";
import { deepClone } from "wot-design-uni/components/common/util";
export function persist({ store }: PiniaPluginContext) {
// 暂存State
let persistState = deepClone(store.$state);
// 从缓存中读取
const storageState = uni.getStorageSync(store.$id);
if (storageState) {
persistState = storageState;
}
store.$state = persistState;
store.$subscribe(() => {
// 在存储变化的时候将store缓存
uni.setStorageSync(store.$id, deepClone(store.$state));
});
}

10
src/stores/useTabbar.ts Normal file
View File

@ -0,0 +1,10 @@
import { defineStore } from "pinia";
import { ref } from "vue";
export const useTabbar = defineStore('tabbar', () => {
const activeTab = ref('home')
return {
activeTab
}
})

19
src/stores/useUser.ts Normal file
View File

@ -0,0 +1,19 @@
import type { User } from "@/types/api/user";
import { defineStore } from "pinia";
import { ref } from "vue";
export const useUser = defineStore("user", () => {
const token = ref<string | null>(null);
const userinfo = ref<User | null>(null);
function logout() {
token.value = null
userinfo.value = null
}
return {
token,
userinfo,
logout,
};
});

View File

@ -1,10 +0,0 @@
import { defineStore } from "pinia";
import { ref } from "vue";
export const useUserStore = defineStore("user", () => {
const count = ref(0);
return {
count,
};
});

2
src/type.d.ts vendored Normal file
View File

@ -0,0 +1,2 @@
//type.d.ts
declare const ROUTES: [];

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

@ -0,0 +1,43 @@
export interface User {
id: number;
username: string;
email: string;
avatar: string;
department_id: null;
creator_id: number;
status: number;
login_ip: string;
login_at: number;
created_at: string;
updated_at: string;
deleted_at: Date;
permissions: Permission[];
roles: any[];
jobs: any[];
}
export interface Permission {
id: number;
parent_id: number;
permission_name: string;
route: string;
icon: string;
module: PermissionModule;
permission_mark: string;
component: string;
redirect: null | string;
keepalive: number;
type: number;
hidden: boolean;
sort: number;
active_menu: string;
creator_id: number;
created_at: string;
updated_at: string;
}
export enum PermissionModule {
Lesson = "lesson",
Permissions = "permissions",
User = "user",
}

View File

@ -7,7 +7,7 @@
"@/*": ["./src/*"]
},
"lib": ["esnext", "dom"],
"types": ["@dcloudio/types"]
"types": ["@dcloudio/types", "wot-design-uni/global"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}

View File

@ -1,12 +1,18 @@
import { defineConfig } from "vite";
import uni from "@dcloudio/vite-plugin-uni";
import Components from "@uni-helper/vite-plugin-uni-components";
import { WotResolver } from "@uni-helper/vite-plugin-uni-components/resolvers";
// https://vitejs.dev/config/
export default defineConfig(async () => {
const UnoCSS = await import("unocss/vite").then((i) => i.default);
return {
plugins: [
Components({
resolvers: [WotResolver()],
}),
uni(),
// https://github.com/unocss/unocss
UnoCSS(),