更新日报相关功能

This commit is contained in:
hero920103 committed 2026-10-22 17:55:50 +08:00
1 parent 51fd90fc51
commit 68cdaf38fa
23 files changed
+2313 -1056

No files matched your search

+24
View File
@@ -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>