feat(desktop): add type declaration for @brand-logo alias in brand-logo.d.ts
Deployment Config CI / Deployment Config (push) Successful in 12s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 30s
Desktop Client Build / Resolve Build Metadata (push) Successful in 26s
Desktop Client Build / Build Desktop Client (push) Successful in 22m8s
Deployment Config CI / Deployment Config (push) Successful in 12s
Desktop Client Build / Publish Client Artifacts to NAS (push) Successful in 30s
Desktop Client Build / Resolve Build Metadata (push) Successful in 26s
Desktop Client Build / Build Desktop Client (push) Successful in 22m8s
This commit is contained in:
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
@@ -94,6 +94,9 @@ export default defineConfig({
|
||||
resolve: {
|
||||
alias: {
|
||||
'@renderer': resolve(rootDir, 'src/renderer'),
|
||||
// Single source of truth for the brand mark; lives in public/ so the
|
||||
// main process and electron-builder can read the same file.
|
||||
'@brand-logo': resolve(rootDir, 'public/logo.svg'),
|
||||
...workspaceAliases,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"description": "省心推客户端 — 连接和管理您的媒体账号,轻松发布和监控内容表现。",
|
||||
"author": {
|
||||
"name": "Liang Xu",
|
||||
"email": "liangxu@qq.com",
|
||||
"email": "liangxu0920@qq.com",
|
||||
"url": "https://shengxintui.com"
|
||||
},
|
||||
"type": "module",
|
||||
@@ -13,6 +13,7 @@
|
||||
"scripts": {
|
||||
"dev": "electron-vite dev --inspect=9229",
|
||||
"build": "electron-vite build",
|
||||
"gen:icons": "node scripts/generate-brand-icons.cjs",
|
||||
"obfuscate": "node scripts/obfuscate.cjs",
|
||||
"build:obf": "pnpm run build && pnpm run obfuscate",
|
||||
"package:mac": "pnpm run build:obf && electron-builder --mac --arm64 --universal",
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,210 @@
|
||||
#!/usr/bin/env node
|
||||
/*
|
||||
* Generates every brand icon asset the desktop client needs from the single
|
||||
* source of truth: public/logo.svg.
|
||||
*
|
||||
* Outputs:
|
||||
* build/icon.icns macOS app icon (electron-builder)
|
||||
* build/icon.png Linux app icon + Windows/icns fallback (1024x1024)
|
||||
* build/icon.ico Windows app icon
|
||||
* src/main/brand-icons.ts base64 icons consumed by the main process
|
||||
* (tray + dock + window + Windows overlay)
|
||||
*
|
||||
* Re-run after changing public/logo.svg:
|
||||
* node scripts/generate-brand-icons.cjs
|
||||
*
|
||||
* Requires `rsvg-convert`, `sips` and `iconutil` (preinstalled on macOS via
|
||||
* librsvg + the system toolchain).
|
||||
*/
|
||||
const { execFileSync } = require('node:child_process')
|
||||
const fs = require('node:fs')
|
||||
const os = require('node:os')
|
||||
const path = require('node:path')
|
||||
|
||||
const APP_DIR = path.resolve(__dirname, '..')
|
||||
const LOGO_PATH = path.join(APP_DIR, 'public', 'logo.svg')
|
||||
const BUILD_DIR = path.join(APP_DIR, 'build')
|
||||
const OUT_TS = path.join(APP_DIR, 'src', 'main', 'brand-icons.ts')
|
||||
|
||||
// Brand palette — keep in sync with the renderer (#1677ff is Ant Design blue).
|
||||
const BRAND_BLUE = '#1677ff'
|
||||
const DANGER_RED = '#ff4d4f'
|
||||
const LOGO_FILL = '#dfdfdf' // the fill baked into logo.svg
|
||||
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'brand-icons-'))
|
||||
const cleanup = () => fs.rmSync(tmpDir, { recursive: true, force: true })
|
||||
|
||||
function rsvg(args) {
|
||||
return execFileSync('rsvg-convert', args)
|
||||
}
|
||||
|
||||
/** Extract the inner markup (the <path> elements) and viewBox from logo.svg. */
|
||||
function readLogo() {
|
||||
const raw = fs.readFileSync(LOGO_PATH, 'utf8')
|
||||
const viewBox = (raw.match(/viewBox="([^"]+)"/) || [])[1]
|
||||
const inner = (raw.match(/<svg[^>]*>([\s\S]*)<\/svg>/) || [])[1]
|
||||
if (!viewBox || !inner) {
|
||||
throw new Error('Could not parse public/logo.svg (missing viewBox or body)')
|
||||
}
|
||||
const [, , vbW, vbH] = viewBox.split(/\s+/).map(Number)
|
||||
return { inner: inner.trim(), width: vbW, height: vbH }
|
||||
}
|
||||
|
||||
const logo = readLogo()
|
||||
|
||||
/** A square app icon: logo centered on a rounded-rect (squircle-ish) bg. */
|
||||
function squareIconSvg(bg) {
|
||||
const CANVAS = 1024
|
||||
// macOS icon grid: 824x824 art area inside a 1024 canvas (100px margin).
|
||||
const PLATE = 824
|
||||
const MARGIN = (CANVAS - PLATE) / 2
|
||||
const targetW = PLATE * 0.62
|
||||
const scale = targetW / logo.width
|
||||
const drawnW = logo.width * scale
|
||||
const drawnH = logo.height * scale
|
||||
const tx = MARGIN + (PLATE - drawnW) / 2
|
||||
const ty = MARGIN + (PLATE - drawnH) / 2
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${CANVAS}" height="${CANVAS}" viewBox="0 0 ${CANVAS} ${CANVAS}">
|
||||
<rect x="${MARGIN}" y="${MARGIN}" width="${PLATE}" height="${PLATE}" rx="185" ry="185" fill="${bg}"/>
|
||||
<g transform="translate(${tx.toFixed(2)},${ty.toFixed(2)}) scale(${scale.toFixed(6)})">${logo.inner}</g>
|
||||
</svg>`
|
||||
}
|
||||
|
||||
/**
|
||||
* Tray glyph: the logo centered (with padding) inside a 32px square canvas.
|
||||
* tray.ts loads it with scaleFactor 2, so it shows at ~16pt on retina menu
|
||||
* bars. Padding keeps the wide/landscape mark from touching the canvas edges.
|
||||
*/
|
||||
function trayLogoSvg(fill) {
|
||||
const CANVAS = 32 // tray asset is 32x32 px
|
||||
// Glyph height inside the 32px canvas — the single knob for tray size.
|
||||
// Bump up for a larger glyph, down for a smaller one.
|
||||
const targetH = 20
|
||||
const scale = targetH / logo.height
|
||||
const drawnW = logo.width * scale
|
||||
const drawnH = logo.height * scale
|
||||
const tx = (CANVAS - drawnW) / 2
|
||||
const ty = (CANVAS - drawnH) / 2
|
||||
const inner = fill ? logo.inner.split(LOGO_FILL).join(fill) : logo.inner
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${CANVAS}" height="${CANVAS}" viewBox="0 0 ${CANVAS} ${CANVAS}">
|
||||
<g transform="translate(${tx.toFixed(2)},${ty.toFixed(2)}) scale(${scale.toFixed(6)})">${inner}</g>
|
||||
</svg>`
|
||||
}
|
||||
|
||||
function svgToPng(svg, name, extraArgs = []) {
|
||||
const svgPath = path.join(tmpDir, `${name}.svg`)
|
||||
const pngPath = path.join(tmpDir, `${name}.png`)
|
||||
fs.writeFileSync(svgPath, svg)
|
||||
rsvg([...extraArgs, '-o', pngPath, svgPath])
|
||||
return pngPath
|
||||
}
|
||||
|
||||
function b64(pngPath) {
|
||||
return fs.readFileSync(pngPath).toString('base64')
|
||||
}
|
||||
|
||||
/** Wrap a PNG buffer in a minimal (PNG-payload) .ico container. */
|
||||
function pngToIco(pngBuffer) {
|
||||
const header = Buffer.alloc(6)
|
||||
header.writeUInt16LE(0, 0) // reserved
|
||||
header.writeUInt16LE(1, 2) // type: icon
|
||||
header.writeUInt16LE(1, 4) // image count
|
||||
const entry = Buffer.alloc(16)
|
||||
entry.writeUInt8(0, 0) // width (0 => 256)
|
||||
entry.writeUInt8(0, 1) // height (0 => 256)
|
||||
entry.writeUInt8(0, 2) // palette
|
||||
entry.writeUInt8(0, 3) // reserved
|
||||
entry.writeUInt16LE(1, 4) // color planes
|
||||
entry.writeUInt16LE(32, 6) // bits per pixel
|
||||
entry.writeUInt32LE(pngBuffer.length, 8) // size of image data
|
||||
entry.writeUInt32LE(6 + 16, 12) // offset to image data
|
||||
return Buffer.concat([header, entry, pngBuffer])
|
||||
}
|
||||
|
||||
function buildAppIcons() {
|
||||
// 1024 master for the normal app icon.
|
||||
const masterPng = svgToPng(squareIconSvg(BRAND_BLUE), 'app-normal', ['-w', '1024', '-h', '1024'])
|
||||
fs.mkdirSync(BUILD_DIR, { recursive: true })
|
||||
fs.copyFileSync(masterPng, path.join(BUILD_DIR, 'icon.png'))
|
||||
|
||||
// .icns via an .iconset downscaled with sips.
|
||||
const iconset = path.join(tmpDir, 'icon.iconset')
|
||||
fs.mkdirSync(iconset, { recursive: true })
|
||||
const variants = [
|
||||
[16, 'icon_16x16.png'],
|
||||
[32, 'icon_16x16@2x.png'],
|
||||
[32, 'icon_32x32.png'],
|
||||
[64, 'icon_32x32@2x.png'],
|
||||
[128, 'icon_128x128.png'],
|
||||
[256, 'icon_128x128@2x.png'],
|
||||
[256, 'icon_256x256.png'],
|
||||
[512, 'icon_256x256@2x.png'],
|
||||
[512, 'icon_512x512.png'],
|
||||
[1024, 'icon_512x512@2x.png'],
|
||||
]
|
||||
for (const [size, file] of variants) {
|
||||
const dest = path.join(iconset, file)
|
||||
execFileSync('sips', ['-z', String(size), String(size), masterPng, '--out', dest], {
|
||||
stdio: 'ignore',
|
||||
})
|
||||
}
|
||||
execFileSync('iconutil', ['-c', 'icns', iconset, '-o', path.join(BUILD_DIR, 'icon.icns')])
|
||||
|
||||
// .ico from a 256px render.
|
||||
const ico256 = svgToPng(squareIconSvg(BRAND_BLUE), 'app-ico', ['-w', '256', '-h', '256'])
|
||||
fs.writeFileSync(path.join(BUILD_DIR, 'icon.ico'), pngToIco(fs.readFileSync(ico256)))
|
||||
}
|
||||
|
||||
function buildMainProcessIcons() {
|
||||
// Tray icons are 32x32 px and loaded @2x in tray.ts (=> ~16pt on retina).
|
||||
// Glyph size is controlled by targetH in trayLogoSvg(), not by an rsvg -h.
|
||||
const trayNormal = b64(svgToPng(trayLogoSvg(), 'tray-normal'))
|
||||
const trayDanger = b64(svgToPng(trayLogoSvg(DANGER_RED), 'tray-danger'))
|
||||
|
||||
// Dock / window icons: logo on the brand plate (normal) and danger plate.
|
||||
const appNormal = b64(svgToPng(squareIconSvg(BRAND_BLUE), 'dock-normal', ['-w', '512', '-h', '512']))
|
||||
const appDanger = b64(svgToPng(squareIconSvg(DANGER_RED), 'dock-danger', ['-w', '512', '-h', '512']))
|
||||
|
||||
// Windows taskbar overlay: a small danger badge (status cue, not the logo).
|
||||
const overlaySvg = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<circle cx="16" cy="16" r="15" fill="${DANGER_RED}"/>
|
||||
<rect x="14" y="7" width="4" height="12" rx="2" fill="#ffffff"/>
|
||||
<circle cx="16" cy="24" r="2.4" fill="#ffffff"/>
|
||||
</svg>`
|
||||
const overlay = b64(svgToPng(overlaySvg, 'win-overlay'))
|
||||
|
||||
const banner = `// AUTO-GENERATED from public/logo.svg by scripts/generate-brand-icons.cjs.
|
||||
// Do not edit by hand — run \`node scripts/generate-brand-icons.cjs\` instead.
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore-start */`
|
||||
const body = [
|
||||
banner,
|
||||
'',
|
||||
'// macOS menu-bar template silhouette (rendered @2x).',
|
||||
`export const TRAY_ICON_BASE64 =\n '${trayNormal}'`,
|
||||
'',
|
||||
'// Tray icon shown when accounts have health issues (rendered @2x, not a template).',
|
||||
`export const TRAY_DANGER_ICON_BASE64 =\n '${trayDanger}'`,
|
||||
'',
|
||||
'// Dock / window icon — logo on the brand-blue plate.',
|
||||
`export const APP_ICON_NORMAL_BASE64 =\n '${appNormal}'`,
|
||||
'',
|
||||
'// Dock / window icon — logo on a danger plate when issues exist.',
|
||||
`export const APP_ICON_DANGER_BASE64 =\n '${appDanger}'`,
|
||||
'',
|
||||
'// Small Windows taskbar overlay badge signalling account health issues.',
|
||||
`export const WINDOWS_OVERLAY_ICON_BASE64 =\n '${overlay}'`,
|
||||
'/* prettier-ignore-end */',
|
||||
'',
|
||||
].join('\n')
|
||||
fs.writeFileSync(OUT_TS, body)
|
||||
}
|
||||
|
||||
try {
|
||||
buildAppIcons()
|
||||
buildMainProcessIcons()
|
||||
console.log('✓ build/icon.icns, build/icon.png, build/icon.ico')
|
||||
console.log('✓ src/main/brand-icons.ts')
|
||||
} finally {
|
||||
cleanup()
|
||||
}
|
||||
@@ -2,6 +2,11 @@ import { nativeImage } from 'electron/common'
|
||||
import type { BrowserWindow as ElectronBrowserWindow } from 'electron/main'
|
||||
import { app } from 'electron/main'
|
||||
|
||||
import {
|
||||
APP_ICON_DANGER_BASE64,
|
||||
APP_ICON_NORMAL_BASE64,
|
||||
WINDOWS_OVERLAY_ICON_BASE64,
|
||||
} from './brand-icons'
|
||||
import { createRuntimeSnapshot } from './runtime-snapshot'
|
||||
import { updateTrayIssueIndicator } from './tray'
|
||||
|
||||
@@ -11,12 +16,6 @@ type AppIndicatorImage = ReturnType<typeof nativeImage.createFromBuffer>
|
||||
|
||||
const HEALTH_ISSUE_AUTH_STATES = new Set(['expired', 'revoked', 'challenge_required'])
|
||||
const ISSUE_INDICATOR_REFRESH_MS = 15_000
|
||||
const APP_ICON_NORMAL_BASE64 =
|
||||
'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'
|
||||
const APP_ICON_DANGER_BASE64 =
|
||||
'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'
|
||||
const WINDOWS_OVERLAY_ICON_BASE64 =
|
||||
'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'
|
||||
|
||||
let refreshTimer: ReturnType<typeof setInterval> | null = null
|
||||
const appIconCache = new Map<AppIconTone, AppIndicatorImage>()
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2,28 +2,27 @@ import { nativeImage } from 'electron/common'
|
||||
import type { Tray as ElectronTray } from 'electron/main'
|
||||
import { Menu, Tray, app } from 'electron/main'
|
||||
|
||||
import { TRAY_DANGER_ICON_BASE64, TRAY_ICON_BASE64 } from './brand-icons'
|
||||
|
||||
type TrayIconImage = ReturnType<typeof nativeImage.createFromBuffer>
|
||||
|
||||
// The tray PNGs are generated at 2x (see scripts/generate-brand-icons.cjs), so
|
||||
// they stay crisp on retina menu bars.
|
||||
const TRAY_ICON_SCALE_FACTOR = 2
|
||||
|
||||
let tray: ElectronTray | null = null
|
||||
let normalTrayIcon: TrayIconImage | null = null
|
||||
let dangerTrayIcon: TrayIconImage | null = null
|
||||
|
||||
const TRAY_ICON_BASE64 =
|
||||
'iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAAnUlEQVR4nN2UwQmAMAxF360Dee8I' +
|
||||
'LuNIxT2cpIv0oJcIEqJGMSIG/qX8fn5/msDfqgcKUIEmqHLW3xHsgAmYTzAJ1+2yOURXNI/77qLo' +
|
||||
'VvzQufX8EchAEmQ5s2LZjUCThwMTg8E3IymG07PSzotFqoqUHcJZ3akWSTctOYST0cT3hMOiCGte' +
|
||||
'2HcjakCIHGmiltDW+eNrU7t/dNF/txaahq+VoZ+VygAAAABJRU5ErkJggg=='
|
||||
|
||||
const TRAY_DANGER_ICON_BASE64 =
|
||||
'iVBORw0KGgoAAAANSUhEUgAAABYAAAAWCAYAAADEtGw7AAAABmJLR0QA/wD/AP+gvaeTAAACSUlEQVQ4jd2Uz04TURTGf2dmkG5spwU3mOgKXoCFGlwM0qZpDYEFvoLEPyuN6F4TTYxLWPAKaqILxKZtmMSQ6CvAygUuhNIpLpowt3NcaMt0GkASN/qtbs6533e+e++5B/41yEnJRqmUlsPDMiLTAmMACt9Q3dBz5z6MrK8fnElYJyeHmrncI1FdAjLHcAMReZEJw1fi++ZU4abnudj2W0RunHSaGOoYs5D1/SAetJJOcZw3ZxAFmMFxXqvnOccKN7PZh8BMgriLyFNVnVXVWeAZsJvYk2/Z9oN4oHcVjVIpbYXhV8CN5T9Ztj2fqVT246RWsZiLOp33wPVYODDt9qULm5s/+hyLMTcTot9leHiuK9oqFMZbhcI4QKZS2bdV54G92H53KJUqD16Fqpc43qq7ttYECPL5lUh1K1LdCvL5ZYB0vd4Q1dU+hkhP48jx7z49qqNfuk4V7vTicLfrPLLtzwnOxUHHfxk9YRXZiSdE5ApAplrdFliJ2VrOVKvbAFanczXB6WkcOY4iP1F0sVUs5gDcWu2eJTJhiUxk6/X7AAeeN6oiiwnORq9Id3FMu23aqnPper0RZx943mjHcd4BU7Fw07Tbl7vt1vel9/P5JwLPEy72RHW1+1CW6jVVvQ2M9F0DLLm12ssBxwDqeU7gOB8Z/H2noeYaU4oPo76uEN83GLMA1P5YUqSKMbeSE26g3bK+H7jGlET1MRAk8zE0BZbcMCwnJxucMuh3p6bOD6VS5ciypiWKxn4ZlB1gI2y317sP9X/gJ6o+6LM9+rGLAAAAAElFTkSuQmCC'
|
||||
|
||||
function createTrayIcon(tone: 'normal' | 'danger'): TrayIconImage {
|
||||
if (tone === 'danger') {
|
||||
if (dangerTrayIcon) {
|
||||
return dangerTrayIcon
|
||||
}
|
||||
|
||||
const icon = nativeImage.createFromBuffer(Buffer.from(TRAY_DANGER_ICON_BASE64, 'base64'))
|
||||
const icon = nativeImage.createFromBuffer(Buffer.from(TRAY_DANGER_ICON_BASE64, 'base64'), {
|
||||
scaleFactor: TRAY_ICON_SCALE_FACTOR,
|
||||
})
|
||||
icon.setTemplateImage(false)
|
||||
dangerTrayIcon = icon.isEmpty() ? createTrayIcon('normal') : icon
|
||||
return dangerTrayIcon
|
||||
@@ -34,7 +33,7 @@ function createTrayIcon(tone: 'normal' | 'danger'): TrayIconImage {
|
||||
}
|
||||
|
||||
const buffer = Buffer.from(TRAY_ICON_BASE64, 'base64')
|
||||
const icon = nativeImage.createFromBuffer(buffer)
|
||||
const icon = nativeImage.createFromBuffer(buffer, { scaleFactor: TRAY_ICON_SCALE_FACTOR })
|
||||
if (icon.isEmpty()) {
|
||||
return nativeImage.createEmpty()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
// Type for the `@brand-logo` alias (electron.vite.config.ts → public/logo.svg).
|
||||
// Kept in its own ambient (non-module) declaration file so vue-tsc resolves the
|
||||
// alias the same way Vite does at build time.
|
||||
declare module '@brand-logo' {
|
||||
const src: string
|
||||
export default src
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { RouterLink, RouterView, useRoute } from 'vue-router'
|
||||
|
||||
import brandLogo from '@brand-logo'
|
||||
import { useClientRelease } from '../composables/useClientRelease'
|
||||
import { useDesktopRuntime } from '../composables/useDesktopRuntime'
|
||||
import { useDesktopSession } from '../composables/useDesktopSession'
|
||||
@@ -232,8 +233,7 @@ watch(
|
||||
<aside class="sidebar">
|
||||
<div class="brand-block">
|
||||
<div class="brand-mark">
|
||||
<span class="brand-ring"></span>
|
||||
<span class="brand-core"></span>
|
||||
<img :src="brandLogo" alt="省心推" class="brand-mark-img" />
|
||||
</div>
|
||||
<div>
|
||||
<p class="eyebrow">Workspace Cockpit</p>
|
||||
@@ -444,30 +444,19 @@ watch(
|
||||
}
|
||||
|
||||
.brand-mark {
|
||||
position: relative;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 11px;
|
||||
background: linear-gradient(135deg, #1677ff 0%, #4096ff 100%);
|
||||
box-shadow: 0 6px 14px rgba(22, 119, 255, 0.28);
|
||||
}
|
||||
|
||||
.brand-ring,
|
||||
.brand-core {
|
||||
position: absolute;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.brand-ring {
|
||||
inset: 0;
|
||||
border: 1px solid #91caff;
|
||||
background: #e6f4ff;
|
||||
}
|
||||
|
||||
.brand-core {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: #1677ff;
|
||||
box-shadow: 0 2px 6px rgba(22, 119, 255, 0.4);
|
||||
.brand-mark-img {
|
||||
width: 62%;
|
||||
height: 62%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
|
||||
@@ -2,6 +2,7 @@ import './console-guard'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
|
||||
import brandLogo from '@brand-logo'
|
||||
import type { DesktopObservedRequest } from '../shared/network-debug'
|
||||
import App from './App.vue'
|
||||
import { router } from './routes'
|
||||
@@ -9,6 +10,13 @@ import { router } from './routes'
|
||||
import 'ant-design-vue/dist/reset.css'
|
||||
import '../../../../packages/ui-shared/src/tokens/index.css'
|
||||
|
||||
// Point the favicon at the real brand mark; the inline SVG in index.html is a
|
||||
// pre-mount fallback.
|
||||
const favicon = document.querySelector<HTMLLinkElement>('link[rel="icon"]')
|
||||
if (favicon) {
|
||||
favicon.href = brandLogo
|
||||
}
|
||||
|
||||
const applyTheme = (prefersDark: boolean) => {
|
||||
document.documentElement.dataset.theme = prefersDark ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||
import brandLogo from '@brand-logo'
|
||||
import { DEFAULT_API_BASE_URL, useDesktopSession } from '../composables/useDesktopSession'
|
||||
|
||||
const { apiBaseURL, error, pending, setApiBaseURL, login } = useDesktopSession()
|
||||
@@ -179,7 +180,7 @@ onBeforeUnmount(() => {
|
||||
<div class="auth-wrapper">
|
||||
<header class="brand-header">
|
||||
<div class="brand-logo">
|
||||
<span class="brand-core"></span>
|
||||
<img :src="brandLogo" alt="省心推" class="brand-logo-img" />
|
||||
</div>
|
||||
<h1 class="brand-name">省心推</h1>
|
||||
</header>
|
||||
@@ -448,21 +449,10 @@ onBeforeUnmount(() => {
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.brand-core {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0 2px 8px rgba(22, 119, 255, 0.25);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.brand-core::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 7px;
|
||||
border-radius: 3px;
|
||||
background: linear-gradient(135deg, #1677ff, #69b1ff);
|
||||
.brand-logo-img {
|
||||
width: 60%;
|
||||
height: 60%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
|
||||
@@ -70,9 +70,19 @@ config_file_path() {
|
||||
done
|
||||
}
|
||||
|
||||
read_object_storage_provider() {
|
||||
config_file_path_in_dir() {
|
||||
local dir="$1" base="$2"
|
||||
for file in "${dir}/${base}.yaml" "${dir}/${base}.yml"; do
|
||||
if [[ -f "${file}" ]]; then
|
||||
printf '%s' "${file}"
|
||||
return
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
read_object_storage_provider_from_files() {
|
||||
local provider="minio" file
|
||||
for file in "$(config_file_path config)" "$(config_file_path config.local)"; do
|
||||
for file in "$@"; do
|
||||
if [[ -z "${file}" || ! -f "${file}" ]]; then
|
||||
continue
|
||||
fi
|
||||
@@ -110,8 +120,19 @@ read_object_storage_provider() {
|
||||
printf '%s' "$(printf '%s' "${provider}" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
|
||||
}
|
||||
|
||||
use_minio_for_object_storage() {
|
||||
case "$(read_object_storage_provider)" in
|
||||
read_object_storage_provider() {
|
||||
read_object_storage_provider_from_files "$(config_file_path config)" "$(config_file_path config.local)"
|
||||
}
|
||||
|
||||
read_k3s_object_storage_provider() {
|
||||
read_object_storage_provider_from_files \
|
||||
"$(config_file_path_in_dir "${SCRIPT_DIR}/k3s/config" config)" \
|
||||
"$(config_file_path config.local)"
|
||||
}
|
||||
|
||||
use_minio_provider() {
|
||||
local provider="$1"
|
||||
case "${provider}" in
|
||||
""|minio|mino)
|
||||
return 0
|
||||
;;
|
||||
@@ -124,6 +145,10 @@ use_minio_for_object_storage() {
|
||||
esac
|
||||
}
|
||||
|
||||
use_minio_for_object_storage() {
|
||||
use_minio_provider "$(read_object_storage_provider)"
|
||||
}
|
||||
|
||||
sync_k3s_config() {
|
||||
mkdir -p "${SCRIPT_DIR}/k3s/config"
|
||||
for name in config ops-config prompts; do
|
||||
@@ -131,6 +156,9 @@ sync_k3s_config() {
|
||||
if [[ "${name}" == "prompts" ]]; then
|
||||
output="prompts.yml"
|
||||
fi
|
||||
if [[ -f "${SCRIPT_DIR}/k3s/config/${output}" ]]; then
|
||||
continue
|
||||
fi
|
||||
local source=""
|
||||
for candidate in "${SCRIPT_DIR}/${name}.yaml" "${SCRIPT_DIR}/${name}.yml"; do
|
||||
if [[ -f "${candidate}" ]]; then
|
||||
@@ -153,6 +181,18 @@ prepare_k3s_runtime_overlay() {
|
||||
cp "$(config_file_path config.local)" "${runtime_dir}/config.local.yaml"
|
||||
cp "$(config_file_path ops-config.local)" "${runtime_dir}/ops-config.local.yaml"
|
||||
|
||||
cat > "${runtime_dir}/secret.env" <<EOF
|
||||
POSTGRES_PASSWORD=$(env_value POSTGRES_PASSWORD geo_dev)
|
||||
RABBITMQ_DEFAULT_PASS=$(env_value RABBITMQ_DEFAULT_PASS geo_dev)
|
||||
MINIO_ROOT_USER=$(env_value MINIO_ROOT_USER minioadmin)
|
||||
MINIO_ROOT_PASSWORD=$(env_value MINIO_ROOT_PASSWORD minioadmin)
|
||||
JWT_SECRET=$(env_value JWT_SECRET geo-web-change-me-in-production)
|
||||
OPS_JWT_SECRET=$(env_value OPS_JWT_SECRET geo-ops-change-me-in-production)
|
||||
OPS_DEFAULT_ADMIN_PASSWORD=$(env_value OPS_DEFAULT_ADMIN_PASSWORD Aa123456)
|
||||
SCHEDULER_INTERNAL_METRICS_TOKEN=$(env_value SCHEDULER_INTERNAL_METRICS_TOKEN geo-metrics-token)
|
||||
BROWSER_FETCH_TOKEN=$(env_value BROWSER_FETCH_TOKEN geo-browser-fetch-change-me)
|
||||
EOF
|
||||
|
||||
cat > "${runtime_dir}/kustomization.yaml" <<EOF
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
@@ -163,6 +203,8 @@ resources:
|
||||
secretGenerator:
|
||||
- name: geo-rankly-app-secret
|
||||
behavior: merge
|
||||
envs:
|
||||
- secret.env
|
||||
files:
|
||||
- config.local.yaml=config.local.yaml
|
||||
- ops-config.local.yaml=ops-config.local.yaml
|
||||
@@ -234,7 +276,11 @@ deploy_k3s() {
|
||||
ensure_local_config "config.local.yaml" "config.local.yaml.example"
|
||||
ensure_local_config "ops-config.local.yaml" "ops-config.local.yaml.example"
|
||||
|
||||
if ! use_minio_for_object_storage; then
|
||||
sync_k3s_config
|
||||
local object_storage_provider
|
||||
object_storage_provider="$(read_k3s_object_storage_provider)"
|
||||
|
||||
if ! use_minio_provider "${object_storage_provider}"; then
|
||||
kustomize_dir="${SCRIPT_DIR}/k3s-aliyun-oss"
|
||||
minio_enabled="false"
|
||||
fi
|
||||
@@ -252,12 +298,11 @@ deploy_k3s() {
|
||||
fi
|
||||
|
||||
echo "==> [2/3] 应用 k3s 清单..."
|
||||
sync_k3s_config
|
||||
kustomize_dir="$(prepare_k3s_runtime_overlay "${kustomize_dir}")"
|
||||
if [[ "${minio_enabled}" == "true" ]]; then
|
||||
echo " object_storage.provider=$(read_object_storage_provider),将部署 MinIO。"
|
||||
echo " object_storage.provider=${object_storage_provider},将部署 MinIO。"
|
||||
else
|
||||
echo " object_storage.provider=$(read_object_storage_provider),将跳过 MinIO。"
|
||||
echo " object_storage.provider=${object_storage_provider},将跳过 MinIO。"
|
||||
${kubectl_cmd} -n geo-rankly delete statefulset minio --ignore-not-found
|
||||
${kubectl_cmd} -n geo-rankly delete service minio --ignore-not-found
|
||||
${kubectl_cmd} -n geo-rankly delete job minio-init --ignore-not-found
|
||||
|
||||
Reference in New Issue
Block a user