mirror of
https://github.com/ialley-workshop-open/uni-halo.git
synced 2026-09-12 16:40:40 +08:00
260d6965e3
1. 重构布局偏好字段映射,移除嵌套layout结构,直接使用插件端同名字段 2. 新增恋爱模块token管理与密码解锁功能,支持恋爱故事/相册/清单的密码保护 3. 重构首页快捷导航,支持插件端直接配置导航项 4. 清理废弃配置字段,统一站点默认值与插件端结构 5. 修复文章卡片布局读取逻辑,适配新的偏好字段结构
208 lines
6.6 KiB
TypeScript
208 lines
6.6 KiB
TypeScript
/**
|
|
* 偏好设置展示行与交互(页面 setting.vue 与全局弹窗 uh-settings-popup 共用)
|
|
* 收敛:字段定义(PrefDef/页面分组/布局/功能)、三态(跟随站点默认/已自定义/恢复默认)、
|
|
* 行构建、选值交互。两个消费方不再各自重复定义。
|
|
*/
|
|
import { computed } from 'vue';
|
|
import { DefaultAppSettings } from '@/config/appSettings';
|
|
import { useSettingStore } from '@/store/setting';
|
|
import { isLocalOverride, readLocalPrefs } from '@/utils/preference';
|
|
import type { LocalPrefs } from '@/utils/preference';
|
|
|
|
/** 偏好字段定义 */
|
|
export interface PrefDef {
|
|
key: string;
|
|
label: string;
|
|
kind: 'bool' | 'enum';
|
|
path: string[];
|
|
options?: { label: string; value: string }[];
|
|
siteLabelOf?: (value: string) => string;
|
|
}
|
|
|
|
/** 布局设置按页面分组(每组:列表布局 + 卡片样式) */
|
|
export const PAGE_GROUPS = [
|
|
{ key: 'home', label: '首页' },
|
|
{ key: 'articles', label: '文章页面' },
|
|
{ key: 'archives', label: '归档页面' }
|
|
];
|
|
|
|
/** 布局偏好字段(页面分组 × 列表布局/卡片样式;path 直接为插件端顶层字段名,无映射) */
|
|
export const LAYOUT_PREFS: PrefDef[] = PAGE_GROUPS.flatMap((group) => [
|
|
{
|
|
key: `${group.key}ListLayout`,
|
|
label: '列表布局',
|
|
kind: 'enum',
|
|
path: [`${group.key}ListLayout`],
|
|
options: [
|
|
{ label: '单列', value: 'single' },
|
|
{ label: '双列', value: 'double' }
|
|
]
|
|
},
|
|
{
|
|
key: `${group.key}CardType`,
|
|
label: '卡片样式',
|
|
kind: 'enum',
|
|
path: [`${group.key}CardType`],
|
|
options: [
|
|
{ label: '上图下文', value: 'image_top' },
|
|
{ label: '左文右图', value: 'image_right' },
|
|
{ label: '上文下图', value: 'image_bottom' },
|
|
{ label: '左图右文', value: 'image_left' }
|
|
]
|
|
}
|
|
]);
|
|
|
|
/** 功能偏好字段(字段名与插件端一致) */
|
|
export const FEATURE_PREFS: PrefDef[] = [{ key: 'avatarRadius', label: '是否圆形头像', kind: 'bool', path: ['avatarRadius'] }];
|
|
|
|
/** 顶部分段器(布局 / 功能) */
|
|
export const SETTING_TABS: { key: 'layout' | 'feature'; label: string }[] = [
|
|
{ key: 'layout', label: '布局' },
|
|
{ key: 'feature', label: '功能' }
|
|
];
|
|
|
|
export function usePreferenceRows() {
|
|
const settingStore = useSettingStore();
|
|
|
|
/* ---------------- 路径取值工具 ---------------- */
|
|
function getByPath(obj: unknown, path: string[]): unknown {
|
|
let cursor: unknown = obj;
|
|
for (const key of path) {
|
|
if (cursor === null || cursor === undefined) {
|
|
return undefined;
|
|
}
|
|
cursor = (cursor as Record<string, unknown>)[key];
|
|
}
|
|
return cursor;
|
|
}
|
|
|
|
/** 按路径构造差异 patch(null 表示删除该键=跟随站点默认) */
|
|
function buildPatch(path: string[], value: unknown): LocalPrefs {
|
|
const [head, ...rest] = path;
|
|
if (rest.length === 0) return { [head]: value } as LocalPrefs;
|
|
return { [head]: buildPatch(rest, value) } as LocalPrefs;
|
|
}
|
|
|
|
/* ---------------- 状态读取 ---------------- */
|
|
function prefValueOf(path: string[]): unknown {
|
|
return getByPath(settingStore.settings, path);
|
|
}
|
|
|
|
/** 站点默认值(未配置时回退内置默认) */
|
|
function siteDefaultOf(path: string[]): unknown {
|
|
const site = getByPath(settingStore.siteDefaults, path);
|
|
if (site !== undefined && site !== null) return site;
|
|
return getByPath(DefaultAppSettings, path);
|
|
}
|
|
|
|
function isOverridden(path: string[]): boolean {
|
|
return isLocalOverride(readLocalPrefs(), path);
|
|
}
|
|
|
|
function enumLabelOf(def: PrefDef, value: unknown): string {
|
|
const hit = def.options?.find((opt) => opt.value === value);
|
|
if (hit) return hit.label;
|
|
if (def.siteLabelOf && typeof value === 'string') return def.siteLabelOf(value);
|
|
return value === undefined || value === null ? '—' : String(value);
|
|
}
|
|
|
|
/* ---------------- 展示行(预计算,避免模板渲染期函数调用) ---------------- */
|
|
interface PrefRow extends PrefDef {
|
|
displayValue: string;
|
|
following: boolean;
|
|
/** 仅 kind==='bool' 使用 */
|
|
boolValue: boolean;
|
|
}
|
|
|
|
function buildRows(defs: PrefDef[]): PrefRow[] {
|
|
return defs.map((def) => ({
|
|
...def,
|
|
displayValue: enumLabelOf(def, prefValueOf(def.path)),
|
|
following: isFollowing(def.path),
|
|
boolValue: def.kind === 'bool' ? prefValueOf(def.path) === true : false
|
|
}));
|
|
}
|
|
|
|
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[0] === `${group.key}ListLayout` || row.path[0] === `${group.key}CardType`
|
|
)
|
|
}))
|
|
);
|
|
|
|
/** 当前项是否处于「跟随站点默认」(本地未覆盖) */
|
|
function isFollowing(path: string[]): boolean {
|
|
return !isOverridden(path);
|
|
}
|
|
|
|
/** 单项还原为跟随站点默认 */
|
|
function handleRevert(path: string[]): void {
|
|
settingStore.savePreference(buildPatch(path, null));
|
|
}
|
|
|
|
/** 开关类:选值等于站点默认则还原为跟随(只存差异) */
|
|
function handleBoolChange(path: string[], next: boolean): void {
|
|
settingStore.savePreference(buildPatch(path, next));
|
|
}
|
|
|
|
/** 给定字段路径,判断该页面列表布局是否为双列(path[0] 即插件端顶层字段名;
|
|
* 列表布局行直接取值,卡片样式行推导同组 ListLayout 字段) */
|
|
function isDoubleColumn(path: string[]): boolean {
|
|
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[0].endsWith('CardType') && isDoubleColumn(path) && value !== 'image_top';
|
|
}
|
|
|
|
function handleChoose(path: string[], value: string | null): void {
|
|
if (value === null) {
|
|
handleRevert(path);
|
|
} else {
|
|
settingStore.savePreference(buildPatch(path, value));
|
|
// 双列约束:列表布局改为双列时,卡片样式强制为 image_top
|
|
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'));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
settingStore,
|
|
PAGE_GROUPS,
|
|
SETTING_TABS,
|
|
getByPath,
|
|
buildPatch,
|
|
prefValueOf,
|
|
siteDefaultOf,
|
|
isOverridden,
|
|
enumLabelOf,
|
|
buildRows,
|
|
layoutRows,
|
|
featureRows,
|
|
layoutGroups,
|
|
isFollowing,
|
|
handleRevert,
|
|
handleBoolChange,
|
|
handleChoose,
|
|
isDoubleColumn,
|
|
isCardTypeOptionDisabled
|
|
};
|
|
}
|