feat: 新增主题配置管理页面,支持企业信息、侧边浮窗、反馈类型、SEO和首页轮播设置,并更新了样式。
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
yiqiu
2025-12-28 13:57:10 +08:00
parent dbe2090549
commit feddfc2e64
5 changed files with 2800 additions and 440 deletions

View File

@@ -1,10 +1,86 @@
<link rel="stylesheet" href="/plugins/addon/theme_configurator/template/admin/theme.css" /> <link rel="stylesheet" href="/plugins/addon/theme_configurator/template/admin/theme.css" />
<div id="theme-config-app" class="template" v-cloak> <div id="theme-config-app" class="admin-container" v-cloak>
<t-tabs v-model="activeTab" placement="top"> <!-- 顶部工具栏 -->
<t-tab-panel value="basic" label="基础信息"> <header class="admin-header">
<t-card class="theme-card" title="企业信息" bordered> <div class="admin-header__left">
<div class="form-grid"> <div class="admin-logo">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>黑果云模板控制器</span>
</div>
</div>
<div class="admin-header__right">
<button class="btn btn-primary btn-lg" @click="saveConfig" :disabled="saving">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" v-if="!saving">
<path d="M13.5 2.5H2.5V13.5H13.5V2.5Z" stroke="currentColor" stroke-width="1.5"/>
<path d="M10.5 2.5V6.5H5.5V2.5" stroke="currentColor" stroke-width="1.5"/>
<path d="M5.5 9.5H10.5V13.5H5.5V9.5Z" stroke="currentColor" stroke-width="1.5"/>
</svg>
<span v-if="saving">保存中...</span>
<span v-else>保存全部配置</span>
</button>
</div>
</header>
<!-- 主体布局 -->
<div class="admin-layout">
<!-- 侧边栏导航 -->
<aside class="admin-sidebar">
<nav class="sidebar-nav">
<a href="#basic" class="nav-item" :class="{active: activeSection === 'basic'}" @click="activeSection = 'basic'">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" stroke-width="2"/>
<line x1="3" y1="9" x2="21" y2="9" stroke="currentColor" stroke-width="2"/>
<line x1="9" y1="21" x2="9" y2="9" stroke="currentColor" stroke-width="2"/>
</svg>
<span>基础配置</span>
</a>
<a href="#seo" class="nav-item" :class="{active: activeSection === 'seo'}" @click="activeSection = 'seo'">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<circle cx="11" cy="11" r="8" stroke="currentColor" stroke-width="2"/>
<path d="M21 21L16.65 16.65" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
<span>SEO设置</span>
</a>
<a href="#home" class="nav-item" :class="{active: activeSection === 'home'}" @click="activeSection = 'home'">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<path d="M3 9L12 2L21 9V20C21 20.5304 20.7893 21.0391 20.4142 21.4142C20.0391 21.7893 19.5304 22 19 22H5C4.46957 22 3.96086 21.7893 3.58579 21.4142C3.21071 21.0391 3 20.5304 3 20V9Z" stroke="currentColor" stroke-width="2"/>
</svg>
<span>首页内容</span>
</a>
<a href="#nav" class="nav-item" :class="{active: activeSection === 'nav'}" @click="activeSection = 'nav'">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<line x1="3" y1="12" x2="21" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="3" y1="6" x2="21" y2="6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<line x1="3" y1="18" x2="21" y2="18" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
<span>导航配置</span>
</a>
<a href="#advanced" class="nav-item" :class="{active: activeSection === 'advanced'}" @click="activeSection = 'advanced'">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2"/>
<path d="M12 1V3M12 21V23M4.22 4.22L5.64 5.64M18.36 18.36L19.78 19.78M1 12H3M21 12H23M4.22 19.78L5.64 18.36M18.36 5.64L19.78 4.22" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
<span>高级设置</span>
</a>
</nav>
</aside>
<!-- 主内容区 -->
<main class="admin-main">
<section id="basic" class="config-section" :class="{active: activeSection === 'basic'}">
<div class="section-card">
<div class="section-header">
<h2>企业信息</h2>
</div>
<div class="section-body">
<div class="form-fields">
<div class="form-item"> <div class="form-item">
<label>企业名称</label> <label>企业名称</label>
<t-input v-model="fullConfig.site_config.enterprise_name" placeholder="主题云"></t-input> <t-input v-model="fullConfig.site_config.enterprise_name" placeholder="主题云"></t-input>
@@ -17,9 +93,9 @@
<label>联系邮箱</label> <label>联系邮箱</label>
<t-input v-model="fullConfig.site_config.enterprise_mailbox" placeholder="support@example.com"></t-input> <t-input v-model="fullConfig.site_config.enterprise_mailbox" placeholder="support@example.com"></t-input>
</div> </div>
<div class="form-item form-item--full"> <div class="form-item form-item">
<label>Logo 地址</label> <label>Logo 地址</label>
<div class="upload-row"> <div class="upload-control">
<t-input v-model="fullConfig.site_config.official_website_logo" placeholder="/upload/logo.png"></t-input> <t-input v-model="fullConfig.site_config.official_website_logo" placeholder="/upload/logo.png"></t-input>
<t-upload theme="custom" :action="uploadUrl" :headers="uploadHeaders" <t-upload theme="custom" :action="uploadUrl" :headers="uploadHeaders"
:format-response="uploadFormatResponse" :show-upload-progress="false" :max="1" :format-response="uploadFormatResponse" :show-upload-progress="false" :max="1"
@@ -30,9 +106,9 @@
</t-upload> </t-upload>
</div> </div>
</div> </div>
<div class="form-item form-item--full"> <div class="form-item form-item">
<label>二维码地址</label> <label>二维码地址</label>
<div class="upload-row"> <div class="upload-control">
<t-input v-model="fullConfig.site_config.enterprise_qrcode" placeholder="/upload/qrcode.png"></t-input> <t-input v-model="fullConfig.site_config.enterprise_qrcode" placeholder="/upload/qrcode.png"></t-input>
<t-upload theme="custom" :action="uploadUrl" :headers="uploadHeaders" <t-upload theme="custom" :action="uploadUrl" :headers="uploadHeaders"
:format-response="uploadFormatResponse" :show-upload-progress="false" :max="1" :format-response="uploadFormatResponse" :show-upload-progress="false" :max="1"
@@ -86,19 +162,23 @@
<label>物理机/DCIM 链接</label> <label>物理机/DCIM 链接</label>
<t-input v-model="fullConfig.site_config.dcim_product_link" placeholder="/cart/goods.htm?id=2"></t-input> <t-input v-model="fullConfig.site_config.dcim_product_link" placeholder="/cart/goods.htm?id=2"></t-input>
</div> </div>
<div class="form-item form-item--full"> <div class="form-item form-item">
<label>版权信息</label> <label>版权信息</label>
<t-input v-model="fullConfig.site_config.copyright_info" placeholder="© 2025 主题云"></t-input> <t-input v-model="fullConfig.site_config.copyright_info" placeholder="© 2025 主题云"></t-input>
</div> </div>
</div> </div>
</t-card> </div>
<t-card class="theme-card" title="侧边浮窗" bordered> <div class="section-card">
<p class="theme-tip"> <div class="section-header">
<h2>侧边浮窗</h2>
</div>
<div class="section-body">
<p class="alert alert-info">
对应前台右侧悬浮工具条(电话咨询/在线客服/提交工单等),结构与模板中的 对应前台右侧悬浮工具条(电话咨询/在线客服/提交工单等),结构与模板中的
<code>side_floating_window</code> 一致。 <code>side_floating_window</code> 一致。
</p> </p>
<div v-if="!fullConfig.side.length" class="empty-tip"> <div v-if="!fullConfig.side.length" class="empty-state">
还没有侧边浮窗,点击下方按钮添加。 还没有侧边浮窗,点击下方按钮添加。
</div> </div>
<div class="config-item" v-for="(item, index) in fullConfig.side" :key="'side-' + index"> <div class="config-item" v-for="(item, index) in fullConfig.side" :key="'side-' + index">
@@ -108,14 +188,14 @@
删除 删除
</t-button> </t-button>
</div> </div>
<div class="form-grid"> <div class="form-fields">
<div class="form-item"> <div class="form-item">
<label>名称</label> <label>名称</label>
<t-input v-model="item.name" placeholder="电话咨询"></t-input> <t-input v-model="item.name" placeholder="电话咨询"></t-input>
</div> </div>
<div class="form-item form-item--full"> <div class="form-item form-item">
<label>图标地址</label> <label>图标地址</label>
<div class="upload-row"> <div class="upload-control">
<t-input v-model="item.icon" placeholder="/upload/side-phone.png"></t-input> <t-input v-model="item.icon" placeholder="/upload/side-phone.png"></t-input>
<t-upload theme="custom" :action="uploadUrl" :headers="uploadHeaders" <t-upload theme="custom" :action="uploadUrl" :headers="uploadHeaders"
:format-response="uploadFormatResponse" :show-upload-progress="false" :max="1" :format-response="uploadFormatResponse" :show-upload-progress="false" :max="1"
@@ -126,7 +206,7 @@
</t-upload> </t-upload>
</div> </div>
</div> </div>
<div class="form-item form-item--full"> <div class="form-item form-item">
<label>内容(支持 HTML</label> <label>内容(支持 HTML</label>
<t-textarea v-model="item.content" :autosize="{ minRows: 2, maxRows: 4 }" <t-textarea v-model="item.content" :autosize="{ minRows: 2, maxRows: 4 }"
placeholder="<p>7x24 小时不间断服务</p>"></t-textarea> placeholder="<p>7x24 小时不间断服务</p>"></t-textarea>
@@ -134,13 +214,17 @@
</div> </div>
</div> </div>
<t-button theme="primary" variant="outline" @click="addSide">新增浮窗</t-button> <t-button theme="primary" variant="outline" @click="addSide">新增浮窗</t-button>
</t-card> </div>
<t-card class="theme-card" title="反馈类型" bordered> <div class="section-card">
<p class="theme-tip"> <div class="section-header">
<h2>反馈类型</h2>
</div>
<div class="section-body">
<p class="alert alert-info">
对应 <code>/console/v1/feedback</code> 的类型选项ID 需与后端保持一致,仅建议修改名称与描述。 对应 <code>/console/v1/feedback</code> 的类型选项ID 需与后端保持一致,仅建议修改名称与描述。
</p> </p>
<div v-if="!fullConfig.feedback_type.length" class="empty-tip"> <div v-if="!fullConfig.feedback_type.length" class="empty-state">
还没有反馈类型,点击下方按钮添加。 还没有反馈类型,点击下方按钮添加。
</div> </div>
<div class="config-item" v-for="(item, index) in fullConfig.feedback_type" :key="'feedback-' + index"> <div class="config-item" v-for="(item, index) in fullConfig.feedback_type" :key="'feedback-' + index">
@@ -150,7 +234,7 @@
删除 删除
</t-button> </t-button>
</div> </div>
<div class="form-grid"> <div class="form-fields">
<div class="form-item"> <div class="form-item">
<label>类型 ID</label> <label>类型 ID</label>
<t-input v-model="item.id" placeholder="1"></t-input> <t-input v-model="item.id" placeholder="1"></t-input>
@@ -159,19 +243,23 @@
<label>名称</label> <label>名称</label>
<t-input v-model="item.name" placeholder="产品建议"></t-input> <t-input v-model="item.name" placeholder="产品建议"></t-input>
</div> </div>
<div class="form-item form-item--full"> <div class="form-item form-item">
<label>描述</label> <label>描述</label>
<t-input v-model="item.description" placeholder="用于产品体验、功能建议等"></t-input> <t-input v-model="item.description" placeholder="用于产品体验、功能建议等"></t-input>
</div> </div>
</div> </div>
</div> </div>
<t-button theme="primary" variant="outline" @click="addFeedbackType">新增反馈类型</t-button> <t-button theme="primary" variant="outline" @click="addFeedbackType">新增反馈类型</t-button>
</t-card> </div>
</t-tab-panel> </section>
<t-tab-panel value="seo" label="SEO 管理"> <section id="seo" class="config-section" :class="{active: activeSection === 'seo'}">
<t-card class="theme-card" title="SEO 设置" bordered> <div class="section-card">
<div class="form-grid"> <div class="section-header">
<h2>SEO 设置</h2>
</div>
<div class="section-body">
<div class="form-fields">
<div class="form-item"> <div class="form-item">
<label>站点标题</label> <label>站点标题</label>
<t-input v-model="fullConfig.seo.title" placeholder="首页标题"></t-input> <t-input v-model="fullConfig.seo.title" placeholder="首页标题"></t-input>
@@ -180,18 +268,22 @@
<label>关键词</label> <label>关键词</label>
<t-input v-model="fullConfig.seo.keywords" placeholder="关键词,逗号分隔"></t-input> <t-input v-model="fullConfig.seo.keywords" placeholder="关键词,逗号分隔"></t-input>
</div> </div>
<div class="form-item form-item--full"> <div class="form-item form-item">
<label>描述</label> <label>描述</label>
<t-textarea v-model="fullConfig.seo.description" :autosize="{ minRows: 2, maxRows: 4 }" <t-textarea v-model="fullConfig.seo.description" :autosize="{ minRows: 2, maxRows: 4 }"
placeholder="站点描述"></t-textarea> placeholder="站点描述"></t-textarea>
</div> </div>
</div> </div>
</t-card> </div>
</t-tab-panel> </section>
<t-tab-panel value="home" label="首页内容"> <section id="home" class="config-section" :class="{active: activeSection === 'home'}">
<t-card class="theme-card" title="首页轮播文案" bordered> <div class="section-card">
<div v-if="!bannerList.length" class="empty-tip"> <div class="section-header">
<h2>首页轮播文案</h2>
</div>
<div class="section-body">
<div v-if="!bannerList.length" class="empty-state">
还没有轮播文案,点击下方按钮添加。 还没有轮播文案,点击下方按钮添加。
</div> </div>
<div class="banner-item" v-for="(banner, index) in bannerList" :key="index"> <div class="banner-item" v-for="(banner, index) in bannerList" :key="index">
@@ -201,14 +293,14 @@
删除 删除
</t-button> </t-button>
</div> </div>
<div class="form-grid"> <div class="form-fields">
<div class="form-item"> <div class="form-item">
<label>标题</label> <label>标题</label>
<t-input v-model="banner.title" placeholder="如:弹性算力"></t-input> <t-input v-model="banner.title" placeholder="如:弹性算力"></t-input>
</div> </div>
<div class="form-item form-item--full"> <div class="form-item form-item">
<label>图片地址</label> <label>图片地址</label>
<div class="upload-row"> <div class="upload-control">
<t-input v-model="banner.img" placeholder="/upload/banner-1.png"></t-input> <t-input v-model="banner.img" placeholder="/upload/banner-1.png"></t-input>
<t-upload theme="custom" :action="uploadUrl" :headers="uploadHeaders" <t-upload theme="custom" :action="uploadUrl" :headers="uploadHeaders"
:format-response="uploadFormatResponse" :show-upload-progress="false" :max="1" :format-response="uploadFormatResponse" :show-upload-progress="false" :max="1"
@@ -223,7 +315,7 @@
<label>描述</label> <label>描述</label>
<t-input v-model="banner.description" placeholder="一句宣传语"></t-input> <t-input v-model="banner.description" placeholder="一句宣传语"></t-input>
</div> </div>
<div class="form-item form-item--full"> <div class="form-item form-item">
<label>标签</label> <label>标签</label>
<t-input v-model="banner.tags" placeholder="如:高速,低价,安全(多个标签用逗号分隔)"></t-input> <t-input v-model="banner.tags" placeholder="如:高速,低价,安全(多个标签用逗号分隔)"></t-input>
</div> </div>
@@ -231,7 +323,7 @@
<label>跳转链接</label> <label>跳转链接</label>
<t-input v-model="banner.url" placeholder="/cloud.html"></t-input> <t-input v-model="banner.url" placeholder="/cloud.html"></t-input>
</div> </div>
<div class="form-item form-item--switch"> <div class="form-item form-switch">
<label><span class="switch-label-icon">🔗</span>新窗口</label> <label><span class="switch-label-icon">🔗</span>新窗口</label>
<t-switch size="large" v-model="banner.blank"></t-switch> <t-switch size="large" v-model="banner.blank"></t-switch>
</div> </div>
@@ -243,17 +335,21 @@
<label>按钮链接</label> <label>按钮链接</label>
<t-input v-model="banner.button_link" placeholder="/cloud.html"></t-input> <t-input v-model="banner.button_link" placeholder="/cloud.html"></t-input>
</div> </div>
<div class="form-item form-item--switch"> <div class="form-item form-switch">
<label><span class="switch-label-icon">🔗</span>按钮新窗口</label> <label><span class="switch-label-icon">🔗</span>按钮新窗口</label>
<t-switch size="large" v-model="banner.button_blank"></t-switch> <t-switch size="large" v-model="banner.button_blank"></t-switch>
</div> </div>
</div> </div>
</div> </div>
<t-button theme="primary" variant="outline" @click="addBanner">新增轮播</t-button> <t-button theme="primary" variant="outline" @click="addBanner">新增轮播</t-button>
</t-card> </div>
<t-card class="theme-card" title="企业资质与荣誉" bordered> <div class="section-card">
<div v-if="!fullConfig.honor.length" class="empty-tip"> <div class="section-header">
<h2>企业资质与荣誉</h2>
</div>
<div class="section-body">
<div v-if="!fullConfig.honor.length" class="empty-state">
用于首页"荣誉资质"模块honor 用于首页"荣誉资质"模块honor
</div> </div>
<div class="config-item" v-for="(item, index) in fullConfig.honor" :key="'honor-' + index"> <div class="config-item" v-for="(item, index) in fullConfig.honor" :key="'honor-' + index">
@@ -263,14 +359,14 @@
删除 删除
</t-button> </t-button>
</div> </div>
<div class="form-grid"> <div class="form-fields">
<div class="form-item"> <div class="form-item">
<label>名称</label> <label>名称</label>
<t-input v-model="item.name" placeholder="高新技术企业"></t-input> <t-input v-model="item.name" placeholder="高新技术企业"></t-input>
</div> </div>
<div class="form-item form-item--full"> <div class="form-item form-item">
<label>图片地址</label> <label>图片地址</label>
<div class="upload-row"> <div class="upload-control">
<t-input v-model="item.img" placeholder="/upload/honor.png"></t-input> <t-input v-model="item.img" placeholder="/upload/honor.png"></t-input>
<t-upload theme="custom" :action="uploadUrl" :headers="uploadHeaders" <t-upload theme="custom" :action="uploadUrl" :headers="uploadHeaders"
:format-response="uploadFormatResponse" :show-upload-progress="false" :max="1" :format-response="uploadFormatResponse" :show-upload-progress="false" :max="1"
@@ -284,24 +380,28 @@
</div> </div>
</div> </div>
<t-button theme="primary" variant="outline" @click="addHonor">新增荣誉</t-button> <t-button theme="primary" variant="outline" @click="addHonor">新增荣誉</t-button>
</t-card> </div>
</t-tab-panel> </section>
<t-tab-panel value="nav" label="导航配置"> <section id="nav" class="config-section" :class="{active: activeSection === 'nav'}">
<t-card class="theme-card" title="顶部导航header_nav" bordered> <div class="section-card">
<p class="theme-tip"> <div class="section-header">
<h2>顶部导航header_nav</h2>
</div>
<div class="section-body">
<p class="alert alert-info">
控制站点顶部导航栏及其下拉菜单结构。第一个导航项用作 Logo 点击跳转地址。 控制站点顶部导航栏及其下拉菜单结构。第一个导航项用作 Logo 点击跳转地址。
</p> </p>
<div class="form-grid"> <div class="form-fields">
<div class="form-item form-item--full"> <div class="form-item form-item">
<label>Logo 点击跳转地址</label> <label>Logo 点击跳转地址</label>
<t-input v-model="homeNav.file_address" placeholder="index.html 或 /index.html"></t-input> <t-input v-model="homeNav.file_address" placeholder="index.html 或 /index.html"></t-input>
</div> </div>
</div> </div>
<h4 class="sub-title mt-10">一级导航</h4> <h4 class="sub-title mt-10">一级导航</h4>
<div v-if="headerNavList.length <= 1" class="empty-tip"> <div v-if="headerNavList.length <= 1" class="empty-state">
还没有自定义导航,请点击下方按钮新增。 还没有自定义导航,请点击下方按钮新增。
</div> </div>
<div class="config-item" v-for="(item, index) in headerNavList" :key="'nav-' + index" v-if="index > 0"> <div class="config-item" v-for="(item, index) in headerNavList" :key="'nav-' + index" v-if="index > 0">
@@ -311,7 +411,7 @@
删除 删除
</t-button> </t-button>
</div> </div>
<div class="form-grid"> <div class="form-fields">
<div class="form-item"> <div class="form-item">
<label>名称</label> <label>名称</label>
<t-input v-model="item.name" placeholder="如:产品"></t-input> <t-input v-model="item.name" placeholder="如:产品"></t-input>
@@ -320,14 +420,14 @@
<label>点击链接(可选)</label> <label>点击链接(可选)</label>
<t-input v-model="item.file_address" placeholder="/cloud.html"></t-input> <t-input v-model="item.file_address" placeholder="/cloud.html"></t-input>
</div> </div>
<div class="form-item form-item--switch"> <div class="form-item form-switch">
<label><span class="switch-label-icon">🔗</span>新窗口</label> <label><span class="switch-label-icon">🔗</span>新窗口</label>
<t-switch v-model="item.blank"></t-switch> <t-switch v-model="item.blank"></t-switch>
</div> </div>
</div> </div>
<h4 class="sub-title mt-10">下拉子菜单</h4> <h4 class="sub-title mt-10">下拉子菜单</h4>
<div v-if="!getHeaderChildren(item).length" class="empty-tip"> <div v-if="!getHeaderChildren(item).length" class="empty-state">
还没有子菜单,点击下方按钮新增。 还没有子菜单,点击下方按钮新增。
</div> </div>
<div class="config-item" v-for="(child, cIndex) in getHeaderChildren(item)" <div class="config-item" v-for="(child, cIndex) in getHeaderChildren(item)"
@@ -338,7 +438,7 @@
删除 删除
</t-button> </t-button>
</div> </div>
<div class="form-grid"> <div class="form-fields">
<div class="form-item"> <div class="form-item">
<label>名称</label> <label>名称</label>
<t-input v-model="child.name" placeholder="如:云服务器"></t-input> <t-input v-model="child.name" placeholder="如:云服务器"></t-input>
@@ -347,13 +447,13 @@
<label>链接地址</label> <label>链接地址</label>
<t-input v-model="child.file_address" placeholder="/cloud.html"></t-input> <t-input v-model="child.file_address" placeholder="/cloud.html"></t-input>
</div> </div>
<div class="form-item form-item--switch"> <div class="form-item form-switch">
<label><span class="switch-label-icon">🔗</span>新窗口</label> <label><span class="switch-label-icon">🔗</span>新窗口</label>
<t-switch v-model="child.blank"></t-switch> <t-switch v-model="child.blank"></t-switch>
</div> </div>
<div class="form-item form-item--full"> <div class="form-item form-item">
<label>图标地址</label> <label>图标地址</label>
<div class="upload-row"> <div class="upload-control">
<t-input v-model="child.icon" placeholder="/upload/nav-icon.png"></t-input> <t-input v-model="child.icon" placeholder="/upload/nav-icon.png"></t-input>
<t-upload theme="custom" :action="uploadUrl" :headers="uploadHeaders" <t-upload theme="custom" :action="uploadUrl" :headers="uploadHeaders"
:format-response="uploadFormatResponse" :show-upload-progress="false" :max="1" @success=" :format-response="uploadFormatResponse" :show-upload-progress="false" :max="1" @success="
@@ -365,7 +465,7 @@
</t-upload> </t-upload>
</div> </div>
</div> </div>
<div class="form-item form-item--full"> <div class="form-item form-item">
<label>描述</label> <label>描述</label>
<t-textarea v-model="child.description" :autosize="{ minRows: 2, maxRows: 3 }" <t-textarea v-model="child.description" :autosize="{ minRows: 2, maxRows: 3 }"
placeholder="如:高可用的弹性计算服务"></t-textarea> placeholder="如:高可用的弹性计算服务"></t-textarea>
@@ -379,16 +479,20 @@
<t-button class="mt-10" theme="primary" variant="outline" @click="addHeaderNav"> <t-button class="mt-10" theme="primary" variant="outline" @click="addHeaderNav">
新增一级导航 新增一级导航
</t-button> </t-button>
</t-card> </div>
</t-tab-panel> </section>
<t-tab-panel value="footer" label="底部栏目"> <section id="footer" class="config-section" :class="{active: activeSection === 'footer'}">
<t-card class="theme-card" title="底部栏目footer_nav" bordered> <div class="section-card">
<p class="theme-tip"> <div class="section-header">
<h2>底部栏目footer_nav</h2>
</div>
<div class="section-body">
<p class="alert alert-info">
控制首页底部多列链接(如【热门产品】【客户支持】等),结构与模板中的 控制首页底部多列链接(如【热门产品】【客户支持】等),结构与模板中的
<code>footer_nav</code> 一致。 <code>footer_nav</code> 一致。
</p> </p>
<div v-if="!footerNavList.length" class="empty-tip"> <div v-if="!footerNavList.length" class="empty-state">
还没有底部栏目,点击下方按钮新增。 还没有底部栏目,点击下方按钮新增。
</div> </div>
<div class="config-item" v-for="(group, index) in footerNavList" :key="'footer-' + index"> <div class="config-item" v-for="(group, index) in footerNavList" :key="'footer-' + index">
@@ -398,14 +502,14 @@
删除 删除
</t-button> </t-button>
</div> </div>
<div class="form-grid"> <div class="form-fields">
<div class="form-item"> <div class="form-item">
<label>栏目标题</label> <label>栏目标题</label>
<t-input v-model="group.name" placeholder="如:热门产品"></t-input> <t-input v-model="group.name" placeholder="如:热门产品"></t-input>
</div> </div>
</div> </div>
<h4 class="sub-title mt-10">栏目链接</h4> <h4 class="sub-title mt-10">栏目链接</h4>
<div v-if="!getFooterChildren(group).length" class="empty-tip"> <div v-if="!getFooterChildren(group).length" class="empty-state">
还没有链接,点击下方按钮新增。 还没有链接,点击下方按钮新增。
</div> </div>
<div class="config-item" v-for="(link, cIndex) in getFooterChildren(group)" <div class="config-item" v-for="(link, cIndex) in getFooterChildren(group)"
@@ -416,7 +520,7 @@
删除 删除
</t-button> </t-button>
</div> </div>
<div class="form-grid"> <div class="form-fields">
<div class="form-item"> <div class="form-item">
<label>名称</label> <label>名称</label>
<t-input v-model="link.name" placeholder="如:云服务器"></t-input> <t-input v-model="link.name" placeholder="如:云服务器"></t-input>
@@ -425,7 +529,7 @@
<label>链接地址</label> <label>链接地址</label>
<t-input v-model="link.url" placeholder="/cloud.html"></t-input> <t-input v-model="link.url" placeholder="/cloud.html"></t-input>
</div> </div>
<div class="form-item form-item--switch"> <div class="form-item form-switch">
<label><span class="switch-label-icon">🔗</span>新窗口</label> <label><span class="switch-label-icon">🔗</span>新窗口</label>
<t-switch v-model="link.blank"></t-switch> <t-switch v-model="link.blank"></t-switch>
</div> </div>
@@ -438,10 +542,14 @@
<t-button class="mt-10" theme="primary" variant="outline" @click="addFooterNav"> <t-button class="mt-10" theme="primary" variant="outline" @click="addFooterNav">
新增栏目 新增栏目
</t-button> </t-button>
</t-card> </div>
<t-card class="theme-card" title="友情链接" bordered> <div class="section-card">
<div v-if="!fullConfig.friendly_link.length" class="empty-tip"> <div class="section-header">
<h2>友情链接</h2>
</div>
<div class="section-body">
<div v-if="!fullConfig.friendly_link.length" class="empty-state">
还没有友情链接,点击下方按钮添加。 还没有友情链接,点击下方按钮添加。
</div> </div>
<div class="config-item" v-for="(item, index) in fullConfig.friendly_link" :key="index"> <div class="config-item" v-for="(item, index) in fullConfig.friendly_link" :key="index">
@@ -451,33 +559,37 @@
删除 删除
</t-button> </t-button>
</div> </div>
<div class="form-grid"> <div class="form-fields">
<div class="form-item"> <div class="form-item">
<label>名称</label> <label>名称</label>
<t-input v-model="item.name" placeholder="合作伙伴名称"></t-input> <t-input v-model="item.name" placeholder="合作伙伴名称"></t-input>
</div> </div>
<div class="form-item form-item--full"> <div class="form-item form-item">
<label>链接地址</label> <label>链接地址</label>
<t-input v-model="item.url" placeholder="https://example.com"></t-input> <t-input v-model="item.url" placeholder="https://example.com"></t-input>
</div> </div>
</div> </div>
</div> </div>
<t-button theme="primary" variant="outline" @click="addFriendlyLink">新增友情链接</t-button> <t-button theme="primary" variant="outline" @click="addFriendlyLink">新增友情链接</t-button>
</t-card> </div>
</t-tab-panel> </section>
<t-tab-panel v-if="showAdvanced" value="advanced" label="高级设置(JSON)"> <t-tab-panel v-if="showAdvanced" value="advanced" label="高级设置(JSON)">
<t-card class="theme-card" title="高级配置 (JSON)" bordered> <div class="section-card">
<p class="theme-tip"> <div class="section-header">
<h2>高级配置 (JSON)</h2>
</div>
<div class="section-body">
<p class="alert alert-info">
用于暂未在 UI 中开放的配置项(如复杂导航结构 header_nav/footer_nav 等)。如非必要,建议优先使用上方表单编辑。 用于暂未在 UI 中开放的配置项(如复杂导航结构 header_nav/footer_nav 等)。如非必要,建议优先使用上方表单编辑。
</p> </p>
<t-textarea v-model="advancedText" :autosize="{ minRows: 14 }" class="theme-textarea"></t-textarea> <t-textarea v-model="advancedText" :autosize="{ minRows: 14 }" class="json-editor"></t-textarea>
<div class="mt-10"> <div class="mt-10">
<t-button variant="outline" @click="syncJson">同步当前配置</t-button> <t-button variant="outline" @click="syncJson">同步当前配置</t-button>
<t-button class="ml-10" @click="applyAdvanced">应用 JSON</t-button> <t-button class="ml-10" @click="applyAdvanced">应用 JSON</t-button>
</div> </div>
</t-card> </div>
</t-tab-panel> </section>
</t-tabs> </t-tabs>
<div class="action-bar"> <div class="action-bar">
@@ -491,8 +603,16 @@
</div> </div>
</div> </div>
<script src="/plugins/addon/theme_configurator/template/admin/lang/index.js"></script> <script src="/plugins/addon/theme_configurator/template/admin/lang/index.js"> </main>
<script src="/plugins/addon/theme_configurator/template/admin/js/axios.min.js"></script> </div>
</div>
</script>
<script src="/plugins/addon/theme_configurator/template/admin/js/axios.min.js"> </main>
</div>
</div>
</script>
<script> <script>
(function () { (function () {
const host = location.origin; const host = location.origin;
@@ -552,7 +672,7 @@
fullConfig: createDefaultConfig(), fullConfig: createDefaultConfig(),
advancedText: "", advancedText: "",
showAdvanced: false, showAdvanced: false,
activeTab: "basic", activeSection: "basic",
uploadUrl: `${host}/${adminPath}/v1/upload`, uploadUrl: `${host}/${adminPath}/v1/upload`,
uploadHeaders: { uploadHeaders: {
Authorization: "Bearer " + localStorage.getItem("backJwt"), Authorization: "Bearer " + localStorage.getItem("backJwt"),
@@ -911,4 +1031,8 @@
}, },
}); });
})(); })();
</main>
</div>
</div>
</script> </script>

