From 04e5161554d9e3c26b2776d59a4abd48f23f4a1d Mon Sep 17 00:00:00 2001 From: zyronon Date: Tue, 12 Aug 2025 22:53:08 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E6=B7=BB=E5=8A=A0Toast=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.ts | 2 +- src/pages/pc/components/Toast/Toast.ts | 120 ++++++++++++++ src/pages/pc/components/Toast/Toast.vue | 206 ++++++++++++++++++++++++ src/pages/pc/components/Toast/type.ts | 26 +++ src/pages/pc/word/WordHomePage.vue | 15 +- src/{ => types}/global.d.ts | 7 +- tsconfig.json | 2 - vite.config.ts | 48 +++--- 8 files changed, 389 insertions(+), 37 deletions(-) create mode 100644 src/pages/pc/components/Toast/Toast.ts create mode 100644 src/pages/pc/components/Toast/Toast.vue create mode 100644 src/pages/pc/components/Toast/type.ts rename src/{ => types}/global.d.ts (92%) diff --git a/src/main.ts b/src/main.ts index 00fba8c8..beef3920 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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) diff --git a/src/pages/pc/components/Toast/Toast.ts b/src/pages/pc/components/Toast/Toast.ts new file mode 100644 index 00000000..ff20c34d --- /dev/null +++ b/src/pages/pc/components/Toast/Toast.ts @@ -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) => { + return Toast({message, type: 'success', ...options}) +} + +Toast.warning = (message: string, options?: Omit) => { + return Toast({message, type: 'warning', ...options}) +} + +Toast.info = (message: string, options?: Omit) => { + return Toast({message, type: 'info', ...options}) +} + +Toast.error = (message: string, options?: Omit) => { + return Toast({message, type: 'error', ...options}) +} + +// 关闭所有消息 +Toast.closeAll = () => { + toastContainers.forEach(container => container.instance.close()) + toastContainers = [] +} + +export default Toast diff --git a/src/pages/pc/components/Toast/Toast.vue b/src/pages/pc/components/Toast/Toast.vue new file mode 100644 index 00000000..fd975be9 --- /dev/null +++ b/src/pages/pc/components/Toast/Toast.vue @@ -0,0 +1,206 @@ + + + + + diff --git a/src/pages/pc/components/Toast/type.ts b/src/pages/pc/components/Toast/type.ts new file mode 100644 index 00000000..83035f5e --- /dev/null +++ b/src/pages/pc/components/Toast/type.ts @@ -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): ToastInstance + + warning(message: string, options?: Omit): ToastInstance + + info(message: string, options?: Omit): ToastInstance + + error(message: string, options?: Omit): ToastInstance + + closeAll(): void +} diff --git a/src/pages/pc/word/WordHomePage.vue b/src/pages/pc/word/WordHomePage.vue index 6e1d1ab8..279412f3 100644 --- a/src/pages/pc/word/WordHomePage.vue +++ b/src/pages/pc/word/WordHomePage.vue @@ -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(() => { 个单词 更改 - + +
开始学习 diff --git a/src/global.d.ts b/src/types/global.d.ts similarity index 92% rename from src/global.d.ts rename to src/types/global.d.ts index 1fc0180f..ce1821d0 100644 --- a/src/global.d.ts +++ b/src/types/global.d.ts @@ -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 {} diff --git a/tsconfig.json b/tsconfig.json index c9efcf51..f27711dd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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": [ { diff --git a/vite.config.ts b/vite.config.ts index f46f1793..94d061f2 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -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: {