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>
|
||||
linksPlugin?: 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>
|
||||
[key: string]: unknown
|
||||
}
|
||||
@@ -63,6 +65,18 @@ export interface IPageConfig {
|
||||
/** 是否显示快捷导航(首页) */
|
||||
useQuickNavigation?: boolean
|
||||
bannerConfig?: IBannerConfig
|
||||
/** 首页快捷导航项(插件端「通用配置 → 页面设置 → 首页」配置,字段命名与插件端一致,
|
||||
* 数组顺序 = 展示顺序;未配置/为空时客户端回退内置默认项) */
|
||||
quickNavigation?: Array<{
|
||||
key?: string
|
||||
title?: string
|
||||
color?: string
|
||||
bgColor?: string
|
||||
iconPrefix?: string
|
||||
icon?: string
|
||||
path?: string
|
||||
visible?: boolean
|
||||
}>
|
||||
/** 首页精选分类引用(插件端「通用配置 → 页面设置 → 首页」配置,固定最多 3 个,
|
||||
* 快照含名称/封面/排序权重,数组顺序 = 展示顺序;配置模式下直接映射渲染不发请求,
|
||||
* 未配置/为空时回退默认取数) */
|
||||
@@ -127,8 +141,7 @@ export interface IAppConfig {
|
||||
auditConfig?: IAuditConfig
|
||||
/**
|
||||
* 站点级展示偏好默认(L0,插件端 GeneralConfig.preferences 经 getConfigs additive 下发;
|
||||
* 客户端 layout.{home,articles,archives}.{listLayout,cardType}/isAvatarRadius 的站点默认来源,
|
||||
* 本地偏好可覆盖;字段映射见 hermes/preferences.md §3)
|
||||
* 字段名与客户端偏好设置一致,客户端直接透传消费、不做映射,本地偏好可覆盖)
|
||||
*/
|
||||
preferences?: {
|
||||
/** 首页列表布局(h_row_col1/2 旧值由前端归一化为 single/double) */
|
||||
|
||||
+34
-12
@@ -5,6 +5,7 @@ import { http } from '@/http/alova'
|
||||
import { RequestFrom } from '@/http/tools/enum'
|
||||
import type { IResponse } from '@/http/types'
|
||||
import { getCache } from '@/utils/storage'
|
||||
import { getLoveModuleToken } from '@/utils/loveModuleToken'
|
||||
import { getNologinEmail, getOpenid } from '@/utils/auth'
|
||||
import { getPersonalToken } from '@/store/token'
|
||||
import type {
|
||||
@@ -201,51 +202,71 @@ export function getLoveConfig() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取恋爱相册列表
|
||||
* 获取恋爱相册列表(lovePhoto 模块设密码时需携带模块解锁 token)
|
||||
*/
|
||||
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', {
|
||||
params,
|
||||
cacheFor: 0,
|
||||
params: { ...params, ...(token ? { token } : {}) },
|
||||
cacheFor: 0,
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取恋爱相册详情
|
||||
* 获取恋爱相册详情(lovePhoto 模块设密码时需携带模块解锁 token)
|
||||
*/
|
||||
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}`, {
|
||||
params,
|
||||
cacheFor: 0,
|
||||
params: { ...params, ...(token ? { token } : {}) },
|
||||
cacheFor: 0,
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 密码解锁相册
|
||||
* 密码解锁相册(lovePhoto 模块设密码时需携带模块解锁 token)
|
||||
*/
|
||||
export function unlockAlbum(name: string, password: string, captcha?: ICaptchaQuery | null) {
|
||||
const token = getLoveModuleToken('lovePhoto')
|
||||
return http.Post<IResponse<{ token: string, photos?: unknown[] }>>(
|
||||
`/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/love-albums/${name}/unlock`,
|
||||
{
|
||||
password,
|
||||
},
|
||||
{
|
||||
params: buildCaptchaQuery(captcha),
|
||||
params: { ...buildCaptchaQuery(captcha), ...(token ? { token } : {}) },
|
||||
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) {
|
||||
const token = getLoveModuleToken('loveDaily')
|
||||
return http.Get<IResponse<ILoveDailyItemListRes>>(
|
||||
'/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/love-daily-items',
|
||||
{
|
||||
params,
|
||||
params: { ...params, ...(token ? { token } : {}) },
|
||||
cacheFor: 0,
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
},
|
||||
@@ -253,11 +274,12 @@ export function getLoveDailyItems(params: ILoveDailyItemListReq) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取恋爱故事列表
|
||||
* 获取恋爱故事列表(ourStory 模块设密码时需携带模块解锁 token)
|
||||
*/
|
||||
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', {
|
||||
params,
|
||||
params: { ...params, ...(token ? { token } : {}) },
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -98,19 +98,26 @@
|
||||
|
||||
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;窄列场景左右布局回退上图下文 */
|
||||
const effectiveLayout = computed<CardLayout>(() => {
|
||||
const _layout = settingStore.settings.layout
|
||||
const settings = settingStore.settings
|
||||
const page = props.from === 'home' || props.from === 'articles' || props.from === 'archives'
|
||||
? props.from
|
||||
: null
|
||||
let raw = props.layout
|
||||
if (!raw) {
|
||||
raw = page
|
||||
? (_layout[page].cardType as CardLayout)
|
||||
? (settings[CARD_TYPE_KEY[page]] as CardLayout)
|
||||
: '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') {
|
||||
return 'image_top'
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script setup lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import { useAppConfigStore } from '@/store/appConfig'
|
||||
|
||||
const appConfigStore = useAppConfigStore()
|
||||
@@ -10,81 +10,119 @@
|
||||
const calcVotePluginEnabled = computed(() => !!haloConfigs.value.pluginConfig?.votePlugin?.enabled)
|
||||
const calcLinksPluginEnabled = computed(() => !!haloConfigs.value.pluginConfig?.linksPlugin?.enabled)
|
||||
|
||||
/** 快捷导航列表(由配置控制显隐) */
|
||||
/** 快捷导航项(字段命名与插件端 quickNavigation 一致:key/title/color/bgColor/iconPrefix/icon/path/visible,无 borderColor) */
|
||||
interface QuickNavItem {
|
||||
key: string
|
||||
title?: string
|
||||
color?: string
|
||||
bgColor?: string
|
||||
iconPrefix?: string
|
||||
icon?: string
|
||||
path?: string
|
||||
visible?: boolean
|
||||
}
|
||||
|
||||
/** 内置默认项(未配置时回退;字段命名与插件端一致,可作配置缺失字段的兜底) */
|
||||
const DEFAULT_NAV_LIST: QuickNavItem[] = [
|
||||
{
|
||||
key: 'archives',
|
||||
title: '文章归档',
|
||||
color: '#03A9F4',
|
||||
bgColor: 'rgba(3, 169, 244, 0.14)',
|
||||
iconPrefix: 'uhemoji2-icon',
|
||||
icon: '-mask',
|
||||
path: '/pages-blog/archives/archives',
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
key: 'vote',
|
||||
title: '投票中心',
|
||||
color: '#00BCD4',
|
||||
bgColor: 'rgba(0, 188, 212, 0.14)',
|
||||
iconPrefix: 'uhemoji2-icon',
|
||||
icon: '-confused',
|
||||
path: '/pages-blog/votes/votes',
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
key: 'disclaimers',
|
||||
title: '友情链接',
|
||||
color: '#009688',
|
||||
bgColor: 'rgba(0, 150, 136, 0.14)',
|
||||
iconPrefix: 'uhemoji2-icon',
|
||||
icon: '-wink',
|
||||
path: '/pages-blog/friend-links/friend-links',
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
key: 'love',
|
||||
title: '恋爱日记',
|
||||
color: '#FF4C67',
|
||||
bgColor: 'rgba(255, 76, 103, 0.14)',
|
||||
iconPrefix: 'uhemoji2-icon',
|
||||
icon: '-in-love',
|
||||
path: '/pages-blog/love/love',
|
||||
visible: true,
|
||||
},
|
||||
{
|
||||
key: 'contact-blogger',
|
||||
title: '联系博主',
|
||||
color: '#FF9800',
|
||||
bgColor: 'rgba(255, 152, 0, 0.14)',
|
||||
iconPrefix: 'uhemoji2-icon',
|
||||
icon: '-cool',
|
||||
path: '/pages-blog/contact/contact',
|
||||
visible: true,
|
||||
},
|
||||
]
|
||||
|
||||
/** 快捷导航列表:优先读插件端配置(零映射,visible 过滤);未配置/为空回退内置默认(保留原显隐推导) */
|
||||
const navList = computed(() => {
|
||||
const loveEnabled = !!(haloConfigs.value.loveConfig as { loveEnabled ?: boolean })?.loveEnabled
|
||||
const socialEnabled = !!(haloConfigs.value.authorConfig?.social as { enabled ?: boolean } | undefined)?.enabled
|
||||
// <wd-icon class-prefix="uhemoji-icon" name="-smile-" size="32rpx" />
|
||||
return [
|
||||
{
|
||||
key: 'archives',
|
||||
title: calcAuditModeEnabled.value ? '内容归档' : '文章归档',
|
||||
color: '#03A9F4',
|
||||
bgGlass: 'rgba(3, 169, 244, 0.14)',
|
||||
borderColor: 'rgba(3, 169, 244, 0.35)',
|
||||
iconPrefix: 'uhemoji2-icon',
|
||||
icon: '-mask',
|
||||
path: '/pages-blog/archives/archives',
|
||||
show: true,
|
||||
},
|
||||
{
|
||||
key: 'vote',
|
||||
title: '投票中心',
|
||||
color: '#00BCD4',
|
||||
bgGlass: 'rgba(0, 188, 212, 0.14)',
|
||||
borderColor: 'rgba(0, 188, 212, 0.35)',
|
||||
iconPrefix: 'uhemoji2-icon',
|
||||
icon: '-confused',
|
||||
path: '/pages-blog/votes/votes',
|
||||
// show: !calcAuditModeEnabled.value && calcVotePluginEnabled.value,
|
||||
show: true,
|
||||
},
|
||||
{
|
||||
key: 'disclaimers',
|
||||
title: '友情链接',
|
||||
color: '#009688',
|
||||
bgGlass: 'rgba(0, 150, 136, 0.14)',
|
||||
borderColor: 'rgba(0, 150, 136, 0.35)',
|
||||
iconPrefix: 'uhemoji2-icon',
|
||||
icon: '-wink',
|
||||
path: '/pages-blog/friend-links/friend-links',
|
||||
// show: calcLinksPluginEnabled.value,
|
||||
show: true,
|
||||
},
|
||||
{
|
||||
key: 'love',
|
||||
title: '恋爱日记',
|
||||
color: '#FF4C67',
|
||||
bgGlass: 'rgba(255, 76, 103, 0.14)',
|
||||
borderColor: 'rgba(255, 76, 103, 0.075)',
|
||||
iconPrefix: 'uhemoji2-icon',
|
||||
icon: '-in-love',
|
||||
path: '/pages-blog/love/love',
|
||||
// show: loveEnabled,
|
||||
show: true,
|
||||
},
|
||||
{
|
||||
key: 'contact-blogger',
|
||||
title: '联系博主',
|
||||
color: '#FF9800',
|
||||
bgGlass: 'rgba(255, 152, 0, 0.14)',
|
||||
borderColor: 'rgba(255, 152, 0, 0.35)',
|
||||
iconPrefix: 'uhemoji2-icon',
|
||||
icon: '-cool',
|
||||
path: '/pages-blog/contact/contact',
|
||||
show: socialEnabled,
|
||||
},
|
||||
].filter(item => item.show)
|
||||
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 })
|
||||
}
|
||||
</script>
|
||||
|
||||
<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>
|
||||
@@ -94,7 +132,7 @@
|
||||
<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"
|
||||
:style="{
|
||||
backgroundColor: item.bgGlass
|
||||
backgroundColor: item.bgColor
|
||||
}">
|
||||
<wd-icon :class-prefix="item.iconPrefix" :name="item.icon" size="64rpx"/>
|
||||
</view>
|
||||
@@ -104,4 +142,4 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
* 小程序友链信息弹窗
|
||||
* 展示本站小程序申请提交的信息,字段结构与小程序提交申请弹窗(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 { useAppConfigStore } from '@/store/appConfig'
|
||||
@@ -22,18 +24,18 @@ const emit = defineEmits<{
|
||||
const isShow = ref(false)
|
||||
const appConfigStore = useAppConfigStore()
|
||||
|
||||
/** 小程序申请信息(字段与 uh-links-mini-apply 表单一致,从 linksSubmitPlugin 配置读取) */
|
||||
/** 小程序申请信息(字段与 uh-links-mini-apply 表单一致,从插件端 linkInfo 配置直接读取,字段名无映射) */
|
||||
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 || '')
|
||||
return {
|
||||
displayName: str('blogName'),
|
||||
miniProgramCode: str('blogLogo'),
|
||||
link: str('blogUrl'),
|
||||
displayName: str('displayName'),
|
||||
miniProgramCode: str('miniProgramCode'),
|
||||
link: str('link'),
|
||||
authorName: str('authorName'),
|
||||
avatar: str('avatar'),
|
||||
website: str('website'),
|
||||
description: str('blogDesc'),
|
||||
description: str('description'),
|
||||
applyRemark: str('applyRemark'),
|
||||
email: str('email'),
|
||||
}
|
||||
|
||||
+5
-16
@@ -2,15 +2,13 @@
|
||||
* 应用配置默认值(源自旧项目 config/index.js 的 DefaultAppConfigs)
|
||||
* 与 src/api/uni-halo.ts 的 getAppConfigs(plugin-uni-halo/getConfigs)配合,deepMerge 使用
|
||||
* 页面/组件依赖其字段结构(pluginConfig.toolsPlugin.Authorization 等),改动需谨慎
|
||||
* 2026-09-08 清理:与插件端 getConfigs 结构统一,移除已下线/无消费的默认字段
|
||||
* (tokenConfig/pageTitle/bannerConfig/categoryConfig.type 等)
|
||||
*/
|
||||
import type { IAppConfig } from '@/api/types/uni-halo'
|
||||
|
||||
export const DefaultAppConfigs: IAppConfig = {
|
||||
basicConfig: {
|
||||
tokenConfig: {
|
||||
personalToken: '',
|
||||
},
|
||||
},
|
||||
basicConfig: {},
|
||||
loveConfig: {},
|
||||
imagesConfig: {},
|
||||
authorConfig: {},
|
||||
@@ -19,6 +17,7 @@ export const DefaultAppConfigs: IAppConfig = {
|
||||
votePlugin: {},
|
||||
toolsPlugin: {},
|
||||
linksPlugin: {},
|
||||
// 保留:友链提交授权头(pluginConfig.linksSubmitPlugin.Authorization)与站点信息展示仍读取
|
||||
linksSubmitPlugin: {},
|
||||
doubanPlugin: {
|
||||
position: 'bottom',
|
||||
@@ -26,19 +25,9 @@ export const DefaultAppConfigs: IAppConfig = {
|
||||
},
|
||||
pageConfig: {
|
||||
homeConfig: {
|
||||
pageTitle: '首页',
|
||||
useCategory: true,
|
||||
bannerConfig: {
|
||||
enabled: true,
|
||||
showTitle: true,
|
||||
showIndicator: true,
|
||||
height: '400rpx',
|
||||
dotPosition: 'right',
|
||||
},
|
||||
},
|
||||
categoryConfig: {
|
||||
type: 'list',
|
||||
},
|
||||
categoryConfig: {},
|
||||
momentConfig: {
|
||||
useTagRandomColor: true,
|
||||
},
|
||||
|
||||
+19
-80
@@ -1,89 +1,28 @@
|
||||
/**
|
||||
* 应用设置默认值与类型(源自旧项目 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 {
|
||||
/** 评论头像是否圆形 */
|
||||
isAvatarRadius: boolean
|
||||
banner: {
|
||||
useDot: boolean
|
||||
dotPosition: string
|
||||
}
|
||||
/** 布局配置(按页面分组,每组可独立自定义) */
|
||||
layout: {
|
||||
/** 首页 */
|
||||
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
|
||||
}
|
||||
/** 评论头像是否圆形(插件端字段 avatarRadius) */
|
||||
avatarRadius: boolean
|
||||
/** 布局偏好(按页面分组,字段名 = 插件端 preferences 字段名,可直接消费 getConfigs) */
|
||||
homeListLayout: string
|
||||
homeCardType: string
|
||||
articlesListLayout: string
|
||||
articleCardType: string
|
||||
archivesListLayout: string
|
||||
archivesCardType: string
|
||||
}
|
||||
|
||||
export const DefaultAppSettings: IAppSettings = {
|
||||
isAvatarRadius: false,
|
||||
banner: {
|
||||
useDot: true,
|
||||
dotPosition: 'right',
|
||||
},
|
||||
layout: {
|
||||
home: { listLayout: 'single', cardType: '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,
|
||||
},
|
||||
avatarRadius: false,
|
||||
homeListLayout: 'single',
|
||||
homeCardType: 'image_top',
|
||||
articlesListLayout: 'double',
|
||||
articleCardType: 'image_top',
|
||||
archivesListLayout: 'single',
|
||||
archivesCardType: 'image_top',
|
||||
}
|
||||
|
||||
@@ -26,13 +26,13 @@ export const PAGE_GROUPS = [
|
||||
{ key: 'archives', label: '归档页面' }
|
||||
];
|
||||
|
||||
/** 布局偏好字段(页面分组 × 列表布局/卡片样式) */
|
||||
/** 布局偏好字段(页面分组 × 列表布局/卡片样式;path 直接为插件端顶层字段名,无映射) */
|
||||
export const LAYOUT_PREFS: PrefDef[] = PAGE_GROUPS.flatMap((group) => [
|
||||
{
|
||||
key: `${group.key}ListLayout`,
|
||||
label: '列表布局',
|
||||
kind: 'enum',
|
||||
path: ['layout', group.key, 'listLayout'],
|
||||
path: [`${group.key}ListLayout`],
|
||||
options: [
|
||||
{ label: '单列', value: 'single' },
|
||||
{ label: '双列', value: 'double' }
|
||||
@@ -42,7 +42,7 @@ export const LAYOUT_PREFS: PrefDef[] = PAGE_GROUPS.flatMap((group) => [
|
||||
key: `${group.key}CardType`,
|
||||
label: '卡片样式',
|
||||
kind: 'enum',
|
||||
path: ['layout', group.key, 'cardType'],
|
||||
path: [`${group.key}CardType`],
|
||||
options: [
|
||||
{ label: '上图下文', value: 'image_top' },
|
||||
{ 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 }[] = [
|
||||
@@ -126,12 +126,14 @@ export function usePreferenceRows() {
|
||||
const layoutRows = computed(() => buildRows(LAYOUT_PREFS));
|
||||
const featureRows = computed(() => buildRows(FEATURE_PREFS));
|
||||
|
||||
/** 布局设置按页面分组的展示行 */
|
||||
/** 布局设置按页面分组的展示行(列表布局 + 卡片样式两行) */
|
||||
const layoutGroups = computed(() =>
|
||||
PAGE_GROUPS.map((group) => ({
|
||||
key: group.key,
|
||||
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));
|
||||
}
|
||||
|
||||
/** 给定页面分组下字段路径,判断该页面列表布局是否为双列 */
|
||||
/** 给定字段路径,判断该页面列表布局是否为双列(path[0] 即插件端顶层字段名;
|
||||
* 列表布局行直接取值,卡片样式行推导同组 ListLayout 字段) */
|
||||
function isDoubleColumn(path: string[]): boolean {
|
||||
if (path.length >= 2 && path[0] === 'layout') {
|
||||
return prefValueOf(['layout', path[1], 'listLayout']) === 'double';
|
||||
const field = path[0] || ''
|
||||
const listKey = field.endsWith('CardType') ? field.replace(/CardType$/, 'ListLayout') : field
|
||||
if (listKey.endsWith('ListLayout')) {
|
||||
return prefValueOf([listKey]) === 'double';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** 卡片样式选项是否因双列约束被禁用(双列仅允许 image_top) */
|
||||
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 {
|
||||
@@ -169,8 +174,8 @@ export function usePreferenceRows() {
|
||||
} else {
|
||||
settingStore.savePreference(buildPatch(path, value));
|
||||
// 双列约束:列表布局改为双列时,卡片样式强制为 image_top
|
||||
if (path[0] === 'layout' && path[2] === 'listLayout' && value === 'double') {
|
||||
const cardTypePath = ['layout', path[1], 'cardType'];
|
||||
if (path[0].endsWith('ListLayout') && value === 'double') {
|
||||
const cardTypePath = [path[0].replace(/ListLayout$/, 'CardType')];
|
||||
if (prefValueOf(cardTypePath) !== 'image_top') {
|
||||
settingStore.savePreference(buildPatch(cardTypePath, 'image_top'));
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
/** 归档页列表布局(偏好设置驱动:single=单列 / double=双列) */
|
||||
const archivesListLayout = computed(() => settingStore.settings.layout.archives.listLayout)
|
||||
const archivesListLayout = computed(() => settingStore.settings.archivesListLayout)
|
||||
|
||||
/* ---------------- 状态 ---------------- */
|
||||
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
|
||||
|
||||
@@ -24,7 +24,7 @@ const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
/** 文章列表页列表布局(偏好设置驱动:single=单列 / double=双列) */
|
||||
const articlesListLayout = computed(() => settingStore.settings.layout.articles.listLayout)
|
||||
const articlesListLayout = computed(() => settingStore.settings.articlesListLayout)
|
||||
|
||||
/* ---------------- 状态 ---------------- */
|
||||
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
|
||||
|
||||
@@ -6,6 +6,7 @@ import { getLoveAlbumByName, getLoveAlbums } from '@/api/uni-halo'
|
||||
import { useAppConfigStore } from '@/store/appConfig'
|
||||
import { checkImageUrl } from '@/utils/url'
|
||||
import { getCache, setCache } from '@/utils/storage'
|
||||
import { handleLoveModuleLocked } from '@/utils/loveModuleToken'
|
||||
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
|
||||
import type { ILoveAlbum, ILovePhoto } from '@/api/types/uni-halo'
|
||||
|
||||
@@ -111,6 +112,8 @@ async function handleGetData() {
|
||||
}
|
||||
catch (e) {
|
||||
console.error('获取相册失败', e)
|
||||
// 模块锁 401:清除 token 并提示
|
||||
handleLoveModuleLocked('lovePhoto', e)
|
||||
updateLoadingStatus(DataLoadingStatusEnum.Error)
|
||||
}
|
||||
finally {
|
||||
@@ -138,6 +141,7 @@ async function handleLoadUnlockedAlbumPhotos() {
|
||||
}
|
||||
catch (e) {
|
||||
console.error('加载相册照片失败', e)
|
||||
handleLoveModuleLocked('lovePhoto', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import { getLoveDailyItems } from '@/api/uni-halo'
|
||||
import { handleLoveModuleLocked } from '@/utils/loveModuleToken'
|
||||
import { checkImageUrl } from '@/utils/url'
|
||||
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
|
||||
import type { ILoveDailyItem } from '@/api/types/uni-halo'
|
||||
@@ -172,6 +173,8 @@
|
||||
}
|
||||
catch (e) {
|
||||
console.error('获取清单失败', e)
|
||||
// 模块锁 401:清除 token 并提示
|
||||
handleLoveModuleLocked('loveDaily', e)
|
||||
updateLoadingStatus(DataLoadingStatusEnum.Error)
|
||||
}
|
||||
finally {
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import { onLoad, onShow } from '@dcloudio/uni-app'
|
||||
import { useAppConfigStore } from '@/store/appConfig'
|
||||
import { checkAvatarUrl, checkImageUrl } from '@/utils/url'
|
||||
import { unlockLoveModule } from '@/api/uni-halo'
|
||||
import { getLoveModuleToken, setLoveModuleToken, type LoveModuleKey } from '@/utils/loveModuleToken'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
@@ -29,9 +31,9 @@
|
||||
waveImageUrl : string
|
||||
heartImageUrl : string
|
||||
}
|
||||
ourStory : { enabled : boolean, iconUrl : string }
|
||||
lovePhoto : { enabled : boolean, iconUrl : string }
|
||||
loveDaily : { enabled : boolean, iconUrl : string }
|
||||
ourStory : { enabled : boolean, passwordEnabled ?: boolean }
|
||||
lovePhoto : { enabled : boolean, passwordEnabled ?: boolean }
|
||||
loveDaily : { enabled : boolean, passwordEnabled ?: boolean }
|
||||
[key : string] : unknown
|
||||
}
|
||||
|
||||
@@ -50,9 +52,9 @@
|
||||
waveImageUrl: '',
|
||||
heartImageUrl: '',
|
||||
},
|
||||
ourStory: { enabled: false, iconUrl: '' },
|
||||
lovePhoto: { enabled: false, iconUrl: '' },
|
||||
loveDaily: { enabled: false, iconUrl: '' },
|
||||
ourStory: { enabled: false, passwordEnabled: false },
|
||||
lovePhoto: { enabled: false, passwordEnabled: false },
|
||||
loveDaily: { enabled: false, passwordEnabled: false },
|
||||
})
|
||||
|
||||
const loveDayCount = ref({ d: 0, h: 0, m: 0, s: 0 })
|
||||
@@ -151,13 +153,66 @@
|
||||
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) {
|
||||
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({
|
||||
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(() => {
|
||||
syncLoveConfigFromStore()
|
||||
@@ -256,6 +311,28 @@
|
||||
</block>
|
||||
</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>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { onLoad, onPullDownRefresh } from '@dcloudio/uni-app'
|
||||
import dayjs from 'dayjs'
|
||||
import { getLoveStories } from '@/api/uni-halo'
|
||||
import { handleLoveModuleLocked } from '@/utils/loveModuleToken'
|
||||
import { useAppConfigStore } from '@/store/appConfig'
|
||||
import { checkImageUrl } from '@/utils/url'
|
||||
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
|
||||
@@ -116,7 +117,10 @@
|
||||
}
|
||||
catch (e) {
|
||||
console.error('获取故事失败', e)
|
||||
handleLoadFromLegacy()
|
||||
// 模块锁 401:清除 token 并提示;命中 locked 时不做旧配置降级(需回入口重新解锁)
|
||||
if (!handleLoveModuleLocked('ourStory', e)) {
|
||||
handleLoadFromLegacy()
|
||||
}
|
||||
}
|
||||
finally {
|
||||
setTimeout(() => {
|
||||
|
||||
@@ -168,37 +168,37 @@
|
||||
</uh-section-title>
|
||||
<view class="setting-sheet uh-global-card-glass overflow-hidden rounded-2xl">
|
||||
<template v-for="(row, index) in featureRows" :key="row.key">
|
||||
<!-- 布尔项:内联分段器(默认 / 开 / 关) -->
|
||||
<view v-if="row.kind === 'bool'" class="box-border p-3"
|
||||
:class="index < featureRows.length - 1 ? 'border-b border-black/5' : ''">
|
||||
<view class="flex items-center justify-between">
|
||||
<text class="row-label text-[28rpx] text-gray-900 font-bold">{{ row.label }}</text>
|
||||
<view class="flex items-center gap-2">
|
||||
<text v-if="row.following" class="row-sub text-2xs text-gray-400">默认</text>
|
||||
<view v-else
|
||||
class="rounded-full bg-secondary px-2 py-1 text-xs text-gray-900 leading-none">
|
||||
已自定义
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mt-3 flex flex-wrap gap-2">
|
||||
<view class="rounded-full px-3 py-1 text-xs uh-global-card-glass shadow-none border"
|
||||
:class="row.following ? 'bg-secondary font-bold' : 'text-gray-500'"
|
||||
@click="handleRevert(row.path)">
|
||||
默认
|
||||
</view>
|
||||
<view class="rounded-full px-3 py-1 text-xs uh-global-card-glass shadow-none border"
|
||||
:class="!row.following && row.boolValue ? 'bg-secondary font-bold' : 'text-gray-500'"
|
||||
@click="handleBoolChange(row.path, true)">
|
||||
开
|
||||
</view>
|
||||
<view class="rounded-full px-3 py-1 text-xs uh-global-card-glass shadow-none border"
|
||||
:class="!row.following && !row.boolValue ? 'bg-secondary font-bold' : 'text-gray-500'"
|
||||
@click="handleBoolChange(row.path, false)">
|
||||
关
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 布尔项:内联分段器(默认 / 开 / 关) -->
|
||||
<view v-if="row.kind === 'bool'" class="box-border p-3"
|
||||
:class="index < featureRows.length - 1 ? 'border-b border-black/5' : ''">
|
||||
<view class="flex items-center justify-between">
|
||||
<text class="row-label text-[28rpx] text-gray-900 font-bold">{{ row.label }}</text>
|
||||
<view class="flex items-center gap-2">
|
||||
<text v-if="row.following" class="row-sub text-2xs text-gray-400">默认</text>
|
||||
<view v-else
|
||||
class="rounded-full bg-secondary px-2 py-1 text-xs text-gray-900 leading-none">
|
||||
已自定义
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="mt-3 flex flex-wrap gap-2">
|
||||
<view class="rounded-full px-3 py-1 text-xs uh-global-card-glass shadow-none border"
|
||||
:class="row.following ? 'bg-secondary font-bold' : 'text-gray-500'"
|
||||
@click="handleRevert(row.path)">
|
||||
默认
|
||||
</view>
|
||||
<view class="rounded-full px-3 py-1 text-xs uh-global-card-glass shadow-none border"
|
||||
:class="!row.following && row.boolValue ? 'bg-secondary font-bold' : 'text-gray-500'"
|
||||
@click="handleBoolChange(row.path, true)">
|
||||
开
|
||||
</view>
|
||||
<view class="rounded-full px-3 py-1 text-xs uh-global-card-glass shadow-none border"
|
||||
:class="!row.following && !row.boolValue ? 'bg-secondary font-bold' : 'text-gray-500'"
|
||||
@click="handleBoolChange(row.path, false)">
|
||||
关
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 枚举选择 -->
|
||||
<view v-else class="pick-row flex items-center justify-between px-4 py-4"
|
||||
:class="index < featureRows.length - 1 ? 'border-b border-black/5' : ''"
|
||||
@@ -245,7 +245,8 @@
|
||||
</view>
|
||||
<!-- 选择器 -->
|
||||
<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">
|
||||
<uh-button custom-class="flex-1 py-2 uh-global-card-glass border !rounded-xl bg-white/90"
|
||||
@@ -261,4 +262,15 @@
|
||||
</view>
|
||||
</uh-glass-popup>
|
||||
</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)
|
||||
|
||||
/** 首页列表布局(偏好设置驱动:single=单列 / double=双列) */
|
||||
const homeListLayout = computed(() => settingStore.settings.layout.home.listLayout)
|
||||
const homeListLayout = computed(() => settingStore.settings.homeListLayout)
|
||||
|
||||
/* ---------------- 数据加载 ---------------- */
|
||||
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({})
|
||||
})
|
||||
|
||||
it('updateLocalPrefs:嵌套字段增量合并', () => {
|
||||
updateLocalPrefs({ layout: { home: { listLayout: 'double' } } })
|
||||
updateLocalPrefs({ layout: { home: { cardType: 'image_bottom' } } })
|
||||
it('updateLocalPrefs:字段增量合并(顶层插件字段名,无嵌套)', () => {
|
||||
updateLocalPrefs({ homeListLayout: 'double' })
|
||||
updateLocalPrefs({ homeCardType: 'image_bottom' })
|
||||
expect(readLocalPrefs()).toEqual({
|
||||
layout: { home: { listLayout: 'double', cardType: 'image_bottom' } },
|
||||
homeListLayout: 'double',
|
||||
homeCardType: 'image_bottom',
|
||||
})
|
||||
})
|
||||
|
||||
it('updateLocalPrefs:null 删除该键(回退跟随站点默认)', () => {
|
||||
updateLocalPrefs({ layout: { home: { listLayout: 'double', cardType: 'image_bottom' } } })
|
||||
updateLocalPrefs({ layout: { home: null } })
|
||||
expect(readLocalPrefs()).toEqual({ layout: {} })
|
||||
updateLocalPrefs({ homeListLayout: 'double', homeCardType: 'image_bottom' })
|
||||
updateLocalPrefs({ homeListLayout: null })
|
||||
expect(readLocalPrefs()).toEqual({ homeCardType: 'image_bottom' })
|
||||
})
|
||||
|
||||
it('updateLocalPrefs(null):整体清空差异', () => {
|
||||
updateLocalPrefs({ layout: { home: { listLayout: 'double' } } })
|
||||
updateLocalPrefs({ homeListLayout: 'double' })
|
||||
updateLocalPrefs(null)
|
||||
expect(readLocalPrefs()).toEqual({})
|
||||
expect(mem.has(LOCAL_PREFS_KEY)).toBe(false)
|
||||
})
|
||||
|
||||
it('clearLocalPrefs:删除存储键', () => {
|
||||
updateLocalPrefs({ layout: { home: { listLayout: 'double' } } })
|
||||
updateLocalPrefs({ homeListLayout: 'double' })
|
||||
clearLocalPrefs()
|
||||
expect(readLocalPrefs()).toEqual({})
|
||||
})
|
||||
|
||||
it('isLocalOverride:按路径判断是否被本地覆盖', () => {
|
||||
updateLocalPrefs({ gallery: { useWaterfull: false } })
|
||||
expect(isLocalOverride(readLocalPrefs(), ['gallery', 'useWaterfull'])).toBe(true)
|
||||
expect(isLocalOverride(readLocalPrefs(), ['banner', 'useDot'])).toBe(false)
|
||||
updateLocalPrefs({ homeListLayout: 'double' })
|
||||
expect(isLocalOverride(readLocalPrefs(), ['homeListLayout'])).toBe(true)
|
||||
expect(isLocalOverride(readLocalPrefs(), ['homeCardType'])).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -76,26 +77,14 @@ describe('mergeWithDefaults / collectSiteDefaults', () => {
|
||||
|
||||
it('本地优先于站点默认,站点默认优先于内置默认', () => {
|
||||
const merged = mergeWithDefaults(
|
||||
{ layout: { home: { listLayout: 'single' } }, gallery: { useWaterfull: true } },
|
||||
{ layout: { home: { listLayout: 'double' } } },
|
||||
{ homeListLayout: 'single' },
|
||||
{ homeListLayout: 'double' },
|
||||
)
|
||||
expect(merged.layout.home.listLayout).toBe('double')
|
||||
expect(merged.gallery.useWaterfull).toBe(true)
|
||||
expect(merged.isAvatarRadius).toBe(DefaultAppSettings.isAvatarRadius)
|
||||
expect(merged.homeListLayout).toBe('double')
|
||||
expect(merged.avatarRadius).toBe(DefaultAppSettings.avatarRadius)
|
||||
})
|
||||
|
||||
it('collectSiteDefaults:banner 站点默认映射(showIndicator→useDot)', () => {
|
||||
const site = collectSiteDefaults({
|
||||
pageConfig: {
|
||||
homeConfig: {
|
||||
bannerConfig: { showIndicator: false, dotPosition: 'bottom' },
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(site.banner).toEqual({ useDot: false, dotPosition: 'bottom' })
|
||||
})
|
||||
|
||||
it('collectSiteDefaults:preferences(L0)映射到 layout 页面分组/isAvatarRadius', () => {
|
||||
it('collectSiteDefaults:preferences(L0)同名字段透传(含旧值归一化)', () => {
|
||||
const site = collectSiteDefaults({
|
||||
preferences: {
|
||||
homeListLayout: 'h_row_col2',
|
||||
@@ -106,12 +95,12 @@ describe('mergeWithDefaults / collectSiteDefaults', () => {
|
||||
avatarRadius: true,
|
||||
},
|
||||
})
|
||||
expect(site.layout).toEqual({
|
||||
home: { listLayout: 'double', cardType: 'image_bottom' },
|
||||
articles: { listLayout: 'single', cardType: 'image_left' },
|
||||
archives: { cardType: 'image_top' },
|
||||
})
|
||||
expect(site.isAvatarRadius).toBe(true)
|
||||
expect(site.homeListLayout).toBe('double')
|
||||
expect(site.homeCardType).toBe('image_bottom')
|
||||
expect(site.articlesListLayout).toBe('single')
|
||||
expect(site.articleCardType).toBe('image_left')
|
||||
expect(site.archivesCardType).toBe('image_top')
|
||||
expect(site.avatarRadius).toBe(true)
|
||||
})
|
||||
|
||||
it('preferences L0 参与合并,本地未覆盖时跟随站点默认', () => {
|
||||
@@ -123,9 +112,9 @@ describe('mergeWithDefaults / collectSiteDefaults', () => {
|
||||
},
|
||||
})
|
||||
const merged = mergeWithDefaults(site, {})
|
||||
expect(merged.layout.home.listLayout).toBe('double')
|
||||
expect(merged.layout.articles.cardType).toBe('image_bottom')
|
||||
expect(merged.isAvatarRadius).toBe(true)
|
||||
expect(merged.homeListLayout).toBe('double')
|
||||
expect(merged.articleCardType).toBe('image_bottom')
|
||||
expect(merged.avatarRadius).toBe(true)
|
||||
})
|
||||
|
||||
it('preferences L0 可被本地差异覆盖,重置后回退站点默认', () => {
|
||||
@@ -136,29 +125,15 @@ describe('mergeWithDefaults / collectSiteDefaults', () => {
|
||||
avatarRadius: true,
|
||||
},
|
||||
})
|
||||
const merged = mergeWithDefaults(site, { layout: { home: { listLayout: 'single' } }, isAvatarRadius: false })
|
||||
expect(merged.layout.home.listLayout).toBe('single')
|
||||
expect(merged.layout.articles.cardType).toBe('image_bottom')
|
||||
expect(merged.isAvatarRadius).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)
|
||||
const merged = mergeWithDefaults(site, { homeListLayout: 'single', avatarRadius: false })
|
||||
expect(merged.homeListLayout).toBe('single')
|
||||
expect(merged.articleCardType).toBe('image_bottom')
|
||||
expect(merged.avatarRadius).toBe(false)
|
||||
})
|
||||
|
||||
it('未知枚举值不回退抛错(跟随默认)', () => {
|
||||
const merged = mergeWithDefaults({}, { layout: { home: { listLayout: 'not-exist' } } })
|
||||
expect(merged.layout.home.listLayout).toBe('not-exist')
|
||||
const merged = mergeWithDefaults({}, { homeListLayout: 'not-exist' })
|
||||
expect(merged.homeListLayout).toBe('not-exist')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -168,28 +143,31 @@ describe('migrateLegacyLocalPrefs', () => {
|
||||
setupUniStorageMock()
|
||||
})
|
||||
|
||||
it('旧 persist 存在时仅迁移被改过的叶子字段', () => {
|
||||
const legacySettings: IAppSettings = JSON.parse(JSON.stringify(DefaultAppSettings))
|
||||
// 旧结构 layout.home 为 string(列表布局),类型上绕过新结构约束
|
||||
;(legacySettings.layout as unknown as Record<string, unknown>).home = 'h_row_col2'
|
||||
legacySettings.gallery.useWaterfull = false
|
||||
it('旧 persist 存在时仅迁移与新默认结构可对比的叶子字段(旧 layout 嵌套字段不迁移)', () => {
|
||||
// 旧结构 layout.home 为 string(列表布局),类型上绕过新结构约束模拟旧数据
|
||||
const legacySettings = JSON.parse(JSON.stringify(DefaultAppSettings)) as IAppSettings & {
|
||||
layout?: unknown
|
||||
}
|
||||
legacySettings.layout = { home: 'h_row_col2' }
|
||||
legacySettings.avatarRadius = true
|
||||
mem.set('setting', JSON.stringify({ settings: legacySettings }))
|
||||
|
||||
expect(migrateLegacyLocalPrefs()).toBe(true)
|
||||
expect(readLocalPrefs()).toEqual({
|
||||
layout: { home: 'h_row_col2' },
|
||||
gallery: { useWaterfull: false },
|
||||
avatarRadius: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('已存在新差异键时不再重复迁移', () => {
|
||||
updateLocalPrefs({ layout: { home: { listLayout: 'single' } } })
|
||||
const legacySettings = JSON.parse(JSON.stringify(DefaultAppSettings)) as IAppSettings
|
||||
;(legacySettings.layout as unknown as Record<string, unknown>).home = 'h_row_col2'
|
||||
updateLocalPrefs({ homeListLayout: 'single' })
|
||||
const legacySettings = JSON.parse(JSON.stringify(DefaultAppSettings)) as IAppSettings & {
|
||||
layout?: unknown
|
||||
}
|
||||
legacySettings.layout = { home: 'h_row_col2' }
|
||||
mem.set('setting', JSON.stringify({ settings: legacySettings }))
|
||||
|
||||
expect(migrateLegacyLocalPrefs()).toBe(false)
|
||||
expect(readLocalPrefs()).toEqual({ layout: { home: { listLayout: 'single' } } })
|
||||
expect(readLocalPrefs()).toEqual({ homeListLayout: 'single' })
|
||||
})
|
||||
|
||||
it('无旧键或格式异常时返回 false 且不写新键', () => {
|
||||
|
||||
+16
-30
@@ -56,12 +56,12 @@ export function clearLocalPrefs(): void {
|
||||
|
||||
/**
|
||||
* 把 L0 站点默认(getConfigs 下发值)中与偏好相关的字段收集为本地差异形状的站点默认。
|
||||
* 偏好字段与 getConfigs 字段不完全同名,此处维护映射表:
|
||||
* 2026-09-08 起去映射:偏好字段与 getConfigs.preferences 字段名完全一致,只做值校验后透传,
|
||||
* 不再改写为 layout.{home,articles,archives}.{listLayout,cardType} 嵌套/isAvatarRadius。
|
||||
* 字段对照(与插件端一致):
|
||||
* - preferences.homeListLayout/homeCardType/articlesListLayout/articleCardType/
|
||||
* archivesListLayout/archivesCardType(L0 additive 顶层键)→ layout.{home,articles,archives}.{listLayout,cardType};
|
||||
* cardType 值为组件 layout 值(image_top/image_right/image_bottom/image_left),与设置页选项一致;
|
||||
* - preferences.avatarRadius → isAvatarRadius;
|
||||
* - pageConfig.homeConfig.bannerConfig → banner.useDot / dotPosition。
|
||||
* archivesListLayout/archivesCardType → 同名顶层字段;
|
||||
* - preferences.avatarRadius → avatarRadius。
|
||||
*/
|
||||
export function collectSiteDefaults(configs: Partial<IAppConfig>): LocalPrefs {
|
||||
const result: LocalPrefs = {}
|
||||
@@ -76,40 +76,26 @@ export function collectSiteDefaults(configs: Partial<IAppConfig>): LocalPrefs {
|
||||
return undefined
|
||||
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 cardValue = typeof prefs[cardKey] === 'string' ? prefs[cardKey] : undefined
|
||||
if (listValue === undefined && cardValue === undefined)
|
||||
return
|
||||
result.layout = {
|
||||
...result.layout,
|
||||
[page]: {
|
||||
...(listValue !== undefined ? { listLayout: listValue } : {}),
|
||||
...(cardValue !== undefined ? { cardType: cardValue } : {}),
|
||||
},
|
||||
if (listValue !== undefined) {
|
||||
result[listKey] = listValue
|
||||
}
|
||||
if (cardValue !== undefined) {
|
||||
result[cardKey] = cardValue
|
||||
}
|
||||
}
|
||||
setPage('home', 'homeListLayout', 'homeCardType')
|
||||
setPage('articles', 'articlesListLayout', 'articleCardType')
|
||||
setPage('archives', 'archivesListLayout', 'archivesCardType')
|
||||
setLayout('homeListLayout', 'homeCardType')
|
||||
setLayout('articlesListLayout', 'articleCardType')
|
||||
setLayout('archivesListLayout', 'archivesCardType')
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user