更新website后端

This commit is contained in:
2026-09-15 00:08:36 +08:00
parent 7e6a3e969f
commit 9875832019
30 changed files with 1428 additions and 38 deletions
+22
View File
@@ -0,0 +1,22 @@
import request from '@/utils/request'
// ==================== 官网管理 - 站点设置(平台端) ====================
// 数据落在 yz_platform_site_setting(单行配置),字段可被官网模板标签调用:
// {yz:sitename} {yz:logo} {yz:logow} {yz:icp} {yz:policeicp} {yz:copyright} {yz:privacy} {yz:terms}
// 读取站点设置
export function getWebsiteSetting() {
return request({
url: '/platform/website/setting/info',
method: 'get'
})
}
// 保存站点设置
export function saveWebsiteSetting(data) {
return request({
url: '/platform/website/setting/save',
method: 'post',
data
})
}
+6
View File
@@ -27,6 +27,12 @@ const staticMainChildren = [
component: () => import("@/views/tools/passwordStore/index.vue"),
meta: { requiresAuth: true, title: "密码存储" }
},
{
path: "/website/setting",
name: "PlatformWebsiteSetting",
component: () => import("@/views/website/setting/index.vue"),
meta: { requiresAuth: true, title: "站点设置" }
},
{
path: "/template/index",
name: "PlatformTemplateManage",
@@ -0,0 +1,56 @@
<template>
<div class="tag-tip">
<span class="tag-tip__label">调用标签</span>
<code class="tag-tip__code">{{ tag }}</code>
<el-button link type="primary" size="small" @click="copy">复制</el-button>
<span v-if="desc" class="tag-tip__desc">{{ desc }}</span>
</div>
</template>
<script setup>
import { ElMessage } from 'element-plus'
const props = defineProps({
// 标签代码,如 {yz:sitename}
tag: { type: String, required: true },
// 用法补充说明
desc: { type: String, default: '' }
})
const copy = async () => {
try {
await navigator.clipboard.writeText(props.tag)
ElMessage.success('已复制')
} catch (e) {
ElMessage.error('复制失败,请手动复制')
}
}
</script>
<style lang="less" scoped>
.tag-tip {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 6px;
margin-top: 4px;
font-size: 12px;
line-height: 20px;
&__label {
color: #909399;
}
&__code {
padding: 1px 6px;
border-radius: 3px;
background: #f4f4f5;
color: #3973ff;
font-family: Consolas, Monaco, monospace;
}
&__desc {
color: #a8abb2;
}
}
</style>
@@ -0,0 +1,154 @@
<template>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="130px"
class="tab-form"
>
<el-form-item label="站点名称" prop="sitename">
<el-input v-model="form.sitename" placeholder="请输入站点名称" />
<TagTip tag="{yz:sitename}" desc="常用于 &lt;title&gt; 和页头" />
</el-form-item>
<el-form-item label="副标题" prop="subtitle">
<el-input v-model="form.subtitle" placeholder="请输入副标题" />
<TagTip tag="{yz:subtitle}" desc="站点名称下方的补充说明,常用于页头/首页" />
</el-form-item>
<el-form-item label="Logo(深色)" prop="logo">
<el-upload
class="logo-uploader"
:show-file-list="false"
:auto-upload="false"
:on-change="(file) => handleUpload(file, 'logo')"
>
<img v-if="form.logo" :src="previewUrl(form.logo)" class="logo-image" />
<el-icon v-else class="logo-uploader-icon"><Plus /></el-icon>
</el-upload>
<TagTip tag="{yz:logo}" desc="深色 Logo,用于浅色背景(如页头)" />
</el-form-item>
<el-form-item label="Logo(浅色)" prop="logow">
<el-upload
class="logo-uploader"
:show-file-list="false"
:auto-upload="false"
:on-change="(file) => handleUpload(file, 'logow')"
>
<img
v-if="form.logow"
:src="previewUrl(form.logow)"
class="logo-image darkbg"
/>
<el-icon v-else class="logo-uploader-icon"><Plus /></el-icon>
</el-upload>
<TagTip tag="{yz:logow}" desc="浅色 Logo,用于深色背景(如页脚)" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="saving" @click="handleSave">保存设置</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
import { ref } from 'vue'
import { ElMessage } from 'element-plus'
import { Plus } from '@element-plus/icons-vue'
import { uploadFile } from '@/api/file'
import TagTip from './TagTip.vue'
const props = defineProps({
// 与父组件共享的表单对象(key 即标签名)
form: { type: Object, required: true },
saving: { type: Boolean, default: false }
})
const emit = defineEmits(['save', 'reset'])
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL
const formRef = ref(null)
const rules = {
sitename: [{ required: true, message: '请输入站点名称', trigger: 'blur' }]
}
/** 图片预览地址:完整 URL 直接使用,站内相对路径拼接后端地址 */
const previewUrl = (url) => {
if (!url) return ''
if (/^https?:\/\//i.test(url)) return url
return API_BASE_URL + url.replace(/^\//, '/')
}
// Logo 上传:上传成功后回填图片地址
const handleUpload = (file, field) => {
const fd = new FormData()
fd.append('file', file.raw)
fd.append('cate', 'site')
uploadFile(fd).then((res) => {
if ((res.code === 200 || res.code === 201) && res.data && res.data.url) {
props.form[field] = res.data.url.replace(/\\/g, '/')
} else {
ElMessage.error(res.msg || '上传失败')
}
})
}
const handleSave = async () => {
if (!formRef.value) return
await formRef.value.validate((valid) => {
if (valid) emit('save')
})
}
const handleReset = () => {
formRef.value?.clearValidate()
emit('reset')
}
</script>
<style lang="less" scoped>
.tab-form {
max-width: 720px;
}
.darkbg {
background-color: #efefef;
}
.logo-uploader {
:deep(.el-upload) {
border: 1px dashed #d9d9d9;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: all 0.3s;
&:hover {
border-color: #3973ff;
}
}
.logo-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 100px;
height: 100px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
}
.logo-image {
width: 100px;
height: 100px;
object-fit: contain;
display: block;
}
}
</style>
@@ -0,0 +1,66 @@
<template>
<el-form ref="formRef" :model="form" label-width="130px" class="tab-form">
<el-form-item label="公司名称" prop="companyname">
<el-input v-model="form.companyname" placeholder="请输入公司名称" />
<TagTip tag="{yz:companyname}" desc="公司全称,常用于页脚与「关于我们」" />
</el-form-item>
<el-form-item label="联系电话" prop="phone">
<el-input v-model="form.phone" placeholder="如:400-000-0000" />
<TagTip tag="{yz:phone}" desc="服务热线,常用于页头与页脚" />
</el-form-item>
<el-form-item label="电子邮箱" prop="email">
<el-input v-model="form.email" placeholder="如:contact@yunzer.com" />
<TagTip tag="{yz:email}" desc="商务合作邮箱,可配合 mailto: 使用" />
</el-form-item>
<el-form-item label="公司地址" prop="address">
<el-input
v-model="form.address"
type="textarea"
:rows="2"
placeholder="请输入公司地址"
/>
<TagTip tag="{yz:address}" desc="公司所在地址,展示于页脚/联系页面" />
</el-form-item>
<el-form-item label="工作时间" prop="worktime">
<el-input v-model="form.worktime" placeholder="如:工作日 9:00 - 18:00" />
<TagTip tag="{yz:worktime}" desc="常用于页脚与联系我们页面" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="saving" @click="handleSave">保存设置</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
import { ref } from 'vue'
import TagTip from './TagTip.vue'
defineProps({
// 与父组件共享的表单对象(key 即标签名)
form: { type: Object, required: true },
saving: { type: Boolean, default: false }
})
const emit = defineEmits(['save', 'reset'])
const formRef = ref(null)
const handleSave = () => emit('save')
const handleReset = () => {
formRef.value?.clearValidate()
emit('reset')
}
</script>
<style lang="less" scoped>
.tab-form {
max-width: 720px;
}
</style>
@@ -0,0 +1,52 @@
<template>
<el-form ref="formRef" :model="form" label-width="130px" class="tab-form">
<el-form-item label="隐私政策" prop="privacy">
<el-input v-model="form.privacy" placeholder="如:/policy/privacy" />
<TagTip
tag="{yz:privacy}"
desc="链接地址,模板中用 &lt;a href=&quot;{yz:privacy}&quot;&gt; 承接"
/>
</el-form-item>
<el-form-item label="使用条款" prop="terms">
<el-input v-model="form.terms" placeholder="如:/policy/terms" />
<TagTip
tag="{yz:terms}"
desc="链接地址,模板中用 &lt;a href=&quot;{yz:terms}&quot;&gt; 承接"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="saving" @click="handleSave">保存设置</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
import { ref } from 'vue'
import TagTip from './TagTip.vue'
defineProps({
// 与父组件共享的表单对象(key 即标签名)
form: { type: Object, required: true },
saving: { type: Boolean, default: false }
})
const emit = defineEmits(['save', 'reset'])
const formRef = ref(null)
const handleSave = () => emit('save')
const handleReset = () => {
formRef.value?.clearValidate()
emit('reset')
}
</script>
<style lang="less" scoped>
.tab-form {
max-width: 720px;
}
</style>
@@ -0,0 +1,51 @@
<template>
<el-form ref="formRef" :model="form" label-width="130px" class="tab-form">
<el-form-item label="ICP 备案号" prop="icp">
<el-input v-model="form.icp" placeholder="如:苏ICP备2026000000号-1" />
<TagTip tag="{yz:icp}" desc="常见于页脚,请填写真实备案号" />
</el-form-item>
<el-form-item label="公安备案" prop="policeicp">
<el-input v-model="form.policeicp" placeholder="如:苏公网安备32010000000000号" />
<TagTip tag="{yz:policeicp}" desc="公安网备案号,常见于页脚" />
</el-form-item>
<el-form-item label="版权信息" prop="copyright">
<el-input v-model="form.copyright" placeholder="如:© 2026 公司名称 版权所有" />
<TagTip tag="{yz:copyright}" desc="常用于页脚" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="saving" @click="handleSave">保存设置</el-button>
<el-button @click="handleReset">重置</el-button>
</el-form-item>
</el-form>
</template>
<script setup>
import { ref } from 'vue'
import TagTip from './TagTip.vue'
defineProps({
// 与父组件共享的表单对象(key 即标签名)
form: { type: Object, required: true },
saving: { type: Boolean, default: false }
})
const emit = defineEmits(['save', 'reset'])
const formRef = ref(null)
const handleSave = () => emit('save')
const handleReset = () => {
formRef.value?.clearValidate()
emit('reset')
}
</script>
<style lang="less" scoped>
.tab-form {
max-width: 720px;
}
</style>
@@ -0,0 +1,184 @@
<template>
<div class="container-box website-setting">
<div class="header-bar">
<h2>站点设置</h2>
</div>
<el-alert
class="tips"
type="info"
:closable="false"
show-icon
title="每一项都对应一个官网模板标签 {yz:标签名},数据保存在 yz_platform_site_setting(通用键值表,key 即标签名)。"
/>
<div class="setting-container" v-loading="loading">
<el-tabs v-model="activeTab" class="setting-tabs">
<el-tab-pane label="基本设置" name="basic">
<tab-basic
v-if="activeTab === 'basic'"
:form="form"
:saving="saving"
@save="handleSave"
@reset="resetForm"
/>
</el-tab-pane>
<el-tab-pane label="公司信息" name="company">
<tab-company
v-if="activeTab === 'company'"
:form="form"
:saving="saving"
@save="handleSave"
@reset="resetForm"
/>
</el-tab-pane>
<el-tab-pane label="备案信息" name="record">
<tab-record
v-if="activeTab === 'record'"
:form="form"
:saving="saving"
@save="handleSave"
@reset="resetForm"
/>
</el-tab-pane>
<el-tab-pane label="政策条款" name="policy">
<tab-policy
v-if="activeTab === 'policy'"
:form="form"
:saving="saving"
@save="handleSave"
@reset="resetForm"
/>
</el-tab-pane>
</el-tabs>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { getWebsiteSetting, saveWebsiteSetting } from '@/api/websiteSetting'
import tabBasic from './components/tab_basic.vue'
import tabCompany from './components/tab_company.vue'
import tabRecord from './components/tab_record.vue'
import tabPolicy from './components/tab_policy.vue'
const activeTab = ref('basic')
const loading = ref(false)
const saving = ref(false)
// 表单字段的 key 与数据表的 tag 一一对应(提交时即为 { 标签名: 值 })
const form = reactive({
// 基本设置
sitename: '', // {yz:sitename}
subtitle: '', // {yz:subtitle}
logo: '', // {yz:logo}
logow: '', // {yz:logow}
// 公司信息
companyname: '', // {yz:companyname}
phone: '', // {yz:phone}
email: '', // {yz:email}
address: '', // {yz:address}
worktime: '', // {yz:worktime}
// 备案信息
icp: '', // {yz:icp}
policeicp: '', // {yz:policeicp}
copyright: '', // {yz:copyright}
// 政策条款
privacy: '', // {yz:privacy}
terms: '' // {yz:terms}
})
// 拉取站点设置:接口返回 values(标签名 -> 值)回填到表单
const initSetting = async () => {
loading.value = true
try {
const res = await getWebsiteSetting()
if (res.code === 200 && res.data) {
const data = res.data.values || {}
Object.keys(form).forEach((key) => {
form[key] = data[key] || ''
})
} else {
ElMessage.error(res.msg || '获取站点设置失败')
}
} catch (e) {
ElMessage.error('获取站点设置失败')
} finally {
loading.value = false
}
}
// 保存:整个表单按「标签名 -> 值」提交,后端按 tag 逐项 upsert
const handleSave = async () => {
if (!String(form.sitename || '').trim()) {
ElMessage.warning('站点名称不能为空')
return
}
saving.value = true
try {
const res = await saveWebsiteSetting({ ...form })
if (res.code === 200) {
ElMessage.success('保存成功')
} else {
ElMessage.error(res.msg || '保存失败')
}
} catch (e) {
ElMessage.error('保存失败')
} finally {
saving.value = false
}
}
// 重置:丢弃本地改动,重新拉取服务端配置
const resetForm = () => {
initSetting()
}
onMounted(() => {
initSetting()
})
</script>
<style lang="less" scoped>
.container-box {
padding: 20px;
border-radius: 4px;
}
.header-bar {
display: flex;
justify-content: space-between;
align-items: center;
color: var(--el-text-color-primary);
h2 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
}
.tips {
margin-top: 16px;
}
.setting-container {
margin-top: 16px;
}
.setting-tabs {
:deep(.el-tabs__header) {
margin-bottom: 20px;
}
:deep(.el-tabs__nav-wrap::after) {
height: 1px;
}
}
</style>