release: prepare v0.1.131
This commit is contained in:
@@ -187,6 +187,7 @@ import { useAdminSettingsStore, useAppStore, useAuthStore, useOnboardingStore }
|
||||
import VersionBadge from '@/components/common/VersionBadge.vue'
|
||||
import { sanitizeSvg } from '@/utils/sanitize'
|
||||
import { FeatureFlags, makeSidebarFlag } from '@/utils/featureFlags'
|
||||
import { getCustomMenuRoute, isSidebarMenuPlacement, normalizeCustomMenuItems } from '@/utils/custom-menu'
|
||||
|
||||
interface NavItem {
|
||||
path: string
|
||||
@@ -693,7 +694,7 @@ function buildSelfNavItems(withDashboard: boolean): NavItem[] {
|
||||
{ path: '/affiliate', label: t('nav.affiliate'), icon: UsersIcon, hideInSimpleMode: true, featureFlag: flagAffiliate },
|
||||
{ path: '/profile', label: t('nav.profile'), icon: UserIcon },
|
||||
...customMenuItemsForUser.value.map((item): NavItem => ({
|
||||
path: `/custom/${item.id}`,
|
||||
path: getCustomMenuRoute(item.id),
|
||||
label: item.label,
|
||||
icon: null,
|
||||
iconSvg: item.icon_svg,
|
||||
@@ -718,15 +719,15 @@ const personalNavItems = computed((): NavItem[] => finalizeNav(buildSelfNavItems
|
||||
|
||||
// Custom menu items filtered by visibility
|
||||
const customMenuItemsForUser = computed(() => {
|
||||
const items = appStore.cachedPublicSettings?.custom_menu_items ?? []
|
||||
const items = normalizeCustomMenuItems(appStore.cachedPublicSettings?.custom_menu_items)
|
||||
return items
|
||||
.filter((item) => item.visibility === 'user')
|
||||
.filter((item) => item.visibility === 'user' && isSidebarMenuPlacement(item))
|
||||
.sort((a, b) => a.sort_order - b.sort_order)
|
||||
})
|
||||
|
||||
const customMenuItemsForAdmin = computed(() => {
|
||||
return adminSettingsStore.customMenuItems
|
||||
.filter((item) => item.visibility === 'admin')
|
||||
return normalizeCustomMenuItems(adminSettingsStore.customMenuItems)
|
||||
.filter((item) => item.visibility === 'admin' && isSidebarMenuPlacement(item))
|
||||
.sort((a, b) => a.sort_order - b.sort_order)
|
||||
})
|
||||
|
||||
@@ -795,7 +796,7 @@ const adminNavItems = computed((): NavItem[] => {
|
||||
filtered.push({ path: '/admin/settings', label: t('nav.settings'), icon: CogIcon })
|
||||
}
|
||||
for (const cm of customMenuItemsForAdmin.value) {
|
||||
filtered.push({ path: `/custom/${cm.id}`, label: cm.label, icon: null, iconSvg: cm.icon_svg })
|
||||
filtered.push({ path: getCustomMenuRoute(cm.id), label: cm.label, icon: null, iconSvg: cm.icon_svg })
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
@@ -804,7 +805,7 @@ const adminNavItems = computed((): NavItem[] => {
|
||||
visible.push({ path: '/admin/settings', label: t('nav.settings'), icon: CogIcon })
|
||||
}
|
||||
for (const cm of customMenuItemsForAdmin.value) {
|
||||
visible.push({ path: `/custom/${cm.id}`, label: cm.label, icon: null, iconSvg: cm.icon_svg })
|
||||
visible.push({ path: getCustomMenuRoute(cm.id), label: cm.label, icon: null, iconSvg: cm.icon_svg })
|
||||
}
|
||||
return visible
|
||||
})
|
||||
|
||||
@@ -5457,7 +5457,7 @@ export default {
|
||||
},
|
||||
customMenu: {
|
||||
title: 'Custom Menu Pages',
|
||||
description: 'Add custom iframe pages to the sidebar navigation. Each page can be visible to regular users or administrators.',
|
||||
description: 'Add custom pages to site navigation. Each page can be visible to regular users or administrators, and can be shown in the sidebar, the home header, or both.',
|
||||
itemLabel: 'Menu Item #{n}',
|
||||
name: 'Menu Name',
|
||||
namePlaceholder: 'e.g. Help Center',
|
||||
@@ -5471,6 +5471,10 @@ export default {
|
||||
visibility: 'Visible To',
|
||||
visibilityUser: 'Regular Users',
|
||||
visibilityAdmin: 'Administrators',
|
||||
placement: 'Placement',
|
||||
placementSidebar: 'Sidebar only',
|
||||
placementHomeHeader: 'Home header only',
|
||||
placementBoth: 'Sidebar + Home header',
|
||||
add: 'Add Menu Item',
|
||||
remove: 'Remove',
|
||||
moveUp: 'Move Up',
|
||||
|
||||
@@ -5618,7 +5618,7 @@ export default {
|
||||
},
|
||||
customMenu: {
|
||||
title: '自定义菜单页面',
|
||||
description: '添加自定义 iframe 页面到侧边栏导航。每个页面可以设置为普通用户或管理员可见。',
|
||||
description: '添加自定义页面到站点导航。每个页面可以设置为普通用户或管理员可见,并指定显示在侧边栏、首页头部,或同时显示。',
|
||||
itemLabel: '菜单项 #{n}',
|
||||
name: '菜单名称',
|
||||
namePlaceholder: '如:帮助中心',
|
||||
@@ -5632,6 +5632,10 @@ export default {
|
||||
visibility: '可见角色',
|
||||
visibilityUser: '普通用户',
|
||||
visibilityAdmin: '管理员',
|
||||
placement: '显示位置',
|
||||
placementSidebar: '仅侧边栏',
|
||||
placementHomeHeader: '仅首页头部',
|
||||
placementBoth: '侧边栏 + 首页头部',
|
||||
add: '添加菜单项',
|
||||
remove: '删除',
|
||||
moveUp: '上移',
|
||||
|
||||
@@ -2,6 +2,7 @@ import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { adminAPI } from '@/api'
|
||||
import type { CustomMenuItem } from '@/types'
|
||||
import { normalizeCustomMenuItems } from '@/utils/custom-menu'
|
||||
|
||||
export const useAdminSettingsStore = defineStore('adminSettings', () => {
|
||||
const loaded = ref(false)
|
||||
@@ -70,7 +71,7 @@ export const useAdminSettingsStore = defineStore('adminSettings', () => {
|
||||
opsQueryModeDefault.value = settings.ops_query_mode_default || 'auto'
|
||||
writeCachedString('ops_query_mode_default_cached', opsQueryModeDefault.value)
|
||||
|
||||
customMenuItems.value = Array.isArray(settings.custom_menu_items) ? settings.custom_menu_items : []
|
||||
customMenuItems.value = normalizeCustomMenuItems(settings.custom_menu_items)
|
||||
|
||||
paymentEnabled.value = paymentConfigResp.data?.enabled ?? false
|
||||
writeCachedBool('payment_enabled_cached', paymentEnabled.value)
|
||||
|
||||
@@ -7,6 +7,7 @@ import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import type { Toast, ToastType, PublicSettings } from '@/types'
|
||||
import { i18n } from '@/i18n'
|
||||
import { normalizeCustomMenuItems } from '@/utils/custom-menu'
|
||||
import {
|
||||
checkUpdates as checkUpdatesAPI,
|
||||
type VersionInfo,
|
||||
@@ -288,16 +289,20 @@ export const useAppStore = defineStore('app', () => {
|
||||
* Apply settings to store state (internal helper to avoid code duplication)
|
||||
*/
|
||||
function applySettings(config: PublicSettings): void {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.__APP_CONFIG__ = { ...config }
|
||||
const normalizedConfig: PublicSettings = {
|
||||
...config,
|
||||
custom_menu_items: normalizeCustomMenuItems(config.custom_menu_items)
|
||||
}
|
||||
cachedPublicSettings.value = config
|
||||
siteName.value = config.site_name || 'Sub2API'
|
||||
siteLogo.value = config.site_logo || ''
|
||||
siteVersion.value = config.version || ''
|
||||
contactInfo.value = config.contact_info || ''
|
||||
apiBaseUrl.value = config.api_base_url || ''
|
||||
docUrl.value = config.doc_url || ''
|
||||
if (typeof window !== 'undefined') {
|
||||
window.__APP_CONFIG__ = { ...normalizedConfig }
|
||||
}
|
||||
cachedPublicSettings.value = normalizedConfig
|
||||
siteName.value = normalizedConfig.site_name || 'Sub2API'
|
||||
siteLogo.value = normalizedConfig.site_logo || ''
|
||||
siteVersion.value = normalizedConfig.version || ''
|
||||
contactInfo.value = normalizedConfig.contact_info || ''
|
||||
apiBaseUrl.value = normalizedConfig.api_base_url || ''
|
||||
docUrl.value = normalizedConfig.doc_url || ''
|
||||
publicSettingsLoaded.value = true
|
||||
}
|
||||
|
||||
|
||||
@@ -163,6 +163,8 @@ export interface SendVerifyCodeResponse {
|
||||
countdown: number
|
||||
}
|
||||
|
||||
export type CustomMenuPlacement = 'sidebar' | 'home_header' | 'both'
|
||||
|
||||
export interface CustomMenuItem {
|
||||
id: string
|
||||
label: string
|
||||
@@ -170,6 +172,7 @@ export interface CustomMenuItem {
|
||||
url: string
|
||||
page_slug?: string
|
||||
visibility: 'user' | 'admin'
|
||||
placement?: CustomMenuPlacement
|
||||
sort_order: number
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import type { CustomMenuItem, CustomMenuPlacement } from '@/types'
|
||||
|
||||
export const DEFAULT_CUSTOM_MENU_PLACEMENT: CustomMenuPlacement = 'sidebar'
|
||||
|
||||
export function normalizeCustomMenuPlacement(
|
||||
value: unknown,
|
||||
): CustomMenuPlacement {
|
||||
if (value === 'home_header' || value === 'both') {
|
||||
return value
|
||||
}
|
||||
return DEFAULT_CUSTOM_MENU_PLACEMENT
|
||||
}
|
||||
|
||||
export function normalizeCustomMenuItem(item: CustomMenuItem): CustomMenuItem {
|
||||
return {
|
||||
...item,
|
||||
placement: normalizeCustomMenuPlacement(item.placement),
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeCustomMenuItems(
|
||||
items: CustomMenuItem[] | null | undefined,
|
||||
): CustomMenuItem[] {
|
||||
if (!Array.isArray(items)) {
|
||||
return []
|
||||
}
|
||||
return items.map((item) => normalizeCustomMenuItem(item))
|
||||
}
|
||||
|
||||
export function isSidebarMenuPlacement(
|
||||
item: Pick<CustomMenuItem, 'placement'>,
|
||||
): boolean {
|
||||
const placement = normalizeCustomMenuPlacement(item.placement)
|
||||
return placement === 'sidebar' || placement === 'both'
|
||||
}
|
||||
|
||||
export function isHomeHeaderMenuPlacement(
|
||||
item: Pick<CustomMenuItem, 'placement'>,
|
||||
): boolean {
|
||||
const placement = normalizeCustomMenuPlacement(item.placement)
|
||||
return placement === 'home_header' || placement === 'both'
|
||||
}
|
||||
|
||||
export function getCustomMenuRoute(id: string): string {
|
||||
return `/custom/${encodeURIComponent(id)}`
|
||||
}
|
||||
@@ -47,7 +47,26 @@
|
||||
</div>
|
||||
|
||||
<!-- Nav Actions -->
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center gap-2 sm:gap-3">
|
||||
<div
|
||||
v-if="homeHeaderMenuItems.length > 0"
|
||||
class="hidden items-center gap-1 lg:flex"
|
||||
>
|
||||
<router-link
|
||||
v-for="item in homeHeaderMenuItems"
|
||||
:key="item.id"
|
||||
:to="customMenuRoute(item.id)"
|
||||
class="inline-flex items-center gap-2 rounded-full border border-gray-200/70 bg-white/80 px-3 py-2 text-sm font-medium text-gray-700 transition-colors hover:border-primary-300 hover:text-primary-700 dark:border-dark-700/80 dark:bg-dark-900/70 dark:text-dark-100 dark:hover:border-primary-500/50 dark:hover:text-white"
|
||||
>
|
||||
<span
|
||||
v-if="item.icon_svg"
|
||||
class="flex h-4 w-4 items-center justify-center text-current"
|
||||
v-html="sanitizeSvg(item.icon_svg)"
|
||||
></span>
|
||||
<span>{{ item.label }}</span>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Language Switcher -->
|
||||
<LocaleSwitcher />
|
||||
|
||||
@@ -410,6 +429,12 @@ import { useI18n } from 'vue-i18n'
|
||||
import { useAuthStore, useAppStore } from '@/stores'
|
||||
import LocaleSwitcher from '@/components/common/LocaleSwitcher.vue'
|
||||
import Icon from '@/components/icons/Icon.vue'
|
||||
import { sanitizeSvg } from '@/utils/sanitize'
|
||||
import {
|
||||
getCustomMenuRoute,
|
||||
isHomeHeaderMenuPlacement,
|
||||
normalizeCustomMenuItems,
|
||||
} from '@/utils/custom-menu'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -422,6 +447,11 @@ const siteLogo = computed(() => appStore.cachedPublicSettings?.site_logo || appS
|
||||
const siteSubtitle = computed(() => appStore.cachedPublicSettings?.site_subtitle || 'AI API Gateway Platform')
|
||||
const docUrl = computed(() => appStore.cachedPublicSettings?.doc_url || appStore.docUrl || '')
|
||||
const homeContent = computed(() => appStore.cachedPublicSettings?.home_content || '')
|
||||
const homeHeaderMenuItems = computed(() =>
|
||||
normalizeCustomMenuItems(appStore.cachedPublicSettings?.custom_menu_items)
|
||||
.filter((item) => item.visibility === 'user' && isHomeHeaderMenuPlacement(item))
|
||||
.sort((a, b) => a.sort_order - b.sort_order)
|
||||
)
|
||||
|
||||
// Check if homeContent is a URL (for iframe display)
|
||||
const isHomeContentUrl = computed(() => {
|
||||
@@ -448,6 +478,10 @@ const userInitial = computed(() => {
|
||||
// Current year for footer
|
||||
const currentYear = computed(() => new Date().getFullYear())
|
||||
|
||||
function customMenuRoute(id: string) {
|
||||
return getCustomMenuRoute(id)
|
||||
}
|
||||
|
||||
// Toggle theme
|
||||
function toggleTheme() {
|
||||
isDark.value = !isDark.value
|
||||
|
||||
@@ -4408,6 +4408,26 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Placement -->
|
||||
<div>
|
||||
<label
|
||||
class="mb-1 block text-xs font-medium text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
{{ t("admin.settings.customMenu.placement") }}
|
||||
</label>
|
||||
<select v-model="item.placement" class="input text-sm">
|
||||
<option value="sidebar">
|
||||
{{ t("admin.settings.customMenu.placementSidebar") }}
|
||||
</option>
|
||||
<option value="home_header">
|
||||
{{ t("admin.settings.customMenu.placementHomeHeader") }}
|
||||
</option>
|
||||
<option value="both">
|
||||
{{ t("admin.settings.customMenu.placementBoth") }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- URL (full width) -->
|
||||
<div class="sm:col-span-2">
|
||||
<label
|
||||
@@ -6145,6 +6165,7 @@ import type {
|
||||
} from "@/api/admin/settings";
|
||||
import type {
|
||||
AdminGroup,
|
||||
CustomMenuPlacement,
|
||||
LoginAgreementDocument,
|
||||
NotifyEmailEntry,
|
||||
Proxy,
|
||||
@@ -6163,6 +6184,10 @@ import ProxySelector from "@/components/common/ProxySelector.vue";
|
||||
import ImageUpload from "@/components/common/ImageUpload.vue";
|
||||
import BackupSettings from "@/views/admin/BackupView.vue";
|
||||
import { useClipboard } from "@/composables/useClipboard";
|
||||
import {
|
||||
DEFAULT_CUSTOM_MENU_PLACEMENT,
|
||||
normalizeCustomMenuItems,
|
||||
} from "@/utils/custom-menu";
|
||||
import { affiliatesAPI, type AffiliateAdminEntry, type SimpleUser as AffiliateSimpleUser } from "@/api/admin/affiliates";
|
||||
import { extractApiErrorMessage, extractI18nErrorMessage } from "@/utils/apiError";
|
||||
import { useAppStore } from "@/stores";
|
||||
@@ -6491,6 +6516,7 @@ const form = reactive<SettingsForm>({
|
||||
icon_svg: string;
|
||||
url: string;
|
||||
visibility: "user" | "admin";
|
||||
placement: CustomMenuPlacement;
|
||||
sort_order: number;
|
||||
}>,
|
||||
custom_endpoints: [] as Array<{
|
||||
@@ -7094,6 +7120,7 @@ function addMenuItem() {
|
||||
icon_svg: "",
|
||||
url: "",
|
||||
visibility: "user",
|
||||
placement: DEFAULT_CUSTOM_MENU_PLACEMENT,
|
||||
sort_order: form.custom_menu_items.length,
|
||||
});
|
||||
}
|
||||
@@ -7209,6 +7236,12 @@ async function loadSettings() {
|
||||
(form as Record<string, unknown>)[key] = value;
|
||||
}
|
||||
}
|
||||
form.custom_menu_items = normalizeCustomMenuItems(
|
||||
settings.custom_menu_items,
|
||||
).map((item) => ({
|
||||
...item,
|
||||
placement: item.placement || DEFAULT_CUSTOM_MENU_PLACEMENT,
|
||||
}));
|
||||
form.login_agreement_mode =
|
||||
settings.login_agreement_mode === "checkbox" ? "checkbox" : "modal";
|
||||
form.login_agreement_updated_at =
|
||||
@@ -7575,7 +7608,10 @@ async function saveSettings() {
|
||||
hide_ccs_import_button: form.hide_ccs_import_button,
|
||||
table_default_page_size: form.table_default_page_size,
|
||||
table_page_size_options: form.table_page_size_options,
|
||||
custom_menu_items: form.custom_menu_items,
|
||||
custom_menu_items: form.custom_menu_items.map((item) => ({
|
||||
...item,
|
||||
placement: item.placement || DEFAULT_CUSTOM_MENU_PLACEMENT,
|
||||
})),
|
||||
custom_endpoints: form.custom_endpoints,
|
||||
frontend_url: form.frontend_url,
|
||||
smtp_host: form.smtp_host,
|
||||
|
||||
Reference in New Issue
Block a user