mirror of
https://github.com/ialley-workshop-open/uni-halo.git
synced 2026-09-12 16:40:40 +08:00
refactor: 重构偏好设置与恋爱模块,对齐插件端字段结构
1. 重构布局偏好字段映射,移除嵌套layout结构,直接使用插件端同名字段 2. 新增恋爱模块token管理与密码解锁功能,支持恋爱故事/相册/清单的密码保护 3. 重构首页快捷导航,支持插件端直接配置导航项 4. 清理废弃配置字段,统一站点默认值与插件端结构 5. 修复文章卡片布局读取逻辑,适配新的偏好字段结构
This commit is contained in:
@@ -20,6 +20,8 @@ export interface IPluginConfig {
|
|||||||
toolsPlugin?: { Authorization?: string } & Record<string, unknown>
|
toolsPlugin?: { Authorization?: string } & Record<string, unknown>
|
||||||
linksPlugin?: Record<string, unknown>
|
linksPlugin?: Record<string, unknown>
|
||||||
linksSubmitPlugin?: { Authorization?: string } & Record<string, unknown>
|
linksSubmitPlugin?: { Authorization?: string } & Record<string, unknown>
|
||||||
|
/** 链接配置(插件端 spec.linkInfo 直接下发到本键,字段名无映射:displayName/miniProgramCode/link/description/applyRemark/authorName/avatar/website) */
|
||||||
|
linkInfo?: Record<string, unknown>
|
||||||
doubanPlugin?: { position?: string } & Record<string, unknown>
|
doubanPlugin?: { position?: string } & Record<string, unknown>
|
||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
@@ -63,6 +65,18 @@ export interface IPageConfig {
|
|||||||
/** 是否显示快捷导航(首页) */
|
/** 是否显示快捷导航(首页) */
|
||||||
useQuickNavigation?: boolean
|
useQuickNavigation?: boolean
|
||||||
bannerConfig?: IBannerConfig
|
bannerConfig?: IBannerConfig
|
||||||
|
/** 首页快捷导航项(插件端「通用配置 → 页面设置 → 首页」配置,字段命名与插件端一致,
|
||||||
|
* 数组顺序 = 展示顺序;未配置/为空时客户端回退内置默认项) */
|
||||||
|
quickNavigation?: Array<{
|
||||||
|
key?: string
|
||||||
|
title?: string
|
||||||
|
color?: string
|
||||||
|
bgColor?: string
|
||||||
|
iconPrefix?: string
|
||||||
|
icon?: string
|
||||||
|
path?: string
|
||||||
|
visible?: boolean
|
||||||
|
}>
|
||||||
/** 首页精选分类引用(插件端「通用配置 → 页面设置 → 首页」配置,固定最多 3 个,
|
/** 首页精选分类引用(插件端「通用配置 → 页面设置 → 首页」配置,固定最多 3 个,
|
||||||
* 快照含名称/封面/排序权重,数组顺序 = 展示顺序;配置模式下直接映射渲染不发请求,
|
* 快照含名称/封面/排序权重,数组顺序 = 展示顺序;配置模式下直接映射渲染不发请求,
|
||||||
* 未配置/为空时回退默认取数) */
|
* 未配置/为空时回退默认取数) */
|
||||||
@@ -127,8 +141,7 @@ export interface IAppConfig {
|
|||||||
auditConfig?: IAuditConfig
|
auditConfig?: IAuditConfig
|
||||||
/**
|
/**
|
||||||
* 站点级展示偏好默认(L0,插件端 GeneralConfig.preferences 经 getConfigs additive 下发;
|
* 站点级展示偏好默认(L0,插件端 GeneralConfig.preferences 经 getConfigs additive 下发;
|
||||||
* 客户端 layout.{home,articles,archives}.{listLayout,cardType}/isAvatarRadius 的站点默认来源,
|
* 字段名与客户端偏好设置一致,客户端直接透传消费、不做映射,本地偏好可覆盖)
|
||||||
* 本地偏好可覆盖;字段映射见 hermes/preferences.md §3)
|
|
||||||
*/
|
*/
|
||||||
preferences?: {
|
preferences?: {
|
||||||
/** 首页列表布局(h_row_col1/2 旧值由前端归一化为 single/double) */
|
/** 首页列表布局(h_row_col1/2 旧值由前端归一化为 single/double) */
|
||||||
|
|||||||
+32
-10
@@ -5,6 +5,7 @@ import { http } from '@/http/alova'
|
|||||||
import { RequestFrom } from '@/http/tools/enum'
|
import { RequestFrom } from '@/http/tools/enum'
|
||||||
import type { IResponse } from '@/http/types'
|
import type { IResponse } from '@/http/types'
|
||||||
import { getCache } from '@/utils/storage'
|
import { getCache } from '@/utils/storage'
|
||||||
|
import { getLoveModuleToken } from '@/utils/loveModuleToken'
|
||||||
import { getNologinEmail, getOpenid } from '@/utils/auth'
|
import { getNologinEmail, getOpenid } from '@/utils/auth'
|
||||||
import { getPersonalToken } from '@/store/token'
|
import { getPersonalToken } from '@/store/token'
|
||||||
import type {
|
import type {
|
||||||
@@ -201,51 +202,71 @@ export function getLoveConfig() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取恋爱相册列表
|
* 获取恋爱相册列表(lovePhoto 模块设密码时需携带模块解锁 token)
|
||||||
*/
|
*/
|
||||||
export function getLoveAlbums(params: ILoveAlbumListReq) {
|
export function getLoveAlbums(params: ILoveAlbumListReq) {
|
||||||
|
const token = getLoveModuleToken('lovePhoto')
|
||||||
return http.Get<IResponse<ILoveAlbumListRes>>('/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/love-albums', {
|
return http.Get<IResponse<ILoveAlbumListRes>>('/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/love-albums', {
|
||||||
params,
|
params: { ...params, ...(token ? { token } : {}) },
|
||||||
cacheFor: 0,
|
cacheFor: 0,
|
||||||
meta: { requestFrom: RequestFrom.Halo },
|
meta: { requestFrom: RequestFrom.Halo },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取恋爱相册详情
|
* 获取恋爱相册详情(lovePhoto 模块设密码时需携带模块解锁 token)
|
||||||
*/
|
*/
|
||||||
export function getLoveAlbumByName(name: string, params: ILoveAlbumListReq) {
|
export function getLoveAlbumByName(name: string, params: ILoveAlbumListReq) {
|
||||||
|
const token = getLoveModuleToken('lovePhoto')
|
||||||
return http.Get<IResponse<ILoveAlbum>>(`/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/love-albums/${name}`, {
|
return http.Get<IResponse<ILoveAlbum>>(`/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/love-albums/${name}`, {
|
||||||
params,
|
params: { ...params, ...(token ? { token } : {}) },
|
||||||
cacheFor: 0,
|
cacheFor: 0,
|
||||||
meta: { requestFrom: RequestFrom.Halo },
|
meta: { requestFrom: RequestFrom.Halo },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 密码解锁相册
|
* 密码解锁相册(lovePhoto 模块设密码时需携带模块解锁 token)
|
||||||
*/
|
*/
|
||||||
export function unlockAlbum(name: string, password: string, captcha?: ICaptchaQuery | null) {
|
export function unlockAlbum(name: string, password: string, captcha?: ICaptchaQuery | null) {
|
||||||
|
const token = getLoveModuleToken('lovePhoto')
|
||||||
return http.Post<IResponse<{ token: string, photos?: unknown[] }>>(
|
return http.Post<IResponse<{ token: string, photos?: unknown[] }>>(
|
||||||
`/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/love-albums/${name}/unlock`,
|
`/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/love-albums/${name}/unlock`,
|
||||||
{
|
{
|
||||||
password,
|
password,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
params: buildCaptchaQuery(captcha),
|
params: { ...buildCaptchaQuery(captcha), ...(token ? { token } : {}) },
|
||||||
meta: { requestFrom: RequestFrom.Halo },
|
meta: { requestFrom: RequestFrom.Halo },
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取恋爱清单列表(分页)
|
* 恋爱模块入口解锁(模块密码,签发 30 分钟 token;模块:ourStory/lovePhoto/loveDaily)
|
||||||
|
*/
|
||||||
|
export function unlockLoveModule(module: string, password: string) {
|
||||||
|
return http.Post<IResponse<{ token: string }>>(
|
||||||
|
'/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/love-modules/unlock',
|
||||||
|
{
|
||||||
|
module,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
meta: { requestFrom: RequestFrom.Halo },
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取恋爱清单列表(分页)(loveDaily 模块设密码时需携带模块解锁 token)
|
||||||
*/
|
*/
|
||||||
export function getLoveDailyItems(params: ILoveDailyItemListReq) {
|
export function getLoveDailyItems(params: ILoveDailyItemListReq) {
|
||||||
|
const token = getLoveModuleToken('loveDaily')
|
||||||
return http.Get<IResponse<ILoveDailyItemListRes>>(
|
return http.Get<IResponse<ILoveDailyItemListRes>>(
|
||||||
'/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/love-daily-items',
|
'/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/love-daily-items',
|
||||||
{
|
{
|
||||||
params,
|
params: { ...params, ...(token ? { token } : {}) },
|
||||||
cacheFor: 0,
|
cacheFor: 0,
|
||||||
meta: { requestFrom: RequestFrom.Halo },
|
meta: { requestFrom: RequestFrom.Halo },
|
||||||
},
|
},
|
||||||
@@ -253,11 +274,12 @@ export function getLoveDailyItems(params: ILoveDailyItemListReq) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取恋爱故事列表
|
* 获取恋爱故事列表(ourStory 模块设密码时需携带模块解锁 token)
|
||||||
*/
|
*/
|
||||||
export function getLoveStories(params: ILoveStoryListReq) {
|
export function getLoveStories(params: ILoveStoryListReq) {
|
||||||
|
const token = getLoveModuleToken('ourStory')
|
||||||
return http.Get<IResponse<ILoveStoryListRes>>('/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/love-stories', {
|
return http.Get<IResponse<ILoveStoryListRes>>('/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/love-stories', {
|
||||||
params,
|
params: { ...params, ...(token ? { token } : {}) },
|
||||||
meta: { requestFrom: RequestFrom.Halo },
|
meta: { requestFrom: RequestFrom.Halo },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,19 +98,26 @@
|
|||||||
|
|
||||||
const isGrid = computed(() => props.variant === 'grid')
|
const isGrid = computed(() => props.variant === 'grid')
|
||||||
|
|
||||||
|
/** 各页面卡片样式字段名(与插件端 preferences 字段一致) */
|
||||||
|
const CARD_TYPE_KEY: Record<'home' | 'articles' | 'archives', string> = {
|
||||||
|
home: 'homeCardType',
|
||||||
|
articles: 'articleCardType',
|
||||||
|
archives: 'archivesCardType',
|
||||||
|
}
|
||||||
|
|
||||||
/** 实际生效布局:显式 layout > 按页面读取全局 cardType(首页/文章列表/文章归档)> image_top;窄列场景左右布局回退上图下文 */
|
/** 实际生效布局:显式 layout > 按页面读取全局 cardType(首页/文章列表/文章归档)> image_top;窄列场景左右布局回退上图下文 */
|
||||||
const effectiveLayout = computed<CardLayout>(() => {
|
const effectiveLayout = computed<CardLayout>(() => {
|
||||||
const _layout = settingStore.settings.layout
|
const settings = settingStore.settings
|
||||||
const page = props.from === 'home' || props.from === 'articles' || props.from === 'archives'
|
const page = props.from === 'home' || props.from === 'articles' || props.from === 'archives'
|
||||||
? props.from
|
? props.from
|
||||||
: null
|
: null
|
||||||
let raw = props.layout
|
let raw = props.layout
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
raw = page
|
raw = page
|
||||||
? (_layout[page].cardType as CardLayout)
|
? (settings[CARD_TYPE_KEY[page]] as CardLayout)
|
||||||
: 'image_top'
|
: 'image_top'
|
||||||
}
|
}
|
||||||
const narrow = isGrid.value || (props.from === 'home' && _layout.home.listLayout === 'double')
|
const narrow = isGrid.value || (props.from === 'home' && settings.homeListLayout === 'double')
|
||||||
if (narrow && raw !== 'image_top') {
|
if (narrow && raw !== 'image_top') {
|
||||||
return 'image_top'
|
return 'image_top'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script lang="ts" setup>
|
||||||
import { useAppConfigStore } from '@/store/appConfig'
|
import { useAppConfigStore } from '@/store/appConfig'
|
||||||
|
|
||||||
const appConfigStore = useAppConfigStore()
|
const appConfigStore = useAppConfigStore()
|
||||||
@@ -10,81 +10,119 @@
|
|||||||
const calcVotePluginEnabled = computed(() => !!haloConfigs.value.pluginConfig?.votePlugin?.enabled)
|
const calcVotePluginEnabled = computed(() => !!haloConfigs.value.pluginConfig?.votePlugin?.enabled)
|
||||||
const calcLinksPluginEnabled = computed(() => !!haloConfigs.value.pluginConfig?.linksPlugin?.enabled)
|
const calcLinksPluginEnabled = computed(() => !!haloConfigs.value.pluginConfig?.linksPlugin?.enabled)
|
||||||
|
|
||||||
/** 快捷导航列表(由配置控制显隐) */
|
/** 快捷导航项(字段命名与插件端 quickNavigation 一致:key/title/color/bgColor/iconPrefix/icon/path/visible,无 borderColor) */
|
||||||
const navList = computed(() => {
|
interface QuickNavItem {
|
||||||
const loveEnabled = !!(haloConfigs.value.loveConfig as { loveEnabled ?: boolean })?.loveEnabled
|
key: string
|
||||||
const socialEnabled = !!(haloConfigs.value.authorConfig?.social as { enabled ?: boolean } | undefined)?.enabled
|
title?: string
|
||||||
// <wd-icon class-prefix="uhemoji-icon" name="-smile-" size="32rpx" />
|
color?: string
|
||||||
return [
|
bgColor?: string
|
||||||
|
iconPrefix?: string
|
||||||
|
icon?: string
|
||||||
|
path?: string
|
||||||
|
visible?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 内置默认项(未配置时回退;字段命名与插件端一致,可作配置缺失字段的兜底) */
|
||||||
|
const DEFAULT_NAV_LIST: QuickNavItem[] = [
|
||||||
{
|
{
|
||||||
key: 'archives',
|
key: 'archives',
|
||||||
title: calcAuditModeEnabled.value ? '内容归档' : '文章归档',
|
title: '文章归档',
|
||||||
color: '#03A9F4',
|
color: '#03A9F4',
|
||||||
bgGlass: 'rgba(3, 169, 244, 0.14)',
|
bgColor: 'rgba(3, 169, 244, 0.14)',
|
||||||
borderColor: 'rgba(3, 169, 244, 0.35)',
|
|
||||||
iconPrefix: 'uhemoji2-icon',
|
iconPrefix: 'uhemoji2-icon',
|
||||||
icon: '-mask',
|
icon: '-mask',
|
||||||
path: '/pages-blog/archives/archives',
|
path: '/pages-blog/archives/archives',
|
||||||
show: true,
|
visible: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'vote',
|
key: 'vote',
|
||||||
title: '投票中心',
|
title: '投票中心',
|
||||||
color: '#00BCD4',
|
color: '#00BCD4',
|
||||||
bgGlass: 'rgba(0, 188, 212, 0.14)',
|
bgColor: 'rgba(0, 188, 212, 0.14)',
|
||||||
borderColor: 'rgba(0, 188, 212, 0.35)',
|
|
||||||
iconPrefix: 'uhemoji2-icon',
|
iconPrefix: 'uhemoji2-icon',
|
||||||
icon: '-confused',
|
icon: '-confused',
|
||||||
path: '/pages-blog/votes/votes',
|
path: '/pages-blog/votes/votes',
|
||||||
// show: !calcAuditModeEnabled.value && calcVotePluginEnabled.value,
|
visible: true,
|
||||||
show: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'disclaimers',
|
key: 'disclaimers',
|
||||||
title: '友情链接',
|
title: '友情链接',
|
||||||
color: '#009688',
|
color: '#009688',
|
||||||
bgGlass: 'rgba(0, 150, 136, 0.14)',
|
bgColor: 'rgba(0, 150, 136, 0.14)',
|
||||||
borderColor: 'rgba(0, 150, 136, 0.35)',
|
|
||||||
iconPrefix: 'uhemoji2-icon',
|
iconPrefix: 'uhemoji2-icon',
|
||||||
icon: '-wink',
|
icon: '-wink',
|
||||||
path: '/pages-blog/friend-links/friend-links',
|
path: '/pages-blog/friend-links/friend-links',
|
||||||
// show: calcLinksPluginEnabled.value,
|
visible: true,
|
||||||
show: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'love',
|
key: 'love',
|
||||||
title: '恋爱日记',
|
title: '恋爱日记',
|
||||||
color: '#FF4C67',
|
color: '#FF4C67',
|
||||||
bgGlass: 'rgba(255, 76, 103, 0.14)',
|
bgColor: 'rgba(255, 76, 103, 0.14)',
|
||||||
borderColor: 'rgba(255, 76, 103, 0.075)',
|
|
||||||
iconPrefix: 'uhemoji2-icon',
|
iconPrefix: 'uhemoji2-icon',
|
||||||
icon: '-in-love',
|
icon: '-in-love',
|
||||||
path: '/pages-blog/love/love',
|
path: '/pages-blog/love/love',
|
||||||
// show: loveEnabled,
|
visible: true,
|
||||||
show: true,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'contact-blogger',
|
key: 'contact-blogger',
|
||||||
title: '联系博主',
|
title: '联系博主',
|
||||||
color: '#FF9800',
|
color: '#FF9800',
|
||||||
bgGlass: 'rgba(255, 152, 0, 0.14)',
|
bgColor: 'rgba(255, 152, 0, 0.14)',
|
||||||
borderColor: 'rgba(255, 152, 0, 0.35)',
|
|
||||||
iconPrefix: 'uhemoji2-icon',
|
iconPrefix: 'uhemoji2-icon',
|
||||||
icon: '-cool',
|
icon: '-cool',
|
||||||
path: '/pages-blog/contact/contact',
|
path: '/pages-blog/contact/contact',
|
||||||
show: socialEnabled,
|
visible: true,
|
||||||
},
|
},
|
||||||
].filter(item => item.show)
|
]
|
||||||
|
|
||||||
|
/** 快捷导航列表:优先读插件端配置(零映射,visible 过滤);未配置/为空回退内置默认(保留原显隐推导) */
|
||||||
|
const navList = computed(() => {
|
||||||
|
const socialEnabled = !!(haloConfigs.value.authorConfig?.social as { enabled ?: boolean } | undefined)?.enabled
|
||||||
|
const loveEnabled = !!(haloConfigs.value.loveConfig as { loveEnabled ?: boolean })?.loveEnabled
|
||||||
|
const configured = haloConfigs.value.pageConfig?.homeConfig?.quickNavigation
|
||||||
|
let list: QuickNavItem[]
|
||||||
|
if (configured && configured.length) {
|
||||||
|
// 配置模式:以配置项为准,缺失字段(icon/iconPrefix/color/path 等)按 key 从默认项兜底
|
||||||
|
list = configured.map(item => {
|
||||||
|
const fallback = DEFAULT_NAV_LIST.find(d => d.key === item.key) || DEFAULT_NAV_LIST[0]
|
||||||
|
return { ...fallback, ...item }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// 默认模式(老部署无配置):回退内置默认项,保留原显隐推导
|
||||||
|
list = DEFAULT_NAV_LIST.map(item => {
|
||||||
|
if (item.key === 'archives') {
|
||||||
|
return { ...item, title: calcAuditModeEnabled.value ? '内容归档' : item.title }
|
||||||
|
}
|
||||||
|
if (item.key === 'contact-blogger') {
|
||||||
|
return { ...item, visible: socialEnabled }
|
||||||
|
}
|
||||||
|
if (item.key === 'love') {
|
||||||
|
return { ...item, visible: loveEnabled }
|
||||||
|
}
|
||||||
|
if (item.key === 'vote') {
|
||||||
|
return { ...item, visible: calcVotePluginEnabled.value }
|
||||||
|
}
|
||||||
|
if (item.key === 'disclaimers') {
|
||||||
|
return { ...item, visible: calcLinksPluginEnabled.value }
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return list.filter(item => item.visible !== false)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
function handleClickNav(item : { path : string }) {
|
function handleClickNav(item : { path ?: string }) {
|
||||||
|
if (!item.path)
|
||||||
|
return
|
||||||
uni.navigateTo({ url: item.path })
|
uni.navigateTo({ url: item.path })
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<view v-if="navList.length" class="box-border overflow-hidden rounded-xl p-3 px-4 mb-3">
|
<view v-if="calcIsShowQuickNavigationEnabled && navList.length" class="box-border overflow-hidden rounded-xl p-3 px-4 mb-3">
|
||||||
<uh-section-title class="mb-4">
|
<uh-section-title class="mb-4">
|
||||||
快捷导航
|
快捷导航
|
||||||
</uh-section-title>
|
</uh-section-title>
|
||||||
@@ -94,7 +132,7 @@
|
|||||||
<view
|
<view
|
||||||
class="uh-global-card-glass uh-shadow-xs h-13 w-13 flex items-center justify-center rounded-2xl border transition-transform active:scale-90"
|
class="uh-global-card-glass uh-shadow-xs h-13 w-13 flex items-center justify-center rounded-2xl border transition-transform active:scale-90"
|
||||||
:style="{
|
:style="{
|
||||||
backgroundColor: item.bgGlass
|
backgroundColor: item.bgColor
|
||||||
}">
|
}">
|
||||||
<wd-icon :class-prefix="item.iconPrefix" :name="item.icon" size="64rpx"/>
|
<wd-icon :class-prefix="item.iconPrefix" :name="item.icon" size="64rpx"/>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
* 小程序友链信息弹窗
|
* 小程序友链信息弹窗
|
||||||
* 展示本站小程序申请提交的信息,字段结构与小程序提交申请弹窗(uh-links-mini-apply)一致:
|
* 展示本站小程序申请提交的信息,字段结构与小程序提交申请弹窗(uh-links-mini-apply)一致:
|
||||||
* 小程序名称/太阳码/跳转地址/作者昵称/作者头像/作者网站/描述/申请说明/邮箱
|
* 小程序名称/太阳码/跳转地址/作者昵称/作者头像/作者网站/描述/申请说明/邮箱
|
||||||
* 数据源:linksSubmitPlugin 配置(blogName→名称、blogLogo→太阳码、blogUrl→跳转地址、blogDesc→描述)
|
* 数据源:插件端 getConfigs.pluginConfig.linkInfo(字段名与插件端一致,无映射;
|
||||||
|
* displayName/miniProgramCode/link/description/applyRemark/authorName/avatar/website;
|
||||||
|
* email 不在插件端维护,读不到时弹窗该栏自动隐藏)
|
||||||
*/
|
*/
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { useAppConfigStore } from '@/store/appConfig'
|
import { useAppConfigStore } from '@/store/appConfig'
|
||||||
@@ -22,18 +24,18 @@ const emit = defineEmits<{
|
|||||||
const isShow = ref(false)
|
const isShow = ref(false)
|
||||||
const appConfigStore = useAppConfigStore()
|
const appConfigStore = useAppConfigStore()
|
||||||
|
|
||||||
/** 小程序申请信息(字段与 uh-links-mini-apply 表单一致,从 linksSubmitPlugin 配置读取) */
|
/** 小程序申请信息(字段与 uh-links-mini-apply 表单一致,从插件端 linkInfo 配置直接读取,字段名无映射) */
|
||||||
const miniInfo = computed(() => {
|
const miniInfo = computed(() => {
|
||||||
const cfg = (appConfigStore.configs.pluginConfig?.linksSubmitPlugin || {}) as Record<string, unknown>
|
const cfg = (appConfigStore.configs.pluginConfig?.linkInfo || {}) as Record<string, unknown>
|
||||||
const str = (key: string, fallback = '') => String(cfg[key] || fallback || '')
|
const str = (key: string, fallback = '') => String(cfg[key] || fallback || '')
|
||||||
return {
|
return {
|
||||||
displayName: str('blogName'),
|
displayName: str('displayName'),
|
||||||
miniProgramCode: str('blogLogo'),
|
miniProgramCode: str('miniProgramCode'),
|
||||||
link: str('blogUrl'),
|
link: str('link'),
|
||||||
authorName: str('authorName'),
|
authorName: str('authorName'),
|
||||||
avatar: str('avatar'),
|
avatar: str('avatar'),
|
||||||
website: str('website'),
|
website: str('website'),
|
||||||
description: str('blogDesc'),
|
description: str('description'),
|
||||||
applyRemark: str('applyRemark'),
|
applyRemark: str('applyRemark'),
|
||||||
email: str('email'),
|
email: str('email'),
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-16
@@ -2,15 +2,13 @@
|
|||||||
* 应用配置默认值(源自旧项目 config/index.js 的 DefaultAppConfigs)
|
* 应用配置默认值(源自旧项目 config/index.js 的 DefaultAppConfigs)
|
||||||
* 与 src/api/uni-halo.ts 的 getAppConfigs(plugin-uni-halo/getConfigs)配合,deepMerge 使用
|
* 与 src/api/uni-halo.ts 的 getAppConfigs(plugin-uni-halo/getConfigs)配合,deepMerge 使用
|
||||||
* 页面/组件依赖其字段结构(pluginConfig.toolsPlugin.Authorization 等),改动需谨慎
|
* 页面/组件依赖其字段结构(pluginConfig.toolsPlugin.Authorization 等),改动需谨慎
|
||||||
|
* 2026-09-08 清理:与插件端 getConfigs 结构统一,移除已下线/无消费的默认字段
|
||||||
|
* (tokenConfig/pageTitle/bannerConfig/categoryConfig.type 等)
|
||||||
*/
|
*/
|
||||||
import type { IAppConfig } from '@/api/types/uni-halo'
|
import type { IAppConfig } from '@/api/types/uni-halo'
|
||||||
|
|
||||||
export const DefaultAppConfigs: IAppConfig = {
|
export const DefaultAppConfigs: IAppConfig = {
|
||||||
basicConfig: {
|
basicConfig: {},
|
||||||
tokenConfig: {
|
|
||||||
personalToken: '',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
loveConfig: {},
|
loveConfig: {},
|
||||||
imagesConfig: {},
|
imagesConfig: {},
|
||||||
authorConfig: {},
|
authorConfig: {},
|
||||||
@@ -19,6 +17,7 @@ export const DefaultAppConfigs: IAppConfig = {
|
|||||||
votePlugin: {},
|
votePlugin: {},
|
||||||
toolsPlugin: {},
|
toolsPlugin: {},
|
||||||
linksPlugin: {},
|
linksPlugin: {},
|
||||||
|
// 保留:友链提交授权头(pluginConfig.linksSubmitPlugin.Authorization)与站点信息展示仍读取
|
||||||
linksSubmitPlugin: {},
|
linksSubmitPlugin: {},
|
||||||
doubanPlugin: {
|
doubanPlugin: {
|
||||||
position: 'bottom',
|
position: 'bottom',
|
||||||
@@ -26,19 +25,9 @@ export const DefaultAppConfigs: IAppConfig = {
|
|||||||
},
|
},
|
||||||
pageConfig: {
|
pageConfig: {
|
||||||
homeConfig: {
|
homeConfig: {
|
||||||
pageTitle: '首页',
|
|
||||||
useCategory: true,
|
useCategory: true,
|
||||||
bannerConfig: {
|
|
||||||
enabled: true,
|
|
||||||
showTitle: true,
|
|
||||||
showIndicator: true,
|
|
||||||
height: '400rpx',
|
|
||||||
dotPosition: 'right',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
categoryConfig: {
|
|
||||||
type: 'list',
|
|
||||||
},
|
},
|
||||||
|
categoryConfig: {},
|
||||||
momentConfig: {
|
momentConfig: {
|
||||||
useTagRandomColor: true,
|
useTagRandomColor: true,
|
||||||
},
|
},
|
||||||
|
|||||||
+19
-80
@@ -1,89 +1,28 @@
|
|||||||
/**
|
/**
|
||||||
* 应用设置默认值与类型(源自旧项目 utils/app.js 的 _DefaultAppSettings)
|
* 应用设置默认值与类型(源自旧项目 utils/app.js 的 _DefaultAppSettings)
|
||||||
|
* 布局偏好字段命名与插件端 getConfigs.preferences 一致(2026-09-08 起去映射,以插件端字段为准)
|
||||||
|
* 2026-09-08 清理:banner/ad/gallery/links/about/article/contact 等无消费字段已移除,
|
||||||
|
* 仅保留偏好相关字段(与插件端 preferences 结构对齐)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** 单页布局偏好(列表布局 + 卡片样式,与后端 preferences 分区对齐) */
|
|
||||||
export interface IPageLayoutPref {
|
|
||||||
/** 列表布局:single=单列 / double=双列 */
|
|
||||||
listLayout: string
|
|
||||||
/** 卡片样式(组件 layout 值):image_top=上图下文 / image_right=左文右图 / image_bottom=上文下图(社交卡片) / image_left=左图右文 */
|
|
||||||
cardType: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IAppSettings {
|
export interface IAppSettings {
|
||||||
/** 评论头像是否圆形 */
|
/** 评论头像是否圆形(插件端字段 avatarRadius) */
|
||||||
isAvatarRadius: boolean
|
avatarRadius: boolean
|
||||||
banner: {
|
/** 布局偏好(按页面分组,字段名 = 插件端 preferences 字段名,可直接消费 getConfigs) */
|
||||||
useDot: boolean
|
homeListLayout: string
|
||||||
dotPosition: string
|
homeCardType: string
|
||||||
}
|
articlesListLayout: string
|
||||||
/** 布局配置(按页面分组,每组可独立自定义) */
|
articleCardType: string
|
||||||
layout: {
|
archivesListLayout: string
|
||||||
/** 首页 */
|
archivesCardType: string
|
||||||
home: IPageLayoutPref
|
|
||||||
/** 文章列表页 */
|
|
||||||
articles: IPageLayoutPref
|
|
||||||
/** 文章归档页 */
|
|
||||||
archives: IPageLayoutPref
|
|
||||||
}
|
|
||||||
/** 广告配置 */
|
|
||||||
ad: {
|
|
||||||
/** 屏蔽广告时长,时间到后自动恢复展示(单位小时) */
|
|
||||||
timeout: number
|
|
||||||
/** 是否屏蔽广告 */
|
|
||||||
disabled: boolean
|
|
||||||
}
|
|
||||||
gallery: {
|
|
||||||
/** 是否使用瀑布流 */
|
|
||||||
useWaterfull: boolean
|
|
||||||
}
|
|
||||||
links: {
|
|
||||||
useSimple: boolean
|
|
||||||
useGroup: boolean
|
|
||||||
}
|
|
||||||
about: {
|
|
||||||
/** 显示后台登录入口 */
|
|
||||||
showAdmin: boolean
|
|
||||||
/** 显示所有的统计信息(关于页面) */
|
|
||||||
showAllCount: boolean
|
|
||||||
}
|
|
||||||
/** 文章配置 */
|
|
||||||
article: Record<string, unknown>
|
|
||||||
/** 联系博主页面 */
|
|
||||||
contact: {
|
|
||||||
/** 链接是否使用复制的方式,否则直接在内部打开 */
|
|
||||||
isLinkCopy: boolean
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DefaultAppSettings: IAppSettings = {
|
export const DefaultAppSettings: IAppSettings = {
|
||||||
isAvatarRadius: false,
|
avatarRadius: false,
|
||||||
banner: {
|
homeListLayout: 'single',
|
||||||
useDot: true,
|
homeCardType: 'image_top',
|
||||||
dotPosition: 'right',
|
articlesListLayout: 'double',
|
||||||
},
|
articleCardType: 'image_top',
|
||||||
layout: {
|
archivesListLayout: 'single',
|
||||||
home: { listLayout: 'single', cardType: 'image_top' },
|
archivesCardType: 'image_top',
|
||||||
articles: { listLayout: 'double', cardType: 'image_top' },
|
|
||||||
archives: { listLayout: 'single', cardType: 'image_top' },
|
|
||||||
},
|
|
||||||
ad: {
|
|
||||||
timeout: 3,
|
|
||||||
disabled: false,
|
|
||||||
},
|
|
||||||
gallery: {
|
|
||||||
useWaterfull: true,
|
|
||||||
},
|
|
||||||
links: {
|
|
||||||
useSimple: false,
|
|
||||||
useGroup: false,
|
|
||||||
},
|
|
||||||
about: {
|
|
||||||
showAdmin: false,
|
|
||||||
showAllCount: false,
|
|
||||||
},
|
|
||||||
article: {},
|
|
||||||
contact: {
|
|
||||||
isLinkCopy: true,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,13 +26,13 @@ export const PAGE_GROUPS = [
|
|||||||
{ key: 'archives', label: '归档页面' }
|
{ key: 'archives', label: '归档页面' }
|
||||||
];
|
];
|
||||||
|
|
||||||
/** 布局偏好字段(页面分组 × 列表布局/卡片样式) */
|
/** 布局偏好字段(页面分组 × 列表布局/卡片样式;path 直接为插件端顶层字段名,无映射) */
|
||||||
export const LAYOUT_PREFS: PrefDef[] = PAGE_GROUPS.flatMap((group) => [
|
export const LAYOUT_PREFS: PrefDef[] = PAGE_GROUPS.flatMap((group) => [
|
||||||
{
|
{
|
||||||
key: `${group.key}ListLayout`,
|
key: `${group.key}ListLayout`,
|
||||||
label: '列表布局',
|
label: '列表布局',
|
||||||
kind: 'enum',
|
kind: 'enum',
|
||||||
path: ['layout', group.key, 'listLayout'],
|
path: [`${group.key}ListLayout`],
|
||||||
options: [
|
options: [
|
||||||
{ label: '单列', value: 'single' },
|
{ label: '单列', value: 'single' },
|
||||||
{ label: '双列', value: 'double' }
|
{ label: '双列', value: 'double' }
|
||||||
@@ -42,7 +42,7 @@ export const LAYOUT_PREFS: PrefDef[] = PAGE_GROUPS.flatMap((group) => [
|
|||||||
key: `${group.key}CardType`,
|
key: `${group.key}CardType`,
|
||||||
label: '卡片样式',
|
label: '卡片样式',
|
||||||
kind: 'enum',
|
kind: 'enum',
|
||||||
path: ['layout', group.key, 'cardType'],
|
path: [`${group.key}CardType`],
|
||||||
options: [
|
options: [
|
||||||
{ label: '上图下文', value: 'image_top' },
|
{ label: '上图下文', value: 'image_top' },
|
||||||
{ label: '左文右图', value: 'image_right' },
|
{ label: '左文右图', value: 'image_right' },
|
||||||
@@ -52,8 +52,8 @@ export const LAYOUT_PREFS: PrefDef[] = PAGE_GROUPS.flatMap((group) => [
|
|||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
/** 功能偏好字段 */
|
/** 功能偏好字段(字段名与插件端一致) */
|
||||||
export const FEATURE_PREFS: PrefDef[] = [{ key: 'isAvatarRadius', label: '是否圆形头像', kind: 'bool', path: ['isAvatarRadius'] }];
|
export const FEATURE_PREFS: PrefDef[] = [{ key: 'avatarRadius', label: '是否圆形头像', kind: 'bool', path: ['avatarRadius'] }];
|
||||||
|
|
||||||
/** 顶部分段器(布局 / 功能) */
|
/** 顶部分段器(布局 / 功能) */
|
||||||
export const SETTING_TABS: { key: 'layout' | 'feature'; label: string }[] = [
|
export const SETTING_TABS: { key: 'layout' | 'feature'; label: string }[] = [
|
||||||
@@ -126,12 +126,14 @@ export function usePreferenceRows() {
|
|||||||
const layoutRows = computed(() => buildRows(LAYOUT_PREFS));
|
const layoutRows = computed(() => buildRows(LAYOUT_PREFS));
|
||||||
const featureRows = computed(() => buildRows(FEATURE_PREFS));
|
const featureRows = computed(() => buildRows(FEATURE_PREFS));
|
||||||
|
|
||||||
/** 布局设置按页面分组的展示行 */
|
/** 布局设置按页面分组的展示行(列表布局 + 卡片样式两行) */
|
||||||
const layoutGroups = computed(() =>
|
const layoutGroups = computed(() =>
|
||||||
PAGE_GROUPS.map((group) => ({
|
PAGE_GROUPS.map((group) => ({
|
||||||
key: group.key,
|
key: group.key,
|
||||||
label: group.label,
|
label: group.label,
|
||||||
rows: layoutRows.value.filter((row) => row.path[1] === group.key)
|
rows: layoutRows.value.filter((row) =>
|
||||||
|
row.path[0] === `${group.key}ListLayout` || row.path[0] === `${group.key}CardType`
|
||||||
|
)
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -150,17 +152,20 @@ export function usePreferenceRows() {
|
|||||||
settingStore.savePreference(buildPatch(path, next));
|
settingStore.savePreference(buildPatch(path, next));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 给定页面分组下字段路径,判断该页面列表布局是否为双列 */
|
/** 给定字段路径,判断该页面列表布局是否为双列(path[0] 即插件端顶层字段名;
|
||||||
|
* 列表布局行直接取值,卡片样式行推导同组 ListLayout 字段) */
|
||||||
function isDoubleColumn(path: string[]): boolean {
|
function isDoubleColumn(path: string[]): boolean {
|
||||||
if (path.length >= 2 && path[0] === 'layout') {
|
const field = path[0] || ''
|
||||||
return prefValueOf(['layout', path[1], 'listLayout']) === 'double';
|
const listKey = field.endsWith('CardType') ? field.replace(/CardType$/, 'ListLayout') : field
|
||||||
|
if (listKey.endsWith('ListLayout')) {
|
||||||
|
return prefValueOf([listKey]) === 'double';
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 卡片样式选项是否因双列约束被禁用(双列仅允许 image_top) */
|
/** 卡片样式选项是否因双列约束被禁用(双列仅允许 image_top) */
|
||||||
function isCardTypeOptionDisabled(path: string[], value: string): boolean {
|
function isCardTypeOptionDisabled(path: string[], value: string): boolean {
|
||||||
return path[2] === 'cardType' && isDoubleColumn(path) && value !== 'image_top';
|
return path[0].endsWith('CardType') && isDoubleColumn(path) && value !== 'image_top';
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChoose(path: string[], value: string | null): void {
|
function handleChoose(path: string[], value: string | null): void {
|
||||||
@@ -169,8 +174,8 @@ export function usePreferenceRows() {
|
|||||||
} else {
|
} else {
|
||||||
settingStore.savePreference(buildPatch(path, value));
|
settingStore.savePreference(buildPatch(path, value));
|
||||||
// 双列约束:列表布局改为双列时,卡片样式强制为 image_top
|
// 双列约束:列表布局改为双列时,卡片样式强制为 image_top
|
||||||
if (path[0] === 'layout' && path[2] === 'listLayout' && value === 'double') {
|
if (path[0].endsWith('ListLayout') && value === 'double') {
|
||||||
const cardTypePath = ['layout', path[1], 'cardType'];
|
const cardTypePath = [path[0].replace(/ListLayout$/, 'CardType')];
|
||||||
if (prefValueOf(cardTypePath) !== 'image_top') {
|
if (prefValueOf(cardTypePath) !== 'image_top') {
|
||||||
settingStore.savePreference(buildPatch(cardTypePath, 'image_top'));
|
settingStore.savePreference(buildPatch(cardTypePath, 'image_top'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
|
|||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
|
|
||||||
/** 归档页列表布局(偏好设置驱动:single=单列 / double=双列) */
|
/** 归档页列表布局(偏好设置驱动:single=单列 / double=双列) */
|
||||||
const archivesListLayout = computed(() => settingStore.settings.layout.archives.listLayout)
|
const archivesListLayout = computed(() => settingStore.settings.archivesListLayout)
|
||||||
|
|
||||||
/* ---------------- 状态 ---------------- */
|
/* ---------------- 状态 ---------------- */
|
||||||
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
|
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
|
|||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
|
|
||||||
/** 文章列表页列表布局(偏好设置驱动:single=单列 / double=双列) */
|
/** 文章列表页列表布局(偏好设置驱动:single=单列 / double=双列) */
|
||||||
const articlesListLayout = computed(() => settingStore.settings.layout.articles.listLayout)
|
const articlesListLayout = computed(() => settingStore.settings.articlesListLayout)
|
||||||
|
|
||||||
/* ---------------- 状态 ---------------- */
|
/* ---------------- 状态 ---------------- */
|
||||||
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
|
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { getLoveAlbumByName, getLoveAlbums } from '@/api/uni-halo'
|
|||||||
import { useAppConfigStore } from '@/store/appConfig'
|
import { useAppConfigStore } from '@/store/appConfig'
|
||||||
import { checkImageUrl } from '@/utils/url'
|
import { checkImageUrl } from '@/utils/url'
|
||||||
import { getCache, setCache } from '@/utils/storage'
|
import { getCache, setCache } from '@/utils/storage'
|
||||||
|
import { handleLoveModuleLocked } from '@/utils/loveModuleToken'
|
||||||
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
|
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
|
||||||
import type { ILoveAlbum, ILovePhoto } from '@/api/types/uni-halo'
|
import type { ILoveAlbum, ILovePhoto } from '@/api/types/uni-halo'
|
||||||
|
|
||||||
@@ -111,6 +112,8 @@ async function handleGetData() {
|
|||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
console.error('获取相册失败', e)
|
console.error('获取相册失败', e)
|
||||||
|
// 模块锁 401:清除 token 并提示
|
||||||
|
handleLoveModuleLocked('lovePhoto', e)
|
||||||
updateLoadingStatus(DataLoadingStatusEnum.Error)
|
updateLoadingStatus(DataLoadingStatusEnum.Error)
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
@@ -138,6 +141,7 @@ async function handleLoadUnlockedAlbumPhotos() {
|
|||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
console.error('加载相册照片失败', e)
|
console.error('加载相册照片失败', e)
|
||||||
|
handleLoveModuleLocked('lovePhoto', e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||||
import { getLoveDailyItems } from '@/api/uni-halo'
|
import { getLoveDailyItems } from '@/api/uni-halo'
|
||||||
|
import { handleLoveModuleLocked } from '@/utils/loveModuleToken'
|
||||||
import { checkImageUrl } from '@/utils/url'
|
import { checkImageUrl } from '@/utils/url'
|
||||||
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
|
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
|
||||||
import type { ILoveDailyItem } from '@/api/types/uni-halo'
|
import type { ILoveDailyItem } from '@/api/types/uni-halo'
|
||||||
@@ -172,6 +173,8 @@
|
|||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
console.error('获取清单失败', e)
|
console.error('获取清单失败', e)
|
||||||
|
// 模块锁 401:清除 token 并提示
|
||||||
|
handleLoveModuleLocked('loveDaily', e)
|
||||||
updateLoadingStatus(DataLoadingStatusEnum.Error)
|
updateLoadingStatus(DataLoadingStatusEnum.Error)
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||||
import { useAppConfigStore } from '@/store/appConfig'
|
import { useAppConfigStore } from '@/store/appConfig'
|
||||||
import { checkAvatarUrl, checkImageUrl } from '@/utils/url'
|
import { checkAvatarUrl, checkImageUrl } from '@/utils/url'
|
||||||
|
import { unlockLoveModule } from '@/api/uni-halo'
|
||||||
|
import { getLoveModuleToken, setLoveModuleToken, type LoveModuleKey } from '@/utils/loveModuleToken'
|
||||||
|
|
||||||
definePage({
|
definePage({
|
||||||
style: {
|
style: {
|
||||||
@@ -29,9 +31,9 @@
|
|||||||
waveImageUrl : string
|
waveImageUrl : string
|
||||||
heartImageUrl : string
|
heartImageUrl : string
|
||||||
}
|
}
|
||||||
ourStory : { enabled : boolean, iconUrl : string }
|
ourStory : { enabled : boolean, passwordEnabled ?: boolean }
|
||||||
lovePhoto : { enabled : boolean, iconUrl : string }
|
lovePhoto : { enabled : boolean, passwordEnabled ?: boolean }
|
||||||
loveDaily : { enabled : boolean, iconUrl : string }
|
loveDaily : { enabled : boolean, passwordEnabled ?: boolean }
|
||||||
[key : string] : unknown
|
[key : string] : unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,9 +52,9 @@
|
|||||||
waveImageUrl: '',
|
waveImageUrl: '',
|
||||||
heartImageUrl: '',
|
heartImageUrl: '',
|
||||||
},
|
},
|
||||||
ourStory: { enabled: false, iconUrl: '' },
|
ourStory: { enabled: false, passwordEnabled: false },
|
||||||
lovePhoto: { enabled: false, iconUrl: '' },
|
lovePhoto: { enabled: false, passwordEnabled: false },
|
||||||
loveDaily: { enabled: false, iconUrl: '' },
|
loveDaily: { enabled: false, passwordEnabled: false },
|
||||||
})
|
})
|
||||||
|
|
||||||
const loveDayCount = ref({ d: 0, h: 0, m: 0, s: 0 })
|
const loveDayCount = ref({ d: 0, h: 0, m: 0, s: 0 })
|
||||||
@@ -151,13 +153,66 @@
|
|||||||
countDownFn()
|
countDownFn()
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------------- 跳转 ---------------- */
|
/* ---------------- 跳转(模块密码拦截) ---------------- */
|
||||||
|
/** 页面名 → 恋爱模块 scope 映射(stories→ourStory、album→lovePhoto、list→loveDaily) */
|
||||||
|
const MODULE_KEY_MAP: Record<string, LoveModuleKey> = {
|
||||||
|
stories: 'ourStory',
|
||||||
|
album: 'lovePhoto',
|
||||||
|
list: 'loveDaily',
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 模块密码弹窗状态 */
|
||||||
|
const passwordModalVisible = ref(false)
|
||||||
|
const pendingPage = ref('')
|
||||||
|
const pendingModule = ref<LoveModuleKey>('ourStory')
|
||||||
|
const modulePassword = ref('')
|
||||||
|
const unlocking = ref(false)
|
||||||
|
|
||||||
function handleToPage(pageName : string) {
|
function handleToPage(pageName : string) {
|
||||||
|
const module = MODULE_KEY_MAP[pageName]
|
||||||
|
const moduleCfg = module
|
||||||
|
? (loveConfig.value[module] as { enabled ?: boolean, passwordEnabled ?: boolean })
|
||||||
|
: undefined
|
||||||
|
// 模块设了密码且本地无有效 token → 先弹密码框验证
|
||||||
|
if (moduleCfg?.passwordEnabled && !getLoveModuleToken(module)) {
|
||||||
|
pendingPage.value = pageName
|
||||||
|
pendingModule.value = module
|
||||||
|
modulePassword.value = ''
|
||||||
|
passwordModalVisible.value = true
|
||||||
|
return
|
||||||
|
}
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages-blog/love/${pageName}`,
|
url: `/pages-blog/love/${pageName}`,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 密码确认:unlock 签发 token 后进入模块 */
|
||||||
|
async function handleConfirmPassword() {
|
||||||
|
if (!modulePassword.value.trim()) {
|
||||||
|
uni.showToast({ icon: 'none', title: '请输入密码' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
unlocking.value = true
|
||||||
|
const res = await unlockLoveModule(pendingModule.value, modulePassword.value)
|
||||||
|
const token = res.data?.token
|
||||||
|
if (token) {
|
||||||
|
setLoveModuleToken(pendingModule.value, token)
|
||||||
|
passwordModalVisible.value = false
|
||||||
|
uni.navigateTo({ url: `/pages-blog/love/${pendingPage.value}` })
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
uni.showToast({ icon: 'none', title: '解锁失败,请重试' })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
uni.showToast({ icon: 'none', title: '密码不正确' })
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
unlocking.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------------- 生命周期 ---------------- */
|
/* ---------------- 生命周期 ---------------- */
|
||||||
onLoad(() => {
|
onLoad(() => {
|
||||||
syncLoveConfigFromStore()
|
syncLoveConfigFromStore()
|
||||||
@@ -256,6 +311,28 @@
|
|||||||
</block>
|
</block>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<!-- 模块密码验证弹窗 -->
|
||||||
|
<view v-if="passwordModalVisible" class="fixed inset-0 z-50 flex items-center justify-center bg-black/40"
|
||||||
|
@click="passwordModalVisible = false">
|
||||||
|
<view class="box-border w-4/5 rounded-2xl bg-white p-6" @click.stop>
|
||||||
|
<view class="text-center text-lg font-bold text-love">请输入访问密码</view>
|
||||||
|
<view class="mt-2 text-center text-xs text-gray-500">
|
||||||
|
{{ pendingModule === 'ourStory' ? '恋爱故事' : pendingModule === 'lovePhoto' ? '恋爱相册' : '恋爱清单' }}已设置访问密码,输入后进入
|
||||||
|
</view>
|
||||||
|
<input v-model="modulePassword" password
|
||||||
|
class="mt-4 h-11 w-full rounded-xl border border-gray-200 bg-gray-50 px-4 text-sm outline-none"
|
||||||
|
placeholder="请输入密码" />
|
||||||
|
<view class="mt-5 flex items-center gap-3">
|
||||||
|
<view class="flex-1 rounded-xl bg-gray-100 py-2.5 text-center text-sm text-gray-600"
|
||||||
|
@click="passwordModalVisible = false">取消</view>
|
||||||
|
<view class="flex-1 rounded-xl bg-love py-2.5 text-center text-sm text-white"
|
||||||
|
:class="unlocking ? 'opacity-60' : ''" @click="handleConfirmPassword">
|
||||||
|
{{ unlocking ? '验证中…' : '进入' }}
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { getLoveStories } from '@/api/uni-halo'
|
import { getLoveStories } from '@/api/uni-halo'
|
||||||
|
import { handleLoveModuleLocked } from '@/utils/loveModuleToken'
|
||||||
import { useAppConfigStore } from '@/store/appConfig'
|
import { useAppConfigStore } from '@/store/appConfig'
|
||||||
import { checkImageUrl } from '@/utils/url'
|
import { checkImageUrl } from '@/utils/url'
|
||||||
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
|
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
|
||||||
@@ -116,8 +117,11 @@
|
|||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
console.error('获取故事失败', e)
|
console.error('获取故事失败', e)
|
||||||
|
// 模块锁 401:清除 token 并提示;命中 locked 时不做旧配置降级(需回入口重新解锁)
|
||||||
|
if (!handleLoveModuleLocked('ourStory', e)) {
|
||||||
handleLoadFromLegacy()
|
handleLoadFromLegacy()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
finally {
|
finally {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
uni.stopPullDownRefresh()
|
uni.stopPullDownRefresh()
|
||||||
|
|||||||
@@ -245,7 +245,8 @@
|
|||||||
</view>
|
</view>
|
||||||
<!-- 选择器 -->
|
<!-- 选择器 -->
|
||||||
<wd-picker-view :columns="enumColumns" v-model="pickerValue"
|
<wd-picker-view :columns="enumColumns" v-model="pickerValue"
|
||||||
custom-class="!p-0 !bg-transparent !rounded-xl overflow-hidden" @change="handlePickerChange" />
|
custom-class="uh-picker-view !p-0 !bg-transparent !rounded-xl overflow-hidden"
|
||||||
|
@change="handlePickerChange" />
|
||||||
<!-- 底部操作:取消 / 确认 -->
|
<!-- 底部操作:取消 / 确认 -->
|
||||||
<view class="mt-4 flex items-center justify-center gap-x-3">
|
<view class="mt-4 flex items-center justify-center gap-x-3">
|
||||||
<uh-button custom-class="flex-1 py-2 uh-global-card-glass border !rounded-xl bg-white/90"
|
<uh-button custom-class="flex-1 py-2 uh-global-card-glass border !rounded-xl bg-white/90"
|
||||||
@@ -262,3 +263,14 @@
|
|||||||
</uh-glass-popup>
|
</uh-glass-popup>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
:deep(.uh-picker-view) {
|
||||||
|
.wd-picker-view__mask {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
.wd-picker-view__roller{
|
||||||
|
border-radius: 16rpx !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
const globalAppSettings = computed(() => settingStore.settings)
|
const globalAppSettings = computed(() => settingStore.settings)
|
||||||
|
|
||||||
/** 首页列表布局(偏好设置驱动:single=单列 / double=双列) */
|
/** 首页列表布局(偏好设置驱动:single=单列 / double=双列) */
|
||||||
const homeListLayout = computed(() => settingStore.settings.layout.home.listLayout)
|
const homeListLayout = computed(() => settingStore.settings.homeListLayout)
|
||||||
|
|
||||||
/* ---------------- 数据加载 ---------------- */
|
/* ---------------- 数据加载 ---------------- */
|
||||||
async function handleQuery() {
|
async function handleQuery() {
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
/**
|
||||||
|
* 恋爱模块入口解锁 token 管理(插件端 POST /love-modules/unlock 签发,HMAC 30 分钟有效)
|
||||||
|
* 模块 scope:ourStory(恋爱故事) / lovePhoto(恋爱相册) / loveDaily(恋爱清单)
|
||||||
|
*/
|
||||||
|
import { delCache, getCache, setCache } from '@/utils/storage'
|
||||||
|
|
||||||
|
/** 恋爱模块解锁 token 存储键(module → token) */
|
||||||
|
const LOVE_MODULE_TOKEN_KEY = 'uh_love_module_token_v1'
|
||||||
|
|
||||||
|
export type LoveModuleKey = 'ourStory' | 'lovePhoto' | 'loveDaily'
|
||||||
|
|
||||||
|
/** 读取指定模块的解锁 token(未解锁则 undefined) */
|
||||||
|
export function getLoveModuleToken(module: LoveModuleKey): string | undefined {
|
||||||
|
const cache = getCache<Record<string, string>>(LOVE_MODULE_TOKEN_KEY)
|
||||||
|
return cache?.[module]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 保存指定模块的解锁 token(插件端 30 分钟有效,过期由服务端 401 兜底重新解锁) */
|
||||||
|
export function setLoveModuleToken(module: LoveModuleKey, token: string): void {
|
||||||
|
const cache = getCache<Record<string, string>>(LOVE_MODULE_TOKEN_KEY) || {}
|
||||||
|
cache[module] = token
|
||||||
|
setCache(LOVE_MODULE_TOKEN_KEY, cache)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 清除指定模块的解锁 token(401 locked 时调用,回到未解锁状态) */
|
||||||
|
export function clearLoveModuleToken(module: LoveModuleKey): void {
|
||||||
|
const cache = getCache<Record<string, string>>(LOVE_MODULE_TOKEN_KEY)
|
||||||
|
if (cache && module in cache) {
|
||||||
|
delete cache[module]
|
||||||
|
setCache(LOVE_MODULE_TOKEN_KEY, cache)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 恋爱模块数据接口 401 locked 处理(清除 token + 提示,返回是否命中 locked) */
|
||||||
|
export function handleLoveModuleLocked(module: LoveModuleKey, err: unknown): boolean {
|
||||||
|
// alova 错误:err.cause?.response?.status === 401 且响应体 reason === 'locked'
|
||||||
|
const status = (err as { cause?: { response?: { status?: number } } })?.cause?.response?.status
|
||||||
|
if (status === 401) {
|
||||||
|
clearLoveModuleToken(module)
|
||||||
|
uni.showToast({ icon: 'none', title: '访问密码已失效,请返回重新解锁' })
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -35,37 +35,38 @@ describe('preference 基础读写', () => {
|
|||||||
expect(readLocalPrefs()).toEqual({})
|
expect(readLocalPrefs()).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('updateLocalPrefs:嵌套字段增量合并', () => {
|
it('updateLocalPrefs:字段增量合并(顶层插件字段名,无嵌套)', () => {
|
||||||
updateLocalPrefs({ layout: { home: { listLayout: 'double' } } })
|
updateLocalPrefs({ homeListLayout: 'double' })
|
||||||
updateLocalPrefs({ layout: { home: { cardType: 'image_bottom' } } })
|
updateLocalPrefs({ homeCardType: 'image_bottom' })
|
||||||
expect(readLocalPrefs()).toEqual({
|
expect(readLocalPrefs()).toEqual({
|
||||||
layout: { home: { listLayout: 'double', cardType: 'image_bottom' } },
|
homeListLayout: 'double',
|
||||||
|
homeCardType: 'image_bottom',
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('updateLocalPrefs:null 删除该键(回退跟随站点默认)', () => {
|
it('updateLocalPrefs:null 删除该键(回退跟随站点默认)', () => {
|
||||||
updateLocalPrefs({ layout: { home: { listLayout: 'double', cardType: 'image_bottom' } } })
|
updateLocalPrefs({ homeListLayout: 'double', homeCardType: 'image_bottom' })
|
||||||
updateLocalPrefs({ layout: { home: null } })
|
updateLocalPrefs({ homeListLayout: null })
|
||||||
expect(readLocalPrefs()).toEqual({ layout: {} })
|
expect(readLocalPrefs()).toEqual({ homeCardType: 'image_bottom' })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('updateLocalPrefs(null):整体清空差异', () => {
|
it('updateLocalPrefs(null):整体清空差异', () => {
|
||||||
updateLocalPrefs({ layout: { home: { listLayout: 'double' } } })
|
updateLocalPrefs({ homeListLayout: 'double' })
|
||||||
updateLocalPrefs(null)
|
updateLocalPrefs(null)
|
||||||
expect(readLocalPrefs()).toEqual({})
|
expect(readLocalPrefs()).toEqual({})
|
||||||
expect(mem.has(LOCAL_PREFS_KEY)).toBe(false)
|
expect(mem.has(LOCAL_PREFS_KEY)).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('clearLocalPrefs:删除存储键', () => {
|
it('clearLocalPrefs:删除存储键', () => {
|
||||||
updateLocalPrefs({ layout: { home: { listLayout: 'double' } } })
|
updateLocalPrefs({ homeListLayout: 'double' })
|
||||||
clearLocalPrefs()
|
clearLocalPrefs()
|
||||||
expect(readLocalPrefs()).toEqual({})
|
expect(readLocalPrefs()).toEqual({})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('isLocalOverride:按路径判断是否被本地覆盖', () => {
|
it('isLocalOverride:按路径判断是否被本地覆盖', () => {
|
||||||
updateLocalPrefs({ gallery: { useWaterfull: false } })
|
updateLocalPrefs({ homeListLayout: 'double' })
|
||||||
expect(isLocalOverride(readLocalPrefs(), ['gallery', 'useWaterfull'])).toBe(true)
|
expect(isLocalOverride(readLocalPrefs(), ['homeListLayout'])).toBe(true)
|
||||||
expect(isLocalOverride(readLocalPrefs(), ['banner', 'useDot'])).toBe(false)
|
expect(isLocalOverride(readLocalPrefs(), ['homeCardType'])).toBe(false)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -76,26 +77,14 @@ describe('mergeWithDefaults / collectSiteDefaults', () => {
|
|||||||
|
|
||||||
it('本地优先于站点默认,站点默认优先于内置默认', () => {
|
it('本地优先于站点默认,站点默认优先于内置默认', () => {
|
||||||
const merged = mergeWithDefaults(
|
const merged = mergeWithDefaults(
|
||||||
{ layout: { home: { listLayout: 'single' } }, gallery: { useWaterfull: true } },
|
{ homeListLayout: 'single' },
|
||||||
{ layout: { home: { listLayout: 'double' } } },
|
{ homeListLayout: 'double' },
|
||||||
)
|
)
|
||||||
expect(merged.layout.home.listLayout).toBe('double')
|
expect(merged.homeListLayout).toBe('double')
|
||||||
expect(merged.gallery.useWaterfull).toBe(true)
|
expect(merged.avatarRadius).toBe(DefaultAppSettings.avatarRadius)
|
||||||
expect(merged.isAvatarRadius).toBe(DefaultAppSettings.isAvatarRadius)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('collectSiteDefaults:banner 站点默认映射(showIndicator→useDot)', () => {
|
it('collectSiteDefaults:preferences(L0)同名字段透传(含旧值归一化)', () => {
|
||||||
const site = collectSiteDefaults({
|
|
||||||
pageConfig: {
|
|
||||||
homeConfig: {
|
|
||||||
bannerConfig: { showIndicator: false, dotPosition: 'bottom' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
expect(site.banner).toEqual({ useDot: false, dotPosition: 'bottom' })
|
|
||||||
})
|
|
||||||
|
|
||||||
it('collectSiteDefaults:preferences(L0)映射到 layout 页面分组/isAvatarRadius', () => {
|
|
||||||
const site = collectSiteDefaults({
|
const site = collectSiteDefaults({
|
||||||
preferences: {
|
preferences: {
|
||||||
homeListLayout: 'h_row_col2',
|
homeListLayout: 'h_row_col2',
|
||||||
@@ -106,12 +95,12 @@ describe('mergeWithDefaults / collectSiteDefaults', () => {
|
|||||||
avatarRadius: true,
|
avatarRadius: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
expect(site.layout).toEqual({
|
expect(site.homeListLayout).toBe('double')
|
||||||
home: { listLayout: 'double', cardType: 'image_bottom' },
|
expect(site.homeCardType).toBe('image_bottom')
|
||||||
articles: { listLayout: 'single', cardType: 'image_left' },
|
expect(site.articlesListLayout).toBe('single')
|
||||||
archives: { cardType: 'image_top' },
|
expect(site.articleCardType).toBe('image_left')
|
||||||
})
|
expect(site.archivesCardType).toBe('image_top')
|
||||||
expect(site.isAvatarRadius).toBe(true)
|
expect(site.avatarRadius).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('preferences L0 参与合并,本地未覆盖时跟随站点默认', () => {
|
it('preferences L0 参与合并,本地未覆盖时跟随站点默认', () => {
|
||||||
@@ -123,9 +112,9 @@ describe('mergeWithDefaults / collectSiteDefaults', () => {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
const merged = mergeWithDefaults(site, {})
|
const merged = mergeWithDefaults(site, {})
|
||||||
expect(merged.layout.home.listLayout).toBe('double')
|
expect(merged.homeListLayout).toBe('double')
|
||||||
expect(merged.layout.articles.cardType).toBe('image_bottom')
|
expect(merged.articleCardType).toBe('image_bottom')
|
||||||
expect(merged.isAvatarRadius).toBe(true)
|
expect(merged.avatarRadius).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('preferences L0 可被本地差异覆盖,重置后回退站点默认', () => {
|
it('preferences L0 可被本地差异覆盖,重置后回退站点默认', () => {
|
||||||
@@ -136,29 +125,15 @@ describe('mergeWithDefaults / collectSiteDefaults', () => {
|
|||||||
avatarRadius: true,
|
avatarRadius: true,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
const merged = mergeWithDefaults(site, { layout: { home: { listLayout: 'single' } }, isAvatarRadius: false })
|
const merged = mergeWithDefaults(site, { homeListLayout: 'single', avatarRadius: false })
|
||||||
expect(merged.layout.home.listLayout).toBe('single')
|
expect(merged.homeListLayout).toBe('single')
|
||||||
expect(merged.layout.articles.cardType).toBe('image_bottom')
|
expect(merged.articleCardType).toBe('image_bottom')
|
||||||
expect(merged.isAvatarRadius).toBe(false)
|
expect(merged.avatarRadius).toBe(false)
|
||||||
})
|
|
||||||
|
|
||||||
it('站点 banner 默认参与合并,本地未覆盖时跟随站点默认', () => {
|
|
||||||
const site = collectSiteDefaults({
|
|
||||||
pageConfig: {
|
|
||||||
homeConfig: {
|
|
||||||
bannerConfig: { showIndicator: false, dotPosition: 'bottom' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const merged = mergeWithDefaults(site, {})
|
|
||||||
expect(merged.banner.useDot).toBe(false)
|
|
||||||
expect(merged.banner.dotPosition).toBe('bottom')
|
|
||||||
expect(merged.layout.home.listLayout).toBe(DefaultAppSettings.layout.home.listLayout)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('未知枚举值不回退抛错(跟随默认)', () => {
|
it('未知枚举值不回退抛错(跟随默认)', () => {
|
||||||
const merged = mergeWithDefaults({}, { layout: { home: { listLayout: 'not-exist' } } })
|
const merged = mergeWithDefaults({}, { homeListLayout: 'not-exist' })
|
||||||
expect(merged.layout.home.listLayout).toBe('not-exist')
|
expect(merged.homeListLayout).toBe('not-exist')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -168,28 +143,31 @@ describe('migrateLegacyLocalPrefs', () => {
|
|||||||
setupUniStorageMock()
|
setupUniStorageMock()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('旧 persist 存在时仅迁移被改过的叶子字段', () => {
|
it('旧 persist 存在时仅迁移与新默认结构可对比的叶子字段(旧 layout 嵌套字段不迁移)', () => {
|
||||||
const legacySettings: IAppSettings = JSON.parse(JSON.stringify(DefaultAppSettings))
|
// 旧结构 layout.home 为 string(列表布局),类型上绕过新结构约束模拟旧数据
|
||||||
// 旧结构 layout.home 为 string(列表布局),类型上绕过新结构约束
|
const legacySettings = JSON.parse(JSON.stringify(DefaultAppSettings)) as IAppSettings & {
|
||||||
;(legacySettings.layout as unknown as Record<string, unknown>).home = 'h_row_col2'
|
layout?: unknown
|
||||||
legacySettings.gallery.useWaterfull = false
|
}
|
||||||
|
legacySettings.layout = { home: 'h_row_col2' }
|
||||||
|
legacySettings.avatarRadius = true
|
||||||
mem.set('setting', JSON.stringify({ settings: legacySettings }))
|
mem.set('setting', JSON.stringify({ settings: legacySettings }))
|
||||||
|
|
||||||
expect(migrateLegacyLocalPrefs()).toBe(true)
|
expect(migrateLegacyLocalPrefs()).toBe(true)
|
||||||
expect(readLocalPrefs()).toEqual({
|
expect(readLocalPrefs()).toEqual({
|
||||||
layout: { home: 'h_row_col2' },
|
avatarRadius: true,
|
||||||
gallery: { useWaterfull: false },
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('已存在新差异键时不再重复迁移', () => {
|
it('已存在新差异键时不再重复迁移', () => {
|
||||||
updateLocalPrefs({ layout: { home: { listLayout: 'single' } } })
|
updateLocalPrefs({ homeListLayout: 'single' })
|
||||||
const legacySettings = JSON.parse(JSON.stringify(DefaultAppSettings)) as IAppSettings
|
const legacySettings = JSON.parse(JSON.stringify(DefaultAppSettings)) as IAppSettings & {
|
||||||
;(legacySettings.layout as unknown as Record<string, unknown>).home = 'h_row_col2'
|
layout?: unknown
|
||||||
|
}
|
||||||
|
legacySettings.layout = { home: 'h_row_col2' }
|
||||||
mem.set('setting', JSON.stringify({ settings: legacySettings }))
|
mem.set('setting', JSON.stringify({ settings: legacySettings }))
|
||||||
|
|
||||||
expect(migrateLegacyLocalPrefs()).toBe(false)
|
expect(migrateLegacyLocalPrefs()).toBe(false)
|
||||||
expect(readLocalPrefs()).toEqual({ layout: { home: { listLayout: 'single' } } })
|
expect(readLocalPrefs()).toEqual({ homeListLayout: 'single' })
|
||||||
})
|
})
|
||||||
|
|
||||||
it('无旧键或格式异常时返回 false 且不写新键', () => {
|
it('无旧键或格式异常时返回 false 且不写新键', () => {
|
||||||
|
|||||||
+16
-30
@@ -56,12 +56,12 @@ export function clearLocalPrefs(): void {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 把 L0 站点默认(getConfigs 下发值)中与偏好相关的字段收集为本地差异形状的站点默认。
|
* 把 L0 站点默认(getConfigs 下发值)中与偏好相关的字段收集为本地差异形状的站点默认。
|
||||||
* 偏好字段与 getConfigs 字段不完全同名,此处维护映射表:
|
* 2026-09-08 起去映射:偏好字段与 getConfigs.preferences 字段名完全一致,只做值校验后透传,
|
||||||
|
* 不再改写为 layout.{home,articles,archives}.{listLayout,cardType} 嵌套/isAvatarRadius。
|
||||||
|
* 字段对照(与插件端一致):
|
||||||
* - preferences.homeListLayout/homeCardType/articlesListLayout/articleCardType/
|
* - preferences.homeListLayout/homeCardType/articlesListLayout/articleCardType/
|
||||||
* archivesListLayout/archivesCardType(L0 additive 顶层键)→ layout.{home,articles,archives}.{listLayout,cardType};
|
* archivesListLayout/archivesCardType → 同名顶层字段;
|
||||||
* cardType 值为组件 layout 值(image_top/image_right/image_bottom/image_left),与设置页选项一致;
|
* - preferences.avatarRadius → avatarRadius。
|
||||||
* - preferences.avatarRadius → isAvatarRadius;
|
|
||||||
* - pageConfig.homeConfig.bannerConfig → banner.useDot / dotPosition。
|
|
||||||
*/
|
*/
|
||||||
export function collectSiteDefaults(configs: Partial<IAppConfig>): LocalPrefs {
|
export function collectSiteDefaults(configs: Partial<IAppConfig>): LocalPrefs {
|
||||||
const result: LocalPrefs = {}
|
const result: LocalPrefs = {}
|
||||||
@@ -76,40 +76,26 @@ export function collectSiteDefaults(configs: Partial<IAppConfig>): LocalPrefs {
|
|||||||
return undefined
|
return undefined
|
||||||
return v === 'h_row_col2' ? 'double' : v === 'h_row_col1' ? 'single' : v
|
return v === 'h_row_col2' ? 'double' : v === 'h_row_col1' ? 'single' : v
|
||||||
}
|
}
|
||||||
/** 页面布局:后端字段名(列表布局 + 卡片样式)→ 本地嵌套路径 */
|
/** 布局字段透传:字段名与插件端一致,仅做值校验(有值才写入) */
|
||||||
const setPage = (page: 'home' | 'articles' | 'archives', listKey: string, cardKey: string) => {
|
const setLayout = (listKey: string, cardKey: string) => {
|
||||||
const listValue = listLayoutOf(prefs[listKey])
|
const listValue = listLayoutOf(prefs[listKey])
|
||||||
const cardValue = typeof prefs[cardKey] === 'string' ? prefs[cardKey] : undefined
|
const cardValue = typeof prefs[cardKey] === 'string' ? prefs[cardKey] : undefined
|
||||||
if (listValue === undefined && cardValue === undefined)
|
if (listValue !== undefined) {
|
||||||
return
|
result[listKey] = listValue
|
||||||
result.layout = {
|
}
|
||||||
...result.layout,
|
if (cardValue !== undefined) {
|
||||||
[page]: {
|
result[cardKey] = cardValue
|
||||||
...(listValue !== undefined ? { listLayout: listValue } : {}),
|
|
||||||
...(cardValue !== undefined ? { cardType: cardValue } : {}),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setPage('home', 'homeListLayout', 'homeCardType')
|
setLayout('homeListLayout', 'homeCardType')
|
||||||
setPage('articles', 'articlesListLayout', 'articleCardType')
|
setLayout('articlesListLayout', 'articleCardType')
|
||||||
setPage('archives', 'archivesListLayout', 'archivesCardType')
|
setLayout('archivesListLayout', 'archivesCardType')
|
||||||
|
|
||||||
if (typeof prefs.avatarRadius === 'boolean') {
|
if (typeof prefs.avatarRadius === 'boolean') {
|
||||||
result.isAvatarRadius = prefs.avatarRadius
|
result.avatarRadius = prefs.avatarRadius
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 轮播渲染参数(L0):站点「显示指示器」→ 本地偏好 banner.useDot
|
|
||||||
const bannerConfig = configs.pageConfig?.homeConfig?.bannerConfig
|
|
||||||
if (bannerConfig && typeof bannerConfig === 'object') {
|
|
||||||
result.banner = {
|
|
||||||
useDot: bannerConfig.showIndicator,
|
|
||||||
dotPosition: bannerConfig.dotPosition,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 预留:图库瀑布流 L0(galleryConfig.useWaterfall)随二期下发后在此补充
|
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user