diff --git a/common/common.js b/common/common.js index 2af2752..a1b1622 100644 --- a/common/common.js +++ b/common/common.js @@ -131,6 +131,25 @@ $(function () { if (pluginRes && pluginRes.status === 200 && pluginRes.data) { var cfg = pluginRes.data || {}; + // SEO:如配置了插件 SEO,则覆盖页面标题与 meta + if (cfg.seo) { + if (cfg.seo.title) { + document.title = cfg.seo.title; + } + if (cfg.seo.keywords) { + var $kw = $('meta[name="keywords"]'); + if ($kw.length) { + $kw.attr("content", cfg.seo.keywords); + } + } + if (cfg.seo.description) { + var $desc = $('meta[name="description"]'); + if ($desc.length) { + $desc.attr("content", cfg.seo.description); + } + } + } + // 覆盖基础字段:企业信息 & ICP 等 baseData.enterprise_name = cfg.enterprise_name || baseData.enterprise_name; @@ -142,6 +161,9 @@ $(function () { cfg.enterprise_qrcode || baseData.enterprise_qrcode; baseData.official_website_logo = cfg.official_website_logo || baseData.official_website_logo; + baseData.online_customer_service_link = + cfg.online_customer_service_link || + baseData.online_customer_service_link; baseData.icp_info = cfg.icp_info || baseData.icp_info; baseData.icp_info_link = cfg.icp_info_link || baseData.icp_info_link; @@ -177,6 +199,15 @@ $(function () { if (cfg.feedback_type) { baseData.feedback_type = cfg.feedback_type; } + if (cfg.header_nav) { + baseData.header_nav = cfg.header_nav; + } + if (cfg.footer_nav) { + baseData.footer_nav = cfg.footer_nav; + } + if (cfg.side_floating_window) { + baseData.side_floating_window = cfg.side_floating_window; + } } sessionStorage.commentData = JSON.stringify(baseData); diff --git a/plugins/addon/theme_configurator/controller/clientarea/ThemeController.php b/plugins/addon/theme_configurator/controller/clientarea/ThemeController.php index 5df5882..1007df8 100644 --- a/plugins/addon/theme_configurator/controller/clientarea/ThemeController.php +++ b/plugins/addon/theme_configurator/controller/clientarea/ThemeController.php @@ -21,6 +21,8 @@ class ThemeController extends PluginBaseController $config = $model->getConfig(); $data = [ + // SEO 配置单独返回,前端可用于覆盖 /<meta> 等 + 'seo' => $config['seo'] ?? [], 'enterprise_name' => $config['site_config']['enterprise_name'] ?? '', 'enterprise_telephone' => $config['site_config']['enterprise_telephone'] ?? '', 'enterprise_mailbox' => $config['site_config']['enterprise_mailbox'] ?? '', diff --git a/plugins/addon/theme_configurator/template/admin/index.html b/plugins/addon/theme_configurator/template/admin/index.html index e8cdb74..59d30ae 100644 --- a/plugins/addon/theme_configurator/template/admin/index.html +++ b/plugins/addon/theme_configurator/template/admin/index.html @@ -1,297 +1,673 @@ <link rel="stylesheet" href="/plugins/addon/theme_configurator/template/admin/theme.css" /> <div id="theme-config-app" class="template" v-cloak> - <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-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"> - <label>Logo 地址</label> - <t-input v-model="fullConfig.site_config.official_website_logo" placeholder="/upload/logo.png"></t-input> - </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>二维码地址</label> - <t-input v-model="fullConfig.site_config.enterprise_qrcode" placeholder="/upload/qrcode.png"></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="© 2024 主题云"></t-input> - </div> - </div> - </t-card> - - <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> + <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="banner.title" placeholder="如:弹性算力"></t-input> + <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="banner.description" placeholder="一句宣传语"></t-input> + <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>图片地址</label> - <t-input v-model="banner.img" placeholder="/upload/banner.png"></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"> - <label>新窗口打开</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"> - <label>按钮新窗口</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.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> + <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> - <t-input v-model="item.url" placeholder="https://example.com"></t-input> + <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> - </div> - <t-button theme="primary" variant="outline" @click="addFriendlyLink">新增友情链接</t-button> - </t-card> - - <t-card class="theme-card" title="荣誉与合作伙伴" bordered> - <h4 class="sub-title">企业荣誉</h4> - <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> + <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="item.img" placeholder="/upload/honor.png"></t-input> + <label>版权信息</label> + <t-input + v-model="fullConfig.site_config.copyright_info" + placeholder="© 2025 主题云" + ></t-input> </div> </div> - </div> - <t-button theme="primary" variant="outline" @click="addHonor">新增荣誉</t-button> + </t-card> - <h4 class="sub-title mt-10">合作伙伴/成功案例</h4> - <div v-if="!fullConfig.partner.length" class="empty-tip">用于首页“典型案例/合作伙伴”模块(partner)。</div> - <div class="config-item" v-for="(item, index) in fullConfig.partner" :key="'partner-' + index"> - <div class="config-item__header"> - <h4>伙伴 {{ index + 1 }}</h4> - <t-button size="small" theme="danger" variant="outline" @click="removePartner(index)"> - 删除 - </t-button> + <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="item.name" placeholder="合作伙伴/客户名称"></t-input> + <label>站点标题</label> + <t-input v-model="fullConfig.seo.title" placeholder="首页标题"></t-input> </div> <div class="form-item"> - <label>图片地址</label> - <t-input v-model="item.img" placeholder="/upload/partner.png"></t-input> + <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="item.description" :autosize="{ minRows: 2, maxRows: 3 }" placeholder="一句话介绍该案例"></t-textarea> - </div> - </div> - </div> - <t-button theme="primary" variant="outline" @click="addPartner">新增合作伙伴</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-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"> - <label>图标地址</label> - <t-input v-model="item.icon" placeholder="/upload/side-phone.png"></t-input> - </div> - <div class="form-item form-item--full"> - <label>内容(支持 HTML)</label> <t-textarea - v-model="item.content" + v-model="fullConfig.seo.description" :autosize="{ minRows: 2, maxRows: 4 }" - placeholder="<p>7x24 小时不间断服务</p>" + placeholder="站点描述" ></t-textarea> </div> </div> - </div> - <t-button theme="primary" variant="outline" @click="addSide">新增浮窗</t-button> - </t-card> + </t-card> + </t-tab-panel> - <t-card v-if="showAdvanced" 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 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"> + <label>描述</label> + <t-input v-model="banner.description" 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.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.url" placeholder="/cloud.html"></t-input> + </div> + <div class="form-item"> + <label>新窗口打开</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"> + <label>按钮新窗口</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> + <h4 class="sub-title">企业荣誉</h4> + <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> + + <h4 class="sub-title mt-10">合作伙伴/成功案例</h4> + <div v-if="!fullConfig.partner.length" class="empty-tip"> + 用于首页“典型案例/合作伙伴”模块(partner)。 + </div> + <div + class="config-item" + v-for="(item, index) in fullConfig.partner" + :key="'partner-' + index" + > + <div class="config-item__header"> + <h4>伙伴 {{ index + 1 }}</h4> + <t-button size="small" theme="danger" variant="outline" @click="removePartner(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/partner.png"></t-input> + <t-upload + theme="custom" + :action="uploadUrl" + :headers="uploadHeaders" + :format-response="uploadFormatResponse" + :show-upload-progress="false" + :max="1" + @success="(ctx) => handleUpload(['partner', 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 form-item--full"> + <label>描述</label> + <t-textarea + v-model="item.description" + :autosize="{ minRows: 2, maxRows: 3 }" + placeholder="一句话介绍该案例" + ></t-textarea> + </div> + </div> + </div> + <t-button theme="primary" variant="outline" @click="addPartner">新增合作伙伴</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"> + <label>新窗口打开</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"> + <label>新窗口打开</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 form-item--full"> + <label>链接地址</label> + <t-input v-model="link.url" placeholder="/cloud.html"></t-input> + </div> + <div class="form-item"> + <label>新窗口打开</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="showAdvanced = !showAdvanced"> - {{ showAdvanced ? '隐藏高级 JSON' : '显示高级 JSON' }} - </t-button> + <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> @@ -325,6 +701,7 @@ enterprise_mailbox: "", enterprise_qrcode: "", official_website_logo: "", + online_customer_service_link: "", icp_info: "", icp_info_link: "", public_security_network_preparation: "", @@ -355,6 +732,11 @@ fullConfig: createDefaultConfig(), advancedText: "", showAdvanced: false, + activeTab: "basic", + uploadUrl: `${host}/${adminPath}/v1/upload`, + uploadHeaders: { + Authorization: "Bearer " + localStorage.getItem("backJwt"), + }, }; }, computed: { @@ -364,11 +746,34 @@ } 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(); - return { + const merged = { ...defaults, ...data, seo: { @@ -380,31 +785,95 @@ ...(data.site_config || {}), }, banner: Array.isArray(data.banner) ? data.banner : [], - header_nav: Array.isArray(data.header_nav) ? data.header_nav : [], + 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 - : [], + 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 - : [], + ? 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 - : [], + ? data.site_config.honor + : [], partner: Array.isArray(data.partner) ? data.partner : Array.isArray(data.site_config && data.site_config.partner) - ? 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", "上传失败:未获取到响应数据"); + } + let 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; @@ -424,7 +893,7 @@ }); }, syncJson() { - this.advancedText = JSON.stringify(this.fullConfig, null, 2); + this.advancedText = JSON.stringify(this.fullConfig, null, 2); }, applyAdvanced() { if (!this.advancedText.trim()) { @@ -516,6 +985,85 @@ 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 = { @@ -536,6 +1084,15 @@ 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(); @@ -543,3 +1100,4 @@ }); })(); </script> + diff --git a/plugins/addon/theme_configurator/template/admin/theme.css b/plugins/addon/theme_configurator/template/admin/theme.css index e964046..a487c5c 100644 --- a/plugins/addon/theme_configurator/template/admin/theme.css +++ b/plugins/addon/theme_configurator/template/admin/theme.css @@ -97,6 +97,11 @@ margin-top: 20px; } +.upload-row { + display: flex; + align-items: center; +} + .mt-10 { margin-top: 10px; }