feat: 优化管理后台发布体验和页面层级
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from "vue";
|
||||
import { computed, nextTick, onMounted, onUnmounted, reactive, ref } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import {
|
||||
adminRequest,
|
||||
@@ -32,6 +32,14 @@ const activeTab = ref("overview");
|
||||
const loading = ref(false);
|
||||
const publishing = ref(false);
|
||||
const publishJobMessage = ref("");
|
||||
const pageRef = ref<HTMLElement | null>(null);
|
||||
const dashboardRef = ref<HTMLElement | null>(null);
|
||||
const dashboardSpacerHeight = ref(0);
|
||||
const dashboardStyle = reactive<Record<string, string>>({
|
||||
top: "0px",
|
||||
right: "0px",
|
||||
left: "0px"
|
||||
});
|
||||
const apps = ref<any[]>([]);
|
||||
const channels = ref<any[]>([]);
|
||||
const versions = ref<any[]>([]);
|
||||
@@ -66,6 +74,8 @@ const publishForm = reactive({
|
||||
client_protocol: 3,
|
||||
mode: "directory"
|
||||
});
|
||||
const publishVersionManuallyEdited = ref(false);
|
||||
const lastSuggestedPublishVersion = ref("1.0.0");
|
||||
const policyForm = reactive<any>({
|
||||
channel: "stable",
|
||||
force_update: false,
|
||||
@@ -129,6 +139,7 @@ const releaseManifestError = ref("");
|
||||
const installRootDetectMessage = ref("");
|
||||
const installRootDialogVisible = ref(false);
|
||||
const installRootDialogReason = ref("");
|
||||
let dashboardResizeObserver: ResizeObserver | null = null;
|
||||
|
||||
const enabledChannels = computed(() =>
|
||||
channels.value.filter(item => item.enabled)
|
||||
@@ -154,6 +165,8 @@ const currentPolicyDisabledText = computed(() => {
|
||||
return disabled.length ? disabled.join(", ") : "无";
|
||||
});
|
||||
|
||||
const suggestedPublishVersion = computed(() => suggestPublishVersion());
|
||||
|
||||
const releaseManifestRules = computed(() =>
|
||||
parseReleaseManifest(releaseManifestText.value, selectedReleaseRoot.value)
|
||||
);
|
||||
@@ -313,6 +326,82 @@ function isLicenseUsable(row: any) {
|
||||
return row?.status === "active" && row?.license_key && !isLicenseFull(row);
|
||||
}
|
||||
|
||||
function compareVersionText(left: string, right: string) {
|
||||
const leftParts = String(left || "").match(/\d+/g)?.map(Number) || [];
|
||||
const rightParts = String(right || "").match(/\d+/g)?.map(Number) || [];
|
||||
const max = Math.max(leftParts.length, rightParts.length);
|
||||
for (let index = 0; index < max; index += 1) {
|
||||
const diff = (leftParts[index] || 0) - (rightParts[index] || 0);
|
||||
if (diff !== 0) return diff;
|
||||
}
|
||||
return String(left || "").localeCompare(String(right || ""));
|
||||
}
|
||||
|
||||
function incrementLastNumber(version: string) {
|
||||
const raw = String(version || "").trim();
|
||||
const match = raw.match(/^(.*?)(\d+)$/);
|
||||
if (!match) return "1.0.0";
|
||||
const [, prefix, numberText] = match;
|
||||
const nextNumber = String(Number(numberText) + 1).padStart(
|
||||
numberText.startsWith("0") ? numberText.length : 1,
|
||||
"0"
|
||||
);
|
||||
return `${prefix}${nextNumber}`;
|
||||
}
|
||||
|
||||
function latestVersionForPublish() {
|
||||
if (!publishForm.app_id || publishForm.app_id !== currentAppId.value) return "";
|
||||
const rows = versions.value.filter(row => row.channel === publishForm.channel);
|
||||
if (!rows.length) return "";
|
||||
const markedLatest = rows.find(row => row.latest);
|
||||
if (markedLatest?.version) return String(markedLatest.version);
|
||||
return (
|
||||
[...rows].sort((left, right) =>
|
||||
compareVersionText(String(right.version || ""), String(left.version || ""))
|
||||
)[0]?.version || ""
|
||||
);
|
||||
}
|
||||
|
||||
function suggestPublishVersion() {
|
||||
const latest = latestVersionForPublish();
|
||||
return latest ? incrementLastNumber(latest) : "1.0.0";
|
||||
}
|
||||
|
||||
function syncPublishVersionSuggestion(force = false) {
|
||||
const next = suggestPublishVersion();
|
||||
const canReplace =
|
||||
force ||
|
||||
!publishVersionManuallyEdited.value ||
|
||||
!publishForm.version ||
|
||||
publishForm.version === lastSuggestedPublishVersion.value;
|
||||
lastSuggestedPublishVersion.value = next;
|
||||
if (canReplace) {
|
||||
publishForm.version = next;
|
||||
publishVersionManuallyEdited.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handlePublishAppChange(appId: string) {
|
||||
const value = String(appId || "").trim();
|
||||
publishForm.app_id = value;
|
||||
publishVersionManuallyEdited.value = false;
|
||||
const existing = apps.value.find(item => item.app_id === value);
|
||||
if (existing && value !== currentAppId.value) {
|
||||
await selectApp(value);
|
||||
return;
|
||||
}
|
||||
syncPublishVersionSuggestion(true);
|
||||
}
|
||||
|
||||
function handlePublishChannelChange() {
|
||||
publishVersionManuallyEdited.value = false;
|
||||
syncPublishVersionSuggestion(true);
|
||||
}
|
||||
|
||||
function handlePublishVersionInput() {
|
||||
publishVersionManuallyEdited.value = true;
|
||||
}
|
||||
|
||||
async function loadRuntime() {
|
||||
const data = await adminRequest<any>("/admin/runtime-config");
|
||||
Object.assign(runtime, data || {});
|
||||
@@ -327,7 +416,9 @@ async function loadRuntime() {
|
||||
async function loadApps() {
|
||||
const data = await adminRequest<any>("/admin/app/list");
|
||||
apps.value = data?.list || [];
|
||||
if (!currentAppId.value && apps.value.length) selectApp(apps.value[0].app_id);
|
||||
if (!currentAppId.value && apps.value.length) {
|
||||
await selectApp(apps.value[0].app_id);
|
||||
}
|
||||
}
|
||||
|
||||
async function addApp() {
|
||||
@@ -345,17 +436,28 @@ async function addApp() {
|
||||
async function selectApp(appId: string) {
|
||||
currentAppId.value = appId;
|
||||
publishForm.app_id = appId;
|
||||
publishVersionManuallyEdited.value = false;
|
||||
configForm.main_executable = inferMainExecutable();
|
||||
await loadAppScope();
|
||||
syncPublishVersionSuggestion(true);
|
||||
}
|
||||
|
||||
async function loadAppScope() {
|
||||
if (!currentAppId.value) return;
|
||||
await loadChannels();
|
||||
const channel = enabledChannels.value[0]?.channel_code || "stable";
|
||||
publishForm.channel ||= channel;
|
||||
policyForm.channel ||= channel;
|
||||
licenseForm.channel ||= channel;
|
||||
const hasPublishChannel = enabledChannels.value.some(
|
||||
item => item.channel_code === publishForm.channel
|
||||
);
|
||||
const hasPolicyChannel = enabledChannels.value.some(
|
||||
item => item.channel_code === policyForm.channel
|
||||
);
|
||||
const hasLicenseChannel = enabledChannels.value.some(
|
||||
item => item.channel_code === licenseForm.channel
|
||||
);
|
||||
if (!publishForm.channel || !hasPublishChannel) publishForm.channel = channel;
|
||||
if (!policyForm.channel || !hasPolicyChannel) policyForm.channel = channel;
|
||||
if (!licenseForm.channel || !hasLicenseChannel) licenseForm.channel = channel;
|
||||
await Promise.allSettled([
|
||||
loadVersions(),
|
||||
loadPolicy(),
|
||||
@@ -483,6 +585,7 @@ async function publishVersion() {
|
||||
const result = await waitPublishJob(job?.job_id);
|
||||
ElMessage.success(result?.msg || `发布成功:${publishForm.version}`);
|
||||
await loadVersions();
|
||||
syncPublishVersionSuggestion(true);
|
||||
} catch (error) {
|
||||
handleError("发布失败", error);
|
||||
} finally {
|
||||
@@ -510,6 +613,7 @@ async function loadVersions() {
|
||||
`/admin/version/list?app_id=${encodeURIComponent(currentAppId.value)}`
|
||||
);
|
||||
versions.value = data?.list || [];
|
||||
syncPublishVersionSuggestion();
|
||||
}
|
||||
|
||||
async function setLatest(row: any) {
|
||||
@@ -991,40 +1095,86 @@ async function loadAll() {
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(loadAll);
|
||||
function updateFloatingDashboardMetrics() {
|
||||
nextTick(() => {
|
||||
if (!pageRef.value || !dashboardRef.value) return;
|
||||
const pageRect = pageRef.value.getBoundingClientRect();
|
||||
const dashboardRect = dashboardRef.value.getBoundingClientRect();
|
||||
const fixedHeader = document.querySelector<HTMLElement>(".fixed-header");
|
||||
const headerBottom =
|
||||
fixedHeader?.getBoundingClientRect().bottom ?? pageRect.top;
|
||||
const fixedTop = Math.max(0, headerBottom);
|
||||
const liftedGap = Math.max(0, pageRect.top - fixedTop);
|
||||
dashboardStyle.top = `${fixedTop}px`;
|
||||
dashboardStyle.left = `${Math.max(0, pageRect.left)}px`;
|
||||
dashboardStyle.right = `${Math.max(0, window.innerWidth - pageRect.right)}px`;
|
||||
dashboardSpacerHeight.value = Math.max(
|
||||
0,
|
||||
Math.ceil(dashboardRect.height + 18 - liftedGap)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function startFloatingDashboard() {
|
||||
updateFloatingDashboardMetrics();
|
||||
window.addEventListener("resize", updateFloatingDashboardMetrics);
|
||||
if (dashboardRef.value && typeof ResizeObserver !== "undefined") {
|
||||
dashboardResizeObserver = new ResizeObserver(updateFloatingDashboardMetrics);
|
||||
dashboardResizeObserver.observe(dashboardRef.value);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadAll();
|
||||
startFloatingDashboard();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("resize", updateFloatingDashboardMetrics);
|
||||
dashboardResizeObserver?.disconnect();
|
||||
dashboardResizeObserver = null;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-loading="loading" class="simcae-page">
|
||||
<div class="page-heading">
|
||||
<div>
|
||||
<h2>SimCAE 升级服务管理</h2>
|
||||
<p>应用、版本、License、设备、崩溃报告和审计日志统一管理</p>
|
||||
<div ref="pageRef" v-loading="loading" class="simcae-page">
|
||||
<div ref="dashboardRef" class="sticky-dashboard" :style="dashboardStyle">
|
||||
<div class="page-heading">
|
||||
<div>
|
||||
<h2>SimCAE 升级服务管理</h2>
|
||||
<p>应用、版本、License、设备、崩溃报告和审计日志统一管理</p>
|
||||
</div>
|
||||
<div class="heading-actions">
|
||||
<el-select
|
||||
v-model="currentAppId"
|
||||
filterable
|
||||
placeholder="选择应用"
|
||||
@change="selectApp"
|
||||
>
|
||||
<el-option
|
||||
v-for="app in apps"
|
||||
:key="app.app_id"
|
||||
:label="app.app_id"
|
||||
:value="app.app_id"
|
||||
>
|
||||
<div class="app-option">
|
||||
<strong>{{ app.app_id }}</strong>
|
||||
<span>{{ app.app_name }}</span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-button @click="loadAll">刷新全部</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="heading-actions">
|
||||
<el-select
|
||||
v-model="currentAppId"
|
||||
filterable
|
||||
placeholder="选择应用"
|
||||
@change="selectApp"
|
||||
>
|
||||
<el-option
|
||||
v-for="app in apps"
|
||||
:key="app.app_id"
|
||||
:label="`${app.app_name} / ${app.app_id}`"
|
||||
:value="app.app_id"
|
||||
/>
|
||||
</el-select>
|
||||
<el-button @click="loadAll">刷新全部</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-grid">
|
||||
<el-card shadow="never"><span>应用</span><strong>{{ apps.length }}</strong></el-card>
|
||||
<el-card shadow="never"><span>版本</span><strong>{{ versions.length }}</strong></el-card>
|
||||
<el-card shadow="never"><span>授权</span><strong>{{ licenses.length }}</strong></el-card>
|
||||
<el-card shadow="never"><span>设备</span><strong>{{ devices.length }}</strong></el-card>
|
||||
<div class="metric-grid">
|
||||
<el-card shadow="never"><span>应用</span><strong>{{ apps.length }}</strong></el-card>
|
||||
<el-card shadow="never"><span>版本</span><strong>{{ versions.length }}</strong></el-card>
|
||||
<el-card shadow="never"><span>授权</span><strong>{{ licenses.length }}</strong></el-card>
|
||||
<el-card shadow="never"><span>设备</span><strong>{{ devices.length }}</strong></el-card>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-spacer" :style="{ height: `${dashboardSpacerHeight}px` }" />
|
||||
|
||||
<el-tabs v-model="activeTab" class="console-tabs">
|
||||
<el-tab-pane label="应用渠道" name="overview">
|
||||
@@ -1104,17 +1254,62 @@ onMounted(loadAll);
|
||||
show-icon
|
||||
title="发布前请选择 SimCAE 的 Release 发布根目录,或上传已经压缩好的发布包。目录模式会按发布清单决定上传范围。"
|
||||
/>
|
||||
<div class="publish-flow">
|
||||
<div class="publish-flow-item">
|
||||
<strong>1. 选择应用</strong>
|
||||
<span>默认使用当前应用,也可以从已有 App ID 中选择或手动输入新 App ID。</span>
|
||||
</div>
|
||||
<div class="publish-flow-item">
|
||||
<strong>2. 确认版本</strong>
|
||||
<span>系统会按当前应用和渠道自动建议下一版,例如 1.1.3 后建议 1.1.4。</span>
|
||||
</div>
|
||||
<div class="publish-flow-item">
|
||||
<strong>3. 选择发布包</strong>
|
||||
<span>可以选择 Release 目录,也可以上传 zip、tar.gz、rar 等压缩包。</span>
|
||||
</div>
|
||||
<div class="publish-flow-item">
|
||||
<strong>4. 发布</strong>
|
||||
<span>发布成功后,客户端检查更新时会拿到新的 Manifest 和下载地址。</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-form label-position="top" :model="publishForm" class="form-grid">
|
||||
<el-form-item label="App ID">
|
||||
<el-input v-model="publishForm.app_id" placeholder="通常会自动填入当前应用" />
|
||||
<el-select
|
||||
v-model="publishForm.app_id"
|
||||
class="full-width"
|
||||
filterable
|
||||
allow-create
|
||||
default-first-option
|
||||
placeholder="选择已有 App ID,也可以手动输入"
|
||||
@change="handlePublishAppChange"
|
||||
>
|
||||
<el-option
|
||||
v-for="app in apps"
|
||||
:key="app.app_id"
|
||||
:label="app.app_id"
|
||||
:value="app.app_id"
|
||||
>
|
||||
<div class="app-option">
|
||||
<strong>{{ app.app_id }}</strong>
|
||||
<span>{{ app.app_name }}</span>
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
<div class="field-help">默认填入顶部当前应用;选择已有 App ID 会自动切换应用并加载版本。</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="版本号">
|
||||
<el-input v-model="publishForm.version" placeholder="例如 1.0.1" />
|
||||
<el-input
|
||||
v-model="publishForm.version"
|
||||
placeholder="例如 1.0.1"
|
||||
@input="handlePublishVersionInput"
|
||||
/>
|
||||
<div class="field-help">建议版本:{{ suggestedPublishVersion }};可以手动修改。</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="渠道">
|
||||
<el-select v-model="publishForm.channel">
|
||||
<el-select v-model="publishForm.channel" class="full-width" @change="handlePublishChannelChange">
|
||||
<el-option v-for="c in enabledChannels" :key="c.channel_code" :label="c.display_name" :value="c.channel_code" />
|
||||
</el-select>
|
||||
<div class="field-help">切换渠道后,会按该渠道的最新版本重新建议版本号。</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户端协议">
|
||||
<el-input-number v-model="publishForm.client_protocol" :min="1" />
|
||||
@@ -1320,7 +1515,7 @@ test_file/*
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="日志审计" name="logs">
|
||||
<el-tabs>
|
||||
<el-tabs class="inner-tabs">
|
||||
<el-tab-pane label="升级日志">
|
||||
<el-button type="danger" plain @click="clearLog('/admin/report/clear', loadUpgradeLogs)">清空</el-button>
|
||||
<el-table :data="upgradeLogs" stripe empty-text="暂无升级日志">
|
||||
@@ -1517,7 +1712,11 @@ test_file/*
|
||||
|
||||
<style scoped>
|
||||
.simcae-page {
|
||||
padding: 16px;
|
||||
min-height: 100%;
|
||||
padding: 20px;
|
||||
background:
|
||||
linear-gradient(180deg, #eef5ff 0, #f7fafc 260px, #f8fafc 100%);
|
||||
animation: page-fade-in 0.28s ease;
|
||||
}
|
||||
|
||||
.page-heading,
|
||||
@@ -1529,50 +1728,318 @@ test_file/*
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.sticky-dashboard {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
box-sizing: border-box;
|
||||
padding: 4px 4px 14px;
|
||||
background:
|
||||
linear-gradient(180deg, rgb(238 245 255 / 96%) 0%, rgb(248 250 252 / 88%) 100%);
|
||||
border-bottom: 1px solid rgb(15 23 42 / 6%);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.dashboard-spacer {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.sticky-dashboard::after {
|
||||
position: absolute;
|
||||
right: 18px;
|
||||
bottom: -1px;
|
||||
left: 18px;
|
||||
height: 18px;
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
background: linear-gradient(180deg, rgb(15 23 42 / 10%), transparent);
|
||||
filter: blur(10px);
|
||||
opacity: 0.38;
|
||||
}
|
||||
|
||||
.page-heading {
|
||||
position: relative;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 12px;
|
||||
padding: 18px 22px;
|
||||
overflow: hidden;
|
||||
color: #fff;
|
||||
background:
|
||||
linear-gradient(135deg, #102033 0%, #145b7b 54%, #16765d 100%);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 18px 38px rgb(16 32 51 / 18%);
|
||||
}
|
||||
|
||||
.page-heading::before {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
background:
|
||||
linear-gradient(115deg, transparent 0%, transparent 38%, rgb(255 255 255 / 14%) 48%, transparent 58%, transparent 100%);
|
||||
transform: translateX(-80%);
|
||||
animation: heading-sheen 7s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.page-heading > * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.page-heading h2 {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.page-heading p {
|
||||
margin: 6px 0 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
color: rgb(255 255 255 / 78%);
|
||||
}
|
||||
|
||||
.heading-actions .el-select {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
.heading-actions :deep(.el-select__wrapper) {
|
||||
background: rgb(255 255 255 / 92%);
|
||||
border: 0;
|
||||
box-shadow: 0 8px 22px rgb(0 0 0 / 12%);
|
||||
}
|
||||
|
||||
.heading-actions :deep(.el-button) {
|
||||
color: #0f513f;
|
||||
font-weight: 700;
|
||||
background: #f5fffb;
|
||||
border: 0;
|
||||
box-shadow: 0 8px 22px rgb(0 0 0 / 10%);
|
||||
}
|
||||
|
||||
.metric-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.metric-grid :deep(.el-card) {
|
||||
--metric-color: #2563eb;
|
||||
--metric-bg: #eff6ff;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, var(--metric-bg), #fff);
|
||||
border: 1px solid rgb(15 23 42 / 6%);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 12px 26px rgb(15 23 42 / 7%);
|
||||
transition:
|
||||
box-shadow 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
}
|
||||
|
||||
.metric-grid :deep(.el-card:nth-child(2)) {
|
||||
--metric-color: #0891b2;
|
||||
--metric-bg: #ecfeff;
|
||||
}
|
||||
|
||||
.metric-grid :deep(.el-card:nth-child(3)) {
|
||||
--metric-color: #16a34a;
|
||||
--metric-bg: #f0fdf4;
|
||||
}
|
||||
|
||||
.metric-grid :deep(.el-card:nth-child(4)) {
|
||||
--metric-color: #d97706;
|
||||
--metric-bg: #fffbeb;
|
||||
}
|
||||
|
||||
.metric-grid :deep(.el-card:hover) {
|
||||
box-shadow: 0 18px 34px rgb(15 23 42 / 12%);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.metric-grid :deep(.el-card__body) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-height: 60px;
|
||||
padding: 14px 18px;
|
||||
}
|
||||
|
||||
.metric-grid span {
|
||||
color: var(--el-text-color-secondary);
|
||||
color: #475569;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.metric-grid strong {
|
||||
color: var(--el-color-primary);
|
||||
font-size: 28px;
|
||||
color: var(--metric-color);
|
||||
font-size: 26px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.console-tabs {
|
||||
padding: 18px;
|
||||
background: rgb(255 255 255 / 92%);
|
||||
border: 1px solid rgb(15 23 42 / 6%);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 12px 34px rgb(15 23 42 / 8%);
|
||||
}
|
||||
|
||||
.console-tabs :deep(.el-tabs__header) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.console-tabs :deep(.el-tabs__nav-wrap::after),
|
||||
.console-tabs :deep(.el-tabs__active-bar) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.console-tabs :deep(.el-tabs__nav) {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.console-tabs :deep(.el-tabs__item) {
|
||||
height: 42px;
|
||||
padding: 0 18px !important;
|
||||
color: #334155;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
border-radius: 8px;
|
||||
transition:
|
||||
color 0.18s ease,
|
||||
background-color 0.18s ease,
|
||||
box-shadow 0.18s ease,
|
||||
transform 0.18s ease;
|
||||
}
|
||||
|
||||
.console-tabs :deep(.el-tabs__item:hover) {
|
||||
color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.console-tabs :deep(.el-tabs__item.is-active) {
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, var(--el-color-primary), #36a3ff);
|
||||
box-shadow: 0 8px 18px rgb(64 158 255 / 24%);
|
||||
}
|
||||
|
||||
.console-tabs :deep(.el-tabs__content) {
|
||||
animation: content-rise 0.2s ease;
|
||||
}
|
||||
|
||||
.console-tabs :deep(.el-card) {
|
||||
border: 1px solid rgb(15 23 42 / 7%);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgb(15 23 42 / 5%);
|
||||
transition:
|
||||
border-color 0.18s ease,
|
||||
box-shadow 0.18s ease,
|
||||
transform 0.18s ease;
|
||||
}
|
||||
|
||||
.console-tabs :deep(.el-card:hover) {
|
||||
border-color: rgb(64 158 255 / 28%);
|
||||
box-shadow: 0 14px 30px rgb(15 23 42 / 9%);
|
||||
}
|
||||
|
||||
.console-tabs :deep(.el-card__header) {
|
||||
font-weight: 800;
|
||||
background: linear-gradient(90deg, #f8fbff, #fff);
|
||||
border-bottom: 1px solid rgb(15 23 42 / 6%);
|
||||
}
|
||||
|
||||
.console-tabs :deep(.el-card__body) {
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.console-tabs :deep(.el-card__body > .el-form) {
|
||||
padding: 16px;
|
||||
background: var(--el-bg-color);
|
||||
margin-bottom: 16px;
|
||||
background: linear-gradient(135deg, #f8fbff, #fff);
|
||||
border: 1px solid rgb(64 158 255 / 14%);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.console-tabs :deep(.el-card__body > .el-form + .el-table),
|
||||
.console-tabs :deep(.el-card__body > .el-table) {
|
||||
padding: 6px;
|
||||
background: #fff;
|
||||
border: 1px solid rgb(15 23 42 / 7%);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.console-tabs :deep(.el-card__body > .el-form + .el-table) {
|
||||
margin-top: 18px;
|
||||
}
|
||||
|
||||
.console-tabs :deep(.el-table__header-wrapper th) {
|
||||
color: #334155;
|
||||
font-weight: 800;
|
||||
background: #f8fafc !important;
|
||||
}
|
||||
|
||||
.console-tabs :deep(.el-table__body-wrapper) {
|
||||
border-radius: 0 0 8px 8px;
|
||||
}
|
||||
|
||||
.console-tabs :deep(.el-alert + .el-form),
|
||||
.console-tabs :deep(.el-alert + .el-table) {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.console-tabs :deep(.el-form-item__label) {
|
||||
color: #334155;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.console-tabs :deep(.el-button:not(.is-link)) {
|
||||
transition:
|
||||
box-shadow 0.18s ease,
|
||||
transform 0.18s ease;
|
||||
}
|
||||
|
||||
.console-tabs :deep(.el-button:not(.is-link):hover) {
|
||||
box-shadow: 0 8px 18px rgb(64 158 255 / 18%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.console-tabs :deep(.el-table__row) {
|
||||
transition: background-color 0.16s ease;
|
||||
}
|
||||
|
||||
.inner-tabs {
|
||||
padding: 14px;
|
||||
background: linear-gradient(135deg, #f8fbff, #fff);
|
||||
border: 1px solid rgb(15 23 42 / 7%);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.inner-tabs :deep(.el-tabs__header) {
|
||||
padding: 8px;
|
||||
margin-bottom: 14px;
|
||||
background: #eef6ff;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.inner-tabs :deep(.el-tabs__nav) {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.inner-tabs :deep(.el-tabs__item) {
|
||||
height: 34px;
|
||||
padding: 0 14px !important;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
border-radius: 7px;
|
||||
}
|
||||
|
||||
.inner-tabs :deep(.el-tabs__item.is-active) {
|
||||
color: var(--el-color-primary);
|
||||
background: #fff;
|
||||
box-shadow: 0 6px 14px rgb(15 23 42 / 8%);
|
||||
}
|
||||
|
||||
.inner-tabs :deep(.el-table) {
|
||||
margin-top: 12px;
|
||||
border: 1px solid rgb(15 23 42 / 7%);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@@ -1590,8 +2057,9 @@ test_file/*
|
||||
.policy-summary {
|
||||
padding: 14px;
|
||||
margin-bottom: 16px;
|
||||
background: var(--el-fill-color-lighter);
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
background: linear-gradient(135deg, #f0fdf4, #fff);
|
||||
border: 1px solid rgb(22 163 74 / 18%);
|
||||
border-left: 4px solid #16a34a;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@@ -1632,6 +2100,18 @@ test_file/*
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.app-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.app-option span {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.el-table__row.app-selected-row > td.el-table__cell) {
|
||||
background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
@@ -1650,14 +2130,96 @@ test_file/*
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.publish-flow {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.publish-flow-item {
|
||||
--flow-color: #2563eb;
|
||||
--flow-bg: #eff6ff;
|
||||
position: relative;
|
||||
min-height: 86px;
|
||||
padding: 14px;
|
||||
overflow: hidden;
|
||||
background:
|
||||
linear-gradient(135deg, var(--flow-bg), #fff 72%);
|
||||
border: 1px solid rgb(15 23 42 / 7%);
|
||||
border-left: 4px solid var(--flow-color);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 20px rgb(15 23 42 / 5%);
|
||||
transition:
|
||||
box-shadow 0.18s ease,
|
||||
transform 0.18s ease;
|
||||
}
|
||||
|
||||
.publish-flow-item:nth-child(2) {
|
||||
--flow-color: #0891b2;
|
||||
--flow-bg: #ecfeff;
|
||||
}
|
||||
|
||||
.publish-flow-item:nth-child(3) {
|
||||
--flow-color: #16a34a;
|
||||
--flow-bg: #f0fdf4;
|
||||
}
|
||||
|
||||
.publish-flow-item:nth-child(4) {
|
||||
--flow-color: #d97706;
|
||||
--flow-bg: #fffbeb;
|
||||
}
|
||||
|
||||
.publish-flow-item::after {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 76px;
|
||||
height: 34px;
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
background:
|
||||
linear-gradient(135deg, transparent 0 44%, rgb(15 23 42 / 5%) 44% 52%, transparent 52% 100%);
|
||||
}
|
||||
|
||||
.publish-flow-item:hover {
|
||||
box-shadow: 0 14px 28px rgb(15 23 42 / 10%);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.publish-flow-item strong {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: var(--flow-color);
|
||||
font-size: 14px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.publish-flow-item span {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.upload-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin: 16px 0;
|
||||
padding: 14px;
|
||||
border: 1px dashed var(--el-border-color);
|
||||
background: #fbfdff;
|
||||
border: 1px dashed rgb(64 158 255 / 42%);
|
||||
border-radius: 8px;
|
||||
transition:
|
||||
background-color 0.18s ease,
|
||||
border-color 0.18s ease,
|
||||
box-shadow 0.18s ease;
|
||||
}
|
||||
|
||||
.upload-box:hover {
|
||||
background: #f2f8ff;
|
||||
border-color: var(--el-color-primary);
|
||||
box-shadow: 0 10px 24px rgb(64 158 255 / 12%);
|
||||
}
|
||||
|
||||
.publish-job-alert {
|
||||
@@ -1672,8 +2234,8 @@ test_file/*
|
||||
margin-top: 12px;
|
||||
padding: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
background: var(--el-fill-color-lighter);
|
||||
border: 1px solid var(--el-border-color-light);
|
||||
background: linear-gradient(135deg, #f8fbff, #fff);
|
||||
border: 1px solid rgb(64 158 255 / 18%);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
@@ -1702,11 +2264,11 @@ test_file/*
|
||||
}
|
||||
|
||||
.mt {
|
||||
margin-top: 16px;
|
||||
margin-top: 26px;
|
||||
}
|
||||
|
||||
.mt-sm {
|
||||
margin-top: 12px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.ml-xs {
|
||||
@@ -1741,7 +2303,9 @@ test_file/*
|
||||
overflow: auto;
|
||||
color: #d1e9ff;
|
||||
background: #101828;
|
||||
border: 1px solid rgb(209 233 255 / 12%);
|
||||
border-radius: 8px;
|
||||
box-shadow: inset 0 0 0 1px rgb(255 255 255 / 3%);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@@ -1751,7 +2315,8 @@ test_file/*
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.metric-grid,
|
||||
.form-grid {
|
||||
.form-grid,
|
||||
.publish-flow {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
@@ -1764,7 +2329,8 @@ test_file/*
|
||||
}
|
||||
|
||||
.metric-grid,
|
||||
.form-grid {
|
||||
.form-grid,
|
||||
.publish-flow {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -1772,4 +2338,40 @@ test_file/*
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes page-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(6px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes content-rise {
|
||||
from {
|
||||
opacity: 0.86;
|
||||
transform: translateY(4px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes heading-sheen {
|
||||
0%,
|
||||
58% {
|
||||
transform: translateX(-82%);
|
||||
}
|
||||
|
||||
78%,
|
||||
100% {
|
||||
transform: translateX(82%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user