fix(粘贴): URL 提取按括号配对裁剪,修复高德链接被截断导致地址丢失

## 现象(用户实测)
粘贴高德分享链接后,识别出的地点"只有一半,只有一部分"。

## 根因
高德分享链接里的 POI 名称含**未编码的半角括号**:
    ...%E9%9D%92%E5%B2%9B%E8%8F%9C(黄岛珠江路店)%2C武夷山路118号...
                              ↑ 就是这个 ( 和 )

而 URL 提取正则是 `/https?:\/\/[^\s"'<>()【】]+/i`(当时还排除了半角 `)`),
于是一到第一个 `)` 就停:

    提取长度 221 / 原文 456      ← 只拿到不到一半
    __p 切成 4 段(应为 5 段)
    名称 = "船歌·鱼水饺青岛菜(黄岛珠江路店"   ← 括号不闭合
    地址 = 空                                ← 第 5 段整个丢失

## 为什么当初排除括号
本意是防止把 URL 后跟着的中文括注一起吞掉,如
`看这个 https://x.com/a(路线图在这里)`。
但规则太粗暴,误伤了 URL 内部的合法括号 —— RFC 3986 允许 `(` `)` 出现在 query。

## 修法
1. 正则放宽:只排除空白 / 引号 / 尖括号 / **全角**括号(全角括号不会被编码进 URL,
   出现在明文里必然是用户自己加的中文括注)
2. 新增 `trimUrlTail()`:从左扫描数括号,`depth < 0` 说明遇到多余的右括号,
   那里才是真正的 URL 边界
3. 再裁掉末尾可能粘上的中文标点(。,、;!?)

## 验证(用用户提供的真实 URL 跑等价逻辑)

修复后:
    提取长度 780 / 原文 780     ← 完整
    __p 段数 5
      [0] B02140WITR
      [1] 35.95086086125046
      [2] 120.19152268767353
      [3] 船歌·鱼水饺青岛菜(黄岛珠江路店)          ← 括号完整
      [4] 武夷山路118号中南·海湾新城(珠江路金石国际广场旁) ← 地址回来了

边界场景:
    "https://x?a=1(路线图)"  →  https://x?a=1        ✅ 中文括注被裁掉
    "https://x?a=1) 后面有字"  →  https://x?a=1        ✅ 多余右括号被裁掉

同时确认另外两处 `split(',')` 都用了 `slice(2).join(',')` 拼回,
地址内部含逗号时不会被切错。

## 编译
`BUILD SUCCESSFUL`

## 未验证
真机端到端(写剪贴板 → 点粘贴 → 看结果)需人工操作:
hdc 没有可用的剪贴板写入工具(`paste` 是 Linux 文本合并命令,非剪贴板服务)。
解析逻辑本身已用真实 URL 在等价实现上验证通过。
This commit is contained in:
2026-11-04 07:36:51 +08:00
parent b660a986dc
commit c0b9b44ca2
2 changed files with 48 additions and 3 deletions
+43 -2
View File
@@ -63,13 +63,54 @@ export class PlaceParser {
// ==================== 链接解析 ====================
/**
* 裁剪 URL 尾部:只裁"多出来"的右括号与中文标点,保留 URL 内部的合法括号。
*
* 为什么需要:高德分享链接里的 POI 名称常含未编码的半角括号,
* 如 `...青岛菜(黄岛珠江路店)%2C武夷山路...`
* 若正则直接排除 ')',就会在第一个 ')' 处截断 → 后面的地址段全丢。
* 实测该链接被截到 221/456 字符,__p 从 5 段变成 4 段。
*
* 做法:从左往右数括号,depth 变负说明这个 ')' 没有对应 '(',
* 它不属于 URL → 从这里截断。
*/
private static trimUrlTail(raw: string): string {
let depth: number = 0;
let cut: number = raw.length;
for (let i = 0; i < raw.length; i++) {
const ch: string = raw.charAt(i);
if (ch === '(') {
depth++;
} else if (ch === ')') {
depth--;
if (depth < 0) {
// 多出来的右括号 → URL 到此为止
cut = i;
break;
}
}
}
let out: string = raw.substring(0, cut);
// 再裁掉可能粘在末尾的中文标点(用户手动加的全角句号/逗号等)
while (out.length > 0) {
const last: string = out.charAt(out.length - 1);
if (last === '。' || last === ',' || last === '、' || last === ';' ||
last === '!' || last === '?') {
out = out.substring(0, out.length - 1);
} else {
break;
}
}
return out;
}
private static async parseUrl(text: string): Promise<ParsedPlace | null> {
// 从可能夹带其他文字的段落里抽出第一个 http(s) 链接
const urlMatch: RegExpExecArray | null = /https?:\/\/[^\s"'<>))]+/i.exec(text);
const urlMatch: RegExpExecArray | null = /https?:\/\/[^\s"'<>()【】]+/i.exec(text);
if (urlMatch === null) {
return null;
}
const url: string = urlMatch[0];
const url: string = PlaceParser.trimUrlTail(urlMatch[0]);
// ① poi/detail: __p=<id>,<lat>,<lng>,<name>,<address...>
const p: ParsedPlace | null = PlaceParser.parseAmapP(url);
if (p !== null) {
+5 -1
View File
@@ -1754,7 +1754,11 @@ struct EventEditPage {
// 外观与右边的「地图」按钮保持一致:同字号、同色、同底色、同圆角、同高度。
PasteButton({
text: PasteDescription.PASTE,
buttonType: ButtonType.Capsule
// ⚠️ 必须用 Normal,不能用 Capsule:
// 按 SDK 文档,Capsule / Circle 下 borderRadius **不生效**,
// 圆角由按钮类型自动决定(= 两端全圆),与「地图」的圆角矩形不一致。
// 只有 Normal / ROUNDED_RECTANGLE 才认 borderRadius。
buttonType: ButtonType.Normal
})
.fontSize(13)
.fontColor($r('app.color.brand_text'))