Skip to content

更多的提示工具#336

Merged
Stapxs merged 4 commits intoStapxs:devfrom
Chzxxuanzheng:more-tooltip
Jan 19, 2026
Merged

更多的提示工具#336
Stapxs merged 4 commits intoStapxs:devfrom
Chzxxuanzheng:more-tooltip

Conversation

@Chzxxuanzheng
Copy link
Contributor

@Chzxxuanzheng Chzxxuanzheng commented Jan 2, 2026

  • 重构了旧的提示工具
  • 新增了图片预览提示工具
图片

顺便反馈一个bug,为毛llbt的动画表情尺寸这么大?这分明是常规图片的尺寸啊,class也没有face。快去修bug去

Summary by Sourcery

引入一个支持长按悬停的新通用提示系统,并将其应用到用户信息和表情预览中。

新功能:

  • 添加可复用的长悬停指令和提示基础设施,能够在光标位置渲染任意 Vue 组件。
  • 在表情面板中为动态贴纸和本地表情图片添加图片预览提示。
  • 为头像和 @提及 添加用户信息提示,替换之前专用的用户信息面板组件。

优化改进:

  • 优化聊天和表情面板样式,包括贴纸和表情项的网格布局与尺寸,以及成员信息和图片预览的共享提示样式。
  • 将表情面板组件转换为使用 <script setup> 以集成新的提示系统。
Original summary in English

Summary by Sourcery

Introduce a new generic tooltip system with long-hover support and apply it to user info and emoji previews.

New Features:

  • Add a reusable long-hover directive and tooltip infrastructure capable of rendering arbitrary Vue components at cursor position.
  • Add image preview tooltips for animated stickers and local emoji images in the face panel.
  • Add a user info tooltip for avatars and @mentions, replacing the previous dedicated user info panel component.

Enhancements:

  • Refine chat and emoji panel styles, including grid layout and sizing for sticker and emoji items, and shared tooltip styling for member info and image previews.
  • Convert the face panel component to use <script setup> for new tooltip integration.

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了 4 个问题,并给出了一些整体反馈:

  • resolveBinding 中,你有时会构造 { component: result } / { component: binding, props: {} },但在其他地方(以及 Tooltip.vue 中)代码期望的是 compData.comp,所以这些分支应该设置 comp 而不是 component,以避免在运行时向 <component :is="compData.comp"> 传入 undefined
  • UserInfoTooltip.vue 在 setup 阶段只执行了一次把 userProp 解析为 userInfo(以及 titleClass)的逻辑,所以之后如果 user 这个 prop 发生变化,tooltip 不会更新;建议基于 userProp 使用一个 computed(并基于它派生 titleClass),这样内容就能随着响应式变化保持同步。
给 AI Agent 的提示
Please address the comments from this code review:

## Overall Comments
- In `resolveBinding` you sometimes construct `{ component: result }` / `{ component: binding, props: {} }` but elsewhere (and in `Tooltip.vue`) you expect `compData.comp`, so these branches should set `comp` instead of `component` to avoid `undefined` being passed to `<component :is="compData.comp">` at runtime.
- `UserInfoTooltip.vue` resolves `userProp` into `userInfo` (and `titleClass`) once at setup time, so if the `user` prop changes later the tooltip won't update; consider using a `computed` based on `userProp` (and deriving `titleClass` from it) so content stays in sync with reactive changes.

## Individual Comments

### Comment 1
<location> `src/renderer/src/function/utils/appUtil.ts:2035-2044` </location>
<code_context>
+    | (() => T | VueCompData<T> )
+    | ((eventData: {x: number, y: number}) => T | VueCompData<T> )
+
+function resolveBinding<T extends Component>(binding: VTooltipBinding<T>, eventData: {x: number, y: number}): VueCompData<T> {
+    if (typeof binding === 'function') {
+        // eslint-disable-next-line multiline-ternary
+        const result = binding.length === 0
+            // eslint-disable-next-line multiline-ternary
+            ? (binding as () => T | VueCompData<T>)()
+            : (binding as (eventData: {x: number, y: number}) => T | VueCompData<T>)(eventData)
+        if ('comp' in result) return result
+        return { component: result } as unknown as VueCompData<T>
+    } else if ('comp' in binding) {
+        return binding
+    } else {
+        return { component: binding, props: {} } as unknown as VueCompData<T>
+    }
+}
</code_context>

