批量政策标签替换功能集成指南

本文档说明如何在广告账户列表页面中集成批量政策标签替换功能。

功能概述

批量政策标签替换功能允许用户: - 在表格中选择多个广告账户 - 批量设置政策标签(折扣、后返、扣点、奖励) - 查看所有选中账户的共同可用标签 - 一次性为所有账户应用相同的政策标签配置

集成步骤

1. 安装依赖

确保项目中已安装所需组件和工具:

import { BatchAccountPolicyLabelDialog, AccountInfo } from '@/components/policy-label';
import { toast } from 'sonner';

2. 在页面中添加状态管理

'use client';

import { useState, useCallback } from 'react';
import { Button } from '@/components/ui/button';
import { BatchAccountPolicyLabelDialog, AccountInfo } from '@/components/policy-label';
import { Tags } from 'lucide-react';

export default function AdAccountPage() {
  // 用于存储选中的账户
  const [selectedAccounts, setSelectedAccounts] = useState<AccountInfo[]>([]);

  // 控制批量对话框显示
  const [showBatchPolicyDialog, setShowBatchPolicyDialog] = useState(false);

  // 从表格获取选中的行数据
  const handleSelectionChanged = useCallback(() => {
    const selectedRows = gridRef.current?.api?.getSelectedRows() || [];

    // 转换为 AccountInfo 格式
    const accounts: AccountInfo[] = selectedRows.map(row => ({
      accountId: row.accountId,
      accountName: row.accountName || row.accountId.toString(),
      customerId: row.customerId,
      customerName: row.customer?.name,
      appliedLabels: row.policyLabels || [],
    }));

    setSelectedAccounts(accounts);
  }, []);

  // 打开批量设置对话框
  const handleBatchPolicyClick = () => {
    if (selectedAccounts.length === 0) {
      toast.error('请先选择要操作的账户');
      return;
    }
    setShowBatchPolicyDialog(true);
  };

  // 批量操作成功后的回调
  const handleBatchSuccess = () => {
    // 刷新表格数据
    gridRef.current?.api?.refreshServerSide();

    // 清空选择
    gridRef.current?.api?.deselectAll();
    setSelectedAccounts([]);

    toast.success('批量政策标签设置成功');
  };

  return (
    <>
      {/* 工具栏按钮 */}
      <div className="flex gap-2">
        <Button
          onClick={handleBatchPolicyClick}
          disabled={selectedAccounts.length === 0}
          variant="outline"
        >
          <Tags className="mr-2 h-4 w-4" />
          批量设置政策标签
          {selectedAccounts.length > 0 && ` (${selectedAccounts.length})`}
        </Button>
      </div>

      {/* AG Grid */}
      <AgGridReact
        ref={gridRef}
        rowSelection="multiple"
        onSelectionChanged={handleSelectionChanged}
        // ... 其他配置
      />

      {/* 批量政策标签对话框 */}
      <BatchAccountPolicyLabelDialog
        open={showBatchPolicyDialog}
        onClose={() => setShowBatchPolicyDialog(false)}
        accounts={selectedAccounts}
        onSuccess={handleBatchSuccess}
      />
    </>
  );
}

3. AG Grid 配置

确保 AG Grid 配置了行选择功能:

<AgGridReact
  ref={gridRef}
  rowSelection="multiple"  // 启用多选
  rowMultiSelectWithClick={true}  // 点击行即可选择
  suppressRowClickSelection={false}  // 允许点击行选择
  onSelectionChanged={handleSelectionChanged}  // 选择变化回调
  // ... 其他配置
/>

4. 在底部固定行中添加批量操作按钮(可选)

const [pinnedBottomRowData, setPinnedBottomRowData] = useState<any[]>([{
  accountId: '选择上方行项目进行批量操作',
  batchActions: true,  // 标记为批量操作行
}]);

// 在列定义中添加批量操作渲染器
{
  field: 'policyLabels',
  cellRenderer: (params: any) => {
    if (params.data?.batchActions) {
      return (
        <Button
          size="sm"
          onClick={handleBatchPolicyClick}
          disabled={selectedAccounts.length === 0}
        >
          批量设置政策标签 ({selectedAccounts.length})
        </Button>
      );
    }
    // 正常的单行政策标签渲染
    return <ProductTagsRenderer {...params} />;
  }
}

