pnpm create vue
vite已自动配置@路径,在.config.js jsconfig.json 中
按需引用element-plus 官网中有
pnpm install element-plus
npm install -D unplugin-vue-components unplugin-auto-import
// vite.config.ts import { defineConfig } from 'vite' import AutoImport from 'unplugin-auto-import/vite' import Components from 'unplugin-vue-components/vite' import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' export default defineConfig({ // ... plugins: [ // ... AutoImport({ resolvers: [ElementPlusResolver()], }), Components({ resolvers: [ElementPlusResolver()], }), ], })
如果要自定义主题,方式如下
如果您的项目也使用了 SCSS,可以直接修改 Element Plus 的样式变量。 新建一个样式文件,例如
styles/element/index.scss
:pnpm i sass -D
index.css 代码定制
/* 只需要重写你需要的即可 */ @forward 'element-plus/theme-chalk/src/common/var.scss' with ( $colors: ( 'primary': ( // 主色 'base': #27ba9b, ), 'success': ( // 成功色 'base': #1dc779, ), 'warning': ( // 警告色 'base': #ffb302, ), 'danger': ( // 危险色 'base': #e26237, ), 'error': ( // 错误色 'base': #cf4444, ), ) )
样式重置common.scss
// 重置样式 * { box-sizing: border-box; } html { height: 100%; font-size: 14px; } body { height: 100%; color: #333; min-width: 1240px; font: 1em/1.4 'Microsoft Yahei', 'PingFang SC', 'Avenir', 'Segoe UI', 'Hiragino Sans GB', 'STHeiti', 'Microsoft Sans Serif', 'WenQuanYi Micro Hei', sans-serif; } body, ul, h1, h3, h4, p, dl, dd { padding: 0; margin: 0; } a { text-decoration: none; color: #333; outline: none; } i { font-style: normal; } input[type='text'], input[type='search'], input[type='password'], input[type='checkbox'] { padding: 0; outline: none; border: none; -webkit-appearance: none; &::placeholder { color: #ccc; } } img { max-width: 100%; max-height: 100%; vertical-align: middle; background: #ebebeb url('@/assets/images/200.png') no-repeat center / contain; } ul { list-style: none; } #app { background: #f5f5f5; user-select: none; } .container { width: 1240px; margin: 0 auto; position: relative; } .ellipsis { white-space: nowrap; text-overflow: ellipsis; overflow: hidden; } .ellipsis-2 { word-break: break-all; text-overflow: ellipsis; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; } .fl { float: left; } .fr { float: right; } .clearfix:after { content: '.'; display: block; visibility: hidden; height: 0; line-height: 0; clear: both; } // reset element .el-breadcrumb__inner.is-link { font-weight: 400 !important; }
var.scss 定义变量
$xtxColor: #27ba9b; $helpColor: #e26237; $sucColor: #1dc779; $warnColor: #ffb302; $priceColor: #cf4444;
最后是config.js 文件指定配色相关,全代码都有注释
import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import vueDevTools from 'vite-plugin-vue-devtools' // element-plus 按需引入 import AutoImport from 'unplugin-auto-import/vite' import Components from 'unplugin-vue-components/vite' import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' // https://vite.dev/config/ export default defineConfig({ plugins: [ vue(), vueDevTools(), AutoImport({ resolvers: [ElementPlusResolver()] }), Components({ // 指定配色为sass resolvers: [ElementPlusResolver({ importStyle: 'sass' })] }) ], resolve: { alias: { // @ 实际转换为 src '@': fileURLToPath(new URL('./src', import.meta.url)) } }, css: { // 主题配色 preprocessorOptions: { scss: { additionalData: `@use "@/styles/element/index.scss" as *;` } } }, server: { port: 5600, open: true } })
重启项目测试主题是否有用
pnpm i axios
封装它 代码要修改
// import loadingPlugin from "./plugins/loading/index.js"; // 详细参考 todoproject ---web-coce 中 import axios from 'axios' const TOKEN_KEY = '__TOKEN__' const pendingMap = new Map() function getTokenAUTH() { return localStorage.getItem(TOKEN_KEY) } /** * 生成唯一的每个请求的唯一key * @param {*} config * @returns */ function getPendingKey(config) { let { url, method, params, data } = config if (typeof data === 'string') data = JSON.parse(data) // response里面返回的config.data是个字符串对象 return [url, method, JSON.stringify(params), JSON.stringify(data)].join('&') } /** * 储存每个请求的唯一cancel回调, 以此为标识 * @param {*} config */ function addPending(config) { const pendingKey = getPendingKey(config) config.cancelToken = config.cancelToken || new axios.CancelToken((cancel) => { if (!pendingMap.has(pendingKey)) { pendingMap.set(pendingKey, cancel) } }) } /** * 删除重复的请求 * @param {*} config */ function removePending(config) { const pendingKey = getPendingKey(config) if (pendingMap.has(pendingKey)) { const cancelToken = pendingMap.get(pendingKey) cancelToken(pendingKey) pendingMap.delete(pendingKey) } } /** * 关闭Loading层实例 * @param {*} _options */ function closeLoading(_options) { if (_options.loading && LoadingInstance._count > 0) LoadingInstance._count-- if (LoadingInstance._count === 0) { if (LoadingInstance._target) { LoadingInstance._target.close() } LoadingInstance._target = null } } const LoadingInstance = { _target: null, _count: 0 } // 添加loading // const hideLoading = () => app.config.globalProperties.$smallLoading.hideLoading; // const showLoading = () => // app.config.globalProperties.$smallLoading.showLoading(); // let onProgress = () => app.config.globalProperties.$smallLoading.onProgress; export function __MyAxios__(axiosConfig, customOptions) { const service = axios.create({ // baseURL: "", // 设置统一的请求前缀 timeout: 30000, // 设置统一的超时时长 responseType: 'json' // default }) // 自定义配置 let custom_options = { repeat_request_cancel: true, // 是否开启取消重复请求, 默认为 true loading: false, // 是否开启loading层效果, 默认为false reduct_data_format: true, // 是否开启简洁的数据结构响应, 默认为true error_message_show: true, // 是否开启接口错误信息展示,默认为true code_message_show: false, // 是否开启code不为0时的信息提示, 默认为false scroll_to_top: false, // 请求成功后滑动到页面顶部 ...customOptions } // 请求拦截 service.interceptors.request.use( (config) => { removePending(config) custom_options.repeat_request_cancel && addPending(config) // if (custom_options.loading) { // LoadingInstance._count++; // LoadingInstance._target = loadingPlugin(); // } // 自动携带token if (getTokenAUTH() && typeof window !== 'undefined') { config.headers.Authorization = getTokenAUTH() } // 添加loading // showLoading(); // config.onDownloadProgress = (progressEvent) => { // let percentCompleted = Math.floor( // (progressEvent.loaded * 100) / progressEvent?.total // ); // onProgress()(percentCompleted); // }; return config }, (error) => { // hideLoading()(); return Promise.reject(error) } ) // 响应拦截 service.interceptors.response.use( (response) => { removePending(response.config) if (custom_options.scroll_to_top) { window.scrollTo(0, 0) } closeLoading(custom_options) // loading // hideLoading()(); return custom_options.reduct_data_format ? response.data : response }, (error) => { if (custom_options.scroll_to_top) { window.scrollTo(0, 0) } error.config && removePending(error.config) closeLoading(custom_options) // loading // hideLoading()(); // TODO:401 const { status = 200 } = error.response || {} if (status === 401) { return window.location.replace('/login') } return Promise.reject(error) // 错误继续返回给到具体页面 } ) return service(axiosConfig) }
封装二 测试
//css
创建pinia 持久化存储
import { createPinia, defineStore } from 'pinia' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' import { ref } from 'vue' const pinia = createPinia() pinia.use(piniaPluginPersistedstate) export const useStore = defineStore( 'store', () => { const profile = ref() const setProfile = (val) => { profile.value = val return profile.value } const clearProfile = () => { profile.value = undefined } return { setProfile, clearProfile, profile } }, { persist: { // 默认为true 自定义key key: 'my--xtx-store' } } ) export default pinia
测试代码
import { useStore } from './stores' const { setProfile, clearProfile } = useStore() import { ref } from 'vue' const testProfile = ref('我是测试用户') const onSet = () => { setProfile(testProfile.value) console.log('🚀 ~ onSet ~ onSet:', testProfile.value) } const onClear = () => { clearProfile() }
vue-router
import { createRouter, createWebHistory } from 'vue-router' import HomeView from '../views/HomeView.vue' import Home from '@/views/Home/index.vue' import Layout from '@/views/Layout/index.vue' import Login from '@/views/Login/index.vue' import Category from '@/views/Category/index.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/', name: 'layout', component: Layout, children: [ { path: '', name: 'home', component: Home }, { path: 'category', name: 'category', component: Category } ] }, { path: '/login', name: 'login', component: () => import('@/views/Login/index.vue') } ] }) export default router
测试 在App.vue Layout.vue 中放入路由出口
<router-view />
切换不同路由 /category
导入main.js 中导入重置样式common.cscc
查看
index.html
<link
rel="stylesheet"
href="//at.alicdn.com/t/font_2143783_iq6z4ey5vu.css"
/>
请求放在一起,目前请求还未封装完成 ToDo
吸顶效果
<script setup> // 使用VueUse import { useScroll } from '@vueuse/core' const { y } = useScroll(window) import { useHeaderStore } from '@/stores/Header.js' const HeaderStore = useHeaderStore() import HeaderCart from './HeaderCart.vue' </script> <template> <div class="app-header-sticky" :class="{ show: y > 78 }"> <div class="container"> <RouterLink class="logo" to="/" /> <!-- 导航区域 --> <ul class="app-header-nav"> <li class="home"> <RouterLink to="/">首页</RouterLink> </li> <li class="home" v-for="item in HeaderStore.category" :key="item.key"> <RouterLink :to="`/category/${item.id}`">{{ item.name }}</RouterLink> </li> </ul> <div class="right"> <RouterLink to="/">品牌</RouterLink> <RouterLink to="/">专题</RouterLink> </div> <HeaderCart></HeaderCart> </div> </div> </template> <style scoped lang="scss"> .app-header-sticky { width: 100%; height: 80px; position: fixed; left: 0; top: 0; z-index: 999; background-color: #fff; border-bottom: 1px solid #e4e4e4; // 此处为关键样式!!! // 状态一:往上平移自身高度 + 完全透明 transform: translateY(-100%); opacity: 0; // 状态二:移除平移 + 完全不透明 &.show { transition: all 1s linear; transform: none; opacity: 1; } .container { display: flex; align-items: center; } .logo { width: 200px; height: 80px; background: url('@/assets/images/logo.png') no-repeat right 2px; background-size: 160px auto; } .right { width: 220px; display: flex; text-align: center; padding-left: 40px; border-left: 2px solid $xtxColor; a { width: 38px; margin-right: 40px; font-size: 16px; line-height: 1; &:hover { color: $xtxColor; } } } } .app-header-nav { width: 820px; display: flex; padding-left: 40px; position: relative; z-index: 998; li { margin-right: 40px; width: 38px; text-align: center; a { font-size: 16px; line-height: 32px; height: 32px; display: inline-block; &:hover { color: $xtxColor; border-bottom: 1px solid $xtxColor; } } .active { color: $xtxColor; border-bottom: 1px solid $xtxColor; } } } </style>
之前方式对比 判断滚动距离监听是否添加class
head.js 调用一次就可以直接在其他组件使用其数据
import { ref } from 'vue' import { defineStore } from 'pinia' import {getCategoryAPI} from '@/api/user.js' export const useHeaderStore = defineStore('Header', () => { const category = ref([]) const getCategory= async ()=>{ const res = await getCategoryAPI() // console.log(res.data.result); category.value = res.data.result } return {category,getCategory } })
图片懒加载 当图片进入视口区就开始加载
// 定义懒加载插件 import { useIntersectionObserver } from '@vueuse/core' export const lazyPlugin = { install(app) { // 定义全局指令 app.directive('img-lazy', { mounted(el, binding) { // el:绑定的元素 // binding:指令的参数 binding-value:指令等于号后面绑定的表达式的值 图片url // console.log(el,binding.value); const { stop } = useIntersectionObserver(el, ([{ isIntersecting }]) => { // console.log(isIntersecting); if (isIntersecting) { // 图片懒加载 el.src = binding.value // 成功加载后停止监听 stop() } }) } }) } }
激活模块
<RouterLink active-class="active" :to="`/category/${item.id}`">{{ item.name }}</RouterLink>
列表无限加载
const disabled = ref(false) const load = async () => { console.log('++++++++') reqDate.value.page++ const res = await getSubCategoryAPI(reqDate.value) goodList.value = [...goodList.value, ...res.data.result.items] // 加载完毕 停止监听 if (res.data.result.items.length === 0) { disabled.value = true } } <div class="body" :infinite-scroll-disabled="disabled" v-infinite-scroll="load" style="overflow: auto" > <GoodsItem v-for="item in goodList" :key="item.id" :good="item" ></GoodsItem> </div>