Просмотр исходного кода

小程序投流 多链接生成 + 权限模型扩展 + 去备注:

- 权限模型:AdminRouterItem.requireType 由 number 扩展为 number | number[],sidebar 判断走 Array.isArray + includes;小程序投流菜单 requireType 由 2 改为 [2, 3],代理商也可见
- 列表新增「复制多链接」按钮(仅 type∈{2,3} 渲染),点击弹 Modal 输入数量 1~200(默认 10),调 xcxPlanMultiLinkApi,成功后将 N 个 pageUrl 以换行拼接复制到剪贴板并刷新列表,confirmLoading 期间禁止关闭
- 列表去掉「备注」列;创建发布弹窗去掉「备注」Form.Item;保存 payload 去 remark 字段
- api.ts 加 xcxPlanMultiLinkApi 常量

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
刘立冬 2 дней назад
Родитель
Сommit
4147437a9e

+ 4 - 1
src/components/layout/sidebar.tsx

@@ -10,7 +10,10 @@ const { Sider } = Layout;
 const getMenuItems = (routes: AdminRouterItem[], notReadMessageCount: number, userType?: number): any[] => {
   return routes.map(itm => {
     if (!itm.meta) return null
-    if (itm.requireType != null && itm.requireType !== userType) return null
+    if (itm.requireType != null) {
+      const allowed = Array.isArray(itm.requireType) ? itm.requireType : [itm.requireType]
+      if (userType == null || !allowed.includes(userType)) return null
+    }
     let children: any[] = []
     if (itm.children) children = getMenuItems(itm.children, notReadMessageCount, userType)
 

+ 1 - 0
src/http/api.ts

@@ -44,6 +44,7 @@ export const getShareQrLink = `${import.meta.env.VITE_API_URL}/contentPlatform/p
 export const getXcxPlanListApi = `${import.meta.env.VITE_API_URL}/contentPlatform/plan/xcx/list`
 export const saveXcxPlanApi = `${import.meta.env.VITE_API_URL}/contentPlatform/plan/xcx/save`
 export const deleteXcxPlanApi = `${import.meta.env.VITE_API_URL}/contentPlatform/plan/xcx/delete`
+export const xcxPlanMultiLinkApi = `${import.meta.env.VITE_API_URL}/contentPlatform/plan/xcx/multiLink`
 export const getXcxSharePicApi = `${import.meta.env.VITE_API_URL}/contentPlatform/plan/xcx/getSharePic`
 export const getXcxAudiencePackageListApi = `${import.meta.env.VITE_API_URL}/contentPlatform/plan/xcx/getAudiencePackageList`
 

+ 3 - 2
src/router/index.tsx

@@ -13,8 +13,9 @@ export type AdminRouterItem = RouteObject & {
 	children?: AdminRouterItem[],
 	sortKey?: number,
 	extra?: any,
-	// 仅当登录用户的 ContentPlatformAccount.type 等于该值时显示此菜单
-	requireType?: number
+	// 仅当登录用户的 ContentPlatformAccount.type 命中时显示此菜单
+	// 支持单值或数组,数组语义为"任一命中即可"
+	requireType?: number | number[]
 }
 
 /**

+ 1 - 1
src/views/publishContent/publishContent.router.tsx

@@ -63,7 +63,7 @@ const demoRoutes: AdminRouterItem[] = [
 			{
 				path: 'xcxTouliu',
 				element: <LazyComponent Component={XcxTouliu} />,
-				requireType: 2,
+				requireType: [2, 3],
 				meta: {
 					label: "小程序投流",
 					title: "小程序投流",

+ 1 - 10
src/views/publishContent/xcxTouliu/components/publishPlanModal/index.tsx

@@ -1,5 +1,5 @@
 import React, { useEffect, useState } from 'react';
-import { Modal, Form, Input, Select, Button, Card, Typography, message } from 'antd';
+import { Modal, Form, Select, Button, Card, Typography, message } from 'antd';
 import { CloseOutlined, PlusOutlined, EditOutlined, CaretRightFilled } from '@ant-design/icons';
 import VideoSelectModal from '@src/views/publishContent/weGZH/components/videoSelectModal';
 import EditTitleCoverModal from '@src/views/publishContent/weGZH/components/editTitleCoverModal';
@@ -140,15 +140,6 @@ const XcxPublishPlanModal: React.FC<XcxPublishPlanModalProps> = ({
 							onChange={() => setSelectedVideos([])}
 						/>
 					</Form.Item>
-					<Form.Item
-						name="remark"
-						label="备注"
-						labelCol={{ span: 4 }}
-						labelAlign="left"
-						layout="horizontal"
-					>
-						<Input.TextArea placeholder="可选,内部备注" className="!w-80" rows={2} maxLength={200} />
-					</Form.Item>
 					<Form.Item label="发布内容" required>
 						<div className="flex flex-wrap gap-4 max-h-[660px] overflow-y-auto pr-2">
 							{selectedVideos.map((video) => (

+ 86 - 11
src/views/publishContent/xcxTouliu/index.tsx

@@ -1,5 +1,5 @@
-import React, { useEffect, useState } from 'react';
-import { Space, Table, Button, Input, Select, DatePicker, message, Typography, Spin, Popconfirm } from 'antd';
+import React, { useEffect, useMemo, useState } from 'react';
+import { Space, Table, Button, Input, Select, DatePicker, message, Typography, Spin, Popconfirm, Modal, InputNumber } from 'antd';
 import type { TableProps } from 'antd';
 import dayjs, { Dayjs } from 'dayjs';
 import copy from 'copy-to-clipboard';
@@ -12,7 +12,8 @@ import { useXcxPlanList, XcxPlanDataType } from './hooks/useXcxPlanList';
 import { useAudiencePackageOptions } from './hooks/useAudiencePackageOptions';
 import { VideoItem } from '@src/views/publishContent/weGZH/components/types';
 import http from '@src/http';
-import { deleteXcxPlanApi, getShareQrLink, saveXcxPlanApi } from '@src/http/api';
+import { deleteXcxPlanApi, getShareQrLink, saveXcxPlanApi, xcxPlanMultiLinkApi } from '@src/http/api';
+import { getUserInfo } from '@src/http/sso';
 
 const { RangePicker } = DatePicker;
 const TableHeight = window.innerHeight - 380;
@@ -27,8 +28,13 @@ const XcxTouliuContent: React.FC = () => {
 	const [pageNum, setPageNum] = useState<number>(1);
 	const [pageSize, setPageSize] = useState<number>(10);
 	const [playingPlan, setPlayingPlan] = useState<XcxPlanDataType | null>(null);
+	const [multiLinkPlan, setMultiLinkPlan] = useState<XcxPlanDataType | null>(null);
+	const [multiLinkCount, setMultiLinkCount] = useState<number>(10);
+	const [multiLinkLoading, setMultiLinkLoading] = useState<boolean>(false);
 	const { xcxPlanList, getXcxPlanList, totalSize } = useXcxPlanList();
 	const { options: audiencePackageOptions } = useAudiencePackageOptions();
+	const userType = useMemo(() => getUserInfo()?.type, []);
+	const canMultiLink = userType === 2 || userType === 3;
 
 	const columns: TableProps<XcxPlanDataType>['columns'] = [
 		{
@@ -61,13 +67,6 @@ const XcxTouliuContent: React.FC = () => {
 					<img src={record.cover} referrerPolicy="no-referrer" className="w-[80px] h-auto" />
 				) : '-',
 		},
-		{
-			title: '备注',
-			dataIndex: 'remark',
-			key: 'remark',
-			width: 180,
-			ellipsis: true,
-		},
 		{
 			title: '计划创建时间',
 			dataIndex: 'createTimestamp',
@@ -87,6 +86,9 @@ const XcxTouliuContent: React.FC = () => {
 					<Button type="link" onClick={() => downloadFile(record.shareCover || record.cover, (record.title || 'cover') + '_cover')}>下载封面</Button>
 					<Button type="link" onClick={() => showQrCodeModal(record.pageUrl)}>二维码</Button>
 					<Button type="link" onClick={() => copyToClipboard(record.pageUrl)}>复制链接</Button>
+					{canMultiLink && (
+						<Button type="link" onClick={() => openMultiLinkModal(record)}>复制多链接</Button>
+					)}
 					<Popconfirm
 						title="确定删除该计划吗?"
 						okText="确定"
@@ -140,6 +142,47 @@ const XcxTouliuContent: React.FC = () => {
 		});
 	};
 
+	const openMultiLinkModal = (record: XcxPlanDataType) => {
+		setMultiLinkPlan(record);
+		setMultiLinkCount(10);
+	};
+
+	const closeMultiLinkModal = () => {
+		if (multiLinkLoading) return;
+		setMultiLinkPlan(null);
+	};
+
+	const handleMultiLink = async () => {
+		if (!multiLinkPlan) return;
+		if (!multiLinkCount || multiLinkCount < 1 || multiLinkCount > 200) {
+			message.warning('生成数量必须在 1-200 之间');
+			return;
+		}
+		setMultiLinkLoading(true);
+		try {
+			const res = await http.post<XcxPlanDataType[]>(xcxPlanMultiLinkApi, {
+				planId: multiLinkPlan.id,
+				count: multiLinkCount,
+			});
+			if (res?.code === 0 && Array.isArray(res.data) && res.data.length > 0) {
+				const links = res.data.map((p) => p.pageUrl).filter(Boolean).join('\n');
+				if (links && copy(links)) {
+					message.success(`已生成 ${res.data.length} 条并复制到剪贴板`);
+				} else {
+					message.warning('生成成功但复制失败,请手动到列表中获取');
+				}
+				setMultiLinkPlan(null);
+				refresh();
+			} else {
+				message.error(res?.msg || '生成失败');
+			}
+		} catch (err: any) {
+			message.error(err?.msg || '生成失败');
+		} finally {
+			setMultiLinkLoading(false);
+		}
+	};
+
 	const deletePlan = async (record: XcxPlanDataType) => {
 		setIsLoading(true);
 		const res = await http
@@ -166,7 +209,6 @@ const XcxTouliuContent: React.FC = () => {
 		setIsSubmiting(true);
 		const payload = {
 			audiencePackage: params.audiencePackage,
-			remark: params.remark,
 			videoList: (params.videoList || []).map((v) => ({
 				videoId: v.videoId,
 				title: v.customTitle || v.title,
@@ -301,6 +343,39 @@ const XcxTouliuContent: React.FC = () => {
 					videoUrl={playingPlan?.video || ''}
 					title={playingPlan?.title || ''}
 				/>
+
+				<Modal
+					title="批量生成链接"
+					open={!!multiLinkPlan}
+					onCancel={closeMultiLinkModal}
+					onOk={handleMultiLink}
+					confirmLoading={multiLinkLoading}
+					maskClosable={false}
+					closable={!multiLinkLoading}
+					okText="生成并复制"
+					cancelText="取消"
+					destroyOnClose
+				>
+					<div className="text-gray-600 text-sm mb-3">
+						将基于该计划生成 N 份相同视频/人群包的新计划,生成完成后所有链接将自动复制到剪贴板。
+					</div>
+					<div className="flex items-center gap-2">
+						<span>生成数量:</span>
+						<InputNumber
+							min={1}
+							max={200}
+							value={multiLinkCount}
+							onChange={(v) => setMultiLinkCount(Number(v) || 1)}
+							disabled={multiLinkLoading}
+						/>
+						<span className="text-gray-400 text-xs">(1-200)</span>
+					</div>
+					{multiLinkLoading && (
+						<div className="text-orange-500 text-xs mt-3">
+							链接生成耗时较长,请勿关闭窗口...
+						</div>
+					)}
+				</Modal>
 			</div>
 		</Spin>
 	);