mirror of
https://github.com/ialley-workshop-open/uni-halo.git
synced 2026-09-11 16:00:44 +08:00
feat: 接入小程序链接与审核配置公开接口
- 友链接入:friend-links 双 tab(站点/小程序)、小程序详情弹窗(太阳码长按保存/地址复制/预览图轮播/作者信息)、申请收录弹窗(POST /submissions) - 审核配置接入:新增 getAuditData,移除旧 mockJson,9 页审核模式改为真实数据按 name 过滤,友链站点 tab 按 LinkGroup 过滤、小程序 tab 隐藏 - 修复:补 uni.getLocale mock、pages.json vitest alias、getI18nText 无占位符容错、getPostList 参数类型 Co-Authored-By: AtomCode (deepseek-v4-flash) <noreply@atomgit.com>
This commit is contained in:
+146
-142
@@ -1,42 +1,42 @@
|
||||
/**
|
||||
* Halo 官方 API 接口定义
|
||||
*/
|
||||
import { http } from '@/http/alova';
|
||||
import { RequestFrom } from '@/http/tools/enum';
|
||||
import type { IResponse } from '@/http/types';
|
||||
import { getCache } from '@/utils/storage';
|
||||
import { getNologinEmail, getOpenid } from '@/utils/auth';
|
||||
import { http } from '@/http/alova'
|
||||
import { RequestFrom } from '@/http/tools/enum'
|
||||
import type { IResponse } from '@/http/types'
|
||||
import { getCache } from '@/utils/storage'
|
||||
import { getNologinEmail, getOpenid } from '@/utils/auth'
|
||||
import type {
|
||||
IBlogStats,
|
||||
ICategory,
|
||||
ICategoryListReq,
|
||||
ICategoryListRes,
|
||||
IComment,
|
||||
ICommentListReq,
|
||||
ICommentListRes,
|
||||
ILink,
|
||||
ILinkGroupListRes,
|
||||
ILinkListRes,
|
||||
IMoment,
|
||||
IMomentListReq,
|
||||
IMomentListRes,
|
||||
IPhotoGroupListReq,
|
||||
IPhotoGroupListRes,
|
||||
IPhotoListReq,
|
||||
IPhotoListRes,
|
||||
IPluginAvailable,
|
||||
IPost,
|
||||
IPostListReq,
|
||||
IPostListRes,
|
||||
ISearchReq,
|
||||
ISearchRes,
|
||||
ITagListRes,
|
||||
ITrackerCounterReq,
|
||||
IUpvoteReq
|
||||
} from './types/halo';
|
||||
IBlogStats,
|
||||
ICategory,
|
||||
ICategoryListReq,
|
||||
ICategoryListRes,
|
||||
IComment,
|
||||
ICommentListReq,
|
||||
ICommentListRes,
|
||||
ILink,
|
||||
ILinkGroupListRes,
|
||||
ILinkListRes,
|
||||
IMoment,
|
||||
IMomentListReq,
|
||||
IMomentListRes,
|
||||
IPhotoGroupListReq,
|
||||
IPhotoGroupListRes,
|
||||
IPhotoListReq,
|
||||
IPhotoListRes,
|
||||
IPluginAvailable,
|
||||
IPost,
|
||||
IPostListReq,
|
||||
IPostListRes,
|
||||
ISearchReq,
|
||||
ISearchRes,
|
||||
ITagListRes,
|
||||
ITrackerCounterReq,
|
||||
IUpvoteReq,
|
||||
} from './types/halo'
|
||||
|
||||
/** 评论验证码 cookie key */
|
||||
const COMMENT_WIDGET_CAPTCHA_COOKIES = 'comment-widget-captcha';
|
||||
const COMMENT_WIDGET_CAPTCHA_COOKIES = 'comment-widget-captcha'
|
||||
|
||||
/* ==================== 文章 ==================== */
|
||||
|
||||
@@ -44,32 +44,32 @@ const COMMENT_WIDGET_CAPTCHA_COOKIES = 'comment-widget-captcha';
|
||||
* 文章列表
|
||||
*/
|
||||
export function getPostList(params: IPostListReq) {
|
||||
return http.Get<IResponse<IPostListRes>>('/apis/api.content.halo.run/v1alpha1/posts', {
|
||||
query: params,
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
return http.Get<IResponse<IPostListRes>>('/apis/api.content.halo.run/v1alpha1/posts', {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 文章详情(带访客标识头)
|
||||
*/
|
||||
export function getPostByName(name: string) {
|
||||
return http.Get<IResponse<IPost>>(`/apis/api.content.halo.run/v1alpha1/posts/${name}`, {
|
||||
headers: {
|
||||
'Wechat-Session-Id': getOpenid(),
|
||||
'nologin-email': getNologinEmail()
|
||||
},
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
return http.Get<IResponse<IPost>>(`/apis/api.content.halo.run/v1alpha1/posts/${name}`, {
|
||||
headers: {
|
||||
'Wechat-Session-Id': getOpenid(),
|
||||
'nologin-email': getNologinEmail(),
|
||||
},
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 关键词搜索文章
|
||||
*/
|
||||
export function getPostListByKeyword(params: ISearchReq) {
|
||||
return http.Post<IResponse<ISearchRes>>('/apis/api.halo.run/v1alpha1/indices/-/search', params, {
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
return http.Post<IResponse<ISearchRes>>('/apis/api.halo.run/v1alpha1/indices/-/search', params, {
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
}
|
||||
|
||||
/* ==================== 分类 / 标签 ==================== */
|
||||
@@ -80,40 +80,40 @@ export function getPostListByKeyword(params: ISearchReq) {
|
||||
* alova 的 params 对数组默认即 repeat 形式(a=1&a=2),已等价;如遇嵌套对象场景再单独处理
|
||||
*/
|
||||
export function getCategoryList(params: ICategoryListReq) {
|
||||
return http.Get<IResponse<ICategoryListRes>>('/apis/api.content.halo.run/v1alpha1/categories', {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
return http.Get<IResponse<ICategoryListRes>>('/apis/api.content.halo.run/v1alpha1/categories', {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 分类下文章列表
|
||||
*/
|
||||
export function getCategoryPostList(name: string, params: IPostListReq) {
|
||||
return http.Get<IResponse<IPostListRes>>(`/apis/api.content.halo.run/v1alpha1/categories/${name}/posts`, {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
return http.Get<IResponse<IPostListRes>>(`/apis/api.content.halo.run/v1alpha1/categories/${name}/posts`, {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 标签列表
|
||||
*/
|
||||
export function getTagList(params: ICategoryListReq) {
|
||||
return http.Get<IResponse<ITagListRes>>('/apis/api.content.halo.run/v1alpha1/tags', {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
return http.Get<IResponse<ITagListRes>>('/apis/api.content.halo.run/v1alpha1/tags', {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 标签下文章列表
|
||||
*/
|
||||
export function getPostByTagName(tagName: string, params: IPostListReq) {
|
||||
return http.Get<IResponse<IPostListRes>>(`/apis/api.content.halo.run/v1alpha1/tags/${tagName}/posts`, {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
return http.Get<IResponse<IPostListRes>>(`/apis/api.content.halo.run/v1alpha1/tags/${tagName}/posts`, {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
}
|
||||
|
||||
/* ==================== 评论(含验证码 cookie 链路) ==================== */
|
||||
@@ -122,71 +122,75 @@ export function getPostByTagName(tagName: string, params: IPostListReq) {
|
||||
* 评论列表
|
||||
*/
|
||||
export function getPostCommentList(params: ICommentListReq) {
|
||||
return http.Get<IResponse<ICommentListRes>>('/apis/api.halo.run/v1alpha1/comments', {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
return http.Get<IResponse<ICommentListRes>>('/apis/api.halo.run/v1alpha1/comments', {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 评论回复列表
|
||||
*/
|
||||
export function getPostCommentReplyList(commentName: string, params: ICommentListReq) {
|
||||
return http.Get<IResponse<ICommentListRes>>(`/apis/api.halo.run/v1alpha1/comments/${commentName}/reply`, {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
return http.Get<IResponse<ICommentListRes>>(`/apis/api.halo.run/v1alpha1/comments/${commentName}/reply`, {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
}
|
||||
|
||||
/** 新增评论(带验证码,captchaCode 转入请求头) */
|
||||
export interface IAddCommentReq {
|
||||
allowNotification: boolean;
|
||||
raw: string;
|
||||
content?: string;
|
||||
owner?: Record<string, unknown>;
|
||||
/** 评论目标引用(subjectRef: group/kind/name/version) */
|
||||
subjectRef?: {
|
||||
group: string;
|
||||
kind: string;
|
||||
name: string;
|
||||
version?: string;
|
||||
};
|
||||
/** 验证码,提交时转入 X-Captcha-Code 头 */
|
||||
captchaCode?: string;
|
||||
allowNotification: boolean
|
||||
raw: string
|
||||
content?: string
|
||||
owner?: Record<string, unknown>
|
||||
/** 评论目标引用(subjectRef: group/kind/name/version) */
|
||||
subjectRef?: {
|
||||
group: string
|
||||
kind: string
|
||||
name: string
|
||||
version?: string
|
||||
}
|
||||
/** 验证码,提交时转入 X-Captcha-Code 头 */
|
||||
captchaCode?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增评论(captchaCode 拆出转 X-Captcha-Code 头 + Cookie)
|
||||
*/
|
||||
export function addPostComment(data: IAddCommentReq) {
|
||||
const { captchaCode, ...rest } = data;
|
||||
const headers: Record<string, string> = {
|
||||
Accept: 'application/json'
|
||||
};
|
||||
if (captchaCode) headers['X-Captcha-Code'] = captchaCode;
|
||||
const cookie = getCache<string>(COMMENT_WIDGET_CAPTCHA_COOKIES);
|
||||
if (cookie) headers.Cookie = cookie;
|
||||
return http.Post<IResponse<IComment>>('/apis/api.halo.run/v1alpha1/comments', rest, {
|
||||
headers,
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
const { captchaCode, ...rest } = data
|
||||
const headers: Record<string, string> = {
|
||||
Accept: 'application/json',
|
||||
}
|
||||
if (captchaCode)
|
||||
headers['X-Captcha-Code'] = captchaCode
|
||||
const cookie = getCache<string>(COMMENT_WIDGET_CAPTCHA_COOKIES)
|
||||
if (cookie)
|
||||
headers.Cookie = cookie
|
||||
return http.Post<IResponse<IComment>>('/apis/api.halo.run/v1alpha1/comments', rest, {
|
||||
headers,
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增评论回复(同上,验证码逻辑)
|
||||
*/
|
||||
export function addPostCommentReply(commentName: string, data: IAddCommentReq) {
|
||||
const { captchaCode, ...rest } = data;
|
||||
const headers: Record<string, string> = {
|
||||
Accept: 'application/json'
|
||||
};
|
||||
if (captchaCode) headers['X-Captcha-Code'] = captchaCode;
|
||||
const cookie = getCache<string>(COMMENT_WIDGET_CAPTCHA_COOKIES);
|
||||
if (cookie) headers.Cookie = cookie;
|
||||
return http.Post<IResponse<IComment>>(`/apis/api.halo.run/v1alpha1/comments/${commentName}/reply`, rest, {
|
||||
headers,
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
const { captchaCode, ...rest } = data
|
||||
const headers: Record<string, string> = {
|
||||
Accept: 'application/json',
|
||||
}
|
||||
if (captchaCode)
|
||||
headers['X-Captcha-Code'] = captchaCode
|
||||
const cookie = getCache<string>(COMMENT_WIDGET_CAPTCHA_COOKIES)
|
||||
if (cookie)
|
||||
headers.Cookie = cookie
|
||||
return http.Post<IResponse<IComment>>(`/apis/api.halo.run/v1alpha1/comments/${commentName}/reply`, rest, {
|
||||
headers,
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
}
|
||||
|
||||
/* ==================== 瞬间 ==================== */
|
||||
@@ -195,19 +199,19 @@ export function addPostCommentReply(commentName: string, data: IAddCommentReq) {
|
||||
* 瞬间列表
|
||||
*/
|
||||
export function getMomentList(params: IMomentListReq) {
|
||||
return http.Get<IResponse<IMomentListRes>>('/apis/api.moment.halo.run/v1alpha1/moments', {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
return http.Get<IResponse<IMomentListRes>>('/apis/api.moment.halo.run/v1alpha1/moments', {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 瞬间详情
|
||||
*/
|
||||
export function getMomentByName(name: string) {
|
||||
return http.Get<IResponse<IMoment>>(`/apis/api.moment.halo.run/v1alpha1/moments/${name}`, {
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
return http.Get<IResponse<IMoment>>(`/apis/api.moment.halo.run/v1alpha1/moments/${name}`, {
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
}
|
||||
|
||||
/* ==================== 图库 ==================== */
|
||||
@@ -216,20 +220,20 @@ export function getMomentByName(name: string) {
|
||||
* 相册分组列表
|
||||
*/
|
||||
export function getPhotoGroupList(params: IPhotoGroupListReq) {
|
||||
return http.Get<IResponse<IPhotoGroupListRes>>('/apis/api.photo.halo.run/v1alpha1/photogroups', {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
return http.Get<IResponse<IPhotoGroupListRes>>('/apis/api.photo.halo.run/v1alpha1/photogroups', {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 照片列表(按相册)
|
||||
*/
|
||||
export function getPhotoListByGroupName(params: IPhotoListReq) {
|
||||
return http.Get<IResponse<IPhotoListRes>>('/apis/api.photo.halo.run/v1alpha1/photos', {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
return http.Get<IResponse<IPhotoListRes>>('/apis/api.photo.halo.run/v1alpha1/photos', {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
}
|
||||
|
||||
/* ==================== 友链 ==================== */
|
||||
@@ -238,20 +242,20 @@ export function getPhotoListByGroupName(params: IPhotoListReq) {
|
||||
* 友链分组列表
|
||||
*/
|
||||
export function getFriendLinkGroupList(params: ICategoryListReq) {
|
||||
return http.Get<IResponse<ILinkGroupListRes>>('/apis/api.link.halo.run/v1alpha1/linkgroups', {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
return http.Get<IResponse<ILinkGroupListRes>>('/apis/api.link.halo.run/v1alpha1/linkgroups', {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 友链列表
|
||||
*/
|
||||
export function getFriendLinkList(params: ICategoryListReq) {
|
||||
return http.Get<IResponse<ILinkListRes>>('/apis/api.link.halo.run/v1alpha1/links', {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
return http.Get<IResponse<ILinkListRes>>('/apis/api.link.halo.run/v1alpha1/links', {
|
||||
params,
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
}
|
||||
|
||||
/* ==================== 统计 / 埋点 / 插件 ==================== */
|
||||
@@ -260,37 +264,37 @@ export function getFriendLinkList(params: ICategoryListReq) {
|
||||
* 博客统计信息
|
||||
*/
|
||||
export function getBlogStatistics() {
|
||||
return http.Get<IResponse<IBlogStats>>('/apis/api.halo.run/v1alpha1/stats/-', {
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
return http.Get<IResponse<IBlogStats>>('/apis/api.halo.run/v1alpha1/stats/-', {
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交点赞
|
||||
*/
|
||||
export function submitUpvote(data: IUpvoteReq) {
|
||||
return http.Post<IResponse<unknown>>('/apis/api.halo.run/v1alpha1/trackers/upvote', data, {
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
return http.Post<IResponse<unknown>>('/apis/api.halo.run/v1alpha1/trackers/upvote', data, {
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交计数埋点
|
||||
*/
|
||||
export function postTrackersCounter(data: ITrackerCounterReq) {
|
||||
return http.Post<IResponse<unknown>>('/apis/api.halo.run/v1alpha1/trackers/counter', data, {
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
return http.Post<IResponse<unknown>>('/apis/api.halo.run/v1alpha1/trackers/counter', data, {
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查插件是否可用
|
||||
*/
|
||||
export function checkPluginAvailable(name: string) {
|
||||
return http.Get<IResponse<IPluginAvailable>>(`/apis/api.plugin.halo.run/v1alpha1/plugins/${name}/available`, {
|
||||
meta: { requestFrom: RequestFrom.Halo }
|
||||
});
|
||||
return http.Get<IResponse<IPluginAvailable>>(`/apis/api.plugin.halo.run/v1alpha1/plugins/${name}/available`, {
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
}
|
||||
|
||||
/** 分类资源(供加密分类判断等场景) */
|
||||
export type { ICategory, ILink, IMoment, IPost };
|
||||
export type { ICategory, ILink, IMoment, IPost }
|
||||
|
||||
+114
-3
@@ -62,6 +62,28 @@ export interface IAuditConfig {
|
||||
}
|
||||
}
|
||||
|
||||
/** 审核模式数据(公开接口 GET /audit-data 返回) */
|
||||
export interface IAuditDataResult {
|
||||
/** 审核模式开关(联动设置页 auditModeEnabled) */
|
||||
enabled: boolean
|
||||
/** 选中的引用 name 列表(数组顺序即展示顺序;开关关闭时为空) */
|
||||
spec?: {
|
||||
/** 选中的文章 Post metadata.name 列表 */
|
||||
posts?: string[]
|
||||
/** 选中的分类 Category metadata.name 列表 */
|
||||
categories?: string[]
|
||||
/** 选中的图库分组 PhotoGroup metadata.name 列表 */
|
||||
galleryGroups?: string[]
|
||||
/** 选中的瞬间 Moment metadata.name 列表 */
|
||||
moments?: string[]
|
||||
/** 选中的链接分组 LinkGroup metadata.name 列表 */
|
||||
linkGroups?: string[]
|
||||
/** 备注 */
|
||||
description?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
|
||||
/** 应用基础配置(对应旧 DefaultAppConfigs) */
|
||||
export interface IAppConfig {
|
||||
basicConfig?: {
|
||||
@@ -90,9 +112,6 @@ export interface IHaloGlobalConfig {
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/** 审计模式模拟数据 */
|
||||
export type IMockJson = Record<string, unknown>
|
||||
|
||||
/* ---------- plugin-uni-halo 二维码 / 检查更新 ---------- */
|
||||
|
||||
export interface IQRCodeInfo {
|
||||
@@ -310,4 +329,96 @@ export interface ILoveStoryListReq {
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/* ---------- 小程序链接(plugin-uni-halo mini-program-links) ---------- */
|
||||
|
||||
/** 小程序链接 spec(对齐插件 MiniProgramLinkSpec) */
|
||||
export interface IMiniProgramLinkSpec {
|
||||
/** 小程序名称 */
|
||||
displayName?: string
|
||||
/** 太阳码(小程序码图片 URL,必填) */
|
||||
miniProgramCode?: string
|
||||
/** 小程序地址(跳转链接) */
|
||||
link?: string
|
||||
/** 作者昵称 */
|
||||
authorName?: string
|
||||
/** 作者头像(图片 URL) */
|
||||
avatar?: string
|
||||
/** 作者网站 */
|
||||
website?: string
|
||||
/** 分组(引用分组 metadata.name;空=未分组) */
|
||||
groupName?: string
|
||||
/** 描述 */
|
||||
description?: string
|
||||
/** 预览图(多图) */
|
||||
screenshots?: string[]
|
||||
/** 可见性(公开接口恒为 true) */
|
||||
visible?: boolean
|
||||
/** 来源:manual 手动 / submitted 申请 */
|
||||
source?: string
|
||||
/** 排序权重 */
|
||||
priority?: number
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
/** 小程序链接 */
|
||||
export interface IMiniProgramLink {
|
||||
metadata?: {
|
||||
name?: string
|
||||
creationTimestamp?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
spec?: IMiniProgramLinkSpec
|
||||
}
|
||||
|
||||
export interface IMiniProgramLinkListReq {
|
||||
page?: number
|
||||
size?: number
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export type IMiniProgramLinkListRes = IMiniProgramLink[]
|
||||
|
||||
/** grouped=true 分组返回项 */
|
||||
export interface IMiniProgramLinkGroupVo {
|
||||
/** 分组名(空=未分组) */
|
||||
groupName?: string
|
||||
/** 分组显示名 */
|
||||
displayName?: string
|
||||
links: IMiniProgramLink[]
|
||||
}
|
||||
|
||||
export type IMiniProgramLinkGroupedRes = IMiniProgramLinkGroupVo[]
|
||||
|
||||
/** 分组选项(/types) */
|
||||
export interface IMiniProgramLinkGroupOption {
|
||||
name?: string
|
||||
displayName?: string
|
||||
}
|
||||
|
||||
/** 提交申请表单(公开 POST /submissions,body 为 { spec: {...} }) */
|
||||
export interface IMiniProgramLinkSubmissionForm {
|
||||
/** 小程序名称(必填) */
|
||||
displayName: string
|
||||
/** 太阳码图片 URL(必填) */
|
||||
miniProgramCode: string
|
||||
/** 小程序地址 */
|
||||
link?: string
|
||||
/** 作者昵称 */
|
||||
authorName?: string
|
||||
/** 作者头像 */
|
||||
avatar?: string
|
||||
/** 作者网站 */
|
||||
website?: string
|
||||
/** 分组 */
|
||||
groupName?: string
|
||||
/** 描述 */
|
||||
description?: string
|
||||
/** 申请说明 */
|
||||
applyRemark?: string
|
||||
/** 预览图 */
|
||||
screenshots?: string[]
|
||||
/** 申请人邮箱(非必填,填写校验格式) */
|
||||
email?: string
|
||||
}
|
||||
|
||||
export type ILoveStoryListRes = ILoveStory[]
|
||||
|
||||
+70
-2
@@ -14,6 +14,7 @@ import { getNologinEmail, getOpenid } from '@/utils/auth'
|
||||
import { getPersonalToken } from '@/store/token'
|
||||
import type {
|
||||
IAppConfig,
|
||||
IAuditDataResult,
|
||||
ICommentWidgetConfig,
|
||||
IDoubanDetail,
|
||||
IHaloGlobalConfig,
|
||||
@@ -27,6 +28,10 @@ import type {
|
||||
ILoveStory,
|
||||
ILoveStoryListReq,
|
||||
ILoveStoryListRes,
|
||||
IMiniProgramLink,
|
||||
IMiniProgramLinkGroupedRes,
|
||||
IMiniProgramLinkGroupOption,
|
||||
IMiniProgramLinkSubmissionForm,
|
||||
IQRCodeInfo,
|
||||
IRestrictReadCheckReq,
|
||||
IRestrictReadCheckRes,
|
||||
@@ -53,6 +58,15 @@ export function getAppConfigs() {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取审核模式数据(公开接口;auditModeEnabled=true 时返回选中引用列表,否则 {enabled:false})
|
||||
*/
|
||||
export function getAuditData() {
|
||||
return http.Get<IResponse<IAuditDataResult>>('/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/audit-data', {
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Halo 全局配置信息
|
||||
*/
|
||||
@@ -168,6 +182,60 @@ export function getLoveStories(params: ILoveStoryListReq) {
|
||||
})
|
||||
}
|
||||
|
||||
/* ==================== 小程序链接(plugin-uni-halo) ==================== */
|
||||
|
||||
/**
|
||||
* 获取小程序链接分组列表(grouped=true,仅可见,按分组聚合返回)
|
||||
*/
|
||||
export function getMiniProgramLinkGroupedList() {
|
||||
return http.Get<IResponse<IMiniProgramLinkGroupedRes>>(
|
||||
'/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/mini-program-links',
|
||||
{
|
||||
params: { grouped: true },
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取小程序链接分组选项(/types,仅可见链接引用的分组)
|
||||
*/
|
||||
export function getMiniProgramLinkTypes() {
|
||||
return http.Get<IResponse<IMiniProgramLinkGroupOption[]>>(
|
||||
'/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/mini-program-links/types',
|
||||
{
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取小程序链接详情(仅可见,不存在返回 404)
|
||||
*/
|
||||
export function getMiniProgramLinkDetail(name: string) {
|
||||
return http.Get<IResponse<IMiniProgramLink>>(
|
||||
`/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/mini-program-links/${name}`,
|
||||
{
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交小程序链接申请(公开接口,落库为待审核;受 linkConfig.submissionEnabled 开关控制)
|
||||
*/
|
||||
export function submitMiniProgramLinkApplication(data: IMiniProgramLinkSubmissionForm) {
|
||||
return http.Post<IResponse<unknown>>(
|
||||
'/apis/api.unihalo.ialley.cn/v1alpha1/plugins/plugin-uni-halo/mini-program-links/submissions',
|
||||
{
|
||||
spec: data,
|
||||
},
|
||||
{
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/* ==================== 受限阅读(tools.muyin.site) ==================== */
|
||||
|
||||
/**
|
||||
@@ -284,8 +352,8 @@ export function getDoubanDetail(url: string) {
|
||||
*/
|
||||
export function getCommentWidgetCaptcha() {
|
||||
return http.Get<IResponse<string>>('/apis/api.commentwidget.halo.run/v1alpha1/captcha/-/generate', {
|
||||
cacheFor:0,
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
cacheFor: 0,
|
||||
meta: { requestFrom: RequestFrom.Halo },
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 小程序链接申请弹窗(源自 mini-program-links-design 申请入口规划)
|
||||
* 公开提交到 plugin-uni-halo POST /submissions,落库为待审核(受 linkConfig.submissionEnabled 开关控制)
|
||||
*/
|
||||
import { ref, watch } from 'vue'
|
||||
import { submitMiniProgramLinkApplication } from '@/api/uni-halo'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
show?: boolean
|
||||
}>(), {
|
||||
show: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'on-close', data: { isSubmit: boolean, refresh: boolean }): void
|
||||
}>()
|
||||
|
||||
const isShow = ref(false)
|
||||
|
||||
interface IApplyForm {
|
||||
displayName: string
|
||||
miniProgramCode: string
|
||||
link: string
|
||||
authorName: string
|
||||
avatar: string
|
||||
website: string
|
||||
description: string
|
||||
applyRemark: string
|
||||
email: string
|
||||
}
|
||||
|
||||
const form = ref<IApplyForm>({
|
||||
displayName: '',
|
||||
miniProgramCode: '',
|
||||
link: '',
|
||||
authorName: '',
|
||||
avatar: '',
|
||||
website: '',
|
||||
description: '',
|
||||
applyRemark: '',
|
||||
email: '',
|
||||
})
|
||||
|
||||
const submitting = ref(false)
|
||||
|
||||
function handleResetForm() {
|
||||
form.value = {
|
||||
displayName: '',
|
||||
miniProgramCode: '',
|
||||
link: '',
|
||||
authorName: '',
|
||||
avatar: '',
|
||||
website: '',
|
||||
description: '',
|
||||
applyRemark: '',
|
||||
email: '',
|
||||
}
|
||||
}
|
||||
|
||||
function checkIsUrl(url: string): boolean {
|
||||
return /^https?:\/\//i.test(url)
|
||||
}
|
||||
|
||||
function checkIsEmail(email: string): boolean {
|
||||
return /^[\w.-]+@[\w-]+(?:\.[\w-]+)+$/.test(email)
|
||||
}
|
||||
|
||||
/** 提交校验 */
|
||||
function validateForm(): boolean {
|
||||
if (!form.value.displayName.trim()) {
|
||||
uni.showToast({ icon: 'none', title: '请填写小程序名称' })
|
||||
return false
|
||||
}
|
||||
if (!form.value.miniProgramCode.trim()) {
|
||||
uni.showToast({ icon: 'none', title: '请填写太阳码图片地址' })
|
||||
return false
|
||||
}
|
||||
if (form.value.miniProgramCode.trim() && !checkIsUrl(form.value.miniProgramCode.trim())) {
|
||||
uni.showToast({ icon: 'none', title: '太阳码地址需为 http(s) 链接' })
|
||||
return false
|
||||
}
|
||||
if (form.value.link.trim() && !checkIsUrl(form.value.link.trim())) {
|
||||
uni.showToast({ icon: 'none', title: '小程序地址需为 http(s) 链接' })
|
||||
return false
|
||||
}
|
||||
if (form.value.avatar.trim() && !checkIsUrl(form.value.avatar.trim())) {
|
||||
uni.showToast({ icon: 'none', title: '头像地址需为 http(s) 链接' })
|
||||
return false
|
||||
}
|
||||
if (form.value.website.trim() && !checkIsUrl(form.value.website.trim())) {
|
||||
uni.showToast({ icon: 'none', title: '网站地址需为 http(s) 链接' })
|
||||
return false
|
||||
}
|
||||
if (form.value.email.trim() && !checkIsEmail(form.value.email.trim())) {
|
||||
uni.showToast({ icon: 'none', title: '请输入正确的邮箱地址' })
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/** 提交申请 */
|
||||
async function handleHandle() {
|
||||
if (!validateForm())
|
||||
return
|
||||
|
||||
submitting.value = true
|
||||
uni.showLoading({ title: '正在提交...' })
|
||||
try {
|
||||
await submitMiniProgramLinkApplication({
|
||||
displayName: form.value.displayName.trim(),
|
||||
miniProgramCode: form.value.miniProgramCode.trim(),
|
||||
link: form.value.link.trim() || undefined,
|
||||
authorName: form.value.authorName.trim() || undefined,
|
||||
avatar: form.value.avatar.trim() || undefined,
|
||||
website: form.value.website.trim() || undefined,
|
||||
description: form.value.description.trim() || undefined,
|
||||
applyRemark: form.value.applyRemark.trim() || undefined,
|
||||
email: form.value.email.trim() || undefined,
|
||||
})
|
||||
uni.showToast({ icon: 'none', title: '申请提交成功,等待审核!' })
|
||||
handleClose(true)
|
||||
handleResetForm()
|
||||
}
|
||||
catch (err) {
|
||||
console.error('小程序链接申请提交失败', err)
|
||||
uni.showToast({ icon: 'none', title: '提交失败,请稍后重试!' })
|
||||
}
|
||||
finally {
|
||||
submitting.value = false
|
||||
uni.hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
function handleOnChange(isOpen: boolean) {
|
||||
isShow.value = isOpen
|
||||
if (!isOpen)
|
||||
emit('on-close', { isSubmit: false, refresh: false })
|
||||
}
|
||||
|
||||
function handleClose(refresh = false) {
|
||||
isShow.value = false
|
||||
emit('on-close', { isSubmit: true, refresh })
|
||||
}
|
||||
|
||||
watch(() => props.show, (newVal) => {
|
||||
if (!newVal)
|
||||
return
|
||||
isShow.value = true
|
||||
handleResetForm()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<wd-popup v-model="isShow" position="center" custom-style="width:640rpx;border-radius:12rpx;">
|
||||
<view class="uh-mini-link-apply max-h-[80vh] overflow-y-auto p-8">
|
||||
<view class="modal-title mb-1 flex items-center justify-between">
|
||||
<text class="text-[32rpx] font-bold">申请收录小程序</text>
|
||||
<wd-icon name="close" size="20px" color="#999" @click="handleClose(false)" />
|
||||
</view>
|
||||
<view class="modal-tip mb-6 text-[24rpx] text-[#999]">
|
||||
提交后将在后台审核,审核通过后展示在「小程序」列表中
|
||||
</view>
|
||||
|
||||
<view class="form-item mb-5 flex items-center">
|
||||
<text class="label w-[140rpx] shrink-0 text-[26rpx] text-[#666]">名称 *</text>
|
||||
<input v-model="form.displayName" class="input h-[72rpx] flex-1 rounded-xl bg-[#f5f5f5] px-5 text-[26rpx]" placeholder="请输入小程序名称">
|
||||
</view>
|
||||
|
||||
<view class="form-item mb-5 flex items-center">
|
||||
<text class="label w-[140rpx] shrink-0 text-[26rpx] text-[#666]">太阳码 *</text>
|
||||
<input v-model="form.miniProgramCode" class="input h-[72rpx] flex-1 rounded-xl bg-[#f5f5f5] px-5 text-[26rpx]" placeholder="小程序码图片链接(必填)">
|
||||
</view>
|
||||
|
||||
<view class="form-item mb-5 flex items-center">
|
||||
<text class="label w-[140rpx] shrink-0 text-[26rpx] text-[#666]">小程序地址</text>
|
||||
<input v-model="form.link" class="input h-[72rpx] flex-1 rounded-xl bg-[#f5f5f5] px-5 text-[26rpx]" placeholder="跳转链接(选填)">
|
||||
</view>
|
||||
|
||||
<view class="form-item mb-5 flex items-center">
|
||||
<text class="label w-[140rpx] shrink-0 text-[26rpx] text-[#666]">作者昵称</text>
|
||||
<input v-model="form.authorName" class="input h-[72rpx] flex-1 rounded-xl bg-[#f5f5f5] px-5 text-[26rpx]" placeholder="选填">
|
||||
</view>
|
||||
|
||||
<view class="form-item mb-5 flex items-center">
|
||||
<text class="label w-[140rpx] shrink-0 text-[26rpx] text-[#666]">作者头像</text>
|
||||
<input v-model="form.avatar" class="input h-[72rpx] flex-1 rounded-xl bg-[#f5f5f5] px-5 text-[26rpx]" placeholder="头像图片链接(选填)">
|
||||
</view>
|
||||
|
||||
<view class="form-item mb-5 flex items-center">
|
||||
<text class="label w-[140rpx] shrink-0 text-[26rpx] text-[#666]">作者网站</text>
|
||||
<input v-model="form.website" class="input h-[72rpx] flex-1 rounded-xl bg-[#f5f5f5] px-5 text-[26rpx]" placeholder="选填">
|
||||
</view>
|
||||
|
||||
<view class="form-item mb-5">
|
||||
<text class="label mb-2 block text-[26rpx] text-[#666]">描述</text>
|
||||
<textarea v-model="form.description" class="content-input w-full rounded-xl bg-[#f5f5f5] p-5 text-[26rpx]" placeholder="介绍一下这个小程序(选填)" :maxlength="200" />
|
||||
</view>
|
||||
|
||||
<view class="form-item mb-5">
|
||||
<text class="label mb-2 block text-[26rpx] text-[#666]">申请说明</text>
|
||||
<textarea v-model="form.applyRemark" class="content-input w-full rounded-xl bg-[#f5f5f5] p-5 text-[26rpx]" placeholder="方便管理员了解申请意图(选填)" :maxlength="200" />
|
||||
</view>
|
||||
|
||||
<view class="form-item mb-5 flex items-center">
|
||||
<text class="label w-[140rpx] shrink-0 text-[26rpx] text-[#666]">邮箱</text>
|
||||
<input v-model="form.email" class="input h-[72rpx] flex-1 rounded-xl bg-[#f5f5f5] px-5 text-[26rpx]" placeholder="审核结果通知(选填)">
|
||||
</view>
|
||||
|
||||
<view class="submit-btn my-6">
|
||||
<wd-button type="primary" block size="medium" :loading="submitting" @click="handleHandle">
|
||||
提交申请
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.uh-mini-link-apply {
|
||||
.content-input {
|
||||
min-height: 140rpx;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -5,7 +5,6 @@
|
||||
*/
|
||||
import { computed, ref } from 'vue'
|
||||
import { onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
||||
import dayjs from 'dayjs'
|
||||
import { getPostList } from '@/api/halo'
|
||||
import { useAppConfigStore } from '@/store/appConfig'
|
||||
import { useSettingStore } from '@/store/setting'
|
||||
@@ -23,8 +22,7 @@ definePage({
|
||||
const appConfigStore = useAppConfigStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const calcAuditModeEnabled = computed(() => !!appConfigStore.configs.auditConfig?.auditModeEnabled)
|
||||
const mockJson = computed(() => appConfigStore.mockJson)
|
||||
const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
|
||||
const globalAppSettings = computed(() => settingStore.settings)
|
||||
|
||||
/* ---------------- 状态 ---------------- */
|
||||
@@ -106,44 +104,26 @@ function handleUniqueCacheDatalist(list: IPost[]): IPost[] {
|
||||
/* ---------------- 数据加载 ---------------- */
|
||||
async function handleGetData() {
|
||||
if (calcAuditModeEnabled.value) {
|
||||
const archivesMock = mockJson.value.archives as { list?: { time?: string, cover?: string, title?: string, desc?: string }[] } | undefined
|
||||
const dataListMock: IPost[] = (archivesMock?.list || []).map((item) => {
|
||||
const date = new Date(item.time || Date.now())
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth() + 1
|
||||
return {
|
||||
metadata: {
|
||||
name: String(Date.now() * Math.random()),
|
||||
labels: {
|
||||
[postLabelYearKey]: String(year),
|
||||
[postLabelMonthKey]: String(month),
|
||||
},
|
||||
},
|
||||
spec: {
|
||||
title: item.title || '',
|
||||
slug: '',
|
||||
cover: item.cover,
|
||||
pinned: false,
|
||||
publishTime: item.time,
|
||||
deleted: false,
|
||||
publish: true,
|
||||
allowComment: true,
|
||||
visible: 'PUBLIC',
|
||||
priority: 0,
|
||||
categories: [],
|
||||
tags: [],
|
||||
},
|
||||
status: { permalink: '', inProgress: false, excerpt: item.desc },
|
||||
stats: { visit: 0 },
|
||||
}
|
||||
})
|
||||
const posts = handleGetPosts(dataListMock)
|
||||
dataList.value = handleGetShowDataList(posts)
|
||||
cacheDataList.value = dataListMock
|
||||
loading.value = 'success'
|
||||
loadMoreText.value = '呜呜,没有更多数据啦~'
|
||||
uni.hideLoading()
|
||||
uni.stopPullDownRefresh()
|
||||
// 审核模式:真实文章按 audit-data posts 过滤(数组顺序即展示顺序)
|
||||
const auditPostNames = appConfigStore.auditData.spec?.posts || []
|
||||
try {
|
||||
const res = await getPostList({ page: 1, size: 99999, sort: ['spec.publishTime,desc'] })
|
||||
const filtered = res.data.items.filter(item => auditPostNames.includes(item.metadata.name))
|
||||
const orderMap = new Map(auditPostNames.map((name, index) => [name, index]))
|
||||
filtered.sort((a, b) => (orderMap.get(a.metadata.name) ?? 999) - (orderMap.get(b.metadata.name) ?? 999))
|
||||
const posts = handleGetPosts(filtered)
|
||||
dataList.value = handleGetShowDataList(posts)
|
||||
cacheDataList.value = filtered
|
||||
loading.value = 'success'
|
||||
loadMoreText.value = '呜呜,没有更多数据啦~'
|
||||
uni.hideLoading()
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
loading.value = 'error'
|
||||
loadMoreText.value = '加载失败,请下拉刷新!'
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ const bloggerInfo = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const calcAuditModeEnabled = computed(() => !!haloConfigs.value.auditConfig?.auditModeEnabled)
|
||||
const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
|
||||
|
||||
const calcIsShowComment = computed(() => !!postDetailConfig.value?.showComment)
|
||||
|
||||
@@ -306,13 +306,13 @@ async function getVerificationCode() {
|
||||
|
||||
/* ---------------- 评论 ---------------- */
|
||||
function handleToComment() {
|
||||
console.log('calcIsShowComment.value',calcIsShowComment.value)
|
||||
console.log('result.value',result.value)
|
||||
if (!result.value){
|
||||
return
|
||||
console.log('calcIsShowComment.value', calcIsShowComment.value)
|
||||
console.log('result.value', result.value)
|
||||
if (!result.value) {
|
||||
return
|
||||
}
|
||||
if (!calcIsShowComment.value){
|
||||
return
|
||||
if (!calcIsShowComment.value) {
|
||||
return
|
||||
}
|
||||
if (!result.value.spec.allowComment) {
|
||||
uni.showToast({ icon: 'none', title: '文章已开启禁止评论!' })
|
||||
@@ -715,7 +715,7 @@ const globalAppSettings = computed(() => settingStore.settings)
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.app-page {
|
||||
.app-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@@ -1,16 +1,20 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 友情链接页(源自旧项目 pagesA/friend-links,新建复刻)
|
||||
* 展示友链列表(色彩版/简洁版),支持分组名解析、详情弹窗、申请入口
|
||||
* 友情链接页(源自旧项目 pagesA/friend-links,新建复刻 + tabs 改造)
|
||||
* 顶部 tabs 切换「站点 / 小程序」:
|
||||
* - 站点:plugin-links 博客友链(色彩版/简洁版/详情弹窗/申请入口,保持现状)
|
||||
* - 小程序:plugin-uni-halo 小程序链接(按分组聚合展示 + 详情弹窗 + 申请收录弹窗)
|
||||
*/
|
||||
import { computed, ref } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { onLoad, onPullDownRefresh, onReachBottom } from '@dcloudio/uni-app'
|
||||
import { getFriendLinkGroupList, getFriendLinkList } from '@/api/halo'
|
||||
import { getMiniProgramLinkGroupedList } from '@/api/uni-halo'
|
||||
import { useAppConfigStore } from '@/store/appConfig'
|
||||
import { useSettingStore } from '@/store/setting'
|
||||
import { checkAvatarUrl, checkImageUrl } from '@/utils/url'
|
||||
import { usePluginAvailable } from '@/utils/plugin'
|
||||
import { NeedPluginIds, usePluginAvailable } from '@/utils/plugin'
|
||||
import type { ILink, ILinkGroup } from '@/api/types/halo'
|
||||
import type { IMiniProgramLink, IMiniProgramLinkGroupVo } from '@/api/types/uni-halo'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
@@ -25,10 +29,28 @@ const settingStore = useSettingStore()
|
||||
const haloPluginConfigs = computed(() => appConfigStore.configs.pluginConfig)
|
||||
const globalAppSettings = computed(() => settingStore.settings)
|
||||
|
||||
/** 依赖插件(plugin-links) */
|
||||
const uniHaloPluginId = 'plugin-links'
|
||||
const uniHaloPluginAvailable = ref(true)
|
||||
/* ---------------- 依赖插件 ---------------- */
|
||||
/** 站点 tab:plugin-links */
|
||||
const sitePluginId = NeedPluginIds.PluginLinks
|
||||
const sitePluginAvailable = ref(true)
|
||||
/** 小程序 tab:plugin-uni-halo */
|
||||
const miniPluginId = NeedPluginIds.PluginUniHalo
|
||||
const miniPluginAvailable = ref(true)
|
||||
|
||||
/* ---------------- tabs ---------------- */
|
||||
const activeTabIndex = ref(0)
|
||||
|
||||
function handleOnTabChange(e: { index: number }) {
|
||||
activeTabIndex.value = e.index
|
||||
}
|
||||
|
||||
// 审核模式下小程序 tab 隐藏,强制停留在站点 tab
|
||||
watch(() => appConfigStore.auditModeEnabled, (enabled) => {
|
||||
if (enabled)
|
||||
activeTabIndex.value = 0
|
||||
})
|
||||
|
||||
/* ==================== 站点 tab(plugin-links) ==================== */
|
||||
/* ---------------- 状态 ---------------- */
|
||||
const loading = ref<'loading' | 'success' | 'error'>('loading')
|
||||
const queryParams = ref({ size: 10, page: 1 })
|
||||
@@ -68,7 +90,13 @@ async function handleGetData() {
|
||||
try {
|
||||
const res = await getFriendLinkList({ ...queryParams.value })
|
||||
hasNext.value = res.data.hasNext
|
||||
const list = res.data.items.map(item => ({
|
||||
// 审核模式:站点链接仅展示选中 LinkGroup 分组内的
|
||||
let items = res.data.items
|
||||
if (appConfigStore.auditModeEnabled) {
|
||||
const auditGroupNames = appConfigStore.auditData.spec?.linkGroups || []
|
||||
items = items.filter(item => item.spec.groupName && auditGroupNames.includes(item.spec.groupName))
|
||||
}
|
||||
const list = items.map(item => ({
|
||||
...item,
|
||||
spec: {
|
||||
...item.spec,
|
||||
@@ -94,7 +122,7 @@ async function handleGetData() {
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- 交互 ---------------- */
|
||||
/* ---------------- 站点交互 ---------------- */
|
||||
function handleOnLinkEvent(link: ILink) {
|
||||
detail.value = { show: true, data: link }
|
||||
}
|
||||
@@ -133,34 +161,157 @@ function calcSiteThumbnail(val?: string): string {
|
||||
return `https://image.thum.io/get/width/1000/crop/800/${_val}`
|
||||
}
|
||||
|
||||
/* ---------------- 生命周期 ---------------- */
|
||||
onLoad(async () => {
|
||||
uniHaloPluginAvailable.value = await usePluginAvailable(uniHaloPluginId)
|
||||
if (!uniHaloPluginAvailable.value) {
|
||||
uni.stopPullDownRefresh()
|
||||
/* ==================== 小程序 tab(plugin-uni-halo) ==================== */
|
||||
/* ---------------- 状态 ---------------- */
|
||||
const miniLoading = ref<'loading' | 'success' | 'error'>('loading')
|
||||
const miniGroups = ref<IMiniProgramLinkGroupVo[]>([])
|
||||
const miniDetail = ref<{ show: boolean, data: IMiniProgramLink | null }>({ show: false, data: null })
|
||||
const applyShow = ref(false)
|
||||
|
||||
/* ---------------- 数据加载 ---------------- */
|
||||
async function handleGetMiniProgramLinks() {
|
||||
miniLoading.value = 'loading'
|
||||
try {
|
||||
const res = await getMiniProgramLinkGroupedList()
|
||||
miniGroups.value = res.data || []
|
||||
setTimeout(() => {
|
||||
miniLoading.value = 'success'
|
||||
}, 500)
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
miniLoading.value = 'error'
|
||||
}
|
||||
finally {
|
||||
setTimeout(() => {
|
||||
uni.stopPullDownRefresh()
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
|
||||
function handleOnMiniLinkEvent(link: IMiniProgramLink) {
|
||||
miniDetail.value = { show: true, data: link }
|
||||
}
|
||||
|
||||
function handleOpenApply() {
|
||||
applyShow.value = true
|
||||
}
|
||||
|
||||
function handleApplyClose(data: { isSubmit: boolean, refresh: boolean }) {
|
||||
applyShow.value = false
|
||||
if (data.refresh)
|
||||
handleGetMiniProgramLinks()
|
||||
}
|
||||
|
||||
/** 复制小程序地址 */
|
||||
function handleCopyMiniProgramCode(link: IMiniProgramLink) {
|
||||
const url = link.spec?.link
|
||||
if (!url) {
|
||||
uni.showToast({ icon: 'none', title: '该小程序未填写跳转地址' })
|
||||
return
|
||||
}
|
||||
handleGetLinkGroupData()
|
||||
uni.setClipboardData({
|
||||
data: url,
|
||||
showToast: false,
|
||||
success: () => {
|
||||
uni.showToast({ icon: 'none', title: '地址复制成功!' })
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({ icon: 'none', title: '复制失败!' })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** 预览太阳码(支持长按识别/保存) */
|
||||
function handlePreviewMiniProgramCode(link: IMiniProgramLink) {
|
||||
const code = link.spec?.miniProgramCode
|
||||
if (!code)
|
||||
return
|
||||
uni.previewImage({
|
||||
urls: [checkImageUrl(code)],
|
||||
current: checkImageUrl(code),
|
||||
})
|
||||
}
|
||||
|
||||
/** 保存太阳码到相册 */
|
||||
function handleSaveMiniProgramCode(link: IMiniProgramLink) {
|
||||
const code = link.spec?.miniProgramCode
|
||||
if (!code)
|
||||
return
|
||||
uni.showLoading({ title: '保存中...' })
|
||||
uni.downloadFile({
|
||||
url: checkImageUrl(code),
|
||||
success: (res) => {
|
||||
uni.saveImageToPhotosAlbum({
|
||||
filePath: res.tempFilePath,
|
||||
success: () => {
|
||||
uni.showToast({ icon: 'none', title: '已保存到相册' })
|
||||
},
|
||||
fail: () => {
|
||||
uni.showModal({
|
||||
title: '保存失败',
|
||||
content: '请检查相册权限后重试',
|
||||
showCancel: false,
|
||||
})
|
||||
},
|
||||
complete: () => {
|
||||
uni.hideLoading()
|
||||
},
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
uni.hideLoading()
|
||||
uni.showToast({ icon: 'none', title: '图片下载失败' })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/* ---------------- 生命周期 ---------------- */
|
||||
onLoad(async () => {
|
||||
;[sitePluginAvailable.value, miniPluginAvailable.value] = await Promise.all([
|
||||
usePluginAvailable(sitePluginId),
|
||||
usePluginAvailable(miniPluginId),
|
||||
])
|
||||
if (sitePluginAvailable.value)
|
||||
handleGetLinkGroupData()
|
||||
if (miniPluginAvailable.value)
|
||||
handleGetMiniProgramLinks()
|
||||
if (!sitePluginAvailable.value && !miniPluginAvailable.value)
|
||||
uni.stopPullDownRefresh()
|
||||
})
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
if (!uniHaloPluginAvailable.value) {
|
||||
uni.stopPullDownRefresh()
|
||||
return
|
||||
if (activeTabIndex.value === 0) {
|
||||
if (!sitePluginAvailable.value) {
|
||||
uni.stopPullDownRefresh()
|
||||
return
|
||||
}
|
||||
isLoadMore.value = false
|
||||
queryParams.value.page = 1
|
||||
dataList.value = []
|
||||
handleGetData()
|
||||
}
|
||||
else {
|
||||
if (!miniPluginAvailable.value) {
|
||||
uni.stopPullDownRefresh()
|
||||
return
|
||||
}
|
||||
handleGetMiniProgramLinks()
|
||||
}
|
||||
isLoadMore.value = false
|
||||
queryParams.value.page = 1
|
||||
dataList.value = []
|
||||
handleGetData()
|
||||
})
|
||||
|
||||
onReachBottom(() => {
|
||||
if (!uniHaloPluginAvailable.value)
|
||||
return
|
||||
if (hasNext.value) {
|
||||
queryParams.value.page += 1
|
||||
isLoadMore.value = true
|
||||
handleGetData()
|
||||
if (activeTabIndex.value === 0) {
|
||||
if (!sitePluginAvailable.value)
|
||||
return
|
||||
if (hasNext.value) {
|
||||
queryParams.value.page += 1
|
||||
isLoadMore.value = true
|
||||
handleGetData()
|
||||
}
|
||||
else {
|
||||
uni.showToast({ icon: 'none', title: '没有更多数据了' })
|
||||
}
|
||||
}
|
||||
else {
|
||||
uni.showToast({ icon: 'none', title: '没有更多数据了' })
|
||||
@@ -170,99 +321,241 @@ onReachBottom(() => {
|
||||
|
||||
<template>
|
||||
<view class="app-page min-h-screen w-screen flex flex-col" style="background-color: #fafafd;">
|
||||
<uh-plugin-unavailable
|
||||
v-if="!uniHaloPluginAvailable"
|
||||
:plugin-id="uniHaloPluginId"
|
||||
error-text="检测到当前插件没有安装或者启用,无法使用友情链接功能哦,请联系管理员"
|
||||
@on-refresh="handleGetLinkGroupData"
|
||||
/>
|
||||
<!-- 顶部 tabs -->
|
||||
<view class="tabs-wrap sticky top-0 z-10 bg-white px-6">
|
||||
<wd-tabs v-model="activeTabIndex" align="left" @change="handleOnTabChange">
|
||||
<wd-tab title="站点" />
|
||||
<wd-tab v-if="!appConfigStore.auditModeEnabled" title="小程序" />
|
||||
</wd-tabs>
|
||||
</view>
|
||||
|
||||
<!-- ==================== 站点 tab ==================== -->
|
||||
<template v-if="activeTabIndex === 0">
|
||||
<uh-plugin-unavailable
|
||||
v-if="!sitePluginAvailable"
|
||||
:plugin-id="sitePluginId"
|
||||
error-text="检测到当前插件没有安装或者启用,无法使用友情链接功能哦,请联系管理员"
|
||||
@on-refresh="handleGetLinkGroupData"
|
||||
/>
|
||||
<template v-else>
|
||||
<view v-if="loading !== 'success'" class="loading-wrap min-h-screen p-3">
|
||||
<wd-skeleton :row="5" :animated="true" />
|
||||
</view>
|
||||
|
||||
<view v-else class="content pt-6" :class="{ 'bg-white': dataList.length !== 0 }">
|
||||
<view v-if="dataList.length === 0" class="h-[60vh] flex items-center justify-center content-empty">
|
||||
<wd-empty description="啊偶,博主还没有朋友呢~" />
|
||||
</view>
|
||||
|
||||
<!-- 友链列表 -->
|
||||
<view v-else class="link-list px-6">
|
||||
<view v-for="(link, index) in dataList" :key="index">
|
||||
<!-- 色彩版 -->
|
||||
<view
|
||||
v-if="!globalAppSettings.links.useSimple"
|
||||
class="info flex bg-white p-3"
|
||||
:class="{ 'border-b-2 border-[#f5f5f5]': index !== dataList.length - 1 }"
|
||||
@click="handleOnLinkEvent(link)"
|
||||
>
|
||||
<image class="link-logo h-[140rpx] w-[140rpx] shrink-0 rounded-xl" :src="link.spec.logo" mode="aspectFill" />
|
||||
<view class="info-detail flex flex-1 flex-col justify-center pl-7">
|
||||
<view class="link-card-name text-[30rpx] text-[#f44336] font-bold">
|
||||
<text class="group-tag mr-3 rounded-md px-1.5 py-0.5 text-[20rpx] text-white font-normal" style="background: linear-gradient(135deg, #64b5f6, #2196f3);">{{ link.spec.groupName || '暂未分组' }}</text>
|
||||
{{ link.spec.displayName }}
|
||||
</view>
|
||||
<view class="link-card-url mt-1 overflow-hidden text-ellipsis whitespace-nowrap text-[24rpx] text-[#666]">
|
||||
站点地址:{{ link.spec.url }}
|
||||
</view>
|
||||
<view class="link-card-desc mt-2 overflow-hidden text-ellipsis whitespace-nowrap text-[24rpx] text-[#303133]">
|
||||
博客简介:{{ link.spec.description || '这个博主很懒,没写简介~' }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 简洁版 -->
|
||||
<view v-else class="link-card mb-6 flex items-center rounded-xl bg-white p-6 shadow-sm" @click="handleOnLinkEvent(link)">
|
||||
<image class="logo h-[80rpx] w-[80rpx] shrink-0 border-6 border-white rounded-xl" :src="link.spec.logo" mode="aspectFill" />
|
||||
<view class="link-info flex-1 pl-6">
|
||||
<view class="name text-[30rpx] text-[#303133] font-bold">
|
||||
{{ link.spec.displayName }}
|
||||
</view>
|
||||
<view class="desc mt-3 overflow-hidden text-ellipsis whitespace-nowrap text-[24rpx] text-[#909399]">
|
||||
{{ link.spec.description }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 悬浮按钮 -->
|
||||
<view class="flot-buttons fixed bottom-[100rpx] right-8 z-999 flex flex-col gap-1.5">
|
||||
<view class="fab-btn h-[72rpx] w-[72rpx] flex items-center justify-center rounded-full bg-white shadow-sm" @click="handleToTopPage()">
|
||||
<wd-icon name="arrow-up" size="20px" color="#03a9f4" />
|
||||
</view>
|
||||
<view v-if="(haloPluginConfigs?.linksSubmitPlugin as { enabled?: boolean } | undefined)?.enabled" class="fab-btn h-[72rpx] w-[72rpx] flex items-center justify-center rounded-full bg-white shadow-sm" @click="toSubmitLinkPage">
|
||||
<wd-icon name="edit" size="20px" color="#ff9800" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<wd-popup v-model="detail.show" position="center" custom-style="width:640rpx;border-radius:12rpx;">
|
||||
<view v-if="detail.data" class="poup p-9">
|
||||
<view class="info flex">
|
||||
<image class="poup-logo h-[140rpx] w-[140rpx] shrink-0 rounded-full" :src="checkImageUrl(detail.data.spec.logo)" mode="aspectFill" />
|
||||
<view class="poup-info ml-6 flex flex-1 flex-col justify-center">
|
||||
<view class="poup-name text-[34rpx] font-bold">
|
||||
{{ detail.data.spec.displayName }}
|
||||
</view>
|
||||
<view class="poup-tag mt-2 text-[24rpx] text-[#999]">
|
||||
{{ detail.data.spec.groupName }}
|
||||
</view>
|
||||
<view class="poup-link mt-3" @click="handleCopyLink(detail.data)">
|
||||
<text class="poup-url text-[24rpx] text-[#ff9800]">{{ detail.data.spec.url }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="poup-desc mt-5 text-[28rpx] text-[#555] leading-[1.6]">
|
||||
博客简介:{{ detail.data.spec.description || '这个博主很懒,没写简介~' }}
|
||||
</view>
|
||||
<image class="poup-img mt-6 h-[320rpx] w-[568rpx] rounded-xl" :src="calcSiteThumbnail(detail.data.spec.url)" mode="aspectFill" />
|
||||
</view>
|
||||
</wd-popup>
|
||||
|
||||
<view class="load-text py-5 text-center text-[24rpx] text-[#999]">
|
||||
{{ loadMoreText }}
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- ==================== 小程序 tab ==================== -->
|
||||
<template v-else>
|
||||
<view v-if="loading !== 'success'" class="loading-wrap min-h-screen p-3">
|
||||
<wd-skeleton :row="5" :animated="true" />
|
||||
</view>
|
||||
|
||||
<view v-else class="content pt-6" :class="{ 'bg-white': dataList.length !== 0 }">
|
||||
<view v-if="dataList.length === 0" class="h-[60vh] flex items-center justify-center content-empty">
|
||||
<wd-empty description="啊偶,博主还没有朋友呢~" />
|
||||
<uh-plugin-unavailable
|
||||
v-if="!miniPluginAvailable"
|
||||
:plugin-id="miniPluginId"
|
||||
error-text="检测到当前插件没有安装或者启用,无法使用小程序链接功能哦,请联系管理员"
|
||||
@on-refresh="handleGetMiniProgramLinks"
|
||||
/>
|
||||
<template v-else>
|
||||
<view v-if="miniLoading !== 'success'" class="loading-wrap min-h-screen p-3">
|
||||
<wd-skeleton :row="5" :animated="true" />
|
||||
</view>
|
||||
|
||||
<!-- 友链列表 -->
|
||||
<view v-else class="link-list px-6">
|
||||
<view v-for="(link, index) in dataList" :key="index">
|
||||
<!-- 色彩版 -->
|
||||
<view
|
||||
v-if="!globalAppSettings.links.useSimple"
|
||||
class="info flex bg-white p-3"
|
||||
:class="{ 'border-b-2 border-[#f5f5f5]': index !== dataList.length - 1 }"
|
||||
@click="handleOnLinkEvent(link)"
|
||||
>
|
||||
<image class="link-logo h-[140rpx] w-[140rpx] shrink-0 rounded-xl" :src="link.spec.logo" mode="aspectFill" />
|
||||
<view class="info-detail flex flex-1 flex-col justify-center pl-7">
|
||||
<view class="link-card-name text-[30rpx] text-[#f44336] font-bold">
|
||||
<text class="group-tag mr-3 rounded-md px-1.5 py-0.5 text-[20rpx] text-white font-normal" style="background: linear-gradient(135deg, #64b5f6, #2196f3);">{{ link.spec.groupName || '暂未分组' }}</text>
|
||||
{{ link.spec.displayName }}
|
||||
</view>
|
||||
<view class="link-card-url mt-1 overflow-hidden text-ellipsis whitespace-nowrap text-[24rpx] text-[#666]">
|
||||
站点地址:{{ link.spec.url }}
|
||||
</view>
|
||||
<view class="link-card-desc mt-2 overflow-hidden text-ellipsis whitespace-nowrap text-[24rpx] text-[#303133]">
|
||||
博客简介:{{ link.spec.description || '这个博主很懒,没写简介~' }}
|
||||
</view>
|
||||
<view v-else class="content flex flex-1 flex-col">
|
||||
<!-- 空态 -->
|
||||
<view v-if="miniGroups.length === 0" class="h-[60vh] flex items-center justify-center content-empty">
|
||||
<wd-empty description="还没有收录的小程序呢~" />
|
||||
</view>
|
||||
|
||||
<!-- 分组列表 -->
|
||||
<view v-else class="mini-link-list flex-1 px-6 py-4">
|
||||
<view v-for="group in miniGroups" :key="group.groupName || 'ungrouped'" class="group-item mb-8">
|
||||
<view class="group-title mb-4 flex items-center">
|
||||
<text class="mr-2 inline-block h-[28rpx] w-[8rpx] rounded-full" style="background-color:#2196f3;" />
|
||||
<text class="text-[30rpx] text-[#303133] font-bold">{{ group.displayName || '未分组' }}</text>
|
||||
<text class="ml-3 text-[24rpx] text-[#999]">({{ group.links.length }})</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 简洁版 -->
|
||||
<view v-else class="link-card mb-6 flex items-center rounded-xl bg-white p-6 shadow-sm" @click="handleOnLinkEvent(link)">
|
||||
<image class="logo h-[80rpx] w-[80rpx] shrink-0 border-6 border-white rounded-xl" :src="link.spec.logo" mode="aspectFill" />
|
||||
<view class="link-info flex-1 pl-6">
|
||||
<view class="name text-[30rpx] text-[#303133] font-bold">
|
||||
{{ link.spec.displayName }}
|
||||
</view>
|
||||
<view class="desc mt-3 overflow-hidden text-ellipsis whitespace-nowrap text-[24rpx] text-[#909399]">
|
||||
{{ link.spec.description }}
|
||||
<view class="group-cards flex flex-col gap-4">
|
||||
<view
|
||||
v-for="link in group.links"
|
||||
:key="link.metadata?.name"
|
||||
class="mini-card flex items-center rounded-xl bg-white p-4 shadow-sm"
|
||||
@click="handleOnMiniLinkEvent(link)"
|
||||
>
|
||||
<image
|
||||
class="mini-code h-[120rpx] w-[120rpx] shrink-0 rounded-lg"
|
||||
:src="checkImageUrl(link.spec?.miniProgramCode)"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="mini-info flex flex-1 flex-col pl-5">
|
||||
<view class="mini-name overflow-hidden text-ellipsis whitespace-nowrap text-[30rpx] text-[#303133] font-bold">
|
||||
{{ link.spec?.displayName }}
|
||||
</view>
|
||||
<view v-if="link.spec?.authorName" class="mini-author mt-1 overflow-hidden text-ellipsis whitespace-nowrap text-[24rpx] text-[#999]">
|
||||
{{ link.spec.authorName }}
|
||||
</view>
|
||||
<view class="mini-desc mt-1 overflow-hidden text-ellipsis whitespace-nowrap text-[24rpx] text-[#666]">
|
||||
{{ link.spec?.description || '暂无简介~' }}
|
||||
</view>
|
||||
</view>
|
||||
<wd-icon name="arrow-right" size="16px" color="#c0c4cc" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 悬浮按钮 -->
|
||||
<view class="flot-buttons fixed bottom-[100rpx] right-8 z-999 flex flex-col gap-1.5">
|
||||
<view class="fab-btn h-[72rpx] w-[72rpx] flex items-center justify-center rounded-full bg-white shadow-sm" @click="handleToTopPage()">
|
||||
<wd-icon name="arrow-up" size="20px" color="#03a9f4" />
|
||||
</view>
|
||||
<view v-if="(haloPluginConfigs?.linksSubmitPlugin as { enabled?: boolean } | undefined)?.enabled" class="fab-btn h-[72rpx] w-[72rpx] flex items-center justify-center rounded-full bg-white shadow-sm" @click="toSubmitLinkPage">
|
||||
<wd-icon name="edit" size="20px" color="#ff9800" />
|
||||
<!-- 申请收录悬浮按钮 -->
|
||||
<view class="apply-btn-wrap fixed bottom-[100rpx] right-8 z-999">
|
||||
<view class="apply-btn h-[88rpx] flex items-center rounded-full bg-[#2196f3] px-6 shadow-sm" @click="handleOpenApply">
|
||||
<wd-icon name="add" size="20px" color="#fff" />
|
||||
<text class="ml-2 text-[26rpx] text-white">申请收录</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<wd-popup v-model="detail.show" position="center" custom-style="width:640rpx;border-radius:12rpx;">
|
||||
<view v-if="detail.data" class="poup p-9">
|
||||
<view class="info flex">
|
||||
<image class="poup-logo h-[140rpx] w-[140rpx] shrink-0 rounded-full" :src="checkImageUrl(detail.data.spec.logo)" mode="aspectFill" />
|
||||
<view class="poup-info ml-6 flex flex-1 flex-col justify-center">
|
||||
<view class="poup-name text-[34rpx] font-bold">
|
||||
{{ detail.data.spec.displayName }}
|
||||
</view>
|
||||
<view class="poup-tag mt-2 text-[24rpx] text-[#999]">
|
||||
{{ detail.data.spec.groupName }}
|
||||
</view>
|
||||
<view class="poup-link mt-3" @click="handleCopyLink(detail.data)">
|
||||
<text class="poup-url text-[24rpx] text-[#ff9800]">{{ detail.data.spec.url }}</text>
|
||||
</view>
|
||||
<!-- 小程序详情弹窗 -->
|
||||
<wd-popup v-model="miniDetail.show" position="center" custom-style="width:640rpx;border-radius:12rpx;">
|
||||
<view v-if="miniDetail.data" class="mini-poup p-8">
|
||||
<!-- 太阳码大图(点击预览/长按保存) -->
|
||||
<view class="code-area flex flex-col items-center">
|
||||
<image
|
||||
class="code-img h-[320rpx] w-[320rpx] rounded-xl"
|
||||
:src="checkImageUrl(miniDetail.data.spec?.miniProgramCode)"
|
||||
mode="aspectFill"
|
||||
@click="handlePreviewMiniProgramCode(miniDetail.data)"
|
||||
@longpress="handleSaveMiniProgramCode(miniDetail.data)"
|
||||
/>
|
||||
<view class="code-tip mt-3 flex items-center text-[24rpx] text-[#999]">
|
||||
<wd-icon name="picture" size="14px" color="#999" />
|
||||
<text class="ml-1">点击预览,长按保存太阳码</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="poup-desc mt-5 text-[28rpx] text-[#555] leading-[1.6]">
|
||||
博客简介:{{ detail.data.spec.description || '这个博主很懒,没写简介~' }}
|
||||
|
||||
<!-- 名称与分组 -->
|
||||
<view class="mini-head mt-5 flex items-center">
|
||||
<text class="mini-name text-[34rpx] text-[#303133] font-bold">{{ miniDetail.data.spec?.displayName }}</text>
|
||||
<text v-if="miniDetail.data.spec?.groupName" class="group-tag ml-3 rounded-md px-2 py-0.5 text-[20rpx] text-white" style="background: linear-gradient(135deg, #64b5f6, #2196f3);">
|
||||
{{ miniDetail.data.spec.groupName }}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 描述 -->
|
||||
<view v-if="miniDetail.data.spec?.description" class="mini-desc mt-4 text-[28rpx] text-[#555] leading-[1.6]">
|
||||
{{ miniDetail.data.spec.description }}
|
||||
</view>
|
||||
|
||||
<!-- 作者信息 -->
|
||||
<view v-if="miniDetail.data.spec?.authorName || miniDetail.data.spec?.avatar || miniDetail.data.spec?.website" class="mini-author-info mt-5 flex items-center rounded-xl bg-[#f5f5f5] p-4">
|
||||
<image v-if="miniDetail.data.spec?.avatar" class="author-avatar h-[72rpx] w-[72rpx] shrink-0 rounded-full" :src="checkAvatarUrl(miniDetail.data.spec.avatar)" mode="aspectFill" />
|
||||
<view class="author-detail ml-4 flex flex-1 flex-col">
|
||||
<text v-if="miniDetail.data.spec?.authorName" class="author-name text-[28rpx] text-[#303133] font-medium">{{ miniDetail.data.spec.authorName }}</text>
|
||||
<text v-if="miniDetail.data.spec?.website" class="author-website mt-1 overflow-hidden text-ellipsis whitespace-nowrap text-[24rpx] text-[#999]" @click="handleCopyMiniProgramCode(miniDetail.data)">
|
||||
网站:{{ miniDetail.data.spec.website }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 小程序地址 -->
|
||||
<view v-if="miniDetail.data.spec?.link" class="mini-link mt-5 flex items-center justify-between rounded-xl bg-[#fff8f0] p-4">
|
||||
<view class="link-text flex-1 overflow-hidden text-ellipsis whitespace-nowrap text-[26rpx] text-[#ff9800]">
|
||||
{{ miniDetail.data.spec.link }}
|
||||
</view>
|
||||
<text class="ml-3 shrink-0 text-[26rpx] text-[#ff9800]" @click="handleCopyMiniProgramCode(miniDetail.data)">复制</text>
|
||||
</view>
|
||||
|
||||
<!-- 预览图轮播 -->
|
||||
<view v-if="miniDetail.data.spec?.screenshots?.length" class="mini-screenshots mt-6">
|
||||
<swiper class="screenshots-swiper h-[360rpx] w-full" indicator-dots circular>
|
||||
<swiper-item v-for="(img, idx) in miniDetail.data.spec.screenshots" :key="idx">
|
||||
<image class="screenshot-img h-full w-full rounded-xl" :src="checkImageUrl(img)" mode="aspectFill" @click="handlePreviewMiniProgramCode({ spec: { miniProgramCode: img } } as IMiniProgramLink)" />
|
||||
</swiper-item>
|
||||
</swiper>
|
||||
</view>
|
||||
<image class="poup-img mt-6 h-[320rpx] w-[568rpx] rounded-xl" :src="calcSiteThumbnail(detail.data.spec.url)" mode="aspectFill" />
|
||||
</view>
|
||||
</wd-popup>
|
||||
|
||||
<view class="load-text py-5 text-center text-[24rpx] text-[#999]">
|
||||
{{ loadMoreText }}
|
||||
</view>
|
||||
</view>
|
||||
<!-- 小程序链接申请弹窗 -->
|
||||
<uh-mini-link-apply :show="applyShow" @on-close="handleApplyClose" />
|
||||
</template>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -20,7 +20,7 @@ definePage({
|
||||
})
|
||||
|
||||
const appConfigStore = useAppConfigStore()
|
||||
const calcAuditModeEnabled = computed(() => !!appConfigStore.configs.auditConfig?.auditModeEnabled)
|
||||
const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
|
||||
|
||||
/** 依赖插件(plugin-search-widget) */
|
||||
const uniHaloPluginId = 'plugin-search-widget'
|
||||
@@ -206,7 +206,7 @@ onPullDownRefresh(() => {
|
||||
<view
|
||||
v-for="(item, index) in dataList"
|
||||
:key="index"
|
||||
class="article-card mx-6 mb-6 flex flex-col overflow-hidden rounded-xl bg-white p-6 shadow-sm fade-up"
|
||||
class="article-card fade-up mx-6 mb-6 flex flex-col overflow-hidden rounded-xl bg-white p-6 shadow-sm"
|
||||
:style="{ animationDelay: `${calcAniWait(index)}ms` }"
|
||||
@click="handleToDetail(item)"
|
||||
>
|
||||
|
||||
@@ -18,7 +18,7 @@ definePage({
|
||||
})
|
||||
|
||||
const appConfigStore = useAppConfigStore()
|
||||
const calcAuditModeEnabled = computed(() => !!appConfigStore.configs.auditConfig?.auditModeEnabled)
|
||||
const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
|
||||
|
||||
/** 依赖插件(plugin-vote) */
|
||||
const uniHaloPluginId = 'plugin-vote'
|
||||
|
||||
@@ -8,7 +8,6 @@ import { onLoad } from '@dcloudio/uni-app'
|
||||
import { getQRCodeInfo } from '@/api/uni-halo'
|
||||
import { useAppConfigStore } from '@/store/appConfig'
|
||||
import { usePluginAvailable } from '@/utils/plugin'
|
||||
import { checkJsonAndParse } from '@/utils/json'
|
||||
|
||||
definePage({
|
||||
// 使用 type: "home" 属性设置首页,其他页面不需要设置,默认为page
|
||||
@@ -28,7 +27,7 @@ const articleDetailPath = '/pages-blog/article-detail/article-detail'
|
||||
// 本地开发快速跳转页面,发布请置为 false
|
||||
const DEV_MODE = false
|
||||
const DEV_TO_TYPE = 'page' as 'page' | 'tabbar'
|
||||
const DEV_TO_PATH = articleDetailPath + '?name=01a057b2-3200-74af-8afe-28a054092e82'
|
||||
const DEV_TO_PATH = `${articleDetailPath}?name=01a057b2-3200-74af-8afe-28a054092e82`
|
||||
|
||||
/* ---------------- 状态 ---------------- */
|
||||
const appConfigStore = useAppConfigStore()
|
||||
@@ -56,23 +55,9 @@ async function getPostIdByQRCode(key: string): Promise<string | null> {
|
||||
return null
|
||||
}
|
||||
|
||||
/** 处理审计模式 mock 数据 */
|
||||
async function handleAuditMode(res: Record<string, unknown>) {
|
||||
const auditConfig = (res?.auditConfig ?? {}) as {
|
||||
auditModeEnabled?: boolean
|
||||
auditModeData?: { jsonUrl?: string, jsonData?: string }
|
||||
}
|
||||
if (!auditConfig.auditModeEnabled)
|
||||
return
|
||||
if (auditConfig.auditModeData?.jsonUrl) {
|
||||
await appConfigStore.fetchMockJson()
|
||||
}
|
||||
else {
|
||||
const mockJson = checkJsonAndParse(auditConfig.auditModeData?.jsonData || '')
|
||||
if (mockJson.ok) {
|
||||
appConfigStore.setMockJson(mockJson.jsonData as Record<string, unknown>)
|
||||
}
|
||||
}
|
||||
/** 获取审核模式数据(公开接口 /audit-data,enabled 联动设置页开关) */
|
||||
async function handleAuditMode() {
|
||||
await appConfigStore.fetchAuditData()
|
||||
}
|
||||
|
||||
/** 启动页/首页分流 */
|
||||
@@ -140,8 +125,8 @@ onLoad(async (options) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 审计模式 mock
|
||||
await handleAuditMode(res as Record<string, unknown>)
|
||||
// 审计模式数据(公开接口 /audit-data)
|
||||
await handleAuditMode()
|
||||
|
||||
// 启动页分流
|
||||
handleCheckShowStarted()
|
||||
|
||||
@@ -23,7 +23,7 @@ definePage({
|
||||
|
||||
const appConfigStore = useAppConfigStore()
|
||||
const haloConfigs = computed(() => appConfigStore.configs)
|
||||
const calcAuditModeEnabled = computed(() => !!haloConfigs.value.auditConfig?.auditModeEnabled)
|
||||
const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
|
||||
const calcVotePluginEnabled = computed(() => !!haloConfigs.value.pluginConfig?.votePlugin?.enabled)
|
||||
const calcLinksPluginEnabled = computed(() => !!haloConfigs.value.pluginConfig?.linksPlugin?.enabled)
|
||||
|
||||
|
||||
@@ -20,8 +20,7 @@ definePage({
|
||||
|
||||
const appConfigStore = useAppConfigStore()
|
||||
const haloConfigs = computed(() => appConfigStore.configs)
|
||||
const mockJson = computed(() => appConfigStore.mockJson)
|
||||
const calcAuditModeEnabled = computed(() => !!haloConfigs.value.auditConfig?.auditModeEnabled)
|
||||
const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
|
||||
|
||||
const categoryConfig = computed(() => haloConfigs.value.pageConfig?.categoryConfig)
|
||||
|
||||
@@ -74,22 +73,31 @@ function handleInitPage() {
|
||||
/* ---------------- 数据加载 ---------------- */
|
||||
async function handleGetData() {
|
||||
if (calcAuditModeEnabled.value) {
|
||||
// 审核模式:真实分类按 audit-data categories 过滤(数组顺序即展示顺序)
|
||||
currentCategoryConfig.value.type = 'list'
|
||||
const categoryMock = mockJson.value.category as { list?: { title?: string, cover?: string }[] } | undefined
|
||||
dataList.value = (categoryMock?.list || []).map(item => ({
|
||||
metadata: { name: String(Date.now() * Math.random()) },
|
||||
spec: {
|
||||
displayName: item.title || '',
|
||||
slug: '',
|
||||
priority: 0,
|
||||
cover: checkThumbnailUrl(item.cover, true),
|
||||
},
|
||||
postCount: 0,
|
||||
}))
|
||||
loading.value = 'success'
|
||||
loadMoreText.value = t('common.noMore')
|
||||
uni.hideLoading()
|
||||
uni.stopPullDownRefresh()
|
||||
const auditCategoryNames = appConfigStore.auditData.spec?.categories || []
|
||||
try {
|
||||
const res = await getCategoryList({ page: 1, size: 99999 })
|
||||
const filtered = res.data.items
|
||||
.filter(item => auditCategoryNames.includes(item.metadata.name))
|
||||
.map(item => ({
|
||||
...item,
|
||||
postCount: item.postCount ?? 0,
|
||||
spec: { ...item.spec, cover: checkThumbnailUrl(item.spec.cover, true) },
|
||||
}))
|
||||
const orderMap = new Map(auditCategoryNames.map((name, index) => [name, index]))
|
||||
filtered.sort((a, b) => (orderMap.get(a.metadata.name) ?? 999) - (orderMap.get(b.metadata.name) ?? 999))
|
||||
dataList.value = filtered
|
||||
loading.value = 'success'
|
||||
loadMoreText.value = t('common.noMore')
|
||||
uni.hideLoading()
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
loading.value = 'error'
|
||||
loadMoreText.value = t('common.loadFailed')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { useAppConfigStore } from '@/store/appConfig'
|
||||
import { checkImageUrl } from '@/utils/url'
|
||||
import { t } from '@/locale'
|
||||
import { usePluginAvailable } from '@/utils/plugin'
|
||||
import type { IPhoto } from '@/api/types/halo'
|
||||
import type { IPhoto, IPhotoGroup } from '@/api/types/halo'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
@@ -21,8 +21,7 @@ definePage({
|
||||
|
||||
const appConfigStore = useAppConfigStore()
|
||||
const haloConfigs = computed(() => appConfigStore.configs)
|
||||
const mockJson = computed(() => appConfigStore.mockJson)
|
||||
const calcAuditModeEnabled = computed(() => !!haloConfigs.value.auditConfig?.auditModeEnabled)
|
||||
const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
|
||||
|
||||
const galleryConfig = computed(() => haloConfigs.value.pageConfig?.galleryConfig)
|
||||
|
||||
@@ -46,13 +45,39 @@ const lock = ref(false)
|
||||
/* ---------------- 数据加载 ---------------- */
|
||||
async function handleGetCategory() {
|
||||
if (calcAuditModeEnabled.value) {
|
||||
handleGetData(true)
|
||||
// 审核模式:仅展示所选图库分组(galleryGroups)内的照片,未分组照片不展示
|
||||
const auditGroupNames = appConfigStore.auditData.spec?.galleryGroups || []
|
||||
try {
|
||||
const res = await getPhotoGroupList({ page: 1, size: 99999 })
|
||||
const filtered = ((res.data as unknown as IPhotoGroup[] | undefined) || [])
|
||||
.filter(item => auditGroupNames.includes(item.metadata.name))
|
||||
.map(item => ({
|
||||
name: item.metadata.name,
|
||||
displayName: item.spec.displayName,
|
||||
priority: item.spec.priority ?? 0,
|
||||
}))
|
||||
.sort((a, b) => a.priority - b.priority)
|
||||
category.value.list = filtered
|
||||
if (category.value.list.length !== 0) {
|
||||
queryParams.value.group = category.value.list[0].name || ''
|
||||
handleGetData(true)
|
||||
}
|
||||
else {
|
||||
loading.value = 'success'
|
||||
loadMoreText.value = t('common.noMore')
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
loading.value = 'error'
|
||||
category.value = { activeIndex: 0, list: [] }
|
||||
}
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await getPhotoGroupList({ page: 1, size: 0 })
|
||||
console.log('分类数据',res.data)
|
||||
category.value.list = (res.data || [])
|
||||
category.value.list = ((res.data as unknown as IPhotoGroup[] | undefined) || [])
|
||||
.map(item => ({
|
||||
name: item.metadata.name,
|
||||
displayName: item.spec.displayName,
|
||||
@@ -73,21 +98,9 @@ async function handleGetCategory() {
|
||||
}
|
||||
|
||||
async function handleGetData(isClearList = false) {
|
||||
if (calcAuditModeEnabled.value) {
|
||||
const galleryMock = mockJson.value.gallery as { list?: string[] } | undefined
|
||||
dataList.value = (galleryMock?.list || []).map(item => ({
|
||||
metadata: { name: String(Date.now() * Math.random()) },
|
||||
spec: {
|
||||
displayName: '',
|
||||
url: checkImageUrl(item),
|
||||
},
|
||||
}))
|
||||
loading.value = 'success'
|
||||
loadMoreText.value = t('common.noMore')
|
||||
uni.hideLoading()
|
||||
uni.stopPullDownRefresh()
|
||||
lock.value = false
|
||||
return
|
||||
if (isClearList) {
|
||||
dataList.value = []
|
||||
queryParams.value.page = 1
|
||||
}
|
||||
|
||||
if (!isLoadMore.value) {
|
||||
@@ -135,8 +148,8 @@ function handleGetDataByCategory(index: number) {
|
||||
handleGetData(true)
|
||||
}
|
||||
|
||||
function handleOnCategoryChange(e:{index:number,name:number}) {
|
||||
console.log('切换分类', e)
|
||||
function handleOnCategoryChange(e: { index: number, name: number }) {
|
||||
console.log('切换分类', e)
|
||||
if (lock.value)
|
||||
return
|
||||
handleGetDataByCategory(e.index)
|
||||
@@ -209,17 +222,17 @@ onReachBottom(() => {
|
||||
/>
|
||||
<template v-else>
|
||||
<!-- 顶部切换 -->
|
||||
<wd-tabs
|
||||
v-if="category.list.length > 0"
|
||||
v-model="category.activeIndex"
|
||||
align="left"
|
||||
sticky
|
||||
:offset-top="0"
|
||||
@change="handleOnCategoryChange"
|
||||
>
|
||||
<wd-tab v-for="cate in category.list" :key="cate.displayName" :title="cate.displayName"></wd-tab>
|
||||
</wd-tabs>
|
||||
|
||||
<wd-tabs
|
||||
v-if="category.list.length > 0"
|
||||
v-model="category.activeIndex"
|
||||
align="left"
|
||||
sticky
|
||||
:offset-top="0"
|
||||
@change="handleOnCategoryChange"
|
||||
>
|
||||
<wd-tab v-for="cate in category.list" :key="cate.displayName" :title="cate.displayName" />
|
||||
</wd-tabs>
|
||||
|
||||
<!-- 骨架屏 -->
|
||||
<view v-if="loading === 'loading'" class="loading-wrap box-border p-3">
|
||||
<wd-skeleton :row="4" :animated="true" />
|
||||
|
||||
@@ -18,7 +18,7 @@ definePage({
|
||||
navigationBarTitleText: '首页',
|
||||
enablePullDownRefresh: true,
|
||||
navigationStyle: 'custom',
|
||||
backgroundColor:'#F8F8F8'
|
||||
backgroundColor: '#F8F8F8',
|
||||
},
|
||||
})
|
||||
|
||||
@@ -26,7 +26,6 @@ const appConfigStore = useAppConfigStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const haloConfigs = computed(() => appConfigStore.configs)
|
||||
const mockJson = computed(() => appConfigStore.mockJson)
|
||||
|
||||
/* ---------------- 状态 ---------------- */
|
||||
const loading = ref<'loading' | 'success' | 'error'>('loading')
|
||||
@@ -65,7 +64,7 @@ const bloggerInfo = computed(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const calcAuditModeEnabled = computed(() => !!haloConfigs.value.auditConfig?.auditModeEnabled)
|
||||
const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
|
||||
|
||||
const calcIsShowQuickNavigationEnabled = computed(() => haloConfigs.value.pageConfig?.homeConfig?.useQuickNavigation)
|
||||
|
||||
@@ -139,14 +138,14 @@ async function handleQuery() {
|
||||
/** 轮播图 */
|
||||
function handleGetBanner() {
|
||||
if (calcAuditModeEnabled.value) {
|
||||
const homeMock = mockJson.value.home as { bannerList?: { title?: string, cover?: string, time?: string }[] } | undefined
|
||||
bannerList.value = (homeMock?.bannerList || []).map(item => ({
|
||||
id: Date.now() * Math.random(),
|
||||
title: item.title,
|
||||
image: checkThumbnailUrl(item.cover),
|
||||
src: checkThumbnailUrl(item.cover),
|
||||
type: 'custom',
|
||||
content: '',
|
||||
// 审核模式:轮播取选中文章前 5 条(articleList 已按 audit-data posts 过滤)
|
||||
bannerList.value = articleList.value.slice(0, 5).map(item => ({
|
||||
id: item.metadata.name,
|
||||
title: item.spec.title,
|
||||
image: checkThumbnailUrl(item.spec.cover),
|
||||
src: checkThumbnailUrl(item.spec.cover),
|
||||
type: 'post',
|
||||
content: item.status?.excerpt || '',
|
||||
url: '',
|
||||
}))
|
||||
return
|
||||
@@ -209,30 +208,30 @@ async function handleGetCategoryList() {
|
||||
/** 文章列表 */
|
||||
async function handleGetArticleList() {
|
||||
if (calcAuditModeEnabled.value) {
|
||||
const homeMock = mockJson.value.home as { postList?: { title?: string, cover?: string, time?: string, desc?: string }[] } | undefined
|
||||
articleList.value = (homeMock?.postList || []).map(item => ({
|
||||
metadata: { name: String(Date.now() * Math.random()) },
|
||||
spec: {
|
||||
title: item.title || '',
|
||||
slug: '',
|
||||
cover: item.cover,
|
||||
pinned: false,
|
||||
publishTime: item.time,
|
||||
deleted: false,
|
||||
publish: true,
|
||||
allowComment: true,
|
||||
visible: 'PUBLIC' as const,
|
||||
priority: 0,
|
||||
categories: [],
|
||||
tags: [],
|
||||
},
|
||||
status: { permalink: '', inProgress: false, excerpt: item.desc },
|
||||
stats: { visit: 0 },
|
||||
}))
|
||||
loading.value = 'success'
|
||||
loadMoreText.value = t('common.noMore')
|
||||
uni.hideLoading()
|
||||
uni.stopPullDownRefresh()
|
||||
// 审核模式:真实文章按 audit-data posts 过滤(数组顺序即展示顺序)
|
||||
const auditPostNames = appConfigStore.auditData.spec?.posts || []
|
||||
try {
|
||||
const res = await getPostList({ page: 1, size: 99999, sort: ['spec.publishTime,desc'] })
|
||||
const filtered = res.data.items.filter(item => auditPostNames.includes(item.metadata.name))
|
||||
const orderMap = new Map(auditPostNames.map((name, index) => [name, index]))
|
||||
filtered.sort((a, b) => (orderMap.get(a.metadata.name) ?? 999) - (orderMap.get(b.metadata.name) ?? 999))
|
||||
articleList.value = filtered
|
||||
loading.value = 'success'
|
||||
loadMoreText.value = t('common.noMore')
|
||||
// post 型轮播依赖文章列表,若启用则刷新
|
||||
if (bannerConfig.value?.enabled && bannerConfig.value.type !== 'custom') {
|
||||
handleGetBanner()
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error('获取审核文章失败', err)
|
||||
loading.value = 'error'
|
||||
loadMoreText.value = t('common.loadFailed')
|
||||
}
|
||||
finally {
|
||||
uni.hideLoading()
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -278,7 +277,7 @@ function handleToArticleDetail(article: IPost) {
|
||||
function handleToCategoryPage() {
|
||||
uni.switchTab({ url: '/pages/tabbar/category/category' })
|
||||
}
|
||||
|
||||
|
||||
function handleToCategoryBy(category: ICategory) {
|
||||
if (calcAuditModeEnabled.value)
|
||||
return
|
||||
@@ -395,7 +394,7 @@ handleQuery()
|
||||
|
||||
<block v-else>
|
||||
<!-- 轮播 Banner -->
|
||||
<view v-if="bannerConfig?.enabled" class="bg-white mb-4">
|
||||
<view v-if="bannerConfig?.enabled" class="mb-4 bg-white">
|
||||
<view v-if="bannerList.length !== 0" class="banner mx-3 mt-3 overflow-hidden rounded-xl">
|
||||
<uh-swiper
|
||||
:height="bannerConfig.height"
|
||||
@@ -411,7 +410,7 @@ handleQuery()
|
||||
</view>
|
||||
|
||||
<!-- 快捷导航 -->
|
||||
<view v-if="navList.filter(x => x.show).length" class="nav-box px-4 overflow-hidden rounded-xl bg-white p-3">
|
||||
<view v-if="navList.filter(x => x.show).length" class="nav-box overflow-hidden rounded-xl bg-white p-3 px-4">
|
||||
<view class="page-item-title font-bold">
|
||||
快捷导航
|
||||
</view>
|
||||
@@ -497,4 +496,3 @@ handleQuery()
|
||||
/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
@@ -25,8 +25,7 @@ definePage({
|
||||
|
||||
const appConfigStore = useAppConfigStore()
|
||||
const haloConfigs = computed(() => appConfigStore.configs)
|
||||
const mockJson = computed(() => appConfigStore.mockJson)
|
||||
const calcAuditModeEnabled = computed(() => !!haloConfigs.value.auditConfig?.auditModeEnabled)
|
||||
const calcAuditModeEnabled = computed(() => appConfigStore.auditModeEnabled)
|
||||
const calcUseTagRandomColor = computed(() => !!haloConfigs.value.pageConfig?.momentConfig?.useTagRandomColor)
|
||||
|
||||
const bloggerInfo = computed(() => {
|
||||
@@ -59,30 +58,51 @@ function removeTagLinksCompletely(htmlString: string): string {
|
||||
return htmlString.replace(regex, '')
|
||||
}
|
||||
|
||||
/** 瞬间项映射(medium 拆分为 images/videos/audios + 内容 tag 清理) */
|
||||
function mapMomentItem(item: IMoment) {
|
||||
const medium = (item.spec as unknown as { medium?: { type?: string, url: string }[] }).medium || []
|
||||
return {
|
||||
...item,
|
||||
spec: {
|
||||
...item.spec,
|
||||
owner: {
|
||||
displayName: bloggerInfo.value.nickname,
|
||||
avatar: bloggerInfo.value.avatar,
|
||||
},
|
||||
newHtml: removeTagLinksCompletely((item.spec as unknown as { content?: { html?: string } }).content?.html || ''),
|
||||
},
|
||||
images: medium.filter(x => x.type === 'PHOTO').map(x => ({ ...x, url: checkThumbnailUrl(x.url, true) })),
|
||||
videos: medium.filter(x => x.type === 'VIDEO').map(x => ({ ...x, id: generateUUID() })),
|
||||
audios: medium.filter(x => x.type === 'AUDIO'),
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- 数据加载 ---------------- */
|
||||
async function handleGetData() {
|
||||
if (calcAuditModeEnabled.value) {
|
||||
const momentsMock = mockJson.value.moments as { list?: { content?: string, time?: string, images?: string[] }[] } | undefined
|
||||
dataList.value = (momentsMock?.list || []).map(item => ({
|
||||
metadata: { name: String(Date.now() * Math.random()) },
|
||||
spec: {
|
||||
content: item.content || '',
|
||||
owner: {
|
||||
displayName: bloggerInfo.value.nickname,
|
||||
avatar: bloggerInfo.value.avatar,
|
||||
},
|
||||
visible: 'PUBLIC',
|
||||
allowComment: true,
|
||||
approved: true,
|
||||
releaseTime: item.time,
|
||||
},
|
||||
images: (item.images || []).map(img => ({ type: 'PHOTO', url: checkThumbnailUrl(img) })),
|
||||
videos: [],
|
||||
}))
|
||||
loading.value = 'success'
|
||||
loadMoreText.value = t('common.noMore')
|
||||
uni.hideLoading()
|
||||
uni.stopPullDownRefresh()
|
||||
// 审核模式:真实瞬间按 audit-data moments 过滤(数组顺序即展示顺序)
|
||||
const auditMomentNames = appConfigStore.auditData.spec?.moments || []
|
||||
try {
|
||||
const res = await getMomentList({ page: 1, size: 99999 })
|
||||
const filtered = res.data.items
|
||||
.filter(x => x.spec.visible === 'PUBLIC' && auditMomentNames.includes(x.metadata.name))
|
||||
const orderMap = new Map(auditMomentNames.map((name, index) => [name, index]))
|
||||
filtered.sort((a, b) => (orderMap.get(a.metadata.name) ?? 999) - (orderMap.get(b.metadata.name) ?? 999))
|
||||
const tempItems = filtered.map(mapMomentItem)
|
||||
dataList.value = tempItems
|
||||
nextTick(() => {
|
||||
createVideoContexts(tempItems)
|
||||
})
|
||||
loading.value = 'success'
|
||||
loadMoreText.value = t('common.noMore')
|
||||
uni.hideLoading()
|
||||
uni.stopPullDownRefresh()
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err)
|
||||
loading.value = 'error'
|
||||
loadMoreText.value = t('common.loadFailed')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -94,30 +114,13 @@ async function handleGetData() {
|
||||
|
||||
try {
|
||||
const res = await getMomentList({ ...queryParams.value })
|
||||
loading.value = 'success'
|
||||
loading.value = 'success'
|
||||
loadMoreText.value = res.data.hasNext ? t('common.loadMore') : t('common.noMore')
|
||||
hasNext.value = res.data.hasNext
|
||||
|
||||
const tempItems = res.data.items
|
||||
.filter(x => x.spec.visible === 'PUBLIC')
|
||||
.map((item) => {
|
||||
const medium = (item.spec as unknown as { medium?: { type?: string, url: string }[] }).medium || []
|
||||
const newItem = {
|
||||
...item,
|
||||
spec: {
|
||||
...item.spec,
|
||||
owner: {
|
||||
displayName: bloggerInfo.value.nickname,
|
||||
avatar: bloggerInfo.value.avatar,
|
||||
},
|
||||
newHtml: removeTagLinksCompletely((item.spec as unknown as { content?: { html?: string } }).content?.html || ''),
|
||||
},
|
||||
images: medium.filter(x => x.type === 'PHOTO').map(x => ({ ...x, url: checkThumbnailUrl(x.url, true) })),
|
||||
videos: medium.filter(x => x.type === 'VIDEO').map(x => ({ ...x, id: generateUUID() })),
|
||||
audios: medium.filter(x => x.type === 'AUDIO'),
|
||||
}
|
||||
return newItem
|
||||
})
|
||||
.map(mapMomentItem)
|
||||
|
||||
dataList.value = isLoadMore.value
|
||||
? dataList.value.concat(tempItems)
|
||||
@@ -248,7 +251,7 @@ onReachBottom(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class=" box-border min-h-screen w-screen flex flex-col py-6">
|
||||
<view class="box-border min-h-screen w-screen flex flex-col py-6">
|
||||
<uh-plugin-unavailable
|
||||
v-if="!uniHaloPluginAvailable"
|
||||
:plugin-id="uniHaloPluginId"
|
||||
|
||||
+24
-30
@@ -1,15 +1,14 @@
|
||||
/**
|
||||
* 应用配置 store(源自旧项目 store/config.js 的 configs/mockJson 部分)
|
||||
* 应用配置 store(源自旧项目 store/config.js 的 configs/auditData 部分)
|
||||
* 注意:旧 fetchConfigs 中会把 basicConfig.tokenConfig 写入缓存,供 getPersonalToken 使用,此处保留
|
||||
*/
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
import { getAppConfigs } from '@/api/uni-halo'
|
||||
import { computed, ref } from 'vue'
|
||||
import { getAppConfigs, getAuditData } from '@/api/uni-halo'
|
||||
import { DefaultAppConfigs } from '@/config/appConfig'
|
||||
import { deepMerge } from '@/utils/merge'
|
||||
import { setCache } from '@/utils/storage'
|
||||
import { checkUrl } from '@/utils/url'
|
||||
import type { IAppConfig, IMockJson } from '@/api/types/uni-halo'
|
||||
import type { IAppConfig, IAuditDataResult } from '@/api/types/uni-halo'
|
||||
|
||||
/** 个人令牌存储 key(与 src/store/token.ts 的 getPersonalToken 保持一致) */
|
||||
const APP_TOKENS_KEY = 'APP_TOKENS'
|
||||
@@ -18,11 +17,15 @@ export const useAppConfigStore = defineStore(
|
||||
'appConfig',
|
||||
() => {
|
||||
const configs = ref<IAppConfig>(JSON.parse(JSON.stringify(DefaultAppConfigs)))
|
||||
const mockJson = ref<IMockJson>({})
|
||||
/** 审核模式数据(公开接口 /audit-data;enabled 联动设置页开关) */
|
||||
const auditData = ref<IAuditDataResult>({ enabled: false })
|
||||
/** 审核模式开关(单一数据源:公开接口返回的 enabled) */
|
||||
const auditModeEnabled = computed(() => !!auditData.value.enabled)
|
||||
|
||||
/** 重置为默认配置 */
|
||||
const setDefaultAppSettings = () => {
|
||||
configs.value = JSON.parse(JSON.stringify(DefaultAppConfigs))
|
||||
auditData.value = { enabled: false }
|
||||
}
|
||||
|
||||
/** 获取应用配置(与默认值深合并,并存储 token) */
|
||||
@@ -48,36 +51,27 @@ export const useAppConfigStore = defineStore(
|
||||
}
|
||||
}
|
||||
|
||||
/** 请求模拟数据(审计模式) */
|
||||
const fetchMockJson = async () => {
|
||||
const mockJsonUrl = checkUrl(configs.value.auditConfig?.auditModeData?.jsonUrl)
|
||||
return new Promise<{ ok: boolean, data: unknown }>((resolve) => {
|
||||
uni.request({
|
||||
url: mockJsonUrl,
|
||||
method: 'GET',
|
||||
success: (res) => {
|
||||
mockJson.value = res.data as IMockJson
|
||||
resolve({ ok: true, data: res.data })
|
||||
},
|
||||
fail: (err) => {
|
||||
resolve({ ok: false, data: err })
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/** 设置模拟数据(jsonData 本地解析场景) */
|
||||
const setMockJson = (data: IMockJson) => {
|
||||
mockJson.value = data
|
||||
/** 获取审核模式数据(公开接口;enabled=false 时 spec 为空) */
|
||||
const fetchAuditData = async () => {
|
||||
try {
|
||||
const res = await getAuditData()
|
||||
auditData.value = res.data || { enabled: false }
|
||||
return auditData.value
|
||||
}
|
||||
catch (err) {
|
||||
console.error('获取审核模式数据失败', err)
|
||||
auditData.value = { enabled: false }
|
||||
return auditData.value
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
configs,
|
||||
mockJson,
|
||||
auditData,
|
||||
auditModeEnabled,
|
||||
fetchConfigs,
|
||||
setDefaultAppSettings,
|
||||
fetchMockJson,
|
||||
setMockJson,
|
||||
fetchAuditData,
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
+4
-4
@@ -7,10 +7,10 @@ import { isNativeTabbar, tabbarList } from './config'
|
||||
export function getI18nText(key: string) {
|
||||
// 获取 %xxx% 中的 xxx
|
||||
const match = key.match(/%(.+?)%/)
|
||||
if (match) {
|
||||
key = match[1]
|
||||
}
|
||||
console.log('设置多语言:', key)
|
||||
// 无 %占位符% 的文本(如直接配置中文)原样返回,不进入翻译
|
||||
if (!match)
|
||||
return key
|
||||
key = match[1]
|
||||
return t(key)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"pages": [
|
||||
{
|
||||
"path": "pages/index/index",
|
||||
"type": "home",
|
||||
"style": {}
|
||||
}
|
||||
],
|
||||
"subPackages": []
|
||||
}
|
||||
@@ -33,6 +33,7 @@ const uniMock = {
|
||||
chooseImage: vi.fn(),
|
||||
getSystemInfoSync: vi.fn().mockReturnValue({ platform: 'devtools' }),
|
||||
getSystemInfo: vi.fn(),
|
||||
getLocale: vi.fn().mockReturnValue('zh-Hans'),
|
||||
onNetworkStatusChange: vi.fn(),
|
||||
getNetworkType: vi.fn(),
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ export default defineConfig({
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
// 生成物 src/pages.json 为 JSONC(带注释),vite:json 无法解析,测试环境映射到合法 mock
|
||||
'@/pages.json': path.resolve(process.cwd(), 'src/test-mocks/pages.json'),
|
||||
'@': path.resolve(process.cwd(), 'src'),
|
||||
'@img': path.resolve(process.cwd(), 'src/static/images'),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user