feat(task-buttons): 外链按钮新增「打开方式」(新窗口/当前窗口)

按钮管理(/page/strategy/admin/task-buttons)的 url 外链按钮可配置打开方式:
- blank=新窗口(默认,window.open(_blank),行为同改前)
- self=当前窗口(window.location.assign,嵌入态即当前 iframe)

新增 TaskButton.openMode 字段贯通前后端:前端 task-buttons.ts 类型+种子+
runTaskButton 打开逻辑、api 客户端 snake⇄camel 映射、TaskButtonsPage 下拉;
后端 router schema + ORM open_mode 列(可空默认 blank,启动自动 ALTER 补列,
无需手动迁移)+ sql/memory store 归一化。仅 url 类型生效。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
fengxiangdebaoziandClaude Opus 4.8 committed 2026-06-19 16:09:01 +08:00
1 parent e906a911b2
commit a9f01bec79
7 files changed
+41 -1

No files matched your search

+16 -1
View File
@@ -59,6 +59,9 @@ export type TaskButtonLinkType = "url" | "business" | "purpose";
/** 追加的 id 取哪种:``task``=任务id(默认)/ ``action``=行动id(仅 xdfx 二者不同)。 */
export type TaskButtonIdKind = "task" | "action";
/** ``url`` 外链打开方式:``blank``=新窗口(默认)/ ``self``=当前窗口(嵌入态即当前 iframe)。 */
export type TaskButtonOpenMode = "blank" | "self";
/** 一个可配置跳转按钮。 */
export interface TaskButton {
id: string;
@@ -76,6 +79,8 @@ export interface TaskButton {
* 实为行动id,任务id 由 action 接口解析。其它业务两者相同。
*/
idKind: TaskButtonIdKind;
/** ``url`` 外链打开方式:``blank``=新窗口(默认)/ ``self``=当前窗口。仅 ``url`` 类型有意义。 */
openMode: TaskButtonOpenMode;
enabled: boolean;
sortOrder: number;
}
@@ -109,6 +114,7 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] {
appendTaskId: false,
idParam: "id",
idKind: "task",
openMode: "blank",
enabled: true,
sortOrder,
});
@@ -122,6 +128,7 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] {
appendTaskId: false,
idParam: "id",
idKind: "task",
openMode: "blank",
enabled: true,
sortOrder,
});
@@ -136,6 +143,7 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] {
appendTaskId: true,
idParam: "id",
idKind: "task",
openMode: "blank",
enabled: true,
sortOrder: 0,
},
@@ -148,6 +156,7 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] {
appendTaskId: true,
idParam: "id",
idKind: "task",
openMode: "blank",
enabled: true,
sortOrder: 1,
},
@@ -160,6 +169,7 @@ export function defaultTaskButtons(cfg: TaskDeeplinkConfig): TaskButton[] {
appendTaskId: true,
idParam: "id",
idKind: "task",
openMode: "blank",
enabled: true,
sortOrder: 2,
},
@@ -227,7 +237,12 @@ export async function runTaskButton(btn: TaskButton, ctx: RunTaskButtonCtx): Pro
const id = btn.idKind === "action" ? ctx.actionId || ctx.taskId : ctx.taskId;
const param = (btn.idParam || "id").trim() || "id";
const url = btn.appendTaskId && id ? appendQueryParam(base, param, id) : base;
window.open(url, "_blank", "noopener,noreferrer");
// 打开方式:self=当前窗口(嵌入态即当前 iframe);否则 blank=新窗口(默认)。
if (btn.openMode === "self") {
window.location.assign(url);
} else {
window.open(url, "_blank", "noopener,noreferrer");
}
return;
}
// business:内部切换到另一业务
@@ -104,6 +104,7 @@ export default function TaskButtonsPage() {
appendTaskId: true,
idParam: "id",
idKind: "task",
openMode: "blank",
enabled: true,
sortOrder: maxSort + 1,
},
@@ -305,6 +306,19 @@ export default function TaskButtonsPage() {
)}
</div>
)}
<label className="text-muted-foreground ml-2 shrink-0 text-xs">打开方式</label>
<Select
value={r.openMode}
onValueChange={(v) => update(r.id, { openMode: v as TaskButton["openMode"] })}
>
<SelectTrigger className="h-8 w-32" title="外链打开方式">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="blank">新窗口</SelectItem>
<SelectItem value="self">当前窗口</SelectItem>
</SelectContent>
</Select>
</>
) : (
<Select value={r.target} onValueChange={(v) => update(r.id, { target: v })}>
@@ -15,6 +15,7 @@ import type {
TaskButton,
TaskButtonIdKind,
TaskButtonLinkType,
TaskButtonOpenMode,
} from "@/core/auth/task-buttons";
const BASE = () => `${getBackendBaseURL()}/api/task-buttons`;
@@ -28,6 +29,7 @@ interface ServerButton {
append_task_id: boolean;
id_param: string;
id_kind: string;
open_mode: string;
enabled: boolean;
sort_order: number;
}
@@ -42,6 +44,7 @@ function toButton(s: ServerButton): TaskButton {
appendTaskId: s.append_task_id ?? true,
idParam: s.id_param || "id",
idKind: (s.id_kind as TaskButtonIdKind) ?? "task",
openMode: (s.open_mode as TaskButtonOpenMode) ?? "blank",
enabled: s.enabled ?? true,
sortOrder: s.sort_order ?? 0,
};
@@ -57,6 +60,7 @@ function toBody(b: TaskButton): ServerButton {
append_task_id: b.appendTaskId,
id_param: b.idParam,
id_kind: b.idKind,
open_mode: b.openMode,
enabled: b.enabled,
sort_order: b.sortOrder,
};
@@ -46,6 +46,8 @@ class TaskButton(BaseModel):
id_param: str = Field(default="id", max_length=64)
# task=任务id(默认)/ action=行动id(仅 xdfx 二者不同)。
id_kind: str = Field(default="task", max_length=16)
# url 外链打开方式:blank=新窗口(默认)/ self=当前窗口。
open_mode: str = Field(default="blank", max_length=16)
enabled: bool = True
sort_order: int = 0
@@ -18,6 +18,7 @@ def _normalize(data: dict[str, Any], updated_by: str | None) -> dict[str, Any]:
"append_task_id": bool(data.get("append_task_id", True)),
"id_param": str(data.get("id_param") or "id"),
"id_kind": str(data.get("id_kind") or "task"),
"open_mode": str(data.get("open_mode") or "blank"),
"enabled": bool(data.get("enabled", True)),
"sort_order": int(data.get("sort_order", 0)),
"updated_by": updated_by,
@@ -46,6 +46,8 @@ class TaskButtonRow(Base):
# 追加的 id 取哪种:``task``=任务id(默认)/ ``action``=行动id(仅 xdfx 二者不同:深链原始
# ?taskId= 实为行动id,任务id 由 action 接口解析)。可空,读时归一化为 ``task``。
id_kind: Mapped[str | None] = mapped_column(String(16), default="task")
# ``url`` 外链打开方式:``blank``=新窗口(默认)/ ``self``=当前窗口。可空,读时归一化为 ``blank``。
open_mode: Mapped[str | None] = mapped_column(String(16), default="blank")
enabled: Mapped[bool] = mapped_column(Boolean, default=True)
sort_order: Mapped[int] = mapped_column(Integer, default=0)
updated_by: Mapped[str | None] = mapped_column(String(64))
@@ -20,6 +20,7 @@ def _to_dict(row: TaskButtonRow) -> dict[str, Any]:
# 旧行 / NULL 归一化为默认值。
data["id_param"] = data.get("id_param") or "id"
data["id_kind"] = data.get("id_kind") or "task"
data["open_mode"] = data.get("open_mode") or "blank"
return data
@@ -33,6 +34,7 @@ def _row_from(data: dict[str, Any], updated_by: str | None) -> TaskButtonRow:
append_task_id=bool(data.get("append_task_id", True)),
id_param=str(data.get("id_param") or "id"),
id_kind=str(data.get("id_kind") or "task"),
open_mode=str(data.get("open_mode") or "blank"),
enabled=bool(data.get("enabled", True)),
sort_order=int(data.get("sort_order", 0)),
updated_by=updated_by,