<issue_to_address>
**issue (bug_risk):** Returned tooltip data uses `component` key but the rest of the code expects `comp`, which will break tooltip rendering.

In `resolveBinding`, the returned objects use `{ component: ... }`, but `VueCompData<T>` and the tooltip pipeline (e.g. `Tooltip.vue` reading `compData.comp`) expect `comp`. This means `compData.comp` will be `undefined` for the function and non-`comp` branches, breaking rendering at runtime. Please update both `component` occurrences to `comp`, e.g.:

```ts
if ('comp' in result) return result
return { comp: result } as unknown as VueCompData<T>
...
} else {
  return { comp: binding, props: {} } as unknown as VueCompData<T>
}
```
</issue_to_address>

### Comment 2
<location> `src/renderer/src/components/tooltip/UserInfoTooltip.vue:1-7` </location>
<code_context>
-\<!--
- * @FileDescription: 群成员消息悬浮窗
- * @Author: Mr.Lee
</code_context>

<issue_to_address>
**issue (bug_risk):** Leading backslash before the HTML comment will render a stray `\` in the DOM and is unnecessary.

Starting the template with `\<!--` makes the backslash render as a literal character before the HTML comment, which can introduce an unwanted `\` node and confuse tooling. This should just be a standard HTML comment:

```vue
<!--
 * @FileDescription: 群成员消息悬浮窗
 ...
-->
```
</issue_to_address>

### Comment 3
<location> `src/renderer/src/function/elements/vueComp.ts:43` </location>
<code_context>
+    [K in keyof E]?: E[K]
+} : never
+
+type SetOptional<O extends Record<string, unknown>, T> = {
+    [K in keyof O as T extends O[K] ? K : never]?: O[K]
+} & {
</code_context>

<issue_to_address>
**issue (complexity):** Consider simplifying the VueCompData type by removing the SetOptional/CreateOptionalKey helpers and using simpler optional properties and Partial-typed emits instead.

You can drop a good chunk of the type-level machinery without losing any runtime or useful typing functionality.

Specifically, `SetOptional` and `CreateOptionalKey` only control whether `model`/`props` appear as required vs optional based on emptiness. That adds a lot of indirection for a small ergonomic gain. You can also simplify `GetEmitArgs` to just use `Partial`.

Concrete changes:

```ts
// Remove these helpers entirely
// type SetOptional<...> = ...
// type CreateOptionalKey<...> = ...

type GetEmitArgs<T extends Component> = Partial<GetEmit<T>>

