├── .vscode └── extensions.json ├── src ├── assets │ ├── icons │ │ ├── category.png │ │ ├── TAB/首页/新备份@2x.png │ │ ├── un_home.svg │ │ ├── mine.svg │ │ ├── un_category.svg │ │ ├── home.svg │ │ ├── un_shopping.svg │ │ ├── shopping.svg │ │ └── un_mine.svg │ ├── images │ │ └── empty │ │ │ ├── no_data.png │ │ │ ├── no_goods.png │ │ │ ├── no_login.png │ │ │ ├── no_order.png │ │ │ ├── no_address.png │ │ │ ├── no_collect.png │ │ │ ├── no_look_up.png │ │ │ ├── no_network.png │ │ │ ├── no_bank_card.png │ │ │ └── goods_delisting.png │ ├── vue.svg │ └── svg │ │ ├── delete.svg │ │ ├── search.svg │ │ ├── file.svg │ │ ├── copy.svg │ │ ├── search_grey.svg │ │ ├── shopping.svg │ │ ├── special_subject.svg │ │ ├── album.svg │ │ ├── customer_service.svg │ │ ├── loading.svg │ │ └── event.svg ├── components │ ├── globalComponents │ │ ├── NavBar │ │ │ ├── index.js │ │ │ └── index.vue │ │ ├── TabBar │ │ │ ├── index.js │ │ │ └── index.vue │ │ ├── NoData │ │ │ ├── index.js │ │ │ └── index.vue │ │ ├── SvgIcon │ │ │ ├── index.js │ │ │ └── index.vue │ │ ├── TabNav │ │ │ ├── index.js │ │ │ └── index.vue │ │ ├── VideoPlayer │ │ │ ├── index.js │ │ │ ├── svg │ │ │ │ ├── play_icon.svg │ │ │ │ ├── pause_icon.svg │ │ │ │ ├── fullscreen.svg │ │ │ │ ├── play.svg │ │ │ │ ├── video_audio_on.svg │ │ │ │ ├── video_audio_off.svg │ │ │ │ └── loading.svg │ │ │ └── index.vue │ │ └── TestGlobal │ │ │ └── index.vue │ ├── localComponents │ │ ├── TestLocal │ │ │ └── index.vue │ │ ├── CopyText │ │ │ └── index.vue │ │ └── ScrollList │ │ │ └── index.vue │ └── registerGlobComp.js ├── styles │ ├── variables.less │ ├── vant.less │ ├── animation.less │ ├── tab-nav.less │ ├── base.less │ └── index.less ├── views │ ├── exception │ │ └── NotFound.vue │ ├── login │ │ └── index.vue │ ├── shopping │ │ └── index.vue │ ├── mine │ │ └── index.vue │ ├── integral │ │ └── index.vue │ ├── category │ │ └── index.vue │ └── index │ │ └── index.vue ├── api │ └── index.js ├── directives │ ├── index.js │ ├── rotate.js │ └── copy.js ├── utils │ ├── auth.js │ ├── dynamicTitle.js │ ├── copyPaste.js │ ├── proxy.js │ ├── validate.js │ ├── index.js │ ├── request.js │ └── wxJssdkTools.js ├── hooks │ └── useLoading.js ├── store │ ├── modules │ │ ├── settings.js │ │ └── user.js │ └── index.js ├── main.js ├── App.vue └── router │ └── index.js ├── postcss.config.cjs ├── mock ├── _createProductionServer.js ├── user.js └── index.js ├── .gitignore ├── .env ├── index.html ├── .env.development ├── .env.production ├── package.json ├── public └── vite.svg ├── vite.config.js └── README.md /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"] 3 | } 4 | -------------------------------------------------------------------------------- /src/assets/icons/category.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junfeng-git/vue3-vant4-vite-pinia-pro-h5/HEAD/src/assets/icons/category.png -------------------------------------------------------------------------------- /src/assets/icons/TAB/首页/新备份@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junfeng-git/vue3-vant4-vite-pinia-pro-h5/HEAD/src/assets/icons/TAB/首页/新备份@2x.png -------------------------------------------------------------------------------- /src/assets/images/empty/no_data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junfeng-git/vue3-vant4-vite-pinia-pro-h5/HEAD/src/assets/images/empty/no_data.png -------------------------------------------------------------------------------- /src/assets/images/empty/no_goods.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junfeng-git/vue3-vant4-vite-pinia-pro-h5/HEAD/src/assets/images/empty/no_goods.png -------------------------------------------------------------------------------- /src/assets/images/empty/no_login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junfeng-git/vue3-vant4-vite-pinia-pro-h5/HEAD/src/assets/images/empty/no_login.png -------------------------------------------------------------------------------- /src/assets/images/empty/no_order.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junfeng-git/vue3-vant4-vite-pinia-pro-h5/HEAD/src/assets/images/empty/no_order.png -------------------------------------------------------------------------------- /src/assets/images/empty/no_address.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junfeng-git/vue3-vant4-vite-pinia-pro-h5/HEAD/src/assets/images/empty/no_address.png -------------------------------------------------------------------------------- /src/assets/images/empty/no_collect.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junfeng-git/vue3-vant4-vite-pinia-pro-h5/HEAD/src/assets/images/empty/no_collect.png -------------------------------------------------------------------------------- /src/assets/images/empty/no_look_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junfeng-git/vue3-vant4-vite-pinia-pro-h5/HEAD/src/assets/images/empty/no_look_up.png -------------------------------------------------------------------------------- /src/assets/images/empty/no_network.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junfeng-git/vue3-vant4-vite-pinia-pro-h5/HEAD/src/assets/images/empty/no_network.png -------------------------------------------------------------------------------- /src/assets/images/empty/no_bank_card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junfeng-git/vue3-vant4-vite-pinia-pro-h5/HEAD/src/assets/images/empty/no_bank_card.png -------------------------------------------------------------------------------- /src/assets/images/empty/goods_delisting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/junfeng-git/vue3-vant4-vite-pinia-pro-h5/HEAD/src/assets/images/empty/goods_delisting.png -------------------------------------------------------------------------------- /src/components/globalComponents/NavBar/index.js: -------------------------------------------------------------------------------- 1 | import { withInstall } from "@/utils"; 2 | 3 | import navBar from "./index.vue"; 4 | 5 | export const JNavBar = withInstall(navBar); 6 | -------------------------------------------------------------------------------- /src/components/globalComponents/TabBar/index.js: -------------------------------------------------------------------------------- 1 | import { withInstall } from "@/utils"; 2 | 3 | import tabBar from "./index.vue"; 4 | 5 | export const TabBar = withInstall(tabBar); 6 | -------------------------------------------------------------------------------- /src/components/globalComponents/NoData/index.js: -------------------------------------------------------------------------------- 1 | import { withInstall } from "@/utils"; 2 | 3 | import noData from "./index.vue"; 4 | 5 | export const NoData = withInstall(noData); 6 | -------------------------------------------------------------------------------- /src/components/globalComponents/SvgIcon/index.js: -------------------------------------------------------------------------------- 1 | import { withInstall } from "@/utils"; 2 | 3 | import svgIcon from "./index.vue"; 4 | 5 | export const SvgIcon = withInstall(svgIcon); 6 | -------------------------------------------------------------------------------- /src/components/globalComponents/TabNav/index.js: -------------------------------------------------------------------------------- 1 | import { withInstall } from "@/utils"; 2 | 3 | import tabNav from "./index.vue"; 4 | 5 | export const TabNav = withInstall(tabNav); 6 | -------------------------------------------------------------------------------- /src/components/globalComponents/VideoPlayer/index.js: -------------------------------------------------------------------------------- 1 | import { withInstall } from "@/utils"; 2 | 3 | import videoPlayer from "./index.vue"; 4 | 5 | export const VideoPlayer = withInstall(videoPlayer); 6 | -------------------------------------------------------------------------------- /src/styles/variables.less: -------------------------------------------------------------------------------- 1 | // 全局 css 变量 2 | @goods-price: #ee141e; // 商品价格 3 | @gradient-color: linear-gradient( 4 | to right, 5 | #eea2a2 0%, 6 | #bbc1bf 19%, 7 | #57c6e1 42%, 8 | #b49fda 79%, 9 | #7ac5d8 100% 10 | ); 11 | -------------------------------------------------------------------------------- /src/views/exception/NotFound.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/api/index.js: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | 3 | // banner 列表 4 | export const getBannerList = (params) => request.get(`/goodsBanner/list`, { params }); 5 | // 菜单列表 6 | export const getMenuList = (params) => request.get(`/homeMenu/getList`, { params }); -------------------------------------------------------------------------------- /src/directives/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 配置和注册全局指令 3 | */ 4 | import { setupCopygDirective } from "./copy"; 5 | import { setupRotateDirective } from "./rotate"; 6 | 7 | export function setupGlobDirectives(app) { 8 | setupCopygDirective(app); 9 | setupRotateDirective(app); 10 | } 11 | -------------------------------------------------------------------------------- /src/styles/vant.less: -------------------------------------------------------------------------------- 1 | :root { 2 | --van-primary-color: #00bfc6; // 主题色 3 | --van-button-default-height: 32px; // 按钮默认高度 4 | --van-button-default-line-height: 32px; // 按钮默认行高 5 | } 6 | // 上面覆盖不了的,可以在这里覆盖样式 7 | :root:root { 8 | --van-nav-bar-height: 44px; // 顶部导航 高度 9 | } 10 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | // postcss.config.js 2 | module.exports = { 3 | plugins: { 4 | // postcss-pxtorem 插件的版本需要 >= 5.0.0 5 | "postcss-pxtorem": { 6 | rootValue({ file }) { 7 | return file.indexOf("vant") !== -1 ? 37.5 : 75; 8 | }, 9 | propList: ["*"], 10 | }, 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /src/utils/auth.js: -------------------------------------------------------------------------------- 1 | import Cookie from "js-cookie"; 2 | 3 | const Token = "token"; 4 | 5 | export function getToken() { 6 | return Cookie.get(Token); 7 | } 8 | 9 | export function setToken(token) { 10 | return Cookie.set(Token, token); 11 | } 12 | 13 | export function removeToken() { 14 | return Cookie.remove(Token); 15 | } 16 | -------------------------------------------------------------------------------- /src/styles/animation.less: -------------------------------------------------------------------------------- 1 | /** 2 | * 全局自定义动画 3 | */ 4 | 5 | // loading 360旋转动画 6 | @keyframes loading { 7 | 0% { 8 | transform: rotate3d(0, 0, 1, 0deg); 9 | } 10 | 11 | 100% { 12 | transform: rotate3d(0, 0, 1, 360deg); 13 | } 14 | } 15 | 16 | .v-rotate { 17 | animation: loading 1s steps(12, end) infinite; 18 | } 19 | -------------------------------------------------------------------------------- /mock/_createProductionServer.js: -------------------------------------------------------------------------------- 1 | // 生产环境下将不会导入mock 2 | import { createProdMockServer } from "vite-plugin-mock/es/createProdMockServer"; 3 | import indexMock from "./index"; 4 | import userMock from "./user"; 5 | export const mockModules = [...indexMock, ...userMock]; 6 | 7 | export function setupProdMockServer() { 8 | createProdMockServer(mockModules); 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /src/views/login/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/components/localComponents/TestLocal/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | 19 | -------------------------------------------------------------------------------- /src/components/globalComponents/TestGlobal/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | 19 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # port 2 | VITE_PORT = 3100 3 | 4 | # 网站标题 5 | VITE_GLOB_APP_TITLE = H5移动端模板 6 | 7 | # 简称,用于配置文件名字 不要出现空格、数字开头等特殊字符 8 | VITE_GLOB_APP_SHORT_NAME = 9 | 10 | # 是否开启动态标题 11 | VITE_GLOB_APP_DYNAMIC_TITLE = true 12 | 13 | # 是否开启单点登录 14 | VITE_GLOB_APP_OPEN_SSO = false 15 | 16 | # 开启微前端模式 17 | VITE_GLOB_APP_OPEN_QIANKUN=true 18 | 19 | # 文件预览地址 20 | VITE_GLOB_ONLINE_VIEW_URL=http://fileview.jeecg.com/onlinePreview -------------------------------------------------------------------------------- /src/views/shopping/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | 20 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= title %> 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/dynamicTitle.js: -------------------------------------------------------------------------------- 1 | import useSettingsStore from "@/store/modules/settings"; 2 | const defaultTitle = import.meta.env.VITE_GLOB_APP_TITLE; 3 | /** 4 | * 动态修改标题 5 | */ 6 | export function useDynamicTitle() { 7 | const settingsStore = useSettingsStore(); 8 | let _defaultTitle = defaultTitle ? " - " + defaultTitle : defaultTitle; 9 | if (settingsStore.dynamicTitle) { 10 | document.title = settingsStore.title + _defaultTitle; 11 | } else { 12 | document.title = defaultTitle; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # 是否打开mock 2 | VITE_USE_MOCK = true 3 | 4 | # 发布路径 5 | VITE_PUBLIC_PATH = ./ 6 | 7 | # 跨域代理,您可以配置多个 ,请注意,没有换行符 8 | VITE_PROXY = [["/api","http://localhost:3100"],["/upload","http://localhost:3100/upload"]] 9 | 10 | # 控制台不输出 11 | VITE_DROP_CONSOLE = false 12 | 13 | # 是否开启调试工具 Vconsole 14 | VITE_OPEN_VCONSOLE = false 15 | 16 | # H5接口父地址(必填) 17 | VITE_GLOB_API_URL=/api 18 | 19 | # H5接口全路径地址(必填) 20 | VITE_GLOB_BASE_URL=https://xxxxx.com/v1 # 测试服接口 21 | 22 | # 接口前缀 23 | VITE_GLOB_API_URL_PREFIX= 24 | -------------------------------------------------------------------------------- /src/hooks/useLoading.js: -------------------------------------------------------------------------------- 1 | import { showLoadingToast } from "vant"; 2 | import { onBeforeUnmount } from "vue"; 3 | 4 | export function useLoading() { 5 | let toast = null; 6 | const startLoading = () => { 7 | toast = showLoadingToast({ 8 | duration: 0, 9 | forbidClick: true, 10 | message: "加载中...", 11 | overlay: true, 12 | }); 13 | }; 14 | const stopLoading = () => { 15 | toast && toast.close(); 16 | }; 17 | 18 | onBeforeUnmount(stopLoading); 19 | 20 | return { startLoading, stopLoading }; 21 | } 22 | -------------------------------------------------------------------------------- /src/store/modules/settings.js: -------------------------------------------------------------------------------- 1 | import { useDynamicTitle } from "@/utils/dynamicTitle"; 2 | import { defineStore } from "pinia"; 3 | 4 | // 是否开启动态标题 5 | const dynamicTitle = import.meta.env.VITE_GLOB_APP_DYNAMIC_TITLE === "true"; 6 | 7 | const useSettingsStore = defineStore("settings", { 8 | state: () => ({ 9 | title: "", 10 | dynamicTitle, 11 | }), 12 | actions: { 13 | // 设置网页标题 14 | setTitle(title) { 15 | this.title = title; 16 | useDynamicTitle(); 17 | }, 18 | }, 19 | }); 20 | 21 | export default useSettingsStore; 22 | -------------------------------------------------------------------------------- /src/components/globalComponents/VideoPlayer/svg/play_icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createPinia, defineStore } from "pinia"; 2 | // 引入持久化插件 3 | import piniaPluginPersistedstate from "pinia-plugin-persistedstate"; 4 | // 实例化pinia 5 | export const store = createPinia(); 6 | // 使用持久化存储插件 7 | store.use(piniaPluginPersistedstate); 8 | 9 | // 系统统一存储 10 | export const useAppStore = defineStore("app", { 11 | state: () => ({ 12 | appName: "我是app名称", 13 | }), 14 | actions: { 15 | // 设置app名称 16 | setAppName(data) { 17 | this.appName = data; 18 | }, 19 | }, 20 | persist: { 21 | storage: localStorage, //default localStorage 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /src/store/modules/user.js: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia"; //引入pinia 2 | 3 | // 这里官网是单独导出 是可以写成默认导出的 官方的解释为大家一起约定仓库用use打头的单词 4 | export const useUserStore = defineStore("user", { 5 | state: () => ({ 6 | nickname: "少年的范儿", 7 | userInfo: null, // 用户信息 8 | role: undefined, // 用户角色 9 | token: "哈哈", // token 10 | }), 11 | actions: { 12 | // 设置网页标题 13 | setToken(data) { 14 | this.token = data; 15 | }, 16 | setNickname(data) { 17 | this.nickname = data; 18 | }, 19 | }, 20 | persist: { 21 | storage: localStorage, //default localStorage 22 | }, 23 | }); 24 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "./App.vue"; 3 | import { ConfigProvider } from "vant"; 4 | import { setupGlobDirectives } from "./directives"; 5 | import { registerGlobComp } from "@/components/registerGlobComp"; 6 | const app = createApp(App); 7 | 8 | // 引入注册脚本 - svg 9 | import "virtual:svg-icons-register"; 10 | // px 自动转 rem 插件 11 | import "lib-flexible"; 12 | // 引入路由 13 | import router from "@/router/index"; 14 | // 引入 pinia 15 | import { store } from "@/store/index"; 16 | // 注册全局指令 17 | setupGlobDirectives(app); 18 | // 注册全局组件 19 | registerGlobComp(app); 20 | 21 | app.use(ConfigProvider).use(router).use(store).mount("#app"); 22 | -------------------------------------------------------------------------------- /src/utils/copyPaste.js: -------------------------------------------------------------------------------- 1 | function copyPaste(bool) { 2 | // 右键菜单 3 | document.oncontextmenu = function () { 4 | return bool; 5 | }; 6 | 7 | // 文字选择 8 | document.onselectstart = function () { 9 | return bool; 10 | }; 11 | 12 | // 复制 13 | document.oncopy = function () { 14 | return bool; 15 | }; 16 | 17 | // 剪切 18 | document.oncut = function () { 19 | return bool; 20 | }; 21 | 22 | // 粘贴 23 | document.onpaste = function () { 24 | return bool; 25 | }; 26 | } 27 | 28 | function enable() { 29 | copyPaste(true); 30 | } 31 | 32 | function disable() { 33 | copyPaste(false); 34 | } 35 | 36 | export default { enable, disable }; 37 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # 是否启用mock 2 | VITE_USE_MOCK = false 3 | 4 | # 发布路径 5 | VITE_PUBLIC_PATH = ./ 6 | 7 | # 控制台不输出 8 | VITE_DROP_CONSOLE = true 9 | 10 | # 是否开启调试工具 Vconsole 11 | VITE_OPEN_VCONSOLE = false 12 | 13 | # 是否启用gzip或brotli压缩 14 | # 选项值: gzip | brotli | none 15 | # 如果需要多个可以使用“,”分隔 16 | VITE_BUILD_COMPRESS = 'none' 17 | 18 | # 使用压缩时是否删除原始文件,默认为false 19 | VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false 20 | 21 | # H5接口父地址(必填) 22 | VITE_GLOB_API_URL=/api 23 | 24 | # H5接口全路径地址(必填) 25 | VITE_GLOB_BASE_URL=https://xxxxx.com/v1 26 | 27 | # 接口父路径前缀 28 | VITE_GLOB_API_URL_PREFIX= 29 | 30 | # 是否启用图像压缩 31 | VITE_USE_IMAGEMIN= true 32 | 33 | # 使用pwa 34 | VITE_USE_PWA = false 35 | 36 | # 是否兼容旧浏览器 37 | VITE_LEGACY = false 38 | -------------------------------------------------------------------------------- /src/utils/proxy.js: -------------------------------------------------------------------------------- 1 | const httpsRE = /^https:\/\//; 2 | /** 3 | * Generate proxy 4 | * @param list 5 | */ 6 | // export function createProxy(list = []) { 7 | // // console.log('list:', list, typeof JSON.parse(list)); 8 | // const ret = {}; 9 | // for (const [prefix, target] of JSON.parse(list)) { 10 | // const isHttps = httpsRE.test(target); 11 | 12 | // // https://github.com/http-party/node-http-proxy#options 13 | // ret[prefix] = { 14 | // target: target, 15 | // changeOrigin: true, 16 | // ws: true, 17 | // rewrite: (path) => path.replace(new RegExp(`^${prefix}`), ""), 18 | // // https is require secure=false 19 | // ...(isHttps ? { secure: false } : {}), 20 | // }; 21 | // } 22 | // return ret; 23 | // } 24 | -------------------------------------------------------------------------------- /src/assets/svg/delete.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/globalComponents/VideoPlayer/svg/pause_icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mock/user.js: -------------------------------------------------------------------------------- 1 | //createUserList:次函数执行会返回一个数组 2 | function createUserList() { 3 | return [ 4 | { 5 | userId: 1, 6 | username: "admin", 7 | password: "123456", 8 | token: "ZHESHICESHITOKEN", 9 | }, 10 | ]; 11 | } 12 | export default [ 13 | { 14 | url: "/api/user/login", 15 | method: "post", 16 | response: ({ body }) => { 17 | //获取请求体携带过来的用户名与密码 18 | const { username, password } = body; 19 | //调用获取用户信息函数,用于判断是否有此用户 20 | const checkUser = createUserList().find( 21 | (item) => item.username === username && item.password === password 22 | ); 23 | //没有用户返回失败信息 24 | if (!checkUser) { 25 | return { code: 201, data: { message: "账号或者密码不正确" } }; 26 | } 27 | //如果有返回成功信息 28 | const { token } = checkUser; 29 | return { code: 200, data: { token } }; 30 | }, 31 | }, 32 | ]; 33 | -------------------------------------------------------------------------------- /src/utils/validate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * url 校验 3 | * @param {string} path 4 | * @returns {Boolean} 5 | */ 6 | export function isExternal(path) { 7 | return /^(https?:|mailto:|tel:)/.test(path); 8 | } 9 | 10 | /** 11 | * 手机号 校验 12 | * @param {string} tel 13 | * @returns {Boolean} 14 | */ 15 | export function isPhoneNumber(tel) { 16 | return /^1(3\d|4[5-9]|5[0-35-9]|6[567]|7[0-8]|8\d|9[0-35-9])\d{8}$/.test(tel); 17 | } 18 | 19 | /** 20 | * 判断 当前设备是否是 ios系统 21 | * @returns {Boolean} 22 | */ 23 | export function isIos() { 24 | return !!navigator.userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); 25 | } 26 | 27 | /** 28 | * 判断 当前设备环境是否是微信环境 29 | * @returns Boolean true: 表示在微信环境 false: 表示不在微信环境 30 | */ 31 | export const isWechatEnvironment = () => { 32 | const ua = window.navigator.userAgent.toLowerCase(); 33 | return ua.match(/MicroMessenger/i) == "micromessenger"; 34 | }; 35 | -------------------------------------------------------------------------------- /src/assets/svg/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/localComponents/CopyText/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/directives/rotate.js: -------------------------------------------------------------------------------- 1 | const rotateDirective = { 2 | /** 3 | * mounted 钩子函数,在绑定元素的父组件及他自己的所有子节点都挂载完成后调用 4 | * el: 指令绑定到的元素。这可以用于直接操作 DOM。 5 | * value: 传给指令的值,也就是我们要 copy 的值 6 | */ 7 | mounted(el, { value }) { 8 | // console.log("value==:", value, typeof value); 9 | el.$value = value; // 用一个全局属性来存传进来的值,因为这个值在别的钩子函数里还会用到 10 | // el.className = 'v-rotate' 11 | el.classList.add("v-rotate"); 12 | if (typeof value === "undefined") return; 13 | el.style.display = value ? "block" : "none"; 14 | }, 15 | // 在绑定元素的父组件及他自己的所有子节点都更新后调用 16 | updated(el, { value }) { 17 | // console.log("更新触发", value); 18 | if (typeof value === "undefined") return; 19 | el.style.display = value ? "block" : "none"; 20 | }, 21 | // 指令与元素解绑后,移除事件绑定 22 | unmounted(el) { 23 | el.classList.remove("v-rotate"); 24 | }, 25 | }; 26 | 27 | export function setupRotateDirective(app) { 28 | app.directive("rotate", rotateDirective); 29 | } 30 | 31 | export default rotateDirective; 32 | -------------------------------------------------------------------------------- /src/assets/icons/un_home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Icon/Tab Bar/首页/置灰@2x 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/svg/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 24 | 38 | -------------------------------------------------------------------------------- /src/components/globalComponents/VideoPlayer/svg/fullscreen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svg/copy.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/mine.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Icon/Tab Bar/我的/高亮@2x 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/components/globalComponents/SvgIcon/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 44 | 50 | -------------------------------------------------------------------------------- /src/assets/svg/search_grey.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue3-vant4-vite-pinia-pro-h5", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "dev:mock": "cross-env USE_MOCK=true vite", 10 | "build:mock": "cross-env USE_MOCK=true vite build", 11 | "preview": "pnpm run build && vite preview" 12 | }, 13 | "dependencies": { 14 | "axios": "^1.4.0", 15 | "js-cookie": "^3.0.5", 16 | "nprogress": "^0.2.0", 17 | "pinia": "^2.0.35", 18 | "pinia-plugin-persistedstate": "^3.2.1", 19 | "vant": "^4.6.0", 20 | "vue": "^3.2.47", 21 | "vue-clipboard3": "^2.0.0", 22 | "vue-router": "^4.2.2" 23 | }, 24 | "devDependencies": { 25 | "@vitejs/plugin-vue": "^4.1.0", 26 | "cross-env": "^7.0.3", 27 | "less": "^4.1.3", 28 | "lib-flexible": "^0.3.2", 29 | "mockjs": "^1.1.0", 30 | "postcss-pxtorem": "^6.0.0", 31 | "unplugin-vue-components": "^0.25.1", 32 | "vconsole": "^3.15.1", 33 | "vite": "^4.3.9", 34 | "vite-plugin-html": "^3.2.0", 35 | "vite-plugin-mock": "2.9.6", 36 | "vite-plugin-svg-icons": "^2.0.1", 37 | "vite-plugin-vconsole": "^1.3.1" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/assets/svg/shopping.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/tab-nav.less: -------------------------------------------------------------------------------- 1 | // ###该样式文件的作用:tab-nav 导航内部 样式布局 - 所有的导航样式都在这里统一设置,方便管理 2 | 3 | // ### 起类名 规范: 一般两种情况 4 | // 1. 一个页面一个 tab 导航 5 | // custom-[name]-tab-nav-text name: 根据当前页面路由名称来定义,这样可以防止重复 6 | // 列如:在个人中心页 (路由名称是 mine)此时这样定义类:.custom-mine-tab-nav-text 7 | 8 | // 2. 一个页面 两个及以上的 tab 导航 9 | // custom-[name]-[role]-tab-nav-text name: 同上 role: 该 tab导航的作用 10 | // 列如:在个人中心页 (路由名称是 mine),有一个导航是分类导航,此时这样定义类:.custom-mine-classify-tab-nav-text 11 | 12 | // 个人中心导航 13 | .custom-mine-tab-nav-text { 14 | padding: 0px 8px; 15 | height: 24px; 16 | line-height: 24px; 17 | box-sizing: border-box; 18 | border-radius: 22px; 19 | background-color: #f5f5f5; 20 | color: #666666; 21 | &.active { 22 | color: var(--van-primary-color); 23 | background-color: #edfafa; 24 | } 25 | } 26 | 27 | // 个人中心分类导航 28 | .custom-mine-classify-tab-nav-text { 29 | color: #666666; 30 | font-size: 18px; 31 | &.active { 32 | position: relative; 33 | font-size: 22px; 34 | color: #101010; 35 | font-weight: 600; 36 | &::after { 37 | position: absolute; 38 | content: ""; 39 | left: 0; 40 | bottom: 0; 41 | width: 100%; 42 | height: 10px; 43 | background: linear-gradient( 44 | 180deg, 45 | rgba(0, 191, 198, 0) 0%, 46 | rgba(0, 191, 198, 0.9) 100% 47 | ); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/assets/svg/special_subject.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/icons/un_category.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 切片 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/assets/icons/home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 切片 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/styles/base.less: -------------------------------------------------------------------------------- 1 | /*! minireset.css v0.0.4 | MIT License | github.com/jgthms/minireset.css */ 2 | html, 3 | body, 4 | p, 5 | ol, 6 | ul, 7 | li, 8 | dl, 9 | dt, 10 | dd, 11 | blockquote, 12 | figure, 13 | fieldset, 14 | legend, 15 | textarea, 16 | pre, 17 | iframe, 18 | hr, 19 | h1, 20 | h2, 21 | h3, 22 | h4, 23 | h5, 24 | h6 { 25 | margin: 0; 26 | padding: 0; 27 | } 28 | 29 | h1, 30 | h2, 31 | h3, 32 | h4, 33 | h5, 34 | h6 { 35 | font-size: 100%; 36 | font-weight: normal; 37 | } 38 | 39 | ul { 40 | list-style: none; 41 | } 42 | 43 | button, 44 | input, 45 | select, 46 | textarea { 47 | margin: 0; 48 | } 49 | 50 | html { 51 | box-sizing: border-box; 52 | } 53 | 54 | *, 55 | *:before, 56 | *:after { 57 | box-sizing: inherit; 58 | } 59 | 60 | img, 61 | embed, 62 | iframe, 63 | object, 64 | video { 65 | height: auto; 66 | max-width: 100%; 67 | } 68 | 69 | img { 70 | display: block; 71 | } 72 | 73 | audio { 74 | max-width: 100%; 75 | } 76 | 77 | iframe { 78 | border: 0; 79 | } 80 | 81 | table { 82 | border-collapse: collapse; 83 | border-spacing: 0; 84 | } 85 | 86 | td, 87 | th { 88 | padding: 0; 89 | text-align: left; 90 | } 91 | 92 | html, 93 | body { 94 | width: 100%; 95 | font-size: 14px; 96 | overflow-x: hidden; 97 | overflow-y: scroll; 98 | } 99 | 100 | #app { 101 | font-size: 14px; 102 | height: 100%; 103 | } 104 | 105 | #root { 106 | height: 100%; 107 | width: 100%; 108 | } 109 | -------------------------------------------------------------------------------- /src/components/globalComponents/VideoPlayer/svg/play.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svg/album.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/styles/index.less: -------------------------------------------------------------------------------- 1 | @import "./base.less"; 2 | @import "./tab-nav.less"; 3 | @import "./animation.less"; 4 | @import "./vant.less"; 5 | @import "./variables.less"; 6 | 7 | .container { 8 | // min-height: 100vh; 9 | height: 100%; 10 | } 11 | 12 | // 单行文本溢出 13 | .single-line-overflow { 14 | white-space: nowrap; 15 | overflow: hidden; 16 | text-overflow: ellipsis; 17 | } 18 | 19 | // 多行文本溢出 20 | .multiline-overflow { 21 | text-overflow: -o-ellipsis-lastline; 22 | overflow: hidden; 23 | text-overflow: ellipsis; 24 | display: -webkit-box; 25 | -webkit-line-clamp: 2; 26 | -webkit-box-orient: vertical; 27 | } 28 | 29 | // 隐藏 x 轴滚动条 30 | .hide-x-scrollbar { 31 | height: 100%; 32 | overflow-x: scroll; 33 | overflow-y: hidden; 34 | &::-webkit-scrollbar { 35 | display: none; 36 | } 37 | } 38 | 39 | // 隐藏 y 轴滚动条 40 | .hide-y-scrollbar { 41 | height: 100%; 42 | overflow-x: hidden; 43 | overflow-y: scroll; 44 | &::-webkit-scrollbar { 45 | display: none; 46 | } 47 | } 48 | 49 | // 0.5像素的 分割线 50 | .hairline { 51 | position: relative; 52 | &::after { 53 | position: absolute; 54 | box-sizing: border-box; 55 | content: " "; 56 | pointer-events: none; 57 | top: -50%; 58 | right: -50%; 59 | bottom: -50%; 60 | left: -50%; 61 | border: 0 solid #ebedf0; 62 | transform: scale(0.5); 63 | border-top-width: 1px; 64 | } 65 | } 66 | 67 | // 进度条样式修改 - 开始 68 | #nprogress .bar { 69 | background: var(--van-primary-color) !important; //自定义颜色 70 | } 71 | #nprogress .spinner { 72 | display: none !important; 73 | } 74 | // 进度条样式修改 - 结束 75 | -------------------------------------------------------------------------------- /src/assets/icons/un_shopping.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 切片 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/assets/icons/shopping.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 切片 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/components/globalComponents/VideoPlayer/svg/video_audio_on.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | 20 | -------------------------------------------------------------------------------- /src/components/registerGlobComp.js: -------------------------------------------------------------------------------- 1 | // vant ui 库的组件 2 | import { 3 | Button, Swipe, 4 | SwipeItem, 5 | Skeleton, 6 | SkeletonTitle, 7 | SkeletonImage, 8 | SkeletonAvatar, 9 | SkeletonParagraph, 10 | Tabbar, TabbarItem, 11 | Icon, 12 | NavBar, 13 | Tab, Tabs, Empty, 14 | Sidebar, SidebarItem, 15 | IndexBar, IndexAnchor, 16 | Cell, CellGroup, 17 | Loading, 18 | ShareSheet, 19 | ImagePreview, 20 | ActionBar, ActionBarIcon, ActionBarButton 21 | } from 'vant'; 22 | 23 | // 自定义的组件 - 注册全局组件 24 | import { JNavBar } from './globalComponents/NavBar'; 25 | import { TabBar } from './globalComponents/TabBar'; 26 | import { VideoPlayer } from './globalComponents/VideoPlayer'; 27 | import { SvgIcon } from './globalComponents/SvgIcon'; 28 | import { TabNav } from './globalComponents/TabNav'; 29 | import { NoData } from './globalComponents/NoData'; 30 | 31 | const compList = [JNavBar, TabBar, VideoPlayer, SvgIcon, TabNav, NoData]; 32 | 33 | // vant ui 库的组件 - 注册全局组件 34 | export function registerGlobComp(app) { 35 | compList.forEach((comp) => { 36 | app.component(comp.name, comp); 37 | }); 38 | 39 | app.use(Button) 40 | .use(Swipe) 41 | .use(SwipeItem) 42 | .use(Skeleton) 43 | .use(SkeletonTitle) 44 | .use(SkeletonImage) 45 | .use(SkeletonAvatar) 46 | .use(SkeletonParagraph) 47 | .use(Tabbar) 48 | .use(TabbarItem) 49 | .use(Icon) 50 | .use(NavBar) 51 | .use(Tab) 52 | .use(Tabs) 53 | .use(Empty) 54 | .use(Sidebar) 55 | .use(SidebarItem) 56 | .use(IndexBar) 57 | .use(IndexAnchor) 58 | .use(Cell) 59 | .use(CellGroup) 60 | .use(Loading) 61 | .use(ShareSheet) 62 | .use(ImagePreview) 63 | .use(ActionBar) 64 | .use(ActionBarIcon) 65 | .use(ActionBarButton) 66 | } 67 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 获取当前 url 参数的 value 3 | * @param {*} name 参数的 key 4 | * @param {*} url 目标 url, 不传会获取当前页面的 url 5 | * @returns 参数的 value 6 | */ 7 | export const getUrlParam = (name, url) => { 8 | url = url || window.location.href; 9 | var res = {}; 10 | url.replace(/[?|&](\w+)=([^&#]*)/g, (all, key, val) => { 11 | res[key] = val; 12 | return all; 13 | }); 14 | return res[name] || ""; 15 | }; 16 | 17 | /** 18 | * 一键复制 【不推荐使用,可以用自定义指令(已封装),去 README.md 文件查看】 19 | * @param {string} str 需要复制的文本 20 | * @returns String 复制状态 success: 成功 fail: 失败 21 | */ 22 | export function useCopyToClipboard(str) { 23 | return new Promise((resolve, reject) => { 24 | // 使用textarea的原因是能进行换行,input不支持换行 25 | var copyTextArea = document.createElement("textarea"); 26 | // 将该 textarea 设为 readonly 防止 iOS 下自动唤起键盘,同时将 textarea 移出可视区域 27 | copyTextArea.readOnly = "readonly"; 28 | copyTextArea.style.position = "absolute"; 29 | copyTextArea.style.left = "-9999px"; 30 | copyTextArea.value = str; 31 | document.body.appendChild(copyTextArea); 32 | copyTextArea.select(); 33 | try { 34 | var copyed = document.execCommand("copy"); 35 | if (copyed) { 36 | document.body.removeChild(copyTextArea); 37 | resolve("success"); 38 | } 39 | } catch { 40 | reject("fail"); 41 | } 42 | }); 43 | } 44 | /** 45 | * 注册组件 46 | * @param {*} component 47 | * @param {*} alias 48 | * @returns 49 | */ 50 | export const withInstall = (component, alias) => { 51 | const comp = component; 52 | comp.install = (app) => { 53 | app.component(comp.name || comp.displayName, component); 54 | if (alias) { 55 | app.config.globalProperties[alias] = component; 56 | } 57 | }; 58 | return component; 59 | }; 60 | -------------------------------------------------------------------------------- /src/components/globalComponents/VideoPlayer/svg/video_audio_off.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20 | 21 | -------------------------------------------------------------------------------- /src/assets/icons/un_mine.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 切片 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/assets/svg/customer_service.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import router from "@/router"; 3 | import { showToast } from "vant"; 4 | 5 | import { getToken, removeToken } from "@/utils/auth"; 6 | 7 | import nprogress from "nprogress"; 8 | 9 | import "nprogress/nprogress.css"; 10 | 11 | const { VITE_GLOB_BASE_URL, VITE_GLOB_API_URL, DEV, VITE_USE_MOCK, VITE_PORT } = 12 | import.meta.env; 13 | 14 | const isUseMock = VITE_USE_MOCK === "true"; // 是否开启mock 15 | const baseURL = 16 | DEV && isUseMock 17 | ? `http://localhost:${VITE_PORT}${VITE_GLOB_API_URL}` 18 | : VITE_GLOB_BASE_URL; 19 | 20 | // 创建axios实例 21 | const service = axios.create({ 22 | baseURL, // URL地址 23 | timeout: 120 * 1000, // 超时时间 24 | }); 25 | 26 | // 添加请求拦截器 27 | service.interceptors.request.use( 28 | (config) => { 29 | nprogress.start(); 30 | config.headers = { 31 | nonce: Date.parse(new Date()), 32 | clientType: "H5", 33 | clientVersion: "2.2.1", 34 | timestamp: Date.parse(new Date()), 35 | Authorization: getToken() ? "Bearer " + getToken() : undefined, 36 | }; 37 | return Promise.resolve(config); 38 | }, 39 | (error) => { 40 | return Promise.reject(error); 41 | } 42 | ); 43 | // 添加响应拦截器 44 | service.interceptors.response.use( 45 | (response) => { 46 | const res = response.data; 47 | if (res.code != 1) { 48 | // 500005 用户没有token或者过期 49 | if (res.code == 500005) { 50 | removeToken(); 51 | localStorage.removeItem("userInfo"); 52 | router.replace({ path: "/login" }); 53 | nprogress.done(); 54 | } 55 | if (res.code == 100001 || res.code == 2000) { 56 | nprogress.done(); 57 | showToast(res.msg); 58 | } 59 | return Promise.reject(res.msg); 60 | } else { 61 | nprogress.done(); 62 | return Promise.resolve(res); 63 | } 64 | }, 65 | (error) => { 66 | return Promise.reject(error); 67 | } 68 | ); 69 | export default service; 70 | -------------------------------------------------------------------------------- /src/components/globalComponents/NoData/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 50 | 51 | 71 | -------------------------------------------------------------------------------- /src/directives/copy.js: -------------------------------------------------------------------------------- 1 | import { showToast, showSuccessToast, showFailToast } from "vant"; 2 | 3 | const copyDirective = { 4 | /** 5 | * mounted 钩子函数,在绑定元素的父组件及他自己的所有子节点都挂载完成后调用 6 | * el: 指令绑定到的元素。这可以用于直接操作 DOM。 7 | * value: 传给指令的值,也就是我们要 copy 的值 8 | */ 9 | mounted(el, { value }) { 10 | console.log("value==:", value); 11 | el.$value = value; // 用一个全局属性来存传进来的值,因为这个值在别的钩子函数里还会用到 12 | el.handler = () => { 13 | console.log("触发了:", el.$value); 14 | if (!el.$value) { 15 | // 值为空的时候,给出提示,我这里的提示是用的 vant 的提示,你们随意 16 | showToast("无复制内容"); 17 | return; 18 | } 19 | // 动态创建 textarea 标签 20 | const textarea = document.createElement("textarea"); 21 | // 将该 textarea 设为 readonly 防止 iOS 下自动唤起键盘,同时将 textarea 移出可视区域 22 | textarea.readOnly = "readonly"; 23 | textarea.style.position = "absolute"; 24 | textarea.style.left = "-9999px"; 25 | // 将要 copy 的值赋给 textarea 标签的 value 属性 26 | textarea.value = el.$value; 27 | // 将 textarea 插入到 body 中 28 | document.body.appendChild(textarea); 29 | console.log("textarea.value:", textarea.value); 30 | // 选中值并复制 31 | textarea.select(); 32 | textarea.setSelectionRange(0, textarea.value.length); 33 | const result = document.execCommand("copy"); 34 | console.log("result", result); 35 | if (result) { 36 | showSuccessToast("复制成功"); 37 | } else { 38 | showFailToast("复制失败"); 39 | } 40 | document.body.removeChild(textarea); 41 | }; 42 | // 绑定点击事件,就是所谓的一键 copy 啦 43 | el.addEventListener("click", el.handler); 44 | }, 45 | // 在绑定元素的父组件及他自己的所有子节点都更新后调用 46 | updated(el, { value }) { 47 | console.log("更新触发"); 48 | el.$value = value; 49 | }, 50 | // 指令与元素解绑后,移除事件绑定 51 | unmounted(el) { 52 | el.removeEventListener("click", el.handler); 53 | }, 54 | }; 55 | 56 | export function setupCopygDirective(app) { 57 | app.directive("copy", copyDirective); 58 | } 59 | 60 | export default copyDirective; 61 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from "vue-router"; 2 | import useSettingsStore from "@/store/modules/settings"; 3 | 4 | let routes = [ 5 | { path: "/", redirect: "/index" }, 6 | { 7 | path: "/index", 8 | name: "Index", 9 | meta: { title: "首页", keepAlive: true }, 10 | component: () => import("@/views/index/index.vue"), 11 | }, 12 | { 13 | path: "/login", 14 | name: "Login", 15 | meta: { title: "登录" }, 16 | component: () => import("@/views/login/index.vue"), 17 | }, 18 | { 19 | path: "/category", 20 | name: "Category", 21 | meta: { title: "分类" }, 22 | component: () => import("@/views/category/index.vue"), 23 | }, 24 | { 25 | path: "/shopping", 26 | name: "Shopping", 27 | meta: { title: "购物车" }, 28 | component: () => import("@/views/shopping/index.vue"), 29 | }, 30 | { 31 | path: "/mine", 32 | name: "Mine", 33 | meta: { title: "我的" }, 34 | component: () => import("@/views/mine/index.vue"), 35 | }, 36 | { 37 | path: "/integral", 38 | name: "Integral", 39 | meta: { title: "积分商城", keepAlive: true }, 40 | component: () => import("@/views/integral/index.vue"), 41 | }, 42 | { 43 | // 配置404页面 44 | path: "/:catchAll(.*)", 45 | name: "NotFound", 46 | component: () => import("@/views/exception/NotFound.vue"), 47 | }, 48 | ]; 49 | // 路由 50 | const router = createRouter({ 51 | history: createWebHashHistory(), 52 | routes, 53 | // scrollBehavior: () => ({ left: 0, top: 0 }), 54 | scrollBehavior(to, from, savePosition) { 55 | // console.log('savePosition:', savePosition); 56 | if (savePosition) { 57 | // 解决页面从列表页跳转到详情页返回,初始在原来位置 58 | return savePosition; 59 | } else { 60 | // 解决页面跳转后页面高度和前一个页面高度一样 61 | return { left: 0, top: 0 }; 62 | } 63 | }, 64 | }); 65 | 66 | // 导航守卫 67 | router.beforeEach((to, from, next) => { 68 | to.meta.title && useSettingsStore().setTitle(to.meta.title); 69 | next(); 70 | }); 71 | // 导出 72 | export default router; 73 | -------------------------------------------------------------------------------- /src/views/mine/index.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 70 | 71 | 96 | -------------------------------------------------------------------------------- /src/components/globalComponents/TabBar/index.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /mock/index.js: -------------------------------------------------------------------------------- 1 | //模拟返回10条用户信息+ 2 | export default [ 3 | { 4 | url: "/api/userInfo", //匹配到指定url 5 | method: "get", //请求类型 6 | response: () => { 7 | return { 8 | code: 1, 9 | msg: "操作成功", 10 | "result|10": { 11 | userName: "少年的范儿", 12 | phone: "18388888888", 13 | password: "123456", 14 | desc: "坚持,绝不轻易放弃!", 15 | avatar: 16 | "https://profile-avatar.csdnimg.cn/50dba479a529427cbb47770829e8d2b0_m0_49045925.jpg!1", 17 | token: `Bearer ${"@word(32)"}`, 18 | }, 19 | }; 20 | }, 21 | }, 22 | { 23 | url: "/homeMenu/getList", // 菜单列表 24 | method: "get", //请求类型 25 | response: () => { 26 | return { 27 | code: 1, 28 | msg: "操作成功", 29 | "result|16": [ 30 | { 31 | menuName: "@cword(3,3)", //100-200之间的随机整数 32 | jumpType: "@natural(1,11)", 33 | jumpUrl: "@url(https)", 34 | menuImageUrl: "@image(44, @color, 我是文本)", 35 | }, 36 | ], 37 | }; 38 | }, 39 | }, 40 | { 41 | url: "/goodsBanner/list", // banner 列表 42 | method: "get", //请求类型 43 | response: () => { 44 | return { 45 | code: 1, 46 | msg: "操作成功", 47 | "result|6": [ 48 | { 49 | bannerType: "@natural(1,5)", 50 | jumpUrl: "@url(https)", 51 | "url|1": [ 52 | "https://img1.baidu.com/it/u=1063627317,4109173401&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500", 53 | "https://img1.baidu.com/it/u=2734240624,2848071286&fm=253&fmt=auto&app=138&f=JPEG?w=749&h=500", 54 | "https://img1.baidu.com/it/u=900329638,1715201440&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500", 55 | "https://img0.baidu.com/it/u=4162443464,2854908495&fm=253&fmt=auto&app=138&f=JPEG?w=800&h=500", 56 | "https://img0.baidu.com/it/u=1931706057,3494340607&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=313", 57 | "https://img1.baidu.com/it/u=1876627393,303388089&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=313", 58 | "https://img1.baidu.com/it/u=1393608433,1663672027&fm=253&fmt=auto&app=138&f=JPEG?w=750&h=500", 59 | "https://img0.baidu.com/it/u=3643895624,2552772604&fm=253&fmt=auto&app=120&f=JPEG?w=1200&h=675", 60 | "https://img2.baidu.com/it/u=3750649395,919624016&fm=253&fmt=auto&app=138&f=PNG?w=667&h=500", 61 | ], 62 | }, 63 | ], 64 | }; 65 | }, 66 | }, 67 | ]; 68 | -------------------------------------------------------------------------------- /src/components/globalComponents/NavBar/index.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 95 | 96 | 101 | -------------------------------------------------------------------------------- /src/components/globalComponents/TabNav/index.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 100 | 101 | 106 | -------------------------------------------------------------------------------- /src/assets/svg/loading.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/globalComponents/VideoPlayer/svg/loading.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/utils/wxJssdkTools.js: -------------------------------------------------------------------------------- 1 | import wx from "weixin-js-sdk"; 2 | import { getWeChatSignStr } from "@/https/api"; // 获取签名的接口 3 | /** 4 | * @param {*} oType String 业务类型 例如: app 跳转app, share 微信和朋友圈分享等. 5 | * @param {*} paramsObj Object 接手一些需要处理的 参数 6 | * @param {*} customUrl String 自定义url 7 | * @returns 8 | */ 9 | export const wxJssdkTools = (oType, paramsObj = {}, customUrl) => { 10 | return new Promise(async (resolve, reject) => { 11 | let url = ``; 12 | var isIos = !!navigator.userAgent.match(/\(i[^;]+;( U;)? CPU.+Mac OS X/); 13 | if (isIos) { 14 | url = location.href.split("#")[0]; 15 | // url = window.jsUrl // 进入 h5的第一个页面时, 将当前 url 存起来 16 | } else { 17 | url = location.href.split("#")[0]; 18 | } 19 | 20 | // console.log('wxJssdkTools参数', paramsObj) 21 | if (customUrl) url = customUrl; 22 | const res = await getWeChatSignStr({ url }); 23 | // console.log('jssdk 签名检验:', res) 24 | const { title, desc, link, imgUrl } = paramsObj; 25 | // console.log(title, desc, link, imgUrl) 26 | 27 | wx.config({ 28 | debug: false, // 开启调试模式 true 开启, false 关闭 29 | appId: res.result.appId, // 必填,公众号的唯一标识 30 | timestamp: res.result.timestamp, // 必填,生成签名的时间戳 31 | nonceStr: res.result.nonceStr, // 必填,生成签名的随机串 32 | signature: res.result.signature, // 必填,签名 33 | // 必填,需要使用的JS接口列表(根据自己的需求添加进去) 34 | jsApiList: [ 35 | "scanQRCode", 36 | "getLocation", 37 | "chooseImage", 38 | "previewImage", 39 | "updateAppMessageShareData", 40 | "updateTimelineShareData", 41 | "onMenuShareTimeline", 42 | "onMenuShareAppMessage", 43 | ], 44 | // 可选,需要使用的开放标签列表,例如['wx-open-launch-weapp'] 45 | openTagList: ["wx-open-launch-weapp", "wx-open-launch-app"], 46 | }); 47 | wx.checkJsApi({ 48 | jsApiList: [ 49 | "updateAppMessageShareData", // 分享到朋友/QQ 的 API 50 | "updateTimelineShareData", // 分享到微信朋友圈/QQ空间 的 API 51 | ], 52 | success: function () { 53 | console.log("checkJsApi成功"); 54 | }, 55 | fail: function () { 56 | console.log("checkJsApi失败"); 57 | }, 58 | }); 59 | wx.ready(function () { 60 | // console.log('进入到wx.ready里面啦......') 61 | if (oType === "app") { 62 | console.log("进入app - ready 了吗?"); 63 | resolve(JSON.stringify(paramsObj)); 64 | } else if (oType == "share") { 65 | // console.log('首页分享', link) 66 | wx.updateAppMessageShareData({ 67 | title, // 分享标题 68 | desc, // 分享描述 69 | link, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致 70 | imgUrl, // 分享图标 71 | success: function () { 72 | // console.log('resolve ---好友-- 分享成功') 73 | resolve(); // 分享成功 74 | }, 75 | fail: function (err) { 76 | // console.log('reject ---好友---- 分享失败') 77 | reject(err); // 分享失败 78 | }, 79 | }); 80 | wx.updateTimelineShareData({ 81 | title, // 分享标题 82 | link, // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致 83 | imgUrl, // 分享图标 84 | success: function () { 85 | // console.log('resolve ---朋友圈-- 分享成功') 86 | resolve(); // 分享成功 87 | }, 88 | fail: function (err) { 89 | // console.log('reject ---朋友圈---- 分享失败') 90 | reject(err); // 分享失败 91 | }, 92 | }); 93 | } 94 | }); 95 | wx.error(function (err) { 96 | reject(err); 97 | }); 98 | }); 99 | }; 100 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig, loadEnv } from "vite"; 2 | import vue from "@vitejs/plugin-vue"; 3 | import Components from "unplugin-vue-components/vite"; 4 | import { VantResolver } from "unplugin-vue-components/resolvers"; 5 | import { createSvgIconsPlugin } from "vite-plugin-svg-icons"; 6 | import { createHtmlPlugin } from "vite-plugin-html"; 7 | import { viteVConsole } from "vite-plugin-vconsole"; 8 | import { viteMockServe } from "vite-plugin-mock"; 9 | import { createProxy } from "./build/vite/proxy"; 10 | import { wrapperEnv } from "./build/utils"; 11 | import { resolve } from "path"; 12 | 13 | // https://vitejs.dev/config/ 14 | export default ({ mode }) => { 15 | const root = process.cwd(); 16 | const env = loadEnv(mode, root); 17 | // The boolean type read by loadEnv is a string. This function can be converted to boolean type 18 | const viteEnv = wrapperEnv(env); 19 | // console.log("viteEnv:", viteEnv); 20 | const { 21 | VITE_PROXY, 22 | VITE_GLOB_APP_TITLE, 23 | VITE_DROP_CONSOLE, 24 | VITE_OPEN_VCONSOLE, 25 | VITE_USE_MOCK, 26 | VITE_PORT, 27 | } = viteEnv; 28 | 29 | return defineConfig({ 30 | plugins: [ 31 | vue(), 32 | // Components({ 33 | // resolvers: [VantResolver()], 34 | // dirs: ["src/components/globalComponents"], 35 | // }), 36 | createSvgIconsPlugin({ 37 | // 指定需要缓存的图标文件夹(路径为存放所有svg图标的文件夹不单个svg图标) 38 | iconDirs: [resolve(process.cwd(), "src/assets/svg")], 39 | // 指定symbolId格式 40 | symbolId: "icon-[dir]-[name]", 41 | }), 42 | createHtmlPlugin({ 43 | inject: { 44 | data: { 45 | //将环境变量 VITE_APP_TITLE 赋值给 title 方便 html页面使用 title 获取系统标题 46 | title: VITE_GLOB_APP_TITLE, 47 | }, 48 | }, 49 | }), 50 | viteVConsole({ 51 | entry: [resolve("src/main.js")], // or you can use entry: [path.resolve('src/main.ts')] 52 | localEnabled: VITE_OPEN_VCONSOLE, 53 | enabled: VITE_OPEN_VCONSOLE, 54 | config: { 55 | maxLogNumber: 1000, 56 | theme: "light", // light | dark 57 | }, 58 | }), 59 | viteMockServe({ 60 | // mock配置文件的路径,相对于vite.config.js 61 | mockPath: "mock", 62 | // dev是否开启mock 63 | localEnabled: VITE_USE_MOCK, 64 | // prod是否开启mock 65 | prodEnabled: VITE_USE_MOCK, 66 | //设置是否监视mockPath对应的文件夹内文件中的更改 67 | watchFiles: true, 68 | // 动态导入mock生效代码,这样可以控制关闭mock的时候不让mock打包到最终代码内 69 | // /mock/_createProductionServer 这个路径是相对于 main.js的位置来的 70 | injectCode: ` 71 | import { setupProdMockServer } from '/mock/_createProductionServer'; 72 | setupProdMockServer(); 73 | `, 74 | supportTs: false, // 不监听ts,监听js 75 | logger: false, // 开启log 76 | }), 77 | ], 78 | resolve: { 79 | // ↓路径别名 80 | alias: { 81 | "@": resolve(__dirname, "./src"), 82 | }, 83 | }, 84 | esbuild: { 85 | drop: VITE_DROP_CONSOLE ? ["console", "debugger"] : [], 86 | }, 87 | server: { 88 | // Listening on all local IPs 89 | host: true, 90 | https: false, 91 | port: VITE_PORT, 92 | proxy: createProxy(VITE_PROXY), 93 | }, 94 | build: { 95 | sourcemap: false, // 打包后是否生成 source map 文件 96 | }, 97 | css: { 98 | preprocessorOptions: { 99 | less: { 100 | additionalData: `@import './src/styles/variables.less';`, 101 | }, 102 | }, 103 | }, 104 | }); 105 | }; 106 | -------------------------------------------------------------------------------- /src/views/integral/index.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 89 | 90 | 118 | -------------------------------------------------------------------------------- /src/views/category/index.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 102 | 103 | 146 | -------------------------------------------------------------------------------- /src/assets/svg/event.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/localComponents/ScrollList/index.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 174 | 175 | 252 | -------------------------------------------------------------------------------- /src/views/index/index.vue: -------------------------------------------------------------------------------- 1 | 62 | 63 | 186 | 187 | 288 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue 3 + Vite 2 | 3 | This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 ` 271 | 426 | --------------------------------------------------------------------------------