diff --git a/frontend/auto-imports.d.ts b/frontend/auto-imports.d.ts index 78813d8..78d1e91 100644 --- a/frontend/auto-imports.d.ts +++ b/frontend/auto-imports.d.ts @@ -6,4 +6,5 @@ export {} declare global { const ElMessage: typeof import('element-plus/es')['ElMessage'] + const showConfirmDialog: typeof import('vant/es')['showConfirmDialog'] } diff --git a/frontend/components.d.ts b/frontend/components.d.ts index c0bdab2..f91f6b6 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -44,6 +44,22 @@ declare module 'vue' { RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] SearchBar: typeof import('./src/components/SearchBar.vue')['default'] + VanBackTop: typeof import('vant/es')['BackTop'] + VanButton: typeof import('vant/es')['Button'] + VanCell: typeof import('vant/es')['Cell'] + VanCellGroup: typeof import('vant/es')['CellGroup'] + VanField: typeof import('vant/es')['Field'] + VanForm: typeof import('vant/es')['Form'] + VanIcon: typeof import('vant/es')['Icon'] + VanImage: typeof import('vant/es')['Image'] + VanList: typeof import('vant/es')['List'] + VanLoading: typeof import('vant/es')['Loading'] + VanSearch: typeof import('vant/es')['Search'] + VanTab: typeof import('vant/es')['Tab'] + VanTabbar: typeof import('vant/es')['Tabbar'] + VanTabbarItem: typeof import('vant/es')['TabbarItem'] + VanTabs: typeof import('vant/es')['Tabs'] + VanTag: typeof import('vant/es')['Tag'] } export interface ComponentCustomProperties { vLoading: typeof import('element-plus/es')['ElLoadingDirective'] diff --git a/frontend/package.json b/frontend/package.json index 7480697..8a8595b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -14,15 +14,18 @@ "element-plus": "^2.6.1", "pinia": "^2.1.7", "socket.io-client": "^4.8.1", + "vant": "^4.9.17", "vue": "^3.4.21", "vue-router": "^4.3.0" }, "devDependencies": { "@types/node": "^20.11.25", + "@vant/auto-import-resolver": "^1.3.0", "@vitejs/plugin-vue": "^5.0.4", + "postcss-pxtorem": "^6.1.0", "sass": "^1.83.4", "typescript": "^5.4.2", - "unplugin-auto-import": "^0.17.5", + "unplugin-auto-import": "^0.17.8", "unplugin-vue-components": "^0.26.0", "vite": "^5.1.5", "vue-tsc": "^2.0.6" diff --git a/frontend/postcss.config.cjs b/frontend/postcss.config.cjs new file mode 100644 index 0000000..2080a27 --- /dev/null +++ b/frontend/postcss.config.cjs @@ -0,0 +1,13 @@ +module.exports = { + plugins: { + "postcss-pxtorem": { + rootValue({ file }) { + return file.indexOf("vant") !== -1 ? 37.5 : 75; + }, + propList: ["*"], + exclude: (file) => { + return !file.includes("mobile") && !file.includes("vant"); + }, + }, + }, +}; diff --git a/frontend/src/assets/images/mobile-login-bg.png b/frontend/src/assets/images/mobile-login-bg.png new file mode 100644 index 0000000..5321e3f Binary files /dev/null and b/frontend/src/assets/images/mobile-login-bg.png differ diff --git a/frontend/src/components/mobile/FolderSelect.vue b/frontend/src/components/mobile/FolderSelect.vue new file mode 100644 index 0000000..f101997 --- /dev/null +++ b/frontend/src/components/mobile/FolderSelect.vue @@ -0,0 +1,138 @@ + + + + + diff --git a/frontend/src/components/mobile/ResourceCard.vue b/frontend/src/components/mobile/ResourceCard.vue new file mode 100644 index 0000000..2d97575 --- /dev/null +++ b/frontend/src/components/mobile/ResourceCard.vue @@ -0,0 +1,138 @@ + + + + + diff --git a/frontend/src/components/mobile/ResourceSelect.vue b/frontend/src/components/mobile/ResourceSelect.vue new file mode 100644 index 0000000..e85e41b --- /dev/null +++ b/frontend/src/components/mobile/ResourceSelect.vue @@ -0,0 +1,82 @@ + + + + + diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 02f4bc5..c6251c3 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -3,7 +3,12 @@ import { createPinia } from "pinia"; import ElementPlus from "element-plus"; import "element-plus/dist/index.css"; import * as ElementPlusIconsVue from "@element-plus/icons-vue"; +import { isMobileDevice } from "@/utils/index"; import App from "./App.vue"; +import { Lazyload } from "vant"; +import "vant/es/notify/style"; +import "vant/es/dialog/style"; + import router from "./router/index"; const app = createApp(App); @@ -13,7 +18,24 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) { } app.use(createPinia()); +app.use(Lazyload); app.use(router); app.use(ElementPlus); app.mount("#app"); + +const setRootFontSize = () => { + const isMobile = isMobileDevice(); + console.log(isMobile); + if (!isMobile) { + return; + } // PC端不干预 + const clientWidth = document.documentElement.clientWidth; + const baseSize = clientWidth / 7.5; // 按750px设计稿 + document.documentElement.style.fontSize = baseSize + "px"; +}; + +// 初始化执行 +setRootFontSize(); +// 监听窗口变化 +window.addEventListener("resize", setRootFontSize); diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 5946833..c9cbc2d 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -1,41 +1,11 @@ import { createRouter, createWebHistory } from "vue-router"; -import type { RouteRecordRaw } from "vue-router"; -import Login from "@/views/Login.vue"; -import Home from "@/views/Home.vue"; - -const routes: RouteRecordRaw[] = [ - { - path: "/", - name: "home", - component: Home, - children: [ - { - path: "", - name: "resource", - component: () => import("@/views/ResourceList.vue"), - }, - { - path: "/douban", - name: "douban", - component: () => import("@/views/Douban.vue"), - }, - { - path: "/setting", - name: "setting", - component: () => import("@/views/Setting.vue"), - }, - ], - }, - { - path: "/login", - name: "login", - component: Login, - }, -]; +import mobileRoutes from "./mobile-routes"; +import pcRoutes from "./pc-routes"; +import { isMobileDevice } from "@/utils/index"; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), - routes, + routes: [...(isMobileDevice() ? mobileRoutes : pcRoutes)], }); export default router; diff --git a/frontend/src/router/mobile-routes.ts b/frontend/src/router/mobile-routes.ts new file mode 100644 index 0000000..21171d1 --- /dev/null +++ b/frontend/src/router/mobile-routes.ts @@ -0,0 +1,35 @@ +import Login from "@/views/mobile/Login.vue"; +import Home from "@/views/mobile/Home.vue"; +import type { RouteRecordRaw } from "vue-router"; +const routes: RouteRecordRaw[] = [ + { + path: "/", + name: "home", + component: Home, + redirect: "/resource", + children: [ + { + path: "/resource", + name: "resource", + component: () => import("@/views/mobile/ResourceList.vue"), + }, + { + path: "/douban", + name: "douban", + component: () => import("@/views/mobile/Douban.vue"), + }, + { + path: "/setting", + name: "setting", + component: () => import("@/views/mobile/Setting.vue"), + }, + ], + }, + { + path: "/login", + name: "login", + component: Login, + }, +]; + +export default routes; diff --git a/frontend/src/router/pc-routes.ts b/frontend/src/router/pc-routes.ts new file mode 100644 index 0000000..6407fce --- /dev/null +++ b/frontend/src/router/pc-routes.ts @@ -0,0 +1,35 @@ +import Login from "@/views/Login.vue"; +import Home from "@/views/Home.vue"; +import type { RouteRecordRaw } from "vue-router"; +const routes: RouteRecordRaw[] = [ + { + path: "/", + name: "home", + component: Home, + redirect: "/resource", + children: [ + { + path: "/resource", + name: "resource", + component: () => import("@/views/ResourceList.vue"), + }, + { + path: "/douban", + name: "douban", + component: () => import("@/views/Douban.vue"), + }, + { + path: "/setting", + name: "setting", + component: () => import("@/views/Setting.vue"), + }, + ], + }, + { + path: "/login", + name: "login", + component: Login, + }, +]; + +export default routes; diff --git a/frontend/src/utils/index.ts b/frontend/src/utils/index.ts index 09cb076..4fcb218 100644 --- a/frontend/src/utils/index.ts +++ b/frontend/src/utils/index.ts @@ -7,3 +7,10 @@ export const formattedFileSize = (size: number): string => { } return `${(size / 1024 / 1024 / 1024).toFixed(2)}GB`; }; + +export function isMobileDevice() { + return ( + /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent) || + window.innerWidth <= 768 + ); +} diff --git a/frontend/src/utils/request.ts b/frontend/src/utils/request.ts index cbd4307..e1d5e69 100644 --- a/frontend/src/utils/request.ts +++ b/frontend/src/utils/request.ts @@ -1,7 +1,21 @@ import axios, { AxiosResponse, AxiosRequestConfig } from "axios"; import { ElMessage } from "element-plus"; +import { isMobileDevice } from "@/utils/index"; +import { showNotify } from "vant"; import { RequestResult } from "../types/response"; +const errorMessage = (message: string) => { + if (isMobileDevice()) { + console.log(message); + showNotify({ + type: "danger", + message, + }); + return; + } + ElMessage.error(message); +}; + const axiosInstance = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL as string, timeout: 16000, @@ -21,7 +35,7 @@ axiosInstance.interceptors.request.use( if (token) { config.headers.Authorization = `Bearer ${token}`; } else if (!isLoginAndRedirect(config.url || "")) { - ElMessage.error("请先登录"); + errorMessage("请先登录"); window.location.href = "/login"; } return config; @@ -38,12 +52,12 @@ axiosInstance.interceptors.response.use( }, (error) => { if (error.response.status === 401) { - ElMessage.error("登录过期,请重新登录"); + errorMessage("登录过期,请重新登录"); localStorage.removeItem("token"); window.location.href = "/login"; return Promise.reject(new Error("登录过期,请重新登录")); } - ElMessage.error(error.response.statusText); + errorMessage(error.response.statusText); return Promise.reject(new Error(error.response.statusText)); } ); diff --git a/frontend/src/views/mobile/Douban.vue b/frontend/src/views/mobile/Douban.vue new file mode 100644 index 0000000..2e7685d --- /dev/null +++ b/frontend/src/views/mobile/Douban.vue @@ -0,0 +1,147 @@ + + + + + diff --git a/frontend/src/views/mobile/Home.vue b/frontend/src/views/mobile/Home.vue new file mode 100644 index 0000000..305a534 --- /dev/null +++ b/frontend/src/views/mobile/Home.vue @@ -0,0 +1,143 @@ + + + + + diff --git a/frontend/src/views/mobile/Login.vue b/frontend/src/views/mobile/Login.vue new file mode 100644 index 0000000..ce86a6d --- /dev/null +++ b/frontend/src/views/mobile/Login.vue @@ -0,0 +1,113 @@ + + + diff --git a/frontend/src/views/mobile/ResourceList.vue b/frontend/src/views/mobile/ResourceList.vue new file mode 100644 index 0000000..73c7638 --- /dev/null +++ b/frontend/src/views/mobile/ResourceList.vue @@ -0,0 +1,246 @@ + + + + + diff --git a/frontend/src/views/mobile/Setting.vue b/frontend/src/views/mobile/Setting.vue new file mode 100644 index 0000000..8d523f6 --- /dev/null +++ b/frontend/src/views/mobile/Setting.vue @@ -0,0 +1,182 @@ + + + + + diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index e7e446c..aa428af 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -4,16 +4,17 @@ import { fileURLToPath, URL } from "node:url"; import AutoImport from "unplugin-auto-import/vite"; import Components from "unplugin-vue-components/vite"; import { ElementPlusResolver } from "unplugin-vue-components/resolvers"; +import { VantResolver } from "@vant/auto-import-resolver"; export default defineConfig({ base: "/", plugins: [ vue(), AutoImport({ - resolvers: [ElementPlusResolver()], + resolvers: [ElementPlusResolver(), VantResolver()], }), Components({ - resolvers: [ElementPlusResolver()], + resolvers: [ElementPlusResolver(), VantResolver()], }), ], css: {