export type VueCompData<T extends Component> = {
  comp: T
  // keeps model typed, but always optional at the top level
  model?: GetModel<T>
  // keeps props typed, but always optional
  props?: GetProps<T>
  // same emit typing, just simpler
  emit?: GetEmitArgs<T>
}
```

This preserves:

- `model` strongly typed from `GetModel<T>`
- `props` strongly typed from `GetProps<T>`
- `emit` strongly typed from the derived emit map

while removing:

- `SetOptional`
- `CreateOptionalKey`
- the non-obvious conditional logic around “empty” objects vs `undefined`

The resulting `VueCompData<T>` shape is immediately clear from its definition, and the type error surface is much smaller and easier to reason about.
</issue_to_address>

### Comment 4
<location> `src/renderer/src/function/utils/appUtil.ts:1958` </location>
<code_context>
     }
 }}

+function createVLongHover(): Directive<HTMLElement, undefined> {
+    const {
+        handle: userHoverHandle,
</code_context>

<issue_to_address>
**issue (complexity):** Consider extracting the shared long-hover listener setup into a reusable helper so both vLongHover and vTooltip can use it without cross-calling each other or duplicating logic.

You can keep all current behavior but reduce complexity and coupling by extracting the long‑hover wiring into a shared helper, instead of having `vTooltip` call `vLongHover.mounted/unmounted` via `as any` and managing a second `AbortController`.

### 1. Factor out shared long‑hover setup

Move the event wiring that `createVLongHover` currently does into a reusable function:

```ts
function setupLongHover(
  el: HTMLElement,
  handlers: {
    onHover: (eventData: { x: number; y: number }) => void
    onEnd: () => void
  }
): AbortController {
  const { handle: userHoverHandle, handleEnd: userHoverEnd } = useStayEvent(
    (event: MouseEvent) => ({ x: event.clientX, y: event.clientY }),
    {
      onFit: (eventData, ctx: HTMLElement) => handlers.onHover(eventData),
      onLeave: () => handlers.onEnd(),
    },
    495,
  )

  const controller = new AbortController()
  const options = { signal: controller.signal }

  el.addEventListener(
    'mouseenter',
    (event) => {
      userHoverHandle(event, el)
    },
    options,
  )
  el.addEventListener(
    'mousemove',
    (event) => {
      userHoverHandle(event, el)
    },
    options,
  )
  el.addEventListener(
    'mouseleave',
    (event) => {
      userHoverEnd(event)
    },
    options,
  )

  return controller
}
```

Then `vLongHover` becomes just a thin wrapper that dispatches events, with no duplication:

```ts
function createVLongHover(): Directive<HTMLElement, undefined> {
  return {
    mounted(el) {
      const controller = setupLongHover(el, {
        onHover(eventData) {
          el.dispatchEvent(new CustomEvent('v-long-hover', { detail: eventData }))
        },
        onEnd() {
          el.dispatchEvent(new CustomEvent('v-long-hover-end'))
        },
      })
      ;(el as any)._vLongHoverController = controller
    },
    unmounted(el) {
      const controller = (el as any)._vLongHoverController
      if (!controller) return
      controller.abort()
      delete (el as any)._vLongHoverController
    },
  }
}
```

And `vTooltip` can use the same helper directly, without cross‑directive calls or a second controller:

```ts
export const vTooltip = {
  mounted<T extends Component>(
    el: HTMLElement,
    binding: DirectiveBinding<VTooltipBinding<T>> & { modifiers: { debug?: boolean } },
  ) {
    let tooltip: TooltipController | undefined

    const controller = setupLongHover(el, {
      onHover(detail) {
        const compData = resolveBinding(binding.value, detail)
        tooltip = addTooltip(compData, { x: detail.x, y: detail.y })
      },
      onEnd() {
        if (binding.modifiers?.debug) return
        tooltip?.close()
        tooltip = undefined
      },
    })

    ;(el as any)._vTooltipController = controller
  },

  unmounted(el: HTMLElement) {
    const controller = (el as any)._vTooltipController
    if (!controller) return
    controller.abort()
    delete (el as any)._vTooltipController
  },
}
```

This keeps:

- The same external `v-long-hover` events.
- All tooltip behavior, including `debug` modifier.
- Support for all existing binding shapes via your current `resolveBinding`.

But it:

- Removes the `as any` cross‑directive calls.
- Centralizes the stay/hover logic in one place.
- Avoids layering two separate `AbortController`‑backed listener sets on the same element.

Sourcery 对开源项目是免费的——如果你觉得我们的代码审查有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据这些反馈改进后续的代码审查。
Original comment in English

Hey - I've found 4 issues, and left some high level feedback:

  • In resolveBinding you sometimes construct { component: result } / { component: binding, props: {} } but elsewhere (and in Tooltip.vue) you expect compData.comp, so these branches should set comp instead of component to avoid undefined being passed to <component :is="compData.comp"> at runtime.
  • UserInfoTooltip.vue resolves userProp into userInfo (and titleClass) once at setup time, so if the user prop changes later the tooltip won't update; consider using a computed based on userProp (and deriving titleClass from it) so content stays in sync with reactive changes.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `resolveBinding` you sometimes construct `{ component: result }` / `{ component: binding, props: {} }` but elsewhere (and in `Tooltip.vue`) you expect `compData.comp`, so these branches should set `comp` instead of `component` to avoid `undefined` being passed to `<component :is="compData.comp">` at runtime.
- `UserInfoTooltip.vue` resolves `userProp` into `userInfo` (and `titleClass`) once at setup time, so if the `user` prop changes later the tooltip won't update; consider using a `computed` based on `userProp` (and deriving `titleClass` from it) so content stays in sync with reactive changes.

## Individual Comments

### Comment 1
<location> `src/renderer/src/function/utils/appUtil.ts:2035-2044` </location>
<code_context>
+    | (() => T | VueCompData<T> )
+    | ((eventData: {x: number, y: number}) => T | VueCompData<T> )
+
+function resolveBinding<T extends Component>(binding: VTooltipBinding<T>, eventData: {x: number, y: number}): VueCompData<T> {
+    if (typeof binding === 'function') {
+        // eslint-disable-next-line multiline-ternary
+        const result = binding.length === 0
+            // eslint-disable-next-line multiline-ternary
+            ? (binding as () => T | VueCompData<T>)()
+            : (binding as (eventData: {x: number, y: number}) => T | VueCompData<T>)(eventData)
+        if ('comp' in result) return result
+        return { component: result } as unknown as VueCompData<T>
+    } else if ('comp' in binding) {
+        return binding
+    } else {
+        return { component: binding, props: {} } as unknown as VueCompData<T>
+    }
+}
</code_context>

<issue_to_address>
**issue (bug_risk):** Returned tooltip data uses `component` key but the rest of the code expects `comp`, which will break tooltip rendering.

In `resolveBinding`, the returned objects use `{ component: ... }`, but `VueCompData<T>` and the tooltip pipeline (e.g. `Tooltip.vue` reading `compData.comp`) expect `comp`. This means `compData.comp` will be `undefined` for the function and non-`comp` branches, breaking rendering at runtime. Please update both `component` occurrences to `comp`, e.g.:

```ts
if ('comp' in result) return result
return { comp: result } as unknown as VueCompData<T>
...
} else {
  return { comp: binding, props: {} } as unknown as VueCompData<T>
}
```
</issue_to_address>

### Comment 2
<location> `src/renderer/src/components/tooltip/UserInfoTooltip.vue:1-7` </location>
<code_context>
-\<!--
- * @FileDescription: 群成员消息悬浮窗
- * @Author: Mr.Lee
</code_context>

<issue_to_address>
**issue (bug_risk):** Leading backslash before the HTML comment will render a stray `\` in the DOM and is unnecessary.

Starting the template with `\<!--` makes the backslash render as a literal character before the HTML comment, which can introduce an unwanted `\` node and confuse tooling. This should just be a standard HTML comment:

```vue
<!--
 * @FileDescription: 群成员消息悬浮窗
 ...
