页面管理.html 83 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>后台管理 - 汇丰权益配置</title>
  7. <script src="https://cdn.tailwindcss.com"></script>
  8. <script src="https://unpkg.com/@phosphor-icons/web"></script>
  9. <style>
  10. ::-webkit-scrollbar { width: 8px; height: 8px; }
  11. ::-webkit-scrollbar-track { background: #f1f1f1; }
  12. ::-webkit-scrollbar-thumb { background: #ccc; border-radius: 4px; }
  13. ::-webkit-scrollbar-thumb:hover { background: #aaa; }
  14. .menu-item { transition: all 0.2s; }
  15. .menu-item:hover { color: #fff; }
  16. </style>
  17. </head>
  18. <body class="bg-gray-100 h-screen flex overflow-hidden font-sans text-gray-800">
  19. <!-- 左侧菜单栏 -->
  20. <aside class="w-[240px] bg-[#364252] text-gray-300 flex flex-col h-full flex-shrink-0 shadow-xl z-20">
  21. <div class="py-6 text-2xl font-bold text-white flex justify-center items-center gap-3 bg-[#2b3644] border-b border-[#2b3644]">
  22. <i class="ph ph-desktop"></i>
  23. 管理后台
  24. </div>
  25. <div class="flex-1 overflow-y-auto py-4 text-[15px]">
  26. <div>
  27. <div class="px-5 py-3 flex justify-between items-center cursor-pointer text-gray-200 hover:text-white transition">
  28. <div class="flex items-center gap-3"><i class="ph ph-gear text-lg"></i><span>定制功能</span></div>
  29. <i class="ph ph-caret-up text-sm text-gray-400"></i>
  30. </div>
  31. <div class="flex flex-col">
  32. <div class="px-5 py-3 pl-11 flex justify-between items-center cursor-pointer text-gray-200 hover:text-white transition">
  33. <span>汇丰银行</span>
  34. <i class="ph ph-caret-up text-sm text-gray-400"></i>
  35. </div>
  36. <div class="flex flex-col">
  37. <div class="px-5 py-3 pl-16 text-gray-200 hover:text-white transition cursor-default">页面管理</div>
  38. <div class="flex flex-col text-sm">
  39. <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">
  40. 权益介绍页
  41. </a>
  42. <a href="#" onclick="switchMenu('zone')" id="menu-zone" class="py-2.5 pl-[72px] pr-5 text-gray-400 hover:text-white block transition">
  43. 权益首页
  44. </a>
  45. <a href="#" onclick="switchMenu('assembly')" id="menu-assembly" class="py-2.5 pl-[72px] pr-5 text-gray-400 hover:text-white block transition">
  46. 组装页
  47. </a>
  48. </div>
  49. <div class="px-5 py-3 pl-16 flex justify-between items-center cursor-pointer text-gray-200 hover:text-white transition mt-2">
  50. <span>权益管理</span>
  51. <i class="ph ph-caret-up text-sm text-gray-400"></i>
  52. </div>
  53. <div class="flex flex-col text-[14px]">
  54. <a href="#" class="py-2.5 pl-[72px] pr-5 text-gray-400 hover:text-white transition block">借记卡权益</a>
  55. <a href="#" class="py-2.5 pl-[72px] pr-5 text-gray-400 hover:text-white transition block">信用卡权益</a>
  56. <a href="#" class="py-2.5 pl-[72px] pr-5 text-gray-400 hover:text-white transition block">人群管理</a>
  57. </div>
  58. </div>
  59. </div>
  60. </div>
  61. </div>
  62. </aside>
  63. <!-- 右侧主内容区 -->
  64. <main class="flex-1 flex flex-col h-full bg-[#f4f6f8] relative">
  65. <!-- 1. 极简面包屑 -->
  66. <div class="bg-white h-10 border-b border-gray-100 flex items-center px-6 flex-shrink-0 text-[13px] text-gray-500">
  67. <span>页面管理</span>
  68. <i class="ph ph-caret-right mx-1.5 text-xs"></i>
  69. <span class="text-gray-900" id="breadcrumb-title">权益介绍页配置</span>
  70. </div>
  71. <!-- 2. 独立操作工具栏 (悬浮吸顶) -->
  72. <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">
  73. <div class="flex items-center gap-4">
  74. <h1 class="text-[20px] font-bold text-gray-900 tracking-wide" id="toolbar-title">权益介绍页配置</h1>
  75. <!-- 全局状态指示器 -->
  76. <div id="global-status-indicator" class="hidden items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium border">
  77. <!-- 动态内容渲染 -->
  78. </div>
  79. </div>
  80. <div id="header-actions" class="flex items-center gap-3">
  81. <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">
  82. <i class="ph ph-link text-lg"></i> 获取链接
  83. </button>
  84. <div class="w-px h-5 bg-gray-300 mx-1"></div>
  85. <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">
  86. <i class="ph ph-floppy-disk text-lg"></i> 保存草稿
  87. </button>
  88. <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">
  89. <i class="ph ph-paper-plane-tilt text-lg"></i> 发布上线
  90. </button>
  91. </div>
  92. </div>
  93. <div id="tier-tabs-container" class="bg-white px-6 pt-4 border-b border-gray-200 flex-shrink-0">
  94. <div class="flex gap-6" id="tier-tabs">
  95. <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>
  96. <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>
  97. <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>
  98. </div>
  99. </div>
  100. <div class="flex-1 overflow-y-auto p-6" id="form-container"></div>
  101. </main>
  102. <!-- 通用弹窗通知 -->
  103. <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">
  104. <i class="ph ph-check-circle text-green-400 text-xl" id="toast-icon"></i>
  105. <span id="toast-text">配置已保存</span>
  106. </div>
  107. <!-- 获取链接弹窗 -->
  108. <div id="link-modal-backdrop" class="fixed inset-0 bg-black/50 z-[80] hidden transition-opacity" onclick="closeLinkModal()"></div>
  109. <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">
  110. <div class="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50">
  111. <h3 class="text-lg font-bold text-gray-800 flex items-center gap-2"><i class="ph ph-link text-blue-600"></i> 获取页面链接</h3>
  112. <button onclick="closeLinkModal()" class="text-gray-400 hover:text-gray-600"><i class="ph ph-x text-xl"></i></button>
  113. </div>
  114. <div class="p-6" id="link-modal-content">
  115. <!-- 链接内容动态渲染 -->
  116. </div>
  117. </div>
  118. <!-- 富文本插入超链接弹窗 -->
  119. <div id="rt-link-modal-backdrop" class="fixed inset-0 bg-black/50 z-[110] hidden transition-opacity" onclick="closeRtLinkModal()"></div>
  120. <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">
  121. <div class="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gray-50">
  122. <h3 class="text-base font-bold text-gray-800 flex items-center gap-2"><i class="ph ph-link text-blue-600"></i> 插入超链接</h3>
  123. <button onclick="closeRtLinkModal()" class="text-gray-400 hover:text-gray-600"><i class="ph ph-x text-xl"></i></button>
  124. </div>
  125. <div class="p-6 space-y-4">
  126. <div>
  127. <label class="block text-sm font-medium text-gray-700 mb-1">链接地址 (URL) <span class="text-red-500">*</span></label>
  128. <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://">
  129. </div>
  130. <div>
  131. <label class="block text-sm font-medium text-gray-700 mb-1">展示文字</label>
  132. <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="例如:点击查看详情">
  133. </div>
  134. </div>
  135. <div class="px-6 py-4 bg-gray-50 border-t border-gray-100 flex justify-end gap-3">
  136. <button onclick="closeRtLinkModal()" class="px-4 py-2 border border-gray-300 text-gray-700 rounded text-sm hover:bg-gray-100 transition">取消</button>
  137. <button onclick="confirmRtLink()" class="px-4 py-2 bg-blue-600 text-white rounded text-sm hover:bg-blue-700 transition font-medium">确认插入</button>
  138. </div>
  139. </div>
  140. <script>
  141. // ==========================================
  142. // 核心数据模型与状态
  143. // ==========================================
  144. let currentMenu = 'home';
  145. let currentTier = 'premier';
  146. let currentLinkAssemblyId = null;
  147. let pageStatus = {
  148. home: 'published',
  149. zone: 'published'
  150. };
  151. const availableTags = Array.from({length: 8}, (_, i) => ({ id: `t${i+1}`, name: `人群标签${i+1}` }));
  152. // 1. 权益介绍页数据
  153. const homeData = {
  154. premier: createEmptyTierData('卓越理财'),
  155. elite: createEmptyTierData('卓越理财·尊尚'),
  156. global: createEmptyTierData('环球私人银行')
  157. };
  158. function createEmptyTierData(defaultTitle) {
  159. return {
  160. tabName: defaultTitle,
  161. buttons: { unlocked: '进入权益专区', locked: '待解锁' },
  162. header: { imageUrl: 'https://images.unsplash.com/photo-1518893494013-481c1d8ed3fd?w=800', mainTitle: defaultTitle, subTitle: '为您和您的家人提供尊崇专属礼遇和权益' },
  163. privileges: [
  164. { imageUrl: '', title: '国际教育', desc: '享专属国际化学校优惠及背景提升项目' },
  165. { imageUrl: '', title: '酒店礼遇', desc: '高端酒店及商场会籍匹配' }
  166. ]
  167. };
  168. }
  169. // 2. 权益首页数据 (原权益专区)
  170. const zoneData = {
  171. adCarousel: [{
  172. imageUrl: 'https://images.unsplash.com/photo-1556742049-0cfed4f6a45d?w=800',
  173. linkH5: '', linkApp: '', wechatAppId: '', wechatPath: '', title: '汇丰卓越理财新户达标礼', subtitle: '报名参加汇丰活动,体验更多精彩礼遇', tags: ['t1', 't2']
  174. }],
  175. exchangeConfig: {
  176. items: [
  177. { imageUrl: 'https://images.unsplash.com/photo-1436491865332-7a61a109cc05?w=200', title: '航空里程', link: '/flight' },
  178. { imageUrl: 'https://images.unsplash.com/photo-1518893494013-481c1d8ed3fd?w=200', title: '贵宾厅', link: '/lounge' }
  179. ]
  180. },
  181. ruleModal: { title: '积分规则', content: '<p>本积分使用规则为汇丰银行(中国)有限公司...</p>' }
  182. };
  183. // 3. 组装页数据
  184. let assemblyViewState = 'list';
  185. let editAssemblyIndex = null;
  186. let tempAssemblyData = null;
  187. function generateBlockId() { return 'b_' + Math.random().toString(36).substr(2, 9); }
  188. function createEmptyAssembly(name) {
  189. const now = new Date().toLocaleString('zh-CN', { hour12: false });
  190. return {
  191. id: 'A' + Math.floor(Math.random() * 10000),
  192. name: name || '', status: 'draft', createTime: now, updateTime: now,
  193. blocks: [
  194. { type: 'image', id: generateBlockId(), data: { imageUrl: '' } },
  195. { type: 'privilege', id: generateBlockId(), data: { title: '权益标题', items: [ { id: '1', title: '权益名称', desc: '权益简介' } ] } },
  196. { type: 'richtext', id: generateBlockId(), data: { content: '<p>组装页富文本区块内容...</p>' } },
  197. { type: 'buttonGroup', id: generateBlockId(), data: { buttons: [ { text: '点击配置', link: '', style: 'primary', actionType: 'link' } ] } }
  198. ]
  199. };
  200. }
  201. const assemblyList = [
  202. {
  203. id: 'A7538', name: '双十一迎新专属活动页', status: 'published', createTime: '2025-10-01 10:00:00', updateTime: '2025-10-15 14:30:00',
  204. blocks: [
  205. { type: 'image', id: generateBlockId(), data: { imageUrl: 'https://images.unsplash.com/photo-1557683316-973673baf926?w=800' } },
  206. { type: 'privilege', id: generateBlockId(), data: { title: '专属权益', items: [{ id: '1', title: '机场贵宾厅服务', desc: '全年免费使用6次指定机场贵宾厅' }, { id: '2', title: '境内礼宾车', desc: '免费2次接送' }] } },
  207. { type: 'richtext', id: generateBlockId(), data: { content: '<p>欢迎参与双十一专属福利活动...</p>' } },
  208. { type: 'buttonGroup', id: generateBlockId(), data: { buttons: [ { text: '立即参与', link: '/join', style: 'primary', actionType: 'link' } ] } }
  209. ]
  210. },
  211. {
  212. id: 'A2091', name: '私行高净值沙龙引导页', status: 'draft', createTime: '2025-11-20 09:15:00', updateTime: '2025-11-20 09:15:00',
  213. blocks: [
  214. { type: 'image', id: generateBlockId(), data: { imageUrl: 'https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?w=800' } },
  215. { type: 'privilege', id: generateBlockId(), data: { title: '尊享体验', items: [{ id: '1', title: '高尔夫权益', desc: 'VIP高尔夫俱乐部免会籍预约' }] } }
  216. ]
  217. }
  218. ];
  219. let openTagDropdownIndex = null;
  220. function getCurrentAssemblyData() { return tempAssemblyData; }
  221. // ==========================================
  222. // 渲染与切换
  223. // ==========================================
  224. function switchMenu(menuId) {
  225. currentMenu = menuId;
  226. const activeClass = 'py-2.5 pl-[72px] pr-5 text-blue-400 bg-[#2b3644] font-medium block transition';
  227. const inactiveClass = 'py-2.5 pl-[72px] pr-5 text-gray-400 hover:text-white block transition';
  228. document.getElementById('menu-home').className = (menuId === 'home') ? activeClass : inactiveClass;
  229. document.getElementById('menu-zone').className = (menuId === 'zone') ? activeClass : inactiveClass;
  230. document.getElementById('menu-assembly').className = (menuId === 'assembly') ? activeClass : inactiveClass;
  231. const titles = { 'home': '权益介绍页配置', 'zone': '权益首页配置', 'assembly': '组装页管理' };
  232. document.getElementById('breadcrumb-title').innerText = titles[menuId];
  233. document.getElementById('toolbar-title').innerText = titles[menuId];
  234. document.getElementById('tier-tabs-container').style.display = (menuId === 'home') ? 'block' : 'none';
  235. if (menuId === 'home') {
  236. document.getElementById('tab-premier').innerText = homeData.premier.tabName || '卓越理财';
  237. document.getElementById('tab-elite').innerText = homeData.elite.tabName || '卓越理财·尊尚';
  238. document.getElementById('tab-global').innerText = homeData.global.tabName || '环球私人银行';
  239. }
  240. if (menuId === 'assembly') assemblyViewState = 'list';
  241. openTagDropdownIndex = null;
  242. renderLayout();
  243. updateStatusIndicator();
  244. }
  245. function switchTier(tier) {
  246. currentTier = tier;
  247. const tabs = ['premier', 'elite', 'global'];
  248. tabs.forEach(t => {
  249. const el = document.getElementById(`tab-${t}`);
  250. 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";
  251. });
  252. renderLayout();
  253. }
  254. function renderLayout() {
  255. const actions = document.getElementById('header-actions');
  256. actions.style.display = (currentMenu === 'assembly' && assemblyViewState === 'list') ? 'none' : 'flex';
  257. if (currentMenu === 'home') renderHomeConfig();
  258. else if (currentMenu === 'zone') renderZoneConfig();
  259. else renderAssemblyController();
  260. updateStatusIndicator();
  261. }
  262. function markAsModified() {
  263. if (currentMenu === 'home') pageStatus.home = 'modified';
  264. if (currentMenu === 'zone') pageStatus.zone = 'modified';
  265. if (currentMenu === 'assembly' && assemblyViewState === 'edit' && tempAssemblyData) {
  266. tempAssemblyData._isModified = true;
  267. }
  268. updateStatusIndicator();
  269. }
  270. function updateStatusIndicator() {
  271. const indicator = document.getElementById('global-status-indicator');
  272. if (currentMenu === 'assembly' && assemblyViewState === 'list') {
  273. indicator.classList.add('hidden');
  274. return;
  275. }
  276. 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');
  277. let currentStatus = '';
  278. if (currentMenu === 'home') currentStatus = pageStatus.home;
  279. else if (currentMenu === 'zone') currentStatus = pageStatus.zone;
  280. else if (currentMenu === 'assembly' && assemblyViewState === 'edit') {
  281. if (tempAssemblyData._isModified) {
  282. currentStatus = 'modified';
  283. } else {
  284. currentStatus = tempAssemblyData.status;
  285. }
  286. }
  287. if (currentStatus === 'published') {
  288. indicator.classList.add('bg-green-50', 'text-green-700', 'border-green-200');
  289. indicator.innerHTML = `<span class="w-1.5 h-1.5 rounded-full bg-green-500"></span> 正式线上环境`;
  290. } else if (currentStatus === 'modified') {
  291. indicator.classList.add('bg-yellow-50', 'text-yellow-700', 'border-yellow-200');
  292. indicator.innerHTML = `<span class="w-1.5 h-1.5 rounded-full bg-yellow-500"></span> 有未发布的修改`;
  293. } else {
  294. indicator.classList.add('bg-gray-100', 'text-gray-600', 'border-gray-200');
  295. indicator.innerHTML = `<span class="w-1.5 h-1.5 rounded-full bg-gray-400"></span> 草稿 (未发布)`;
  296. }
  297. }
  298. function handleSaveAction(type) {
  299. const pageName = (currentMenu === 'home') ? '权益介绍页' : (currentMenu === 'zone' ? '权益首页' : '组装页');
  300. if (currentMenu === 'home' || currentMenu === 'zone') {
  301. if (currentMenu === 'home') pageStatus.home = (type === 'publish') ? 'published' : 'draft';
  302. if (currentMenu === 'zone') pageStatus.zone = (type === 'publish') ? 'published' : 'draft';
  303. }
  304. if (currentMenu === 'assembly' && assemblyViewState === 'edit') {
  305. // 校验:如果按钮模块的点击动作是“跳转链接”,则链接不能为空
  306. let hasError = false;
  307. tempAssemblyData.blocks.forEach(block => {
  308. if (block.type === 'buttonGroup') {
  309. block.data.buttons.forEach(btn => {
  310. if (btn.actionType === 'link' && !btn.link.trim()) {
  311. hasError = true;
  312. }
  313. });
  314. }
  315. });
  316. if (hasError) {
  317. showToast('保存失败:按钮模块中存在未填写的跳转链接!', 'error');
  318. return;
  319. }
  320. if (!tempAssemblyData.name.trim()) tempAssemblyData.name = '未命名组装页';
  321. tempAssemblyData.status = (type === 'publish') ? 'published' : 'draft';
  322. tempAssemblyData.updateTime = new Date().toLocaleString('zh-CN', { hour12: false });
  323. delete tempAssemblyData._isModified;
  324. if (editAssemblyIndex === null) {
  325. assemblyList.unshift(tempAssemblyData);
  326. if (type === 'draft') editAssemblyIndex = 0;
  327. } else {
  328. assemblyList[editAssemblyIndex] = tempAssemblyData;
  329. }
  330. if (type === 'publish') {
  331. editAssemblyIndex = null;
  332. tempAssemblyData = null;
  333. assemblyViewState = 'list';
  334. }
  335. renderLayout();
  336. }
  337. showToast(type === 'publish' ? `已发布${pageName}配置!` : `已保存${pageName}配置草稿!`);
  338. updateStatusIndicator();
  339. }
  340. // ==========================================
  341. // 1. 权益介绍页渲染
  342. // ==========================================
  343. function renderHomeConfig() {
  344. const data = homeData[currentTier];
  345. // 兼容历史数据,确保字段存在
  346. if (!data.tabName) data.tabName = currentTier === 'premier' ? '卓越理财' : (currentTier === 'elite' ? '卓越理财·尊尚' : '环球私人银行');
  347. if (!data.buttons) data.buttons = { unlocked: '进入权益专区', locked: '待解锁' };
  348. document.getElementById('form-container').innerHTML = `
  349. <div class="max-w-4xl mx-auto space-y-8 pb-10">
  350. <section class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
  351. <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>
  352. <div class="p-6">
  353. <label class="block text-sm font-medium text-gray-700 mb-1">Tab名称</label>
  354. <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="例如:卓越理财">
  355. </div>
  356. </section>
  357. <section class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
  358. <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>
  359. <div class="p-6 space-y-5">
  360. <div><label class="block text-sm font-medium mb-2">头图配图</label>
  361. <div class="flex items-center gap-4">
  362. <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">
  363. ${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>`}
  364. <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>
  365. <input type="file" class="hidden" accept="image/*" onchange="uploadImageCallback(event, (url) => {homeData[currentTier].header.imageUrl=url; markAsModified(); renderLayout();})">
  366. </label>
  367. <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>
  368. </div>
  369. </div>
  370. <div class="space-y-4 pt-2">
  371. <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>
  372. <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>
  373. </div>
  374. </div>
  375. </section>
  376. <section class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
  377. <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>
  378. <div class="p-6">
  379. <div class="grid grid-cols-2 gap-5">
  380. <div>
  381. <label class="block text-sm font-medium text-gray-700 mb-1">已解锁按钮文案</label>
  382. <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="例如:进入权益专区">
  383. </div>
  384. <div>
  385. <label class="block text-sm font-medium text-gray-700 mb-1">待解锁按钮文案</label>
  386. <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="例如:待解锁">
  387. </div>
  388. </div>
  389. </div>
  390. </section>
  391. <section class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
  392. <div class="bg-gray-50 px-5 py-3 border-b border-gray-200 font-medium flex justify-between items-center">
  393. <div class="flex items-center gap-2"><i class="ph ph-list-bullets text-gray-500 text-lg"></i> 权益列表配置</div>
  394. <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>
  395. </div>
  396. <div class="p-6 space-y-4">
  397. ${data.privileges.map((item, index) => renderHomePrivilegeItem(item, index)).join('') || `<div class="text-center text-gray-400 py-6 border border-dashed rounded">暂无权益项</div>`}
  398. </div>
  399. </section>
  400. </div>`;
  401. }
  402. function renderHomePrivilegeItem(item, index) {
  403. return `
  404. <div class="flex gap-4 items-start p-5 pt-8 border border-gray-200 rounded-lg bg-white relative shadow-sm mt-2">
  405. <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>
  406. <div class="absolute right-4 top-4 flex items-center gap-2 z-20">
  407. ${renderSortPopover('homePrivilege', index, homeData[currentTier].privileges.length)}
  408. <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>
  409. </div>
  410. <div class="flex flex-col items-center gap-1 shrink-0 mt-1">
  411. <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">
  412. ${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>`}
  413. <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>
  414. </div>
  415. <div class="text-[10px] text-gray-400 text-center leading-tight">非必填<br>无图展示圆点</div>
  416. </div>
  417. <div class="flex-1 grid grid-cols-1 md:grid-cols-3 gap-4 pr-24 mt-1">
  418. <div class="col-span-1">
  419. <label class="block text-xs text-gray-500 mb-1">标题</label>
  420. <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">
  421. </div>
  422. <div class="col-span-2">
  423. <label class="block text-xs text-gray-500 mb-1">权益文字描述</label>
  424. <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">
  425. </div>
  426. </div>
  427. </div>`;
  428. }
  429. // ==========================================
  430. // 2. 权益首页渲染 (Zone)
  431. // ==========================================
  432. function renderZoneConfig() {
  433. const container = document.getElementById('form-container');
  434. container.innerHTML = `
  435. <div class="max-w-4xl mx-auto space-y-8 pb-10">
  436. <section class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-visible mt-2">
  437. <div class="bg-gray-50 px-5 py-3 border-b font-medium flex justify-between items-center rounded-t-xl">
  438. <div class="flex items-center gap-2"><i class="ph ph-slideshow text-gray-500 text-lg"></i> 活动广告轮播图</div>
  439. <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>
  440. </div>
  441. <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>
  442. </section>
  443. <section class="bg-white rounded-xl shadow-sm border border-gray-200">
  444. <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>
  445. <div class="p-6">
  446. <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>
  447. </div>
  448. </section>
  449. <section class="bg-white rounded-xl shadow-sm border border-gray-200">
  450. <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>
  451. <div class="p-6 space-y-4">
  452. <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>
  453. <div>
  454. <label class="block text-sm font-medium mb-1">规则内容 (富文本编辑器)</label>
  455. <div class="border rounded shadow-sm flex flex-col">
  456. ${getRichTextToolbar('zone')}
  457. <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>
  458. </div>
  459. </div>
  460. </div>
  461. </section>
  462. </div>`;
  463. }
  464. function renderZoneCarouselItem(item, index) {
  465. return `
  466. <div class="border rounded-xl p-5 pt-8 bg-white relative shadow-sm overflow-visible">
  467. <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>
  468. <div class="absolute right-5 top-5 flex items-center gap-2">
  469. ${renderSortPopover('zoneCarousel', index, zoneData.adCarousel.length)}
  470. <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>
  471. </div>
  472. <div class="flex flex-col gap-4 mt-3">
  473. <div class="flex items-center gap-4">
  474. <div class="w-64 h-36 bg-[#eef6fc] rounded-md relative group overflow-hidden border flex items-center justify-center shrink-0">
  475. ${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>`}
  476. <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>
  477. </div>
  478. <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>
  479. </div>
  480. <div class="space-y-3">
  481. <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>
  482. <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>
  483. </div>
  484. <div class="tag-dropdown-container relative">
  485. <label class="block text-xs text-gray-600 mb-1">可见人群标签 (多选)</label>
  486. <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})">
  487. ${(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('')}
  488. <div class="text-gray-400 text-sm">${(!item.tags || item.tags.length === 0) ? '点击选择...' : ''}</div>
  489. </div>
  490. <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">
  491. <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>
  492. <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>
  493. </div>
  494. </div>
  495. <div class="bg-gray-50 p-3 rounded-lg border border-gray-200 space-y-3 mt-2">
  496. <div class="text-xs font-bold text-gray-700 border-b border-gray-200 pb-2"><i class="ph ph-link"></i> 跳转链接配置</div>
  497. <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>
  498. <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>
  499. <div class="grid grid-cols-2 gap-3">
  500. <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>
  501. <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>
  502. </div>
  503. </div>
  504. </div>
  505. </div>`;
  506. }
  507. function renderZoneExchangeItem(item, index) {
  508. 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>`;
  509. }
  510. // ==========================================
  511. // 3. 组装页渲染逻辑
  512. // ==========================================
  513. function renderAssemblyController() {
  514. if (assemblyViewState === 'list') renderAssemblyList();
  515. else renderAssemblyEdit();
  516. }
  517. function renderAssemblyList() {
  518. const container = document.getElementById('form-container');
  519. let tableRows = assemblyList.map((item, index) => `
  520. <tr class="border-b hover:bg-gray-50 transition">
  521. <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>
  522. <td class="border px-5 py-3 text-sm text-gray-600 text-center">${item.id}</td>
  523. <td class="border px-5 py-3 text-sm font-medium text-gray-900">${item.name || '未命名'}</td>
  524. <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>
  525. <td class="border px-5 py-3 text-sm text-gray-500 text-center whitespace-nowrap">${item.createTime}</td>
  526. <td class="border px-5 py-3 text-sm text-gray-500 text-center whitespace-nowrap">${item.updateTime}</td>
  527. </tr>`).join('');
  528. container.innerHTML = `
  529. <div class="w-full space-y-6">
  530. <div class="bg-white p-5 rounded shadow-sm border mb-4 mt-2">
  531. <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>
  532. <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>
  533. </div>
  534. <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>
  535. </div>`;
  536. }
  537. function renderAssemblyEdit() {
  538. const data = getCurrentAssemblyData();
  539. let html = `
  540. <div class="max-w-4xl mx-auto space-y-6 pb-20 mt-2">
  541. <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>
  542. <section class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 flex gap-6 items-center">
  543. <div class="shrink-0 flex items-center gap-2 h-[38px]">
  544. <span class="text-sm font-medium text-gray-500">页面 ID:</span>
  545. <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>
  546. </div>
  547. <div class="flex-1 flex items-center gap-3 border-l border-gray-200 pl-6 h-[38px]">
  548. <label class="text-sm font-medium text-gray-700 shrink-0">组装页名称<span class="text-red-500 ml-0.5">*</span></label>
  549. <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="请为该页面设置一个名称 (必填)">
  550. </div>
  551. </section>
  552. `;
  553. const hasPrivilege = data.blocks.some(b => b.type === 'privilege');
  554. data.blocks.forEach((block, index) => {
  555. html += `<section class="bg-white rounded-xl shadow-sm border overflow-hidden relative mb-6">
  556. <div class="absolute right-0 top-0 flex items-center z-10 bg-gray-50 border-b border-l rounded-bl-lg shadow-sm">
  557. ${renderSortPopover('assemblyBlock', index, data.blocks.length)}`;
  558. if (block.type !== 'privilege') {
  559. 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>`;
  560. }
  561. 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>`;
  562. if (block.type === 'image') {
  563. 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>
  564. <div class="p-6">
  565. <div class="flex gap-4 items-center">
  566. <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">
  567. ${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>`}
  568. <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>
  569. <input type="file" class="hidden" accept="image/*" onchange="uploadImageCallback(event, (url)=>{tempAssemblyData.blocks[${index}].data.imageUrl=url; markAsModified(); renderLayout();})">
  570. </label>
  571. <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>
  572. </div>
  573. </div>`;
  574. }
  575. else if (block.type === 'privilege') {
  576. 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>
  577. <div class="p-6">
  578. <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>
  579. <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>
  580. </div>`;
  581. }
  582. else if (block.type === 'richtext') {
  583. 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>
  584. <div class="p-6">
  585. <label class="block text-sm font-medium mb-1">图文内容 (富文本编辑器)</label>
  586. <div class="border rounded flex flex-col shadow-sm">
  587. ${getRichTextToolbar('assembly', index)}
  588. <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>
  589. </div>
  590. </div>`;
  591. }
  592. else if (block.type === 'buttonGroup') {
  593. 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>
  594. <div class="p-6 space-y-4">`;
  595. block.data.buttons.forEach((btn, idx) => {
  596. const actionType = btn.actionType || 'link';
  597. html += `<div class="flex gap-4 items-start p-4 border rounded-lg bg-gray-50 relative">
  598. <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>
  599. <div class="flex-1 pt-2 space-y-4">
  600. <div class="grid grid-cols-3 gap-4">
  601. <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>
  602. <div><label class="block text-xs mb-1">点击动作</label>
  603. <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">
  604. <option value="link" ${actionType === 'link' ? 'selected' : ''}>跳转链接</option>
  605. <option value="dialog" ${actionType === 'dialog' ? 'selected' : ''}>弹窗提示</option>
  606. </select>
  607. </div>
  608. <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>
  609. </div>`;
  610. if (actionType === 'link') {
  611. 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>`;
  612. } else {
  613. const d = btn.dialog || { title: '', desc: '', confirmText: '确认', confirmLink: '' };
  614. html += `<div class="bg-white p-4 border border-gray-200 rounded text-sm space-y-3 shadow-sm">
  615. <div class="font-medium text-gray-800 mb-2 border-b border-gray-100 pb-2"><i class="ph ph-browser"></i> 弹窗内容配置 (包含取消、确认按钮)</div>
  616. <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>
  617. <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>
  618. <div class="grid grid-cols-2 gap-4">
  619. <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>
  620. <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>
  621. </div>
  622. </div>`;
  623. }
  624. html += `</div>
  625. <div class="flex items-center gap-2 shrink-0 pb-0.5 pt-2">
  626. ${renderSortPopover(`assemblyButton-${index}`, idx, block.data.buttons.length)}
  627. <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>
  628. </div>
  629. </div>`;
  630. });
  631. if(block.data.buttons.length === 0) {
  632. html += `<div class="text-center text-gray-400 py-4 border border-dashed rounded">暂无按钮</div>`;
  633. }
  634. html += `</div>`;
  635. }
  636. html += `</section>`;
  637. });
  638. html += `
  639. <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">
  640. <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>
  641. ${!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>` : ''}
  642. <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>
  643. <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>
  644. </div>
  645. </div>`;
  646. document.getElementById('form-container').innerHTML = html;
  647. }
  648. // ==========================================
  649. // 富文本功能 & 工具函数 (排序、上传、Toast)
  650. // ==========================================
  651. let rtTarget = { type: null, index: null };
  652. function getRichTextToolbar(type, index = null) {
  653. return `
  654. <div class="bg-gray-50 border-b border-gray-300 px-3 py-2 flex gap-1 flex-wrap text-gray-400 items-center">
  655. <button type="button" class="p-1.5 rounded cursor-not-allowed hover:bg-gray-200" title="字体大小"><i class="ph ph-text-t"></i></button>
  656. <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>
  657. <div class="w-px h-5 bg-gray-300 mx-1"></div>
  658. <button type="button" class="p-1.5 rounded cursor-not-allowed hover:bg-gray-200" title="加粗"><i class="ph ph-text-b"></i></button>
  659. <button type="button" class="p-1.5 rounded cursor-not-allowed hover:bg-gray-200" title="斜体"><i class="ph ph-text-italic"></i></button>
  660. <button type="button" class="p-1.5 rounded cursor-not-allowed hover:bg-gray-200" title="下划线"><i class="ph ph-text-underline"></i></button>
  661. <div class="w-px h-5 bg-gray-300 mx-1"></div>
  662. <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>
  663. <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>
  664. <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>
  665. <div class="w-px h-5 bg-gray-300 mx-1"></div>
  666. <button type="button" class="p-1.5 rounded cursor-not-allowed hover:bg-gray-200" title="无序列表"><i class="ph ph-list-bullets"></i></button>
  667. <button type="button" class="p-1.5 rounded cursor-not-allowed hover:bg-gray-200" title="有序列表"><i class="ph ph-list-numbers"></i></button>
  668. <div class="w-px h-5 bg-gray-300 mx-1"></div>
  669. <!-- 实际可用功能 -->
  670. <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>
  671. <label class="p-1.5 rounded hover:bg-gray-200 cursor-pointer text-blue-600 transition font-medium flex items-center gap-1" title="插入图片">
  672. <i class="ph ph-image"></i> 图片
  673. <input type="file" class="hidden" accept="image/*" onchange="handleRichTextImage(event, '${type}', ${index})">
  674. </label>
  675. <label class="p-1.5 rounded hover:bg-gray-200 cursor-pointer text-blue-600 transition font-medium flex items-center gap-1" title="插入附件文件">
  676. <i class="ph ph-paperclip"></i> 附件
  677. <input type="file" class="hidden" onchange="handleRichTextFile(event, '${type}', ${index})">
  678. </label>
  679. </div>
  680. `;
  681. }
  682. function openRtLinkModal(type, index) {
  683. rtTarget = { type, index };
  684. document.getElementById('rt-link-url').value = '';
  685. document.getElementById('rt-link-text').value = '';
  686. document.getElementById('rt-link-modal-backdrop').classList.remove('hidden');
  687. document.getElementById('rt-link-modal').classList.remove('hidden');
  688. document.getElementById('rt-link-modal').classList.add('flex');
  689. }
  690. function closeRtLinkModal() {
  691. document.getElementById('rt-link-modal-backdrop').classList.add('hidden');
  692. document.getElementById('rt-link-modal').classList.add('hidden');
  693. document.getElementById('rt-link-modal').classList.remove('flex');
  694. }
  695. function confirmRtLink() {
  696. const url = document.getElementById('rt-link-url').value.trim();
  697. const text = document.getElementById('rt-link-text').value.trim() || '点击查看';
  698. if(!url) { showToast('请输入链接地址', 'error'); return; }
  699. const html = `<a href="${url}" target="_blank" style="color:#2563eb; text-decoration:underline;">${text}</a>`;
  700. appendToRichText(html);
  701. closeRtLinkModal();
  702. }
  703. function handleRichTextImage(event, type, index) {
  704. const file = event.target.files[0];
  705. if (!file) return;
  706. const html = `\n<img src="[图片: ${file.name}]" style="max-width:100%; border-radius:4px; margin-top:8px;" />\n`;
  707. rtTarget = { type, index };
  708. appendToRichText(html);
  709. event.target.value = '';
  710. }
  711. function handleRichTextFile(event, type, index) {
  712. const file = event.target.files[0];
  713. if (!file) return;
  714. 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`;
  715. rtTarget = { type, index };
  716. appendToRichText(html);
  717. event.target.value = '';
  718. }
  719. function appendToRichText(html) {
  720. if (rtTarget.type === 'zone') {
  721. zoneData.ruleModal.content += html;
  722. } else if (rtTarget.type === 'assembly') {
  723. tempAssemblyData.blocks[rtTarget.index].data.content += html;
  724. }
  725. markAsModified();
  726. renderLayout();
  727. }
  728. function renderSortPopover(type, index, maxCount) {
  729. 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>
  730. <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">
  731. <label class="block text-xs text-gray-700 mb-2">移动到序号:</label>
  732. <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>
  733. </div></div>`;
  734. }
  735. function toggleSortPopover(type, index, e) {
  736. e.stopPropagation();
  737. const pid = `popover-${type}-${index}`;
  738. document.querySelectorAll('[id^="popover-"]').forEach(el => { if(el.id !== pid) el.classList.add('hidden'); });
  739. const p = document.getElementById(pid);
  740. p.classList.toggle('hidden');
  741. if(!p.classList.contains('hidden')) setTimeout(()=>document.getElementById(`sort-input-${type}-${index}`).select(), 50);
  742. }
  743. function confirmSort(type, oldIdx) {
  744. const val = parseInt(document.getElementById(`sort-input-${type}-${oldIdx}`).value) - 1;
  745. let arr;
  746. if (type === 'homePrivilege') arr = homeData[currentTier].privileges;
  747. else if (type === 'zoneCarousel') arr = zoneData.adCarousel;
  748. else if (type === 'zoneExchange') arr = zoneData.exchangeConfig.items;
  749. else if (type === 'echoPrivileges') arr = zoneData.echoPrivileges;
  750. else if (type === 'echoCreditCards') arr = zoneData.echoCreditCards;
  751. else if (type === 'assemblyBlock') arr = getCurrentAssemblyData().blocks;
  752. else if (type.startsWith('assemblyPrivilege-')) arr = getCurrentAssemblyData().blocks[parseInt(type.split('-')[1])].data.items;
  753. else if (type.startsWith('assemblyButton-')) arr = getCurrentAssemblyData().blocks[parseInt(type.split('-')[1])].data.buttons;
  754. if (arr && val >= 0 && val < arr.length && val !== oldIdx) {
  755. const item = arr.splice(oldIdx, 1)[0];
  756. arr.splice(val, 0, item);
  757. markAsModified();
  758. }
  759. renderLayout();
  760. }
  761. function uploadImageCallback(event, updateFn) {
  762. const file = event.target.files[0];
  763. if (file) { const r = new FileReader(); r.onload = e => updateFn(e.target.result); r.readAsDataURL(file); }
  764. }
  765. function showToast(msg, type = 'success') {
  766. const t = document.getElementById('toast');
  767. document.getElementById('toast-text').innerText = msg;
  768. const icon = document.getElementById('toast-icon');
  769. if(type === 'error') {
  770. icon.className = 'ph ph-x-circle text-red-500 text-xl';
  771. } else {
  772. icon.className = 'ph ph-check-circle text-green-400 text-xl';
  773. }
  774. t.classList.remove('translate-x-full', 'opacity-0');
  775. setTimeout(() => t.classList.add('translate-x-full', 'opacity-0'), 3000);
  776. }
  777. // ==========================================
  778. // 链接弹窗动态处理
  779. // ==========================================
  780. function showLinkModal(menuType = null, listIndex = null) {
  781. const targetMenu = menuType || currentMenu;
  782. if (targetMenu === 'home') {
  783. currentLinkAssemblyId = 'home';
  784. } else if (targetMenu === 'zone') {
  785. currentLinkAssemblyId = 'zone';
  786. } else if (targetMenu === 'assembly') {
  787. if (assemblyViewState === 'list' && listIndex !== null) {
  788. currentLinkAssemblyId = assemblyList[listIndex].id;
  789. } else if (assemblyViewState === 'edit' && tempAssemblyData) {
  790. if (!tempAssemblyData.id) {
  791. showToast('请先保存组装页再获取链接!', 'error');
  792. return;
  793. }
  794. currentLinkAssemblyId = tempAssemblyData.id;
  795. }
  796. }
  797. if (!currentLinkAssemblyId) return;
  798. document.getElementById('link-modal-content').innerHTML = `
  799. <div class="flex gap-4 mb-2">
  800. <div class="flex-1">
  801. <label class="block text-xs font-medium text-gray-700 mb-1">Campaign ID</label>
  802. <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">
  803. <option value="">请选择 (无)</option>
  804. <option value="CAMP202511">CAMP202511</option>
  805. <option value="CAMP_WINTER">CAMP_WINTER</option>
  806. </select>
  807. </div>
  808. <div class="flex-1">
  809. <label class="block text-xs font-medium text-gray-700 mb-1">Channel ID</label>
  810. <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">
  811. <option value="">请选择 (无)</option>
  812. <option value="CH_APP">APP推送</option>
  813. <option value="CH_WECHAT">微信公众号</option>
  814. </select>
  815. </div>
  816. </div>
  817. <div id="modal-links-container" class="space-y-4"></div>
  818. `;
  819. updateModalLinks();
  820. document.getElementById('link-modal-backdrop').classList.remove('hidden');
  821. document.getElementById('link-modal').classList.remove('hidden');
  822. document.getElementById('link-modal').classList.add('flex');
  823. }
  824. function updateModalLinks() {
  825. if(!currentLinkAssemblyId) return;
  826. const campId = document.getElementById('modal-campaign-id').value;
  827. const chanId = document.getElementById('modal-channel-id').value;
  828. let queryParams = [];
  829. if(campId) queryParams.push(`CampaignId=${campId}`);
  830. if(chanId) queryParams.push(`Channelid=${chanId}`);
  831. const queryStr = queryParams.length > 0 ? (currentLinkAssemblyId === 'home' || currentLinkAssemblyId === 'zone' ? '?' : '&') + queryParams.join('&') : '';
  832. let basePath = '';
  833. let isPublished = false;
  834. if (currentLinkAssemblyId === 'home') {
  835. basePath = 'home';
  836. isPublished = pageStatus.home === 'published';
  837. } else if (currentLinkAssemblyId === 'zone') {
  838. basePath = 'zone';
  839. isPublished = pageStatus.zone === 'published';
  840. } else {
  841. basePath = `assembly?id=${currentLinkAssemblyId}`;
  842. const asm = assemblyList.find(a => a.id === currentLinkAssemblyId) || tempAssemblyData;
  843. isPublished = asm && asm.status === 'published';
  844. }
  845. const prodH5Url = `https://hsbc.com/${basePath}${queryStr}`;
  846. const prodAppUrl = `hsbc-app://${basePath}${queryStr}`;
  847. const draftJoiner = (basePath === 'home' || basePath === 'zone') ? '?' : '&';
  848. const testUrl = `https://test.hsbc.com/${basePath}${draftJoiner}draft=1${queryStr.replace('?', '&')}`;
  849. let html = '';
  850. if (isPublished) {
  851. html += `
  852. <div class="bg-white border border-gray-200 rounded-lg p-4 shadow-sm relative">
  853. <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>
  854. <div class="space-y-2">
  855. <div class="flex gap-2 items-center">
  856. <span class="text-xs text-gray-500 w-16">H5 链接</span>
  857. <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">
  858. <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>
  859. </div>
  860. <div class="flex gap-2 items-center">
  861. <span class="text-xs text-gray-500 w-16">小程序/APP</span>
  862. <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">
  863. <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>
  864. </div>
  865. </div>
  866. </div>
  867. `;
  868. }
  869. html += `
  870. <div class="bg-white border border-gray-200 rounded-lg p-4 shadow-sm relative">
  871. <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>
  872. <div class="flex gap-2 items-center">
  873. <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">
  874. <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>
  875. </div>
  876. </div>
  877. `;
  878. document.getElementById('modal-links-container').innerHTML = html;
  879. }
  880. function closeLinkModal() {
  881. document.getElementById('link-modal-backdrop').classList.add('hidden');
  882. document.getElementById('link-modal').classList.add('hidden');
  883. document.getElementById('link-modal').classList.remove('flex');
  884. currentLinkAssemblyId = null;
  885. }
  886. function copyToClipboard(text) {
  887. navigator.clipboard.writeText(text).then(() => showToast('链接已复制到剪贴板!'));
  888. }
  889. function toggleTagDropdown(e, i) { e.stopPropagation(); openTagDropdownIndex = (openTagDropdownIndex === i) ? null : i; renderLayout(); }
  890. function filterTags(e, i) {
  891. const k = e.target.value.toLowerCase();
  892. document.querySelectorAll(`#tag-list-${i} label`).forEach(l => {
  893. l.style.display = l.innerText.toLowerCase().includes(k) ? 'flex' : 'none';
  894. });
  895. }
  896. // ==========================================
  897. // 组装页模块增删复制逻辑
  898. // ==========================================
  899. function handleCreateNewAssembly() {
  900. editAssemblyIndex = null;
  901. tempAssemblyData = createEmptyAssembly('');
  902. assemblyViewState = 'edit';
  903. markAsModified();
  904. renderLayout();
  905. }
  906. function editAssembly(index) {
  907. editAssemblyIndex = index;
  908. tempAssemblyData = JSON.parse(JSON.stringify(assemblyList[index]));
  909. assemblyViewState = 'edit';
  910. renderLayout();
  911. }
  912. function deleteAssembly(index) {
  913. if(confirm('确认要删除该组装页吗?此操作不可恢复。')) {
  914. assemblyList.splice(index, 1);
  915. showToast('删除成功');
  916. renderLayout();
  917. }
  918. }
  919. function backToAssemblyList() {
  920. editAssemblyIndex = null;
  921. tempAssemblyData = null;
  922. assemblyViewState = 'list';
  923. renderLayout();
  924. }
  925. function updateAssemblyName(val) {
  926. if (getCurrentAssemblyData()) {
  927. getCurrentAssemblyData().name = val;
  928. markAsModified();
  929. }
  930. }
  931. function addAssemblyBlock(type) {
  932. let newBlock = { type: type, id: generateBlockId(), data: {} };
  933. if (type === 'image') newBlock.data = { imageUrl: '' };
  934. if (type === 'privilege') newBlock.data = { title: '新权益标题', items: [{ id:'1', title:'模拟权益', desc:'描述' }] };
  935. if (type === 'richtext') newBlock.data = { content: '<p>输入富文本内容...</p>' };
  936. if (type === 'buttonGroup') newBlock.data = { buttons: [{ text: '点击跳转', link: '', style: 'primary', actionType: 'link' }] };
  937. getCurrentAssemblyData().blocks.push(newBlock);
  938. markAsModified();
  939. renderLayout();
  940. }
  941. function duplicateAssemblyBlock(index) {
  942. let block = getCurrentAssemblyData().blocks[index];
  943. if (block.type === 'privilege') {
  944. alert('权益商品模块每页仅限配置一个,无法复制!');
  945. return;
  946. }
  947. let newBlock = JSON.parse(JSON.stringify(block));
  948. newBlock.id = generateBlockId(); // 新ID防止重复
  949. getCurrentAssemblyData().blocks.splice(index + 1, 0, newBlock);
  950. markAsModified();
  951. renderLayout();
  952. }
  953. function removeAssemblyBlock(index) {
  954. if(confirm('确定删除该模块吗?')) {
  955. getCurrentAssemblyData().blocks.splice(index, 1);
  956. markAsModified();
  957. renderLayout();
  958. }
  959. }
  960. function addAssemblyButton(blockIndex) {
  961. tempAssemblyData.blocks[blockIndex].data.buttons.push({
  962. text: '新按钮', link: '', style: 'secondary',
  963. actionType: 'link', dialog: { title: '', desc: '', confirmText: '确认', confirmLink: '' }
  964. });
  965. markAsModified();
  966. renderLayout();
  967. }
  968. function updateAssemblyButtonDialog(blockIndex, btnIndex, field, value) {
  969. let block = tempAssemblyData.blocks[blockIndex];
  970. if (!block.data.buttons[btnIndex].dialog) {
  971. block.data.buttons[btnIndex].dialog = { title: '', desc: '', confirmText: '确认', confirmLink: '' };
  972. }
  973. block.data.buttons[btnIndex].dialog[field] = value;
  974. markAsModified();
  975. }
  976. switchMenu('home'); // Init
  977. </script>
  978. </body>
  979. </html>