Repository: Tencent/tdesign-mobile-vue
Pull request #931 (2023-07-05 10:40:09)

fix(Message): resolved link invalid
<!--
首先，感谢你的贡献！😄
请阅读并遵循 [TDesign 贡献指南](https://github.com/Tencent/tdesign/blob/main/docs/contributing.md)，填写以下 pull request 的信息。
PR 在维护者审核通过后会合并，谢谢！
-->

### 🤔 这个 PR 的性质是？

- [x] 日常 bug 修复
- [ ] 新特性提交
- [ ] 文档改进
- [ ] 演示代码改进
- [ ] 组件样式/交互改进
- [ ] CI/CD 改进
- [ ] 重构
- [ ] 代码风格优化
- [ ] 测试用例
- [ ] 分支合并
- [ ] 其他

### 🔗 相关 Issue

<!--
1. 描述相关需求的来源，如相关的 issue 讨论链接。
-->

### 💡 需求背景和解决方案

<!--
1. 要解决的具体问题。
2. 列出最终的 API 实现和用法。
3. 涉及UI/交互变动需要有截图或 GIF。
-->

### 📝 更新日志

<!--
从用户角度描述具体变化，以及可能的 breaking change 和其他风险。
-->

- fix(Message): 修复 `link` 、`onLinkClick`属性无效

- [ ] 本条 PR 不需要纳入 Changelog

### ☑️ 请求合并前的自查清单

⚠️ 请自检并全部**勾选全部选项**。⚠️

- [ ] 文档已补充或无须补充
- [ ] 代码演示已提供或无须提供
- [ ] TypeScript 定义已补充或无须补充
- [ ] Changelog 已提供或无须提供


github-actions[bot]: **Size Change:** +143 B (0%) 

**Total Size:** 500 kB

| Filename | Size | Change |
| :--- | :---: | :---: |
| `./dist/tdesign.css` | 37.4 kB | +38 B (0%) |
| `./dist/tdesign.js` | 294 kB | +91 B (0%) |
| `./dist/tdesign.min.css` | 27.2 kB | +13 B (0%) |
| `./dist/tdesign.min.js` | 141 kB | +1 B (0%) |

<details><summary>ℹ️ <strong>View Unchanged</strong></summary>

| Filename | Size |
| :--- | :---: |
| `./dist/reset.css` | 672 B |

</details>


github-actions[bot]: <a href="https://preview-pr931-tdesign-mobile-vue.surge.sh"><img height="96" alt="完成" src="https://user-images.githubusercontent.com/15634204/150816437-9f5bb788-cd67-4cbc-9897-b82d74e9aa65.png" /></a>
<!-- [工作流地址](https://github.com/Tencent/tdesign-mobile-vue/actions/runs/5463379825) -->
<!-- AUTO_PREVIEW_HOOK -->
github-actions[bot]: **Size Change:** +166 B (0%) 

**Total Size:** 387 kB

| Filename | Size | Change |
| :--- | :---: | :---: |
| `./dist/tdesign.css` | 37.4 kB | +38 B (0%) |
| `./dist/tdesign.js` | 204 kB | +122 B (0%) |
| `./dist/tdesign.min.css` | 27.2 kB | +13 B (0%) |
| `./dist/tdesign.min.js` | 118 kB | -7 B (0%) |

<details><summary>ℹ️ <strong>View Unchanged</strong></summary>

| Filename | Size |
| :--- | :---: |
| `./dist/reset.css` | 672 B |

</details>



Patch:
diff --git a/src/message/demos/base.vue b/src/message/demos/base.vue
index 0328beb9..50d9aebd 100644
--- a/src/message/demos/base.vue
+++ b/src/message/demos/base.vue
@@ -14,12 +14,14 @@
   <t-message v-model="visible1" :offset="['10px', '16px']" :duration="5000" content="这是一条普通的通知信息" />
 
   <!-- 带关闭通知 -->
-  <t-message v-model="visible2" :offset="['10px', 16]" :duration="-1" content="这是一条普通的通知信息">
-    <template #closeBtn>
-      <t-button class="close-btn" variant="text">按钮</t-button>
-      <closeIcon class="t-message__icon--right" />
-    </template>
-  </t-message>
+  <t-message
+    v-model="visible2"
+    :offset="['10px', 16]"
+    :duration="-1"
+    content="这是一条普通的通知信息"
+    :link="{ content: '按钮' }"
+    :close-btn="true"
+  />
 
   <!-- 滚动通知 -->
   <t-message
@@ -31,11 +33,7 @@
   />
 
   <!-- 带按钮通知 -->
-  <t-message v-model="visible4" :offset="[10, 16]" :duration="-1" content="这是一条普通的通知信息">
-    <template #closeBtn>
-      <t-button class="close-btn" variant="text">按钮</t-button>
-    </template>
-  </t-message>
+  <t-message v-model="visible4" :offset="[10, 16]" :duration="-1" content="这是一条普通的通知信息" link="链接" />
 </template>
 
 <script lang="ts" setup>
diff --git a/src/message/message.vue b/src/message/message.vue
index 8c274a66..d03ad0ec 100644
--- a/src/message/message.vue
+++ b/src/message/message.vue
@@ -1,7 +1,9 @@
 <template>
   <transition name="message">
     <div v-if="currentVisible" ref="root" :class="rootClasses" :style="rootStyles">
-      <t-node v-if="computedPrefixIcon" :content="computedPrefixIcon" :class="`${name}__icon--left`"></t-node>
+      <div v-if="prefixIconContent" :class="`${name}__icon--left`">
+        <t-node :content="prefixIconContent" />
+      </div>
       <div ref="textWrapDOM" :class="textWrapClasses">
         <div
           ref="textDOM"
@@ -12,8 +14,11 @@
           <t-node v-if="computedContent" :content="computedContent"></t-node>
         </div>
       </div>
-      <div v-if="computedCloseBtn" :class="`${name}__close-wrap`" @click="onCloseBtnClick">
-        <t-node :content="computedCloseBtn" :class="`${name}__icon--right`"></t-node>
+      <div v-if="linkContent" :class="`${name}__link`" @click="onLinkClick">
+        <t-node :content="linkContent"></t-node>
+      </div>
+      <div v-if="closeBtnContent" :class="[`${name}__close-wrap`, `${name}__icon--right`]" @click="onCloseBtnClick">
+        <t-node :content="closeBtnContent"></t-node>
       </div>
     </div>
   </transition>
@@ -33,7 +38,9 @@ import {
   onMounted,
 } from 'vue';
 import { CheckCircleFilledIcon, CloseIcon, InfoCircleFilledIcon } from 'tdesign-icons-vue-next';
-import isFunction from 'lodash/isFunction';
+import { isObject, isString } from '@vueuse/core';
+
+import Link from '../link';
 import messageProps from './props';
 import { DrawMarquee, TdMessageProps } from './type';
 import config from '../config';
@@ -47,7 +54,7 @@ const iconDefault = {
   warning: h(InfoCircleFilledIcon),
   error: h(InfoCircleFilledIcon),
 };
-
+const closeBtnDefault = h(CloseIcon);
 export default defineComponent({
   name,
   components: { TNode },
@@ -107,31 +114,27 @@ export default defineComponent({
       };
     });
 
-    const computedPrefixIcon = computed(() => {
-      const { icon } = props;
-      if (!icon) return null;
-      if (icon && !context.slots.icon && !isFunction(icon)) {
-        const theme = props.theme as string;
-        return iconDefault?.[theme] || null;
-      }
-      return renderTNode(internalInstance, 'icon');
-    });
-
+    const prefixIconContent = computed(() =>
+      renderTNode(internalInstance, 'icon', { defaultNode: iconDefault?.[props.theme || 'info'] }),
+    );
     // content
     const computedContent = computed(() => renderContent(internalInstance, 'default', 'content'));
 
     // closeBtn
-    const computedCloseBtn = computed(() => {
-      const { closeBtn } = props;
-      if (isFunction(closeBtn || context.slots.closeBtn)) {
-        return renderTNode(internalInstance, 'closeBtn');
+    const closeBtnContent = computed(() => renderTNode(internalInstance, 'closeBtn', { defaultNode: closeBtnDefault }));
+
+    // link
+    const linkContent = computed(() => {
+      if (typeof props.link === 'function' || context.slots?.link) {
+        return renderTNode(internalInstance, 'link');
       }
-      if (closeBtn) {
-        return h(CloseIcon);
+
+      if (isObject(props.link) || isString(props.link)) {
+        const _link = isObject(props.link) ? { ...props.link } : { content: props.link };
+        return h(Link, { theme: 'primary', ..._link });
       }
       return null;
     });
-
     // 动画
     const animateStyle = computed(() => ({
       transform: state.offset ? `translateX(${state.offset}px)` : '',
@@ -196,6 +199,10 @@ export default defineComponent({
       }, 0);
     };
 
+    const onLinkClick = (e: MouseEvent) => {
+      props.onLinkClick?.({ e });
+    };
+
     const onCloseBtnClick = () => {
       props.onCloseBtnClick?.();
       setVisible(false);
@@ -235,13 +242,15 @@ export default defineComponent({
       rootClasses,
       textWrapClasses,
       rootStyles,
-      computedPrefixIcon,
+      prefixIconContent,
       computedContent,
-      computedCloseBtn,
+      closeBtnContent,
+      linkContent,
       textWrapDOM,
       textDOM,
       animateStyle,
       onCloseBtnClick,
+      onLinkClick,
       handleTransitionend,
     };
   },


Test patch:
diff --git a/src/message/__test__/index.test.jsx b/src/message/__test__/index.test.jsx
index 6db08bb4..a3ea399d 100644
--- a/src/message/__test__/index.test.jsx
+++ b/src/message/__test__/index.test.jsx
@@ -3,13 +3,21 @@ import { mount } from '@vue/test-utils';
 import { describe, it, expect } from 'vitest';
 import { nextTick } from 'vue';
 import Message from '../message.vue';
-import { AppIcon as TIconApp } from 'tdesign-icons-vue-next';
+import { AppIcon, AppIcon as TIconApp } from 'tdesign-icons-vue-next';
 const prefix = 't';
 const name = `${prefix}-message`;
+
 describe('Message.vue', () => {
-  describe('event', () => {
-    const offset = [-10, 20];
+  describe('props', () => {
+    it(': align', () => {
+      ['left', 'center'].map((align) => {
+        const wrapper =  mount(<Message visible align={align} />);
+        expect(wrapper.find(`.${name}`).classes().includes(`${name}-align--${align}`)).toBeTruthy();
+      });
+    })
+
     it(': offset', () => {
+      const offset = [-10, 20];
       const wrapper = mount(<Message visible offset={offset} />);
       const $message = wrapper.find(`.${name}`);
       expect(
@@ -61,10 +69,63 @@ describe('Message.vue', () => {
       const wrapper = mount(<Message visible closeBtn={closeBtn} />);
       expect(wrapper.findComponent(TIconApp).exists()).toBeTruthy();
     });
+
+    it('icon', () => {
+      // icon = false
+      const wrapper0 = mount(() => <Message visible={true} icon={false} content="这是一条消息通知" />);
+      expect(wrapper0.find('.t-icon').exists()).toBeFalsy();
+
+      // icon 使用内置主题icon
+      const wrapper1 = mount(() => <Message visible={true} content="这是一条消息通知" theme={'success'} />);
+      const successIcon = wrapper1.find('.t-icon-check-circle-filled');
+      expect(successIcon).toBeTruthy();
+
+      // icon is function
+      const appIcon = () => h(TIconApp);
+      const wrapper2 = mount(() => <Message visible={true} content="这是一条消息通知" icon={appIcon} />);
+      expect(wrapper2.find(`.${name}__icon--left`).findComponent(TIconApp).exists()).toBeTruthy();
+
+      // icon is slot
+      const slots = {
+        icon: () => h(TIconApp),
+      };
+      const wrapper3 = mount(<Message visible={true} content="这是一条消息通知" v-slots={slots} />);
+      expect(wrapper3.find(`.${name}__icon--left`).findComponent(TIconApp).exists()).toBeTruthy();
+    });
+
+
+    it('link', () => {
+      const wrapper0 = mount(() => <Message visible={true} content="这是一条消息通知"/>);
+      expect(wrapper0.find(`.${name}__link`).exists()).toBeFalsy();
+
+      // lin is string
+      const wrapper1 = mount(() => <Message visible={true} content="这是一条消息通知" link={'link'} />);
+      expect(wrapper1.find(`.${name}__link`).exists()).toBeTruthy();
+      expect(wrapper1.find(`.${name}__link`).text()).toEqual('link')
+      // link is Object
+      const wrapper2 = mount(() => <Message visible={true} content="这是一条消息通知" link={{content: 'content'}} />);
+      expect(wrapper2.find(`.${name}__link`).exists()).toBeTruthy();
+      expect(wrapper2.find(`.${name}__link`).text()).toEqual('content')
+
+      // link is function
+      const appIcon = () => h(TIconApp);
+      const wrapper3 = mount(() => <Message visible={true} content="这是一条消息通知" link={appIcon} />);
+      expect(wrapper3.find(`.${name}__link`).exists()).toBeTruthy();
+      expect(wrapper3.find(`.${name}__link`).findComponent(TIconApp).exists()).toBeTruthy();
+
+      // link is slot
+      const slots = {
+        link: () => h(TIconApp),
+      };
+      const wrapper4 = mount(<Message visible={true} content="这是一条消息通知" v-slots={slots} />);
+      expect(wrapper4.find(`.${name}__link`).exists()).toBeTruthy();
+      expect(wrapper4.find(`.${name}__link`).findComponent(TIconApp).exists()).toBeTruthy();
+    });
+
   });
 
   describe('event', () => {
-    it('close', () => {
+    it('onCloseBtn', () => {
       const fn = vi.fn();
       const wrapper = mount(() => (
         <Message visible={true} content="这是一条带关闭的消息通知，常驻可关闭" closeBtn={true} onCloseBtnClick={fn} />
@@ -73,16 +134,15 @@ describe('Message.vue', () => {
       closeBtn.trigger('click');
       expect(fn).toBeCalledTimes(1);
     });
-  });
-
-  describe('slots', () => {
-    it('icon', () => {
-      const wrapper0 = mount(() => <Message visible={true} icon={false} content="这是一条消息通知" />);
-      expect(wrapper0.find('.t-icon').exists()).toBeFalsy();
 
-      const wrapper = mount(() => <Message visible={true} content="这是一条消息通知" theme={'success'} />);
-      const successIcon = wrapper.find('.t-icon-check-circle-filled');
-      expect(successIcon).toBeTruthy();
+    it('onLinkClick', () => {
+      const fn = vi.fn();
+      const wrapper = mount(() => (
+        <Message visible={true} content="这是一条消息通知" link={'链接'} onLinkClick={fn} />
+      ));
+      const _link = wrapper.find(`.${name}__link`);
+      _link.trigger('click');
+      expect(fn).toBeCalledTimes(1);
     });
   });
 });