-->
```
</issue_to_address>

### Comment 3
<location> `src/renderer/src/function/elements/vueComp.ts:43` </location>
<code_context>
+    [K in keyof E]?: E[K]
+} : never
+
+type SetOptional<O extends Record<string, unknown>, T> = {
+    [K in keyof O as T extends O[K] ? K : never]?: O[K]
+} & {
</code_context>

<issue_to_address>
**issue (complexity):** Consider simplifying the VueCompData type by removing the SetOptional/CreateOptionalKey helpers and using simpler optional properties and Partial-typed emits instead.

You can drop a good chunk of the type-level machinery without losing any runtime or useful typing functionality.

Specifically, `SetOptional` and `CreateOptionalKey` only control whether `model`/`props` appear as required vs optional based on emptiness. That adds a lot of indirection for a small ergonomic gain. You can also simplify `GetEmitArgs` to just use `Partial`.

Concrete changes:

```ts
// Remove these helpers entirely
// type SetOptional<...> = ...
// type CreateOptionalKey<...> = ...

type GetEmitArgs<T extends Component> = Partial<GetEmit<T>>

export type VueCompData<T extends Component> = {
  comp: T
  // keeps model typed, but always optional at the top level
  model?: GetModel<T>
  // keeps props typed, but always optional
  props?: GetProps<T>
  // same emit typing, just simpler
  emit?: GetEmitArgs<T>
}
```

This preserves:

- `model` strongly typed from `GetModel<T>`
- `props` strongly typed from `GetProps<T>`
- `emit` strongly typed from the derived emit map

while removing:

- `SetOptional`
- `CreateOptionalKey`
- the non-obvious conditional logic around “empty” objects vs `undefined`

The resulting `VueCompData<T>` shape is immediately clear from its definition, and the type error surface is much smaller and easier to reason about.
</issue_to_address>

### Comment 4
<location> `src/renderer/src/function/utils/appUtil.ts:1958` </location>
<code_context>
     }
 }}

+function createVLongHover(): Directive<HTMLElement, undefined> {
+    const {
+        handle: userHoverHandle,
</code_context>

<issue_to_address>
**issue (complexity):** Consider extracting the shared long-hover listener setup into a reusable helper so both vLongHover and vTooltip can use it without cross-calling each other or duplicating logic.

You can keep all current behavior but reduce complexity and coupling by extracting the long‑hover wiring into a shared helper, instead of having `vTooltip` call `vLongHover.mounted/unmounted` via `as any` and managing a second `AbortController`.

### 1. Factor out shared long‑hover setup

Move the event wiring that `createVLongHover` currently does into a reusable function:

```ts
function setupLongHover(
  el: HTMLElement,
  handlers: {
    onHover: (eventData: { x: number; y: number }) => void
    onEnd: () => void
  }
): AbortController {
  const { handle: userHoverHandle, handleEnd: userHoverEnd } = useStayEvent(
    (event: MouseEvent) => ({ x: event.clientX, y: event.clientY }),
    {
      onFit: (eventData, ctx: HTMLElement) => handlers.onHover(eventData),
      onLeave: () => handlers.onEnd(),
    },
    495,
  )

  const controller = new AbortController()
  const options = { signal: controller.signal }

  el.addEventListener(
    'mouseenter',
    (event) => {
      userHoverHandle(event, el)
    },
    options,
  )
  el.addEventListener(
    'mousemove',
    (event) => {
      userHoverHandle(event, el)
    },
    options,
  )
  el.addEventListener(
    'mouseleave',
    (event) => {
      userHoverEnd(event)
    },
    options,
  )

  return controller
}
```

Then `vLongHover` becomes just a thin wrapper that dispatches events, with no duplication:

```ts
function createVLongHover(): Directive<HTMLElement, undefined> {
  return {
    mounted(el) {
      const controller = setupLongHover(el, {
        onHover(eventData) {
          el.dispatchEvent(new CustomEvent('v-long-hover', { detail: eventData }))
        },
        onEnd() {
          el.dispatchEvent(new CustomEvent('v-long-hover-end'))
        },
      })
      ;(el as any)._vLongHoverController = controller
    },
    unmounted(el) {
      const controller = (el as any)._vLongHoverController
      if (!controller) return
      controller.abort()
      delete (el as any)._vLongHoverController
    },
  }
}
```

And `vTooltip` can use the same helper directly, without cross‑directive calls or a second controller:

```ts
export const vTooltip = {
  mounted<T extends Component>(
    el: HTMLElement,
    binding: DirectiveBinding<VTooltipBinding<T>> & { modifiers: { debug?: boolean } },
  ) {
    let tooltip: TooltipController | undefined

    const controller = setupLongHover(el, {
      onHover(detail) {
        const compData = resolveBinding(binding.value, detail)
        tooltip = addTooltip(compData, { x: detail.x, y: detail.y })
      },
      onEnd() {
        if (binding.modifiers?.debug) return
        tooltip?.close()
        tooltip = undefined
      },
    })

    ;(el as any)._vTooltipController = controller
  },

  unmounted(el: HTMLElement) {
    const controller = (el as any)._vTooltipController
    if (!controller) return
    controller.abort()
    delete (el as any)._vTooltipController
  },
}
```

This keeps:

- The same external `v-long-hover` events.
- All tooltip behavior, including `debug` modifier.
- Support for all existing binding shapes via your current `resolveBinding`.

But it:

- Removes the `as any` cross‑directive calls.
- Centralizes the stay/hover logic in one place.
- Avoids layering two separate `AbortController`‑backed listener sets on the same element.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@Chzxxuanzheng
Copy link
Contributor Author

对了,记住密码好像也坏了...太晚了,没排查那里有问题...

@sonarqubecloud
Copy link

sonarqubecloud bot commented Jan 2, 2026

@Stapxs Stapxs mentioned this pull request Jan 19, 2026
@Stapxs Stapxs merged commit e458b54 into Stapxs:dev Jan 19, 2026
3 checks passed
@Chzxxuanzheng Chzxxuanzheng deleted the more-tooltip branch January 19, 2026 09:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants