增加ai办公,考勤管理

This commit is contained in:
hero920103 committed 2026-11-14 18:15:55 +08:00
1 parent eb9b7beff5
commit 31ae07175d
46 files changed
+9250 -296

No files matched your search

+14
View File
@@ -1,5 +1,19 @@
<script setup>
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import { onMounted } from 'vue'
import { getWebsiteSetting } from '@/api/websiteSetting'
import { applyFavicon } from '@/utils/favicon'
// 浏览器标签页图标:取「平台设置 - 平台信息」的站点图标,未配置则保留默认图标
onMounted(async () => {
if (!localStorage.getItem('token')) return
try {
const res = await getWebsiteSetting()
if (res?.code === 200) applyFavicon(res.data?.values?.ico)
} catch (error) {
// 图标属于装饰信息,取不到时不影响页面
}
})
</script>
<template>
+42
View File
@@ -0,0 +1,42 @@
/**
* 浏览器标签页图标(favicon)。
*
* 图标来源:平台「系统设置 - 平台设置 - 平台信息」的「站点图标」(标签 ico)。
* 未配置时保留 index.html 里的默认图标,不做任何改写。
*/
const API_BASE_URL = String(import.meta.env.VITE_API_BASE_URL || "").replace(/\/+$/, "");
/** 相对路径(/uploads/x.ico)补上接口基址,完整 URL 原样使用 */
function resolveIconUrl(url) {
const value = String(url || "").trim().replace(/\\/g, "/");
if (!value) return "";
if (/^(https?:)?\/\//i.test(value) || /^(data|blob):/i.test(value)) return value;
return `${API_BASE_URL}/${value.replace(/^\/+/, "")}`;
}
/**
* 把指定地址应用为页面 favicon。
* @param {string} url 图片地址(可相对)
* @returns {boolean} 是否已应用
*/
export function applyFavicon(url) {
const href = resolveIconUrl(url);
if (!href) return false;
let link = document.querySelector('link[rel="icon"]');
if (!link) {
link = document.createElement("link");
link.setAttribute("rel", "icon");
document.head.appendChild(link);
}
// .ico 显式声明 MIME,其余交给浏览器按扩展名/内容识别
if (/\.ico(\?|#|$)/i.test(href)) {
link.setAttribute("type", "image/x-icon");
} else {
link.removeAttribute("type");
}
link.setAttribute("href", href);
return true;
}
+396 -73
View File
@@ -1,32 +1,120 @@
<template>
<div class="not-found-container">
<div class="not-found-content">
<div class="not-found-big">404</div>
<div class="not-found-message">页面未找到</div>
<div class="not-found-desc">很抱歉,您访问的页面不存在或已被删除。</div>
<div class="action-btns">
<a href="javascript:void(0)" class="back-home-btn" @click.prevent="backToHome">
返回首页
</a>
<a href="javascript:void(0)" class="clear-cache-btn" @click.prevent="clearCache">
清除缓存
</a>
<div class="not-found-page">
<!-- 背景装饰:柔光 + 网格 -->
<div class="bg-decor" aria-hidden="true">
<span class="orb orb-1"></span>
<span class="orb orb-2"></span>
<span class="grid"></span>
</div>
<div class="not-found-card">
<div class="visual">
<p class="code">404</p>
<svg class="illustration" viewBox="0 0 360 240" fill="none" aria-hidden="true">
<defs>
<linearGradient id="nf-blue" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#5b8cff" />
<stop offset="100%" stop-color="#2f6bff" />
</linearGradient>
<linearGradient id="nf-paper" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#ffffff" />
<stop offset="100%" stop-color="#eef4ff" />
</linearGradient>
</defs>
<ellipse cx="180" cy="214" rx="104" ry="12" fill="#2f6bff" opacity=".08" />
<g transform="rotate(-6 180 118)">
<rect x="96" y="40" width="168" height="152" rx="16" fill="url(#nf-paper)" stroke="#d8e3f8" />
<rect x="120" y="68" width="86" height="10" rx="5" fill="#dce7fb" />
<rect x="120" y="90" width="120" height="10" rx="5" fill="#e9f0fd" />
<rect x="120" y="112" width="70" height="10" rx="5" fill="#e9f0fd" />
<rect
x="120"
y="140"
width="120"
height="26"
rx="8"
fill="none"
stroke="#b9cdf4"
stroke-dasharray="6 5"
/>
</g>
<g transform="translate(214 94)">
<circle cx="34" cy="34" r="34" fill="#ffffff" fill-opacity=".92" stroke="url(#nf-blue)" stroke-width="6" />
<path d="M58 58L86 86" stroke="url(#nf-blue)" stroke-width="10" stroke-linecap="round" />
<text
x="34"
y="46"
text-anchor="middle"
font-size="34"
font-weight="700"
fill="#2f6bff"
font-family="inherit"
>
?
</text>
</g>
<circle cx="74" cy="70" r="6" fill="#7aa2ff" opacity=".7" />
<circle cx="306" cy="158" r="9" fill="#9dbcff" opacity=".5" />
</svg>
</div>
<div class="content">
<p class="eyebrow"><i class="dot"></i>ERROR 404</p>
<h1>页面走丢了</h1>
<p class="desc">
你访问的页面不存在、已被移除,或没有访问权限。<br />
请检查链接是否正确,或返回首页继续操作。
</p>
<p v-if="currentPath" class="path" :title="currentPath">{{ currentPath }}</p>
<div class="actions">
<button type="button" class="btn ghost" @click="backToPrev">返回上一页</button>
<button type="button" class="btn primary" @click="backToHome">返回首页</button>
</div>
<p class="footnote">
<button type="button" class="link" @click="clearCache">清除本地缓存</button>
<span class="sep">·</span>
若是系统内部链接失效,请联系管理员
</p>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { useRouter } from "vue-router";
import { computed } from "vue";
import { useRoute, useRouter } from "vue-router";
import { ElMessage, ElMessageBox } from "element-plus";
const router = useRouter();
const route = useRoute();
// 当前访问路径:方便用户/管理员定位是哪个链接失效
const currentPath = computed(() => {
const full = route.fullPath || "";
if (!full || full === "/") return "";
return full.length > 60 ? `${full.slice(0, 60)}…` : full;
});
const backToHome = () => {
// 使用 /home 作为首页(Hash模式下的系统导航页)
// Hash 模式下 /home 是系统导航门户
router.push("/home");
};
const backToPrev = () => {
// 有站内历史就退一步,否则回首页
if (window.history.length > 1) {
router.back();
return;
}
backToHome();
};
const clearAllCookies = () => {
const cookies = document.cookie ? document.cookie.split(";") : [];
for (const cookie of cookies) {
@@ -57,94 +145,329 @@ const clearCache = async () => {
</script>
<style scoped>
.not-found-container {
.not-found-page {
position: relative;
box-sizing: border-box;
min-height: 100vh;
background: linear-gradient(120deg, #6a82fb 0%, #fc5c7d 100%);
display: flex;
justify-content: center;
align-items: center;
justify-content: center;
padding: 40px 24px;
overflow: hidden;
background:
radial-gradient(900px 480px at 10% 6%, #e9f0ff 0%, rgba(233, 240, 255, 0) 62%),
radial-gradient(760px 420px at 92% 96%, #eef4ff 0%, rgba(238, 244, 255, 0) 64%),
#f6f9ff;
color: #24304a;
}
.not-found-content {
background: #fff;
padding: 60px 48px 40px;
border-radius: 16px;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.1);
text-align: center;
animation: float-in 0.8s cubic-bezier(0.7, 0.13, 0.45, 0.81) both;
.bg-decor {
position: absolute;
inset: 0;
pointer-events: none;
}
.not-found-big {
font-size: 100px;
font-weight: bold;
letter-spacing: 3px;
background: linear-gradient(90deg, #6a82fb 0%, #fc5c7d 100%);
.orb {
position: absolute;
border-radius: 50%;
}
.orb-1 {
width: 360px;
height: 360px;
left: -110px;
top: -120px;
background: radial-gradient(circle at 35% 35%, #cfdcff 0%, rgba(207, 220, 255, 0) 70%);
}
.orb-2 {
width: 460px;
height: 460px;
right: -150px;
bottom: -170px;
background: radial-gradient(circle at 60% 40%, #d9e4ff 0%, rgba(217, 228, 255, 0) 70%);
}
.grid {
position: absolute;
inset: 0;
background-image:
linear-gradient(to right, rgba(47, 107, 255, 0.07) 1px, transparent 1px),
linear-gradient(to bottom, rgba(47, 107, 255, 0.07) 1px, transparent 1px);
background-size: 56px 56px;
-webkit-mask-image: radial-gradient(ellipse at center, #000 28%, transparent 76%);
mask-image: radial-gradient(ellipse at center, #000 28%, transparent 76%);
}
.not-found-card {
position: relative;
z-index: 1;
display: grid;
grid-template-columns: minmax(260px, 380px) minmax(300px, 400px);
align-items: center;
gap: 8px 32px;
width: 100%;
max-width: 900px;
padding: 36px 44px;
border: 1px solid #e6eefc;
border-radius: 28px;
background: rgba(255, 255, 255, 0.86);
box-shadow:
0 26px 60px -30px rgba(37, 74, 161, 0.3),
0 2px 10px rgba(37, 74, 161, 0.05);
backdrop-filter: blur(8px);
animation: nf-rise 0.5s cubic-bezier(0.22, 0.68, 0.32, 1) both;
}
.visual {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
}
.code {
margin: 0;
font-size: 84px;
font-weight: 800;
line-height: 1;
letter-spacing: 4px;
background: linear-gradient(135deg, #6f97ff 0%, #2f6bff 58%, #1d4ed8 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
color: transparent;
}
.illustration {
width: 100%;
max-width: 300px;
height: auto;
}
.content {
min-width: 0;
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: 8px;
margin: 0 0 10px;
padding: 5px 12px;
border-radius: 999px;
background: #eef4ff;
color: #2f6bff;
font-size: 12px;
font-weight: 700;
letter-spacing: 1.2px;
}
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: #2f6bff;
box-shadow: 0 0 0 3px rgba(47, 107, 255, 0.16);
}
h1 {
margin: 0 0 12px;
font-size: 30px;
line-height: 1.25;
font-weight: 700;
color: #1b2740;
}
.desc {
margin: 0 0 14px;
font-size: 14px;
line-height: 1.8;
color: #66738c;
}
.path {
display: inline-block;
max-width: 100%;
margin: 0 0 22px;
padding: 6px 12px;
border-radius: 8px;
background: #f2f6ff;
border: 1px dashed #c9d9f7;
color: #5a6a88;
font-size: 12px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 18px;
}
.not-found-message {
font-size: 28px;
color: #222;
margin-bottom: 10px;
.btn {
height: 40px;
padding: 0 22px;
border-radius: 10px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition:
transform 0.15s ease,
box-shadow 0.2s ease,
background-color 0.2s ease,
border-color 0.2s ease;
}
.not-found-desc {
font-size: 16px;
color: #888;
margin-bottom: 28px;
.btn:active {
transform: translateY(1px);
}
.action-btns {
display: flex;
gap: 12px;
justify-content: center;
}
.back-home-btn {
display: inline-block;
background: linear-gradient(90deg, #6a82fb 0%, #fc5c7d 100%);
.btn.primary {
border: none;
color: #fff;
padding: 10px 36px;
border-radius: 24px;
font-size: 16px;
text-decoration: none;
font-weight: 500;
transition: background 0.2s;
box-shadow: 0 2px 8px rgba(252, 92, 125, 0.07);
background: linear-gradient(135deg, #4d82ff 0%, #2f6bff 100%);
box-shadow: 0 10px 22px -10px rgba(47, 107, 255, 0.7);
}
.back-home-btn:hover {
background: linear-gradient(-90deg, #6a82fb 0%, #fc5c7d 100%);
.btn.primary:hover {
box-shadow: 0 14px 26px -12px rgba(47, 107, 255, 0.8);
background: linear-gradient(135deg, #5c8dff 0%, #3a74ff 100%);
}
.clear-cache-btn {
display: inline-block;
.btn.ghost {
border: 1px solid #d7e2f6;
background: #fff;
color: #fc5c7d;
padding: 10px 36px;
border-radius: 24px;
font-size: 16px;
text-decoration: none;
font-weight: 500;
border: 1px solid #fc5c7d;
transition: background 0.2s;
color: #40506e;
}
.clear-cache-btn:hover {
background: #fff0f3;
.btn.ghost:hover {
border-color: #b9cdf4;
background: #f7faff;
color: #2f6bff;
}
@keyframes float-in {
0% {
transform: translateY(30px) scale(0.97);
.footnote {
margin: 0;
font-size: 12px;
color: #93a0b8;
}
.link {
padding: 0;
border: none;
background: none;
color: #6b8fd9;
font-size: 12px;
cursor: pointer;
transition: color 0.2s ease;
}
.link:hover {
color: #2f6bff;
text-decoration: underline;
}
.sep {
margin: 0 8px;
}
@keyframes nf-rise {
from {
opacity: 0;
transform: translateY(14px);
}
100% {
transform: translateY(0) scale(1);
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 860px) {
.not-found-card {
grid-template-columns: 1fr;
gap: 4px;
padding: 28px 24px 32px;
text-align: center;
}
.code {
font-size: 64px;
}
.illustration {
max-width: 240px;
}
.actions {
justify-content: center;
}
.footnote,
.path {
text-align: center;
}
}
/* 深色主题(backend 支持亮/暗切换;platform 无暗色时不会命中) */
html.dark .not-found-page {
background:
radial-gradient(900px 480px at 10% 6%, rgba(47, 107, 255, 0.18) 0%, rgba(47, 107, 255, 0) 62%),
radial-gradient(760px 420px at 92% 96%, rgba(84, 122, 214, 0.14) 0%, rgba(84, 122, 214, 0) 64%),
#10151f;
color: #e5ebf7;
}
html.dark .not-found-card {
border-color: #263049;
background: rgba(24, 31, 46, 0.9);
box-shadow: 0 26px 60px -30px rgba(0, 0, 0, 0.8);
}
html.dark .eyebrow {
background: rgba(47, 107, 255, 0.16);
color: #8fb0ff;
}
html.dark h1 {
color: #eef2fb;
}
html.dark .desc {
color: #9aa7c2;
}
html.dark .path {
border-color: #33415e;
background: #1a2233;
color: #9fb0cd;
}
html.dark .btn.ghost {
border-color: #303c56;
background: #1c2434;
color: #cfdaf0;
}
html.dark .btn.ghost:hover {
border-color: #3f5580;
background: #212b3f;
color: #8fb0ff;
}
html.dark .footnote {
color: #7d8aa6;
}
html.dark .link {
color: #7fa0e0;
}
html.dark .link:hover {
color: #a9c4ff;
}
</style>
@@ -0,0 +1,473 @@
<template>
<div class="platform-info-settings" v-loading="loading">
<el-form
ref="formRef"
:model="form"
:rules="rules"
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">
平台品牌与备案信息,保存在 yz_platform_site_setting(key 即模板标签名),保存后立即生效
</span>
</div>
</template>
<el-form-item label="主标题" prop="sitename">
<el-input
v-model="form.sitename"
maxlength="60"
show-word-limit
placeholder="如:温新云平台"
/>
<div class="form-tip">
平台对外展示的主标题,用于后台顶栏、登录页与官网页头
<code class="tag-chip" title="点击复制" @click="copyTag('{yz:sitename}')">{yz:sitename}</code>。
</div>
</el-form-item>
<el-form-item label="副标题" prop="subtitle">
<el-input
v-model="form.subtitle"
maxlength="80"
show-word-limit
placeholder="如:数据赋能管理 · 科技创造价值"
/>
<div class="form-tip">
主标题下方的补充说明,显示在后台顶栏主标题下方,留空则显示当前功能模块名
<code class="tag-chip" title="点击复制" @click="copyTag('{yz:subtitle}')">{yz:subtitle}</code>。
</div>
</el-form-item>
<el-form-item v-for="item in logoFields" :key="item.key" :label="item.label">
<div class="upload-row">
<el-upload
class="asset-uploader"
:class="[item.shape, { 'is-dark': item.dark }]"
:show-file-list="false"
:auto-upload="false"
accept="image/png,image/jpeg,image/webp,image/svg+xml,image/gif"
:on-change="(file) => handleUpload(file, item.key)"
>
<img
v-if="form[item.key]"
:src="previewUrl(form[item.key])"
class="asset-image"
:alt="item.label"
/>
<el-icon v-else class="uploader-icon"><Plus /></el-icon>
</el-upload>
<div class="upload-meta">
<p>{{ item.tip }}</p>
<p>
标签:
<code
class="tag-chip"
title="点击复制"
@click="copyTag(tagOf(item.key))"
>{{ tagOf(item.key) }}</code>
</p>
<el-button v-if="form[item.key]" link type="danger" @click="form[item.key] = ''">
移除 Logo
</el-button>
</div>
</div>
</el-form-item>
<el-form-item label="站点图标">
<div class="upload-row">
<el-upload
class="asset-uploader is-icon"
:show-file-list="false"
:auto-upload="false"
accept=".ico,image/png,image/svg+xml,image/jpeg,image/webp,image/gif"
:on-change="(file) => handleUpload(file, 'ico')"
>
<img
v-if="form.ico"
:src="previewUrl(form.ico)"
class="asset-image"
alt="站点图标"
/>
<el-icon v-else class="uploader-icon"><Plus /></el-icon>
</el-upload>
<div class="upload-meta">
<p>浏览器标签页图标(favicon),建议 32×32 / 64×64 的 .ico 或 PNG。</p>
<p>
标签:
<code class="tag-chip" title="点击复制" @click="copyTag('{yz:ico}')">{yz:ico}</code>
</p>
<el-button v-if="form.ico" link type="danger" @click="form.ico = ''">
移除图标
</el-button>
</div>
</div>
</el-form-item>
<el-form-item label="ICP 备案号" prop="icp">
<el-input v-model="form.icp" maxlength="80" placeholder="如:苏ICP备2026000000号-1" />
<div class="form-tip">
标签:
<code class="tag-chip" title="点击复制" @click="copyTag('{yz:icp}')">{yz:icp}</code>
</div>
</el-form-item>
<el-form-item label="网警平台备案" prop="policeicp">
<el-input
v-model="form.policeicp"
maxlength="80"
placeholder="如:苏公网安备32010000000000号"
/>
<div class="form-tip">
标签:
<code class="tag-chip" title="点击复制" @click="copyTag('{yz:policeicp}')">{yz:policeicp}</code>
</div>
</el-form-item>
<el-form-item label="平台简介" prop="platform_intro">
<el-input
v-model="form.platform_intro"
type="textarea"
:rows="4"
maxlength="300"
show-word-limit
placeholder="一句话介绍平台定位,如:面向中小企业的一站式数字化管理与官网服务平台"
/>
<div class="form-tip">
标签:
<code class="tag-chip" title="点击复制" @click="copyTag('{yz:platform_intro}')">{yz:platform_intro}</code>,留空则不展示。
</div>
</el-form-item>
</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 { getWebsiteSetting, saveWebsiteSetting } from "@/api/websiteSetting";
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);
// 表单 key 与 yz_platform_site_setting.tag 一一对应
// sitename = 主标题(与官网页头、{yz:sitename} 同源),subtitle = 副标题
const form = reactive({
sitename: "",
subtitle: "",
logo_square: "",
logo_square_dark: "",
logo_rect: "",
logo_rect_dark: "",
ico: "",
icp: "",
policeicp: "",
platform_intro: "",
});
// Logo 四个变体:正方形 / 长方形 × 浅色 / 深色主题
const logoFields = [
{
key: "logo_square",
label: "Logo(正方形)",
shape: "is-square",
dark: false,
tip: "建议 1:1 方形图标(≥ 128×128),用于后台顶栏等小尺寸场景。",
},
{
key: "logo_square_dark",
label: "Logo(正方形 · 深色主题)",
shape: "is-square",
dark: true,
tip: "深色主题下替换为这张方形图,留空则沿用浅色主题的方形 Logo。",
},
{
key: "logo_rect",
label: "Logo(长方形)",
shape: "is-rect",
dark: false,
tip: "建议 4:1 ~ 3:1 横向品牌图,用于页头等较宽场景,透明背景 PNG 效果最佳。",
},
{
key: "logo_rect_dark",
label: "Logo(长方形 · 深色主题)",
shape: "is-rect",
dark: true,
tip: "深色主题下替换为这张横向图(浅色文字),留空则回退官网浅色 Logo。",
},
];
const rules = {
sitename: [{ required: true, message: "请输入主标题", trigger: "blur" }],
};
const previewUrl = (url) => {
if (!url) return "";
if (/^(https?:)?\/\//i.test(url) || url.startsWith("data:")) return url;
return `${API_BASE_URL}/${url.replace(/^\/+/, "")}`;
};
// 标签名 -> 「{yz:tag}」文本(模板里不能直接写花括号字面量,会与插值语法冲突)
const tagOf = (key) => `{yz:${key}}`;
// 标签点击即复制:模板里要反复粘贴 {yz:xxx},避免手打出错
const copyTag = async (text) => {
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(text);
} else {
const input = document.createElement("textarea");
input.value = text;
input.setAttribute("readonly", "");
input.style.position = "fixed";
input.style.opacity = "0";
document.body.appendChild(input);
input.select();
document.execCommand("copy");
document.body.removeChild(input);
}
ElMessage.success(`已复制 ${text}`);
} catch (error) {
ElMessage.error("复制失败,请手动选择复制");
}
};
// 上传:与官网管理共用「官网素材」分组,便于在文件管理里集中复用
const handleUpload = async (file, field) => {
const raw = file?.raw;
if (!raw) return;
// .ico 在部分浏览器/系统上 MIME 为空或 image/x-icon,这里按扩展名一并放行
const isImage = String(raw.type || "").startsWith("image/") || /\.ico$/i.test(raw.name || "");
if (!isImage) {
ElMessage.error("仅支持图片格式");
return;
}
if (raw.size > MAX_IMAGE_SIZE) {
ElMessage.error("图片不能超过 5MB");
return;
}
try {
const res = await uploadToWebsiteMaterial(raw);
if ((res.code === 200 || res.code === 201) && res.data && res.data.url) {
form[field] = res.data.url.replace(/\\/g, "/");
} else {
ElMessage.error(res.msg || "上传失败");
}
} catch (error) {
ElMessage.error(error?.response?.data?.msg || "上传失败");
}
};
const initSetting = async () => {
loading.value = true;
try {
const res = await getWebsiteSetting();
if (res.code === 200 && res.data) {
const values = res.data.values || {};
Object.keys(form).forEach((key) => {
form[key] = values[key] || "";
});
} else {
ElMessage.error(res.msg || "获取平台信息失败");
}
} catch (error) {
ElMessage.error("获取平台信息失败");
} finally {
loading.value = false;
}
};
const handleSubmit = async () => {
if (!formRef.value) return;
await formRef.value.validate(async (valid) => {
if (!valid) return;
submitting.value = true;
try {
const res = await saveWebsiteSetting({
sitename: String(form.sitename || "").trim(),
subtitle: String(form.subtitle || "").trim(),
logo_square: String(form.logo_square || "").trim(),
logo_square_dark: String(form.logo_square_dark || "").trim(),
logo_rect: String(form.logo_rect || "").trim(),
logo_rect_dark: String(form.logo_rect_dark || "").trim(),
ico: String(form.ico || "").trim(),
icp: String(form.icp || "").trim(),
policeicp: String(form.policeicp || "").trim(),
platform_intro: String(form.platform_intro || "").trim(),
});
if (res.code === 200) {
ElMessage.success("保存成功");
} else {
ElMessage.error(res.msg || "保存失败");
}
} catch (error) {
ElMessage.error(error?.response?.data?.msg || "保存失败");
} finally {
submitting.value = false;
}
});
};
const handleReset = () => {
formRef.value?.clearValidate();
initSetting();
};
onMounted(initSetting);
</script>
<style lang="less" scoped>
.platform-info-settings {
display: flex;
flex-direction: column;
gap: 16px;
}
.settings-card {
margin-bottom: 12px;
}
.card-header {
display: flex;
align-items: baseline;
gap: 12px;
font-size: 16px;
font-weight: 600;
}
.card-tip {
font-size: 12px;
font-weight: 400;
color: var(--el-text-color-secondary);
}
.form-tip {
width: 100%;
margin-top: 4px;
font-size: 12px;
line-height: 1.6;
color: var(--el-text-color-secondary);
}
code {
padding: 0 4px;
border-radius: 3px;
background: var(--el-fill-color-light);
font-size: 12px;
}
// 可点击复制的模板标签
.tag-chip {
display: inline-block;
padding: 1px 6px;
border: 1px solid transparent;
border-radius: 4px;
background: var(--el-fill-color-light);
color: var(--el-color-primary);
font-size: 12px;
line-height: 18px;
cursor: pointer;
user-select: none;
transition: color 0.2s ease, background-color 0.2s ease, border-color 0.2s ease;
&:hover {
border-color: var(--el-color-primary-light-5);
background: var(--el-color-primary-light-9);
}
&:active {
background: var(--el-color-primary-light-8);
}
}
.upload-row {
display: flex;
align-items: flex-end;
gap: 16px;
}
.upload-meta {
flex: 1;
min-width: 0;
font-size: 12px;
line-height: 1.7;
color: var(--el-text-color-secondary);
p {
margin: 0 0 2px;
}
}
.asset-uploader {
:deep(.el-upload) {
display: flex;
align-items: center;
justify-content: center;
border: 1px dashed #d9d9d9;
border-radius: 8px;
background: #fafbfe;
cursor: pointer;
overflow: hidden;
transition: border-color 0.3s;
&:hover {
border-color: #3973ff;
}
}
&.is-square :deep(.el-upload) {
width: 88px;
height: 88px;
}
&.is-rect :deep(.el-upload) {
width: 176px;
height: 88px;
}
&.is-icon :deep(.el-upload) {
width: 88px;
height: 88px;
}
// 深色主题 Logo:预览底色改成暗色,浅色 Logo 才看得见
&.is-dark :deep(.el-upload) {
background: #1b1f2b;
border-color: #3a4152;
}
}
.uploader-icon {
font-size: 24px;
color: #8c939d;
}
.asset-image {
display: block;
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.footer-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
}
</style>
@@ -8,6 +8,11 @@
<div class="settings-container">
<el-tabs v-model="activeTab" class="settings-tabs">
<!-- 平台信息:站点名称 / Logo / 备案 / 平台简介(yz_platform_site_setting) -->
<el-tab-pane label="平台信息" name="platformInfo">
<platformInfoSettings v-if="activeTab === 'platformInfo'" />
</el-tab-pane>
<!-- 验证开关 -->
<el-tab-pane label="验证开关" name="platform">
<platformSettings
@@ -43,12 +48,13 @@
<script setup lang="ts">
import { ref } from "vue";
import platformInfoSettings from "./components/platformInfoSettings.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");
const activeTab = ref("platformInfo");
</script>
<style lang="less" scoped>