Files
BlackFruit-UI/plugins/addon/theme_configurator/template/admin/index.html
yiqiu 4ce31b68a9
All checks were successful
continuous-integration/drone/push Build is passing
优化插件UI
2025-11-21 16:21:34 +08:00

1104 lines
40 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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">
<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="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", "上传失败:未获取到响应数据");
}
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;
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: "",
description: "",
img: "",
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);
},
addPartner() {
if (!Array.isArray(this.fullConfig.partner)) {
this.fullConfig.partner = [];
}
this.fullConfig.partner.push({
name: "",
img: "",
description: "",
});
},
removePartner(index) {
this.fullConfig.partner.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>