5. 完整示例代码

'use client';

import { useRef, useState, useCallback } from 'react';
import { AgGridReact } from 'ag-grid-react';
import { Button } from '@/components/ui/button';
import { BatchAccountPolicyLabelDialog, AccountInfo } from '@/components/policy-label';
import { toast } from 'sonner';
import { Tags } from 'lucide-react';

export default function AdAccountPage() {
  const gridRef = useRef<AgGridReact>(null);
  const [selectedAccounts, setSelectedAccounts] = useState<AccountInfo[]>([]);
  const [showBatchPolicyDialog, setShowBatchPolicyDialog] = useState(false);

  // 监听选择变化
  const handleSelectionChanged = useCallback(() => {
    const selectedRows = gridRef.current?.api?.getSelectedRows() || [];
    const accounts: AccountInfo[] = selectedRows
      .filter(row => row.customerId) // 过滤掉没有客户的账户
      .map(row => ({
        accountId: row.accountId,
        accountName: row.accountName || `账户${row.accountId}`,
        customerId: row.customerId,
        customerName: row.customer?.name || `客户${row.customerId}`,
        appliedLabels: row.policyLabels || [],
      }));

    setSelectedAccounts(accounts);
  }, []);

  // 打开批量设置对话框
  const handleBatchPolicyClick = () => {
    if (selectedAccounts.length === 0) {
      toast.error('请先选择要操作的账户');
      return;
    }

    // 检查是否所有账户都属于同一个客户
    const customerIds = new Set(selectedAccounts.map(a => a.customerId));
    if (customerIds.size > 1) {
      toast.warning('所选账户属于不同客户,只会显示共同的可用标签');
    }

    setShowBatchPolicyDialog(true);
  };

  // 批量操作成功回调
  const handleBatchSuccess = () => {
    // 刷新表格
    gridRef.current?.api?.refreshServerSide();

    // 清空选择
    gridRef.current?.api?.deselectAll();
    setSelectedAccounts([]);
  };

  return (
    <div className="space-y-4">
      {/* 工具栏 */}
      <div className="flex items-center justify-between">
        <div className="flex gap-2">
          <Button
            onClick={handleBatchPolicyClick}
            disabled={selectedAccounts.length === 0}
            variant="outline"
            size="sm"
          >
            <Tags className="mr-2 h-4 w-4" />
            批量设置政策标签
            {selectedAccounts.length > 0 && (
              <span className="ml-1 text-muted-foreground">
                ({selectedAccounts.length})
              </span>
            )}
          </Button>
        </div>
      </div>

      {/* 表格 */}
      <AgGridReact
        ref={gridRef}
        rowSelection="multiple"
        rowMultiSelectWithClick={true}
        onSelectionChanged={handleSelectionChanged}
        // ... 其他配置
      />

      {/* 批量政策标签对话框 */}
      <BatchAccountPolicyLabelDialog
        open={showBatchPolicyDialog}
        onClose={() => setShowBatchPolicyDialog(false)}
        accounts={selectedAccounts}
        onSuccess={handleBatchSuccess}
      />
    </div>
  );
}

注意事项

1. 客户限制

  • 如果选中的账户属于不同客户,对话框会自动显示所有客户共同拥有的政策标签(取交集)
  • 建议在UI中提示用户选择相同客户的账户以获得最佳体验

2. 权限控制

确保用户有相应的操作权限:

import { useCan } from '@/lib/hooks/useCasbin';

const canBatchApply = useCan('AdvertiserService.BatchApplyPolicyLabels');

<Button
  onClick={handleBatchPolicyClick}
  disabled={!canBatchApply || selectedAccounts.length === 0}
>
  批量设置政策标签
</Button>

3. 数据刷新

批量操作成功后,建议: - 刷新表格数据以显示最新的政策标签 - 清空选择状态 - 显示成功提示

4. 错误处理

组件内部已经处理了常见错误,但你可以在 onSuccess 回调中添加额外的错误处理逻辑:

