锁定 Outpost 主路径主题,并优化地图编辑属性面板浅色对比。

统一 outpost-light/purple/blue 壳层 token,修复扩展字段与操作按钮在浅色下的可读性。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zhaowei.huang
2026-07-26 11:31:53 +08:00
co-authored by Cursor
parent 6986370e9d
commit 95c1a74122
18 changed files with 3790 additions and 1651 deletions
@@ -0,0 +1,22 @@
# Outpost 视觉规范(迷毂壳层)
> 权威决策见:`Migu2.0/docs/superpowers/specs/2026-07-25-migu-outpost-ui-lock-design.md`
> 参考:https://fairylandtech.amerc.ai
## 一句话
Outpost 壳层:浅主区 + 深色纹理侧栏 + 纸白卡片 + 柔阴影。
主路径精选三色:**浅色(默认)/ 紫色 / 蓝色**;旧深色霓虹主题保留在「高级 → 兼容主题」。
## 精选主题 id
| id | 名称 |
|---|---|
| `outpost-light` | 浅色(默认) |
| `outpost-purple` | 紫色 |
| `outpost-blue` | 蓝色 |
## 例外
- webVRender / 地图 3D 画布:保持深色视口。
- 状态色:继续用 `--mg-status-*`
@@ -1,11 +1,14 @@
<!doctype html>
<html lang="zh-CN">
<html lang="zh-CN" data-shell="outpost" data-theme="outpost-light">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>迷毂 · 智能调度平台</title>
<meta name="theme-color" content="#2e3f8a" />
<meta name="theme-color" content="#7543e8" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
</head>
<body>
<div id="app"></div>
@@ -291,14 +291,23 @@ defineExpose({ reload, enterFullscreen })
.workspace-3d-mask {
position: absolute; inset: 0; display: flex; flex-direction: column;
align-items: center; justify-content: center; gap: 10px;
/* 画布加载光晕固定深紫(不跟浅色壳白底 token,避免被冲成灰白) */
background:
radial-gradient(ellipse at center, rgba(var(--mg-primary-rgb), 0.35) 0%, rgba(var(--mg-bg-app-deep-rgb), 0.92) 80%),
rgba(var(--mg-bg-app-deep-rgb), 0.85);
radial-gradient(
ellipse at 50% 42%,
rgba(155, 124, 255, 0.42) 0%,
rgba(88, 48, 168, 0.72) 38%,
rgba(45, 27, 105, 0.92) 68%,
rgba(15, 4, 32, 0.98) 100%
);
color: rgba(232, 220, 255, 0.92);
font-size: 13px;
backdrop-filter: blur(10px);
pointer-events: none;
}
.workspace-3d-mask .muted { color: rgba(var(--mg-accent-rgb), 0.7); font-size: 12px; }
.workspace-3d-mask .el-icon { color: var(--mg-accent); filter: drop-shadow(0 0 10px rgba(var(--mg-accent-rgb), 0.6)); }
.workspace-3d-mask .muted { color: rgba(196, 181, 253, 0.78); font-size: 12px; }
.workspace-3d-mask .el-icon {
color: #c4b5fd;
filter: drop-shadow(0 0 12px rgba(155, 124, 255, 0.65));
}
</style>
@@ -0,0 +1,168 @@
<template>
<el-dialog
:model-value="open"
title="选择字段增加"
width="520px"
append-to-body
destroy-on-close
class="better-add-field-dialog"
@update:model-value="onOpenChange"
>
<p class="baf-hint">
目标{{ targetSummary }}选择场景已有字段再输入要批量写入的值对齐 Ctrl+I
</p>
<el-input
v-model="search"
clearable
placeholder="搜索字段名"
class="baf-search"
/>
<div v-loading="loadingKeys" class="baf-list">
<button
v-for="key in filteredKeys"
:key="key"
type="button"
class="baf-key"
:class="{ 'is-active': key === pickedKey }"
@click="pickedKey = key"
>
{{ key }}
</button>
<div v-if="!loadingKeys && filteredKeys.length === 0" class="baf-empty">
{{ keys.length === 0 ? '当前场景还没有任何自定义字段可选' : '无匹配字段' }}
</div>
</div>
<el-input
v-model="value"
:disabled="!pickedKey"
:placeholder="pickedKey ? `输入字段 ${pickedKey} 的值` : '先选择字段'"
class="baf-value"
@keydown.enter.prevent="confirm"
/>
<template #footer>
<el-button @click="close">取消</el-button>
<el-button type="primary" :loading="applying" :disabled="!pickedKey" @click="confirm">
写入选中对象
</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { mapEditApi } from '@/api/mapEdit'
const props = defineProps<{
open: boolean
targets: Array<{ kind: string; id: number }>
targetSummary: string
}>()
const emit = defineEmits<{
'update:open': [v: boolean]
applied: [payload: { key: string; value: string; affected: number }]
}>()
const keys = ref<string[]>([])
const loadingKeys = ref(false)
const search = ref('')
const pickedKey = ref('')
const value = ref('')
const applying = ref(false)
const filteredKeys = computed(() => {
const q = search.value.trim().toLowerCase()
if (!q) return keys.value
return keys.value.filter((k) => k.toLowerCase().includes(q))
})
watch(
() => props.open,
async (v) => {
if (!v) return
search.value = ''
pickedKey.value = ''
value.value = ''
loadingKeys.value = true
try {
const r = await mapEditApi.sceneFieldKeys()
keys.value = r.keys ?? []
} catch (err) {
keys.value = []
ElMessage.error(`加载字段列表失败:${(err as Error).message}`)
} finally {
loadingKeys.value = false
}
}
)
function onOpenChange(v: boolean) {
emit('update:open', v)
}
function close() {
emit('update:open', false)
}
async function confirm() {
if (!pickedKey.value || props.targets.length === 0) return
applying.value = true
try {
const r = await mapEditApi.batchSetField(pickedKey.value, value.value, props.targets)
ElMessage.success(`已对 ${r.affected}/${props.targets.length} 个对象设置 ${pickedKey.value}`)
emit('applied', { key: pickedKey.value, value: value.value, affected: r.affected })
close()
} catch (err) {
ElMessage.error(`写入失败:${(err as Error).message}`)
} finally {
applying.value = false
}
}
</script>
<style scoped>
.baf-hint {
margin: 0 0 12px;
font-size: 13px;
line-height: 1.45;
color: var(--el-text-color-secondary);
}
.baf-search { margin-bottom: 10px; width: 100%; }
.baf-list {
min-height: 180px;
max-height: 280px;
overflow: auto;
border: 1px solid var(--el-border-color);
border-radius: 8px;
padding: 6px;
display: flex;
flex-direction: column;
gap: 2px;
margin-bottom: 12px;
}
.baf-key {
appearance: none;
border: 0;
background: transparent;
text-align: left;
padding: 8px 10px;
border-radius: 6px;
font-size: 13px;
color: var(--el-text-color-primary);
cursor: pointer;
}
.baf-key:hover { background: var(--el-fill-color-light); }
.baf-key.is-active {
background: var(--el-color-primary-light-9);
color: var(--el-color-primary);
font-weight: 600;
}
.baf-empty {
padding: 28px 12px;
text-align: center;
font-size: 13px;
color: var(--el-text-color-secondary);
}
.baf-value { width: 100%; }
</style>
@@ -16,11 +16,8 @@
<div v-else class="object-pane">
<!-- Header cardID / 类型 / 名称 / 图层 / 删除按钮 -->
<section class="prop-card prop-card--header">
<div class="prop-card-row">
<div class="prop-id-block">
<span class="prop-id-tag">{{ selectionCount > 1 ? `多选 ${selectionCount}` : `#${primary?.id ?? '-'}` }}</span>
<span class="prop-type-tag">{{ primary?.typeName ?? '-' }}</span>
</div>
<header class="prop-card-header">
<span class="prop-card-title">基础属性</span>
<div class="prop-header-actions">
<el-tag
v-if="primary?.status"
@@ -42,11 +39,18 @@
:disabled="deleting"
@click="onDelete"
>
<span class="prop-delete-icon">{{ deleting ? '⟳' : '🗑' }}</span>
<span class="prop-delete-icon">{{ deleting ? '⟳' : '×' }}</span>
<span>删除</span>
</button>
</el-tooltip>
</div>
</header>
<div class="prop-card-row prop-id-row">
<div class="prop-id-block">
<span class="prop-id-tag">{{ selectionCount > 1 ? `多选 ${selectionCount}` : `#${primary?.id ?? '-'}` }}</span>
<span class="prop-type-tag">{{ primary?.typeName ?? '-' }}</span>
</div>
</div>
<div class="prop-card-row prop-name-row">
@@ -863,17 +867,17 @@ function onDelete() {
</script>
<style scoped>
/* Style 2 分组卡片 + Style 5 主题色相(--me-* / --mg-primary */
.edit-property-panel {
height: 100%;
width: 360px;
background: linear-gradient(180deg, rgba(28, 12, 56, 0.85) 0%, rgba(18, 6, 36, 0.95) 100%);
border-left: 1px solid rgba(255,255,255,0.10);
background: linear-gradient(180deg, var(--me-chrome-1) 0%, var(--me-chrome-2) 100%);
border-left: 1px solid var(--me-border);
padding: 0;
backdrop-filter: blur(10px);
overflow: hidden;
display: flex;
flex-direction: column;
color: rgba(232,215,245,0.92);
color: var(--me-text);
}
.prop-tabs {
height: 100%;
@@ -882,146 +886,205 @@ function onDelete() {
}
.prop-tabs :deep(.el-tabs__header) {
margin: 0;
padding: 0 8px;
border-bottom: 1px solid rgba(255,255,255,0.08);
}
.prop-tabs :deep(.el-tabs__nav-wrap)::after {
background: transparent;
padding: 6px 10px 0;
border-bottom: 1px solid var(--me-border);
background: var(--me-surface);
}
.prop-tabs :deep(.el-tabs__nav-wrap)::after { background: transparent; }
.prop-tabs :deep(.el-tabs__item) {
color: rgba(220, 200, 240, 0.7);
color: var(--me-text-muted);
font-weight: 500;
font-size: 13px;
height: 36px;
line-height: 36px;
height: 34px;
line-height: 34px;
padding: 0 14px !important;
}
.prop-tabs :deep(.el-tabs__item.is-active) {
color: #fff;
color: var(--mg-primary);
font-weight: 700;
}
.prop-tabs :deep(.el-tabs__active-bar) {
background: linear-gradient(90deg, rgba(170, 110, 250, 1) 0%, rgba(255, 110, 220, 1) 100%);
background: var(--mg-primary);
height: 2px;
border-radius: 2px;
}
.prop-tabs :deep(.el-tabs__content) {
flex: 1;
overflow: auto;
padding: 10px 12px 14px;
padding: 12px;
}
.empty-hint {
display: flex; align-items: center; gap: 6px;
padding: 56px 16px; text-align: center;
color: rgba(220, 200, 240, 0.55);
display: flex;
align-items: center;
gap: 8px;
padding: 48px 16px;
text-align: center;
color: var(--me-text-muted);
flex-direction: column;
}
.empty-icon { font-size: 28px; color: rgba(180, 130, 220, 0.45); }
.empty-title { font-size: 13px; color: rgba(232, 215, 245, 0.75); font-weight: 500; }
.empty-desc { font-size: 11.5px; color: rgba(220, 200, 240, 0.5); }
.empty-icon { font-size: 28px; color: rgba(var(--mg-primary-rgb), 0.45); }
.empty-title { font-size: 13px; color: var(--me-text); font-weight: 600; }
.empty-desc { font-size: 11.5px; color: var(--me-text-muted); max-width: 220px; line-height: 1.45; }
.loading-hint {
display: flex; align-items: center; gap: 8px;
padding: 32px 16px; text-align: center;
color: rgba(232,215,245,0.65); font-size: 12.5px;
display: flex;
align-items: center;
gap: 8px;
padding: 32px 16px;
justify-content: center;
color: var(--me-text-muted);
font-size: 12.5px;
}
.object-pane {
.object-pane,
.defaults-pane {
display: flex;
flex-direction: column;
gap: 10px;
gap: 12px;
}
/* 分组卡片:跟 --me-card-*Harbor 浅壳 / 紫蓝深壳共用) */
.prop-card {
background: rgba(255,255,255,0.04);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 8px;
padding: 10px 12px;
box-shadow: 0 1px 4px rgba(0,0,0,0.20);
position: relative;
background: var(--me-card-bg);
border: 1px solid var(--me-card-border);
border-radius: 12px;
padding: 12px 12px 10px 14px;
box-shadow: 0 4px 14px rgba(var(--mg-shadow-rgb, 22, 58, 74), 0.08);
}
.prop-card::before {
content: '';
position: absolute;
left: 0;
top: 10px;
bottom: 10px;
width: 3px;
border-radius: 0 3px 3px 0;
background: rgba(var(--mg-primary-rgb), 0.55);
}
.prop-card--header {
background: linear-gradient(140deg, rgba(170, 110, 250, 0.18) 0%, rgba(70, 30, 110, 0.10) 100%);
border-color: rgba(170, 110, 250, 0.28);
background: linear-gradient(
135deg,
rgba(var(--mg-primary-rgb), 0.12) 0%,
var(--me-card-bg) 55%
);
border-color: rgba(var(--mg-primary-rgb), 0.28);
box-shadow: 0 4px 16px rgba(var(--mg-primary-rgb), 0.08);
}
.prop-card--geom {
background: linear-gradient(140deg, rgba(80, 140, 220, 0.10) 0%, rgba(40, 70, 130, 0.06) 100%);
border-color: rgba(120, 170, 230, 0.22);
.prop-card--header::before {
background: var(--mg-primary);
top: 8px;
bottom: 8px;
width: 3px;
}
.prop-card--style {
background: linear-gradient(140deg, rgba(220, 110, 200, 0.10) 0%, rgba(150, 70, 130, 0.06) 100%);
border-color: rgba(230, 130, 200, 0.22);
.prop-card--geom,
.prop-card--style,
.prop-card--custom,
.prop-card--status,
.prop-card--actions {
background: var(--me-card-bg);
border-color: var(--me-card-border);
}
.prop-card--custom {
background: rgba(255, 255, 255, 0.03);
border-color: rgba(170, 110, 250, 0.18);
}
.prop-card--status { background: rgba(80, 140, 220, 0.06); border-color: rgba(120, 170, 230, 0.18); }
.prop-card--actions { background: rgba(140, 80, 200, 0.06); border-color: rgba(170, 110, 250, 0.18); }
.prop-card-header {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
min-height: 22px;
padding-bottom: 8px;
border-bottom: 1px solid var(--me-card-border);
}
.prop-card-title {
font-size: 11.5px;
font-weight: 600;
letter-spacing: 1px;
color: rgba(220, 180, 250, 0.85);
font-size: 11px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--me-text);
}
.prop-card-meta {
font-size: 10.5px;
color: rgba(200, 180, 220, 0.5);
margin-left: auto;
font-size: 11px;
font-family: var(--mg-font-mono, ui-monospace, Menlo, Consolas, monospace);
font-variant-numeric: tabular-nums;
color: var(--me-text-muted);
background: var(--me-inset);
border: 1px solid var(--me-card-border);
border-radius: 999px;
padding: 1px 8px;
}
.prop-card-flag {
display: inline-flex; align-items: center; gap: 4px;
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 10.5px;
color: rgba(180, 200, 240, 0.6);
padding: 2px 7px;
border-radius: 10px;
background: rgba(120, 160, 220, 0.10);
transition: background .25s ease, color .25s ease, box-shadow .25s ease;
color: var(--me-text-muted);
padding: 2px 8px;
border-radius: 999px;
background: var(--me-surface);
border: 1px solid var(--me-card-border);
margin-left: auto;
}
.prop-card-flag.is-synced {
background: linear-gradient(120deg, rgba(80, 200, 130, 0.32), rgba(120, 220, 200, 0.32));
color: rgba(220, 255, 230, 0.95);
box-shadow: 0 0 8px rgba(80, 200, 130, 0.45);
background: rgba(34, 197, 94, 0.12);
border-color: rgba(34, 197, 94, 0.35);
color: #15803d;
box-shadow: none;
}
.prop-card-flag-dot {
width: 6px; height: 6px; border-radius: 50%;
background: rgba(180, 200, 240, 0.6);
transition: background .25s ease, box-shadow .25s ease;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--me-text-muted);
}
.prop-card-flag.is-synced .prop-card-flag-dot {
background: #6ce7a8;
box-shadow: 0 0 6px rgba(108, 231, 168, 0.85);
background: #4ade80;
box-shadow: none;
}
.prop-card-row {
display: flex; align-items: center; justify-content: space-between;
margin-bottom: 6px;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
gap: 8px;
}
.prop-card-row:last-child { margin-bottom: 0; }
.prop-id-row { margin-bottom: 10px; }
.prop-id-block { display: flex; align-items: center; gap: 6px; }
.prop-id-block {
display: flex;
align-items: center;
gap: 8px;
min-width: 0;
flex-wrap: wrap;
}
.prop-id-tag {
background: rgba(170, 110, 250, 0.30);
background: var(--mg-primary);
color: #fff;
padding: 2px 8px;
font-size: 11.5px;
font-weight: 600;
border-radius: 4px;
letter-spacing: 0.5px;
padding: 3px 9px;
font-size: 12px;
font-weight: 700;
border-radius: 6px;
font-family: var(--mg-font-mono, ui-monospace, Menlo, Consolas, monospace);
font-variant-numeric: tabular-nums;
}
.prop-type-tag {
color: rgba(232, 215, 245, 0.75);
font-size: 11.5px;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
color: var(--me-text-muted);
font-size: 12px;
font-family: var(--mg-font-mono, ui-monospace, Menlo, Consolas, monospace);
background: var(--me-inset);
border: 1px solid var(--me-card-border);
border-radius: 6px;
padding: 2px 8px;
}
.prop-status-tag { font-weight: 500; }
.prop-status-tag { font-weight: 600; }
.prop-header-actions {
display: flex;
align-items: center;
gap: 6px;
margin-left: auto;
}
.prop-delete-btn {
@@ -1030,31 +1093,31 @@ function onDelete() {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 9px;
border-radius: 5px;
font-size: 11.5px;
font-weight: 500;
letter-spacing: 0.4px;
color: rgba(255, 200, 210, 0.95);
background: linear-gradient(135deg, rgba(255, 90, 120, 0.18) 0%, rgba(220, 60, 110, 0.22) 100%);
border: 1px solid rgba(255, 110, 140, 0.45);
transition: background .14s ease, border-color .14s ease, color .14s ease, box-shadow .14s ease, transform .12s ease;
padding: 4px 10px;
border-radius: 6px;
font-size: 12px;
font-weight: 550;
color: #fecaca;
background: rgba(239, 68, 68, 0.12);
border: 1px solid rgba(239, 68, 68, 0.35);
transition: background .12s ease, border-color .12s ease;
}
.prop-delete-btn:hover:not(:disabled) {
background: linear-gradient(135deg, rgba(255, 90, 120, 0.55) 0%, rgba(220, 60, 110, 0.65) 100%);
border-color: rgba(255, 140, 170, 0.85);
background: rgba(239, 68, 68, 0.22);
border-color: rgba(239, 68, 68, 0.55);
color: #fff;
box-shadow: 0 3px 10px rgba(220, 70, 110, 0.45);
box-shadow: none;
}
.prop-delete-btn:active:not(:disabled) { transform: scale(0.96); }
.prop-delete-btn:disabled { opacity: 0.55; cursor: progress; }
.prop-delete-icon { font-size: 12px; line-height: 1; }
.prop-name-row, .prop-layer-row, .prop-direction-row { gap: 8px; }
.prop-name-row,
.prop-layer-row,
.prop-direction-row { gap: 10px; }
.prop-name-label {
width: 36px;
font-size: 11.5px;
color: rgba(200, 180, 220, 0.7);
font-size: 12px;
color: var(--me-text-muted);
flex: none;
}
.prop-layer-select { flex: 1; }
@@ -1062,15 +1125,15 @@ function onDelete() {
.prop-grid {
display: grid;
grid-template-columns: 1fr 1fr;
column-gap: 8px;
row-gap: 8px;
column-gap: 10px;
row-gap: 10px;
}
.prop-grid-item { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
.prop-grid-item { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.prop-grid-item--wide { grid-column: 1 / -1; }
.prop-field-label {
font-size: 11px;
color: rgba(200, 180, 230, 0.78);
letter-spacing: 0.3px;
color: var(--me-text-muted);
letter-spacing: 0.2px;
display: flex;
justify-content: space-between;
align-items: baseline;
@@ -1081,155 +1144,230 @@ function onDelete() {
}
.prop-field-unit {
font-size: 10px;
color: rgba(170, 150, 200, 0.55);
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
color: var(--me-text-muted);
font-family: var(--mg-font-mono, ui-monospace, Menlo, Consolas, monospace);
flex: none;
}
.prop-field-num :deep(.el-input-number) { width: 100%; }
.prop-field-num :deep(.el-input__wrapper) {
background: rgba(255,255,255,0.06);
box-shadow: inset 0 0 0 1px rgba(170, 110, 250, 0.18);
.prop-field-num :deep(.el-input__wrapper),
.prop-card :deep(.el-input__wrapper),
.prop-card :deep(.el-select__wrapper),
.prop-card :deep(.el-textarea__inner) {
background: var(--me-inset) !important;
box-shadow: inset 0 0 0 1px var(--me-card-border) !important;
}
.prop-field-row { display: flex; align-items: center; gap: 4px; }
.prop-field-num :deep(.el-input__inner),
.prop-card :deep(.el-input__inner),
.prop-card :deep(.el-textarea__inner),
.prop-card :deep(.el-select__selected-item),
.prop-card :deep(.el-select__placeholder) {
color: var(--me-text) !important;
}
.prop-field-num :deep(.el-input__wrapper:hover),
.prop-card :deep(.el-input__wrapper:hover),
.prop-card :deep(.el-select__wrapper:hover) {
box-shadow: inset 0 0 0 1px rgba(var(--mg-primary-rgb), 0.55) !important;
}
.prop-field-num :deep(.el-input__wrapper.is-focus),
.prop-card :deep(.el-input__wrapper.is-focus),
.prop-card :deep(.el-select__wrapper.is-focused) {
box-shadow:
inset 0 0 0 1px var(--mg-primary),
0 0 0 3px rgba(var(--mg-primary-rgb), 0.16) !important;
}
.prop-field-row { display: flex; align-items: center; gap: 6px; }
.prop-field-icon-btn {
appearance: none; border: 0; cursor: pointer;
width: 22px; height: 22px;
border-radius: 4px;
background: rgba(255, 100, 130, 0.12);
color: rgba(255, 180, 200, 0.85);
font-size: 14px; line-height: 1;
display: inline-flex; align-items: center; justify-content: center;
appearance: none;
border: 1px solid rgba(239, 68, 68, 0.28);
cursor: pointer;
width: 28px;
height: 28px;
border-radius: 7px;
background: rgba(239, 68, 68, 0.08);
color: #dc2626;
font-size: 15px;
line-height: 1;
display: inline-flex;
align-items: center;
justify-content: center;
flex: none;
transition: background .14s, color .14s;
transition: background .12s ease, border-color .12s ease, color .12s ease;
}
.prop-field-icon-btn:hover {
background: rgba(255, 100, 130, 0.32);
color: #fff;
background: rgba(239, 68, 68, 0.16);
border-color: rgba(239, 68, 68, 0.45);
color: #b91c1c;
}
.prop-empty-row {
font-size: 11.5px;
color: rgba(200, 180, 220, 0.4);
font-size: 12px;
color: var(--me-text-muted);
text-align: center;
padding: 8px 0;
padding: 14px 8px;
border-radius: 8px;
background: var(--me-inset);
border: 1px dashed var(--me-card-border);
}
.prop-add-field-btn {
appearance: none; border: 0; cursor: pointer;
appearance: none;
cursor: pointer;
width: 100%;
margin-top: 8px;
padding: 6px 10px;
border-radius: 6px;
background: rgba(170, 110, 250, 0.14);
border: 1px dashed rgba(170, 110, 250, 0.40);
color: rgba(220, 180, 250, 0.92);
margin-top: 10px;
padding: 8px 10px;
border-radius: 8px;
background: var(--me-surface);
border: 1px dashed rgba(var(--mg-primary-rgb), 0.42);
color: var(--mg-primary);
font-size: 12px;
font-weight: 500;
transition: background .14s ease, border-color .14s ease, color .14s ease;
font-weight: 600;
transition: background .12s ease, border-color .12s ease, color .12s ease;
}
.prop-add-field-btn:hover {
background: rgba(170, 110, 250, 0.26);
border-color: rgba(170, 110, 250, 0.65);
color: #fff;
.prop-add-field-btn:hover:not(:disabled) {
background: var(--me-hover);
border-color: rgba(var(--mg-primary-rgb), 0.7);
border-style: solid;
color: var(--mg-primary);
}
.prop-add-field-btn:disabled { opacity: 0.45; cursor: not-allowed; }
.custom-field-list {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 10px;
gap: 8px;
}
.custom-field-item {
display: flex;
flex-direction: column;
gap: 3px;
gap: 5px;
min-width: 0;
padding: 9px 10px;
border-radius: 9px;
background: var(--me-inset);
border: 1px solid var(--me-card-border);
}
.custom-field-key {
margin: 0;
font-family: var(--mg-font-mono, ui-monospace, monospace);
font-size: 11px;
font-weight: 500;
color: rgba(200, 180, 230, 0.88);
font-weight: 650;
letter-spacing: 0.02em;
color: var(--me-text);
white-space: normal;
word-break: break-all;
line-height: 1.35;
cursor: default;
}
.custom-field-input {
flex: 1;
min-width: 0;
width: 100%;
}
.custom-field-input { flex: 1; min-width: 0; width: 100%; }
.custom-field-input :deep(.el-input__wrapper) {
background: rgba(255, 255, 255, 0.06);
box-shadow: inset 0 0 0 1px rgba(170, 110, 250, 0.18);
background: var(--me-card-bg) !important;
box-shadow: inset 0 0 0 1px var(--me-card-border) !important;
min-height: 30px;
}
.custom-field-input :deep(.el-input__inner) {
color: var(--me-text) !important;
font-weight: 550;
font-variant-numeric: tabular-nums;
}
.custom-field-input :deep(.el-input__wrapper:hover) {
box-shadow: inset 0 0 0 1px rgba(170, 110, 250, 0.32);
box-shadow: inset 0 0 0 1px rgba(var(--mg-primary-rgb), 0.45) !important;
}
.custom-field-input :deep(.el-input__wrapper.is-focus) {
box-shadow: inset 0 0 0 1px rgba(190, 130, 255, 0.55);
box-shadow:
inset 0 0 0 1px var(--mg-primary),
0 0 0 3px rgba(var(--mg-primary-rgb), 0.14) !important;
}
.add-field-select { width: 100%; }
.add-field-select {
width: 100%;
/* 运行状态:胶囊行 */
.prop-status-list {
display: flex;
flex-direction: column;
gap: 5px;
max-height: 280px;
overflow: auto;
padding-right: 2px;
}
.prop-status-list { display: flex; flex-direction: column; gap: 4px; }
.prop-status-item {
display: flex; justify-content: space-between; align-items: center;
padding: 5px 8px;
border-radius: 4px;
background: rgba(255,255,255,0.03);
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
padding: 7px 10px;
border-radius: 8px;
background: var(--me-inset);
border: 1px solid var(--me-card-border);
font-size: 11.5px;
}
.prop-status-key { color: rgba(170, 200, 240, 0.85); font-weight: 500; }
.prop-status-value {
color: rgba(232, 245, 255, 0.95);
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
max-width: 60%;
.prop-status-item:nth-child(even) {
background: rgba(var(--mg-primary-rgb), 0.08);
}
.prop-status-key {
color: var(--me-text-muted);
font-weight: 600;
flex: none;
max-width: 46%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.prop-status-value {
color: var(--me-text);
font-family: var(--mg-font-mono, ui-monospace, Menlo, Consolas, monospace);
font-variant-numeric: tabular-nums;
max-width: 54%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: right;
}
.prop-actions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 6px;
gap: 8px;
}
.prop-action-btn {
appearance: none; border: 0; cursor: pointer;
padding: 7px 10px;
border-radius: 7px;
background: linear-gradient(135deg, rgba(170, 110, 250, 0.20) 0%, rgba(120, 70, 220, 0.20) 100%);
border: 1px solid rgba(170, 110, 250, 0.35);
color: rgba(232, 215, 245, 0.95);
appearance: none;
cursor: pointer;
padding: 9px 11px;
border-radius: 8px;
background: var(--mg-primary);
border: 1px solid var(--mg-primary);
color: #fff;
font-size: 12px;
font-weight: 500;
display: flex; align-items: center; justify-content: flex-start; gap: 6px;
transition: background .14s ease, border-color .14s ease, transform .12s ease, box-shadow .14s ease;
font-weight: 600;
display: flex;
align-items: center;
justify-content: flex-start;
gap: 6px;
text-align: left;
transition: background .12s ease, border-color .12s ease, box-shadow .12s ease, transform .12s ease;
}
.prop-action-btn:nth-child(2n) {
background: var(--me-card-bg);
border-color: rgba(var(--mg-primary-rgb), 0.38);
color: var(--mg-primary);
}
.prop-action-btn:nth-child(2n) .prop-action-icon {
color: var(--mg-primary);
}
.prop-action-btn:hover:not(:disabled) {
background: linear-gradient(135deg, rgba(170, 110, 250, 0.62) 0%, rgba(255, 110, 220, 0.55) 100%);
border-color: rgba(255, 130, 220, 0.7);
background: var(--mg-primary-hover, var(--mg-primary));
border-color: var(--mg-primary-hover, var(--mg-primary));
color: #fff;
box-shadow: 0 4px 12px rgba(180, 90, 220, 0.40);
box-shadow: 0 4px 12px var(--me-active-glow);
transform: translateY(-1px);
}
.prop-action-btn:active:not(:disabled) { transform: scale(0.985); }
.prop-action-btn:disabled { opacity: 0.6; cursor: progress; }
.prop-action-icon {
font-size: 9px;
color: rgba(255, 200, 250, 0.85);
flex: none;
.prop-action-btn:nth-child(2n):hover:not(:disabled) {
background: rgba(var(--mg-primary-rgb), 0.1);
border-color: var(--mg-primary);
color: var(--mg-primary);
box-shadow: 0 2px 8px var(--me-active-glow);
}
.prop-action-btn:nth-child(2n):hover:not(:disabled) .prop-action-icon {
color: var(--mg-primary);
}
.prop-action-btn:disabled { opacity: 0.55; cursor: progress; transform: none; }
.prop-action-icon { font-size: 9px; color: rgba(255, 255, 255, 0.92); flex: none; }
.prop-action-spinner {
display: inline-block;
animation: prop-spin .8s linear infinite;
@@ -1237,56 +1375,63 @@ function onDelete() {
}
.prop-action-label {
flex: 1;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
@keyframes prop-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
to { transform: rotate(360deg); }
}
.defaults-pane { display: flex; flex-direction: column; gap: 10px; }
.defaults-alert { margin-bottom: 4px; }
.defaults-empty { padding: 40px 12px; }
.defaults-alert { margin-bottom: 0; }
.defaults-empty { padding: 32px 12px; }
.defaults-form .el-button { margin-top: 8px; }
.defaults-form :deep(.el-form-item) { margin-bottom: 10px; }
/* 图层 Tab:白底数据块(深侧栏上的可读「岛」),各主题下表头/单元格均用深字 */
.defaults-form :deep(.el-form-item__label) { color: var(--me-text-muted); }
.layer-table {
margin: 6px 0;
border-radius: 8px;
margin: 0;
border-radius: 12px;
overflow: hidden;
background: #fff;
box-shadow: 0 0 0 1px rgba(124, 58, 237, 0.14);
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.10);
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);
}
.layer-table :deep(.el-table__inner-wrapper) {
background: #fff;
.layer-table :deep(.el-table__inner-wrapper),
.layer-table :deep(.el-table),
.layer-table :deep(.el-table tr) {
background: transparent !important;
}
.layer-table :deep(th.el-table__cell) {
background: #f5f3ff !important;
color: #2d1b69 !important;
font-weight: 600;
background: rgba(var(--mg-primary-rgb), 0.16) !important;
color: var(--me-text) !important;
font-weight: 650;
border-color: rgba(255, 255, 255, 0.08) !important;
}
.layer-table :deep(td.el-table__cell) {
background: #fff !important;
color: #1a0f3d !important;
font-weight: 500;
}
.layer-table :deep(.cell) {
color: #1a0f3d !important;
background: transparent !important;
color: var(--me-text) !important;
border-color: rgba(255, 255, 255, 0.06) !important;
}
.layer-table :deep(.cell) { color: inherit !important; }
.layer-table :deep(tr:hover > td.el-table__cell) {
background: #ede9fe !important;
background: rgba(var(--mg-primary-rgb), 0.12) !important;
}
.new-layer { margin-top: 10px; }
.new-layer :deep(.el-input__wrapper) {
background: rgba(0, 0, 0, 0.22) !important;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.10) !important;
}
.new-layer { margin-top: 8px; }
.new-layer :deep(.el-input__inner) {
color: #1a0f3d;
color: var(--me-text);
font-weight: 500;
}
.new-layer :deep(.el-input__inner::placeholder) {
color: #7a6b9a;
color: var(--me-text-muted);
}
.new-layer :deep(.el-input-group__append .el-button) {
color: #5b21b6;
color: var(--mg-accent);
font-weight: 600;
}
</style>
@@ -43,15 +43,15 @@ function formatXY(v: number) {
align-items: center;
gap: 16px;
padding: 4px 14px;
background: rgba(20, 8, 40, 0.85);
border-top: 1px solid rgba(255,255,255,0.08);
color: rgba(232,215,245,0.85);
background: var(--me-chrome-2);
border-top: 1px solid var(--me-border);
color: var(--me-text-muted);
font-size: 12px;
font-family: ui-monospace, Menlo, Consolas, monospace;
font-family: var(--mg-font-mono, ui-monospace, Menlo, Consolas, monospace);
height: 28px;
}
.spacer { flex: 1; }
.seg b { color: var(--mg-accent, #c4a4ff); }
.seg b { color: var(--mg-accent); }
.seg .on { color: #67c23a; }
.seg .off { color: #a0a0a0; }
.seg.ok { color: #67c23a; }
@@ -99,7 +99,6 @@ const groups: ToolGroup[] = [
{ id: 'transform.move', label: '移动', glyph: '✥' },
{ id: 'transform.rotate', label: '旋转', glyph: '⟳' },
{ id: 'transform.scale', label: '缩放', glyph: '⤡' },
{ id: 'transform.duplicate', label: '复制副本', glyph: '⎘', tip: 'Ctrl+D' },
{ id: 'transform.copy', label: '复制', glyph: '📋', tip: 'Ctrl+C' },
{ id: 'transform.paste', label: '粘贴', glyph: '📥', tip: 'Ctrl+V' },
{ id: 'transform.delete', label: '删除', glyph: '🗑', tip: 'Delete' },
@@ -152,12 +151,12 @@ function onClick(id: EditToolId) { emit('select-tool', id) }
.edit-tool-rail {
height: 100%;
width: 170px;
background: linear-gradient(180deg, rgba(28, 12, 56, 0.85) 0%, rgba(18, 6, 36, 0.95) 100%);
border-right: 1px solid rgba(255,255,255,0.10);
background: linear-gradient(180deg, var(--me-chrome-1) 0%, var(--me-chrome-2) 100%);
border-right: 1px solid var(--me-border);
padding: 4px 0 12px;
backdrop-filter: blur(10px);
display: flex; flex-direction: column;
box-shadow: inset -1px 0 0 rgba(255,255,255,0.04);
box-shadow: inset -1px 0 0 var(--me-card-border, transparent);
}
.rail-scroll { height: 100%; }
.rail-group {
@@ -168,12 +167,12 @@ function onClick(id: EditToolId) { emit('select-tool', id) }
display: flex; align-items: center; gap: 6px;
font-size: 10px;
letter-spacing: 2px;
color: rgba(190, 160, 230, 0.7);
color: var(--me-text-muted);
text-transform: uppercase;
margin: 4px 0 6px;
font-weight: 600;
}
.rail-group-bar { flex: 1; height: 1px; background: rgba(190, 160, 230, 0.2); }
.rail-group-bar { flex: 1; height: 1px; background: rgba(var(--mg-primary-rgb), 0.18); }
.rail-group-list {
display: flex; flex-direction: column;
gap: 2px;
@@ -181,7 +180,7 @@ function onClick(id: EditToolId) { emit('select-tool', id) }
.rail-divider {
width: 100%;
height: 1px;
background: linear-gradient(90deg, rgba(255,255,255,0.0) 0%, rgba(255,255,255,0.08) 50%, rgba(255,255,255,0.0) 100%);
background: linear-gradient(90deg, transparent 0%, var(--me-border) 50%, transparent 100%);
margin: 6px 0 2px;
}
.rail-btn {
@@ -192,8 +191,8 @@ function onClick(id: EditToolId) { emit('select-tool', id) }
min-height: 32px;
padding: 0 8px;
border-radius: 6px;
background: rgba(255,255,255,0.04);
color: rgba(232,215,245,0.92);
background: var(--me-surface);
color: var(--me-text);
display: flex; align-items: center;
gap: 8px;
font-size: 12.5px;
@@ -202,16 +201,16 @@ function onClick(id: EditToolId) { emit('select-tool', id) }
transition: background .14s ease, color .14s ease, box-shadow .14s ease, transform .12s ease;
}
.rail-btn:hover {
background: rgba(150, 90, 230, 0.20);
color: #fff;
background: var(--me-hover);
color: var(--mg-primary);
}
.rail-btn:active {
transform: scale(0.985);
}
.rail-btn.rail-btn--active {
background: linear-gradient(120deg, rgba(170, 110, 250, 0.92) 0%, rgba(120, 70, 220, 0.92) 100%);
background: var(--mg-primary);
color: #fff;
box-shadow: 0 3px 10px rgba(140, 80, 230, 0.45), inset 0 0 0 1px rgba(255,255,255,0.18);
box-shadow: 0 3px 10px var(--me-active-glow), inset 0 0 0 1px rgba(255,255,255,0.18);
}
.rail-btn-glyph {
width: 20px;
@@ -236,21 +235,21 @@ function onClick(id: EditToolId) { emit('select-tool', id) }
position: absolute;
left: 8px; right: 8px; top: 0;
height: 1px;
background: linear-gradient(90deg, rgba(255,255,255,0.0) 0%, rgba(220, 140, 230, 0.35) 50%, rgba(255,255,255,0.0) 100%);
background: linear-gradient(90deg, transparent 0%, rgba(var(--mg-accent-rgb), 0.4) 50%, transparent 100%);
}
.rail-btn--ai {
background: linear-gradient(135deg, rgba(120, 70, 220, 0.55) 0%, rgba(255, 90, 200, 0.55) 100%);
background: var(--mg-primary);
color: #fff;
font-weight: 600;
box-shadow: 0 2px 10px rgba(220, 90, 220, 0.30), inset 0 0 0 1px rgba(255,255,255,0.20);
box-shadow: 0 2px 10px var(--me-active-glow), inset 0 0 0 1px rgba(255,255,255,0.20);
}
.rail-btn--ai:hover {
background: linear-gradient(135deg, rgba(150, 90, 240, 0.85) 0%, rgba(255, 110, 220, 0.85) 100%);
background: var(--mg-primary-hover);
color: #fff;
box-shadow: 0 4px 14px rgba(230, 110, 230, 0.55), inset 0 0 0 1px rgba(255,255,255,0.30);
box-shadow: 0 4px 14px var(--me-active-glow), inset 0 0 0 1px rgba(255,255,255,0.30);
}
.rail-btn--ai .rail-btn-glyph {
font-size: 16px;
text-shadow: 0 0 8px rgba(255, 200, 250, 0.6);
text-shadow: 0 0 8px rgba(var(--mg-accent-rgb), 0.55);
}
</style>
@@ -148,16 +148,16 @@ function mark(on: boolean): string {
align-items: center;
gap: 4px;
padding: 8px 14px;
background: linear-gradient(180deg, rgba(50, 22, 88, 0.95) 0%, rgba(34, 14, 64, 0.95) 100%);
border-bottom: 1px solid rgba(255,255,255,0.12);
background: linear-gradient(180deg, var(--me-chrome-1) 0%, var(--me-chrome-2) 100%);
border-bottom: 1px solid var(--me-border);
backdrop-filter: blur(10px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
box-shadow: 0 2px 8px rgba(var(--mg-shadow-rgb, 22, 58, 74), 0.08);
}
.spacer { flex: 1; }
.el-divider--vertical { margin: 0 6px; background: rgba(255,255,255,0.15); }
.el-divider--vertical { margin: 0 6px; background: var(--me-border); }
.topbar-btn {
color: #fdf6ff !important;
color: var(--me-text) !important;
font-weight: 600;
font-size: 13.5px;
padding: 6px 12px;
@@ -166,22 +166,23 @@ function mark(on: boolean): string {
transition: background .15s ease, color .15s ease;
}
.topbar-btn:hover {
color: #fff !important;
background: rgba(170, 110, 250, 0.28) !important;
color: var(--mg-primary) !important;
background: var(--me-hover) !important;
}
.topbar-btn:focus {
background: rgba(170, 110, 250, 0.30) !important;
background: var(--me-hover) !important;
}
.topbar-icon-btn {
color: rgba(253, 246, 255, 0.92) !important;
background: rgba(255,255,255,0.05) !important;
border: 1px solid rgba(255,255,255,0.15) !important;
color: var(--me-text) !important;
background: var(--me-surface) !important;
border: 1px solid var(--me-border) !important;
font-size: 15px;
}
.topbar-icon-btn:hover:not(.is-disabled) {
background: rgba(170, 110, 250, 0.30) !important;
border-color: rgba(170, 110, 250, 0.55) !important;
background: var(--me-hover) !important;
border-color: rgba(var(--mg-primary-rgb), 0.55) !important;
color: var(--mg-primary) !important;
}
.topbar-icon-btn.is-disabled {
opacity: 0.4;
@@ -194,11 +195,19 @@ function mark(on: boolean): string {
height: auto;
border-radius: 6px;
margin-right: 4px;
--el-button-bg-color: var(--mg-primary) !important;
--el-button-border-color: var(--mg-primary) !important;
--el-button-hover-bg-color: var(--mg-primary-hover) !important;
--el-button-hover-border-color: var(--mg-primary-hover) !important;
--el-button-active-bg-color: var(--mg-primary-active) !important;
--el-button-active-border-color: var(--mg-primary-active) !important;
--el-button-text-color: #fff !important;
--el-button-hover-text-color: #fff !important;
}
.topbar-filter-menu :deep(.topbar-filter-header) {
font-size: 11px;
color: rgba(232, 215, 245, 0.55);
color: var(--me-text-muted);
cursor: default;
pointer-events: none;
padding-top: 6px;
@@ -53,7 +53,7 @@
<div class="aside-footer">
<span class="aside-footer-badge">v1.6</span>
<span v-if="!ui.sidebarCollapsed">{{ ui.activeTheme.name }} · {{ ui.activeTheme.preview }}</span>
<span v-if="!ui.sidebarCollapsed">{{ ui.activeTheme.name }}</span>
</div>
</el-aside>
@@ -82,6 +82,15 @@
<el-dropdown-menu>
<el-dropdown-item v-if="auth.scope === 'Platform'" command="wizard">配置向导</el-dropdown-item>
<el-dropdown-item command="status">服务状态</el-dropdown-item>
<el-dropdown-item divided disabled class="legacy-theme-label">高级 · 兼容主题</el-dropdown-item>
<el-dropdown-item
v-for="t in ui.legacyThemes"
:key="t.id"
:command="`theme:${t.id}`"
:class="{ 'is-legacy-active': t.id === ui.themeId }">
<span class="legacy-swatch" :style="{ background: t.preview }" />
{{ t.name }}
</el-dropdown-item>
<el-dropdown-item command="logout" divided>退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
@@ -97,10 +106,13 @@
</el-main>
<el-footer class="footer">
<span>{{ scopeLabel }} · webVRender :{{ vrPort }} · API {{ apiBase }}</span>
<span>{{ scopeLabel }} · Projection :{{ slPort }} · API {{ apiBase }}</span>
</el-footer>
</el-container>
</el-container>
<!-- 全局 AI 助手右侧抽屉 + 右下角悬浮唤起所有界面可呼出后端要求 Platform 权限故按 scope 显示 -->
<AiAssistantDrawer v-if="auth.scope === 'Platform'" />
</div>
</template>
@@ -113,13 +125,14 @@ import { useAuthStore } from '@/stores/auth'
import { useUiStore } from '@/stores/ui'
import ScopeSwitcher from '@/components/ScopeSwitcher.vue'
import ThemeSwitcher from '@/components/ThemeSwitcher.vue'
import AiAssistantDrawer from '@/components/assistant/AiAssistantDrawer.vue'
const route = useRoute()
const router = useRouter()
const auth = useAuthStore()
const ui = useUiStore()
const vrPort = computed(() => (import.meta.env.VITE_VRENDER_HOST as string | undefined)?.split(':')[1] ?? '8223')
const slPort = computed(() => (import.meta.env.VITE_SL_PORT as string | undefined) ?? '8222')
const apiBase = computed(() => (import.meta.env.VITE_API_BASE as string | undefined) ?? '/api')
const initial = computed(() => (auth.user?.displayName ?? auth.user?.username ?? '?').slice(0, 1).toUpperCase())
@@ -171,6 +184,10 @@ const activePath = computed(() => {
const currentTitle = computed(() => (route.meta.title as string | undefined) ?? '')
function onUserCommand(cmd: string) {
if (cmd.startsWith('theme:')) {
ui.setThemeId(cmd.slice('theme:'.length))
return
}
if (cmd === 'logout') {
auth.logout()
router.push('/login')
@@ -187,6 +204,7 @@ function onUserCommand(cmd: string) {
.app-shell {
position: relative;
height: 100vh;
height: 100dvh;
overflow: hidden;
}
@@ -236,6 +254,7 @@ function onUserCommand(cmd: string) {
position: relative;
z-index: 1;
height: 100vh;
height: 100dvh;
}
/* ─────────────── Sidebar ─────────────── */
@@ -549,3 +568,25 @@ function onUserCommand(cmd: string) {
flex-direction: column;
}
</style>
<style>
/* 用户菜单「兼容主题」项(teleport 到 body */
.legacy-theme-label {
font-size: 11px !important;
opacity: 0.65;
cursor: default !important;
}
.legacy-swatch {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 8px;
vertical-align: middle;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.12);
}
.el-dropdown-menu__item.is-legacy-active {
color: var(--mg-primary) !important;
font-weight: 600;
}
</style>
@@ -2,8 +2,11 @@ import { defineStore } from 'pinia'
import {
DEFAULT_THEME_ID,
LEGACY_THEMES,
PRIMARY_THEMES,
applyThemeVars,
findTheme,
resolveThemeId,
THEMES,
type ThemePreset
} from '@/styles/themes'
@@ -28,6 +31,9 @@ interface UiState {
}
const STORAGE_KEY = 'simple.ui.state'
/** Outpost 壳层上线:旧本地主题只换了 id、观感几乎不变 → 升 schema 强制一次默认浅色 */
const THEME_SCHEMA_KEY = 'simple.ui.themeSchema'
const THEME_SCHEMA_VERSION = 2
const DEFAULT_STATE: UiState = {
sidebarCollapsed: false,
@@ -39,14 +45,33 @@ const DEFAULT_STATE: UiState = {
function loadInitial(): UiState {
try {
const raw = localStorage.getItem(STORAGE_KEY)
if (!raw) return { ...DEFAULT_STATE }
if (!raw) {
localStorage.setItem(THEME_SCHEMA_KEY, String(THEME_SCHEMA_VERSION))
return { ...DEFAULT_STATE }
}
const parsed = JSON.parse(raw) as Partial<UiState>
return {
let themeId = resolveThemeId(parsed.themeId ?? DEFAULT_STATE.themeId)
const schema = Number(localStorage.getItem(THEME_SCHEMA_KEY) || '0')
const migrated = schema < THEME_SCHEMA_VERSION
if (migrated) {
// ponytail: 一次强制 outpost-light,兼容主题仍可从高级菜单切回
themeId = DEFAULT_THEME_ID
localStorage.setItem(THEME_SCHEMA_KEY, String(THEME_SCHEMA_VERSION))
}
const state: UiState = {
sidebarCollapsed: parsed.sidebarCollapsed ?? DEFAULT_STATE.sidebarCollapsed,
theme: parsed.theme === 'dark' ? 'dark' : 'light',
themeId: parsed.themeId ?? DEFAULT_STATE.themeId,
themeId,
themeOverrides: parsed.themeOverrides ?? {}
}
if (migrated) {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state))
} catch {
/* ignore */
}
}
return state
} catch (err) {
console.warn('[ui.store] localStorage 中的 UI 状态解析失败,已回退默认:', err)
return { ...DEFAULT_STATE }
@@ -60,8 +85,16 @@ export const useUiStore = defineStore('ui', {
activeTheme(state): ThemePreset {
return mergeThemePreset(findTheme(state.themeId), state.themeOverrides[state.themeId])
},
/** 所有可选主题列表 */
/** 顶栏精选主题(浅色 / 紫色 / 蓝色) */
availableThemes(): ThemePreset[] {
return PRIMARY_THEMES
},
/** 高级 → 兼容主题(旧深色霓虹) */
legacyThemes(): ThemePreset[] {
return LEGACY_THEMES
},
/** 全量(自定义配色等内部用) */
allThemes(): ThemePreset[] {
return THEMES
}
},
@@ -107,7 +140,7 @@ export const useUiStore = defineStore('ui', {
this.persist()
},
/** 切换品牌主题色板 */
/** 切换品牌主题色板(精选或兼容均可) */
setThemeId(id: string) {
const preset = findTheme(id)
this.themeId = preset.id
File diff suppressed because it is too large Load Diff
@@ -1,10 +1,9 @@
/**
*
*
*
* - CSS document.documentElement.style.setProperty
* - theme.css --mg-*
* - (industrial-purple) +
* Outpost / /
* LEGACY_THEMES
* docs/superpowers/specs/2026-07-25-migu-outpost-ui-lock-design.md
*/
export interface ThemePreset {
@@ -15,116 +14,229 @@ export interface ThemePreset {
preview: string
/** 强调高光,用于 UI 预览的次代表色 */
previewAccent: string
/** Outpost 浅色壳(纸白主区);legacy 深色为 false */
outpostShell?: boolean
vars: Record<string, string>
}
export const DEFAULT_THEME_ID = 'fame-lavender'
/** Outpost 浅色壳共用表面(纸白卡、深字、关 orb);各主题可覆盖 card/tint */
const OUTPOST_SHELL_VARS: Record<string, string> = {
'--mg-bg-card-rgb': '255, 254, 253',
'--mg-bg-card-hi-rgb': '251, 249, 253',
'--mg-bg-card-darker-rgb': '245, 240, 251',
'--mg-text-tint-rgb': '40, 33, 58',
'--mg-text-hi-rgb': '117, 109, 133',
'--mg-orb-a-opacity': '0',
'--mg-orb-b-opacity': '0',
'--mg-orb-c-opacity': '0',
'--mg-radius-sm': '10px',
'--mg-radius': '16px',
'--mg-radius-lg': '20px',
'--mg-radius-xl': '24px'
}
export const THEMES: ThemePreset[] = [
{
id: 'fame-lavender',
name: '迷毂浅紫',
description: '参考 FAME 系统:深紫侧栏 + 浅紫白主区,柔和耐看,长时间使用不刺眼',
/**
* chrome///
* Harbor --me-*
*
*/
function withMapEditorChrome(vars: Record<string, string>): Record<string, string> {
const primaryRgb = vars['--mg-primary-rgb'] ?? '30, 58, 95'
const chrome1 = vars['--mg-bg-aside-1'] ?? '#1e3a5f'
const chrome2 = vars['--mg-bg-aside-2'] ?? '#0b1220'
const chromeRgb = vars['--mg-bg-aside-rgb'] ?? primaryRgb
const lightChrome = vars['--me-chrome-mode'] === 'light'
return {
...vars,
'--me-chrome-1': vars['--me-chrome-1'] ?? chrome1,
'--me-chrome-2': vars['--me-chrome-2'] ?? chrome2,
'--me-chrome-rgb': vars['--me-chrome-rgb'] ?? chromeRgb,
'--me-border': vars['--me-border'] ?? (lightChrome ? 'rgba(30, 58, 95, 0.12)' : 'rgba(255, 255, 255, 0.12)'),
'--me-text': vars['--me-text'] ?? (lightChrome ? 'rgba(11, 18, 32, 0.92)' : 'rgba(245, 242, 255, 0.94)'),
'--me-text-muted': vars['--me-text-muted'] ?? (lightChrome ? 'rgba(11, 18, 32, 0.55)' : 'rgba(210, 200, 235, 0.68)'),
'--me-surface': vars['--me-surface'] ?? (lightChrome ? 'rgba(30, 58, 95, 0.04)' : 'rgba(255, 255, 255, 0.05)'),
'--me-hover': vars['--me-hover'] ?? (lightChrome ? `rgba(${primaryRgb}, 0.08)` : `rgba(${primaryRgb}, 0.28)`),
'--me-active-glow': vars['--me-active-glow'] ?? `rgba(${primaryRgb}, 0.28)`,
'--me-card-bg': vars['--me-card-bg'] ?? (lightChrome ? 'rgba(255, 255, 255, 0.96)' : 'rgba(0, 0, 0, 0.22)'),
'--me-card-border': vars['--me-card-border'] ?? (lightChrome ? '#e2e6ec' : 'rgba(255, 255, 255, 0.08)'),
'--me-inset': vars['--me-inset'] ?? (lightChrome ? '#f4f6f8' : 'rgba(0, 0, 0, 0.28)')
}
}
function outpostPreset(
partial: Omit<ThemePreset, 'outpostShell' | 'vars'> & { vars: Record<string, string> }
): ThemePreset {
return {
...partial,
outpostShell: true,
vars: withMapEditorChrome({ ...OUTPOST_SHELL_VARS, ...partial.vars })
}
}
/** 默认:Outpost 浅色(对齐 fairylandtech.amerc.ai */
export const DEFAULT_THEME_ID = 'outpost-light'
/** 主路径精选三色(顶栏 ThemeSwitcher */
export const PRIMARY_THEMES: ThemePreset[] = [
outpostPreset({
id: 'outpost-light',
name: '浅色',
description: '纯白壳 + 紫色品牌点缀;地图壳白,画布深色',
preview: '#ffffff',
previewAccent: '#7c3aed',
vars: {
'--mg-primary': '#7c3aed',
'--mg-primary-rgb': '124, 58, 237',
'--mg-primary-hover': '#8b5cf6',
'--mg-primary-hover-rgb': '139, 92, 246',
'--mg-primary-active': '#5b21b6',
'--mg-accent': '#a855f7',
'--mg-accent-rgb': '168, 85, 247',
'--mg-primary-light-3': '#a78bfa',
'--mg-primary-light-5': '#c4b5fd',
'--mg-primary-light-7': '#ddd6fe',
'--mg-primary-light-8': '#ede9fe',
'--mg-primary-light-9': '#f5f3ff',
'--mg-primary-dark-2': '#6d28d9',
'--mg-bg-app-1': '#ffffff',
'--mg-bg-app-2': '#ffffff',
'--mg-bg-app-3': '#ffffff',
'--mg-bg-app-deep-rgb': '255, 255, 255',
'--mg-bg-aside-1': '#ffffff',
'--mg-bg-aside-2': '#fafafa',
'--mg-bg-aside-rgb': '255, 255, 255',
'--mg-shadow-rgb': '76, 29, 149',
'--mg-bg-card-rgb': '255, 255, 255',
'--mg-bg-card-hi-rgb': '255, 255, 255',
'--mg-bg-card-darker-rgb': '248, 250, 252',
'--mg-text-tint-rgb': '45, 27, 105',
'--mg-text-hi-rgb': '109, 40, 217',
/* 地图编辑:纯白壳 */
'--me-chrome-mode': 'light',
'--me-chrome-1': '#ffffff',
'--me-chrome-2': '#ffffff',
'--me-chrome-rgb': '255, 255, 255',
'--me-border': 'rgba(124, 58, 237, 0.12)',
'--me-text': 'rgba(45, 27, 105, 0.92)',
'--me-text-muted': 'rgba(76, 29, 149, 0.55)',
'--me-surface': 'rgba(124, 58, 237, 0.04)',
'--me-hover': 'rgba(124, 58, 237, 0.08)',
'--me-active-glow': 'rgba(124, 58, 237, 0.18)',
'--me-card-bg': '#ffffff',
'--me-card-border': '#e5e7eb',
'--me-inset': '#f8fafc'
}
}),
outpostPreset({
id: 'outpost-purple',
name: '紫色',
description: '同壳层、更饱和紫品牌;侧栏深蓝紫',
preview: '#7c3aed',
previewAccent: '#a855f7',
vars: {
// 主色保留紫色(与 FAME 一致),但 hover/accent 使用更明亮的 violet/purple
'--mg-primary': '#7c3aed',
'--mg-primary-rgb': '124, 58, 237',
'--mg-primary-hover': '#8b5cf6',
'--mg-primary': '#7c3aed',
'--mg-primary-rgb': '124, 58, 237',
'--mg-primary-hover': '#8b5cf6',
'--mg-primary-hover-rgb': '139, 92, 246',
'--mg-primary-active': '#5b21b6',
'--mg-accent': '#a855f7',
'--mg-accent-rgb': '168, 85, 247',
'--mg-primary-light-3': '#a78bfa',
'--mg-primary-light-5': '#c4b5fd',
'--mg-primary-light-7': '#ddd6fe',
'--mg-primary-light-8': '#ede9fe',
'--mg-primary-light-9': '#f5f3ff',
'--mg-primary-dark-2': '#6d28d9',
// 主区背景:浅薰衣草灰(v3:从近白下沉到 ~92% 明度,让白卡片靠阴影清晰浮起)
'--mg-bg-app-1': '#edeaf5',
'--mg-bg-app-2': '#e7e4f1',
'--mg-bg-app-3': '#f1eef8',
'--mg-bg-app-deep-rgb': '231, 228, 241',
// 侧栏:FAME 深紫渐变 #2d1b69 -> #1a1040
'--mg-bg-aside-1': '#2d1b69',
'--mg-bg-aside-2': '#1a1040',
'--mg-bg-aside-rgb': '45, 27, 105',
// 卡片:白底(在 fame-lavender 专属规则中走纯 #fff
'--mg-bg-card-rgb': '255, 255, 255',
'--mg-bg-card-hi-rgb': '249, 246, 255',
'--mg-bg-card-darker-rgb':'245, 240, 251',
'--mg-shadow-rgb': '124, 58, 237',
// 文字:深紫标题 #2d1b69, 中灰正文 #606266
'--mg-text-tint-rgb': '45, 27, 105',
'--mg-text-hi-rgb': '96, 98, 102',
// 关闭装饰性 orb
'--mg-orb-a-opacity': '0',
'--mg-orb-b-opacity': '0',
'--mg-orb-c-opacity': '0'
'--mg-primary-active': '#5b21b6',
'--mg-accent': '#a855f7',
'--mg-accent-rgb': '168, 85, 247',
'--mg-primary-light-3': '#a78bfa',
'--mg-primary-light-5': '#c4b5fd',
'--mg-primary-light-7': '#ddd6fe',
'--mg-primary-light-8': '#ede9fe',
'--mg-primary-light-9': '#f5f3ff',
'--mg-primary-dark-2': '#6d28d9',
'--mg-bg-app-1': '#f3eefc',
'--mg-bg-app-2': '#edeaf5',
'--mg-bg-app-3': '#f8f5ff',
'--mg-bg-app-deep-rgb': '243, 238, 252',
'--mg-bg-aside-1': '#2d1b69',
'--mg-bg-aside-2': '#1a1040',
'--mg-bg-aside-rgb': '45, 27, 105',
'--mg-shadow-rgb': '124, 58, 237'
}
},
}),
outpostPreset({
id: 'outpost-blue',
name: '蓝色',
description: '同壳层、蓝科技感;侧栏深蓝',
preview: '#3c78d2',
previewAccent: '#6ea8f0',
vars: {
'--mg-primary': '#3c78d2',
'--mg-primary-rgb': '60, 120, 210',
'--mg-primary-hover': '#4d8ae0',
'--mg-primary-hover-rgb': '77, 138, 224',
'--mg-primary-active': '#2a5fad',
'--mg-accent': '#6ea8f0',
'--mg-accent-rgb': '110, 168, 240',
'--mg-primary-light-3': '#6ea8f0',
'--mg-primary-light-5': '#9bc4f5',
'--mg-primary-light-7': '#c5ddf9',
'--mg-primary-light-8': '#e3effc',
'--mg-primary-light-9': '#f3f7fc',
'--mg-primary-dark-2': '#2a5fad',
'--mg-bg-app-1': '#f3f6fb',
'--mg-bg-app-2': '#eef2f8',
'--mg-bg-app-3': '#f8fafc',
'--mg-bg-app-deep-rgb': '243, 246, 251',
'--mg-bg-aside-1': '#0f2744',
'--mg-bg-aside-2': '#0a1a30',
'--mg-bg-aside-rgb': '15, 39, 68',
'--mg-shadow-rgb': '30, 58, 95'
}
})
]
/** 高级兼容:旧深色霓虹主题(用户菜单「兼容主题」) */
export const LEGACY_THEMES: ThemePreset[] = [
{
id: 'industrial-purple',
name: '星云紫',
description: '科幻紫主色,辅以蓝紫渐变,磨砂玻璃 · 霓虹辉光',
description: '科幻紫主色 · 磨砂玻璃 · 霓虹辉光(兼容)',
preview: '#7c3aed',
previewAccent: '#c4b5fd',
outpostShell: false,
vars: {
// ── 主色:明亮 violet(紫),主导整个画面 ──
'--mg-primary': '#7c3aed',
'--mg-primary-rgb': '124, 58, 237',
// hover/辉光:偏蓝紫 indigo,带来氛围流动感
'--mg-primary-hover': '#8b5cf6',
'--mg-primary': '#7c3aed',
'--mg-primary-rgb': '124, 58, 237',
'--mg-primary-hover': '#8b5cf6',
'--mg-primary-hover-rgb': '139, 92, 246',
'--mg-primary-active': '#5b21b6',
// accent:浅紫高光,边缘霓虹与点缀
'--mg-accent': '#c4b5fd',
'--mg-accent-rgb': '196, 181, 253',
'--mg-primary-active': '#5b21b6',
'--mg-accent': '#c4b5fd',
'--mg-accent-rgb': '196, 181, 253',
'--mg-primary-light-3': '#9d6cf2',
'--mg-primary-light-5': '#b594f7',
'--mg-primary-light-7': '#6f4eab',
'--mg-primary-light-8': '#4c357a',
'--mg-primary-light-9': '#2d1f55',
'--mg-primary-dark-2': '#4c1d95',
// ── 背景:深紫黑底,带 indigo 余晖 ──
'--mg-bg-app-1': '#0e0a24',
'--mg-bg-app-2': '#1f1147',
'--mg-bg-app-3': '#06031a',
'--mg-primary-dark-2': '#4c1d95',
'--mg-bg-app-1': '#0e0a24',
'--mg-bg-app-2': '#1f1147',
'--mg-bg-app-3': '#06031a',
'--mg-bg-app-deep-rgb': '6, 3, 26',
'--mg-bg-aside-1': '#0c0820',
'--mg-bg-aside-2': '#03020e',
'--mg-bg-aside-rgb': '10, 6, 30',
'--mg-bg-card-rgb': '38, 24, 78',
'--mg-bg-card-hi-rgb': '58, 38, 110',
'--mg-bg-aside-1': '#0c0820',
'--mg-bg-aside-2': '#03020e',
'--mg-bg-aside-rgb': '10, 6, 30',
'--mg-bg-card-rgb': '38, 24, 78',
'--mg-bg-card-hi-rgb': '58, 38, 110',
'--mg-bg-card-darker-rgb': '20, 12, 48',
'--mg-shadow-rgb': '8, 2, 24',
'--mg-text-tint-rgb': '242, 232, 255',
'--mg-text-hi-rgb': '220, 200, 252',
'--mg-orb-a-opacity': '0.55',
'--mg-orb-b-opacity': '0.50',
'--mg-orb-c-opacity': '0.40'
'--mg-shadow-rgb': '8, 2, 24',
'--mg-text-tint-rgb': '242, 232, 255',
'--mg-text-hi-rgb': '220, 200, 252',
'--mg-orb-a-opacity': '0.55',
'--mg-orb-b-opacity': '0.50',
'--mg-orb-c-opacity': '0.40'
}
},
{
id: 'deep-azure',
name: '深邃蓝',
description: '科技感深蓝,沉静稳重',
description: '科技感深蓝,沉静稳重(兼容)',
preview: '#1565c0',
previewAccent: '#82b1ff',
outpostShell: false,
vars: {
'--mg-primary': '#1565c0',
'--mg-primary-rgb': '21, 101, 192',
@@ -133,28 +245,23 @@ export const THEMES: ThemePreset[] = [
'--mg-primary-active': '#0d47a1',
'--mg-accent': '#82b1ff',
'--mg-accent-rgb': '130, 177, 255',
'--mg-primary-light-3': '#5a8fce',
'--mg-primary-light-5': '#83aedb',
'--mg-primary-light-7': '#adcde7',
'--mg-primary-light-8': '#c4dbed',
'--mg-primary-light-9': '#e3eef8',
'--mg-primary-dark-2': '#0d47a1',
'--mg-bg-app-1': '#051a3a',
'--mg-bg-app-2': '#0a2f6b',
'--mg-bg-app-3': '#020a1f',
'--mg-bg-app-deep-rgb': '2, 8, 26',
'--mg-bg-aside-1': '#06122a',
'--mg-bg-aside-2': '#02060f',
'--mg-bg-aside-rgb': '8, 18, 42',
'--mg-bg-card-rgb': '12, 36, 80',
'--mg-bg-card-hi-rgb': '18, 50, 110',
'--mg-bg-card-darker-rgb': '6, 18, 44',
'--mg-shadow-rgb': '2, 4, 14',
'--mg-text-tint-rgb': '218, 234, 252',
'--mg-text-hi-rgb': '170, 200, 240'
}
@@ -162,9 +269,10 @@ export const THEMES: ThemePreset[] = [
{
id: 'emerald-forge',
name: '翡翠绿',
description: '机械翡翠绿,沉稳活力',
description: '机械翡翠绿(兼容)',
preview: '#00796b',
previewAccent: '#80cbc4',
outpostShell: false,
vars: {
'--mg-primary': '#00796b',
'--mg-primary-rgb': '0, 121, 107',
@@ -173,28 +281,23 @@ export const THEMES: ThemePreset[] = [
'--mg-primary-active': '#004d40',
'--mg-accent': '#80cbc4',
'--mg-accent-rgb': '128, 203, 196',
'--mg-primary-light-3': '#4ca094',
'--mg-primary-light-5': '#79b9b0',
'--mg-primary-light-7': '#a6d2cc',
'--mg-primary-light-8': '#bfdfdb',
'--mg-primary-light-9': '#e1f0ee',
'--mg-primary-dark-2': '#004d40',
'--mg-bg-app-1': '#06241f',
'--mg-bg-app-2': '#0a4538',
'--mg-bg-app-3': '#020f0c',
'--mg-bg-app-deep-rgb': '2, 12, 10',
'--mg-bg-aside-1': '#061a16',
'--mg-bg-aside-2': '#020a08',
'--mg-bg-aside-rgb': '8, 26, 22',
'--mg-bg-card-rgb': '10, 50, 42',
'--mg-bg-card-hi-rgb': '14, 66, 56',
'--mg-bg-card-darker-rgb': '6, 28, 24',
'--mg-shadow-rgb': '2, 10, 8',
'--mg-text-tint-rgb': '220, 244, 240',
'--mg-text-hi-rgb': '170, 220, 210'
}
@@ -202,9 +305,10 @@ export const THEMES: ThemePreset[] = [
{
id: 'crimson-iron',
name: '熔铁赤',
description: '熔铁炉火般的暗红工业风',
description: '暗红工业风(兼容)',
preview: '#b71c1c',
previewAccent: '#ff8a80',
outpostShell: false,
vars: {
'--mg-primary': '#b71c1c',
'--mg-primary-rgb': '183, 28, 28',
@@ -213,28 +317,23 @@ export const THEMES: ThemePreset[] = [
'--mg-primary-active': '#8a0c0c',
'--mg-accent': '#ff8a80',
'--mg-accent-rgb': '255, 138, 128',
'--mg-primary-light-3': '#c95a5a',
'--mg-primary-light-5': '#d68585',
'--mg-primary-light-7': '#e4afaf',
'--mg-primary-light-8': '#edc8c8',
'--mg-primary-light-9': '#f7e6e6',
'--mg-primary-dark-2': '#8a0c0c',
'--mg-bg-app-1': '#2a0606',
'--mg-bg-app-2': '#4a0c0c',
'--mg-bg-app-3': '#150303',
'--mg-bg-app-deep-rgb': '20, 4, 4',
'--mg-bg-aside-1': '#200505',
'--mg-bg-aside-2': '#0c0202',
'--mg-bg-aside-rgb': '36, 6, 6',
'--mg-bg-card-rgb': '60, 10, 10',
'--mg-bg-card-hi-rgb': '80, 14, 14',
'--mg-bg-card-darker-rgb': '34, 5, 5',
'--mg-shadow-rgb': '10, 1, 1',
'--mg-text-tint-rgb': '252, 220, 220',
'--mg-text-hi-rgb': '240, 175, 175'
}
@@ -242,9 +341,10 @@ export const THEMES: ThemePreset[] = [
{
id: 'graphite-steel',
name: '石墨钢',
description: '中性冷灰,纯粹工业',
description: '中性冷灰(兼容)',
preview: '#455a64',
previewAccent: '#b0bec5',
outpostShell: false,
vars: {
'--mg-primary': '#455a64',
'--mg-primary-rgb': '69, 90, 100',
@@ -253,28 +353,23 @@ export const THEMES: ThemePreset[] = [
'--mg-primary-active': '#263238',
'--mg-accent': '#b0bec5',
'--mg-accent-rgb': '176, 190, 197',
'--mg-primary-light-3': '#7a8a92',
'--mg-primary-light-5': '#9aa6ac',
'--mg-primary-light-7': '#bac2c7',
'--mg-primary-light-8': '#ccd2d6',
'--mg-primary-light-9': '#e8ebed',
'--mg-primary-dark-2': '#263238',
'--mg-bg-app-1': '#161e24',
'--mg-bg-app-2': '#243038',
'--mg-bg-app-3': '#0a0e12',
'--mg-bg-app-deep-rgb': '10, 14, 18',
'--mg-bg-aside-1': '#11181d',
'--mg-bg-aside-2': '#060a0d',
'--mg-bg-aside-rgb': '18, 26, 32',
'--mg-bg-card-rgb': '30, 42, 50',
'--mg-bg-card-hi-rgb': '42, 56, 66',
'--mg-bg-card-darker-rgb': '16, 22, 28',
'--mg-shadow-rgb': '4, 6, 10',
'--mg-text-tint-rgb': '226, 232, 238',
'--mg-text-hi-rgb': '186, 198, 210'
}
@@ -282,9 +377,10 @@ export const THEMES: ThemePreset[] = [
{
id: 'amber-forge',
name: '琥珀金',
description: '高对比的橙金调,警示与活力并存',
description: '橙金调(兼容)',
preview: '#e65100',
previewAccent: '#ffb74d',
outpostShell: false,
vars: {
'--mg-primary': '#e65100',
'--mg-primary-rgb': '230, 81, 0',
@@ -293,44 +389,69 @@ export const THEMES: ThemePreset[] = [
'--mg-primary-active': '#bf360c',
'--mg-accent': '#ffb74d',
'--mg-accent-rgb': '255, 183, 77',
'--mg-primary-light-3': '#ec8344',
'--mg-primary-light-5': '#f2a474',
'--mg-primary-light-7': '#f7c4a3',
'--mg-primary-light-8': '#fad6bc',
'--mg-primary-light-9': '#fdeadd',
'--mg-primary-dark-2': '#bf360c',
'--mg-bg-app-1': '#2a1404',
'--mg-bg-app-2': '#4a2a0c',
'--mg-bg-app-3': '#150a02',
'--mg-bg-app-deep-rgb': '20, 10, 4',
'--mg-bg-aside-1': '#200f04',
'--mg-bg-aside-2': '#0c0602',
'--mg-bg-aside-rgb': '34, 16, 6',
'--mg-bg-card-rgb': '62, 32, 8',
'--mg-bg-card-hi-rgb': '82, 44, 12',
'--mg-bg-card-darker-rgb': '34, 16, 4',
'--mg-shadow-rgb': '10, 4, 1',
'--mg-text-tint-rgb': '252, 232, 210',
'--mg-text-hi-rgb': '240, 196, 140'
}
}
]
export function findTheme(id: string | undefined | null): ThemePreset {
if (!id) return THEMES[0]
return THEMES.find((t) => t.id === id) ?? THEMES[0]
/** 全量(查找用):精选 + 兼容 */
export const THEMES: ThemePreset[] = [...PRIMARY_THEMES, ...LEGACY_THEMES]
/** 旧 id → 新精选 id */
const THEME_ALIASES: Record<string, string> = {
'fame-lavender': 'outpost-purple',
'outpost-fairyland': 'outpost-light'
}
/** 把主题变量写入 :root;多次调用幂等,会覆盖同名属性 */
export function resolveThemeId(id: string | undefined | null): string {
if (!id) return DEFAULT_THEME_ID
const aliased = THEME_ALIASES[id] ?? id
if (THEMES.some((t) => t.id === aliased)) return aliased
return DEFAULT_THEME_ID
}
export function findTheme(id: string | undefined | null): ThemePreset {
const resolved = resolveThemeId(id)
return THEMES.find((t) => t.id === resolved) ?? PRIMARY_THEMES[0]
}
export function isLegacyThemeId(id: string | undefined | null): boolean {
if (!id) return false
const resolved = THEME_ALIASES[id] ?? id
return LEGACY_THEMES.some((t) => t.id === resolved)
}
/** 把主题变量写入 :rootOutpost 壳写 data-shell="outpost" 供浅色 CSS 命中 */
export function applyThemeVars(preset: ThemePreset): void {
const root = document.documentElement
for (const [key, value] of Object.entries(preset.vars)) {
// preset.vars 在 outpostPreset 里已跑过 withMapEditorChrome;再跑一次以补全缺省 --me-*
const vars = withMapEditorChrome({ ...preset.vars })
for (const [key, value] of Object.entries(vars)) {
if (key === '--me-chrome-mode') continue
root.style.setProperty(key, value)
}
root.setAttribute('data-theme', preset.id)
if (preset.outpostShell) {
root.setAttribute('data-shell', 'outpost')
} else {
root.removeAttribute('data-shell')
}
}
@@ -4,17 +4,10 @@
<div class="bg-overlay" />
<div class="bg-orb bg-orb-a" />
<div class="bg-orb bg-orb-b" />
<div class="bg-orb bg-orb-c" />
<div class="bg-stars" aria-hidden="true">
<span v-for="(s, i) in starStyles" :key="i" :style="s" />
</div>
<div class="login-card">
<!-- 左侧品牌 hero -->
<div class="hero">
<div class="hero-stars" aria-hidden="true">
<span v-for="(s, i) in heroStarStyles" :key="i" :style="s" />
</div>
<div class="hero-brand">
<span class="hero-mark">
<img src="/FRLD-logo-white-no_title.png" alt="迷毂" class="hero-mark-img" />
@@ -212,25 +205,6 @@ const rules: FormRules = {
const year = computed(() => new Date().getFullYear())
// n
function makeStars(n: number): Record<string, string>[] {
return Array.from({ length: n }, () => {
const size = 1.5 + Math.random() * 2.8
return {
top: `${Math.random() * 100}%`,
left: `${Math.random() * 100}%`,
width: `${size}px`,
height: `${size}px`,
opacity: `${0.35 + Math.random() * 0.5}`,
animationDelay: `${-Math.random() * 6}s`,
animationDuration: `${2.6 + Math.random() * 3.4}s`
} as Record<string, string>
})
}
// +
const starStyles = makeStars(48)
const heroStarStyles = makeStars(22)
async function submit() {
if (!formRef.value) return
await formRef.value.validate(async (ok) => {
@@ -269,13 +243,8 @@ async function submit() {
</script>
<style scoped>
/*
* 登录页固定配色不随全局主题切换变化
* 统一用一组本地 --lg-* 变量午夜靛蓝 + 青色霓虹保证无论用户切到哪个
* 主题登录界面始终是同一套高级克制的深色质感
* */
/* Outpost 登录:纸白主区 + plum 品牌柱(对齐 fairylandtech.amerc.ai */
.login-page {
/* 梦幻晨曦渐变:粉紫(左上) → 蓝紫(中) → 淡蓝(右下),高明度柔光,参考产品视觉稿。 */
position: relative;
min-height: 100vh;
display: flex;
@@ -283,11 +252,11 @@ async function submit() {
justify-content: center;
overflow: hidden;
background:
radial-gradient(ellipse 75% 55% at 16% 14%, #dcbef7 0%, transparent 55%),
radial-gradient(ellipse 80% 65% at 86% 90%, #abc9f5 0%, transparent 58%),
linear-gradient(135deg, #cbb2f0 0%, #b7adec 46%, #a7c6f2 100%);
color: var(--lg-text);
font-family: 'PingFang SC', 'Microsoft YaHei', 'Segoe UI', sans-serif;
radial-gradient(ellipse 70% 50% at 15% 10%, rgba(117, 67, 232, 0.14), transparent 55%),
radial-gradient(ellipse 60% 45% at 90% 85%, rgba(155, 124, 255, 0.1), transparent 50%),
#f6f3fb;
color: #28213a;
font-family: 'Inter', 'PingFang SC', 'Microsoft YaHei', 'Segoe UI', sans-serif;
}
/* ===== 背景层 ===== */
@@ -329,25 +298,18 @@ async function submit() {
animation: drift 20s ease-in-out infinite;
}
.bg-orb-a {
width: 460px; height: 460px;
top: -150px; left: -130px;
background: radial-gradient(circle, var(--lg-primary-hover) 0%, var(--lg-primary) 52%, transparent 80%);
opacity: 0.24;
width: 420px; height: 420px;
top: -140px; left: -100px;
background: radial-gradient(circle, rgba(155, 124, 255, 0.55) 0%, rgba(117, 67, 232, 0.35) 55%, transparent 80%);
opacity: 0.22;
}
.bg-orb-b {
width: 600px; height: 600px;
bottom: -220px; right: -190px;
background: radial-gradient(circle, var(--lg-accent) 0%, var(--lg-primary-deep) 58%, transparent 86%);
opacity: 0.18;
width: 480px; height: 480px;
bottom: -180px; right: -140px;
background: radial-gradient(circle, rgba(117, 67, 232, 0.4) 0%, rgba(155, 124, 255, 0.2) 55%, transparent 85%);
opacity: 0.16;
animation-delay: -7s;
}
.bg-orb-c {
width: 320px; height: 320px;
top: 32%; right: 9%;
background: radial-gradient(circle, var(--lg-primary-hover) 0%, var(--lg-primary) 60%, transparent 90%);
opacity: 0.14;
animation-delay: -13s;
}
@keyframes drift {
0%, 100% { transform: translate(0, 0) scale(1); }
50% { transform: translate(18px, -28px) scale(1.05); }
@@ -368,7 +330,7 @@ async function submit() {
50% { opacity: 0.95; transform: scale(1.2); }
}
/* ===== 卡片:双栏 hero ===== */
/* ===== 卡片:Outpost 纸白 + plum 品牌柱 ===== */
.login-card {
position: relative;
z-index: 2;
@@ -378,34 +340,19 @@ async function submit() {
display: grid;
grid-template-columns: 360px 1fr;
overflow: hidden;
border-radius: 22px;
border: 1px solid rgba(255, 255, 255, 0.08);
/* 登录框本体:紫 → 靛蓝的「紫蓝混合」渐变,梦幻且不发灰;高不透明度保证文字清晰 */
background:
linear-gradient(140deg,
rgba(48, 26, 96, 0.95) 0%,
rgba(33, 26, 92, 0.955) 50%,
rgba(22, 26, 84, 0.96) 100%);
backdrop-filter: blur(26px) saturate(150%);
-webkit-backdrop-filter: blur(26px) saturate(150%);
border-radius: 20px;
border: 1px solid rgba(40, 33, 58, 0.08);
background: #fffefd;
box-shadow:
0 44px 110px rgba(0, 0, 0, 0.62),
0 0 70px rgba(var(--lg-primary-rgb), 0.24),
0 0 0 1px rgba(var(--lg-accent-rgb), 0.16) inset,
0 1px 0 rgba(255, 255, 255, 0.18) inset;
0 1px 2px rgba(40, 33, 58, 0.04),
0 24px 56px rgba(54, 35, 78, 0.12);
}
/* 顶部霓虹高光线 */
.login-card::before {
content: '';
position: absolute;
top: 0; left: 12%; right: 12%;
height: 1px;
background: linear-gradient(90deg,
transparent 0%,
rgba(var(--lg-accent-rgb), 0.55) 30%,
rgba(255, 255, 255, 0.92) 50%,
rgba(var(--lg-accent-rgb), 0.55) 70%,
transparent 100%);
background: linear-gradient(90deg, transparent 0%, rgba(117, 67, 232, 0.35) 50%, transparent 100%);
pointer-events: none;
z-index: 3;
}
@@ -415,12 +362,8 @@ async function submit() {
position: relative;
padding: 44px 34px 34px;
color: #fff;
/* 品牌区紫蓝混合:左上紫光球 + 右下蓝光球,叠在深紫底上 */
background:
radial-gradient(ellipse at 16% 18%, rgba(123, 77, 255, 0.34) 0%, transparent 56%),
radial-gradient(ellipse at 88% 84%, rgba(94, 124, 255, 0.32) 0%, transparent 58%),
linear-gradient(170deg, rgba(var(--lg-primary-rgb), 0.26) 0%, rgba(var(--lg-aside-rgb), 0.42) 60%, rgba(var(--lg-deep-rgb), 0.55) 100%);
border-right: 1px solid rgba(255, 255, 255, 0.08);
background: linear-gradient(165deg, #3f2454 0%, #241530 100%);
border-right: 1px solid rgba(255, 255, 255, 0.06);
display: flex;
flex-direction: column;
}
@@ -550,56 +493,53 @@ async function submit() {
padding: 44px 44px 32px;
display: flex;
flex-direction: column;
/* 表单侧:紫 → 蓝的洗光,与品牌区一致的「紫蓝混合」基调 */
background:
linear-gradient(135deg, rgba(123, 77, 255, 0.14) 0%, rgba(94, 124, 255, 0.10) 100%);
background: #fffefd;
color: #28213a;
}
.panel-head { margin-bottom: 26px; }
.panel-title {
font-size: 30px; font-weight: 700;
letter-spacing: 12px;
background: linear-gradient(135deg, #ffffff 0%, var(--lg-accent) 100%);
-webkit-background-clip: text;
background-clip: text;
color: transparent;
text-shadow: 0 0 22px rgba(var(--lg-primary-hover-rgb), 0.5);
font-size: 28px; font-weight: 700;
letter-spacing: 8px;
color: #28213a;
background: none;
-webkit-text-fill-color: unset;
text-shadow: none;
}
.panel-sub {
margin-top: 6px;
font-size: 12.5px;
color: var(--lg-text-dim);
color: #756d85;
letter-spacing: 1px;
}
.glass-form { flex: 1; display: flex; flex-direction: column; }
.glass-form :deep(.el-form-item) { margin-bottom: 18px; }
/* 输入框 透明玻璃 */
.glass-form :deep(.el-input__wrapper) {
background: rgba(255, 255, 255, 0.06) !important;
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.16) inset !important;
border-radius: 10px !important;
transition: all .25s;
background: #f6f3fb !important;
box-shadow: 0 0 0 1px rgba(40, 33, 58, 0.1) inset !important;
border-radius: 12px !important;
transition: all .2s;
}
.glass-form :deep(.el-input__wrapper:hover) {
background: rgba(255, 255, 255, 0.11) !important;
box-shadow: 0 0 0 1px rgba(var(--lg-accent-rgb), 0.5) inset !important;
background: #fbf9fd !important;
box-shadow: 0 0 0 1px rgba(117, 67, 232, 0.35) inset !important;
}
.glass-form :deep(.el-input__wrapper.is-focus) {
background: rgba(255, 255, 255, 0.13) !important;
background: #fff !important;
box-shadow:
0 0 0 1px rgba(var(--lg-accent-rgb), 0.85) inset,
0 0 20px rgba(var(--lg-primary-hover-rgb), 0.45) !important;
0 0 0 1px rgba(117, 67, 232, 0.55) inset,
0 0 0 3px rgba(117, 67, 232, 0.12) !important;
}
.glass-form :deep(.el-input__inner) {
color: #fff !important;
-webkit-text-fill-color: #fff !important;
color: #28213a !important;
-webkit-text-fill-color: #28213a !important;
font-size: 14px;
height: 42px;
}
.glass-form :deep(.el-input__inner::placeholder) { color: rgba(255, 255, 255, 0.42); }
.glass-form :deep(.el-input__inner::placeholder) { color: #a8a0c0; }
.glass-form :deep(.el-input__prefix-inner > :first-child),
.glass-form :deep(.el-input__suffix-inner) { color: rgba(255, 255, 255, 0.55); }
.glass-form :deep(.el-input__suffix-inner) { color: #756d85; }
/* scope 双卡 */
.scope-item :deep(.el-form-item__content) { width: 100%; }
@@ -611,45 +551,42 @@ async function submit() {
display: flex; align-items: center; gap: 6px;
margin-bottom: 8px;
font-size: 12px;
color: var(--lg-text-soft);
color: #756d85;
letter-spacing: 1px;
}
.launch-mode-title { font-weight: 500; }
.launch-mode-help {
color: var(--lg-text-dim);
color: #a8a0c0;
cursor: help;
font-size: 14px;
}
.launch-mode-help:hover { color: var(--lg-accent); }
.launch-mode-help:hover { color: #7543e8; }
.launch-wrap { margin-bottom: 4px; }
.launch-tip { max-width: 280px; line-height: 1.6; font-size: 12px; }
.launch-tip > div + div { margin-top: 6px; }
.scope-tab {
appearance: none;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.16);
background: #f6f3fb;
border: 1px solid rgba(40, 33, 58, 0.1);
border-radius: 12px;
padding: 14px 14px;
display: flex; align-items: center; gap: 12px;
color: rgba(255, 255, 255, 0.78);
color: #28213a;
cursor: pointer;
transition: all .2s;
text-align: left;
}
.scope-tab:hover {
background: rgba(255, 255, 255, 0.11);
color: #fff;
background: #fbf9fd;
border-color: rgba(117, 67, 232, 0.3);
color: #28213a;
transform: translateY(-1px);
}
.scope-tab.active {
background: linear-gradient(135deg,
rgba(var(--lg-primary-rgb), 0.55) 0%,
rgba(var(--lg-accent-rgb), 0.28) 100%);
border-color: rgba(var(--lg-accent-rgb), 0.75);
background: linear-gradient(135deg, #7543e8 0%, #9b7cff 100%);
border-color: transparent;
color: #fff;
box-shadow:
0 0 22px rgba(var(--lg-primary-hover-rgb), 0.45),
0 0 0 1px rgba(var(--lg-accent-rgb), 0.45) inset;
box-shadow: 0 8px 20px rgba(117, 67, 232, 0.28);
}
.scope-tab .el-icon { font-size: 20px; }
.scope-meta { display: flex; flex-direction: column; line-height: 1.3; min-width: 0; }
@@ -660,107 +597,83 @@ async function submit() {
.row { display: flex; align-items: center; }
.row-between { justify-content: space-between; margin: -6px 0 10px; }
.remember :deep(.el-checkbox__label) {
color: rgba(255, 255, 255, 0.9) !important;
color: #28213a !important;
font-size: 12.5px;
text-shadow: 0 1px 2px rgba(6, 9, 18, 0.4);
text-shadow: none;
}
.remember :deep(.el-checkbox__inner) {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.32);
background: #fff;
border-color: rgba(40, 33, 58, 0.25);
}
.remember :deep(.el-checkbox.is-checked .el-checkbox__inner) {
background: var(--lg-primary);
border-color: var(--lg-primary);
background: #7543e8;
border-color: #7543e8;
}
.adv-link { color: var(--lg-text-soft) !important; font-size: 12.5px; }
.adv-link:hover { color: #fff !important; }
.adv-link { color: #756d85 !important; font-size: 12.5px; }
.adv-link:hover { color: #7543e8 !important; }
/* 高级折叠 */
.adv-collapse { background: transparent !important; border: none !important; margin-bottom: 16px; }
.adv-collapse :deep(.el-collapse-item__wrap),
.adv-collapse :deep(.el-collapse-item__header) {
background: transparent !important;
border-color: rgba(255, 255, 255, 0.14) !important;
color: var(--lg-text-soft) !important;
border-color: rgba(40, 33, 58, 0.1) !important;
color: #756d85 !important;
}
.adv-title { font-size: 12px; letter-spacing: 1px; }
.adv-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 16px; padding-top: 6px; }
.adv-grid :deep(.el-form-item__label) {
color: var(--lg-text-soft);
color: #756d85;
font-size: 12px;
}
.adv-grid :deep(.el-input-number) { width: 100%; }
.adv-grid :deep(.el-input-number .el-input__inner) { text-align: left; }
/* 登录按钮:靛蓝→青 霓虹 · 扫光动效 */
.btn-login {
position: relative;
overflow: hidden;
height: 46px !important;
border-radius: 10px;
border-radius: 12px;
margin-top: 6px;
font-size: 15px;
letter-spacing: 6px;
font-weight: 600;
color: #fff !important;
background: linear-gradient(135deg, var(--lg-primary) 0%, var(--lg-primary-hover) 100%) !important;
background: #7543e8 !important;
border: none !important;
box-shadow:
0 12px 28px rgba(var(--lg-primary-rgb), 0.5),
0 0 26px rgba(var(--lg-primary-hover-rgb), 0.36),
0 0 0 1px rgba(255, 255, 255, 0.20) inset !important;
transition: all .28s cubic-bezier(.25, .8, .25, 1);
}
.btn-login::after {
content: '';
position: absolute;
top: 0; left: -120%;
width: 80%; height: 100%;
background: linear-gradient(110deg,
transparent 0%,
rgba(255, 255, 255, 0.0) 30%,
rgba(255, 255, 255, 0.38) 50%,
rgba(255, 255, 255, 0.0) 70%,
transparent 100%);
transition: left .6s cubic-bezier(.25, .8, .25, 1);
pointer-events: none;
box-shadow: 0 10px 24px rgba(117, 67, 232, 0.28) !important;
transition: all .2s ease;
}
.btn-login::after { display: none; }
.btn-login:hover {
transform: translateY(-2px);
background: linear-gradient(135deg, var(--lg-primary-hover) 0%, var(--lg-accent) 130%) !important;
box-shadow:
0 18px 38px rgba(var(--lg-primary-rgb), 0.6),
0 0 42px rgba(var(--lg-accent-rgb), 0.5),
0 0 0 1px rgba(var(--lg-accent-rgb), 0.5) inset !important;
transform: translateY(-1px);
background: #8660f0 !important;
box-shadow: 0 14px 28px rgba(117, 67, 232, 0.35) !important;
}
.btn-login:hover::after { left: 120%; }
.bottom-note {
margin-top: 16px;
font-size: 11.5px;
color: var(--lg-text-dim);
color: #756d85;
text-align: center;
line-height: 1.6;
}
/* 底部水印 */
.footer-stamp {
position: absolute;
bottom: 18px; left: 50%;
transform: translateX(-50%);
font-size: 11px;
letter-spacing: 2px;
color: rgba(45, 32, 78, 0.6);
color: rgba(40, 33, 58, 0.45);
z-index: 2;
}
/* 错误提示 */
.glass-form :deep(.el-form-item__error) {
color: #ff9bb8;
color: #c23a5c;
padding-top: 4px;
}
/* 小屏自适应 */
@media (max-width: 760px) {
.login-card { grid-template-columns: 1fr; min-height: auto; }
.hero { border-right: none; border-bottom: 1px solid rgba(255, 255, 255, 0.1); padding: 28px; }
File diff suppressed because it is too large Load Diff
@@ -1740,8 +1740,14 @@ onUnmounted(() => {
.map-editor-page {
height: calc(100vh - 56px);
display: flex; flex-direction: column;
background: radial-gradient(ellipse at 50% 40%, #2d1456 0%, #1a0930 50%, #0f0420 100%);
color: rgba(232,215,245,0.9);
/* 画布区保持深色;色相跟当前主题侧栏 (--me-chrome-*) */
background: radial-gradient(
ellipse at 50% 40%,
var(--me-chrome-1) 0%,
var(--me-chrome-2) 52%,
#080610 100%
);
color: var(--me-text);
}
.me-body {
flex: 1;
@@ -1767,13 +1773,13 @@ onUnmounted(() => {
border-radius: 12px 0 0 12px;
cursor: pointer;
color: #fff;
background: linear-gradient(135deg, rgba(150, 90, 240, 0.96) 0%, rgba(255, 90, 200, 0.92) 100%);
box-shadow: -4px 0 16px rgba(120, 40, 200, 0.45);
background: var(--mg-primary);
box-shadow: -4px 0 16px var(--me-active-glow);
transition: right 0.26s cubic-bezier(0.25, 0.8, 0.25, 1), filter 0.15s ease, box-shadow 0.15s ease;
}
.ai-assistant-fab:hover {
filter: brightness(1.08);
box-shadow: -6px 0 22px rgba(150, 60, 230, 0.6);
box-shadow: -6px 0 22px var(--me-active-glow);
}
/* 展开时 right 偏移由内联样式按面板实际宽度设置(默认 360);此处仅作兜底。 */
.ai-assistant-fab.is-open {
File diff suppressed because it is too large Load Diff