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:
1 parent
e906a911b2
commit
a9f01bec79
7 files changed
+41
-1
No files matched your search
@@ -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
|
||||
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
+2
@@ -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))
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user