增加ai办公,考勤管理
This commit is contained in:
1 parent
eb9b7beff5
commit
31ae07175d
46 files changed
+9250
-296
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user