更新日报相关功能
This commit is contained in:
1 parent
51fd90fc51
commit
68cdaf38fa
23 files changed
+2313
-1056
No files matched your search
@@ -187,4 +187,28 @@ export function sendTestBark(data) {
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取统一登录页品牌配置(主标题 / 副标题 / Logo / 背景图)
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function getLoginBrand() {
|
||||
return request({
|
||||
url: "/platform/loginBrand",
|
||||
method: "get",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存统一登录页品牌配置
|
||||
* @param {Object} data { login_title, login_subtitle, login_logo, login_bg }
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export function saveLoginBrand(data) {
|
||||
return request({
|
||||
url: "/platform/saveLoginBrand",
|
||||
method: "post",
|
||||
data: data,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
<template>
|
||||
<div class="login-brand-settings" v-loading="loading">
|
||||
<el-form ref="formRef" :model="form" label-width="140px" class="settings-form">
|
||||
<el-card shadow="never" class="settings-card">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>统一登录页品牌</span>
|
||||
<span class="card-tip">
|
||||
保存后用于统一认证中心登录页(/auth/login),未配置项使用默认文案
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form-item label="主标题" prop="login_title">
|
||||
<el-input
|
||||
v-model="form.login_title"
|
||||
maxlength="60"
|
||||
show-word-limit
|
||||
placeholder="如:一次登录,全平台通行"
|
||||
/>
|
||||
<div class="form-tip">显示在登录页左侧背景图上方,留空使用默认文案。</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="副标题" prop="login_subtitle">
|
||||
<el-input
|
||||
v-model="form.login_subtitle"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
maxlength="120"
|
||||
show-word-limit
|
||||
placeholder="如:基于 OIDC / OAuth 2.0 的统一身份底座"
|
||||
/>
|
||||
<div class="form-tip">主标题下方的补充说明,留空使用默认文案。</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="Logo">
|
||||
<div class="upload-row">
|
||||
<el-upload
|
||||
class="asset-uploader is-logo"
|
||||
:show-file-list="false"
|
||||
:auto-upload="false"
|
||||
accept="image/png,image/jpeg,image/webp,image/svg+xml,image/gif"
|
||||
:on-change="(file) => handleUpload(file, 'login_logo')"
|
||||
>
|
||||
<img
|
||||
v-if="form.login_logo"
|
||||
:src="previewUrl(form.login_logo)"
|
||||
class="asset-image"
|
||||
alt="登录页 Logo"
|
||||
/>
|
||||
<el-icon v-else class="uploader-icon"><Plus /></el-icon>
|
||||
</el-upload>
|
||||
<div class="upload-meta">
|
||||
<p>显示在登录页左上角,建议透明背景 PNG / SVG,高度不小于 88px。</p>
|
||||
<el-button v-if="form.login_logo" link type="danger" @click="form.login_logo = ''">
|
||||
移除 Logo
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="背景图">
|
||||
<div class="upload-row">
|
||||
<el-upload
|
||||
class="asset-uploader is-bg"
|
||||
:show-file-list="false"
|
||||
:auto-upload="false"
|
||||
accept="image/png,image/jpeg,image/webp"
|
||||
:on-change="(file) => handleUpload(file, 'login_bg')"
|
||||
>
|
||||
<img
|
||||
v-if="form.login_bg"
|
||||
:src="previewUrl(form.login_bg)"
|
||||
class="asset-image"
|
||||
alt="登录页背景图"
|
||||
/>
|
||||
<el-icon v-else class="uploader-icon"><Plus /></el-icon>
|
||||
</el-upload>
|
||||
<div class="upload-meta">
|
||||
<p>铺满登录页左侧,建议浅色系竖版图(≥1200×1600)。深色图片会让标题不清晰。</p>
|
||||
<el-button v-if="form.login_bg" link type="danger" @click="form.login_bg = ''">
|
||||
移除背景图
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-divider content-position="left">效果预览</el-divider>
|
||||
|
||||
<div class="brand-preview">
|
||||
<div class="preview-left" :class="{ 'has-bg': !!form.login_bg }">
|
||||
<img v-if="form.login_bg" :src="previewUrl(form.login_bg)" class="preview-bg" alt="" />
|
||||
<span v-if="form.login_bg" class="preview-veil"></span>
|
||||
<div class="preview-inner">
|
||||
<img
|
||||
v-if="form.login_logo"
|
||||
:src="previewUrl(form.login_logo)"
|
||||
class="preview-logo"
|
||||
alt="Logo"
|
||||
/>
|
||||
<div v-else class="preview-logo-fallback">云泽统一认证中心</div>
|
||||
<p class="preview-title">{{ form.login_title || "一次登录,全平台通行" }}</p>
|
||||
<p class="preview-sub">
|
||||
{{ form.login_subtitle || "基于 OIDC / OAuth 2.0 的统一身份底座" }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="preview-right">
|
||||
<p class="mock-title">登录</p>
|
||||
<div class="mock-tabs">
|
||||
<span class="active">账号密码</span>
|
||||
<span>短信登录</span>
|
||||
<span>扫码登录</span>
|
||||
</div>
|
||||
<div class="mock-input"></div>
|
||||
<div class="mock-input"></div>
|
||||
<div class="mock-btn">登 录</div>
|
||||
<div class="mock-third">微信登录 · QQ 登录</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-form>
|
||||
|
||||
<div class="footer-actions">
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="handleSubmit">保存设置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { Plus } from "@element-plus/icons-vue";
|
||||
import { uploadToWebsiteMaterial } from "@/utils/websiteUpload";
|
||||
import { getLoginBrand, saveLoginBrand } from "@/api/sitesettings";
|
||||
|
||||
const API_BASE_URL = (import.meta.env.VITE_API_BASE_URL || "").replace(/\/+$/, "");
|
||||
const MAX_IMAGE_SIZE = 5 * 1024 * 1024;
|
||||
|
||||
const formRef = ref();
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
|
||||
const form = reactive({
|
||||
login_title: "",
|
||||
login_subtitle: "",
|
||||
login_logo: "",
|
||||
login_bg: "",
|
||||
});
|
||||
|
||||
// 服务端快照,用于“重置”丢弃未保存的改动
|
||||
let snapshot = { ...form };
|
||||
|
||||
const previewUrl = (url) => {
|
||||
const value = String(url || "").trim().replace(/\\/g, "/");
|
||||
if (!value) return "";
|
||||
if (/^(https?:)?\/\//i.test(value) || value.startsWith("data:")) return value;
|
||||
return `${API_BASE_URL}/${value.replace(/^\/+/, "")}`;
|
||||
};
|
||||
|
||||
const loadConfig = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await getLoginBrand();
|
||||
if (res?.code === 200 && res.data) {
|
||||
form.login_title = res.data.login_title || "";
|
||||
form.login_subtitle = res.data.login_subtitle || "";
|
||||
form.login_logo = res.data.login_logo || "";
|
||||
form.login_bg = res.data.login_bg || "";
|
||||
snapshot = { ...form };
|
||||
} else {
|
||||
ElMessage.error(res?.msg || "获取登录页配置失败");
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.response?.data?.msg || "获取登录页配置失败");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleUpload = async (file, field) => {
|
||||
const raw = file?.raw;
|
||||
if (!raw) return;
|
||||
if (!String(raw.type || "").startsWith("image/")) {
|
||||
ElMessage.warning("请选择图片文件");
|
||||
return;
|
||||
}
|
||||
if (raw.size > MAX_IMAGE_SIZE) {
|
||||
ElMessage.warning("图片不能超过 5MB");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await uploadToWebsiteMaterial(raw);
|
||||
const url = res?.data?.url;
|
||||
if ((res?.code === 200 || res?.code === 201) && url) {
|
||||
form[field] = String(url).replace(/\\/g, "/");
|
||||
ElMessage.success("上传成功");
|
||||
} else {
|
||||
ElMessage.error(res?.msg || "上传失败");
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.response?.data?.msg || "上传失败");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (submitting.value) return;
|
||||
submitting.value = true;
|
||||
try {
|
||||
const res = await saveLoginBrand({
|
||||
login_title: String(form.login_title || "").trim(),
|
||||
login_subtitle: String(form.login_subtitle || "").trim(),
|
||||
login_logo: String(form.login_logo || "").trim(),
|
||||
login_bg: String(form.login_bg || "").trim(),
|
||||
});
|
||||
if (res?.code === 200) {
|
||||
ElMessage.success("保存成功,统一登录页已同步更新");
|
||||
snapshot = { ...form };
|
||||
} else {
|
||||
ElMessage.error(res?.msg || "保存失败");
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(error?.response?.data?.msg || "保存失败");
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
Object.assign(form, snapshot);
|
||||
ElMessage.info("已恢复到上次保存的配置");
|
||||
};
|
||||
|
||||
onMounted(loadConfig);
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.login-brand-settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.settings-card {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.card-tip {
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.form-tip {
|
||||
width: 100%;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 1.6;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.upload-row {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.upload-meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
line-height: 1.7;
|
||||
|
||||
p {
|
||||
margin: 0 0 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.asset-uploader {
|
||||
:deep(.el-upload) {
|
||||
border: 1px dashed #d9d9d9;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
transition: border-color 0.3s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #fafbfe;
|
||||
|
||||
&:hover {
|
||||
border-color: #3973ff;
|
||||
}
|
||||
}
|
||||
|
||||
&.is-logo :deep(.el-upload) {
|
||||
width: 160px;
|
||||
height: 72px;
|
||||
}
|
||||
|
||||
&.is-bg :deep(.el-upload) {
|
||||
width: 132px;
|
||||
height: 104px;
|
||||
}
|
||||
}
|
||||
|
||||
.uploader-icon {
|
||||
font-size: 24px;
|
||||
color: #8c939d;
|
||||
}
|
||||
|
||||
.asset-image {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* ---------- 效果预览:左 58% 品牌区 + 右 42% 表单区 ---------- */
|
||||
.brand-preview {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
height: 260px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.preview-left {
|
||||
position: relative;
|
||||
flex: 1 1 58%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(150deg, #1b3b8f 0%, #2f6fe4 52%, #52a8ff 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.preview-left.has-bg {
|
||||
background: #eaf1fb;
|
||||
color: #1f2b48;
|
||||
}
|
||||
|
||||
.preview-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.preview-veil {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(100deg, rgba(255, 255, 255, 0.86), rgba(255, 255, 255, 0.2));
|
||||
}
|
||||
|
||||
.preview-inner {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.preview-logo {
|
||||
height: 26px;
|
||||
max-width: 140px;
|
||||
object-fit: contain;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.preview-logo-fallback {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 18px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.preview-title {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
white-space: pre-line;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.preview-sub {
|
||||
margin: 8px 0 0;
|
||||
font-size: 11px;
|
||||
line-height: 1.7;
|
||||
opacity: 0.78;
|
||||
white-space: pre-line;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.preview-right {
|
||||
flex: 0 0 42%;
|
||||
background: #fff;
|
||||
padding: 26px 24px;
|
||||
}
|
||||
|
||||
.mock-title {
|
||||
margin: 0 0 12px;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #1f2b48;
|
||||
}
|
||||
|
||||
.mock-tabs {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid #eef2fa;
|
||||
font-size: 9px;
|
||||
color: #8a94ad;
|
||||
|
||||
.active {
|
||||
color: #3973ff;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.mock-input {
|
||||
height: 24px;
|
||||
border: 1px solid #e3e9f5;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
background: #fbfcff;
|
||||
}
|
||||
|
||||
.mock-btn {
|
||||
height: 28px;
|
||||
margin-top: 14px;
|
||||
border-radius: 6px;
|
||||
background: linear-gradient(135deg, #3973ff 0%, #52a8ff 100%);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
letter-spacing: 2px;
|
||||
}
|
||||
|
||||
.mock-third {
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
font-size: 9px;
|
||||
color: #a3acbf;
|
||||
}
|
||||
</style>
|
||||
@@ -31,6 +31,11 @@
|
||||
v-if="activeTab === 'notification'"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 登录页设置(统一认证中心 /auth/login) -->
|
||||
<el-tab-pane label="登录页设置" name="loginBrand">
|
||||
<loginBrandSettings v-if="activeTab === 'loginBrand'" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</div>
|
||||
@@ -41,6 +46,7 @@ import { ref } from "vue";
|
||||
import platformSettings from "./components/platformSettings.vue";
|
||||
import storageSettings from "./components/storageSettings.vue";
|
||||
import notificationSettings from "./components/notificationSettings.vue";
|
||||
import loginBrandSettings from "./components/loginBrandSettings.vue";
|
||||
|
||||
const activeTab = ref("platform");
|
||||
</script>
|
||||
|
||||
Reference in new issue
Block a user