From b54ed93b20d2d1805d6b4375d449a9ab300d5382 Mon Sep 17 00:00:00 2001 From: AC-0308 Date: Tue, 2 Dec 2025 19:40:02 +0800 Subject: [PATCH 1/2] =?UTF-8?q?refactor(OAnchor):=20=F0=9F=92=A5=20?= =?UTF-8?q?=E3=80=90Breaking=20Change=E3=80=91=E9=87=8D=E6=9E=84OAnchor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 增加尺寸区别: small、medium、menu - 增加一级锚点圆圈指示器 - 增加行溢出隐藏加气泡提示 - 增加外部链跳转逻辑(除_self都认为是外链) - 增加item的disabled属性 - 去除hover与active的背托、增加hover与active的字体颜色 - 修改粗体字号为600 --- packages/opendesign/src/_utils/is.ts | 23 ++++ packages/opendesign/src/anchor/OAnchor.vue | 40 +++--- .../opendesign/src/anchor/OAnchorItem.vue | 98 +++++++++++--- .../src/anchor/__demo__/AnchorBasic.vue | 46 +++++-- packages/opendesign/src/anchor/provide.ts | 7 +- .../opendesign/src/anchor/style/media.scss | 24 +++- .../opendesign/src/anchor/style/style.scss | 122 +++++++++++++++--- packages/opendesign/src/anchor/style/var.scss | 56 +++++++- packages/opendesign/src/anchor/types.ts | 40 ++++-- 9 files changed, 374 insertions(+), 82 deletions(-) diff --git a/packages/opendesign/src/_utils/is.ts b/packages/opendesign/src/_utils/is.ts index 6153085f1..5c27ca586 100644 --- a/packages/opendesign/src/_utils/is.ts +++ b/packages/opendesign/src/_utils/is.ts @@ -84,3 +84,26 @@ export const isHoverDevice = isClient ? window.matchMedia('(hover: hover)').matc export function isWindow(val: unknown): val is Window { return val === window; } + +/** + * 判断一个给定的路径是否是当前页面的内部链接 + * @param {string} link - 需要判断的路径(可以是相对路径、绝对路径或完整URL) + * @returns {boolean} - 如果是外部链接返回true,否则返回false + */ +export function isCurrentPageLink(link: string): boolean { + // 处理锚点或空路径,通常视为内部链接 + if (link.startsWith('#')) { + return true; + } + + try { + // 使用URL对象解析目标路径(以当前页面为基准) + const targetUrl = new URL(link, window.location.href); + + // 比较协议、主机名、端口和路径 + return targetUrl.origin + targetUrl.pathname === window.location.origin + window.location.pathname; + } catch { + // 如果URL解析失败(如不规范的路径),判定为外部链接 + return false; + } +} diff --git a/packages/opendesign/src/anchor/OAnchor.vue b/packages/opendesign/src/anchor/OAnchor.vue index deb53ecf3..6f385ffb4 100644 --- a/packages/opendesign/src/anchor/OAnchor.vue +++ b/packages/opendesign/src/anchor/OAnchor.vue @@ -1,13 +1,16 @@ @@ -51,5 +80,6 @@ import { OAnchor, OAnchorItem } from '../index'; position: fixed; top: 200px; right: 60px; + display: flex; } diff --git a/packages/opendesign/src/anchor/provide.ts b/packages/opendesign/src/anchor/provide.ts index 10a0eccb0..ad455de9a 100644 --- a/packages/opendesign/src/anchor/provide.ts +++ b/packages/opendesign/src/anchor/provide.ts @@ -3,8 +3,13 @@ import { InjectionKey, Ref } from 'vue'; export const anchorInjectKey: InjectionKey<{ addLink: (link: string) => void; removeLink: (link: string) => void; - handleClick: (ev: MouseEvent, link?: string) => void; + /** + * @deprecated 兼容旧版本,计划1.2.0移除 + */ + onItemClick: (options: { event: MouseEvent; link?: string }) => void; activeLink: Ref; + scrollIntoView: (link: string) => void; + getChangeHash: () => boolean; }> = Symbol('provide-anchor'); export const anchorItemInjectKey: InjectionKey<{ diff --git a/packages/opendesign/src/anchor/style/media.scss b/packages/opendesign/src/anchor/style/media.scss index 1f1e6278f..d2af97697 100644 --- a/packages/opendesign/src/anchor/style/media.scss +++ b/packages/opendesign/src/anchor/style/media.scss @@ -1,9 +1,25 @@ @use '../../_styles/mixin.scss' as *; @include respond-to('<=pc_s') { - .o-anchor-item { - --anchor-item-link-text-size: var(--o-font_size-tip1); - --anchor-item-link-text-height: var(--o-line_height-tip1); - --anchor-item-text-indent: 8px; + .o-anchor:not(.o-anchor-menu) { + .o-anchor-item { + --anchor-item-link-text-size: var(--o-font_size-tip1); + --anchor-item-link-text-height: var(--o-line_height-tip1); + + --anchor-item-sub-link-text-size: var(--o-font_size-tip2); + --anchor-item-sub-link-text-height: var(--o-line_height-tip2); + + --anchor-item-link-padding-v: 5px; + --anchor-item-sub-link-padding-v: 5px; + } } } + +@include respond-to('<=pad_v') { + .o-anchor-menu { + .o-anchor-item { + --anchor-item-sub-link-text-size: var(--o-font_size-tip2); + --anchor-item-sub-link-text-height: var(--o-line_height-tip2); + } + } +} \ No newline at end of file diff --git a/packages/opendesign/src/anchor/style/style.scss b/packages/opendesign/src/anchor/style/style.scss index 5aaf2b3b5..3b61f21e7 100644 --- a/packages/opendesign/src/anchor/style/style.scss +++ b/packages/opendesign/src/anchor/style/style.scss @@ -11,8 +11,8 @@ position: relative; width: var(--anchor-line-width); border-radius: var(--anchor-line-width); - background-color: var(--anchor-line-bg-color); - margin-right: 4px; + margin-right: var(--anchor-line-gap); + margin-left: calc(var(--anchor-circle-size) / 2); } .o-anchor-indicator { @@ -22,7 +22,7 @@ width: var(--anchor-indicator-width); background-color: transparent; opacity: 0; - transition: all var(--o-easing-standard-in) var(--o-duration-m1); + transition: top var(--o-easing-standard-in) var(--o-duration-m1); &::after { content: ''; @@ -39,41 +39,121 @@ } .o-anchor-item-link { + --anchor-item-link-left-extra-indent: calc(var(--anchor-item-text-indent) * var(--anchor-item-depth)); + --anchor-circle-top: calc( + var(--anchor-item-link-padding-v) + var(--anchor-item-link-text-height) / 2 - var(--anchor-circle-size) / 2 + var(--anchor-indicator-adjust) + ); cursor: pointer; display: inline-flex; - color: inherit; text-decoration: none; width: 100%; - color: var(--anchor-item-link-color); + color: var(--anchor-item-link-color, inherit); font-size: var(--anchor-item-link-text-size); line-height: var(--anchor-item-link-text-height); - background-color: var(--anchor-item-link-bg-color); - padding: - var(--anchor-item-link-padding-v) - var(--anchor-item-link-padding-h) - var(--anchor-item-link-padding-v) - calc(var(--anchor-item-text-indent) * var(--anchor-item-depth) + var(--anchor-item-link-padding-h)); + padding: var(--anchor-item-link-padding-v) var(--anchor-item-link-padding-h); + padding-left: calc(var(--anchor-item-link-left-extra-indent) + var(--anchor-item-link-padding-h)); border-radius: var(--anchor-item-link-radius); transition: background-color var(--o-duration-s) var(--o-easing-standard); - word-break: break-word; + word-break: break-word; + position: relative; @include hover { - background-color: var(--anchor-item-link-bg-color-hover); + color: var(--anchor-item-link-color-hover); + } + + &:active { + color: var(--anchor-item-link-color-press); } - &.is-active { - font-weight: 500; + &.disabled { + color: var(--anchor-item-link-color-disabled); + @include hover { + cursor: not-allowed; + } + } +} + +.o-anchor-item:has(.o-anchor-item-link.is-active) > .o-anchor-item-link, +.o-anchor-item-link.is-active { + &:not(.disabled) { + font-weight: 600; color: var(--anchor-item-link-color-active); - background-color: var(--anchor-item-link-bg-color-active); } } -.o-anchor-item + .o-anchor-item { - margin-top: var(--anchor-item-link-gap); +.o-anchor-item-title { + max-height: calc(var(--anchor-item-max-row) * var(--anchor-item-link-text-height)); + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: var(--anchor-item-max-row); } -.o-anchor-item { - .o-anchor-item { - margin-top: var(--anchor-item-link-gap); +.o-anchor-item-sub-link { + font-size: var(--anchor-item-sub-link-text-size); + line-height: var(--anchor-item-sub-link-text-height); + padding-top: var(--anchor-item-sub-link-padding-v); + padding-bottom: var(--anchor-item-sub-link-padding-v); +} + +.o-anchor-item-lines { + width: var(--anchor-line-width); + position: absolute; + top: 0; + bottom: 0; + left: calc(0px - var(--anchor-line-gap) - var(--anchor-line-width)); +} + +.o-anchor-item-top-line, +.o-anchor-item-bottom-line { + background-color: var(--anchor-line-bg-color); + position: absolute; + left: 0; + right: 0; +} + +.o-anchor-item-top-line { + top: 0; + bottom: 50%; +} + +.o-anchor-item-bottom-line { + top: 50%; + bottom: 0; +} + +.o-anchor-item-link:not(.o-anchor-item-sub-link) { + > .o-anchor-item-lines { + .o-anchor-item-circle { + position: absolute; + height: var(--anchor-circle-size); + width: var(--anchor-circle-size); + top: var(--anchor-circle-top); + left: 50%; + transform: translateX(-50%); + border: var(--anchor-circle-ring-width) solid currentColor; + border-radius: 50%; + } + .o-anchor-item-top-line { + bottom: unset; + height: calc(var(--anchor-circle-top) - var(--anchor-circle-gap)); + } + .o-anchor-item-bottom-line { + top: calc(var(--anchor-circle-top) + var(--anchor-circle-size) + var(--anchor-circle-gap)); + } + } +} + +.o-anchor-items > .o-anchor-item { + &:first-child > .o-anchor-item-link .o-anchor-item-top-line { + display: none; } + &:last-child:not(.with-children) > .o-anchor-item-link .o-anchor-item-bottom-line { + display: none; + } +} + +.o-anchor-link-popover-wrapper { + max-width: var(--anchor-popover-max-width); } diff --git a/packages/opendesign/src/anchor/style/var.scss b/packages/opendesign/src/anchor/style/var.scss index 4c9268cd8..1b66aaec7 100644 --- a/packages/opendesign/src/anchor/style/var.scss +++ b/packages/opendesign/src/anchor/style/var.scss @@ -1,10 +1,16 @@ .o-anchor { - --anchor-line-width: 2px; + --anchor-line-width: 1px; --anchor-line-bg-color: var(--o-color-control4); + --anchor-line-gap: var(--o-gap-2); --anchor-indicator-width: 2px; - --anchor-indicator-height: 16px; + --anchor-indicator-height: 12px; --anchor-indicator-bg-color: var(--o-color-primary1); + --anchor-indicator-adjust: 0.12em; + + --anchor-circle-size: 8px; + --anchor-circle-ring-width: 1.5px; + --anchor-circle-gap: 8px; } .o-anchor-item { @@ -12,18 +18,54 @@ --anchor-item-link-color: var(--o-color-info2); --anchor-item-link-color-active: var(--o-color-primary1); + --anchor-item-link-color-hover: var(--o-color-primary2); + --anchor-item-link-color-press: var(--o-color-primary3); + --anchor-item-link-color-disabled: var(--o-color-info4); + + --anchor-item-max-row: 2; --anchor-item-link-text-size: var(--o-font_size-text1); --anchor-item-link-text-height: var(--o-line_height-text1); - --anchor-item-link-bg-color: transparent; - --anchor-item-link-bg-color-hover: var(--o-color-control2-light); - --anchor-item-link-bg-color-active: var(--o-color-control3-light); + --anchor-item-sub-link-text-size: var(--o-font_size-tip1); + --anchor-item-sub-link-text-height: var(--o-line_height-tip1); --anchor-item-link-padding-h: 8px; - --anchor-item-link-padding-v: 7px; + --anchor-item-link-padding-v: 8px; + --anchor-item-sub-link-padding-v: 5px; --anchor-item-link-radius: var(--o-radius_control-s); - --anchor-item-link-gap: 2px; --anchor-item-text-indent: 12px; } + +.o-anchor-small { + .o-anchor-item { + --anchor-item-link-text-size: var(--o-font_size-tip1); + --anchor-item-link-text-height: var(--o-line_height-tip1); + + --anchor-item-sub-link-text-size: var(--o-font_size-tip2); + --anchor-item-sub-link-text-height: var(--o-line_height-tip2); + + --anchor-item-link-padding-v: 5px; + --anchor-item-sub-link-padding-v: 5px; + } +} + +.o-anchor-menu { + .o-anchor-item { + --anchor-item-link-text-size: var(--o-font_size-tip1); + --anchor-item-link-text-height: var(--o-line_height-tip1); + + --anchor-item-sub-link-text-size: var(--o-font_size-tip1); + --anchor-item-sub-link-text-height: var(--o-line_height-tip1); + + --anchor-item-link-padding-v: 5px; + --anchor-item-sub-link-padding-v: 5px; + + --anchor-item-text-indent: 20px; + } +} + +.o-anchor-link-popover-wrapper { + --anchor-popover-max-width: 200px; +} diff --git a/packages/opendesign/src/anchor/types.ts b/packages/opendesign/src/anchor/types.ts index 063c1ce0c..862fb67f3 100644 --- a/packages/opendesign/src/anchor/types.ts +++ b/packages/opendesign/src/anchor/types.ts @@ -1,7 +1,20 @@ import type { ExtractPropTypes, PropType } from 'vue'; +export const AnchorSizeTypes = ['medium', 'small', 'menu'] as const; +export type AnchorSizeT = (typeof AnchorSizeTypes)[number]; +export type AnchorTargetT = '_blank' | '_parent' | '_self' | '_top'; + export const anchorProps = { - /** + /** + * @zh-CN 锚点的尺寸, menu为左侧菜单或移动端菜单混合使用 + * @en-US Anchor size, 'menu' for aside menu-mixed or mobile menu mode + * @default 'medium' + */ + size: { + type: String as PropType, + default: 'medium', + }, + /** * @zh-CN 监测容器 * @en-US Scroll container to monitor * @default window @@ -9,7 +22,7 @@ export const anchorProps = { container: { type: [String, Object] as PropType, }, - /** + /** * @zh-CN 锚点激活的边界范围 * @en-US Boundary for anchor activation * @default 5 @@ -18,7 +31,7 @@ export const anchorProps = { type: Number, default: 5, }, - /** + /** * @zh-CN 锚点激活的判定边界 * @en-US Boundary for anchor activation * @default 0 @@ -31,7 +44,7 @@ export const anchorProps = { * @zh-CN 点击锚点时是否改变浏览器地址栏的 hash 值 * @en-US Whether to change the browser's address bar hash value when clicking the anchor * @default true - */ + */ changeHash: { type: Boolean, default: true, @@ -42,15 +55,15 @@ export const anchorItemProps = { /** * @zh-CN 锚点标题 * @en-US Anchor title - */ + */ title: { type: String, default: '', }, /** - * @zh-CN 锚点跳转的目标元素(带#前缀) + * @zh-CN 锚点监听、跳转的目标元素(带#前缀) * @en-US Target element for anchor navigation (with # prefix) - */ + */ href: { type: String, required: true, @@ -59,11 +72,20 @@ export const anchorItemProps = { * @zh-CN 锚点跳转方式 * @en-US Anchor navigation method * @default '_self' - */ + */ target: { - type: String as PropType<'_blank' | '_parent' | '_self' | '_top'>, + type: String as PropType, default: '_self', }, + /** + * @zh-CN 锚点是否禁用 + * @en-US Anchor disable status + * @default false + */ + disabled: { + type: Boolean as PropType, + default: false, + }, }; export type AnchorContainerT = HTMLElement | Window; -- Gitee From 349a2e47fa73c2641b48e9f2b233fe30f6d60f99 Mon Sep 17 00:00:00 2001 From: AC-0308 Date: Mon, 8 Dec 2025 10:25:12 +0800 Subject: [PATCH 2/2] =?UTF-8?q?doc(OAnchor):=20=F0=9F=93=9D=20=E4=B8=B0?= =?UTF-8?q?=E5=AF=8Canchor=E7=9A=84=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../anchor/__docs__/__case__/AnchorUsage.vue | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/packages/opendesign/src/anchor/__docs__/__case__/AnchorUsage.vue b/packages/opendesign/src/anchor/__docs__/__case__/AnchorUsage.vue index 8f395ea73..ac7addf22 100644 --- a/packages/opendesign/src/anchor/__docs__/__case__/AnchorUsage.vue +++ b/packages/opendesign/src/anchor/__docs__/__case__/AnchorUsage.vue @@ -3,6 +3,10 @@ ### 使用 +`size` 属性 + +- 说明:指定锚点的尺寸风格(默认值:medium) + `container` 属性 - 说明:指定锚点监听的滚动容器(默认值:window) @@ -32,6 +36,10 @@ ### Usage +`size` property + +- Description:The size of anchor (default: medium) + `container` property - Description: Specifies the scroll container to be monitored by the anchor (default: window) @@ -59,7 +67,11 @@ Nested anchors: `OAnchorItem` can be nested to create multi-level anchor structu