const handleBatchSuccess = () => {
  try {
    gridRef.current?.api?.refreshServerSide();
    gridRef.current?.api?.deselectAll();
    toast.success('批量设置完成');
  } catch (error) {
    console.error('刷新失败:', error);
    toast.error('操作成功,但刷新数据失败,请手动刷新页面');
  }
};

5. 审批流程

如果政策标签变更需要审批: - 对话框会自动显示审批提示 - 用户可以点击"查看审批"跳转到审批页面 - 审批通过后,相关账户的政策标签才会生效

API 要求

确保后端支持以下接口:

获取客户政策标签

POST /api/v1/customers/policy_labels
Body: { "customer_id": number }
Response: { "policyLabels": PolicyLabel[] }

批量应用政策标签

POST /api/v1/advertiser/batch_apply_policy_labels
Body: {
  "operations": [
    {
      "account_id": number,
      "policy_label_type": string,
      "new_policy_label_id"?: number,
      "delete_policy_label_id"?: number
    }
  ]
}
Response: {
  "successCount": number,
  "failedCount": number,
  "review"?: boolean,
  "results": [
    {
      "accountId": number,
      "success": boolean,
      "message"?: string,
      "appliedPolicyLabels": PolicyLabel[]
    }
  ]
}

样式定制

如果需要自定义对话框样式,可以通过 CSS 覆盖:

/* 自定义对话框宽度 */
.batch-policy-dialog .dialog-content {
  max-width: 900px;
}

/* 自定义账户卡片样式 */
.batch-policy-dialog .account-card {
  background: var(--muted);
  border-radius: 8px;
}

测试建议

  1. 单客户多账户: 选择属于同一客户的多个账户
  2. 多客户多账户: 选择属于不同客户的账户,验证交集逻辑
  3. 空标签账户: 选择没有应用任何标签的账户
  4. 混合状态: 选择有不同标签状态的账户
  5. 审批流程: 测试需要审批的场景
  6. 错误处理: 测试网络错误、权限错误等场景

故障排查

问题:对话框中没有可选标签

可能原因: - 选中的账户属于不同客户,且没有共同标签 - 客户没有配置政策标签 - API 返回数据为空

解决方案: - 检查选中账户的客户ID - 在客户管理页面为客户配置政策标签 - 检查 API 响应数据

问题:应用后标签没有更新

可能原因: - 表格没有刷新 - 需要审批,标签还未生效 - API 返回成功但实际失败

解决方案: - 确保调用了 refreshServerSide() - 检查是否进入审批流程 - 查看网络请求和响应

扩展功能

1. 添加预设模板

// 定义常用的标签组合
const POLICY_TEMPLATES = [
  {
    name: '标准折扣',
    labels: { TAG_DISCOUNT: 1, TAG_REBATE: null }
  },
  {
    name: 'VIP客户',
    labels: { TAG_DISCOUNT: 2, TAG_REWARD: 5 }
  }
];

// 在对话框中添加快速选择按钮
<Select onValueChange={applyTemplate}>
  <SelectTrigger>快速选择模板</SelectTrigger>
  <SelectContent>
    {POLICY_TEMPLATES.map(t => (
      <SelectItem key={t.name} value={t.name}>{t.name}</SelectItem>
    ))}
  </SelectContent>
</Select>

2. 添加变更预览

在确认对话框中显示将要发生的变更:

// 计算变更摘要
const changes = accounts.map(account => {
  const changes = [];
  Object.entries(selectedLabels).forEach(([type, labelId]) => {
    const current = account.appliedLabels.find(l => l.type === type);
    if (current?.id !== labelId) {
      changes.push({
        account: account.accountName,
        type: getPolicyTypeLabel(type),
        from: current?.name || '无',
        to: labelId ? '新标签' : '移除'
      });
    }
  });
  return changes;
}).flat();

// 在确认对话框中展示
<AlertDialogDescription>
  将要修改 {changes.length} 处配置:
  <ul className="mt-2 space-y-1">
    {changes.slice(0, 5).map((c, i) => (
      <li key={i} className="text-sm">
        {c.account} - {c.type}: {c.from} → {c.to}
      </li>
    ))}
  </ul>
  {changes.length > 5 && <p className="mt-1">...还有 {changes.length - 5} 处变更</p>}
</AlertDialogDescription>