feat:添加Toast组件
This commit is contained in:
@@ -6,7 +6,7 @@ import {createPinia} from "pinia"
|
||||
import router from "@/router.ts";
|
||||
import VueVirtualScroller from 'vue-virtual-scroller'
|
||||
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||
import './global.d.ts'
|
||||
import './types/global.d.ts'
|
||||
|
||||
const pinia = createPinia()
|
||||
const app = createApp(App)
|
||||
|
||||
120
src/pages/pc/components/Toast/Toast.ts
Normal file
120
src/pages/pc/components/Toast/Toast.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import {createVNode, render} from 'vue'
|
||||
import ToastComponent from '@/pages/pc/components/Toast/Toast.vue'
|
||||
import type {ToastOptions, ToastInstance, ToastService} from '@/pages/pc/components/Toast/type.ts'
|
||||
|
||||
interface ToastContainer {
|
||||
id: string
|
||||
container: HTMLElement
|
||||
instance: ToastInstance
|
||||
offset: number
|
||||
}
|
||||
|
||||
let toastContainers: ToastContainer[] = []
|
||||
let toastIdCounter = 0
|
||||
|
||||
// 创建Toast容器
|
||||
const createToastContainer = (): HTMLElement => {
|
||||
const container = document.createElement('div')
|
||||
container.className = 'toast-container'
|
||||
container.style.cssText = `
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
`
|
||||
return container
|
||||
}
|
||||
|
||||
// 更新所有Toast的位置
|
||||
const updateToastPositions = () => {
|
||||
toastContainers.forEach((toastContainer, index) => {
|
||||
const offset = index * 70 // 每个Toast之间的间距,从80px减少到50px
|
||||
toastContainer.offset = offset
|
||||
toastContainer.container.style.marginTop = `${offset}px`
|
||||
})
|
||||
}
|
||||
|
||||
// 移除Toast容器
|
||||
const removeToastContainer = (id: string) => {
|
||||
const index = toastContainers.findIndex(container => container.id === id)
|
||||
if (index > -1) {
|
||||
const container = toastContainers[index]
|
||||
// 延迟销毁,等待动画完成
|
||||
setTimeout(() => {
|
||||
render(null, container.container)
|
||||
container.container.remove()
|
||||
const currentIndex = toastContainers.findIndex(c => c.id === id)
|
||||
if (currentIndex > -1) {
|
||||
toastContainers.splice(currentIndex, 1)
|
||||
updateToastPositions()
|
||||
}
|
||||
}, 300) // 等待动画完成(0.3秒)
|
||||
}
|
||||
}
|
||||
|
||||
const Toast: ToastService = (options: ToastOptions | string): ToastInstance => {
|
||||
const toastOptions = typeof options === 'string' ? {message: options} : options
|
||||
const id = `toast-${++toastIdCounter}`
|
||||
|
||||
// 创建Toast容器
|
||||
const container = createToastContainer()
|
||||
document.body.appendChild(container)
|
||||
|
||||
// 创建VNode
|
||||
const vnode = createVNode(ToastComponent, {
|
||||
...toastOptions,
|
||||
onClose: () => {
|
||||
removeToastContainer(id)
|
||||
}
|
||||
})
|
||||
|
||||
// 渲染到容器
|
||||
render(vnode, container)
|
||||
|
||||
// 创建实例
|
||||
const instance: ToastInstance = {
|
||||
close: () => {
|
||||
vnode.component?.exposed?.close?.()
|
||||
}
|
||||
}
|
||||
|
||||
// 添加到容器列表
|
||||
const toastContainer: ToastContainer = {
|
||||
id,
|
||||
container,
|
||||
instance,
|
||||
offset: 0
|
||||
}
|
||||
|
||||
toastContainers.push(toastContainer)
|
||||
updateToastPositions()
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
// 添加类型方法
|
||||
Toast.success = (message: string, options?: Omit<ToastOptions, 'message' | 'type'>) => {
|
||||
return Toast({message, type: 'success', ...options})
|
||||
}
|
||||
|
||||
Toast.warning = (message: string, options?: Omit<ToastOptions, 'message' | 'type'>) => {
|
||||
return Toast({message, type: 'warning', ...options})
|
||||
}
|
||||
|
||||
Toast.info = (message: string, options?: Omit<ToastOptions, 'message' | 'type'>) => {
|
||||
return Toast({message, type: 'info', ...options})
|
||||
}
|
||||
|
||||
Toast.error = (message: string, options?: Omit<ToastOptions, 'message' | 'type'>) => {
|
||||
return Toast({message, type: 'error', ...options})
|
||||
}
|
||||
|
||||
// 关闭所有消息
|
||||
Toast.closeAll = () => {
|
||||
toastContainers.forEach(container => container.instance.close())
|
||||
toastContainers = []
|
||||
}
|
||||
|
||||
export default Toast
|
||||
206
src/pages/pc/components/Toast/Toast.vue
Normal file
206
src/pages/pc/components/Toast/Toast.vue
Normal file
@@ -0,0 +1,206 @@
|
||||
<template>
|
||||
<Transition name="message-fade" appear>
|
||||
<div v-if="visible" class="message" :class="type" :style="style" @mouseenter="handleMouseEnter"
|
||||
@mouseleave="handleMouseLeave">
|
||||
<div class="message-content">
|
||||
<Icon v-if="icon" :icon="icon" class="message-icon" />
|
||||
<span class="message-text">{{ message }}</span>
|
||||
<Icon v-if="showClose" icon="mdi:close" class="message-close" @click="close" />
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { Icon } from '@iconify/vue'
|
||||
|
||||
interface Props {
|
||||
message: string
|
||||
type?: 'success' | 'warning' | 'info' | 'error'
|
||||
duration?: number
|
||||
showClose?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
type: 'info',
|
||||
duration: 3000,
|
||||
showClose: false
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
const visible = ref(false)
|
||||
let timer = null
|
||||
|
||||
const icon = computed(() => {
|
||||
const icons = {
|
||||
success: 'mdi:check-circle',
|
||||
warning: 'mdi:alert-circle',
|
||||
info: 'mdi:information',
|
||||
error: 'mdi:close-circle'
|
||||
}
|
||||
return icons[props.type]
|
||||
})
|
||||
|
||||
const style = computed(() => ({
|
||||
// 移除offset,现在由容器管理位置
|
||||
}))
|
||||
|
||||
const startTimer = () => {
|
||||
if (props.duration > 0) {
|
||||
timer = setTimeout(close, props.duration)
|
||||
}
|
||||
}
|
||||
|
||||
const clearTimer = () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
timer = null
|
||||
}
|
||||
}
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
clearTimer()
|
||||
}
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
startTimer()
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
visible.value = false
|
||||
// 延迟发出close事件,等待动画完成
|
||||
setTimeout(() => {
|
||||
emit('close')
|
||||
}, 300) // 等待动画完成(0.3秒)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
visible.value = true
|
||||
startTimer()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
clearTimer()
|
||||
})
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
close,
|
||||
show: () => {
|
||||
visible.value = true
|
||||
startTimer()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.message {
|
||||
position: relative;
|
||||
min-width: 16rem;
|
||||
padding: 0.8rem 1rem;
|
||||
border-radius: 0.2rem;
|
||||
box-shadow: 0 0.2rem 0.9rem rgba(0, 0, 0, 0.15);
|
||||
background: white;
|
||||
border: 1px solid #ebeef5;
|
||||
transition: all 0.3s ease;
|
||||
pointer-events: auto;
|
||||
|
||||
&.success {
|
||||
background: #f0f9ff;
|
||||
border-color: #67c23a;
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background: #fdf6ec;
|
||||
border-color: #e6a23c;
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
&.info {
|
||||
background: #f4f4f5;
|
||||
border-color: #909399;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
&.error {
|
||||
background: #fef0f0;
|
||||
border-color: #f56c6c;
|
||||
color: #f56c6c;
|
||||
}
|
||||
}
|
||||
|
||||
// 深色模式支持
|
||||
html.dark {
|
||||
.message {
|
||||
background: var(--color-second);
|
||||
border-color: var(--color-item-border);
|
||||
color: var(--color-main-text);
|
||||
|
||||
&.success {
|
||||
background: rgba(103, 194, 58, 0.1);
|
||||
border-color: #67c23a;
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
&.warning {
|
||||
background: rgba(230, 162, 60, 0.1);
|
||||
border-color: #e6a23c;
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
&.info {
|
||||
background: rgba(144, 147, 153, 0.1);
|
||||
border-color: #909399;
|
||||
color: #909399;
|
||||
}
|
||||
|
||||
&.error {
|
||||
background: rgba(245, 108, 108, 0.1);
|
||||
border-color: #f56c6c;
|
||||
color: #f56c6c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.message-icon {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.message-text {
|
||||
flex: 1;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.message-close {
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
opacity: 0.7;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.message-fade-enter-active,
|
||||
.message-fade-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.message-fade-enter-from {
|
||||
opacity: 0;
|
||||
transform: translateY(-40px);
|
||||
}
|
||||
|
||||
.message-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(-40px);
|
||||
}
|
||||
</style>
|
||||
26
src/pages/pc/components/Toast/type.ts
Normal file
26
src/pages/pc/components/Toast/type.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export type ToastType = 'success' | 'warning' | 'info' | 'error'
|
||||
|
||||
export interface ToastOptions {
|
||||
message: string
|
||||
type?: ToastType
|
||||
duration?: number
|
||||
showClose?: boolean
|
||||
}
|
||||
|
||||
export interface ToastInstance {
|
||||
close: () => void
|
||||
}
|
||||
|
||||
export interface ToastService {
|
||||
(options: ToastOptions | string): ToastInstance
|
||||
|
||||
success(message: string, options?: Omit<ToastOptions, 'message' | 'type'>): ToastInstance
|
||||
|
||||
warning(message: string, options?: Omit<ToastOptions, 'message' | 'type'>): ToastInstance
|
||||
|
||||
info(message: string, options?: Omit<ToastOptions, 'message' | 'type'>): ToastInstance
|
||||
|
||||
error(message: string, options?: Omit<ToastOptions, 'message' | 'type'>): ToastInstance
|
||||
|
||||
closeAll(): void
|
||||
}
|
||||
@@ -12,10 +12,10 @@ import {getCurrentStudyWord} from "@/hooks/dict.ts";
|
||||
import {useRuntimeStore} from "@/stores/runtime.ts";
|
||||
import Book from "@/pages/pc/components/Book.vue";
|
||||
import PopConfirm from "@/pages/pc/components/PopConfirm.vue";
|
||||
import {ElMessage, ElProgress, ElSlider} from 'element-plus';
|
||||
import {ElProgress, ElSlider} from 'element-plus';
|
||||
import Toast from '@/pages/pc/components/Toast/Toast.ts';
|
||||
import BaseButton from "@/components/BaseButton.vue";
|
||||
import {getDefaultDict} from "@/types/func.ts";
|
||||
import ConflictNotice from "@/pages/pc/components/ConflictNotice.vue";
|
||||
|
||||
const store = useBaseStore()
|
||||
const router = useRouter()
|
||||
@@ -47,7 +47,7 @@ async function init() {
|
||||
function startStudy() {
|
||||
if (store.sdict.id) {
|
||||
if (!store.sdict.words.length) {
|
||||
return ElMessage.warning('没有单词可学习!')
|
||||
return Toast.warning('没有单词可学习!')
|
||||
}
|
||||
window.umami?.track('startStudyDict', {
|
||||
name: store.sdict.name,
|
||||
@@ -59,7 +59,7 @@ function startStudy() {
|
||||
nav('study-word', {}, currentStudy)
|
||||
} else {
|
||||
window.umami?.track('no-dict')
|
||||
ElMessage.warning('请先选择一本词典')
|
||||
Toast.warning('请先选择一本词典')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ function setPerDayStudyNumber() {
|
||||
show = true
|
||||
tempPerDayStudyNumber = store.sdict.perDayStudyNumber
|
||||
} else {
|
||||
ElMessage.warning('请先选择一本词典')
|
||||
Toast.warning('请先选择一本词典')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ function handleBatchDel() {
|
||||
}
|
||||
})
|
||||
selectIds = []
|
||||
ElMessage.success("删除成功!")
|
||||
Toast.success("删除成功!")
|
||||
}
|
||||
|
||||
function toggleSelect(item) {
|
||||
@@ -181,7 +181,8 @@ const progressTextRight = $computed(() => {
|
||||
</div>
|
||||
个单词 <span class="color-blue cursor-pointer" @click="setPerDayStudyNumber">更改</span>
|
||||
</div>
|
||||
<BaseButton size="large" :disabled="!store.sdict.name" @click="startStudy">
|
||||
<!-- <BaseButton size="large" :disabled="!store.sdict.name" @click="startStudy">-->
|
||||
<BaseButton size="large" @click="startStudy">
|
||||
<div class="flex items-center gap-2">
|
||||
<span>开始学习</span>
|
||||
<Icon icon="icons8:right-round" class="text-2xl"/>
|
||||
|
||||
7
src/global.d.ts → src/types/global.d.ts
vendored
7
src/global.d.ts → src/types/global.d.ts
vendored
@@ -1,5 +1,3 @@
|
||||
export {}
|
||||
|
||||
declare global {
|
||||
interface Console {
|
||||
parse(v: any): void
|
||||
@@ -9,10 +7,11 @@ declare global {
|
||||
|
||||
interface Window {
|
||||
umami: {
|
||||
track(name:string,data?:any):void
|
||||
track(name: string, data?: any): void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.json = function (v: any, space = 0) {
|
||||
const json = JSON.stringify(
|
||||
v,
|
||||
@@ -30,3 +29,5 @@ console.json = function (v: any, space = 0) {
|
||||
console.parse = function (v: any) {
|
||||
console.log(JSON.parse(v))
|
||||
}
|
||||
|
||||
export {}
|
||||
@@ -47,9 +47,7 @@
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*.vue",
|
||||
"auto-imports.d.ts",
|
||||
"src/vite-env.d.ts",
|
||||
"src/global.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
|
||||
@@ -42,25 +42,25 @@ export default defineConfig(() => {
|
||||
open: true //如果存在本地服务端口,将在打包后自动展示
|
||||
}) : null,
|
||||
SlidePlugin(),
|
||||
// importToCDN({
|
||||
// modules: [
|
||||
// {
|
||||
// name: 'vue',
|
||||
// var: 'Vue',
|
||||
// path: `https://cdn.jsdelivr.net/npm/vue@3.5.14/dist/vue.global.prod.min.js`
|
||||
// },
|
||||
// {
|
||||
// name: 'vue-router',
|
||||
// var: 'VueRouter',
|
||||
// path: `https://cdn.jsdelivr.net/npm/vue-router@4.5.1/dist/vue-router.global.prod.min.js`
|
||||
// },
|
||||
// {
|
||||
// name: 'axios',
|
||||
// var: 'axios',
|
||||
// path: 'https://cdn.jsdelivr.net/npm/axios@1.9.0/dist/axios.min.js'
|
||||
// },
|
||||
// ]
|
||||
// })
|
||||
importToCDN({
|
||||
modules: [
|
||||
{
|
||||
name: 'vue',
|
||||
var: 'Vue',
|
||||
path: `https://type-words.oss-cn-shenzhen.aliyuncs.com/vue.global.prod.min.js`
|
||||
},
|
||||
{
|
||||
name: 'vue-router',
|
||||
var: 'VueRouter',
|
||||
path: `https://type-words.oss-cn-shenzhen.aliyuncs.com/vue-router.global.prod.min.js`
|
||||
},
|
||||
{
|
||||
name: 'axios',
|
||||
var: 'axios',
|
||||
path: 'https://type-words.oss-cn-shenzhen.aliyuncs.com/axios.min.js'
|
||||
},
|
||||
]
|
||||
})
|
||||
],
|
||||
define: {
|
||||
LATEST_COMMIT_HASH: JSON.stringify(latestCommitHash + (process.env.NODE_ENV === 'production' ? '' : ' (dev)')),
|
||||
@@ -73,11 +73,11 @@ export default defineConfig(() => {
|
||||
},
|
||||
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue']
|
||||
},
|
||||
// build: {
|
||||
// rollupOptions: {
|
||||
// external: ['axios'],// 使用全局的 axios。因为百度翻译库内部用了0.19版本的axios,会被打包到代码里面
|
||||
// }
|
||||
// },
|
||||
build: {
|
||||
rollupOptions: {
|
||||
external: ['axios'],// 使用全局的 axios。因为百度翻译库内部用了0.19版本的axios,会被打包到代码里面
|
||||
}
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
|
||||
Reference in New Issue
Block a user