| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>后台管理 - 汇丰权益配置</title>
- <script src="https://cdn.tailwindcss.com"></script>
- <script src="https://unpkg.com/@phosphor-icons/web"></script>
- <style>
- ::-webkit-scrollbar { width: 8px; height: 8px; }
- ::-webkit-scrollbar-track { background: #f1f1f1; }
- ::-webkit-scrollbar-thumb { background: #ccc; border-radius: 4px; }
- ::-webkit-scrollbar-thumb:hover { background: #aaa; }
- .menu-item { transition: all 0.2s; }
- .menu-item:hover { color: #fff; }
- </style>
- </head>
- <body class="bg-gray-100 h-screen flex overflow-hidden font-sans text-gray-800">
- <!-- 左侧菜单栏 -->
- <aside class="w-[240px] bg-[#364252] text-gray-300 flex flex-col h-full flex-shrink-0 shadow-xl z-20">
- <div class="py-6 text-2xl font-bold text-white flex justify-center items-center gap-3 bg-[#2b3644] border-b border-[#2b3644]">
- <i class="ph ph-desktop"></i>
- 管理后台
- </div>
-
- <div class="flex-1 overflow-y-auto py-4 text-[15px]">
- <div>
- <div class="px-5 py-3 flex justify-between items-center cursor-pointer text-gray-200 hover:text-white transition">
- <div class="flex items-center gap-3"><i class="ph ph-gear text-lg"></i><span>定制功能</span></div>
- <i class="ph ph-caret-up text-sm text-gray-400"></i>
- </div>
-
- <div class="flex flex-col">
- <div class="px-5 py-3 pl-11 flex justify-between items-center cursor-pointer text-gray-200 hover:text-white transition">
- <span>汇丰银行</span>
- <i class="ph ph-caret-up text-sm text-gray-400"></i>
- </div>
-
- <div class="flex flex-col">
- <div class="px-5 py-3 pl-16 text-gray-200 hover:text-white transition cursor-default">页面管理</div>
- <div class="flex flex-col text-sm">
- <a href="#" onclick="switchMenu('home')" id="menu-home" class="py-2.5 pl-[72px] pr-5 text-blue-400 bg-[#2b3644] font-medium block transition">
- 权益介绍页
- </a>
- <a href="#" onclick="switchMenu('zone')" id="menu-zone" class="py-2.5 pl-[72px] pr-5 text-gray-400 hover:text-white block transition">
- 权益首页
- </a>
- <a href="#" onclick="switchMenu('assembly')" id="menu-assembly" class="py-2.5 pl-[72px] pr-5 text-gray-400 hover:text-white block transition">
- 组装页
- </a>
- </div>
- <div class="px-5 py-3 pl-16 flex justify-between items-center cursor-pointer text-gray-200 hover:text-white transition mt-2">
- <span>权益管理</span>
- <i class="ph ph-caret-up text-sm text-gray-400"></i>
- </div>
- <div class="flex flex-col text-[14px]">
- <a href="#" class="py-2.5 pl-[72px] pr-5 text-gray-400 hover:text-white transition block">借记卡权益</a>
- <a href="#" class="py-2.5 pl-[72px] pr-5 text-gray-400 hover:text-white transition block">信用卡权益</a>
- <a href="#" class="py-2.5 pl-[72px] pr-5 text-gray-400 hover:text-white transition block">人群管理</a>
- </div>
- </div>
- </div>
- </div>
- </div>
- </aside>
- <!-- 右侧主内容区 -->
- <main class="flex-1 flex flex-col h-full bg-[#f4f6f8] relative">
-
- <!-- 1. 极简面包屑 -->
- <div class="bg-white h-10 border-b border-gray-100 flex items-center px-6 flex-shrink-0 text-[13px] text-gray-500">
- <span>页面管理</span>
- <i class="ph ph-caret-right mx-1.5 text-xs"></i>
- <span class="text-gray-900" id="breadcrumb-title">权益介绍页配置</span>
- </div>
- <!-- 2. 独立操作工具栏 (悬浮吸顶) -->
- <div class="bg-white px-6 py-3 border-b border-gray-200 shadow-sm flex items-center justify-between flex-shrink-0 z-20 relative">
- <div class="flex items-center gap-4">
- <h1 class="text-[20px] font-bold text-gray-900 tracking-wide" id="toolbar-title">权益介绍页配置</h1>
- <!-- 全局状态指示器 -->
- <div id="global-status-indicator" class="hidden items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium border">
- <!-- 动态内容渲染 -->
- </div>
- </div>
- <div id="header-actions" class="flex items-center gap-3">
- <button onclick="showLinkModal()" class="bg-white border border-gray-300 text-gray-700 hover:bg-gray-50 px-4 py-2 rounded-md text-sm font-medium transition shadow-sm flex items-center gap-1.5">
- <i class="ph ph-link text-lg"></i> 获取链接
- </button>
- <div class="w-px h-5 bg-gray-300 mx-1"></div>
- <button onclick="handleSaveAction('draft')" class="bg-white border border-blue-600 text-blue-600 hover:bg-blue-50 px-4 py-2 rounded-md text-sm font-medium transition shadow-sm flex items-center gap-1.5">
- <i class="ph ph-floppy-disk text-lg"></i> 保存草稿
- </button>
- <button onclick="handleSaveAction('publish')" class="bg-blue-600 border border-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-md text-sm font-medium transition shadow-sm flex items-center gap-1.5">
- <i class="ph ph-paper-plane-tilt text-lg"></i> 发布上线
- </button>
- </div>
- </div>
- <div id="tier-tabs-container" class="bg-white px-6 pt-4 border-b border-gray-200 flex-shrink-0">
- <div class="flex gap-6" id="tier-tabs">
- <button onclick="switchTier('premier')" id="tab-premier" class="pb-3 px-2 border-b-2 border-blue-600 text-blue-600 font-medium transition">卓越理财</button>
- <button onclick="switchTier('elite')" id="tab-elite" class="pb-3 px-2 border-b-2 border-transparent text-gray-500 hover:text-gray-800 transition">卓越理财·尊尚</button>
- <button onclick="switchTier('global')" id="tab-global" class="pb-3 px-2 border-b-2 border-transparent text-gray-500 hover:text-gray-800 transition">环球私人银行</button>
- </div>
- </div>
- <div class="flex-1 overflow-y-auto p-6" id="form-container"></div>
- </main>
- <!-- 通用弹窗通知 -->
- <div id="toast" class="fixed top-20 right-10 bg-gray-900 text-white px-6 py-3 rounded shadow-lg transform transition-all duration-300 translate-x-full opacity-0 z-[100] flex items-center gap-2">
- <i class="ph ph-check-circle text-green-400 text-xl" id="toast-icon"></i>
- <span id="toast-text">配置已保存</span>
- </div>
- <!-- 获取链接弹窗 -->
- <div id="link-modal-backdrop" class="fixed inset-0 bg-black/50 z-[80] hidden transition-opacity" onclick="closeLinkModal()"></div>
- <div id="link-modal" class="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white rounded-xl shadow-2xl z-[90] w-[700px] hidden flex-col overflow-hidden">
- <div class="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50">
- <h3 class="text-lg font-bold text-gray-800 flex items-center gap-2"><i class="ph ph-link text-blue-600"></i> 获取页面链接</h3>
- <button onclick="closeLinkModal()" class="text-gray-400 hover:text-gray-600"><i class="ph ph-x text-xl"></i></button>
- </div>
- <div class="p-6" id="link-modal-content">
- <!-- 链接内容动态渲染 -->
- </div>
- </div>
- <!-- 富文本插入超链接弹窗 -->
- <div id="rt-link-modal-backdrop" class="fixed inset-0 bg-black/50 z-[110] hidden transition-opacity" onclick="closeRtLinkModal()"></div>
- <div id="rt-link-modal" class="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 bg-white rounded-xl shadow-2xl z-[120] w-[450px] hidden flex-col overflow-hidden">
- <div class="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50">
- <h3 class="text-base font-bold text-gray-800 flex items-center gap-2"><i class="ph ph-link text-blue-600"></i> 插入超链接</h3>
- <button onclick="closeRtLinkModal()" class="text-gray-400 hover:text-gray-600"><i class="ph ph-x text-xl"></i></button>
- </div>
- <div class="p-6 space-y-4">
- <div>
- <label class="block text-sm font-medium text-gray-700 mb-1">链接地址 (URL) <span class="text-red-500">*</span></label>
- <input type="text" id="rt-link-url" class="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:border-blue-500 outline-none" placeholder="https://">
- </div>
- <div>
- <label class="block text-sm font-medium text-gray-700 mb-1">展示文字</label>
- <input type="text" id="rt-link-text" class="w-full px-3 py-2 border border-gray-300 rounded text-sm focus:border-blue-500 outline-none" placeholder="例如:点击查看详情">
- </div>
- </div>
- <div class="px-6 py-4 bg-gray-50 border-t border-gray-100 flex justify-end gap-3">
- <button onclick="closeRtLinkModal()" class="px-4 py-2 border border-gray-300 text-gray-700 rounded text-sm hover:bg-gray-100 transition">取消</button>
- <button onclick="confirmRtLink()" class="px-4 py-2 bg-blue-600 text-white rounded text-sm hover:bg-blue-700 transition font-medium">确认插入</button>
- </div>
- </div>
- <script>
- // ==========================================
- // 核心数据模型与状态
- // ==========================================
- let currentMenu = 'home';
- let currentTier = 'premier';
- let currentLinkAssemblyId = null;
- let pageStatus = {
- home: 'published',
- zone: 'published'
- };
- const availableTags = Array.from({length: 8}, (_, i) => ({ id: `t${i+1}`, name: `人群标签${i+1}` }));
- // 1. 权益介绍页数据
- const homeData = {
- premier: createEmptyTierData('卓越理财'),
- elite: createEmptyTierData('卓越理财·尊尚'),
- global: createEmptyTierData('环球私人银行')
- };
- function createEmptyTierData(defaultTitle) {
- return {
- tabName: defaultTitle,
- buttons: { unlocked: '进入权益专区', locked: '待解锁' },
- header: { imageUrl: 'https://images.unsplash.com/photo-1518893494013-481c1d8ed3fd?w=800', mainTitle: defaultTitle, subTitle: '为您和您的家人提供尊崇专属礼遇和权益' },
- privileges: [
- { imageUrl: '', title: '国际教育', desc: '享专属国际化学校优惠及背景提升项目' },
- { imageUrl: '', title: '酒店礼遇', desc: '高端酒店及商场会籍匹配' }
- ]
- };
- }
- // 2. 权益首页数据 (原权益专区)
- const zoneData = {
- adCarousel: [{
- imageUrl: 'https://images.unsplash.com/photo-1556742049-0cfed4f6a45d?w=800',
- linkH5: '', linkApp: '', wechatAppId: '', wechatPath: '', title: '汇丰卓越理财新户达标礼', subtitle: '报名参加汇丰活动,体验更多精彩礼遇', tags: ['t1', 't2']
- }],
- exchangeConfig: {
- items: [
- { imageUrl: 'https://images.unsplash.com/photo-1436491865332-7a61a109cc05?w=200', title: '航空里程', link: '/flight' },
- { imageUrl: 'https://images.unsplash.com/photo-1518893494013-481c1d8ed3fd?w=200', title: '贵宾厅', link: '/lounge' }
- ]
- },
- ruleModal: { title: '积分规则', content: '<p>本积分使用规则为汇丰银行(中国)有限公司...</p>' }
- };
- // 3. 组装页数据
- let assemblyViewState = 'list';
- let editAssemblyIndex = null;
- let tempAssemblyData = null;
- function generateBlockId() { return 'b_' + Math.random().toString(36).substr(2, 9); }
- function createEmptyAssembly(name) {
- const now = new Date().toLocaleString('zh-CN', { hour12: false });
- return {
- id: 'A' + Math.floor(Math.random() * 10000),
- name: name || '', status: 'draft', createTime: now, updateTime: now,
- blocks: [
- { type: 'image', id: generateBlockId(), data: { imageUrl: '' } },
- { type: 'privilege', id: generateBlockId(), data: { title: '权益标题', items: [ { id: '1', title: '权益名称', desc: '权益简介' } ] } },
- { type: 'richtext', id: generateBlockId(), data: { content: '<p>组装页富文本区块内容...</p>' } },
- { type: 'buttonGroup', id: generateBlockId(), data: { buttons: [ { text: '点击配置', link: '', style: 'primary', actionType: 'link' } ] } }
- ]
- };
- }
- const assemblyList = [
- {
- id: 'A7538', name: '双十一迎新专属活动页', status: 'published', createTime: '2025-10-01 10:00:00', updateTime: '2025-10-15 14:30:00',
- blocks: [
- { type: 'image', id: generateBlockId(), data: { imageUrl: 'https://images.unsplash.com/photo-1557683316-973673baf926?w=800' } },
- { type: 'privilege', id: generateBlockId(), data: { title: '专属权益', items: [{ id: '1', title: '机场贵宾厅服务', desc: '全年免费使用6次指定机场贵宾厅' }, { id: '2', title: '境内礼宾车', desc: '免费2次接送' }] } },
- { type: 'richtext', id: generateBlockId(), data: { content: '<p>欢迎参与双十一专属福利活动...</p>' } },
- { type: 'buttonGroup', id: generateBlockId(), data: { buttons: [ { text: '立即参与', link: '/join', style: 'primary', actionType: 'link' } ] } }
- ]
- },
- {
- id: 'A2091', name: '私行高净值沙龙引导页', status: 'draft', createTime: '2025-11-20 09:15:00', updateTime: '2025-11-20 09:15:00',
- blocks: [
- { type: 'image', id: generateBlockId(), data: { imageUrl: 'https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?w=800' } },
- { type: 'privilege', id: generateBlockId(), data: { title: '尊享体验', items: [{ id: '1', title: '高尔夫权益', desc: 'VIP高尔夫俱乐部免会籍预约' }] } }
- ]
- }
- ];
- let openTagDropdownIndex = null;
- function getCurrentAssemblyData() { return tempAssemblyData; }
- // ==========================================
- // 渲染与切换
- // ==========================================
- function switchMenu(menuId) {
- currentMenu = menuId;
- const activeClass = 'py-2.5 pl-[72px] pr-5 text-blue-400 bg-[#2b3644] font-medium block transition';
- const inactiveClass = 'py-2.5 pl-[72px] pr-5 text-gray-400 hover:text-white block transition';
- document.getElementById('menu-home').className = (menuId === 'home') ? activeClass : inactiveClass;
- document.getElementById('menu-zone').className = (menuId === 'zone') ? activeClass : inactiveClass;
- document.getElementById('menu-assembly').className = (menuId === 'assembly') ? activeClass : inactiveClass;
-
- const titles = { 'home': '权益介绍页配置', 'zone': '权益首页配置', 'assembly': '组装页管理' };
- document.getElementById('breadcrumb-title').innerText = titles[menuId];
- document.getElementById('toolbar-title').innerText = titles[menuId];
- document.getElementById('tier-tabs-container').style.display = (menuId === 'home') ? 'block' : 'none';
- if (menuId === 'home') {
- document.getElementById('tab-premier').innerText = homeData.premier.tabName || '卓越理财';
- document.getElementById('tab-elite').innerText = homeData.elite.tabName || '卓越理财·尊尚';
- document.getElementById('tab-global').innerText = homeData.global.tabName || '环球私人银行';
- }
- if (menuId === 'assembly') assemblyViewState = 'list';
- openTagDropdownIndex = null;
-
- renderLayout();
- updateStatusIndicator();
- }
- function switchTier(tier) {
- currentTier = tier;
- const tabs = ['premier', 'elite', 'global'];
- tabs.forEach(t => {
- const el = document.getElementById(`tab-${t}`);
- el.className = (t === tier) ? "pb-3 px-2 border-b-2 border-blue-600 text-blue-600 font-medium transition" : "pb-3 px-2 border-b-2 border-transparent text-gray-500 hover:text-gray-800 transition";
- });
- renderLayout();
- }
- function renderLayout() {
- const actions = document.getElementById('header-actions');
- actions.style.display = (currentMenu === 'assembly' && assemblyViewState === 'list') ? 'none' : 'flex';
- if (currentMenu === 'home') renderHomeConfig();
- else if (currentMenu === 'zone') renderZoneConfig();
- else renderAssemblyController();
- updateStatusIndicator();
- }
- function markAsModified() {
- if (currentMenu === 'home') pageStatus.home = 'modified';
- if (currentMenu === 'zone') pageStatus.zone = 'modified';
- if (currentMenu === 'assembly' && assemblyViewState === 'edit' && tempAssemblyData) {
- tempAssemblyData._isModified = true;
- }
- updateStatusIndicator();
- }
- function updateStatusIndicator() {
- const indicator = document.getElementById('global-status-indicator');
-
- if (currentMenu === 'assembly' && assemblyViewState === 'list') {
- indicator.classList.add('hidden');
- return;
- }
- indicator.classList.remove('hidden', 'bg-green-50', 'text-green-700', 'border-green-200', 'bg-yellow-50', 'text-yellow-700', 'border-yellow-200', 'bg-gray-100', 'text-gray-600', 'border-gray-200');
- let currentStatus = '';
- if (currentMenu === 'home') currentStatus = pageStatus.home;
- else if (currentMenu === 'zone') currentStatus = pageStatus.zone;
- else if (currentMenu === 'assembly' && assemblyViewState === 'edit') {
- if (tempAssemblyData._isModified) {
- currentStatus = 'modified';
- } else {
- currentStatus = tempAssemblyData.status;
- }
- }
- if (currentStatus === 'published') {
- indicator.classList.add('bg-green-50', 'text-green-700', 'border-green-200');
- indicator.innerHTML = `<span class="w-1.5 h-1.5 rounded-full bg-green-500"></span> 正式线上环境`;
- } else if (currentStatus === 'modified') {
- indicator.classList.add('bg-yellow-50', 'text-yellow-700', 'border-yellow-200');
- indicator.innerHTML = `<span class="w-1.5 h-1.5 rounded-full bg-yellow-500"></span> 有未发布的修改`;
- } else {
- indicator.classList.add('bg-gray-100', 'text-gray-600', 'border-gray-200');
- indicator.innerHTML = `<span class="w-1.5 h-1.5 rounded-full bg-gray-400"></span> 草稿 (未发布)`;
- }
- }
- function handleSaveAction(type) {
- const pageName = (currentMenu === 'home') ? '权益介绍页' : (currentMenu === 'zone' ? '权益首页' : '组装页');
-
- if (currentMenu === 'home' || currentMenu === 'zone') {
- if (currentMenu === 'home') pageStatus.home = (type === 'publish') ? 'published' : 'draft';
- if (currentMenu === 'zone') pageStatus.zone = (type === 'publish') ? 'published' : 'draft';
- }
- if (currentMenu === 'assembly' && assemblyViewState === 'edit') {
- // 校验:如果按钮模块的点击动作是“跳转链接”,则链接不能为空
- let hasError = false;
- tempAssemblyData.blocks.forEach(block => {
- if (block.type === 'buttonGroup') {
- block.data.buttons.forEach(btn => {
- if (btn.actionType === 'link' && !btn.link.trim()) {
- hasError = true;
- }
- });
- }
- });
- if (hasError) {
- showToast('保存失败:按钮模块中存在未填写的跳转链接!', 'error');
- return;
- }
- if (!tempAssemblyData.name.trim()) tempAssemblyData.name = '未命名组装页';
- tempAssemblyData.status = (type === 'publish') ? 'published' : 'draft';
- tempAssemblyData.updateTime = new Date().toLocaleString('zh-CN', { hour12: false });
- delete tempAssemblyData._isModified;
- if (editAssemblyIndex === null) {
- assemblyList.unshift(tempAssemblyData);
- if (type === 'draft') editAssemblyIndex = 0;
- } else {
- assemblyList[editAssemblyIndex] = tempAssemblyData;
- }
-
- if (type === 'publish') {
- editAssemblyIndex = null;
- tempAssemblyData = null;
- assemblyViewState = 'list';
- }
-
- renderLayout();
- }
- showToast(type === 'publish' ? `已发布${pageName}配置!` : `已保存${pageName}配置草稿!`);
- updateStatusIndicator();
- }
- // ==========================================
- // 1. 权益介绍页渲染
- // ==========================================
- function renderHomeConfig() {
- const data = homeData[currentTier];
- // 兼容历史数据,确保字段存在
- if (!data.tabName) data.tabName = currentTier === 'premier' ? '卓越理财' : (currentTier === 'elite' ? '卓越理财·尊尚' : '环球私人银行');
- if (!data.buttons) data.buttons = { unlocked: '进入权益专区', locked: '待解锁' };
- document.getElementById('form-container').innerHTML = `
- <div class="max-w-4xl mx-auto space-y-8 pb-10">
-
- <section class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
- <div class="bg-gray-50 px-5 py-3 border-b border-gray-200 font-medium flex items-center gap-2"><i class="ph ph-folders text-gray-500 text-lg"></i> Tab名称配置</div>
- <div class="p-6">
- <label class="block text-sm font-medium text-gray-700 mb-1">Tab名称</label>
- <input type="text" value="${data.tabName}" oninput="homeData[currentTier].tabName=this.value; document.getElementById('tab-' + currentTier).innerText = this.value; markAsModified();" class="w-1/2 px-3 py-2 border rounded text-sm" placeholder="例如:卓越理财">
- </div>
- </section>
- <section class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
- <div class="bg-gray-50 px-5 py-3 border-b border-gray-200 font-medium flex items-center gap-2"><i class="ph ph-image text-gray-500 text-lg"></i> 头部配置</div>
- <div class="p-6 space-y-5">
- <div><label class="block text-sm font-medium mb-2">头图配图</label>
- <div class="flex items-center gap-4">
- <label class="w-[360px] h-[160px] bg-[#eef6fc] rounded border border-gray-300 relative group overflow-hidden cursor-pointer flex items-center justify-center shrink-0 shadow-sm">
- ${data.header.imageUrl ? `<img src="${data.header.imageUrl}" class="w-full h-full object-cover">` : `<div class="text-gray-400 flex flex-col items-center"><i class="ph ph-image text-3xl"></i><span class="text-xs mt-1">未上传图片</span></div>`}
- <div class="absolute inset-0 bg-black/50 hidden group-hover:flex flex-col items-center justify-center text-white"><i class="ph ph-upload-simple text-3xl mb-2"></i><span class="text-sm">点击上传本地图片</span></div>
- <input type="file" class="hidden" accept="image/*" onchange="uploadImageCallback(event, (url) => {homeData[currentTier].header.imageUrl=url; markAsModified(); renderLayout();})">
- </label>
- <div class="text-xs text-gray-500 bg-gray-50 p-2 rounded border border-gray-100">建议图片尺寸:<br><span class="font-bold text-gray-700">686 * 296 px</span></div>
- </div>
- </div>
- <div class="space-y-4 pt-2">
- <div><label class="block text-sm font-medium mb-1">主标题(粗体)</label><input type="text" value="${data.header.mainTitle}" oninput="homeData[currentTier].header.mainTitle=this.value; markAsModified();" class="w-full px-3 py-2 border rounded text-sm"></div>
- <div><label class="block text-sm font-medium mb-1">副标题(小字)</label><input type="text" value="${data.header.subTitle}" oninput="homeData[currentTier].header.subTitle=this.value; markAsModified();" class="w-full px-3 py-2 border rounded text-sm"></div>
- </div>
- </div>
- </section>
- <section class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
- <div class="bg-gray-50 px-5 py-3 border-b border-gray-200 font-medium flex items-center gap-2"><i class="ph ph-hand-tap text-gray-500 text-lg"></i> 按钮配置</div>
- <div class="p-6">
- <div class="grid grid-cols-2 gap-5">
- <div>
- <label class="block text-sm font-medium text-gray-700 mb-1">已解锁按钮文案</label>
- <input type="text" value="${data.buttons.unlocked}" oninput="homeData[currentTier].buttons.unlocked=this.value; markAsModified();" class="w-full px-3 py-2 border rounded text-sm" placeholder="例如:进入权益专区">
- </div>
- <div>
- <label class="block text-sm font-medium text-gray-700 mb-1">待解锁按钮文案</label>
- <input type="text" value="${data.buttons.locked}" oninput="homeData[currentTier].buttons.locked=this.value; markAsModified();" class="w-full px-3 py-2 border rounded text-sm" placeholder="例如:待解锁">
- </div>
- </div>
- </div>
- </section>
- <section class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
- <div class="bg-gray-50 px-5 py-3 border-b border-gray-200 font-medium flex justify-between items-center">
- <div class="flex items-center gap-2"><i class="ph ph-list-bullets text-gray-500 text-lg"></i> 权益列表配置</div>
- <button onclick="homeData[currentTier].privileges.push({ imageUrl: '', title: '新权益', desc: '描述...' }); markAsModified(); renderLayout();" class="text-blue-600 text-sm flex items-center gap-1 hover:text-blue-800"><i class="ph ph-plus-circle text-lg"></i> 添加权益</button>
- </div>
- <div class="p-6 space-y-4">
- ${data.privileges.map((item, index) => renderHomePrivilegeItem(item, index)).join('') || `<div class="text-center text-gray-400 py-6 border border-dashed rounded">暂无权益项</div>`}
- </div>
- </section>
- </div>`;
- }
- function renderHomePrivilegeItem(item, index) {
- return `
- <div class="flex gap-4 items-start p-5 pt-8 border border-gray-200 rounded-lg bg-white relative shadow-sm mt-2">
- <div class="absolute left-0 top-0 bg-blue-600 text-white text-xs font-bold px-3 py-1 rounded-tl-lg rounded-br-lg shadow-sm">NO. ${index + 1}</div>
- <div class="absolute right-4 top-4 flex items-center gap-2 z-20">
- ${renderSortPopover('homePrivilege', index, homeData[currentTier].privileges.length)}
- <button onclick="homeData[currentTier].privileges.splice(${index}, 1); markAsModified(); renderLayout();" class="text-red-500 hover:text-red-700 bg-red-50 hover:bg-red-100 p-1.5 rounded transition shadow-sm"><i class="ph ph-trash text-lg"></i></button>
- </div>
-
- <div class="flex flex-col items-center gap-1 shrink-0 mt-1">
- <div class="w-14 h-14 bg-[#eef6fc] rounded border border-gray-300 relative group overflow-hidden flex items-center justify-center cursor-pointer shadow-inner">
- ${item.imageUrl ? `<img src="${item.imageUrl}" class="w-full h-full object-cover">` : `<div class="text-gray-400 flex flex-col items-center"><i class="ph ph-image text-xl mb-0.5"></i><span class="text-[10px]">未上传</span></div>`}
- <label class="absolute inset-0 bg-black/50 hidden group-hover:flex flex-col items-center justify-center text-white cursor-pointer"><i class="ph ph-upload-simple text-lg"></i><input type="file" class="hidden" accept="image/*" onchange="uploadImageCallback(event, (url) => {homeData[currentTier].privileges[${index}].imageUrl = url; markAsModified(); renderLayout();})"></label>
- </div>
- <div class="text-[10px] text-gray-400 text-center leading-tight">非必填<br>无图展示圆点</div>
- </div>
- <div class="flex-1 grid grid-cols-1 md:grid-cols-3 gap-4 pr-24 mt-1">
- <div class="col-span-1">
- <label class="block text-xs text-gray-500 mb-1">标题</label>
- <input type="text" value="${item.title}" oninput="homeData[currentTier].privileges[${index}].title=this.value; markAsModified();" class="w-full px-3 py-1.5 border border-gray-300 rounded text-sm">
- </div>
- <div class="col-span-2">
- <label class="block text-xs text-gray-500 mb-1">权益文字描述</label>
- <input type="text" value="${item.desc}" oninput="homeData[currentTier].privileges[${index}].desc=this.value; markAsModified();" class="w-full px-3 py-1.5 border border-gray-300 rounded text-sm">
- </div>
- </div>
- </div>`;
- }
- // ==========================================
- // 2. 权益首页渲染 (Zone)
- // ==========================================
- function renderZoneConfig() {
- const container = document.getElementById('form-container');
- container.innerHTML = `
- <div class="max-w-4xl mx-auto space-y-8 pb-10">
- <section class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-visible mt-2">
- <div class="bg-gray-50 px-5 py-3 border-b font-medium flex justify-between items-center rounded-t-xl">
- <div class="flex items-center gap-2"><i class="ph ph-slideshow text-gray-500 text-lg"></i> 活动广告轮播图</div>
- <button onclick="zoneData.adCarousel.push({imageUrl:'',linkH5:'',linkApp:'',wechatAppId:'',wechatPath:'',title:'新广告',subtitle:'',tags:[]}); markAsModified(); renderLayout();" class="text-blue-600 text-sm flex items-center gap-1"><i class="ph ph-plus-circle text-lg"></i> 添加轮播</button>
- </div>
- <div class="p-6 space-y-6">${zoneData.adCarousel.map((item, index) => renderZoneCarouselItem(item, index)).join('') || `<div class="text-center text-gray-400 py-6 border border-dashed rounded">暂无广告轮播图</div>`}</div>
- </section>
- <section class="bg-white rounded-xl shadow-sm border border-gray-200">
- <div class="bg-gray-50 px-5 py-3 border-b font-medium flex justify-between items-center rounded-t-xl"><div class="flex items-center gap-2"><i class="ph ph-squares-four text-gray-500 text-lg"></i> 常用兑换配置</div><button onclick="zoneData.exchangeConfig.items.push({imageUrl:'',title:'新兑换项',link:''}); markAsModified(); renderLayout();" class="text-blue-600 text-sm flex items-center gap-1 hover:text-blue-800"><i class="ph ph-plus-circle text-lg"></i> 添加兑换项</button></div>
- <div class="p-6">
- <div class="space-y-4">${zoneData.exchangeConfig.items.map((item, index) => renderZoneExchangeItem(item, index)).join('') || `<div class="text-center text-gray-400 py-6 border border-dashed rounded">暂无常用兑换项</div>`}</div>
- </div>
- </section>
- <section class="bg-white rounded-xl shadow-sm border border-gray-200">
- <div class="bg-gray-50 px-5 py-3 border-b font-medium flex items-center rounded-t-xl"><i class="ph ph-article text-gray-500 text-lg mr-2"></i> 积分规则弹窗配置</div>
- <div class="p-6 space-y-4">
- <div><label class="block text-sm font-medium mb-1">弹窗顶部名称</label><input type="text" value="${zoneData.ruleModal.title}" oninput="zoneData.ruleModal.title=this.value; markAsModified();" class="w-1/2 px-3 py-2 border rounded text-sm"></div>
- <div>
- <label class="block text-sm font-medium mb-1">规则内容 (富文本编辑器)</label>
- <div class="border rounded shadow-sm flex flex-col">
- ${getRichTextToolbar('zone')}
- <textarea id="rt-zone" oninput="zoneData.ruleModal.content=this.value; markAsModified();" class="w-full px-4 py-3 text-sm h-48 focus:outline-none resize-y">${zoneData.ruleModal.content}</textarea>
- </div>
- </div>
- </div>
- </section>
- </div>`;
- }
- function renderZoneCarouselItem(item, index) {
- return `
- <div class="border rounded-xl p-5 pt-8 bg-white relative shadow-sm overflow-visible">
- <div class="absolute left-0 top-0 bg-blue-600 text-white text-xs font-bold px-3 py-1 rounded-tl-xl rounded-br-lg shadow-sm">NO. ${index + 1}</div>
- <div class="absolute right-5 top-5 flex items-center gap-2">
- ${renderSortPopover('zoneCarousel', index, zoneData.adCarousel.length)}
- <button onclick="zoneData.adCarousel.splice(${index},1); markAsModified(); renderLayout();" class="text-red-500 bg-red-50 p-1.5 rounded shadow-sm"><i class="ph ph-trash"></i></button>
- </div>
-
- <div class="flex flex-col gap-4 mt-3">
- <div class="flex items-center gap-4">
- <div class="w-64 h-36 bg-[#eef6fc] rounded-md relative group overflow-hidden border flex items-center justify-center shrink-0">
- ${item.imageUrl ? `<img src="${item.imageUrl}" class="w-full h-full object-cover">` : `<div class="text-gray-400 flex flex-col items-center"><i class="ph ph-image text-3xl mb-1"></i><span class="text-xs">未上传</span></div>`}
- <label class="absolute inset-0 bg-black/50 hidden group-hover:flex flex-col items-center justify-center text-white cursor-pointer"><i class="ph ph-upload-simple text-3xl mb-1"></i><span class="text-sm">点击上传</span><input type="file" class="hidden" accept="image/*" onchange="uploadImageCallback(event, (url) => {zoneData.adCarousel[${index}].imageUrl=url; markAsModified(); renderLayout();})"></label>
- </div>
- <div class="text-xs text-gray-500 bg-gray-50 p-2 rounded border border-gray-100">建议图片尺寸:<br><span class="font-bold text-gray-700">686 * 296 px</span></div>
- </div>
- <div class="space-y-3">
- <div><label class="block text-xs text-gray-600 mb-1">主标题</label><input type="text" value="${item.title}" oninput="zoneData.adCarousel[${index}].title=this.value; markAsModified();" class="w-full px-3 py-1.5 border rounded text-sm"></div>
- <div><label class="block text-xs text-gray-600 mb-1">副标题</label><input type="text" value="${item.subtitle}" oninput="zoneData.adCarousel[${index}].subtitle=this.value; markAsModified();" class="w-full px-3 py-1.5 border rounded text-sm"></div>
- </div>
- <div class="tag-dropdown-container relative">
- <label class="block text-xs text-gray-600 mb-1">可见人群标签 (多选)</label>
- <div class="min-h-[38px] p-1.5 border border-blue-300 bg-blue-50 rounded flex flex-wrap gap-1.5 cursor-pointer" onclick="toggleTagDropdown(event, ${index})">
- ${(item.tags || []).map(tid => `<div class="bg-blue-600 text-white text-[12px] px-2 py-0.5 rounded flex items-center gap-1">${availableTags.find(t=>t.id===tid)?.name || ''}<i class="ph ph-x" onclick="event.stopPropagation(); zoneData.adCarousel[${index}].tags = zoneData.adCarousel[${index}].tags.filter(id=>id!=='${tid}'); markAsModified(); renderLayout();"></i></div>`).join('')}
- <div class="text-gray-400 text-sm">${(!item.tags || item.tags.length === 0) ? '点击选择...' : ''}</div>
- </div>
- <div id="tag-dropdown-${index}" class="${openTagDropdownIndex === index ? 'block' : 'hidden'} absolute top-full left-0 w-full mt-1 bg-white border rounded shadow-lg z-50 overflow-hidden">
- <div class="p-2 bg-gray-50 border-b"><input type="text" placeholder="搜索..." class="w-full px-2 py-1 text-sm border rounded outline-none" onkeyup="filterTags(event, ${index})" onclick="event.stopPropagation()"></div>
- <div class="max-h-48 overflow-y-auto p-1" id="tag-list-${index}">${availableTags.map(tag => `<label class="flex items-center gap-2 px-3 py-2 hover:bg-blue-50 cursor-pointer text-sm"><input type="checkbox" ${(item.tags||[]).includes(tag.id)?'checked':''} onclick="event.stopPropagation(); const ts = zoneData.adCarousel[${index}].tags || []; if(this.checked) ts.push('${tag.id}'); else zoneData.adCarousel[${index}].tags=ts.filter(id=>id!=='${tag.id}'); zoneData.adCarousel[${index}].tags = ts; markAsModified(); renderLayout();"><span>${tag.name}</span></label>`).join('')}</div>
- </div>
- </div>
- <div class="bg-gray-50 p-3 rounded-lg border border-gray-200 space-y-3 mt-2">
- <div class="text-xs font-bold text-gray-700 border-b border-gray-200 pb-2"><i class="ph ph-link"></i> 跳转链接配置</div>
- <div><label class="block text-xs text-gray-600 mb-1">H5 跳转链接</label><input type="text" value="${item.linkH5 || ''}" oninput="zoneData.adCarousel[${index}].linkH5=this.value; markAsModified();" class="w-full px-3 py-1.5 border rounded text-sm"></div>
- <div><label class="block text-xs text-gray-600 mb-1">APP 跳转链接</label><input type="text" value="${item.linkApp || ''}" oninput="zoneData.adCarousel[${index}].linkApp=this.value; markAsModified();" class="w-full px-3 py-1.5 border rounded text-sm"></div>
- <div class="grid grid-cols-2 gap-3">
- <div><label class="block text-xs text-gray-600 mb-1">微信小程序 AppID</label><input type="text" value="${item.wechatAppId || ''}" oninput="zoneData.adCarousel[${index}].wechatAppId=this.value; markAsModified();" class="w-full px-3 py-1.5 border rounded text-sm" placeholder="例如:wx1234567890abcdef"></div>
- <div><label class="block text-xs text-gray-600 mb-1">微信小程序 页面路径</label><input type="text" value="${item.wechatPath || ''}" oninput="zoneData.adCarousel[${index}].wechatPath=this.value; markAsModified();" class="w-full px-3 py-1.5 border rounded text-sm" placeholder="例如:pages/index/index"></div>
- </div>
- </div>
- </div>
- </div>`;
- }
- function renderZoneExchangeItem(item, index) {
- return `<div class="flex gap-4 items-center p-4 pt-6 border rounded-lg bg-white relative shadow-sm mt-3"><div class="absolute left-0 top-0 bg-blue-600 text-white text-xs font-bold px-3 py-1 rounded-tl-lg rounded-br-lg">NO. ${index + 1}</div><div class="absolute right-4 top-4 flex items-center gap-2">${renderSortPopover('zoneExchange', index, zoneData.exchangeConfig.items.length)}<button onclick="zoneData.exchangeConfig.items.splice(${index},1); markAsModified(); renderLayout();" class="text-red-500 bg-red-50 p-1.5 rounded"><i class="ph ph-trash"></i></button></div><div class="w-20 h-20 bg-[#eef6fc] rounded border relative group flex items-center justify-center shrink-0 overflow-hidden shadow-inner">${item.imageUrl ? `<img src="${item.imageUrl}" class="w-full h-full object-cover">` : `<div class="text-gray-400 flex flex-col items-center"><i class="ph ph-image text-xl mb-0.5"></i><span class="text-[10px]">未上传</span></div>`}<label class="absolute inset-0 bg-black/50 hidden group-hover:flex flex-col items-center justify-center text-white cursor-pointer"><i class="ph ph-upload-simple text-xl"></i><input type="file" class="hidden" accept="image/*" onchange="uploadImageCallback(event, (url)=>{zoneData.exchangeConfig.items[${index}].imageUrl=url; markAsModified(); renderLayout();})"></label></div><div class="flex-1 grid grid-cols-2 gap-4 mt-1 pr-16"><div><label class="block text-xs text-gray-600 mb-1">展示文字</label><input type="text" value="${item.title}" oninput="zoneData.exchangeConfig.items[${index}].title=this.value; markAsModified();" class="w-full px-3 py-2 border rounded text-sm"></div><div><label class="block text-xs text-gray-600 mb-1">跳转链接 (URL)</label><input type="text" value="${item.link}" oninput="zoneData.exchangeConfig.items[${index}].link=this.value; markAsModified();" class="w-full px-3 py-2 border rounded text-sm"></div></div></div>`;
- }
- // ==========================================
- // 3. 组装页渲染逻辑
- // ==========================================
- function renderAssemblyController() {
- if (assemblyViewState === 'list') renderAssemblyList();
- else renderAssemblyEdit();
- }
- function renderAssemblyList() {
- const container = document.getElementById('form-container');
- let tableRows = assemblyList.map((item, index) => `
- <tr class="border-b hover:bg-gray-50 transition">
- <td class="px-5 py-3 text-sm border-r flex gap-3 whitespace-nowrap"><button onclick="editAssembly(${index})" class="text-blue-600 font-medium">编辑</button><button onclick="showLinkModal(null, ${index})" class="text-indigo-600 font-medium">链接</button><button onclick="deleteAssembly(${index})" class="text-red-500 font-medium">删除</button></td>
- <td class="border px-5 py-3 text-sm text-gray-600 text-center">${item.id}</td>
- <td class="border px-5 py-3 text-sm font-medium text-gray-900">${item.name || '未命名'}</td>
- <td class="border px-5 py-3 text-center"><span class="px-2 py-1 text-xs rounded font-medium ${item.status === 'published' ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-600'}">${item.status === 'published' ? '已上架' : '草稿'}</span></td>
- <td class="border px-5 py-3 text-sm text-gray-500 text-center whitespace-nowrap">${item.createTime}</td>
- <td class="border px-5 py-3 text-sm text-gray-500 text-center whitespace-nowrap">${item.updateTime}</td>
- </tr>`).join('');
- container.innerHTML = `
- <div class="w-full space-y-6">
- <div class="bg-white p-5 rounded shadow-sm border mb-4 mt-2">
- <div class="grid grid-cols-2 gap-6 mb-5 w-2/3"><div class="flex items-center gap-3"><label class="text-sm font-medium w-16 text-right">页面ID</label><input type="text" class="flex-1 px-3 py-1.5 border rounded text-sm focus:outline-none focus:border-blue-500"></div><div class="flex items-center gap-3"><label class="text-sm font-medium w-16 text-right">页面名称</label><input type="text" class="flex-1 px-3 py-1.5 border rounded text-sm focus:outline-none focus:border-blue-500"></div></div>
- <div class="flex justify-between items-center border-t pt-4 mt-2"><button onclick="handleCreateNewAssembly()" class="bg-blue-600 text-white px-4 py-1.5 rounded text-sm flex items-center gap-1.5 font-medium hover:bg-blue-700"><i class="ph ph-plus text-lg"></i> 新建组装页</button><div class="flex gap-3"><button class="bg-indigo-500 text-white px-5 py-1.5 rounded text-sm font-medium hover:bg-indigo-600">查询</button><button class="bg-white border text-gray-700 px-5 py-1.5 rounded text-sm hover:bg-gray-50">重置</button></div></div>
- </div>
- <div class="bg-white rounded shadow-sm border overflow-hidden"><table class="w-full border-collapse"><thead><tr class="bg-gray-50 text-gray-600 text-sm"><th class="border px-5 py-3 font-medium text-left w-48">操作</th><th class="border px-5 py-3 font-medium">页面ID</th><th class="border px-5 py-3 font-medium text-left">组装页名称</th><th class="border px-5 py-3 font-medium">发布状态</th><th class="border px-5 py-3 font-medium">创建时间</th><th class="border px-5 py-3 font-medium">更新时间</th></tr></thead><tbody>${tableRows || '<tr><td colspan="6" class="p-10 text-center text-gray-400">暂无数据</td></tr>'}</tbody></table></div>
- </div>`;
- }
- function renderAssemblyEdit() {
- const data = getCurrentAssemblyData();
- let html = `
- <div class="max-w-4xl mx-auto space-y-6 pb-20 mt-2">
- <div class="flex items-center justify-between border-b pb-4"><div class="flex items-center gap-4"><button onclick="backToAssemblyList()" class="text-gray-600 hover:text-blue-600 flex items-center gap-1 font-medium"><i class="ph ph-arrow-left text-lg"></i> 返回</button><h2 class="text-xl font-bold">${editAssemblyIndex === null ? '新建组装页' : '编辑组装页'}</h2></div></div>
- <section class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 flex gap-6 items-center">
- <div class="shrink-0 flex items-center gap-2 h-[38px]">
- <span class="text-sm font-medium text-gray-500">页面 ID:</span>
- <span class="text-sm font-bold text-gray-800 bg-gray-100 px-3 py-1 rounded border border-gray-200 leading-none flex items-center">${data.id}</span>
- </div>
- <div class="flex-1 flex items-center gap-3 border-l border-gray-200 pl-6 h-[38px]">
- <label class="text-sm font-medium text-gray-700 shrink-0">组装页名称<span class="text-red-500 ml-0.5">*</span></label>
- <input type="text" value="${data.name}" oninput="updateAssemblyName(this.value)" class="flex-1 px-3 py-1.5 border border-gray-300 rounded focus:outline-none focus:ring-1 focus:ring-blue-500 text-sm font-medium h-[34px]" placeholder="请为该页面设置一个名称 (必填)">
- </div>
- </section>
- `;
- const hasPrivilege = data.blocks.some(b => b.type === 'privilege');
- data.blocks.forEach((block, index) => {
- html += `<section class="bg-white rounded-xl shadow-sm border overflow-hidden relative mb-6">
- <div class="absolute right-0 top-0 flex items-center z-10 bg-gray-50 border-b border-l rounded-bl-lg shadow-sm">
- ${renderSortPopover('assemblyBlock', index, data.blocks.length)}`;
-
- if (block.type !== 'privilege') {
- html += `<div class="w-px h-4 bg-gray-300"></div><button onclick="duplicateAssemblyBlock(${index})" class="p-1.5 px-3 text-blue-600 hover:bg-blue-100 text-sm font-medium transition" title="复制模块"><i class="ph ph-copy mr-1"></i>复制</button>`;
- }
- html += `<div class="w-px h-4 bg-gray-300"></div><button onclick="removeAssemblyBlock(${index})" class="p-1.5 px-3 text-red-500 hover:bg-red-100 text-sm font-medium rounded-bl-lg transition" title="删除模块"><i class="ph ph-trash mr-1"></i>删除</button></div>`;
- if (block.type === 'image') {
- html += `<div class="bg-gray-50 px-5 py-3 border-b font-medium flex items-center gap-2"><i class="ph ph-image text-gray-500 text-lg"></i> 图片模块</div>
- <div class="p-6">
- <div class="flex gap-4 items-center">
- <label class="w-[360px] h-[160px] bg-[#eef6fc] rounded border relative group overflow-hidden cursor-pointer flex items-center justify-center shrink-0 shadow-sm">
- ${block.data.imageUrl ? `<img src="${block.data.imageUrl}" class="w-full h-full object-cover">` : `<div class="text-gray-400 flex flex-col items-center"><i class="ph ph-image text-3xl mb-1"></i><span class="text-xs">未上传图片</span></div>`}
- <div class="absolute inset-0 bg-black/50 hidden group-hover:flex flex-col items-center justify-center text-white"><i class="ph ph-upload-simple text-3xl mb-2"></i><span class="text-sm font-medium">点击上传图片</span></div>
- <input type="file" class="hidden" accept="image/*" onchange="uploadImageCallback(event, (url)=>{tempAssemblyData.blocks[${index}].data.imageUrl=url; markAsModified(); renderLayout();})">
- </label>
- <div class="text-xs text-gray-500 space-y-1 bg-gray-50 p-3 rounded border"><p class="font-medium text-gray-700">建议宽度:750px</p><p>高度不限,系统将按比例自适应展示</p></div>
- </div>
- </div>`;
- }
- else if (block.type === 'privilege') {
- html += `<div class="bg-gray-50 px-5 py-3 border-b font-medium flex items-center gap-2"><i class="ph ph-list-dashes text-gray-500 text-lg"></i> 权益商品模块 <span class="text-xs text-gray-400 ml-2 font-normal">(当前页面仅限添加一个)</span></div>
- <div class="p-6">
- <div class="mb-5 border-b pb-5"><label class="block text-sm font-medium mb-2">可配置大标题</label><input type="text" value="${block.data.title}" oninput="tempAssemblyData.blocks[${index}].data.title=this.value; markAsModified();" class="w-1/2 px-3 py-2 border rounded text-sm"></div>
- <div class="space-y-3">${block.data.items.map((item, idx) => `<div class="flex items-center gap-4 p-3 border bg-gray-50 rounded-lg relative group"><div class="bg-white border rounded text-xs font-bold text-gray-500 px-2 py-1">NO. ${idx + 1}</div><div class="w-10 h-10 rounded-full border bg-white flex items-center justify-center text-gray-500 shrink-0"><i class="ph ph-file-text text-xl"></i></div><div class="flex-1 opacity-80 pointer-events-none"><div class="text-sm font-medium text-gray-900">${item.title}</div><div class="text-xs text-gray-500 mt-0.5 truncate">${item.desc}</div></div><div class="pr-2">${renderSortPopover(`assemblyPrivilege-${index}`, idx, block.data.items.length)}</div></div>`).join('')}</div>
- </div>`;
- }
- else if (block.type === 'richtext') {
- html += `<div class="bg-gray-50 px-5 py-3 border-b font-medium flex items-center gap-2"><i class="ph ph-text-align-left text-gray-500 text-lg"></i> 图文模块</div>
- <div class="p-6">
- <label class="block text-sm font-medium mb-1">图文内容 (富文本编辑器)</label>
- <div class="border rounded flex flex-col shadow-sm">
- ${getRichTextToolbar('assembly', index)}
- <textarea id="rt-assembly-${index}" oninput="tempAssemblyData.blocks[${index}].data.content=this.value; markAsModified();" class="w-full px-4 py-3 text-sm h-32 focus:outline-none resize-y">${block.data.content}</textarea>
- </div>
- </div>`;
- }
- else if (block.type === 'buttonGroup') {
- html += `<div class="bg-gray-50 px-5 py-3 border-b font-medium flex items-center gap-4"><div class="flex items-center gap-2"><i class="ph ph-hand-tap text-gray-500 text-lg"></i> 按钮模块</div><button onclick="addAssemblyButton(${index})" class="text-blue-600 bg-blue-50 hover:bg-blue-100 px-2 py-1 rounded text-sm flex items-center gap-1 transition"><i class="ph ph-plus-circle text-lg"></i> 添加按钮</button></div>
- <div class="p-6 space-y-4">`;
-
- block.data.buttons.forEach((btn, idx) => {
- const actionType = btn.actionType || 'link';
- html += `<div class="flex gap-4 items-start p-4 border rounded-lg bg-gray-50 relative">
- <div class="absolute left-0 top-0 bg-gray-600 text-white text-[10px] font-bold px-2 py-0.5 rounded-tl-lg rounded-br-lg">BTN ${idx + 1}</div>
- <div class="flex-1 pt-2 space-y-4">
- <div class="grid grid-cols-3 gap-4">
- <div><label class="block text-xs mb-1">按钮文案</label><input type="text" value="${btn.text}" oninput="tempAssemblyData.blocks[${index}].data.buttons[${idx}].text=this.value; markAsModified();" class="w-full px-3 py-1.5 border rounded text-sm"></div>
- <div><label class="block text-xs mb-1">点击动作</label>
- <select onchange="tempAssemblyData.blocks[${index}].data.buttons[${idx}].actionType=this.value; markAsModified(); renderLayout();" class="w-full px-3 py-1.5 border rounded text-sm bg-white">
- <option value="link" ${actionType === 'link' ? 'selected' : ''}>跳转链接</option>
- <option value="dialog" ${actionType === 'dialog' ? 'selected' : ''}>弹窗提示</option>
- </select>
- </div>
- <div><label class="block text-xs mb-1">按钮样式</label><select onchange="tempAssemblyData.blocks[${index}].data.buttons[${idx}].style=this.value; markAsModified();" class="w-full px-3 py-1.5 border rounded text-sm bg-white"><option value="primary" ${btn.style === 'primary' ? 'selected' : ''}>突出显示 (红色)</option><option value="secondary" ${btn.style === 'secondary' ? 'selected' : ''}>次要操作 (白色)</option></select></div>
- </div>`;
-
- if (actionType === 'link') {
- html += `<div><label class="block text-xs mb-1">跳转链接 <span class="text-red-500">*</span></label><input type="text" value="${btn.link || ''}" oninput="tempAssemblyData.blocks[${index}].data.buttons[${idx}].link=this.value; markAsModified();" class="w-full px-3 py-1.5 border rounded text-sm" placeholder="必填,输入跳转URL"></div>`;
- } else {
- const d = btn.dialog || { title: '', desc: '', confirmText: '确认', confirmLink: '' };
- html += `<div class="bg-white p-4 border border-gray-200 rounded text-sm space-y-3 shadow-sm">
- <div class="font-medium text-gray-800 mb-2 border-b border-gray-100 pb-2"><i class="ph ph-browser"></i> 弹窗内容配置 (包含取消、确认按钮)</div>
- <div><label class="block text-xs text-gray-600 mb-1">弹窗标题</label><input type="text" value="${d.title}" oninput="updateAssemblyButtonDialog(${index}, ${idx}, 'title', this.value)" class="w-full px-3 py-1.5 border border-gray-300 rounded text-sm focus:border-blue-500 outline-none" placeholder="弹窗标题"></div>
- <div><label class="block text-xs text-gray-600 mb-1">弹窗描述</label><textarea oninput="updateAssemblyButtonDialog(${index}, ${idx}, 'desc', this.value)" class="w-full px-3 py-1.5 border border-gray-300 rounded text-sm h-16 focus:border-blue-500 outline-none" placeholder="弹窗正文提示...">${d.desc}</textarea></div>
- <div class="grid grid-cols-2 gap-4">
- <div><label class="block text-xs text-gray-600 mb-1">右侧确认按钮文案</label><input type="text" value="${d.confirmText}" oninput="updateAssemblyButtonDialog(${index}, ${idx}, 'confirmText', this.value)" class="w-full px-3 py-1.5 border border-gray-300 rounded text-sm focus:border-blue-500 outline-none" placeholder="例如:继续前往"></div>
- <div><label class="block text-xs text-gray-600 mb-1">确认按钮跳转链接</label><input type="text" value="${d.confirmLink}" oninput="updateAssemblyButtonDialog(${index}, ${idx}, 'confirmLink', this.value)" class="w-full px-3 py-1.5 border border-gray-300 rounded text-sm focus:border-blue-500 outline-none" placeholder="选填,无链接则仅关闭弹窗"></div>
- </div>
- </div>`;
- }
-
- html += `</div>
- <div class="flex items-center gap-2 shrink-0 pb-0.5 pt-2">
- ${renderSortPopover(`assemblyButton-${index}`, idx, block.data.buttons.length)}
- <button onclick="tempAssemblyData.blocks[${index}].data.buttons.splice(${idx}, 1); markAsModified(); renderLayout();" class="text-red-500 bg-red-100 p-1.5 rounded shadow-sm"><i class="ph ph-trash text-lg"></i></button>
- </div>
- </div>`;
- });
- if(block.data.buttons.length === 0) {
- html += `<div class="text-center text-gray-400 py-4 border border-dashed rounded">暂无按钮</div>`;
- }
- html += `</div>`;
- }
- html += `</section>`;
- });
- html += `
- <div class="flex gap-4 justify-center py-6 border-t border-gray-200 mt-8 border-dashed border-2 rounded-xl bg-white shadow-sm">
- <button onclick="addAssemblyBlock('image')" class="flex items-center gap-1.5 text-blue-600 font-medium hover:bg-blue-50 px-4 py-2 rounded transition"><i class="ph ph-image text-lg"></i>图片模块</button>
- ${!hasPrivilege ? `<button onclick="addAssemblyBlock('privilege')" class="flex items-center gap-1.5 text-blue-600 font-medium hover:bg-blue-50 px-4 py-2 rounded transition"><i class="ph ph-list-dashes text-lg"></i>权益商品模块</button>` : ''}
- <button onclick="addAssemblyBlock('richtext')" class="flex items-center gap-1.5 text-blue-600 font-medium hover:bg-blue-50 px-4 py-2 rounded transition"><i class="ph ph-text-align-left text-lg"></i>图文模块</button>
- <button onclick="addAssemblyBlock('buttonGroup')" class="flex items-center gap-1.5 text-blue-600 font-medium hover:bg-blue-50 px-4 py-2 rounded transition"><i class="ph ph-hand-tap text-lg"></i>按钮模块</button>
- </div>
- </div>`;
- document.getElementById('form-container').innerHTML = html;
- }
- // ==========================================
- // 富文本功能 & 工具函数 (排序、上传、Toast)
- // ==========================================
-
- let rtTarget = { type: null, index: null };
- function getRichTextToolbar(type, index = null) {
- return `
- <div class="bg-gray-50 border-b border-gray-300 px-3 py-2 flex gap-1 flex-wrap text-gray-400 items-center">
- <button type="button" class="p-1.5 rounded cursor-not-allowed hover:bg-gray-200" title="字体大小"><i class="ph ph-text-t"></i></button>
- <button type="button" class="p-1.5 rounded cursor-not-allowed hover:bg-gray-200" title="字体颜色"><i class="ph ph-text-a-under"></i></button>
- <div class="w-px h-5 bg-gray-300 mx-1"></div>
- <button type="button" class="p-1.5 rounded cursor-not-allowed hover:bg-gray-200" title="加粗"><i class="ph ph-text-b"></i></button>
- <button type="button" class="p-1.5 rounded cursor-not-allowed hover:bg-gray-200" title="斜体"><i class="ph ph-text-italic"></i></button>
- <button type="button" class="p-1.5 rounded cursor-not-allowed hover:bg-gray-200" title="下划线"><i class="ph ph-text-underline"></i></button>
- <div class="w-px h-5 bg-gray-300 mx-1"></div>
- <button type="button" class="p-1.5 rounded cursor-not-allowed hover:bg-gray-200" title="左对齐"><i class="ph ph-text-align-left"></i></button>
- <button type="button" class="p-1.5 rounded cursor-not-allowed hover:bg-gray-200" title="居中对齐"><i class="ph ph-text-align-center"></i></button>
- <button type="button" class="p-1.5 rounded cursor-not-allowed hover:bg-gray-200" title="右对齐"><i class="ph ph-text-align-right"></i></button>
- <div class="w-px h-5 bg-gray-300 mx-1"></div>
- <button type="button" class="p-1.5 rounded cursor-not-allowed hover:bg-gray-200" title="无序列表"><i class="ph ph-list-bullets"></i></button>
- <button type="button" class="p-1.5 rounded cursor-not-allowed hover:bg-gray-200" title="有序列表"><i class="ph ph-list-numbers"></i></button>
- <div class="w-px h-5 bg-gray-300 mx-1"></div>
-
- <!-- 实际可用功能 -->
- <button type="button" onclick="openRtLinkModal('${type}', ${index})" class="p-1.5 rounded hover:bg-gray-200 text-blue-600 transition font-medium" title="插入超链接"><i class="ph ph-link"></i> 链接</button>
- <label class="p-1.5 rounded hover:bg-gray-200 cursor-pointer text-blue-600 transition font-medium flex items-center gap-1" title="插入图片">
- <i class="ph ph-image"></i> 图片
- <input type="file" class="hidden" accept="image/*" onchange="handleRichTextImage(event, '${type}', ${index})">
- </label>
- <label class="p-1.5 rounded hover:bg-gray-200 cursor-pointer text-blue-600 transition font-medium flex items-center gap-1" title="插入附件文件">
- <i class="ph ph-paperclip"></i> 附件
- <input type="file" class="hidden" onchange="handleRichTextFile(event, '${type}', ${index})">
- </label>
- </div>
- `;
- }
- function openRtLinkModal(type, index) {
- rtTarget = { type, index };
- document.getElementById('rt-link-url').value = '';
- document.getElementById('rt-link-text').value = '';
- document.getElementById('rt-link-modal-backdrop').classList.remove('hidden');
- document.getElementById('rt-link-modal').classList.remove('hidden');
- document.getElementById('rt-link-modal').classList.add('flex');
- }
- function closeRtLinkModal() {
- document.getElementById('rt-link-modal-backdrop').classList.add('hidden');
- document.getElementById('rt-link-modal').classList.add('hidden');
- document.getElementById('rt-link-modal').classList.remove('flex');
- }
- function confirmRtLink() {
- const url = document.getElementById('rt-link-url').value.trim();
- const text = document.getElementById('rt-link-text').value.trim() || '点击查看';
- if(!url) { showToast('请输入链接地址', 'error'); return; }
-
- const html = `<a href="${url}" target="_blank" style="color:#2563eb; text-decoration:underline;">${text}</a>`;
- appendToRichText(html);
- closeRtLinkModal();
- }
- function handleRichTextImage(event, type, index) {
- const file = event.target.files[0];
- if (!file) return;
- const html = `\n<img src="[图片: ${file.name}]" style="max-width:100%; border-radius:4px; margin-top:8px;" />\n`;
- rtTarget = { type, index };
- appendToRichText(html);
- event.target.value = '';
- }
-
- function handleRichTextFile(event, type, index) {
- const file = event.target.files[0];
- if (!file) return;
- const html = `\n<a href="[附件: ${file.name}]" target="_blank" style="color:#2563eb; text-decoration:underline; display:inline-block; margin-top:8px;">📎 ${file.name}</a>\n`;
- rtTarget = { type, index };
- appendToRichText(html);
- event.target.value = '';
- }
- function appendToRichText(html) {
- if (rtTarget.type === 'zone') {
- zoneData.ruleModal.content += html;
- } else if (rtTarget.type === 'assembly') {
- tempAssemblyData.blocks[rtTarget.index].data.content += html;
- }
- markAsModified();
- renderLayout();
- }
- function renderSortPopover(type, index, maxCount) {
- return `<div class="relative inline-block"><button onclick="toggleSortPopover('${type}', ${index}, event)" class="text-blue-500 bg-blue-50 p-1.5 rounded shadow-sm"><i class="ph ph-list-numbers"></i></button>
- <div id="popover-${type}-${index}" class="hidden absolute right-0 top-full mt-2 z-50 w-48 bg-white border rounded-lg shadow-xl p-3 text-left">
- <label class="block text-xs text-gray-700 mb-2">移动到序号:</label>
- <div class="flex gap-2"><input type="number" id="sort-input-${type}-${index}" min="1" max="${maxCount}" value="${index + 1}" class="w-full px-2 py-1 border rounded text-sm outline-none"><button onclick="confirmSort('${type}', ${index})" class="bg-blue-600 text-white px-3 py-1 rounded text-sm">确定</button></div>
- </div></div>`;
- }
- function toggleSortPopover(type, index, e) {
- e.stopPropagation();
- const pid = `popover-${type}-${index}`;
- document.querySelectorAll('[id^="popover-"]').forEach(el => { if(el.id !== pid) el.classList.add('hidden'); });
- const p = document.getElementById(pid);
- p.classList.toggle('hidden');
- if(!p.classList.contains('hidden')) setTimeout(()=>document.getElementById(`sort-input-${type}-${index}`).select(), 50);
- }
- function confirmSort(type, oldIdx) {
- const val = parseInt(document.getElementById(`sort-input-${type}-${oldIdx}`).value) - 1;
- let arr;
- if (type === 'homePrivilege') arr = homeData[currentTier].privileges;
- else if (type === 'zoneCarousel') arr = zoneData.adCarousel;
- else if (type === 'zoneExchange') arr = zoneData.exchangeConfig.items;
- else if (type === 'echoPrivileges') arr = zoneData.echoPrivileges;
- else if (type === 'echoCreditCards') arr = zoneData.echoCreditCards;
- else if (type === 'assemblyBlock') arr = getCurrentAssemblyData().blocks;
- else if (type.startsWith('assemblyPrivilege-')) arr = getCurrentAssemblyData().blocks[parseInt(type.split('-')[1])].data.items;
- else if (type.startsWith('assemblyButton-')) arr = getCurrentAssemblyData().blocks[parseInt(type.split('-')[1])].data.buttons;
- if (arr && val >= 0 && val < arr.length && val !== oldIdx) {
- const item = arr.splice(oldIdx, 1)[0];
- arr.splice(val, 0, item);
- markAsModified();
- }
- renderLayout();
- }
- function uploadImageCallback(event, updateFn) {
- const file = event.target.files[0];
- if (file) { const r = new FileReader(); r.onload = e => updateFn(e.target.result); r.readAsDataURL(file); }
- }
- function showToast(msg, type = 'success') {
- const t = document.getElementById('toast');
- document.getElementById('toast-text').innerText = msg;
-
- const icon = document.getElementById('toast-icon');
- if(type === 'error') {
- icon.className = 'ph ph-x-circle text-red-500 text-xl';
- } else {
- icon.className = 'ph ph-check-circle text-green-400 text-xl';
- }
- t.classList.remove('translate-x-full', 'opacity-0');
- setTimeout(() => t.classList.add('translate-x-full', 'opacity-0'), 3000);
- }
- // ==========================================
- // 链接弹窗动态处理
- // ==========================================
- function showLinkModal(menuType = null, listIndex = null) {
-
- const targetMenu = menuType || currentMenu;
-
- if (targetMenu === 'home') {
- currentLinkAssemblyId = 'home';
- } else if (targetMenu === 'zone') {
- currentLinkAssemblyId = 'zone';
- } else if (targetMenu === 'assembly') {
- if (assemblyViewState === 'list' && listIndex !== null) {
- currentLinkAssemblyId = assemblyList[listIndex].id;
- } else if (assemblyViewState === 'edit' && tempAssemblyData) {
- if (!tempAssemblyData.id) {
- showToast('请先保存组装页再获取链接!', 'error');
- return;
- }
- currentLinkAssemblyId = tempAssemblyData.id;
- }
- }
-
- if (!currentLinkAssemblyId) return;
- document.getElementById('link-modal-content').innerHTML = `
- <div class="flex gap-4 mb-2">
- <div class="flex-1">
- <label class="block text-xs font-medium text-gray-700 mb-1">Campaign ID</label>
- <select id="modal-campaign-id" onchange="updateModalLinks()" class="w-full px-3 py-2 border border-gray-300 rounded text-sm outline-none focus:border-blue-500 bg-white">
- <option value="">请选择 (无)</option>
- <option value="CAMP202511">CAMP202511</option>
- <option value="CAMP_WINTER">CAMP_WINTER</option>
- </select>
- </div>
- <div class="flex-1">
- <label class="block text-xs font-medium text-gray-700 mb-1">Channel ID</label>
- <select id="modal-channel-id" onchange="updateModalLinks()" class="w-full px-3 py-2 border border-gray-300 rounded text-sm outline-none focus:border-blue-500 bg-white">
- <option value="">请选择 (无)</option>
- <option value="CH_APP">APP推送</option>
- <option value="CH_WECHAT">微信公众号</option>
- </select>
- </div>
- </div>
- <div id="modal-links-container" class="space-y-4"></div>
- `;
-
- updateModalLinks();
-
- document.getElementById('link-modal-backdrop').classList.remove('hidden');
- document.getElementById('link-modal').classList.remove('hidden');
- document.getElementById('link-modal').classList.add('flex');
- }
- function updateModalLinks() {
- if(!currentLinkAssemblyId) return;
-
- const campId = document.getElementById('modal-campaign-id').value;
- const chanId = document.getElementById('modal-channel-id').value;
-
- let queryParams = [];
- if(campId) queryParams.push(`CampaignId=${campId}`);
- if(chanId) queryParams.push(`Channelid=${chanId}`);
- const queryStr = queryParams.length > 0 ? (currentLinkAssemblyId === 'home' || currentLinkAssemblyId === 'zone' ? '?' : '&') + queryParams.join('&') : '';
- let basePath = '';
- let isPublished = false;
- if (currentLinkAssemblyId === 'home') {
- basePath = 'home';
- isPublished = pageStatus.home === 'published';
- } else if (currentLinkAssemblyId === 'zone') {
- basePath = 'zone';
- isPublished = pageStatus.zone === 'published';
- } else {
- basePath = `assembly?id=${currentLinkAssemblyId}`;
- const asm = assemblyList.find(a => a.id === currentLinkAssemblyId) || tempAssemblyData;
- isPublished = asm && asm.status === 'published';
- }
- const prodH5Url = `https://hsbc.com/${basePath}${queryStr}`;
- const prodAppUrl = `hsbc-app://${basePath}${queryStr}`;
-
- const draftJoiner = (basePath === 'home' || basePath === 'zone') ? '?' : '&';
- const testUrl = `https://test.hsbc.com/${basePath}${draftJoiner}draft=1${queryStr.replace('?', '&')}`;
-
- let html = '';
- if (isPublished) {
- html += `
- <div class="bg-white border border-gray-200 rounded-lg p-4 shadow-sm relative">
- <div class="flex items-center gap-2 mb-3"><span class="w-2 h-2 rounded-full bg-green-500"></span><span class="font-bold text-gray-800">正式环境链接</span></div>
- <div class="space-y-2">
- <div class="flex gap-2 items-center">
- <span class="text-xs text-gray-500 w-16">H5 链接</span>
- <input type="text" readonly value="${prodH5Url}" class="flex-1 bg-gray-50 px-3 py-2 text-sm border border-gray-200 rounded text-gray-700 outline-none">
- <button onclick="copyToClipboard('${prodH5Url}')" class="bg-blue-50 border border-blue-100 text-blue-600 px-4 py-2 rounded text-sm hover:bg-blue-100 font-medium whitespace-nowrap transition">复制</button>
- </div>
- <div class="flex gap-2 items-center">
- <span class="text-xs text-gray-500 w-16">小程序/APP</span>
- <input type="text" readonly value="${prodAppUrl}" class="flex-1 bg-gray-50 px-3 py-2 text-sm border border-gray-200 rounded text-gray-700 outline-none">
- <button onclick="copyToClipboard('${prodAppUrl}')" class="bg-blue-50 border border-blue-100 text-blue-600 px-4 py-2 rounded text-sm hover:bg-blue-100 font-medium whitespace-nowrap transition">复制</button>
- </div>
- </div>
- </div>
- `;
- }
- html += `
- <div class="bg-white border border-gray-200 rounded-lg p-4 shadow-sm relative">
- <div class="flex items-center gap-2 mb-3"><span class="w-2 h-2 rounded-full bg-blue-500"></span><span class="font-bold text-gray-800">草稿预览链接 (测试)</span></div>
- <div class="flex gap-2 items-center">
- <input type="text" readonly value="${testUrl}" class="flex-1 bg-gray-50 px-3 py-2 text-sm border border-gray-200 rounded text-gray-700 outline-none">
- <button onclick="copyToClipboard('${testUrl}')" class="bg-blue-50 border border-blue-100 text-blue-600 px-4 py-2 rounded text-sm hover:bg-blue-100 font-medium whitespace-nowrap transition">复制</button>
- </div>
- </div>
- `;
- document.getElementById('modal-links-container').innerHTML = html;
- }
- function closeLinkModal() {
- document.getElementById('link-modal-backdrop').classList.add('hidden');
- document.getElementById('link-modal').classList.add('hidden');
- document.getElementById('link-modal').classList.remove('flex');
- currentLinkAssemblyId = null;
- }
- function copyToClipboard(text) {
- navigator.clipboard.writeText(text).then(() => showToast('链接已复制到剪贴板!'));
- }
- function toggleTagDropdown(e, i) { e.stopPropagation(); openTagDropdownIndex = (openTagDropdownIndex === i) ? null : i; renderLayout(); }
- function filterTags(e, i) {
- const k = e.target.value.toLowerCase();
- document.querySelectorAll(`#tag-list-${i} label`).forEach(l => {
- l.style.display = l.innerText.toLowerCase().includes(k) ? 'flex' : 'none';
- });
- }
- // ==========================================
- // 组装页模块增删复制逻辑
- // ==========================================
- function handleCreateNewAssembly() {
- editAssemblyIndex = null;
- tempAssemblyData = createEmptyAssembly('');
- assemblyViewState = 'edit';
- markAsModified();
- renderLayout();
- }
- function editAssembly(index) {
- editAssemblyIndex = index;
- tempAssemblyData = JSON.parse(JSON.stringify(assemblyList[index]));
- assemblyViewState = 'edit';
- renderLayout();
- }
- function deleteAssembly(index) {
- if(confirm('确认要删除该组装页吗?此操作不可恢复。')) {
- assemblyList.splice(index, 1);
- showToast('删除成功');
- renderLayout();
- }
- }
- function backToAssemblyList() {
- editAssemblyIndex = null;
- tempAssemblyData = null;
- assemblyViewState = 'list';
- renderLayout();
- }
- function updateAssemblyName(val) {
- if (getCurrentAssemblyData()) {
- getCurrentAssemblyData().name = val;
- markAsModified();
- }
- }
- function addAssemblyBlock(type) {
- let newBlock = { type: type, id: generateBlockId(), data: {} };
- if (type === 'image') newBlock.data = { imageUrl: '' };
- if (type === 'privilege') newBlock.data = { title: '新权益标题', items: [{ id:'1', title:'模拟权益', desc:'描述' }] };
- if (type === 'richtext') newBlock.data = { content: '<p>输入富文本内容...</p>' };
- if (type === 'buttonGroup') newBlock.data = { buttons: [{ text: '点击跳转', link: '', style: 'primary', actionType: 'link' }] };
- getCurrentAssemblyData().blocks.push(newBlock);
- markAsModified();
- renderLayout();
- }
- function duplicateAssemblyBlock(index) {
- let block = getCurrentAssemblyData().blocks[index];
- if (block.type === 'privilege') {
- alert('权益商品模块每页仅限配置一个,无法复制!');
- return;
- }
- let newBlock = JSON.parse(JSON.stringify(block));
- newBlock.id = generateBlockId(); // 新ID防止重复
- getCurrentAssemblyData().blocks.splice(index + 1, 0, newBlock);
- markAsModified();
- renderLayout();
- }
- function removeAssemblyBlock(index) {
- if(confirm('确定删除该模块吗?')) {
- getCurrentAssemblyData().blocks.splice(index, 1);
- markAsModified();
- renderLayout();
- }
- }
- function addAssemblyButton(blockIndex) {
- tempAssemblyData.blocks[blockIndex].data.buttons.push({
- text: '新按钮', link: '', style: 'secondary',
- actionType: 'link', dialog: { title: '', desc: '', confirmText: '确认', confirmLink: '' }
- });
- markAsModified();
- renderLayout();
- }
- function updateAssemblyButtonDialog(blockIndex, btnIndex, field, value) {
- let block = tempAssemblyData.blocks[blockIndex];
- if (!block.data.buttons[btnIndex].dialog) {
- block.data.buttons[btnIndex].dialog = { title: '', desc: '', confirmText: '确认', confirmLink: '' };
- }
- block.data.buttons[btnIndex].dialog[field] = value;
- markAsModified();
- }
- switchMenu('home'); // Init
- </script>
- </body>
- </html>
|