自定义组件
介绍
在使用 VitePress 构建项目的时候呢,咱自己捣鼓了一些组件。现在把它们拿出来总结总结,顺便共享一下,指不定能帮到哪位小伙伴呢。我用到的所有和这些组件相关的引入,都放在下面的代码里啦。 需要用到下面组件:
yarn add element-plus
yarn add imagesloaded
yarn add crypto-js
.vitepress/theme/index.ts
import DefaultTheme from 'vitepress/theme'
import MenuCustom from './components/MenuCustom.vue'
import PasswordCheck from './components/PasswordCheck.vue'
import ImageCustom from './components/ImageCustom.vue'
import MasonryCustom from './components/MasonryCustom.vue'
// 导入elementplus组件
import "element-plus/dist/index.css";
import elementplus from "element-plus"
// 导入elementplus组件-中文
import zhCn from 'element-plus/es/locale/lang/zh-cn'
// 导入elementplus组件-暗黑模式
import 'element-plus/theme-chalk/dark/css-vars.css'
// 导入elementplus组件-图标组件
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import './styles/custom.css'
import {
VPBadge,
VPButton,
VPDocAsideSponsors,
VPHomeFeatures,
VPHomeHero,
VPHomeSponsors,
VPImage,
VPSponsors,
VPTeamMembers,
VPTeamPage,
VPTeamPageSection,
VPTeamPageTitle
} from 'vitepress/theme-without-fonts'
export default {
...DefaultTheme,
// extends: DefaultTheme,
enhanceApp({ app }) {
// DefaultTheme.enhanceApp(ctx);
app.component('MenuCustom', MenuCustom);
app.component('PasswordCheck', PasswordCheck);
app.component('ImageCustom', ImageCustom);
app.component('MasonryCustom', MasonryCustom);
app.component('Badge', VPBadge);
app.component('Button', VPButton);
app.component('DocAsideSponsors', VPDocAsideSponsors);
app.component('HomeFeatures', VPHomeFeatures);
app.component('HomeHero', VPHomeHero);
app.component('HomeSponsors', VPHomeSponsors);
app.component('Image', VPImage);
app.component('Sponsors', VPSponsors);
app.component('TeamMembers', VPTeamMembers);
app.component('TeamPage', VPTeamPage);
app.component('TeamPageSection', VPTeamPageSection);
app.component('TeamPageTitle', VPTeamPageTitle);
app.use(elementplus, {
locale: zhCn,
});
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component)
}
}
}
轮播组件
其实就是采用element的走马灯来制作的
vue
<!--
* @Author: trexwb
* @Date: 2024-10-18 13:50:54
* @LastEditors: trexwb
* @LastEditTime: 2024-10-18 14:16:55
* @FilePath: /xuanyun.wang/docs/attachment/code/vue/components/ImageCustom.vue
* @Description:
* 一花一世界,一叶一如来
* Copyright (c) 2024 by 杭州大美, All Rights Reserved.
-->
<template>
<div ref="container" class="image__preview" v-loading="loading">
<el-carousel :interval="5000" arrow="always">
<el-carousel-item v-for="(item, index) in srcList" :key="index">
<el-link v-if="item.href" :underline="false" :href="item.href"><el-image style="width: 100%;"
:src="item.src || item.photo" /></el-link>
<el-image v-else style="width: 100%;" :src="item.src || item.photo" @click="handleImageClick(index)" />
</el-carousel-item>
</el-carousel>
<el-image-viewer v-if="previewVisible" @close="handleClose" :url-list="allPhotos" :initial-index="currentIndex"></el-image-viewer>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, PropType, nextTick } from 'vue';
import imagesLoaded from 'imagesloaded';
// 定义图片项的接口
interface Item {
id?: String; // 假设每个 item 都有一个唯一的 id
title?: String;
src?: String;
photo?: Array<any>;
href?: String;
content?: String;
}
// 接受一个图片列表作为属性
const props = defineProps({
srcList: {
type: Array as PropType<Item[]>,
required: true
},
});
// 控制加载状态
const loading = ref(true);
// 控制图片预览的可见状态
const previewVisible = ref(false);
// 当前预览图片的索引
const currentIndex = ref(0);
// 所有图片的列表
const allPhotos = ref<any>([]);
// 容器元素的引用
const container = ref<HTMLElement | null>(null);
// 获取第一张图片的URL
const getUrl = () => {
return Array.isArray(props.srcList) ? props.srcList[0] : (props.srcList ?? '');
}
// 处理图片点击事件
const handleImageClick = (index: number) => {
currentIndex.value = index;
previewVisible.value = true;
}
// 处理预览关闭事件
const handleClose = () => {
previewVisible.value = false;
}
// 初始化所有图片的列表
allPhotos.value = Array.isArray(props.srcList) ? props.srcList.map(item => item.photo) : [];
// 在 mounted 钩子中初始化 Masonry
onMounted(async () => {
if (container.value) {
imagesLoaded(container.value, { background: true }, async () => {
await nextTick();
loading.value = false;
});
}
});
</script>
<style scoped>
.el-carousel__item h3 {
color: #475669;
opacity: 0.75;
line-height: 300px;
margin: 0;
text-align: center;
}
.el-carousel__item:nth-child(2n) {
background-color: #99a9bf;
}
.el-carousel__item:nth-child(2n + 1) {
background-color: #d3dce6;
}
.image__error .image-slot {
font-size: 30px;
}
.image__error .image-slot .el-icon {
font-size: 30px;
}
.image__error .el-image {
width: 100%;
height: 200px;
}
</style>
调方式
vue
<ImageCustom :srcList="demoImages" />
<script setup>
const demoImages = [
{
"href": false,
"src": "http://attach.dmcdn.com/riss/2023/12/26/01_57_49_kvexk488ftyzn4sjft5xrysqjedynz06.jpg!a300",
"photo": "http://attach.dmcdn.com/riss/2023/12/26/01_57_49_kvexk488ftyzn4sjft5xrysqjedynz06.jpg!a800"
},
{
"href": false,
"src": "http://attach.dmcdn.com/riss/2024/01/25/09_05_55_iwphzecvoq3l4a0pb3kznxjz29a08djq.jpg!a300",
"photo": "http://attach.dmcdn.com/riss/2024/01/25/09_05_55_iwphzecvoq3l4a0pb3kznxjz29a08djq.jpg!a800"
}
]
</script>
瀑布流
目前这个组件还在改进当中,只是将就能用
vue
<!--
* @Author: trexwb
* @Date: 2024-10-16 11:28:39
* @LastEditors: trexwb
* @LastEditTime: 2024-10-18 14:21:26
* @FilePath: /xuanyun.wang/docs/attachment/code/vue/components/MasonryCustom.vue
* @Description:
* 一花一世界,一叶一如来
* Copyright (c) 2024 by 杭州大美, All Rights Reserved.
-->
<template>
<div class="album-list" v-loading="loading">
<div class="card-container" :style="waterfallStyle" ref="container">
<el-card v-for="(item, index) in dataList" :key="index" :body-style="{ padding: '0px' }" shadow="always"
class="card-item">
<template #header>{{ item.title }}</template>
<el-link v-if="item.href" :underline="false" :href="item.href">
<el-image style="width: 100%;" :src="item.src || item.photos?.[0]" />
</el-link>
<el-image v-else style="width: 100%;" :src="item.src || item.photos?.[0]" :preview-src-list="item.photos ?? []" />
<template #footer>{{ item.content }}</template>
</el-card>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, PropType, onMounted, onBeforeUnmount, watch, nextTick, toDisplayString } from 'vue';
import { ElCard, ElLoading } from 'element-plus';
import imagesLoaded from 'imagesloaded';
interface Item {
id?: String; // 假设每个 item 都有一个唯一的 id
title?: String;
src?: String;
photos?: Array<any>;
href?: String;
content?: String;
}
const props = defineProps({
dataList: {
type: Array as PropType<Item[]>,
required: true
},
});
const waterfallStyle = ref({});
const loading = ref(true);
waterfallStyle.value = {
'position': 'absolute',
'visibility': 'hidden'
}
const container = ref<HTMLElement | null>(null);
const arrangeCards = async () => {
if (!container.value) return;
const columns: HTMLElement[] = [];
const items = Array.from(container.value.children);
// 初始化列
for (let i = 0; i < 4; i++) {
const column = document.createElement('div');
column.classList.add('column'); // 添加 column 类
container.value!.appendChild(column);
columns.push(column);
}
// 清空现有列
for (let col of columns) {
while (col.firstChild) {
col.removeChild(col.firstChild);
}
}
// 将卡片分配到列中
let colIndex = 0;
for (let item of items) {
let minHeight = Infinity; // 初始化为正无穷大
let shortestColumn: HTMLElement | null = null;
// 找到当前最短的列
for (const [index, col] of columns.entries()) {
// console.log(`col[${index}]:`, col.offsetHeight, minHeight)
if (col.offsetHeight < minHeight) {
minHeight = col.offsetHeight;
shortestColumn = col;
colIndex = index;
}
}
if (shortestColumn) {
// 将卡片添加到最短的列
shortestColumn.appendChild(item);
await nextTick(); // 确保 DOM 更新后再继续
// console.log(`minHeight[${colIndex}]:`, shortestColumn.offsetHeight, minHeight)
}
}
waterfallStyle.value = {
'display': 'grid',
'grid-template-columns': 'repeat(4, 1fr)'
}
loading.value = false;
};
onMounted(async () => {
if (container.value) {
imagesLoaded(container.value, { background: true }, async () => {
await nextTick();
setTimeout(() => {
requestAnimationFrame(() => {
arrangeCards();
});
}, 500);
});
// 监听窗口大小变化
window.addEventListener('resize', arrangeCards);
}
});
onBeforeUnmount(() => {
// 清理事件监听器
window.removeEventListener('resize', arrangeCards);
});
</script>
<style scoped>
.album-list {
min-height: 300px;
}
.card-container {
/* display: grid;
grid-template-columns: repeat(4, 1fr); */
gap: 5px;
margin: 5px;
}
.card-container .column {
width: 100%; /* 考虑间距 */
box-sizing: border-box;
}
.card-item {
box-sizing: border-box;
margin: 10px 2px;
}
</style>
调方式
vue
<MasonryCustom :dataList="demoList" />
<script setup>
const demoList = [
{
"id": "1", // 随意写即可,未来可以用来做传参
"title": "测试title名称",
"href": false,
"src": "http://attach.dmcdn.com/riss/2023/12/26/01_57_49_kvexk488ftyzn4sjft5xrysqjedynz06.jpg!a300"
"photos": [
"http://attach.dmcdn.com/riss/2023/12/26/01_57_49_kvexk488ftyzn4sjft5xrysqjedynz06.jpg!a800",
"http://attach.dmcdn.com/riss/2024/01/25/09_05_55_iwphzecvoq3l4a0pb3kznxjz29a08djq.jpg!a800"
],
"content": "2024.07.12"
},
{
"id": "2",
"title": "测试title名称1",
"href": false,
"src": "http://attach.dmcdn.com/riss/2024/01/25/09_05_55_iwphzecvoq3l4a0pb3kznxjz29a08djq.jpg!a300"
"photos": [
"http://attach.dmcdn.com/riss/2024/01/25/09_05_55_iwphzecvoq3l4a0pb3kznxjz29a08djq.jpg!a800"
],
"content": "2022.01.02"
},
...
]
</script>
访问密码
目前这个组件还在改进当中,只是将就能用
vue
<!--
* @Author: trexwb
* @Date: 2024-06-18 15:45:22
* @LastEditors: trexwb
* @LastEditTime: 2024-10-18 14:25:21
* @FilePath: /xuanyun.wang/docs/attachment/code/vue/components/PasswordCheck.vue
* @Description:
* 一花一世界,一叶一如来
* Copyright (c) 2024 by 杭州大美, All Rights Reserved.
-->
<template>
<el-dialog v-model="showMask" :modal="true" :lock-scroll="true" :show-close="false" :close-on-click-modal="false" :close-on-press-escape="false" modal-class="fullscreen-mask" title="请输入密码" width="500"
align-center>
<el-input type="password" v-model="userPassword" placeholder="页面需要输入密码才能访问!虽然你也可以简单绕过!" @keyup.enter="checkPassword" clearable show-password></el-input>
<button @click="checkPassword" class="login-button">登录</button>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import CryptoJS from 'crypto-js'
import { ElMessage } from 'element-plus'
const publicSecret = '32位随机字符串'; // 用来做公钥
const encryptedPassword = '密码'; // 请自己进行一次md5加密
const userPassword = ref('');
const showMask = ref(true);
const checkPassword = () => {
if (md5(userPassword.value) === encryptedPassword) {
// 密码正确时关闭遮罩层或执行其他操作
setTokenStorage(encryptedPassword, 8 * 60 * 1000 * 1000, 'token');
ElMessage({
type: 'success',
message: '登录成功',
grouping: true,
plain: true
});
closeMask();
} else {
ElMessage({
type: 'warning',
message: '密码错误,请重试!',
grouping: true,
plain: true
});
}
};
const closeMask = () => {
// 如果点击遮罩层外部,则关闭遮罩层
showMask.value = false;
};
const md5 = (str: string) => {
return CryptoJS.MD5(str).toString()
}
// 加密函数
const encrypt = (encryptedData: { password: any; expiry: any; }, key: string, iv: string) => {
try {
const encryptedText = JSON.stringify(encryptedData)
const cipher = CryptoJS.AES.encrypt(encryptedText, key, { iv: CryptoJS.enc.Utf8.parse(iv) })
return cipher.toString()
} catch (err) {
return false
}
}
// 解密函数
const decrypt = (encryptedText: any, key: string, iv: any) => {
try {
const decryptedData = CryptoJS.AES.decrypt(encryptedText, key, { iv: CryptoJS.enc.Utf8.parse(iv) })
const decryptedText = decryptedData.toString(CryptoJS.enc.Utf8)
return JSON.parse(decryptedText)
} catch (err) {
return false
}
}
const isJson = (value: string | null) => {
if (typeof value === 'string') {
const obj = JSON.parse(value)
return !!(typeof obj === 'object' && obj)
}
return false
}
const getTokenStorage = (key: undefined) => {
const itemStr = localStorage ? localStorage.getItem(key || 'token') : null
if (itemStr && isJson(itemStr)) {
const item = JSON.parse(itemStr);
const now = new Date().getTime();
// 检查是否已过期
if (now > item.expiry) {
// 过期则删除该条数据
removeTokenStorage(key || 'token');
return null;
}
return item;
} else {
return itemStr
}
}
function generateRandomString(length: number) {
var result = '';
var characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
var charactersLength = characters.length;
for (var i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}
const setTokenStorage = (value: string, ttlMilliseconds: number, key: string) => {
// 计算过期时间
const expiryTime = new Date().getTime() + ttlMilliseconds;
const iv = generateRandomString(16);
const token = encrypt({
password: value,
expiry: expiryTime
}, publicSecret, iv)
return localStorage.setItem(key || 'token', JSON.stringify({
token: token,
iv: iv,
expiry: expiryTime
}))
}
const removeTokenStorage = (key: any) => {
return localStorage.removeItem(key || 'token')
}
// 阻止页面滚动
if (typeof window !== 'undefined') {
const tokenEncrypt = getTokenStorage(undefined);
if (tokenEncrypt) {
const tokenDecrypt = decrypt(tokenEncrypt.token, publicSecret, tokenEncrypt.iv);
if (tokenDecrypt?.password && tokenDecrypt?.password === encryptedPassword) showMask.value = false;
}
}
// document.body.style.overflow = showMask.value ? 'hidden' : 'auto';
// onMounted(() => {
// });
// // 添加卸载时的清理工作,以防万一组件在消息隐藏前被卸载
// onUnmounted(() => {
// document.body.style.overflow = 'auto';
// });
// defineExpose({
// userPassword,
// checkPassword,
// closeMask,
// showMask
// });
</script>
<style scoped>
:root {
--placeholder-color: #322d2d;
/* 默认颜色 */
}
.fullscreen-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(255, 255, 255, 0.9);
/* 半透明黑色背景 */
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
/* 确保遮罩层在最上层 */
}
.login-container {
background-color: #ffffff;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.6);
/* 添加阴影效果 */
border-radius: 10px;
/* 圆角效果 */
padding: 30px;
width: 300px;
/* 登录框宽度自定义 */
}
.login-form {
text-align: center;
}
.input-field {
display: block;
margin: 10px auto;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
color: var(--placeholder-color);
color: #322d2d
}
.login-button {
display: block;
margin: 20px auto 0;
padding: 10px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.login-button:hover {
background-color: #0056b3;
}
/* 为了更好的浏览器兼容性,可以添加针对不同浏览器引擎的前缀 */
input::-webkit-input-placeholder {
color: var(--placeholder-color);
}
input::-moz-placeholder {
color: var(--placeholder-color);
}
input:-ms-input-placeholder {
color: var(--placeholder-color);
}
</style>
调方式
vue
<PasswordCheck />