feat: 消息Message

1. 增加消息提示组件
2. 优化代码结构
3. 统一导入导出
未来:
1. fix引入图标和字体,优化消息提示的图标和字体显示
2. 优化logo显示字体,引入老宋
This commit is contained in:
HeXiaoLong:Suanier 2025-05-21 17:59:39 +08:00
parent 89a8cae1c7
commit 278ea57617
26 changed files with 346 additions and 95 deletions

View File

@ -1,13 +1,17 @@
<script setup lang="ts">
import { NiButton, NiLogo, NiConfigProvider } from 'ni'
import { NiButton, NiLogo, NiConfigProvider, useNiMessage } from 'ni'
const message = useNiMessage()
let i = 0;
const messgeClick = () => {
message.info('ni-message你好' + ++i)
}
</script>
<template>
<NiConfigProvider>
<NiButton>按钮</NiButton>
<NiLogo type="primary" size="large" />
<NiLogo type="primary" size="large" @click="messgeClick"/>
</NiConfigProvider>
</template>
<style scoped></style>

View File

@ -1,3 +1 @@
import NiButton from './Button.vue'
export { NiButton }
export default NiButton
export {default as NiButton} from './Button.vue'

View File

@ -1,18 +1,13 @@
<template>
<slot></slot>
</template>
<script lang="ts" setup>
import { provide, watch } from 'vue'
import type { NiConfig } from './types'
import defaultConfig from './defaultConfig'
import Message from '~/message/Message.vue'
const props = defineProps<{
config?: Partial<NiConfig>
}>()
defaultConfig
//
watch(() => props.config, (newConfig) => {
Object.assign(defaultConfig, newConfig)
@ -20,4 +15,8 @@ watch(() => props.config, (newConfig) => {
//
provide('ni-config', defaultConfig)
</script>
</script>
<template>
<slot></slot>
<Message/>
</template>

View File

@ -1,17 +1,17 @@
import { reactive } from "vue"
import { NiConfig } from "./types"
import type { NiConfig } from "./types"
const defaultConfig = reactive<NiConfig>({
logoName: ['星', '撰'],
messageOnMounted: false,
size: 'default',
zIndex: 2000,
namespace: 'ni',
})
export default defaultConfig
function setNiConfig(config: Partial<NiConfig>) {
Object.assign(defaultConfig, config)
}
export { setNiConfig }
export default defaultConfig

View File

@ -1,5 +1,3 @@
import NiConfigProvider from './ConfigProvider.vue'
export * from './types'
export default NiConfigProvider
export { useNiConfig } from './useConfig'
export { default as NiConfigProvider } from './ConfigProvider.vue'
export { useNiConfig } from './useConfig'
export { setNiConfig } from './defaultConfig'

View File

@ -1,5 +1,8 @@
export interface NiConfig {
// 配置logo
logoName?: [string, string]
// 消息挂载
messageOnMounted?: boolean
name?: string
size?: 'small' | 'default' | 'large'
zIndex?: number

View File

@ -3,7 +3,7 @@ import type { NiConfig } from './types'
export function useNiConfig() {
const config = inject<Reactive<NiConfig>>('ni-config')
if (!config) {
throw new Error('useConfig must be used within NiConfigProvider')
}

View File

@ -1,19 +1,4 @@
import NiButton from './button'
import NiLogo from './logo'
import NiConfigProvider, { useNiConfig } from './config'
import { setNiConfig } from './config/defaultConfig'
export {
NiButton,
NiLogo,
NiConfigProvider,
useNiConfig,
setNiConfig,
}
export default {
NiButton,
NiLogo,
NiConfigProvider,
useNiConfig,
setNiConfig,
}
export { NiButton } from './button'
export { NiLogo } from './logo'
export { NiConfigProvider, useNiConfig, setNiConfig } from './config'
export { useNiMessage } from './message'

View File

@ -1,7 +1,7 @@
<script setup lang="ts">
import { computed } from 'vue';
import { logoProps } from './types';
import { type NiConfig, useNiConfig } from '~/config';
import { useNiConfig } from '~/config';
const niConfig = useNiConfig()
const props = defineProps(logoProps);
@ -10,7 +10,6 @@ const logoContent = computed<string[]>(() => {
const name = props.logoName || niConfig.get('logoName') || ['N', 'i']
return Array.isArray(name) ? name : [String(name)[0], String(name)[1]]
})
console.log(props, niConfig, logoContent);
</script>
<template>

View File

@ -1,3 +1 @@
import NiLogo from './Logo.vue'
export { NiLogo }
export default NiLogo
export { default as NiLogo } from './Logo.vue'

View File

@ -0,0 +1,124 @@
<script setup lang="ts">
import useNiMessage from './index'
import {useNiConfig} from '~/config'
import { onMounted } from 'vue'
const { message } = useNiMessage()
const config = useNiConfig()
onMounted(() => {
config.set('messageOnMounted', true)
})
</script>
<template>
<teleport to="body">
<TransitionGroup name="niMessage" tag="div" class="NiMessage" data-prefix="Ni">
<div class="niMessageContent" v-for="msg in message" :key="msg.messageId" @mouseenter="msg.pause"
@mouseleave="msg.resume">
<div class="niMessageIcon">
<!-- <div v-if="msg.type == 'log'" class="startMessageIconFont messageLog">
<NiLogo/>
</div> -->
<div v-if="msg.type == 'info'" class="startMessageIconFont messageInfo">&#xe871;</div>
<div v-else-if="msg.type == 'success'" class="startMessageIconFont messageSuccess">&#xe842;
</div>
<div v-else-if="msg.type == 'warning'" class="startMessageIconFont messageWarning">&#xe83d;
</div>
<div v-else-if="msg.type == 'error'" class="startMessageIconFont messageError">&#xe839;</div>
</div>
<div class="niMessageBody">
{{ msg.content }}
</div>
</div>
</TransitionGroup>
</teleport>
</template>
<style scoped lang="scss">
@use '@/styles/style.scss' as niStyle;
//
.niMessage-enter-active,
.niMessage-leave-active {
transition: all .5s ease-in-out;
}
.niMessage-enter-from,
.niMessage-leave-to {
transform: translateY(-100%);
opacity: 0;
}
.NiMessage {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
/* 新消息在上方 */
gap: 10px;
z-index: 9999;
pointer-events: none;
/* 容器不阻挡点击 */
--Ni-button-info-bg-default: #2080f0;
--Ni-button-success-bg-default: #18a058;
--Ni-button-warning-bg-default: #f0a020;
--Ni-button-error-bg-default: #d03050;
--Ni-content-padding: 10px;
--Ni-border-radius: 4px;
}
.niMessageContent {
position: relative;
display: flex;
align-items: center;
padding: var(--Ni-content-padding);
transition: all 0.3s;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
border-radius: var(--Ni-border-radius);
background: #fefefe;
cursor: pointer;
letter-spacing: 1px;
pointer-events: auto;
/* 单个消息可交互 */
max-width: min(1000px, 80vw);
&>div.niMessageIcon {
position: relative;
flex-shrink: 0;
margin-right: .8rem;
&>div {
font-size: 1.6rem;
}
&>div.messageLog {
font-size: 6px;
}
&>div.messageInfo {
color: var(--Ni-button-info-bg-default);
}
&>div.messageSuccess {
color: var(--Ni-button-success-bg-default);
}
&>div.messageWarning {
color: var(--Ni-button-warning-bg-default);
}
&>div.messageError {
color: var(--Ni-button-error-bg-default);
}
}
&>div.niMessageBody {
position: relative;
display: flex;
align-items: center;
font-size: 1em;
}
}
</style>

View File

@ -0,0 +1,76 @@
import { reactive } from "vue"
import type { MessageOptions, MessageInstance } from "./type"
import defaultConfig from "~/config/defaultConfig"
/*
*
* 1. message存储一个个进入队列的消息
* 2.
* 3.
* 4.
* 5. ID
* 6.
* 7.
* 8.
* ---
* Message组件被内嵌入ConfigProvider组件中,ConfigProvider组件先挂载
*
*/
// 消息列表
const message = reactive<MessageInstance[]>([]);
// 使用消息组件
export const useNiMessage = () => {
// 添加消息
const addMessage = (options: MessageOptions) => {
// 检测是否在NiConfigProvider组件中使用
if (!defaultConfig.messageOnMounted) {
throw new Error('useMessage must be used within NiConfigProvider!')
};
// 将时间戳作为消息ID
const messageId = new Date().getTime() + Math.random();
// 创建定时器,到时间消息自动销毁
let timer: NodeJS.Timeout | null = null;
// 定时器
const startTimer = () => {
clearTimer()
timer = setTimeout(() => {
const index = message.findIndex(item => item.messageId === messageId)
message.splice(index, 1)
}, options.duration || 3000);
}
// 销毁消息
const destroy = () => {
const index = message.findIndex(item => item.messageId === messageId)
message.splice(index, 1)
}
// 清除定时器
const clearTimer = () => {
timer && clearTimeout(timer)
timer = null;
}
// 执行初次定时器
startTimer()
// 添加消息
message.unshift({
...options,
messageId,
timer,
pause: clearTimer,
resume: startTimer,
destroy: () => {
clearTimer()
destroy()
}
})
}
return {
message,
show: addMessage,
info: (content: string, option: MessageOptions) => addMessage({ ...option, content, type: 'info' }),
success: (content: string, option: MessageOptions) => addMessage({ ...option, content, type: 'success' }),
warning: (content: string, option: MessageOptions) => addMessage({ ...option, content, type: 'warning' }),
error: (content: string, option: MessageOptions) => addMessage({ ...option, content, type: 'error' }),
}
}
export default useNiMessage

View File

@ -0,0 +1,18 @@
export interface MessageOptions {
content: string
type?: 'info' | 'success' | 'warning' | 'error'
duration?: number
}
export interface MessageInstance extends MessageOptions {
// 消息ID
messageId: number,
// 定时器
timer: NodeJS.Timeout | null,
// 暂停消息延时销毁
pause: () => void,
// 恢复消息延时销毁
resume: () => void,
// 销毁消息
destroy: () => void,
}

View File

@ -60,49 +60,18 @@
animation: $name $duration $timing $delay $iteration $direction $fill;
/*
* 1. $name必需参数
* 动画名称
* 必须与 @keyframes 定义的名称匹配
* 例如fade-in, slide-in-right
* 动画名称 fade-in, slide-in-right
* 2. $duration可选参数默认值1s
* 动画持续时间
* 可选值
* 时间值0.5s, 1s, 2s
* 毫秒值500ms, 1000ms
* 动画持续时间 0.5s, 1s, 2s 500ms, 1000ms
* 3. $timing可选参数默认值ease
* 动画的时间函数
* 可选值
* linear匀速
* ease缓入缓出默认
* ease-in缓入
* ease-out缓出
* ease-in-out缓入缓出
* cubic-bezier(n,n,n,n)自定义贝塞尔曲线
* steps(n)分步动画
* 4. $delay可选参数默认值0s
* 动画开始前的延迟时间
* 可选值
* 时间值0.5s, 1s
* 毫秒值500ms, 1000ms
* 负值提前开始动画
* 5. $iteration可选参数默认值1
* 动画重复次数
* 可选值
* 数字1, 2, 3
* infinite无限循环
* 动画的时间函数 linear匀速 ease缓入缓出默认 ease-in缓入 ease-out缓出 ease-in-out缓入缓出
* cubic-bezier(n,n,n,n)自定义贝塞尔曲线 steps(n)分步动画
* 4. $delay可选参数默认值0s 延迟时间 负值提前开始动画
* 5. $iteration可选参数默认值1 数字1, 2, 3 infinite无限循环
* 6. $direction可选参数默认值normal
* 动画方向
* 可选值
* normal正常播放
* reverse反向播放
* alternate交替播放正向->反向
* alternate-reverse交替反向播放反向->正向
* 动画方向 normal正常播放 reverse反向播放 alternate交替播放正向->反向 alternate-reverse交替反向播放反向->正向
* 7. $fill可选参数默认值forwards
* 动画填充模式
* 可选值
* forwards保持最后一帧
* backwards应用第一帧
* both同时应用 forwards backwards
* none不应用任何填充
* 动画填充模式 forwards保持最后一帧backwards应用第一帧both同时应用 forwards backwardsnone不应用任何填充
*/
}

View File

@ -1,3 +1 @@
import NiButton from './Button.vue';
export { NiButton };
export default NiButton;
export { default as NiButton } from './Button.vue';

View File

@ -1,12 +1,12 @@
import { ExtractPropTypes, PropType } from 'vue';
export declare const buttonProps: {
readonly type: {
readonly type: PropType<"default" | "primary" | "success" | "warning" | "danger">;
readonly type: PropType<"default" | "success" | "warning" | "primary" | "danger">;
readonly default: "default";
readonly validator: (value: string) => boolean;
};
readonly size: {
readonly type: PropType<"small" | "medium" | "large">;
readonly type: PropType<"small" | "large" | "medium">;
readonly default: "medium";
readonly validator: (value: string) => boolean;
};

View File

@ -0,0 +1,12 @@
import type { NiConfig } from "./types";
declare const defaultConfig: {
logoName?: [string, string] | undefined;
messageOnMounted?: boolean | undefined;
name?: string | undefined;
size?: "small" | "default" | "large" | undefined;
zIndex?: number | undefined;
namespace?: string | undefined;
};
declare function setNiConfig(config: Partial<NiConfig>): void;
export { setNiConfig };
export default defaultConfig;

3
types/components/config/index.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
export { default as NiConfigProvider } from './ConfigProvider.vue';
export { useNiConfig } from './useConfig';
export { setNiConfig } from './defaultConfig';

9
types/components/config/types.d.ts vendored Normal file
View File

@ -0,0 +1,9 @@
export interface NiConfig {
logoName?: [string, string];
messageOnMounted?: boolean;
name?: string;
size?: 'small' | 'default' | 'large';
zIndex?: number;
namespace?: string;
}
export type NiConfigKey = keyof NiConfig;

13
types/components/config/useConfig.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
import type { NiConfig } from './types';
export declare function useNiConfig(): {
config: import("vue").ComputedRef<{
logoName?: [string, string] | undefined;
messageOnMounted?: boolean | undefined;
name?: string | undefined;
size?: "small" | "default" | "large" | undefined;
zIndex?: number | undefined;
namespace?: string | undefined;
}>;
get: (key: keyof NiConfig) => string | number | boolean | [string, string] | undefined;
set: (key: keyof NiConfig, value: any) => void;
};

View File

@ -1,6 +1,4 @@
import NiButton from './button';
export { NiButton, };
declare const _default: {
NiButton: import("vue").DefineComponent<{}, {}, any>;
};
export default _default;
export { NiButton } from './button';
export { NiLogo } from './logo';
export { NiConfigProvider, useNiConfig, setNiConfig } from './config';
export { useNiMessage } from './message';

1
types/components/logo/index.d.ts vendored Normal file
View File

@ -0,0 +1 @@
export { default as NiLogo } from './Logo.vue';

21
types/components/logo/types.d.ts vendored Normal file
View File

@ -0,0 +1,21 @@
import { ExtractPropTypes, PropType } from 'vue';
export declare const logoProps: {
readonly logoName: {
readonly type: PropType<string[]>;
};
readonly type: {
readonly type: PropType<"default" | "primary">;
readonly default: "default";
readonly validator: (value: string) => boolean;
};
readonly size: {
readonly type: PropType<"small" | "large" | "medium">;
readonly default: "medium";
readonly validator: (value: string) => boolean;
};
readonly disabled: {
readonly type: BooleanConstructor;
readonly default: false;
};
};
export type LogoProps = ExtractPropTypes<typeof logoProps>;

10
types/components/message/index.d.ts vendored Normal file
View File

@ -0,0 +1,10 @@
import type { MessageOptions, MessageInstance } from "./type";
export declare const useNiMessage: () => {
message: import("vue").Reactive<MessageInstance[]>;
show: (options: MessageOptions) => void;
info: (content: string, option: MessageOptions) => void;
success: (content: string, option: MessageOptions) => void;
warning: (content: string, option: MessageOptions) => void;
error: (content: string, option: MessageOptions) => void;
};
export default useNiMessage;

13
types/components/message/type.d.ts vendored Normal file
View File

@ -0,0 +1,13 @@
/// <reference types="node" />
export interface MessageOptions {
content: string;
type?: 'info' | 'success' | 'warning' | 'error';
duration?: number;
}
export interface MessageInstance extends MessageOptions {
messageId: number;
timer: NodeJS.Timeout | null;
pause: () => void;
resume: () => void;
destroy: () => void;
}

2
types/index.d.ts vendored
View File

@ -1,9 +1,11 @@
import { App } from 'vue';
import * as components from './components';
export * from './components';
export declare const version = "0.0.1";
export declare const install: (app: App) => void;
declare const _default: {
version: string;
install: (app: App<any>) => void;
setNiConfig: typeof components.setNiConfig;
};
export default _default;