View File

@@ -0,0 +1,914 @@
<link rel="stylesheet" href="/plugins/addon/theme_configurator/template/admin/theme.css" />
<div id="theme-config-app" class="template" v-cloak>
<t-tabs v-model="activeTab" placement="top">
<t-tab-panel value="basic" label="基础信息">
<t-card class="theme-card" title="企业信息" bordered>
<div class="form-grid">
<div class="form-item">
<label>企业名称</label>
<t-input v-model="fullConfig.site_config.enterprise_name" placeholder="主题云"></t-input>
</div>
<div class="form-item">
<label>联系电话</label>
<t-input v-model="fullConfig.site_config.enterprise_telephone" placeholder="400-000-0000"></t-input>
</div>
<div class="form-item">
<label>联系邮箱</label>
<t-input v-model="fullConfig.site_config.enterprise_mailbox" placeholder="support@example.com"></t-input>
</div>
<div class="form-item form-item--full">
<label>Logo 地址</label>
<div class="upload-row">
<t-input v-model="fullConfig.site_config.official_website_logo" placeholder="/upload/logo.png"></t-input>
<t-upload theme="custom" :action="uploadUrl" :headers="uploadHeaders"
:format-response="uploadFormatResponse" :show-upload-progress="false" :max="1"
@success="(ctx) => handleUpload(['site_config', 'official_website_logo'], ctx)">
<t-button size="small" class="ml-10">
<t-icon name="upload" size="small" /> 上传
</t-button>
</t-upload>
</div>
</div>
<div class="form-item form-item--full">
<label>二维码地址</label>
<div class="upload-row">
<t-input v-model="fullConfig.site_config.enterprise_qrcode" placeholder="/upload/qrcode.png"></t-input>
<t-upload theme="custom" :action="uploadUrl" :headers="uploadHeaders"
:format-response="uploadFormatResponse" :show-upload-progress="false" :max="1"
@success="(ctx) => handleUpload(['site_config', 'enterprise_qrcode'], ctx)">
<t-button size="small" class="ml-10">
<t-icon name="upload" size="small" /> 上传
</t-button>
</t-upload>
</div>
</div>
<div class="form-item">
<label>在线客服链接</label>
<t-input v-model="fullConfig.site_config.online_customer_service_link"
placeholder="http://www.test.com"></t-input>
</div>
<div class="form-item">
<label>ICP 号</label>
<t-input v-model="fullConfig.site_config.icp_info" placeholder="京ICP备XXXX号"></t-input>
</div>
<div class="form-item">
<label>ICP 链接</label>
<t-input v-model="fullConfig.site_config.icp_info_link" placeholder="https://beian.miit.gov.cn"></t-input>
</div>
<div class="form-item">
<label>公安备案号</label>
<t-input v-model="fullConfig.site_config.public_security_network_preparation"
placeholder="京公网安备XXXX号"></t-input>
</div>
<div class="form-item">
<label>公安备案链接</label>
<t-input v-model="fullConfig.site_config.public_security_network_preparation_link"
placeholder="https://beian.mps.gov.cn"></t-input>
</div>
<div class="form-item">
<label>电信增值许可证</label>
<t-input v-model="fullConfig.site_config.telecom_appreciation" placeholder="增值电信业务经营许可证"></t-input>
</div>
<div class="form-item">
<label>用户协议链接</label>
<t-input v-model="fullConfig.site_config.terms_service_url" placeholder="/agreement/service.html"></t-input>
</div>
<div class="form-item">
<label>隐私政策链接</label>
<t-input v-model="fullConfig.site_config.terms_privacy_url" placeholder="/agreement/privacy.html"></t-input>
</div>
<div class="form-item">
<label>云产品购买链接</label>
<t-input v-model="fullConfig.site_config.cloud_product_link" placeholder="/cart/goods.htm?id=1"></t-input>
</div>
<div class="form-item">
<label>物理机/DCIM 链接</label>
<t-input v-model="fullConfig.site_config.dcim_product_link" placeholder="/cart/goods.htm?id=2"></t-input>
</div>
<div class="form-item form-item--full">
<label>版权信息</label>
<t-input v-model="fullConfig.site_config.copyright_info" placeholder="© 2025 主题云"></t-input>
</div>
</div>
</t-card>
<t-card class="theme-card" title="侧边浮窗" bordered>
<p class="theme-tip">
对应前台右侧悬浮工具条(电话咨询/在线客服/提交工单等),结构与模板中的
<code>side_floating_window</code> 一致。
</p>
<div v-if="!fullConfig.side.length" class="empty-tip">
还没有侧边浮窗,点击下方按钮添加。
</div>
<div class="config-item" v-for="(item, index) in fullConfig.side" :key="'side-' + index">
<div class="config-item__header">
<h4>浮窗 {{ index + 1 }}</h4>
<t-button size="small" theme="danger" variant="outline" @click="removeSide(index)">
删除
</t-button>
</div>
<div class="form-grid">
<div class="form-item">
<label>名称</label>
<t-input v-model="item.name" placeholder="电话咨询"></t-input>
</div>
<div class="form-item form-item--full">
<label>图标地址</label>
<div class="upload-row">
<t-input v-model="item.icon" placeholder="/upload/side-phone.png"></t-input>
<t-upload theme="custom" :action="uploadUrl" :headers="uploadHeaders"
:format-response="uploadFormatResponse" :show-upload-progress="false" :max="1"
@success="(ctx) => handleUpload(['side', index, 'icon'], ctx)">
<t-button size="small" class="ml-10">
<t-icon name="upload" size="small" /> 上传
</t-button>
</t-upload>
</div>
</div>
<div class="form-item form-item--full">
<label>内容(支持 HTML</label>
<t-textarea v-model="item.content" :autosize="{ minRows: 2, maxRows: 4 }"
placeholder="<p>7x24 小时不间断服务</p>"></t-textarea>
</div>
</div>
</div>
<t-button theme="primary" variant="outline" @click="addSide">新增浮窗</t-button>
</t-card>
<t-card class="theme-card" title="反馈类型" bordered>
<p class="theme-tip">
对应 <code>/console/v1/feedback</code> 的类型选项ID 需与后端保持一致,仅建议修改名称与描述。
</p>
<div v-if="!fullConfig.feedback_type.length" class="empty-tip">
还没有反馈类型,点击下方按钮添加。
</div>
<div class="config-item" v-for="(item, index) in fullConfig.feedback_type" :key="'feedback-' + index">
<div class="config-item__header">
<h4>类型 {{ index + 1 }}</h4>
<t-button size="small" theme="danger" variant="outline" @click="removeFeedbackType(index)">
删除
</t-button>
</div>
<div class="form-grid">
<div class="form-item">
<label>类型 ID</label>
<t-input v-model="item.id" placeholder="1"></t-input>
</div>
<div class="form-item">
<label>名称</label>
<t-input v-model="item.name" placeholder="产品建议"></t-input>
</div>
<div class="form-item form-item--full">
<label>描述</label>
<t-input v-model="item.description" placeholder="用于产品体验、功能建议等"></t-input>
</div>
</div>
</div>
<t-button theme="primary" variant="outline" @click="addFeedbackType">新增反馈类型</t-button>
</t-card>
</t-tab-panel>
<t-tab-panel value="seo" label="SEO 管理">
<t-card class="theme-card" title="SEO 设置" bordered>
<div class="form-grid">
<div class="form-item">
<label>站点标题</label>
<t-input v-model="fullConfig.seo.title" placeholder="首页标题"></t-input>
</div>
<div class="form-item">
<label>关键词</label>
<t-input v-model="fullConfig.seo.keywords" placeholder="关键词,逗号分隔"></t-input>
</div>
<div class="form-item form-item--full">
<label>描述</label>
<t-textarea v-model="fullConfig.seo.description" :autosize="{ minRows: 2, maxRows: 4 }"
placeholder="站点描述"></t-textarea>
</div>
</div>
</t-card>
</t-tab-panel>
<t-tab-panel value="home" label="首页内容">
<t-card class="theme-card" title="首页轮播文案" bordered>
<div v-if="!bannerList.length" class="empty-tip">
还没有轮播文案,点击下方按钮添加。
</div>
<div class="banner-item" v-for="(banner, index) in bannerList" :key="index">
<div class="banner-item__header">
<h4>轮播 {{ index + 1 }}</h4>
<t-button size="small" theme="danger" variant="outline" @click="removeBanner(index)">
删除
</t-button>
</div>
<div class="form-grid">
<div class="form-item">
<label>标题</label>
<t-input v-model="banner.title" placeholder="如:弹性算力"></t-input>
</div>
<div class="form-item form-item--full">
<label>图片地址</label>
<div class="upload-row">
<t-input v-model="banner.img" placeholder="/upload/banner-1.png"></t-input>
<t-upload theme="custom" :action="uploadUrl" :headers="uploadHeaders"
:format-response="uploadFormatResponse" :show-upload-progress="false" :max="1"
@success="(ctx) => handleUpload(['banner', index, 'img'], ctx)">
<t-button size="small" class="ml-10">
<t-icon name="upload" size="small" /> 上传
</t-button>
</t-upload>
</div>
</div>
<div class="form-item">
<label>描述</label>
<t-input v-model="banner.description" placeholder="一句宣传语"></t-input>
</div>
<div class="form-item form-item--full">
<label>标签</label>
<t-input v-model="banner.tags" placeholder="如:高速,低价,安全(多个标签用逗号分隔)"></t-input>
</div>
<div class="form-item">
<label>跳转链接</label>
<t-input v-model="banner.url" placeholder="/cloud.html"></t-input>
</div>
<div class="form-item form-item--switch">
<label><span class="switch-label-icon">🔗</span>新窗口</label>
<t-switch size="large" v-model="banner.blank"></t-switch>
</div>
<div class="form-item">
<label>按钮文字</label>
<t-input v-model="banner.button_text" placeholder="立即查看"></t-input>
</div>
<div class="form-item">
<label>按钮链接</label>
<t-input v-model="banner.button_link" placeholder="/cloud.html"></t-input>
</div>
<div class="form-item form-item--switch">
<label><span class="switch-label-icon">🔗</span>按钮新窗口</label>
<t-switch size="large" v-model="banner.button_blank"></t-switch>
</div>
</div>
</div>
<t-button theme="primary" variant="outline" @click="addBanner">新增轮播</t-button>
</t-card>
<t-card class="theme-card" title="企业资质与荣誉" bordered>
<div v-if="!fullConfig.honor.length" class="empty-tip">
用于首页"荣誉资质"模块honor
</div>
<div class="config-item" v-for="(item, index) in fullConfig.honor" :key="'honor-' + index">
<div class="config-item__header">
<h4>荣誉 {{ index + 1 }}</h4>
<t-button size="small" theme="danger" variant="outline" @click="removeHonor(index)">
删除
</t-button>
</div>
<div class="form-grid">
<div class="form-item">
<label>名称</label>
<t-input v-model="item.name" placeholder="高新技术企业"></t-input>
</div>
<div class="form-item form-item--full">
<label>图片地址</label>
<div class="upload-row">
<t-input v-model="item.img" placeholder="/upload/honor.png"></t-input>
<t-upload theme="custom" :action="uploadUrl" :headers="uploadHeaders"
:format-response="uploadFormatResponse" :show-upload-progress="false" :max="1"
@success="(ctx) => handleUpload(['honor', index, 'img'], ctx)">
<t-button size="small" class="ml-10">
<t-icon name="upload" size="small" /> 上传
</t-button>
</t-upload>
</div>
</div>
</div>
</div>
<t-button theme="primary" variant="outline" @click="addHonor">新增荣誉</t-button>
</t-card>
</t-tab-panel>
<t-tab-panel value="nav" label="导航配置">
<t-card class="theme-card" title="顶部导航header_nav" bordered>
<p class="theme-tip">
控制站点顶部导航栏及其下拉菜单结构。第一个导航项用作 Logo 点击跳转地址。
</p>
<div class="form-grid">
<div class="form-item form-item--full">
<label>Logo 点击跳转地址</label>
<t-input v-model="homeNav.file_address" placeholder="index.html 或 /index.html"></t-input>
</div>
</div>
<h4 class="sub-title mt-10">一级导航</h4>
<div v-if="headerNavList.length <= 1" class="empty-tip">
还没有自定义导航,请点击下方按钮新增。
</div>
<div class="config-item" v-for="(item, index) in headerNavList" :key="'nav-' + index" v-if="index > 0">
<div class="config-item__header">
<h4>导航 {{ index }}</h4>
<t-button size="small" theme="danger" variant="outline" @click="removeHeaderNav(index)">
删除
</t-button>
</div>
<div class="form-grid">
<div class="form-item">
<label>名称</label>
<t-input v-model="item.name" placeholder="如:产品"></t-input>
</div>
<div class="form-item">
<label>点击链接(可选)</label>
<t-input v-model="item.file_address" placeholder="/cloud.html"></t-input>
</div>
<div class="form-item form-item--switch">
<label><span class="switch-label-icon">🔗</span>新窗口</label>
<t-switch v-model="item.blank"></t-switch>
</div>
</div>
<h4 class="sub-title mt-10">下拉子菜单</h4>
<div v-if="!getHeaderChildren(item).length" class="empty-tip">
还没有子菜单,点击下方按钮新增。
</div>
<div class="config-item" v-for="(child, cIndex) in getHeaderChildren(item)"
:key="'nav-' + index + '-child-' + cIndex">
<div class="config-item__header">
<h4>子菜单 {{ cIndex + 1 }}</h4>
<t-button size="small" theme="danger" variant="outline" @click="removeHeaderNavChild(index, cIndex)">
删除
</t-button>
</div>
<div class="form-grid">
<div class="form-item">
<label>名称</label>
<t-input v-model="child.name" placeholder="如:云服务器"></t-input>
</div>
<div class="form-item">
<label>链接地址</label>
<t-input v-model="child.file_address" placeholder="/cloud.html"></t-input>
</div>
<div class="form-item form-item--switch">
<label><span class="switch-label-icon">🔗</span>新窗口</label>
<t-switch v-model="child.blank"></t-switch>
</div>
<div class="form-item form-item--full">
<label>图标地址</label>
<div class="upload-row">
<t-input v-model="child.icon" placeholder="/upload/nav-icon.png"></t-input>
<t-upload theme="custom" :action="uploadUrl" :headers="uploadHeaders"
:format-response="uploadFormatResponse" :show-upload-progress="false" :max="1" @success="
(ctx) => handleUpload(['header_nav', index, 'children', cIndex, 'icon'], ctx)
">
<t-button size="small" class="ml-10">
<t-icon name="upload" size="small" /> 上传
</t-button>
</t-upload>
</div>
</div>
<div class="form-item form-item--full">
<label>描述</label>
<t-textarea v-model="child.description" :autosize="{ minRows: 2, maxRows: 3 }"
placeholder="如:高可用的弹性计算服务"></t-textarea>
</div>
</div>
</div>
<t-button class="mt-10" theme="primary" size="small" variant="outline" @click="addHeaderNavChild(index)">
新增子菜单
</t-button>
</div>
<t-button class="mt-10" theme="primary" variant="outline" @click="addHeaderNav">
新增一级导航
</t-button>
</t-card>
</t-tab-panel>
<t-tab-panel value="footer" label="底部栏目">
<t-card class="theme-card" title="底部栏目footer_nav" bordered>
<p class="theme-tip">
控制首页底部多列链接(如【热门产品】【客户支持】等),结构与模板中的
<code>footer_nav</code> 一致。
</p>
<div v-if="!footerNavList.length" class="empty-tip">
还没有底部栏目,点击下方按钮新增。
</div>
<div class="config-item" v-for="(group, index) in footerNavList" :key="'footer-' + index">
<div class="config-item__header">
<h4>栏目 {{ index + 1 }}</h4>
<t-button size="small" theme="danger" variant="outline" @click="removeFooterNav(index)">
删除
</t-button>
</div>
<div class="form-grid">
<div class="form-item">
<label>栏目标题</label>
<t-input v-model="group.name" placeholder="如:热门产品"></t-input>
</div>
</div>
<h4 class="sub-title mt-10">栏目链接</h4>
<div v-if="!getFooterChildren(group).length" class="empty-tip">
还没有链接,点击下方按钮新增。
</div>
<div class="config-item" v-for="(link, cIndex) in getFooterChildren(group)"
:key="'footer-' + index + '-child-' + cIndex">
<div class="config-item__header">
<h4>链接 {{ cIndex + 1 }}</h4>
<t-button size="small" theme="danger" variant="outline" @click="removeFooterNavChild(index, cIndex)">
删除
</t-button>
</div>
<div class="form-grid">
<div class="form-item">
<label>名称</label>
<t-input v-model="link.name" placeholder="如:云服务器"></t-input>
</div>
<div class="form-item">
<label>链接地址</label>
<t-input v-model="link.url" placeholder="/cloud.html"></t-input>
</div>
<div class="form-item form-item--switch">
<label><span class="switch-label-icon">🔗</span>新窗口</label>
<t-switch v-model="link.blank"></t-switch>
</div>
</div>
</div>
<t-button class="mt-10" theme="primary" size="small" variant="outline" @click="addFooterNavChild(index)">
新增链接
</t-button>
</div>
<t-button class="mt-10" theme="primary" variant="outline" @click="addFooterNav">
新增栏目
</t-button>
</t-card>
<t-card class="theme-card" title="友情链接" bordered>
<div v-if="!fullConfig.friendly_link.length" class="empty-tip">
还没有友情链接,点击下方按钮添加。
</div>
<div class="config-item" v-for="(item, index) in fullConfig.friendly_link" :key="index">
<div class="config-item__header">
<h4>链接 {{ index + 1 }}</h4>
<t-button size="small" theme="danger" variant="outline" @click="removeFriendlyLink(index)">
删除
</t-button>
</div>
<div class="form-grid">
<div class="form-item">
<label>名称</label>
<t-input v-model="item.name" placeholder="合作伙伴名称"></t-input>
</div>
<div class="form-item form-item--full">
<label>链接地址</label>
<t-input v-model="item.url" placeholder="https://example.com"></t-input>
</div>
</div>
</div>
<t-button theme="primary" variant="outline" @click="addFriendlyLink">新增友情链接</t-button>
</t-card>
</t-tab-panel>
<t-tab-panel v-if="showAdvanced" value="advanced" label="高级设置(JSON)">
<t-card class="theme-card" title="高级配置 (JSON)" bordered>
<p class="theme-tip">
用于暂未在 UI 中开放的配置项(如复杂导航结构 header_nav/footer_nav 等)。如非必要,建议优先使用上方表单编辑。
</p>
<t-textarea v-model="advancedText" :autosize="{ minRows: 14 }" class="theme-textarea"></t-textarea>
<div class="mt-10">
<t-button variant="outline" @click="syncJson">同步当前配置</t-button>
<t-button class="ml-10" @click="applyAdvanced">应用 JSON</t-button>
</div>
</t-card>
</t-tab-panel>
</t-tabs>
<div class="action-bar">
<t-button variant="outline" @click="loadConfig" :loading="loading">重新加载</t-button>
<t-button theme="primary" class="ml-10" @click="saveConfig" :loading="saving">
保存全部配置
</t-button>
<t-button class="ml-10" variant="outline" @click="toggleAdvanced">
{{ showAdvanced ? "隐藏高级 JSON" : "显示高级 JSON" }}
</t-button>
</div>
</div>
<script src="/plugins/addon/theme_configurator/template/admin/lang/index.js"></script>
<script src="/plugins/addon/theme_configurator/template/admin/js/axios.min.js"></script>
<script>
(function () {
const host = location.origin;
const adminPath = location.pathname.split("/")[1];
const base = `${host}/${adminPath}/v1/theme/config`;
const showMessage = (vm, type, text) => {
if (vm.$message && typeof vm.$message[type] === "function") {
vm.$message[type](text);
} else if (window.MessagePlugin && typeof window.MessagePlugin[type] === "function") {
window.MessagePlugin[type](text);
} else {
alert(text);
}
};
const createDefaultConfig = () => ({
seo: {
title: "",
keywords: "",
description: "",
},
site_config: {
enterprise_name: "",
enterprise_telephone: "",
enterprise_mailbox: "",
enterprise_qrcode: "",
official_website_logo: "",
online_customer_service_link: "",
icp_info: "",
icp_info_link: "",
public_security_network_preparation: "",
public_security_network_preparation_link: "",
telecom_appreciation: "",
copyright_info: "",
terms_service_url: "",
terms_privacy_url: "",
cloud_product_link: "",
dcim_product_link: "",
},
banner: [],
header_nav: [],
footer_nav: [],
friendly_link: [],
side: [],
feedback_type: [],
honor: [],
partner: [],
});
new Vue({
el: "#theme-config-app",
data() {
return {
loading: false,
saving: false,
fullConfig: createDefaultConfig(),
advancedText: "",
showAdvanced: false,
activeTab: "basic",
uploadUrl: `${host}/${adminPath}/v1/upload`,
uploadHeaders: {
Authorization: "Bearer " + localStorage.getItem("backJwt"),
},
};
},
computed: {
bannerList() {
if (!Array.isArray(this.fullConfig.banner)) {
this.fullConfig.banner = [];
}
return this.fullConfig.banner;
},
headerNavList() {
if (!Array.isArray(this.fullConfig.header_nav)) {
this.fullConfig.header_nav = [];
}
if (this.fullConfig.header_nav.length === 0) {
this.fullConfig.header_nav.push({
name: "首页",
file_address: "index.html",
blank: false,
children: [],
});
}
return this.fullConfig.header_nav;
},
homeNav() {
return this.headerNavList[0];
},
footerNavList() {
if (!Array.isArray(this.fullConfig.footer_nav)) {
this.fullConfig.footer_nav = [];
}
return this.fullConfig.footer_nav;
},
},
methods: {
normalizeConfig(data = {}) {
const defaults = createDefaultConfig();
const merged = {
...defaults,
...data,
seo: {
...defaults.seo,
...(data.seo || {}),
},
site_config: {
...defaults.site_config,
...(data.site_config || {}),
},
banner: Array.isArray(data.banner) ? data.banner : [],
header_nav:
Array.isArray(data.header_nav) && data.header_nav.length
? data.header_nav
: [],
footer_nav: Array.isArray(data.footer_nav) ? data.footer_nav : [],
friendly_link: Array.isArray(data.friendly_link) ? data.friendly_link : [],
side: Array.isArray(data.side)
? data.side
: Array.isArray(data.side_floating_window)
? data.side_floating_window
: [],
feedback_type: Array.isArray(data.feedback_type)
? data.feedback_type
: Array.isArray(data.site_config && data.site_config.feedback_type)
? data.site_config.feedback_type
: [],
honor: Array.isArray(data.honor)
? data.honor
: Array.isArray(data.site_config && data.site_config.honor)
? data.site_config.honor
: [],
partner: Array.isArray(data.partner)
? data.partner
: Array.isArray(data.site_config && data.site_config.partner)
? data.site_config.partner
: [],
};
if (!Array.isArray(merged.header_nav) || merged.header_nav.length === 0) {
merged.header_nav = [
{
name: "首页",
file_address: "index.html",
blank: false,
children: [],
},
];
}
return merged;
},
uploadFormatResponse(res) {
if (!res || res.status !== 200) {
return { error: "上传失败" };
}
return res;
},
handleUpload(path, ctx) {
try {
const resp = ctx && ctx.file && ctx.file.response;
const data = resp && resp.data ? resp.data : resp;
if (!data) {
return showMessage(this, "error", "上传失败:未获取到响应数据");
}
// 优先使用后端返回的 image_url其次 url最后才是 save_name
let url = data.image_url || data.url || data.save_name || "";
if (!url) {
return showMessage(this, "error", "上传失败:未获取到文件地址");
}
// 相对路径前面补一个 /,保持站点根路径
if (!/^https?:\/\//i.test(url) && url.charAt(0) !== "/") {
url = "/" + url;
}
this.setConfigByPath(path, url);
} catch (e) {
showMessage(this, "error", "上传处理异常:" + e.message);
}
},
setConfigByPath(path, value) {
if (!Array.isArray(path) || !path.length) return;
let target = this.fullConfig;
for (let i = 0; i < path.length - 1; i++) {
const key = path[i];
if (typeof key === "number") {
if (!Array.isArray(target)) return;
target = target[key];
} else {
if (!target[key]) {
this.$set(target, key, {});
}
target = target[key];
}
if (!target) return;
}
const lastKey = path[path.length - 1];
if (typeof lastKey === "number") {
if (!Array.isArray(target)) return;
this.$set(target, lastKey, value);
} else {
this.$set(target, lastKey, value);
}
},
loadConfig() {
this.loading = true;
axios
.get(base, {
headers: {
Authorization: "Bearer " + localStorage.getItem("backJwt"),
},
})
.then((res) => {
const data = (res.data && res.data.data) || {};
this.fullConfig = this.normalizeConfig(data);
this.syncJson();
})
.finally(() => {
this.loading = false;
});
},
syncJson() {
this.advancedText = JSON.stringify(this.fullConfig, null, 2);
},
applyAdvanced() {
if (!this.advancedText.trim()) {
return showMessage(this, "warning", "JSON 内容为空");
}
try {
const parsed = JSON.parse(this.advancedText);
this.fullConfig = this.normalizeConfig(parsed);
showMessage(this, "success", "JSON 已应用");
} catch (err) {
showMessage(this, "error", "JSON 解析失败:" + err.message);
}
},
addBanner() {
this.bannerList.push({
title: "",
img: "",
description: "",
tags: "",
url: "",
blank: false,
button_text: "",
button_link: "",
button_blank: false,
});
},
removeBanner(index) {
this.bannerList.splice(index, 1);
},
addFriendlyLink() {
if (!Array.isArray(this.fullConfig.friendly_link)) {
this.fullConfig.friendly_link = [];
}
this.fullConfig.friendly_link.push({
name: "",
url: "",
});
},
removeFriendlyLink(index) {
this.fullConfig.friendly_link.splice(index, 1);
},
addHonor() {
if (!Array.isArray(this.fullConfig.honor)) {
this.fullConfig.honor = [];
}
this.fullConfig.honor.push({
name: "",
img: ""
});
},
removeHonor(index) {
this.fullConfig.honor.splice(index, 1);
},
addFeedbackType() {
if (!Array.isArray(this.fullConfig.feedback_type)) {
this.fullConfig.feedback_type = [];
}
this.fullConfig.feedback_type.push({
id: "",
name: "",
description: "",
});
},
removeFeedbackType(index) {
this.fullConfig.feedback_type.splice(index, 1);
},
addSide() {
if (!Array.isArray(this.fullConfig.side)) {
this.fullConfig.side = [];
}
this.fullConfig.side.push({
name: "",
icon: "",
content: "",
});
},
removeSide(index) {
this.fullConfig.side.splice(index, 1);
},
getHeaderChildren(item) {
if (!item) return [];
if (!Array.isArray(item.children)) {
this.$set(item, "children", []);
}
return item.children;
},
addHeaderNav() {
if (!Array.isArray(this.fullConfig.header_nav)) {
this.fullConfig.header_nav = [];
}
this.fullConfig.header_nav.push({
name: "",
file_address: "",
blank: false,
children: [],
});
},
removeHeaderNav(index) {
if (index === 0) {
return;
}
this.headerNavList.splice(index, 1);
},
addHeaderNavChild(index) {
const item = this.headerNavList[index];
if (!item) return;
if (!Array.isArray(item.children)) {
this.$set(item, "children", []);
}
item.children.push({
name: "",
file_address: "",
blank: false,
icon: "",
description: "",
});
},
removeHeaderNavChild(index, childIndex) {
const item = this.headerNavList[index];
if (!item || !Array.isArray(item.children)) return;
item.children.splice(childIndex, 1);
},
getFooterChildren(group) {
if (!group) return [];
if (!Array.isArray(group.children)) {
this.$set(group, "children", []);
}
return group.children;
},
addFooterNav() {
if (!Array.isArray(this.fullConfig.footer_nav)) {
this.fullConfig.footer_nav = [];
}
this.fullConfig.footer_nav.push({
name: "",
children: [],
});
},
removeFooterNav(index) {
this.footerNavList.splice(index, 1);
},
addFooterNavChild(index) {
const group = this.footerNavList[index];
if (!group) return;
if (!Array.isArray(group.children)) {
this.$set(group, "children", []);
}
group.children.push({
name: "",
url: "",
blank: false,
});
},
removeFooterNavChild(index, childIndex) {
const group = this.footerNavList[index];
if (!group || !Array.isArray(group.children)) return;
group.children.splice(childIndex, 1);
},
saveConfig() {
this.saving = true;
const payload = {
...this.fullConfig,
side_floating_window: this.fullConfig.side || [],
};
axios
.post(base, payload, {
headers: {
Authorization: "Bearer " + localStorage.getItem("backJwt"),
},
})
.then((res) => {
showMessage(this, "success", (res.data && res.data.msg) || "保存成功");
this.loadConfig();
})
.finally(() => {
this.saving = false;
});
},
toggleAdvanced() {
this.showAdvanced = !this.showAdvanced;
if (this.showAdvanced) {
this.syncJson();
this.activeTab = "advanced";
} else if (this.activeTab === "advanced") {
this.activeTab = "basic";
}
},
},
mounted() {
this.loadConfig();
},
});
})();
</script>

View File

@@ -0,0 +1,914 @@
<link rel="stylesheet" href="/plugins/addon/theme_configurator/template/admin/theme.css" />
<div id="theme-config-app" class="admin-container" v-cloak>
<section id="basic" class="config-section" :class="{active: activeSection === 'basic'}">
<div class="section-card" data-title="企业信息">
<div class="form-grid">
<div class="form-item">
<label>企业名称</label>
<t-input v-model="fullConfig.site_config.enterprise_name" placeholder="主题云"></t-input>
</div>
<div class="form-item">
<label>联系电话</label>
<t-input v-model="fullConfig.site_config.enterprise_telephone" placeholder="400-000-0000"></t-input>
</div>
<div class="form-item">
<label>联系邮箱</label>
<t-input v-model="fullConfig.site_config.enterprise_mailbox" placeholder="support@example.com"></t-input>
</div>
<div class="form-item form-item--full">
<label>Logo 地址</label>
<div class="upload-row">
<t-input v-model="fullConfig.site_config.official_website_logo" placeholder="/upload/logo.png"></t-input>
<t-upload theme="custom" :action="uploadUrl" :headers="uploadHeaders"
:format-response="uploadFormatResponse" :show-upload-progress="false" :max="1"
@success="(ctx) => handleUpload(['site_config', 'official_website_logo'], ctx)">
<t-button size="small" class="ml-10">
<t-icon name="upload" size="small" /> 上传
</t-button>
</t-upload>
</div>
</div>
<div class="form-item form-item--full">
<label>二维码地址</label>
<div class="upload-row">
<t-input v-model="fullConfig.site_config.enterprise_qrcode" placeholder="/upload/qrcode.png"></t-input>
<t-upload theme="custom" :action="uploadUrl" :headers="uploadHeaders"
:format-response="uploadFormatResponse" :show-upload-progress="false" :max="1"
@success="(ctx) => handleUpload(['site_config', 'enterprise_qrcode'], ctx)">
<t-button size="small" class="ml-10">
<t-icon name="upload" size="small" /> 上传
</t-button>
</t-upload>
</div>
</div>
<div class="form-item">
<label>在线客服链接</label>
<t-input v-model="fullConfig.site_config.online_customer_service_link"
placeholder="http://www.test.com"></t-input>
</div>
<div class="form-item">
<label>ICP 号</label>
<t-input v-model="fullConfig.site_config.icp_info" placeholder="京ICP备XXXX号"></t-input>
</div>
<div class="form-item">
<label>ICP 链接</label>
<t-input v-model="fullConfig.site_config.icp_info_link" placeholder="https://beian.miit.gov.cn"></t-input>
</div>
<div class="form-item">
<label>公安备案号</label>
<t-input v-model="fullConfig.site_config.public_security_network_preparation"
placeholder="京公网安备XXXX号"></t-input>
</div>
<div class="form-item">
<label>公安备案链接</label>
<t-input v-model="fullConfig.site_config.public_security_network_preparation_link"
placeholder="https://beian.mps.gov.cn"></t-input>
</div>
<div class="form-item">
<label>电信增值许可证</label>
<t-input v-model="fullConfig.site_config.telecom_appreciation" placeholder="增值电信业务经营许可证"></t-input>
</div>
<div class="form-item">
<label>用户协议链接</label>
<t-input v-model="fullConfig.site_config.terms_service_url" placeholder="/agreement/service.html"></t-input>
</div>
<div class="form-item">
<label>隐私政策链接</label>
<t-input v-model="fullConfig.site_config.terms_privacy_url" placeholder="/agreement/privacy.html"></t-input>
</div>
<div class="form-item">
<label>云产品购买链接</label>
<t-input v-model="fullConfig.site_config.cloud_product_link" placeholder="/cart/goods.htm?id=1"></t-input>
</div>
<div class="form-item">
<label>物理机/DCIM 链接</label>
<t-input v-model="fullConfig.site_config.dcim_product_link" placeholder="/cart/goods.htm?id=2"></t-input>
</div>
<div class="form-item form-item--full">
<label>版权信息</label>
<t-input v-model="fullConfig.site_config.copyright_info" placeholder="© 2025 主题云"></t-input>
</div>
</div>
</div>
<div class="section-card" data-title="侧边浮窗">
<p class="theme-tip">
对应前台右侧悬浮工具条(电话咨询/在线客服/提交工单等),结构与模板中的
<code>side_floating_window</code> 一致。
</p>
<div v-if="!fullConfig.side.length" class="empty-tip">
还没有侧边浮窗,点击下方按钮添加。
</div>
<div class="config-item" v-for="(item, index) in fullConfig.side" :key="'side-' + index">
<div class="config-item__header">
<h4>浮窗 {{ index + 1 }}</h4>
<t-button size="small" theme="danger" variant="outline" @click="removeSide(index)">
删除
</t-button>
</div>
<div class="form-grid">
<div class="form-item">
<label>名称</label>
<t-input v-model="item.name" placeholder="电话咨询"></t-input>
</div>
<div class="form-item form-item--full">
<label>图标地址</label>
<div class="upload-row">
<t-input v-model="item.icon" placeholder="/upload/side-phone.png"></t-input>
<t-upload theme="custom" :action="uploadUrl" :headers="uploadHeaders"
:format-response="uploadFormatResponse" :show-upload-progress="false" :max="1"
@success="(ctx) => handleUpload(['side', index, 'icon'], ctx)">
<t-button size="small" class="ml-10">
<t-icon name="upload" size="small" /> 上传
</t-button>
</t-upload>
</div>
</div>
<div class="form-item form-item--full">
<label>内容(支持 HTML</label>
<t-textarea v-model="item.content" :autosize="{ minRows: 2, maxRows: 4 }"
placeholder="<p>7x24 小时不间断服务</p>"></t-textarea>
</div>
</div>
</div>
<t-button theme="primary" variant="outline" @click="addSide">新增浮窗</t-button>
</div>
<div class="section-card" data-title="反馈类型">
<p class="theme-tip">
对应 <code>/console/v1/feedback</code> 的类型选项ID 需与后端保持一致,仅建议修改名称与描述。
</p>
<div v-if="!fullConfig.feedback_type.length" class="empty-tip">
还没有反馈类型,点击下方按钮添加。
</div>
<div class="config-item" v-for="(item, index) in fullConfig.feedback_type" :key="'feedback-' + index">
<div class="config-item__header">
<h4>类型 {{ index + 1 }}</h4>
<t-button size="small" theme="danger" variant="outline" @click="removeFeedbackType(index)">
删除
</t-button>
</div>
<div class="form-grid">
<div class="form-item">
<label>类型 ID</label>
<t-input v-model="item.id" placeholder="1"></t-input>
</div>
<div class="form-item">
<label>名称</label>
<t-input v-model="item.name" placeholder="产品建议"></t-input>
</div>
<div class="form-item form-item--full">
<label>描述</label>
<t-input v-model="item.description" placeholder="用于产品体验、功能建议等"></t-input>
</div>
</div>
</div>
<t-button theme="primary" variant="outline" @click="addFeedbackType">新增反馈类型</t-button>
</div>
</section>
<section id="seo" class="config-section" :class="{active: activeSection === 'seo'}">
<div class="section-card" data-title="SEO 设置">
<div class="form-grid">
<div class="form-item">
<label>站点标题</label>
<t-input v-model="fullConfig.seo.title" placeholder="首页标题"></t-input>
</div>
<div class="form-item">
<label>关键词</label>
<t-input v-model="fullConfig.seo.keywords" placeholder="关键词,逗号分隔"></t-input>
</div>
<div class="form-item form-item--full">
<label>描述</label>
<t-textarea v-model="fullConfig.seo.description" :autosize="{ minRows: 2, maxRows: 4 }"
placeholder="站点描述"></t-textarea>
</div>
</div>
</div>
</section>
<section id="home" class="config-section" :class="{active: activeSection === 'home'}">
<div class="section-card" data-title="首页轮播文案">
<div v-if="!bannerList.length" class="empty-tip">
还没有轮播文案,点击下方按钮添加。
</div>
<div class="banner-item" v-for="(banner, index) in bannerList" :key="index">
<div class="banner-item__header">
<h4>轮播 {{ index + 1 }}</h4>
<t-button size="small" theme="danger" variant="outline" @click="removeBanner(index)">
删除
</t-button>
</div>
<div class="form-grid">
<div class="form-item">
<label>标题</label>
<t-input v-model="banner.title" placeholder="如:弹性算力"></t-input>
</div>
<div class="form-item form-item--full">
<label>图片地址</label>
<div class="upload-row">
<t-input v-model="banner.img" placeholder="/upload/banner-1.png"></t-input>
<t-upload theme="custom" :action="uploadUrl" :headers="uploadHeaders"
:format-response="uploadFormatResponse" :show-upload-progress="false" :max="1"
@success="(ctx) => handleUpload(['banner', index, 'img'], ctx)">
<t-button size="small" class="ml-10">
<t-icon name="upload" size="small" /> 上传
</t-button>
</t-upload>
</div>
</div>
<div class="form-item">
<label>描述</label>
<t-input v-model="banner.description" placeholder="一句宣传语"></t-input>
</div>
<div class="form-item form-item--full">
<label>标签</label>
<t-input v-model="banner.tags" placeholder="如:高速,低价,安全(多个标签用逗号分隔)"></t-input>
</div>
<div class="form-item">
<label>跳转链接</label>
<t-input v-model="banner.url" placeholder="/cloud.html"></t-input>
</div>
<div class="form-item form-item--switch">
<label><span class="switch-label-icon">🔗</span>新窗口</label>
<t-switch size="large" v-model="banner.blank"></t-switch>
</div>
<div class="form-item">
<label>按钮文字</label>
<t-input v-model="banner.button_text" placeholder="立即查看"></t-input>
</div>
<div class="form-item">
<label>按钮链接</label>
<t-input v-model="banner.button_link" placeholder="/cloud.html"></t-input>
</div>
<div class="form-item form-item--switch">
<label><span class="switch-label-icon">🔗</span>按钮新窗口</label>
<t-switch size="large" v-model="banner.button_blank"></t-switch>
</div>
</div>
</div>
<t-button theme="primary" variant="outline" @click="addBanner">新增轮播</t-button>
</div>
<div class="section-card" data-title="企业资质与荣誉">
<div v-if="!fullConfig.honor.length" class="empty-tip">
用于首页"荣誉资质"模块honor
</div>
<div class="config-item" v-for="(item, index) in fullConfig.honor" :key="'honor-' + index">
<div class="config-item__header">
<h4>荣誉 {{ index + 1 }}</h4>
<t-button size="small" theme="danger" variant="outline" @click="removeHonor(index)">
删除
</t-button>
</div>
<div class="form-grid">
<div class="form-item">
<label>名称</label>
<t-input v-model="item.name" placeholder="高新技术企业"></t-input>
</div>
<div class="form-item form-item--full">
<label>图片地址</label>
<div class="upload-row">
<t-input v-model="item.img" placeholder="/upload/honor.png"></t-input>
<t-upload theme="custom" :action="uploadUrl" :headers="uploadHeaders"
:format-response="uploadFormatResponse" :show-upload-progress="false" :max="1"
@success="(ctx) => handleUpload(['honor', index, 'img'], ctx)">
<t-button size="small" class="ml-10">
<t-icon name="upload" size="small" /> 上传
</t-button>
</t-upload>
</div>
</div>
</div>
</div>
<t-button theme="primary" variant="outline" @click="addHonor">新增荣誉</t-button>
</div>
</section>
<section id="nav" class="config-section" :class="{active: activeSection === 'nav'}">
<div class="section-card" data-title="顶部导航header_nav">
<p class="theme-tip">
控制站点顶部导航栏及其下拉菜单结构。第一个导航项用作 Logo 点击跳转地址。
</p>
<div class="form-grid">
<div class="form-item form-item--full">
<label>Logo 点击跳转地址</label>
<t-input v-model="homeNav.file_address" placeholder="index.html 或 /index.html"></t-input>
</div>
</div>
<h4 class="sub-title mt-10">一级导航</h4>
<div v-if="headerNavList.length <= 1" class="empty-tip">
还没有自定义导航,请点击下方按钮新增。
</div>
<div class="config-item" v-for="(item, index) in headerNavList" :key="'nav-' + index" v-if="index > 0">
<div class="config-item__header">
<h4>导航 {{ index }}</h4>
<t-button size="small" theme="danger" variant="outline" @click="removeHeaderNav(index)">
删除
</t-button>
</div>
<div class="form-grid">
<div class="form-item">
<label>名称</label>
<t-input v-model="item.name" placeholder="如:产品"></t-input>
</div>
<div class="form-item">
<label>点击链接(可选)</label>
<t-input v-model="item.file_address" placeholder="/cloud.html"></t-input>
</div>
<div class="form-item form-item--switch">
<label><span class="switch-label-icon">🔗</span>新窗口</label>
<t-switch v-model="item.blank"></t-switch>
</div>
</div>
<h4 class="sub-title mt-10">下拉子菜单</h4>
<div v-if="!getHeaderChildren(item).length" class="empty-tip">
还没有子菜单,点击下方按钮新增。
</div>
<div class="config-item" v-for="(child, cIndex) in getHeaderChildren(item)"
:key="'nav-' + index + '-child-' + cIndex">
<div class="config-item__header">
<h4>子菜单 {{ cIndex + 1 }}</h4>
<t-button size="small" theme="danger" variant="outline" @click="removeHeaderNavChild(index, cIndex)">
删除
</t-button>
</div>
<div class="form-grid">
<div class="form-item">
<label>名称</label>
<t-input v-model="child.name" placeholder="如:云服务器"></t-input>
</div>
<div class="form-item">
<label>链接地址</label>
<t-input v-model="child.file_address" placeholder="/cloud.html"></t-input>
</div>
<div class="form-item form-item--switch">
<label><span class="switch-label-icon">🔗</span>新窗口</label>
<t-switch v-model="child.blank"></t-switch>
</div>
<div class="form-item form-item--full">
<label>图标地址</label>
<div class="upload-row">
<t-input v-model="child.icon" placeholder="/upload/nav-icon.png"></t-input>
<t-upload theme="custom" :action="uploadUrl" :headers="uploadHeaders"
:format-response="uploadFormatResponse" :show-upload-progress="false" :max="1" @success="
(ctx) => handleUpload(['header_nav', index, 'children', cIndex, 'icon'], ctx)
">
<t-button size="small" class="ml-10">
<t-icon name="upload" size="small" /> 上传
</t-button>
</t-upload>
</div>
</div>
<div class="form-item form-item--full">
<label>描述</label>
<t-textarea v-model="child.description" :autosize="{ minRows: 2, maxRows: 3 }"
placeholder="如:高可用的弹性计算服务"></t-textarea>
</div>
</div>
</div>
<t-button class="mt-10" theme="primary" size="small" variant="outline" @click="addHeaderNavChild(index)">
新增子菜单
</t-button>
</div>
<t-button class="mt-10" theme="primary" variant="outline" @click="addHeaderNav">
新增一级导航
</t-button>
</div>
</section>
<section id="footer" class="config-section" :class="{active: activeSection === 'footer'}">
<div class="section-card" data-title="底部栏目footer_nav">
<p class="theme-tip">
控制首页底部多列链接(如【热门产品】【客户支持】等),结构与模板中的
<code>footer_nav</code> 一致。
</p>
<div v-if="!footerNavList.length" class="empty-tip">
还没有底部栏目,点击下方按钮新增。
</div>
<div class="config-item" v-for="(group, index) in footerNavList" :key="'footer-' + index">
<div class="config-item__header">
<h4>栏目 {{ index + 1 }}</h4>
<t-button size="small" theme="danger" variant="outline" @click="removeFooterNav(index)">
删除
</t-button>
</div>
<div class="form-grid">
<div class="form-item">
<label>栏目标题</label>
<t-input v-model="group.name" placeholder="如:热门产品"></t-input>
</div>
</div>
<h4 class="sub-title mt-10">栏目链接</h4>
<div v-if="!getFooterChildren(group).length" class="empty-tip">
还没有链接,点击下方按钮新增。
</div>
<div class="config-item" v-for="(link, cIndex) in getFooterChildren(group)"
:key="'footer-' + index + '-child-' + cIndex">
<div class="config-item__header">
<h4>链接 {{ cIndex + 1 }}</h4>
<t-button size="small" theme="danger" variant="outline" @click="removeFooterNavChild(index, cIndex)">
删除
</t-button>
</div>
<div class="form-grid">
<div class="form-item">
<label>名称</label>
<t-input v-model="link.name" placeholder="如:云服务器"></t-input>
</div>
<div class="form-item">
<label>链接地址</label>
<t-input v-model="link.url" placeholder="/cloud.html"></t-input>
</div>
<div class="form-item form-item--switch">
<label><span class="switch-label-icon">🔗</span>新窗口</label>
<t-switch v-model="link.blank"></t-switch>
</div>
</div>
</div>
<t-button class="mt-10" theme="primary" size="small" variant="outline" @click="addFooterNavChild(index)">
新增链接
</t-button>
</div>
<t-button class="mt-10" theme="primary" variant="outline" @click="addFooterNav">
新增栏目
</t-button>
</div>
<div class="section-card" data-title="友情链接">
<div v-if="!fullConfig.friendly_link.length" class="empty-tip">
还没有友情链接,点击下方按钮添加。
</div>
<div class="config-item" v-for="(item, index) in fullConfig.friendly_link" :key="index">
<div class="config-item__header">
<h4>链接 {{ index + 1 }}</h4>
<t-button size="small" theme="danger" variant="outline" @click="removeFriendlyLink(index)">
删除
</t-button>
</div>
<div class="form-grid">
<div class="form-item">
<label>名称</label>
<t-input v-model="item.name" placeholder="合作伙伴名称"></t-input>
</div>
<div class="form-item form-item--full">
<label>链接地址</label>
<t-input v-model="item.url" placeholder="https://example.com"></t-input>
</div>
</div>
</div>
<t-button theme="primary" variant="outline" @click="addFriendlyLink">新增友情链接</t-button>
</div>
</section>
<t-tab-panel v-if="showAdvanced" value="advanced" label="高级设置(JSON)">
<div class="section-card" data-title="高级配置 (JSON)">
<p class="theme-tip">
用于暂未在 UI 中开放的配置项(如复杂导航结构 header_nav/footer_nav 等)。如非必要,建议优先使用上方表单编辑。
</p>
<t-textarea v-model="advancedText" :autosize="{ minRows: 14 }" class="theme-textarea"></t-textarea>
<div class="mt-10">
<t-button variant="outline" @click="syncJson">同步当前配置</t-button>
<t-button class="ml-10" @click="applyAdvanced">应用 JSON</t-button>
</div>
</div>
</section>
</t-tabs>
<div class="action-bar">
<t-button variant="outline" @click="loadConfig" :loading="loading">重新加载</t-button>
<t-button theme="primary" class="ml-10" @click="saveConfig" :loading="saving">
保存全部配置
</t-button>
<t-button class="ml-10" variant="outline" @click="toggleAdvanced">
{{ showAdvanced ? "隐藏高级 JSON" : "显示高级 JSON" }}
</t-button>
</div>
</div>
<script src="/plugins/addon/theme_configurator/template/admin/lang/index.js"></script>
<script src="/plugins/addon/theme_configurator/template/admin/js/axios.min.js"></script>
<script>
(function () {
const host = location.origin;
const adminPath = location.pathname.split("/")[1];
const base = `${host}/${adminPath}/v1/theme/config`;
const showMessage = (vm, type, text) => {
if (vm.$message && typeof vm.$message[type] === "function") {
vm.$message[type](text);
} else if (window.MessagePlugin && typeof window.MessagePlugin[type] === "function") {
window.MessagePlugin[type](text);
} else {
alert(text);
}
};
const createDefaultConfig = () => ({
seo: {
title: "",
keywords: "",
description: "",
},
site_config: {
enterprise_name: "",
enterprise_telephone: "",
enterprise_mailbox: "",
enterprise_qrcode: "",
official_website_logo: "",
online_customer_service_link: "",
icp_info: "",
icp_info_link: "",
public_security_network_preparation: "",
public_security_network_preparation_link: "",
telecom_appreciation: "",
copyright_info: "",
terms_service_url: "",
terms_privacy_url: "",
cloud_product_link: "",
dcim_product_link: "",
},
banner: [],
header_nav: [],
footer_nav: [],
friendly_link: [],
side: [],
feedback_type: [],
honor: [],
partner: [],
});
new Vue({
el: "#theme-config-app",
data() {
return {
loading: false,
saving: false,
fullConfig: createDefaultConfig(),
advancedText: "",
showAdvanced: false,
activeTab: "basic",
uploadUrl: `${host}/${adminPath}/v1/upload`,
uploadHeaders: {
Authorization: "Bearer " + localStorage.getItem("backJwt"),
},
};
},
computed: {
bannerList() {
if (!Array.isArray(this.fullConfig.banner)) {
this.fullConfig.banner = [];
}
return this.fullConfig.banner;
},
headerNavList() {
if (!Array.isArray(this.fullConfig.header_nav)) {
this.fullConfig.header_nav = [];
}
if (this.fullConfig.header_nav.length === 0) {
this.fullConfig.header_nav.push({
name: "首页",
file_address: "index.html",
blank: false,
children: [],
});
}
return this.fullConfig.header_nav;
},
homeNav() {
return this.headerNavList[0];
},
footerNavList() {
if (!Array.isArray(this.fullConfig.footer_nav)) {
this.fullConfig.footer_nav = [];
}
return this.fullConfig.footer_nav;
},
},
methods: {
normalizeConfig(data = {}) {
const defaults = createDefaultConfig();
const merged = {
...defaults,
...data,
seo: {
...defaults.seo,
...(data.seo || {}),
},
site_config: {
...defaults.site_config,
...(data.site_config || {}),
},
banner: Array.isArray(data.banner) ? data.banner : [],
header_nav:
Array.isArray(data.header_nav) && data.header_nav.length
? data.header_nav
: [],
footer_nav: Array.isArray(data.footer_nav) ? data.footer_nav : [],
friendly_link: Array.isArray(data.friendly_link) ? data.friendly_link : [],
side: Array.isArray(data.side)
? data.side
: Array.isArray(data.side_floating_window)
? data.side_floating_window
: [],
feedback_type: Array.isArray(data.feedback_type)
? data.feedback_type
: Array.isArray(data.site_config && data.site_config.feedback_type)
? data.site_config.feedback_type
: [],
honor: Array.isArray(data.honor)
? data.honor
: Array.isArray(data.site_config && data.site_config.honor)
? data.site_config.honor
: [],
partner: Array.isArray(data.partner)
? data.partner
: Array.isArray(data.site_config && data.site_config.partner)
? data.site_config.partner
: [],
};
if (!Array.isArray(merged.header_nav) || merged.header_nav.length === 0) {
merged.header_nav = [
{
name: "首页",
file_address: "index.html",
blank: false,
children: [],
},
];
}
return merged;
},
uploadFormatResponse(res) {
if (!res || res.status !== 200) {
return { error: "上传失败" };
}
return res;
},
handleUpload(path, ctx) {
try {
const resp = ctx && ctx.file && ctx.file.response;
const data = resp && resp.data ? resp.data : resp;
if (!data) {
return showMessage(this, "error", "上传失败:未获取到响应数据");
}
// 优先使用后端返回的 image_url其次 url最后才是 save_name
let url = data.image_url || data.url || data.save_name || "";
if (!url) {
return showMessage(this, "error", "上传失败:未获取到文件地址");
}
// 相对路径前面补一个 /,保持站点根路径
if (!/^https?:\/\//i.test(url) && url.charAt(0) !== "/") {
url = "/" + url;
}
this.setConfigByPath(path, url);
} catch (e) {
showMessage(this, "error", "上传处理异常:" + e.message);
}
},
setConfigByPath(path, value) {
if (!Array.isArray(path) || !path.length) return;
let target = this.fullConfig;
for (let i = 0; i < path.length - 1; i++) {
const key = path[i];
if (typeof key === "number") {
if (!Array.isArray(target)) return;
target = target[key];
} else {
if (!target[key]) {
this.$set(target, key, {});
}
target = target[key];
}
if (!target) return;
}
const lastKey = path[path.length - 1];
if (typeof lastKey === "number") {
if (!Array.isArray(target)) return;
this.$set(target, lastKey, value);
} else {
this.$set(target, lastKey, value);
}
},
loadConfig() {
this.loading = true;
axios
.get(base, {
headers: {
Authorization: "Bearer " + localStorage.getItem("backJwt"),
},
})
.then((res) => {
const data = (res.data && res.data.data) || {};
this.fullConfig = this.normalizeConfig(data);
this.syncJson();
})
.finally(() => {
this.loading = false;
});
},
syncJson() {
this.advancedText = JSON.stringify(this.fullConfig, null, 2);
},
applyAdvanced() {
if (!this.advancedText.trim()) {
return showMessage(this, "warning", "JSON 内容为空");
}
try {
const parsed = JSON.parse(this.advancedText);
this.fullConfig = this.normalizeConfig(parsed);
showMessage(this, "success", "JSON 已应用");
} catch (err) {
showMessage(this, "error", "JSON 解析失败:" + err.message);
}
},
addBanner() {
this.bannerList.push({
title: "",
img: "",
description: "",
tags: "",
url: "",
blank: false,
button_text: "",
button_link: "",
button_blank: false,
});
},
removeBanner(index) {
this.bannerList.splice(index, 1);
},
addFriendlyLink() {
if (!Array.isArray(this.fullConfig.friendly_link)) {
this.fullConfig.friendly_link = [];
}
this.fullConfig.friendly_link.push({
name: "",
url: "",
});
},
removeFriendlyLink(index) {
this.fullConfig.friendly_link.splice(index, 1);
},
addHonor() {
if (!Array.isArray(this.fullConfig.honor)) {
this.fullConfig.honor = [];
}
this.fullConfig.honor.push({
name: "",
img: ""
});
},
removeHonor(index) {
this.fullConfig.honor.splice(index, 1);
},
addFeedbackType() {
if (!Array.isArray(this.fullConfig.feedback_type)) {
this.fullConfig.feedback_type = [];
}
this.fullConfig.feedback_type.push({
id: "",
name: "",
description: "",
});
},
removeFeedbackType(index) {
this.fullConfig.feedback_type.splice(index, 1);
},
addSide() {
if (!Array.isArray(this.fullConfig.side)) {
this.fullConfig.side = [];
}
this.fullConfig.side.push({
name: "",
icon: "",
content: "",
});
},
removeSide(index) {
this.fullConfig.side.splice(index, 1);
},
getHeaderChildren(item) {
if (!item) return [];
if (!Array.isArray(item.children)) {
this.$set(item, "children", []);
}
return item.children;
},
addHeaderNav() {
if (!Array.isArray(this.fullConfig.header_nav)) {
this.fullConfig.header_nav = [];
}
this.fullConfig.header_nav.push({
name: "",
file_address: "",
blank: false,
children: [],
});
},
removeHeaderNav(index) {
if (index === 0) {
return;
}
this.headerNavList.splice(index, 1);
},
addHeaderNavChild(index) {
const item = this.headerNavList[index];
if (!item) return;
if (!Array.isArray(item.children)) {
this.$set(item, "children", []);
}
item.children.push({
name: "",
file_address: "",
blank: false,
icon: "",
description: "",
});
},
removeHeaderNavChild(index, childIndex) {
const item = this.headerNavList[index];
if (!item || !Array.isArray(item.children)) return;
item.children.splice(childIndex, 1);
},
getFooterChildren(group) {
if (!group) return [];
if (!Array.isArray(group.children)) {
this.$set(group, "children", []);
}
return group.children;
},
addFooterNav() {
if (!Array.isArray(this.fullConfig.footer_nav)) {
this.fullConfig.footer_nav = [];
}
this.fullConfig.footer_nav.push({
name: "",
children: [],
});
},
removeFooterNav(index) {
this.footerNavList.splice(index, 1);
},
addFooterNavChild(index) {
const group = this.footerNavList[index];
if (!group) return;
if (!Array.isArray(group.children)) {
this.$set(group, "children", []);
}
group.children.push({
name: "",
url: "",
blank: false,
});
},
removeFooterNavChild(index, childIndex) {
const group = this.footerNavList[index];
if (!group || !Array.isArray(group.children)) return;
group.children.splice(childIndex, 1);
},
saveConfig() {
this.saving = true;
const payload = {
...this.fullConfig,
side_floating_window: this.fullConfig.side || [],
};
axios
.post(base, payload, {
headers: {
Authorization: "Bearer " + localStorage.getItem("backJwt"),
},
})
.then((res) => {
showMessage(this, "success", (res.data && res.data.msg) || "保存成功");
this.loadConfig();
})
.finally(() => {
this.saving = false;
});
},
toggleAdvanced() {
this.showAdvanced = !this.showAdvanced;
if (this.showAdvanced) {
this.syncJson();
this.activeTab = "advanced";
} else if (this.activeTab === "advanced") {
this.activeTab = "basic";
}
},
},
mounted() {
this.loadConfig();
},
});
})();
</script>

View File

@@ -0,0 +1,255 @@
<link rel="stylesheet" href="/plugins/addon/theme_configurator/template/admin/theme.css" />
<div id="theme-config-app" class="admin-container" v-cloak>
<!-- 顶部工具栏 -->
<header class="admin-header">
<div class="admin-header__left">
<div class="admin-logo">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" />
<path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" />
<path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" />
</svg>
<span>黑果云模板控制器</span>
</div>
</div>
<div class="admin-header__right">
<button class="btn btn-primary btn-lg" @click="saveConfig" :disabled="saving">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" v-if="!saving">
<path d="M13.5 2.5H2.5V13.5H13.5V2.5Z" stroke="currentColor" stroke-width="1.5" />
<path d="M10.5 2.5V6.5H5.5V2.5" stroke="currentColor" stroke-width="1.5" />
<path d="M5.5 9.5H10.5V13.5H5.5V9.5Z" stroke="currentColor" stroke-width="1.5" />
</svg>
<span v-if="saving">保存中...</span>
<span v-else>保存全部配置</span>
</button>
</div>
</header>
<!-- 主体布局 -->
<div class="admin-layout">
<!-- 侧边栏导航 -->
<aside class="admin-sidebar">
<nav class="sidebar-nav">
<a href="#basic" class="nav-item" :class="{active: activeSection === 'basic'}"
@click="activeSection = 'basic'">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" stroke-width="2" />
<line x1="3" y1="9" x2="21" y2="9" stroke="currentColor" stroke-width="2" />
<line x1="9" y1="21" x2="9" y2="9" stroke="currentColor" stroke-width="2" />
</svg>
<span>基础配置</span>
</a>
<a href="#seo" class="nav-item" :class="{active: activeSection === 'seo'}"
@click="activeSection = 'seo'">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<circle cx="11" cy="11" r="8" stroke="currentColor" stroke-width="2" />
<path d="M21 21L16.65 16.65" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>
<span>SEO设置</span>
</a>
<a href="#home" class="nav-item" :class="{active: activeSection === 'home'}"
@click="activeSection = 'home'">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<path
d="M3 9L12 2L21 9V20C21 20.5304 20.7893 21.0391 20.4142 21.4142C20.0391 21.7893 19.5304 22 19 22H5C4.46957 22 3.96086 21.7893 3.58579 21.4142C3.21071 21.0391 3 20.5304 3 20V9Z"
stroke="currentColor" stroke-width="2" />
</svg>
<span>首页内容</span>
</a>
<a href="#nav" class="nav-item" :class="{active: activeSection === 'nav'}"
@click="activeSection = 'nav'">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<line x1="3" y1="12" x2="21" y2="12" stroke="currentColor" stroke-width="2"
stroke-linecap="round" />
<line x1="3" y1="6" x2="21" y2="6" stroke="currentColor" stroke-width="2"
stroke-linecap="round" />
<line x1="3" y1="18" x2="21" y2="18" stroke="currentColor" stroke-width="2"
stroke-linecap="round" />
</svg>
<span>导航配置</span>
</a>
<a href="#advanced" class="nav-item" :class="{active: activeSection === 'advanced'}"
@click="activeSection = 'advanced'">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2" />
<path
d="M12 1V3M12 21V23M4.22 4.22L5.64 5.64M18.36 18.36L19.78 19.78M1 12H3M21 12H23M4.22 19.78L5.64 18.36M18.36 5.64L19.78 4.22"
stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>
<span>高级设置</span>
</a>
</nav>
</aside>
<!-- 主内容区 -->
<main class="admin-main">
<!-- 基础配置区块 -->
<section id="basic" class="config-section" v-show="activeSection === 'basic'">
<!-- 企业信息 -->
<div class="section-card">
<div class="section-header">
<h2>企业信息</h2>
<p class="section-desc">配置企业基础联系信息</p>
</div>
<div class="section-body">
<div class="form-group">
<label>企业名称</label>
<t-input v-model="fullConfig.site_config.enterprise_name" placeholder="主题云"></t-input>
</div>
<div class="form-group">
<label>联系电话</label>
<t-input v-model="fullConfig.site_config.enterprise_telephone"
placeholder="400-000-0000"></t-input>
</div>
<div class="form-group">
<label>联系邮箱</label>
<t-input v-model="fullConfig.site_config.enterprise_mailbox"
placeholder="support@example.com"></t-input>
</div>
<div class="form-group">
<label>在线客服链接</label>
<t-input v-model="fullConfig.site_config.online_customer_service_link"
placeholder="http://www.test.com"></t-input>
</div>
</div>
</div>
<!-- Logo与图片 -->
<div class="section-card">
<div class="section-header">
<h2>Logo与图片</h2>
<p class="section-desc">上传网站Logo和二维码</p>
</div>
<div class="section-body">
<div class="form-group">
<label>网站Logo</label>
<div class="upload-control">
<t-input v-model="fullConfig.site_config.official_website_logo"
placeholder="/upload/logo.png"></t-input>
<t-upload theme="custom" :action="uploadUrl" :headers="uploadHeaders"
:format-response="uploadFormatResponse" :show-upload-progress="false" :max="1"
@success="(ctx) => handleUpload(['site_config', 'official_website_logo'], ctx)">
<button class="btn btn-secondary">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none">
<path
d="M14 10V12.6667C14 13.0203 13.8595 13.3594 13.6095 13.6095C13.3594 13.8595 13.0203 14 12.6667 14H3.33333C2.97971 14 2.64057 13.8595 2.39052 13.6095C2.14048 13.3594 2 13.0203 2 12.6667V10"
stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
<path d="M11.3333 5.33333L8 2L4.66667 5.33333" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M8 2V10" stroke="currentColor" stroke-width="1.5"
stroke-linecap="round" stroke-linejoin="round" />
</svg>
选择文件
</button>
</t-upload>
</div>
<div class="form-hint">建议尺寸: 200×60 像素, PNG/SVG格式</div>
</div>
<div class="form-group">
<label>企业二维码</label>
<div class="upload-control">
<t-input v-model="fullConfig.site_config.enterprise_qrcode"
placeholder="/upload/qrcode.png"></t-input>
<t-upload theme="custom" :action="uploadUrl" :headers="uploadHeaders"
:format-response="uploadFormatResponse" :show-upload-progress="false" :max="1"
@success="(ctx) => handleUpload(['site_config', 'enterprise_qrcode'], ctx)">
<button class="btn btn-secondary">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none">
<path
d="M14 10V12.6667C14 13.0203 13.8595 13.3594 13.6095 13.6095C13.3594 13.8595 13.0203 14 12.6667 14H3.33333C2.97971 14 2.64057 13.8595 2.39052 13.6095C2.14048 13.3594 2 13.0203 2 12.6667V10"
stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
<path d="M11.3333 5.33333L8 2L4.66667 5.33333" stroke="currentColor"
stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M8 2V10" stroke="currentColor" stroke-width="1.5"
stroke-linecap="round" stroke-linejoin="round" />
</svg>
选择文件
</button>
</t-upload>
</div>
<div class="form-hint">建议尺寸: 200×200 像素, PNG格式</div>
</div>
</div>
</div>
<!-- 备案信息 -->
<div class="section-card">
<div class="section-header">
<h2>备案信息</h2>
<p class="section-desc">网站备案和许可证信息</p>
</div>
<div class="section-body">
<div class="form-group">
<label>ICP备案号</label>
<t-input v-model="fullConfig.site_config.icp_info" placeholder="京ICP备XXXX号"></t-input>
</div>
<div class="form-group">
<label>ICP备案链接</label>
<t-input v-model="fullConfig.site_config.icp_info_link"
placeholder="https://beian.miit.gov.cn"></t-input>
</div>
<div class="form-group">
<label>公安备案号</label>
<t-input v-model="fullConfig.site_config.public_security_network_preparation"
placeholder="京公网安备XXXX号"></t-input>
</div>
<div class="form-group">
<label>公安备案链接</label>
<t-input v-model="fullConfig.site_config.public_security_network_preparation_link"
placeholder="https://beian.mps.gov.cn"></t-input>
</div>
<div class="form-group">
<label>增值电信许可证</label>
<t-input v-model="fullConfig.site_config.telecom_appreciation"
placeholder="增值电信业务经营许可证"></t-input>
</div>
<div class="form-group">
<label>版权信息</label>
<t-input v-model="fullConfig.site_config.copyright_info" placeholder="© 2025 主题云"></t-input>
</div>
</div>
</div>
<!-- 链接配置 -->
<div class="section-card">
<div class="section-header">
<h2>页面链接</h2>
<p class="section-desc">配置各类页面跳转链接</p>
</div>
<div class="section-body">
<div class="form-group">
<label>用户协议链接</label>
<t-input v-model="fullConfig.site_config.terms_service_url"
placeholder="/agreement/service.html"></t-input>
</div>
<div class="form-group">
<label>隐私政策链接</label>
<t-input v-model="fullConfig.site_config.terms_privacy_url"
placeholder="/agreement/privacy.html"></t-input>
</div>
<div class="form-group">
<label>云产品购买链接</label>
<t-input v-model="fullConfig.site_config.cloud_product_link"
placeholder="/cart/goods.htm?id=1"></t-input>
</div>
<div class="form-group">
<label>物理机/DCIM链接</label>
<t-input v-model="fullConfig.site_config.dcim_product_link"
placeholder="/cart/goods.htm?id=2"></t-input>
</div>
</div>
</div>
由于文件太长,我需要分多次发送。让我先完成这一部分,然后继续下一部分。
这个版本采用:
1. ⭐ 侧边栏导航
2. ⭐ 卡片式内容
3. ⭐ 单列表单
4. ⭐ 现代化按钮
是否继续创建剩余部分 (SEO设置、首页内容、导航配置等)?

View File

@@ -1,87 +1,229 @@
/* ============================================ /* ============================================
黑果云模板控制器 - 清爽专业风格 黑果云模板控制器 - 现代管理后台
设计理念:简洁、可靠、商务化 设计风格: 侧边栏 + 卡片式 + Ant Design配色
============================================ */ ============================================ */
/* CSS 变量定义 - 统一配色方案 */
:root { :root {
/* 主色调 - 专业蓝 */ /* Ant Design 配色 */
--primary-color: #2563eb; --primary: #1890ff;
--primary-hover: #1d4ed8; --primary-hover: #40a9ff;
--primary-light: #eff6ff; --primary-active: #096dd9;
--primary-light: #e6f7ff;
/* 背景色 - 清爽白灰 */ /* 背景色 */
--bg-page: #f8fafc; --bg-body: #f0f2f5;
--bg-card: #ffffff; --bg-container: #ffffff;
--bg-section: #f1f5f9; --bg-sidebar: #001529;
--bg-input: #ffffff; --bg-header: #ffffff;
/* 文字色 - 清晰层级 */ /* 文字色 */
--text-primary: #0f172a; --text-primary: #262626;
--text-secondary: #64748b; --text-secondary: #595959;
--text-tertiary: #94a3b8; --text-tertiary: #8c8c8c;
--text-disabled: #cbd5e1; --text-light: rgba(255, 255, 255, 0.85);
/* 边框色 */ /* 边框色 */
--border-light: #e2e8f0; --border: #d9d9d9;
--border-normal: #cbd5e1; --border-light: #e8e8e8;
--border-dark: #94a3b8; --border-lighter: #f0f0f0;
/* 功能色 */
--success: #10b981;
--warning: #f59e0b;
--danger: #ef4444;
/* 间距 */ /* 间距 */
--spacing-xs: 8px; --spacing-sm: 8px;
--spacing-sm: 12px;
--spacing-md: 16px; --spacing-md: 16px;
--spacing-lg: 24px; --spacing-lg: 24px;
--spacing-xl: 32px;
/* 圆角 */ /* 阴影 */
--radius-sm: 6px; --shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.06);
--radius-md: 8px; --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.08);
--radius-lg: 12px;
} }
/* 全局容器 */ /* ============================================
.template { 全局样式
max-width: 1400px; ============================================ */
margin: 0 auto;
padding: var(--spacing-lg);
background: var(--bg-page);
}
/* 隐藏加载时的闪烁 */
[v-cloak] { [v-cloak] {
display: none; display: none;
} }
/* ============================================ body {
卡片样式 margin: 0;
============================================ */ padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
.theme-card { font-size: 14px;
background: var(--bg-card); color: var(--text-primary);
border: 1px solid var(--border-light); background: var(--bg-body);
border-radius: var(--radius-md);
margin-bottom: var(--spacing-lg);
overflow: hidden;
} }
/* 卡片标题 */ /* ============================================
.theme-card .t-card__title { 主容器
============================================ */
.admin-container {
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* ============================================
顶部工具栏
============================================ */
.admin-header {
height: 60px;
background: var(--bg-header);
border-bottom: 1px solid var(--border-light);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--spacing-lg);
position: sticky;
top: 0;
z-index: 100;
box-shadow: var(--shadow-sm);
}
.admin-header__left {
display: flex;
align-items: center;
}
.admin-logo {
display: flex;
align-items: center;
gap: 12px;
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
color: var(--text-primary); color: var(--text-primary);
padding: 20px var(--spacing-lg);
border-bottom: 1px solid var(--bg-section);
background: var(--bg-card);
} }
/* 卡片内容 */ .admin-logo svg {
.theme-card .t-card__body { color: var(--primary);
}
.admin-header__right {
display: flex;
align-items: center;
gap: 12px;
}
/* ============================================
主体布局
============================================ */
.admin-layout {
display: flex;
flex: 1;
overflow: hidden;
}
/* ============================================
侧边栏
============================================ */
.admin-sidebar {
width: 220px;
background: var(--bg-sidebar);
overflow-y: auto;
flex-shrink: 0;
}
.sidebar-nav {
padding: var(--spacing-md) 0;
}
.nav-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px var(--spacing-lg);
color: var(--text-light);
text-decoration: none;
transition: all 0.3s;
cursor: pointer;
border-left: 3px solid transparent;
}
.nav-item svg {
flex-shrink: 0;
opacity: 0.7;
}
.nav-item:hover {
background: rgba(255, 255, 255, 0.08);
color: #ffffff;
}
.nav-item:hover svg {
opacity: 1;
}
.nav-item.active {
background: var(--primary);
border-left-color: #ffffff;
color: #ffffff;
}
.nav-item.active svg {
opacity: 1;
}
/* ============================================
主内容区
============================================ */
.admin-main {
flex: 1;
overflow-y: auto;
padding: var(--spacing-lg);
}
.config-section {
display: none;
}
.config-section.active {
display: block;
}
/* ============================================
区块卡片
============================================ */
.section-card {
background: var(--bg-container);
border-radius: 8px;
margin-bottom: 20px;
border: 1px solid var(--border-lighter);
overflow: hidden;
}
.section-header {
padding: 20px var(--spacing-lg);
border-bottom: 1px solid var(--border-lighter);
}
.section-header h2 {
margin: 0 0 4px 0;
font-size: 16px;
font-weight: 600;
color: var(--text-primary);
display: flex;
align-items: center;
gap: 8px;
}
.section-header h2 svg {
color: var(--primary);
flex-shrink: 0;
}
.section-desc {
margin: 0;
font-size: 13px;
color: var(--text-tertiary);
}
.section-body {
padding: var(--spacing-lg); padding: var(--spacing-lg);
} }
@@ -89,386 +231,397 @@
表单样式 表单样式
============================================ */ ============================================ */
/* 表单网格布局 */ .form-group {
.form-grid { margin-bottom: var(--spacing-lg);
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--spacing-lg) 20px;
padding: 4px 0;
} }
/* 表单项 */ .form-group:last-child {
.form-item { margin-bottom: 0;
display: flex;
flex-direction: column;
} }
.form-item--full { .form-group label {
grid-column: 1 / -1; display: block;
} margin-bottom: 8px;
/* 标签样式 */
.form-item label {
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
color: var(--text-secondary); color: var(--text-primary);
margin-bottom: var(--spacing-xs);
line-height: 1.5;
} }
/* Switch 开关布局 */ .form-group label .required {
.form-item--switch { color: #ff4d4f;
flex-direction: row; margin-left: 4px;
align-items: center;
justify-content: space-between;
padding: var(--spacing-sm) var(--spacing-md);
background: var(--bg-section);
border-radius: var(--radius-sm);
border: 1px solid var(--border-light);
} }
.form-item--switch label { .form-control {
margin-bottom: 0; width: 100%;
font-size: 14px; height: 40px;
} padding: 0 12px;
border: 1px solid var(--border);
/* ============================================
输入框样式
============================================ */
.t-input,
.t-textarea {
border-radius: var(--radius-sm);
border: 1px solid var(--border-light);
transition: all 0.2s;
}
.t-input:focus-within,
.t-textarea:focus-within {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px var(--primary-light);
}
/* ============================================
提示文本
============================================ */
.theme-tip {
margin: 0 0 var(--spacing-md);
padding: var(--spacing-sm) var(--spacing-md);
background: var(--primary-light);
border-left: 3px solid var(--primary-color);
border-radius: var(--radius-sm);
color: var(--text-secondary);
font-size: 13px;
line-height: 1.6;
}
.theme-tip code {
padding: 2px var(--spacing-xs);
background: rgba(37, 99, 235, 0.1);
border-radius: 4px; border-radius: 4px;
color: var(--primary-color); font-size: 14px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace; transition: all 0.3s;
background: var(--bg-container);
}
.form-control:hover {
border-color: var(--primary);
}
.form-control:focus {
border-color: var(--primary);
box-shadow: 0 0 0 2px var(--primary-light);
outline: none;
}
.form-hint {
margin-top: 4px;
font-size: 12px; font-size: 12px;
color: var(--text-tertiary);
} }
/* ============================================ /* ============================================
JSON 编辑器 上传控件
============================================ */ ============================================ */
.theme-textarea textarea { .upload-control {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace; display: flex;
gap: var(--spacing-sm);
}
.upload-control .t-input {
flex: 1;
}
/* ============================================
按钮样式
============================================ */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 0 16px;
height: 36px;
font-size: 14px;
font-weight: 500;
border-radius: 4px;
border: 1px solid transparent;
cursor: pointer;
transition: all 0.3s;
background: transparent;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn svg {
flex-shrink: 0;
}
/* 主按钮 */
.btn-primary {
background: var(--primary);
color: #ffffff;
border-color: var(--primary);
}
.btn-primary:hover:not(:disabled) {
background: var(--primary-hover);
border-color: var(--primary-hover);
}
.btn-primary:active:not(:disabled) {
background: var(--primary-active);
border-color: var(--primary-active);
}
/* 次要按钮 */
.btn-secondary {
background: var(--bg-container);
color: var(--text-primary);
border-color: var(--border);
}
.btn-secondary:hover:not(:disabled) {
color: var(--primary);
border-color: var(--primary);
}
/* 危险按钮 */
.btn-danger {
background: var(--bg-container);
color: #ff4d4f;
border-color: var(--border);
}
.btn-danger:hover:not(:disabled) {
color: #ffffff;
background: #ff4d4f;
border-color: #ff4d4f;
}
/* 大按钮 */
.btn-lg {
height: 40px;
padding: 0 24px;
font-size: 15px;
}
/* 小按钮 */
.btn-sm {
height: 28px;
padding: 0 12px;
font-size: 13px; font-size: 13px;
line-height: 1.6;
padding: var(--spacing-md) !important;
background: #1e293b;
color: #e2e8f0;
border: 1px solid #334155;
border-radius: var(--radius-sm);
} }
.theme-textarea textarea:focus { /* 图标按钮 */
background: #1e293b; .btn-icon {
border-color: var(--primary-color); width: 28px;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15); height: 28px;
padding: 0;
border: none;
background: transparent;
color: var(--text-secondary);
cursor: pointer;
border-radius: 4px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 16px;
transition: all 0.3s;
}
.btn-icon:hover {
background: var(--primary-light);
color: var(--primary);
}
.btn-icon-danger:hover {
background: #fff1f0;
color: #ff4d4f;
} }
/* ============================================ /* ============================================
配置项卡片 (轮播、荣誉等) 配置项列表
============================================ */ ============================================ */
.banner-item, .config-list {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
.config-item { .config-item {
border: 1px solid var(--border-light); border: 1px solid var(--border-light);
border-radius: var(--radius-md); border-radius: 6px;
padding: var(--spacing-lg); overflow: hidden;
margin-bottom: var(--spacing-lg);
background: var(--bg-card);
} }
/* 配置项头部 */
.banner-item__header,
.config-item__header { .config-item__header {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: var(--spacing-md); padding: 12px var(--spacing-md);
padding-bottom: var(--spacing-sm); background: #fafafa;
border-bottom: 1px solid var(--bg-section); border-bottom: 1px solid var(--border-light);
} }
.banner-item__header h4, .config-item__title {
.config-item__header h4 { font-size: 14px;
margin: 0; font-weight: 500;
font-size: 16px;
font-weight: 600;
color: var(--text-primary); color: var(--text-primary);
} }
/* ============================================ .config-item__actions {
子标题 display: flex;
============================================ */ gap: 4px;
}
.sub-title { .config-item__body {
margin: var(--spacing-xl) 0 var(--spacing-md); padding: var(--spacing-md);
padding-left: var(--spacing-sm); }
font-size: 15px;
font-weight: 600; /* 添加按钮 */
color: var(--text-primary); .btn-add-item {
border-left: 3px solid var(--primary-color); width: 100%;
height: 48px;
border: 2px dashed var(--border);
background: #fafafa;
border-radius: 6px;
color: var(--text-tertiary);
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.3s;
}
.btn-add-item:hover {
border-color: var(--primary);
color: var(--primary);
background: var(--primary-light);
}
.btn-add-item svg {
font-size: 20px;
} }
/* ============================================ /* ============================================
空状态 空状态
============================================ */ ============================================ */
.empty-tip { .empty-state {
padding: var(--spacing-xl) var(--spacing-lg); padding: 60px 20px;
border: 2px dashed var(--border-normal);
border-radius: var(--radius-md);
color: var(--text-tertiary);
margin-bottom: var(--spacing-lg);
background: var(--bg-section);
text-align: center; text-align: center;
color: var(--text-tertiary);
}
.empty-state svg {
width: 64px;
height: 64px;
opacity: 0.3;
margin-bottom: 16px;
}
.empty-state p {
margin: 0;
font-size: 14px; font-size: 14px;
}
/* ============================================
提示框
============================================ */
.alert {
padding: 12px 16px;
border-radius: 4px;
margin-bottom: var(--spacing-md);
font-size: 13px;
line-height: 1.6; line-height: 1.6;
} }
/* ============================================ .alert-info {
上传行
============================================ */
.upload-row {
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.upload-row .t-input {
flex: 1;
}
.upload-row .t-button {
flex-shrink: 0;
}
/* ============================================
底部操作栏
============================================ */
.action-bar {
display: flex;
justify-content: flex-end;
align-items: center;
gap: var(--spacing-sm);
margin-top: var(--spacing-xl);
padding: var(--spacing-md) var(--spacing-lg);
background: var(--bg-card);
border-radius: var(--radius-md);
border: 1px solid var(--border-light);
position: sticky;
bottom: 20px;
z-index: 100;
}
/* 按钮样式 */
.action-bar .t-button {
min-width: 120px;
font-weight: 500;
transition: all 0.2s;
}
.action-bar .t-button--primary {
background: var(--primary-color);
border-color: var(--primary-color);
}
.action-bar .t-button--primary:hover {
background: var(--primary-hover);
border-color: var(--primary-hover);
}
/* ============================================
Tab 标签页
============================================ */
.t-tabs__nav {
background: var(--bg-card);
border-bottom: 1px solid var(--border-light);
padding: 0;
margin-bottom: var(--spacing-lg);
}
.t-tab {
font-weight: 500;
font-size: 14px;
color: var(--text-secondary);
padding: var(--spacing-sm) var(--spacing-md);
transition: all 0.2s;
}
.t-tab:hover {
color: var(--primary-color);
background: var(--primary-light); background: var(--primary-light);
border-left: 3px solid var(--primary);
color: var(--text-secondary);
} }
.t-tab.t-is-active { .alert code {
color: var(--primary-color) !important; padding: 2px 6px;
background: var(--primary-light) !important; background: rgba(24, 144, 255, 0.1);
border-bottom: 2px solid var(--primary-color) !important; border-radius: 3px;
color: var(--primary);
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
} }
/* ============================================ /* ============================================
工具类 Switch开关
============================================ */ ============================================ */
.mt-10 { .form-switch {
margin-top: var(--spacing-md); display: flex;
} align-items: center;
justify-content: space-between;
.ml-10 { padding: 12px var(--spacing-md);
margin-left: var(--spacing-sm); background: #fafafa;
} border-radius: 6px;
border: 1px solid var(--border-light);
.mb-10 {
margin-bottom: var(--spacing-md); margin-bottom: var(--spacing-md);
} }
.form-switch label {
margin: 0;
font-size: 14px;
font-weight: 400;
}
/* ============================================
JSON编辑器
============================================ */
.json-editor textarea {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 13px;
line-height: 1.6;
padding: 16px !important;
background: #1e1e1e;
color: #d4d4d4;
border: 1px solid #3c3c3c;
border-radius: 4px;
min-height: 400px;
}
.json-editor textarea:focus {
border-color: var(--primary);
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.15);
}
/* ============================================ /* ============================================
响应式 响应式
============================================ */ ============================================ */
@media (max-width: 1024px) {
.admin-sidebar {
width: 180px;
}
}
@media (max-width: 768px) { @media (max-width: 768px) {
.template { .admin-sidebar {
padding: var(--spacing-sm); position: fixed;
left: -220px;
top: 60px;
bottom: 0;
z-index: 99;
transition: left 0.3s;
} }
.form-grid { .admin-sidebar.is-open {
grid-template-columns: 1fr; left: 0;
gap: var(--spacing-md);
} }
.banner-item, .admin-main {
.config-item {
padding: var(--spacing-md); padding: var(--spacing-md);
} }
.action-bar { .section-body {
flex-direction: column; padding: var(--spacing-md);
gap: var(--spacing-sm);
position: relative;
bottom: 0;
}
.action-bar .t-button {
width: 100%;
}
.form-item--switch {
flex-direction: column;
align-items: flex-start;
gap: var(--spacing-sm);
} }
} }
/* ============================================ /* ============================================
滚动条美化 (保持简洁) TDesign组件覆盖
============================================ */ ============================================ */
::-webkit-scrollbar { .t-input,
width: 8px; .t-textarea,
height: 8px; .t-select {
border-radius: 4px !important;
} }
::-webkit-scrollbar-track { .t-input:hover,
background: var(--bg-section); .t-textarea:hover {
border-color: var(--primary) !important;
} }
::-webkit-scrollbar-thumb { .t-input:focus-within,
background: var(--border-normal); .t-textarea:focus-within {
border-radius: 4px; border-color: var(--primary) !important;
box-shadow: 0 0 0 2px var(--primary-light) !important;
} }
::-webkit-scrollbar-thumb:hover { .t-button--primary {
background: var(--border-dark); background: var(--primary) !important;
border-color: var(--primary) !important;
} }
/* ============================================ .t-button--primary:hover {
按钮状态 background: var(--primary-hover) !important;
============================================ */ border-color: var(--primary-hover) !important;
.t-button--loading {
opacity: 0.6;
cursor: not-allowed;
pointer-events: none;
}
.t-button--danger:hover {
background: #dc2626;
border-color: #dc2626;
}
/* ============================================
表单验证提示
============================================ */
.form-error {
color: var(--danger);
font-size: 13px;
margin-top: var(--spacing-xs);
font-weight: 400;
}
/* ============================================
成功提示
============================================ */
.success-indicator {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
padding: 6px var(--spacing-sm);
background: var(--success);
color: white;
border-radius: 20px;
font-size: 13px;
font-weight: 500;
}
/* ============================================
其他优化
============================================ */
/* 移除emoji标记,使用纯文本 */
.form-item label::before,
.banner-item__header h4::before,
.config-item__header h4::before,
.empty-tip::before {
display: none;
}
/* Switch 图标简化 */
.switch-label-icon {
display: none;
} }