/** * 站点配置 store:公共配置 + Logo/favicon(需求④)+ 背景/页脚/通知(⑤⑥⑦)+ 策略范围(⑧)。 * 远程 URL 默认值已删除:Logo/favicon 使用本地打包资源兜底(config 自定义仍优先)。 */ import { defineStore } from 'pinia' import { request } from '@/api/http' import { paths } from '@/api/paths' import { pick } from '@/utils/format' import logoLocal from '@/assets/brand/logo.svg' import faviconLocal from '@/assets/brand/favicon.png' /** 需求④:默认 Logo(导航栏)与 favicon(浏览器标签)——本地打包资源,无远程依赖 */ export const DEFAULT_LOGO_URL = logoLocal export const DEFAULT_FAVICON_URL = faviconLocal export const DEFAULT_SITE_NAME = '文件快传' export interface ConfigState { loaded: boolean loading: boolean siteName: string siteDomain: string description: string explain: string uploadSize: number allowedFileTypes: string[] expireStyle: string[] enableChunk: boolean openUpload: boolean notifyEnabled: boolean notifyTitle: string notifyContent: string logoUrl: string faviconUrl: string backgroundUrl: string footerText: string footerBeian: string maxFileSize: number maxSaveSeconds: number maxSaveCount: number uploadCount: number uploadMinute: number } function toBool(v: unknown, fallback = true): boolean { if (v === undefined || v === null) return fallback if (typeof v === 'boolean') return v if (typeof v === 'number') return v !== 0 return String(v) !== '0' && String(v) !== 'false' && String(v) !== '' } function toList(v: unknown): string[] { if (Array.isArray(v)) return v.map((x) => String(x).trim()).filter(Boolean) if (typeof v === 'string') { return v .split(',') .map((x) => x.trim()) .filter(Boolean) } return [] } function toNum(v: unknown, fallback: number): number { const n = Number(v) return Number.isFinite(n) ? n : fallback } export const useConfigStore = defineStore('config', { state: (): ConfigState => ({ loaded: false, loading: false, siteName: DEFAULT_SITE_NAME, siteDomain: '', description: '', explain: '', uploadSize: 10 * 1024 * 1024, allowedFileTypes: [], expireStyle: ['day', 'hour', 'minute', 'forever', 'count'], enableChunk: false, openUpload: true, notifyEnabled: false, notifyTitle: '', notifyContent: '', logoUrl: '', faviconUrl: '', backgroundUrl: '', footerText: '', footerBeian: '', maxFileSize: 0, maxSaveSeconds: 0, maxSaveCount: 0, uploadCount: 0, uploadMinute: 0, }), getters: { /** 需求④:自定义优先,本地打包资源兜底 */ displayLogoUrl: (s) => (s.logoUrl?.trim() ? s.logoUrl : DEFAULT_LOGO_URL), displayFaviconUrl: (s) => (s.faviconUrl?.trim() ? s.faviconUrl : DEFAULT_FAVICON_URL), displayName: (s) => (s.siteName?.trim() ? s.siteName : DEFAULT_SITE_NAME), /** v3.1:分享链接基础地址——配置了对外域名用之,否则当前访问地址 */ shareLinkBase: (s): string => (s.siteDomain?.trim() ? s.siteDomain.trim().replace(/\/$/, '') : location.origin), /** ⑧ 存储策略:max_file_size>0 时优先,否则回落 uploadSize */ effectiveMaxFileSize(): number { return this.maxFileSize > 0 ? this.maxFileSize : this.uploadSize }, }, actions: { /** 拉取公共配置;失败静默使用默认值(未部署后端时前端仍可浏览) */ async load(): Promise { this.loading = true try { const raw = await request>(paths.publicConfig, { timeout: 8000 }) const cfg = (pick>(raw, ['config']) ?? raw) as Record this.siteName = String(pick(cfg, ['name', 'site_name', 'siteName']) ?? DEFAULT_SITE_NAME) // v3.1:站点对外域名(空=当前访问地址) this.siteDomain = String(pick(cfg, ['site_domain', 'siteDomain']) ?? '').trim() this.description = String(pick(cfg, ['description']) ?? '') this.explain = String(pick(cfg, ['explain', 'page_explain']) ?? '') this.uploadSize = toNum(pick(cfg, ['uploadSize', 'upload_size']), 10 * 1024 * 1024) this.allowedFileTypes = toList(pick(cfg, ['allowedFileTypes', 'allowed_file_types'])) const styles = toList(pick(cfg, ['expireStyle', 'expire_style'])) if (styles.length) this.expireStyle = styles this.enableChunk = toBool(pick(cfg, ['enableChunk', 'enable_chunk']), false) this.openUpload = toBool(pick(cfg, ['openUpload', 'open_upload']), true) this.notifyTitle = String(pick(cfg, ['notify_title', 'notifyTitle']) ?? '') this.notifyContent = String(pick(cfg, ['notify_content', 'notifyContent']) ?? '') // 需求⑦:通知开关(1/0) this.notifyEnabled = toBool(pick(cfg, ['notify_enabled', 'notifyEnabled']), false) // 需求⑤:背景图 this.backgroundUrl = String(pick(cfg, ['background_url', 'backgroundUrl']) ?? '').trim() // 需求⑥:页脚 this.footerText = String(pick(cfg, ['footer_text', 'footerText']) ?? '') this.footerBeian = String(pick(cfg, ['footer_beian', 'footerBeian']) ?? '') // 需求⑧:保存策略与上传频率 this.maxFileSize = toNum(pick(cfg, ['max_file_size', 'maxFileSize', 'maxFileSize']), 0) this.maxSaveSeconds = toNum(pick(cfg, ['max_save_seconds', 'maxSaveSeconds']), 0) this.maxSaveCount = toNum(pick(cfg, ['max_save_count', 'maxSaveCount']), 0) this.uploadCount = toNum(pick(cfg, ['uploadCount', 'upload_count']), 0) this.uploadMinute = toNum(pick(cfg, ['uploadMinute', 'upload_minute']), 0) // 需求④:运行时优先读 config 的 logo_url/favicon_url,空值回退本地默认 this.logoUrl = String(pick(cfg, ['logo_url', 'logoUrl']) ?? '').trim() this.faviconUrl = String(pick(cfg, ['favicon_url', 'faviconUrl']) ?? '').trim() this.loaded = true this.applyToDocument() } catch { // 保持默认值 } finally { this.loading = false } }, /** 将站点名与 favicon 应用到 document(管理端保存配置后调用即可全站生效) */ applyToDocument(): void { let link = document.querySelector('link[rel="icon"]') if (!link) { link = document.createElement('link') link.rel = 'icon' document.head.appendChild(link) } link.href = this.displayFaviconUrl }, }, })