feat: 新增主题配置管理页面,支持企业信息、侧边浮窗、反馈类型、SEO和首页轮播设置,并更新了样式。
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
914
plugins/addon/theme_configurator/template/admin/index_new.html
Normal file
914
plugins/addon/theme_configurator/template/admin/index_new.html
Normal 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>
|
||||
Reference in New Issue
Block a user