mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-05-04 21:20:51 +08:00
feat(channel-monitor): apply template via subset picker; CC 2.1.114 baseline doc
Apply flow: - POST /admin/channel-monitor-templates/:id/apply now requires monitor_ids (non-empty array). Service applies the template only to the selected subset, gated by AND template_id = :id (so users can't sneak in unrelated monitor IDs). - New GET /admin/channel-monitor-templates/:id/monitors returns the associated monitor briefs (id/name/provider/enabled) for the picker. - ApplyToMonitors signature gains monitorIDs []int64; empty list returns ErrChannelMonitorTemplateApplyEmpty. Frontend: - New MonitorTemplateApplyPickerDialog.vue: list of associated monitors with checkboxes (default all checked), 全选 / 全不选 shortcuts, live selected/total count. Submit calls apply(id, ids). - MonitorTemplateManagerDialog replaces the old ConfirmDialog flow with the picker; onApplied refetches the list to refresh associated counts. i18n: applyPicker* + common.selectAll keys. chore: bump version to 0.1.114.33 The CC 2.1.114 (sdk-cli) UA / APIKeyBetaHeader / JSON metadata.user_id baseline (already verified working via the in-process apply on prod template id=1) is documented in internal/pkg/claude/constants.go and is what the seed template in the manager UI should follow.
This commit is contained in:
@@ -179,17 +179,56 @@ func (h *ChannelMonitorRequestTemplateHandler) Delete(c *gin.Context) {
|
|||||||
response.Success(c, nil)
|
response.Success(c, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type channelMonitorTemplateApplyRequest struct {
|
||||||
|
// MonitorIDs 必填、非空:用户在 picker 里勾选的要被覆盖的监控 ID 列表。
|
||||||
|
// 仅当对应监控当前 template_id == :id 时才会真的被覆盖。
|
||||||
|
MonitorIDs []int64 `json:"monitor_ids" binding:"required,min=1"`
|
||||||
|
}
|
||||||
|
|
||||||
// Apply POST /api/v1/admin/channel-monitor-templates/:id/apply
|
// Apply POST /api/v1/admin/channel-monitor-templates/:id/apply
|
||||||
// 一键把模板当前配置覆盖到所有关联监控上。
|
// 把模板当前配置覆盖到 monitor_ids 列表里的关联监控(picker 选中的子集)。
|
||||||
func (h *ChannelMonitorRequestTemplateHandler) Apply(c *gin.Context) {
|
func (h *ChannelMonitorRequestTemplateHandler) Apply(c *gin.Context) {
|
||||||
id, ok := parseTemplateID(c)
|
id, ok := parseTemplateID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
affected, err := h.templateService.ApplyToMonitors(c.Request.Context(), id)
|
var req channelMonitorTemplateApplyRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
response.ErrorFrom(c, infraerrors.BadRequest("VALIDATION_ERROR", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
affected, err := h.templateService.ApplyToMonitors(c.Request.Context(), id, req.MonitorIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.ErrorFrom(c, err)
|
response.ErrorFrom(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
response.Success(c, gin.H{"affected": affected})
|
response.Success(c, gin.H{"affected": affected})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type associatedMonitorBriefResponse struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssociatedMonitors GET /api/v1/admin/channel-monitor-templates/:id/monitors
|
||||||
|
// 列出关联监控(picker 弹窗用)。
|
||||||
|
func (h *ChannelMonitorRequestTemplateHandler) AssociatedMonitors(c *gin.Context) {
|
||||||
|
id, ok := parseTemplateID(c)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
items, err := h.templateService.ListAssociatedMonitors(c.Request.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
response.ErrorFrom(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out := make([]associatedMonitorBriefResponse, 0, len(items))
|
||||||
|
for _, m := range items {
|
||||||
|
out = append(out, associatedMonitorBriefResponse{
|
||||||
|
ID: m.ID, Name: m.Name, Provider: m.Provider, Enabled: m.Enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
response.Success(c, gin.H{"items": out})
|
||||||
|
}
|
||||||
|
|||||||
@@ -103,11 +103,13 @@ func (r *channelMonitorRequestTemplateRepository) List(ctx context.Context, para
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApplyToMonitors 把模板当前配置批量覆盖到 template_id = id 的监控上。
|
// ApplyToMonitors 把模板当前配置覆盖到 monitorIDs 列表里的关联监控。
|
||||||
//
|
// WHERE 双重过滤:template_id = id AND id IN (monitorIDs),防止用户传了未关联本模板的 id
|
||||||
// 用一条 UPDATE 完成:extra_headers / body_override_mode / body_override 都覆盖。
|
// 就被覆盖。走 ent UpdateMany 保留 hooks。
|
||||||
// 走 ent 的 UpdateMany 保证走 ent hooks;走原生 SQL 也可以但 ent jsonb 序列化更省心。
|
func (r *channelMonitorRequestTemplateRepository) ApplyToMonitors(ctx context.Context, id int64, monitorIDs []int64) (int64, error) {
|
||||||
func (r *channelMonitorRequestTemplateRepository) ApplyToMonitors(ctx context.Context, id int64) (int64, error) {
|
if len(monitorIDs) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
client := clientFromContext(ctx, r.client)
|
client := clientFromContext(ctx, r.client)
|
||||||
tpl, err := client.ChannelMonitorRequestTemplate.Query().
|
tpl, err := client.ChannelMonitorRequestTemplate.Query().
|
||||||
Where(channelmonitorrequesttemplate.IDEQ(id)).
|
Where(channelmonitorrequesttemplate.IDEQ(id)).
|
||||||
@@ -117,7 +119,10 @@ func (r *channelMonitorRequestTemplateRepository) ApplyToMonitors(ctx context.Co
|
|||||||
}
|
}
|
||||||
|
|
||||||
updater := client.ChannelMonitor.Update().
|
updater := client.ChannelMonitor.Update().
|
||||||
Where(channelmonitor.TemplateIDEQ(id)).
|
Where(
|
||||||
|
channelmonitor.TemplateIDEQ(id),
|
||||||
|
channelmonitor.IDIn(monitorIDs...),
|
||||||
|
).
|
||||||
SetExtraHeaders(emptyHeadersIfNilRepo(tpl.ExtraHeaders)).
|
SetExtraHeaders(emptyHeadersIfNilRepo(tpl.ExtraHeaders)).
|
||||||
SetBodyOverrideMode(defaultBodyModeRepo(tpl.BodyOverrideMode))
|
SetBodyOverrideMode(defaultBodyModeRepo(tpl.BodyOverrideMode))
|
||||||
if tpl.BodyOverride != nil {
|
if tpl.BodyOverride != nil {
|
||||||
@@ -144,6 +149,28 @@ func (r *channelMonitorRequestTemplateRepository) CountAssociatedMonitors(ctx co
|
|||||||
return int64(count), nil
|
return int64(count), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListAssociatedMonitors 列出模板关联的所有监控简略字段。
|
||||||
|
// ORDER BY name 稳定输出方便前端展示。
|
||||||
|
func (r *channelMonitorRequestTemplateRepository) ListAssociatedMonitors(ctx context.Context, id int64) ([]*service.AssociatedMonitorBrief, error) {
|
||||||
|
rows, err := r.client.ChannelMonitor.Query().
|
||||||
|
Where(channelmonitor.TemplateIDEQ(id)).
|
||||||
|
Order(dbent.Asc(channelmonitor.FieldName)).
|
||||||
|
All(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("list associated monitors for template %d: %w", id, err)
|
||||||
|
}
|
||||||
|
out := make([]*service.AssociatedMonitorBrief, 0, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
out = append(out, &service.AssociatedMonitorBrief{
|
||||||
|
ID: row.ID,
|
||||||
|
Name: row.Name,
|
||||||
|
Provider: string(row.Provider),
|
||||||
|
Enabled: row.Enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- helpers ----------
|
// ---------- helpers ----------
|
||||||
|
|
||||||
func entToServiceTemplate(row *dbent.ChannelMonitorRequestTemplate) *service.ChannelMonitorRequestTemplate {
|
func entToServiceTemplate(row *dbent.ChannelMonitorRequestTemplate) *service.ChannelMonitorRequestTemplate {
|
||||||
|
|||||||
@@ -587,6 +587,7 @@ func registerChannelMonitorRoutes(admin *gin.RouterGroup, h *handler.Handlers) {
|
|||||||
templates.GET("/:id", h.Admin.ChannelMonitorTemplate.Get)
|
templates.GET("/:id", h.Admin.ChannelMonitorTemplate.Get)
|
||||||
templates.PUT("/:id", h.Admin.ChannelMonitorTemplate.Update)
|
templates.PUT("/:id", h.Admin.ChannelMonitorTemplate.Update)
|
||||||
templates.DELETE("/:id", h.Admin.ChannelMonitorTemplate.Delete)
|
templates.DELETE("/:id", h.Admin.ChannelMonitorTemplate.Delete)
|
||||||
|
templates.GET("/:id/monitors", h.Admin.ChannelMonitorTemplate.AssociatedMonitors)
|
||||||
templates.POST("/:id/apply", h.Admin.ChannelMonitorTemplate.Apply)
|
templates.POST("/:id/apply", h.Admin.ChannelMonitorTemplate.Apply)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,23 @@ type ChannelMonitorRequestTemplateRepository interface {
|
|||||||
Delete(ctx context.Context, id int64) error
|
Delete(ctx context.Context, id int64) error
|
||||||
List(ctx context.Context, params ChannelMonitorRequestTemplateListParams) ([]*ChannelMonitorRequestTemplate, error)
|
List(ctx context.Context, params ChannelMonitorRequestTemplateListParams) ([]*ChannelMonitorRequestTemplate, error)
|
||||||
// ApplyToMonitors 把模板当前的 extra_headers / body_override_mode / body_override
|
// ApplyToMonitors 把模板当前的 extra_headers / body_override_mode / body_override
|
||||||
// 批量覆盖到所有 template_id = id 的监控上。返回被覆盖的监控数量。
|
// 批量覆盖到指定 monitorIDs 的监控上(同时还要求这些监控当前 template_id = id,
|
||||||
ApplyToMonitors(ctx context.Context, id int64) (int64, error)
|
// 防止误覆盖未关联的监控)。monitorIDs 必须非空;空列表直接返回 0 不写库。
|
||||||
|
// 返回被覆盖的监控数量。
|
||||||
|
ApplyToMonitors(ctx context.Context, id int64, monitorIDs []int64) (int64, error)
|
||||||
// CountAssociatedMonitors 统计 template_id = id 的监控数(用于 UI 展示「应用到 N 个配置」)。
|
// CountAssociatedMonitors 统计 template_id = id 的监控数(用于 UI 展示「应用到 N 个配置」)。
|
||||||
CountAssociatedMonitors(ctx context.Context, id int64) (int64, error)
|
CountAssociatedMonitors(ctx context.Context, id int64) (int64, error)
|
||||||
|
// ListAssociatedMonitors 列出所有 template_id = id 的监控简略信息(id/name/provider/enabled)
|
||||||
|
// 给 apply picker UI 用,避免前端再做一次 list+filter。
|
||||||
|
ListAssociatedMonitors(ctx context.Context, id int64) ([]*AssociatedMonitorBrief, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AssociatedMonitorBrief 模板关联监控的简略信息(picker / 列表展示用)。
|
||||||
|
type AssociatedMonitorBrief struct {
|
||||||
|
ID int64
|
||||||
|
Name string
|
||||||
|
Provider string
|
||||||
|
Enabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChannelMonitorRequestTemplateService 模板管理 service。
|
// ChannelMonitorRequestTemplateService 模板管理 service。
|
||||||
@@ -90,13 +103,17 @@ func (s *ChannelMonitorRequestTemplateService) Delete(ctx context.Context, id in
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ApplyToMonitors 把模板当前配置一键应用到所有关联监控。
|
// ApplyToMonitors 把模板当前配置应用到 monitorIDs 列表里的关联监控。
|
||||||
// 返回被影响的监控数。
|
// monitorIDs 必须非空且每个 id 都必须当前 template_id = id;不满足条件的会被 SQL WHERE 过滤掉。
|
||||||
func (s *ChannelMonitorRequestTemplateService) ApplyToMonitors(ctx context.Context, id int64) (int64, error) {
|
// 返回实际被覆盖的监控数。
|
||||||
|
func (s *ChannelMonitorRequestTemplateService) ApplyToMonitors(ctx context.Context, id int64, monitorIDs []int64) (int64, error) {
|
||||||
if _, err := s.repo.GetByID(ctx, id); err != nil {
|
if _, err := s.repo.GetByID(ctx, id); err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
affected, err := s.repo.ApplyToMonitors(ctx, id)
|
if len(monitorIDs) == 0 {
|
||||||
|
return 0, ErrChannelMonitorTemplateApplyEmpty
|
||||||
|
}
|
||||||
|
affected, err := s.repo.ApplyToMonitors(ctx, id, monitorIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("apply template to monitors: %w", err)
|
return 0, fmt.Errorf("apply template to monitors: %w", err)
|
||||||
}
|
}
|
||||||
@@ -108,6 +125,15 @@ func (s *ChannelMonitorRequestTemplateService) CountAssociatedMonitors(ctx conte
|
|||||||
return s.repo.CountAssociatedMonitors(ctx, id)
|
return s.repo.CountAssociatedMonitors(ctx, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListAssociatedMonitors 返回模板关联的所有监控简略信息。
|
||||||
|
// 给前端 apply picker 用,handler 直接吐 JSON 不再做 join。
|
||||||
|
func (s *ChannelMonitorRequestTemplateService) ListAssociatedMonitors(ctx context.Context, id int64) ([]*AssociatedMonitorBrief, error) {
|
||||||
|
if _, err := s.repo.GetByID(ctx, id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return s.repo.ListAssociatedMonitors(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
// ---------- 校验 & 工具 ----------
|
// ---------- 校验 & 工具 ----------
|
||||||
|
|
||||||
// validateTemplateCreateParams 聚合 create 入参校验,避免函数超过 30 行。
|
// validateTemplateCreateParams 聚合 create 入参校验,避免函数超过 30 行。
|
||||||
|
|||||||
@@ -71,4 +71,7 @@ var (
|
|||||||
ErrChannelMonitorTemplateProviderMismatch = infraerrors.BadRequest(
|
ErrChannelMonitorTemplateProviderMismatch = infraerrors.BadRequest(
|
||||||
"CHANNEL_MONITOR_TEMPLATE_PROVIDER_MISMATCH", "monitor provider does not match template provider",
|
"CHANNEL_MONITOR_TEMPLATE_PROVIDER_MISMATCH", "monitor provider does not match template provider",
|
||||||
)
|
)
|
||||||
|
ErrChannelMonitorTemplateApplyEmpty = infraerrors.BadRequest(
|
||||||
|
"CHANNEL_MONITOR_TEMPLATE_APPLY_EMPTY", "monitor_ids must be a non-empty array",
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -51,6 +51,17 @@ export interface ApplyResponse {
|
|||||||
affected: number
|
affected: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AssociatedMonitorBrief {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
provider: Provider
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssociatedMonitorsResponse {
|
||||||
|
items: AssociatedMonitorBrief[]
|
||||||
|
}
|
||||||
|
|
||||||
export async function list(params: ListParams = {}): Promise<ListResponse> {
|
export async function list(params: ListParams = {}): Promise<ListResponse> {
|
||||||
const { data } = await apiClient.get<ListResponse>('/admin/channel-monitor-templates', {
|
const { data } = await apiClient.get<ListResponse>('/admin/channel-monitor-templates', {
|
||||||
params,
|
params,
|
||||||
@@ -86,12 +97,24 @@ export async function del(id: number): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply the template to all associated monitors (overwrite snapshot fields).
|
* Apply the template to the specified associated monitors (overwrite snapshot fields).
|
||||||
* Returns count of affected monitors.
|
* monitorIds must be a non-empty subset of the template's associated monitors.
|
||||||
|
* Returns count of actually affected monitors.
|
||||||
*/
|
*/
|
||||||
export async function apply(id: number): Promise<ApplyResponse> {
|
export async function apply(id: number, monitorIds: number[]): Promise<ApplyResponse> {
|
||||||
const { data } = await apiClient.post<ApplyResponse>(
|
const { data } = await apiClient.post<ApplyResponse>(
|
||||||
`/admin/channel-monitor-templates/${id}/apply`,
|
`/admin/channel-monitor-templates/${id}/apply`,
|
||||||
|
{ monitor_ids: monitorIds },
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List monitors currently associated to this template (used by apply picker).
|
||||||
|
*/
|
||||||
|
export async function listAssociatedMonitors(id: number): Promise<AssociatedMonitorsResponse> {
|
||||||
|
const { data } = await apiClient.get<AssociatedMonitorsResponse>(
|
||||||
|
`/admin/channel-monitor-templates/${id}/monitors`,
|
||||||
)
|
)
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
@@ -103,6 +126,7 @@ export const channelMonitorTemplateAPI = {
|
|||||||
update,
|
update,
|
||||||
del,
|
del,
|
||||||
apply,
|
apply,
|
||||||
|
listAssociatedMonitors,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default channelMonitorTemplateAPI
|
export default channelMonitorTemplateAPI
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
<template>
|
||||||
|
<BaseDialog
|
||||||
|
:show="show"
|
||||||
|
:title="t('admin.channelMonitor.template.applyPickerTitle', { name: templateName })"
|
||||||
|
@close="$emit('close')"
|
||||||
|
>
|
||||||
|
<p class="mb-3 text-sm text-gray-600 dark:text-gray-400">
|
||||||
|
{{ t('admin.channelMonitor.template.applyPickerHint') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div v-if="loading" class="py-6 text-center text-sm text-gray-400">
|
||||||
|
{{ t('common.loading') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="monitors.length === 0" class="py-6 text-center text-sm text-gray-400">
|
||||||
|
{{ t('admin.channelMonitor.template.applyPickerEmpty') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<!-- 全选/全不选 -->
|
||||||
|
<div class="mb-2 flex items-center gap-3 text-xs">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-primary-600 hover:underline dark:text-primary-400"
|
||||||
|
@click="selectAll"
|
||||||
|
>
|
||||||
|
{{ t('common.selectAll') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="text-gray-500 hover:underline dark:text-gray-400"
|
||||||
|
@click="selectNone"
|
||||||
|
>
|
||||||
|
{{ t('admin.channelMonitor.template.selectNone') }}
|
||||||
|
</button>
|
||||||
|
<span class="ml-auto text-gray-500 dark:text-gray-400">
|
||||||
|
{{ t('admin.channelMonitor.template.selectedCount', {
|
||||||
|
n: selectedIds.length,
|
||||||
|
total: monitors.length,
|
||||||
|
}) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul class="max-h-80 divide-y divide-gray-100 overflow-y-auto rounded-lg border border-gray-200 dark:divide-dark-700 dark:border-dark-700">
|
||||||
|
<li
|
||||||
|
v-for="m in monitors"
|
||||||
|
:key="m.id"
|
||||||
|
class="flex cursor-pointer items-center gap-3 px-3 py-2 hover:bg-gray-50 dark:hover:bg-dark-800"
|
||||||
|
@click="toggle(m.id)"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
:checked="selectedSet.has(m.id)"
|
||||||
|
class="h-4 w-4 rounded border-gray-300 text-primary-600 focus:ring-primary-500"
|
||||||
|
@click.stop="toggle(m.id)"
|
||||||
|
/>
|
||||||
|
<span class="font-medium text-gray-900 dark:text-white">{{ m.name }}</span>
|
||||||
|
<span class="text-xs text-gray-400">{{ m.provider }}</span>
|
||||||
|
<span
|
||||||
|
v-if="!m.enabled"
|
||||||
|
class="ml-auto rounded bg-gray-100 px-1.5 py-0.5 text-xs text-gray-500 dark:bg-dark-700 dark:text-gray-400"
|
||||||
|
>
|
||||||
|
{{ t('admin.channelMonitor.onlyDisabled').replace(/^仅|^Only /, '') }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<button class="btn btn-secondary" @click="$emit('close')">
|
||||||
|
{{ t('common.cancel') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary"
|
||||||
|
:disabled="submitting || selectedIds.length === 0"
|
||||||
|
@click="handleApply"
|
||||||
|
>
|
||||||
|
{{ submitting
|
||||||
|
? t('common.submitting')
|
||||||
|
: t('admin.channelMonitor.template.applyPickerConfirm', { n: selectedIds.length }) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</BaseDialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { useAppStore } from '@/stores/app'
|
||||||
|
import { extractApiErrorMessage } from '@/utils/apiError'
|
||||||
|
import { adminAPI } from '@/api/admin'
|
||||||
|
import type { AssociatedMonitorBrief } from '@/api/admin/channelMonitorTemplate'
|
||||||
|
import BaseDialog from '@/components/common/BaseDialog.vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
show: boolean
|
||||||
|
templateId: number | null
|
||||||
|
templateName: string
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'close'): void
|
||||||
|
(e: 'applied', affected: number): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const appStore = useAppStore()
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const submitting = ref(false)
|
||||||
|
const monitors = ref<AssociatedMonitorBrief[]>([])
|
||||||
|
const selectedIds = ref<number[]>([])
|
||||||
|
|
||||||
|
const selectedSet = computed(() => new Set(selectedIds.value))
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => [props.show, props.templateId] as const,
|
||||||
|
([show, id]) => {
|
||||||
|
if (!show || id == null) return
|
||||||
|
void fetchMonitors(id)
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
|
||||||
|
async function fetchMonitors(id: number) {
|
||||||
|
loading.value = true
|
||||||
|
monitors.value = []
|
||||||
|
selectedIds.value = []
|
||||||
|
try {
|
||||||
|
const { items } = await adminAPI.channelMonitorTemplate.listAssociatedMonitors(id)
|
||||||
|
monitors.value = items
|
||||||
|
// 默认全选
|
||||||
|
selectedIds.value = items.map((m) => m.id)
|
||||||
|
} catch (err: unknown) {
|
||||||
|
appStore.showError(extractApiErrorMessage(err, t('common.error')))
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(id: number) {
|
||||||
|
const idx = selectedIds.value.indexOf(id)
|
||||||
|
if (idx >= 0) selectedIds.value.splice(idx, 1)
|
||||||
|
else selectedIds.value.push(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectAll() {
|
||||||
|
selectedIds.value = monitors.value.map((m) => m.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectNone() {
|
||||||
|
selectedIds.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleApply() {
|
||||||
|
if (props.templateId == null || selectedIds.value.length === 0 || submitting.value) return
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const { affected } = await adminAPI.channelMonitorTemplate.apply(
|
||||||
|
props.templateId,
|
||||||
|
[...selectedIds.value],
|
||||||
|
)
|
||||||
|
appStore.showSuccess(t('admin.channelMonitor.template.applySuccess', { n: affected }))
|
||||||
|
emit('applied', affected)
|
||||||
|
emit('close')
|
||||||
|
} catch (err: unknown) {
|
||||||
|
appStore.showError(extractApiErrorMessage(err, t('common.error')))
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -180,14 +180,12 @@
|
|||||||
</template>
|
</template>
|
||||||
</BaseDialog>
|
</BaseDialog>
|
||||||
|
|
||||||
<ConfirmDialog
|
<MonitorTemplateApplyPickerDialog
|
||||||
:show="confirmApply_.show"
|
:show="applyPicker.show"
|
||||||
:title="t('admin.channelMonitor.template.applyTitle')"
|
:template-id="applyPicker.tpl ? applyPicker.tpl.id : null"
|
||||||
:message="confirmApplyMessage"
|
:template-name="applyPicker.tpl ? applyPicker.tpl.name : ''"
|
||||||
:confirm-text="t('admin.channelMonitor.template.applyConfirm')"
|
@close="applyPicker.show = false"
|
||||||
:cancel-text="t('common.cancel')"
|
@applied="onApplied"
|
||||||
@confirm="doApply"
|
|
||||||
@cancel="confirmApply_.show = false"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
@@ -217,6 +215,7 @@ import BaseDialog from '@/components/common/BaseDialog.vue'
|
|||||||
import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
|
import ConfirmDialog from '@/components/common/ConfirmDialog.vue'
|
||||||
import Icon from '@/components/icons/Icon.vue'
|
import Icon from '@/components/icons/Icon.vue'
|
||||||
import MonitorAdvancedRequestConfig from '@/components/admin/monitor/MonitorAdvancedRequestConfig.vue'
|
import MonitorAdvancedRequestConfig from '@/components/admin/monitor/MonitorAdvancedRequestConfig.vue'
|
||||||
|
import MonitorTemplateApplyPickerDialog from '@/components/admin/monitor/MonitorTemplateApplyPickerDialog.vue'
|
||||||
import { useChannelMonitorFormat } from '@/composables/useChannelMonitorFormat'
|
import { useChannelMonitorFormat } from '@/composables/useChannelMonitorFormat'
|
||||||
import {
|
import {
|
||||||
PROVIDER_ANTHROPIC,
|
PROVIDER_ANTHROPIC,
|
||||||
@@ -373,38 +372,21 @@ async function handleSubmit() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- apply to monitors ---
|
// --- apply to monitors (picker 流程) ---
|
||||||
const confirmApply_ = reactive<{ show: boolean; tpl: ChannelMonitorTemplate | null }>({
|
const applyPicker = reactive<{ show: boolean; tpl: ChannelMonitorTemplate | null }>({
|
||||||
show: false,
|
show: false,
|
||||||
tpl: null,
|
tpl: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
function confirmApply(tpl: ChannelMonitorTemplate) {
|
function confirmApply(tpl: ChannelMonitorTemplate) {
|
||||||
confirmApply_.tpl = tpl
|
applyPicker.tpl = tpl
|
||||||
confirmApply_.show = true
|
applyPicker.show = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const confirmApplyMessage = computed(() => {
|
// picker 提交后触发:刷新模板列表(拿最新 associated_monitors)+ 通知父组件
|
||||||
const tpl = confirmApply_.tpl
|
async function onApplied(_affected: number) {
|
||||||
if (!tpl) return ''
|
await fetchTemplates()
|
||||||
return t('admin.channelMonitor.template.applyConfirmMessage', {
|
emit('updated')
|
||||||
name: tpl.name,
|
|
||||||
n: tpl.associated_monitors,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
async function doApply() {
|
|
||||||
const tpl = confirmApply_.tpl
|
|
||||||
confirmApply_.show = false
|
|
||||||
if (!tpl) return
|
|
||||||
try {
|
|
||||||
const { affected } = await adminAPI.channelMonitorTemplate.apply(tpl.id)
|
|
||||||
appStore.showSuccess(t('admin.channelMonitor.template.applySuccess', { n: affected }))
|
|
||||||
await fetchTemplates()
|
|
||||||
emit('updated')
|
|
||||||
} catch (err: unknown) {
|
|
||||||
appStore.showError(extractApiErrorMessage(err, t('common.error')))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- delete ---
|
// --- delete ---
|
||||||
|
|||||||
@@ -273,6 +273,7 @@ export default {
|
|||||||
no: 'No',
|
no: 'No',
|
||||||
all: 'All',
|
all: 'All',
|
||||||
none: 'None',
|
none: 'None',
|
||||||
|
selectAll: 'Select all',
|
||||||
noData: 'No data',
|
noData: 'No data',
|
||||||
expand: 'Expand',
|
expand: 'Expand',
|
||||||
collapse: 'Collapse',
|
collapse: 'Collapse',
|
||||||
@@ -2192,11 +2193,17 @@ export default {
|
|||||||
updateSuccess: 'Template updated',
|
updateSuccess: 'Template updated',
|
||||||
deleteSuccess: 'Template deleted',
|
deleteSuccess: 'Template deleted',
|
||||||
applyButton: 'Apply to monitors',
|
applyButton: 'Apply to monitors',
|
||||||
applyTooltip: 'Overwrite snapshot fields on all associated monitors',
|
applyTooltip: 'Overwrite snapshot fields on associated monitors',
|
||||||
applyTitle: 'Apply template',
|
applyTitle: 'Apply template',
|
||||||
applyConfirm: 'Apply',
|
applyConfirm: 'Apply',
|
||||||
applyConfirmMessage: 'Overwrite {n} associated monitor(s) with the current configuration of "{name}"? Any local customizations on those monitors will be discarded.',
|
applyConfirmMessage: 'Overwrite {n} associated monitor(s) with the current configuration of "{name}"? Any local customizations on those monitors will be discarded.',
|
||||||
applySuccess: 'Applied to {n} monitor(s)',
|
applySuccess: 'Applied to {n} monitor(s)',
|
||||||
|
applyPickerTitle: 'Apply template "{name}"',
|
||||||
|
applyPickerHint: 'Select which monitors to overwrite (all selected by default). Any local customizations will be discarded.',
|
||||||
|
applyPickerEmpty: 'No monitors are currently associated to this template',
|
||||||
|
applyPickerConfirm: 'Apply to {n} monitor(s)',
|
||||||
|
selectNone: 'Select none',
|
||||||
|
selectedCount: 'Selected {n} / {total}',
|
||||||
deleteConfirm: 'Delete template "{name}"? {n} associated monitor(s) will be disassociated but keep their current snapshot and continue running.',
|
deleteConfirm: 'Delete template "{name}"? {n} associated monitor(s) will be disassociated but keep their current snapshot and continue running.',
|
||||||
associatedCount: '{n} associated monitor(s)',
|
associatedCount: '{n} associated monitor(s)',
|
||||||
headersSummary: '{n} custom header(s)',
|
headersSummary: '{n} custom header(s)',
|
||||||
|
|||||||
@@ -273,6 +273,7 @@ export default {
|
|||||||
no: '否',
|
no: '否',
|
||||||
all: '全部',
|
all: '全部',
|
||||||
none: '无',
|
none: '无',
|
||||||
|
selectAll: '全选',
|
||||||
noData: '暂无数据',
|
noData: '暂无数据',
|
||||||
expand: '展开',
|
expand: '展开',
|
||||||
collapse: '收起',
|
collapse: '收起',
|
||||||
@@ -2276,6 +2277,12 @@ export default {
|
|||||||
applyConfirm: '确认应用',
|
applyConfirm: '确认应用',
|
||||||
applyConfirmMessage: '将把模板「{name}」的当前配置覆盖到 {n} 个关联监控。监控本地已编辑的自定义修改会被丢弃,是否继续?',
|
applyConfirmMessage: '将把模板「{name}」的当前配置覆盖到 {n} 个关联监控。监控本地已编辑的自定义修改会被丢弃,是否继续?',
|
||||||
applySuccess: '已应用到 {n} 个监控',
|
applySuccess: '已应用到 {n} 个监控',
|
||||||
|
applyPickerTitle: '应用模板「{name}」',
|
||||||
|
applyPickerHint: '勾选要覆盖请求头/请求体的监控(默认全选)。监控本地已编辑的自定义修改会被丢弃。',
|
||||||
|
applyPickerEmpty: '当前模板没有关联监控',
|
||||||
|
applyPickerConfirm: '应用到 {n} 个监控',
|
||||||
|
selectNone: '全不选',
|
||||||
|
selectedCount: '已选 {n} / {total}',
|
||||||
deleteConfirm: '确定要删除模板「{name}」吗?{n} 个关联监控会解除关联但保留自己的快照继续工作。',
|
deleteConfirm: '确定要删除模板「{name}」吗?{n} 个关联监控会解除关联但保留自己的快照继续工作。',
|
||||||
associatedCount: '{n} 个关联监控',
|
associatedCount: '{n} 个关联监控',
|
||||||
headersSummary: '{n} 个自定义请求头',
|
headersSummary: '{n} 个自定义请求头',
|
||||||
|
|||||||
Reference in New Issue
Block a user