mirror of
https://github.com/ialley-workshop-open/uni-halo.git
synced 2026-09-11 16:00:44 +08:00
feat: 联系博主页对接博主介绍/社交动态列表,下线友链提交页
- contact.vue:博主介绍(intro 富文本)用 mp-html 完整配置渲染(domain/loading-img/ tag-style/container-style/markdown,对齐 moments/文章详情);社交信息改读 authorConfig.social.items 动态列表(priority 排序、visible 过滤、未配置空态、 不回退默认值),去 enabled 开关;收信日期改 formatTime 实时显示 - about.vue:社交入口判定改按社交项列表(存在可见且有内容即展示) - 友链提交功能调整:submit-link 页面下线,mini-info/site-info 与提交开关 (submissionEnabled)随插件端新结构对接 Co-Authored-By: AtomCode (deepseek-v4-flash) <noreply@atomgit.com>
This commit is contained in:
+32
-10
@@ -18,10 +18,14 @@ export interface IImagesConfig {
|
||||
export interface IPluginConfig {
|
||||
toolsPlugin?: { Authorization?: string } & Record<string, unknown>
|
||||
/**
|
||||
* 链接配置(插件端 spec.linkInfo 直接下发到本键,结构 = {miniInfo, siteInfo, authorInfo},字段名无映射;
|
||||
* linksSubmitPlugin 旧键已弃用不再下发;votePlugin/linksPlugin 已下线(2026-09-10,app 端改插件启用检测判定))
|
||||
* 友链信息(插件端 spec.linkInfo 直接下发到本键,结构 = {submissionEnabled, miniInfo, siteInfo},字段名无映射;
|
||||
* linksSubmitPlugin 旧键已弃用不再下发;votePlugin/linksPlugin 已下线(2026-09-10,app 端改插件启用检测判定);
|
||||
* 2026-09-11 起新增基本配置 submissionEnabled(公开提交申请开关,原 setting linkConfig);authorInfo 已下线,
|
||||
* 作者信息改读 authorConfig.blogger)
|
||||
*/
|
||||
linkInfo?: {
|
||||
/** 基本配置:是否开放公开提交申请(默认 true;false 时隐藏「提交申请」入口) */
|
||||
submissionEnabled?: boolean
|
||||
/** 小程序信息(「申请信息」弹窗展示) */
|
||||
miniInfo?: {
|
||||
displayName?: string
|
||||
@@ -30,22 +34,15 @@ export interface IPluginConfig {
|
||||
description?: string
|
||||
applyRemark?: string
|
||||
}
|
||||
/** 站点信息(对齐 Halo 官方友链提交 API:displayName/url/logo/description/email/backlink/feedUrls) */
|
||||
/** 站点信息(对齐 Halo 官方友链提交 API:displayName/url/logo/description/backlink/feedUrls,2026-09-11 起不再含 email) */
|
||||
siteInfo?: {
|
||||
displayName?: string
|
||||
url?: string
|
||||
logo?: string
|
||||
description?: string
|
||||
email?: string
|
||||
backlink?: string
|
||||
feedUrls?: string[]
|
||||
}
|
||||
/** 作者信息(小程序端作者区) */
|
||||
authorInfo?: {
|
||||
authorName?: string
|
||||
avatar?: string
|
||||
website?: string
|
||||
}
|
||||
}
|
||||
doubanPlugin?: { position?: string } & Record<string, unknown>
|
||||
[key: string]: unknown
|
||||
@@ -121,6 +118,23 @@ export interface IPageConfig {
|
||||
aboutConfig?: {
|
||||
bgImageUrl?: string
|
||||
waveImageUrl?: string
|
||||
/** 页脚版权(2026-09-10 起插件端从应用设置迁入,显示于关于页页脚) */
|
||||
copyrightConfig?: {
|
||||
enabled?: boolean
|
||||
content?: string
|
||||
}
|
||||
}
|
||||
/** 免责声明页(2026-09-10 起插件端从应用设置迁入:不再有 enabled 开关,按内容非空展示) */
|
||||
disclaimers?: {
|
||||
content?: string
|
||||
}
|
||||
/** 文章详情页配置(2026-09-10 起插件端从应用设置迁入:评论开关与版权文案) */
|
||||
postDetailConfig?: {
|
||||
showComment?: boolean
|
||||
copyrightEnabled?: boolean
|
||||
copyrightAuthor?: string
|
||||
copyrightDesc?: string
|
||||
copyrightViolation?: string
|
||||
}
|
||||
[key: string]: unknown
|
||||
}
|
||||
@@ -169,8 +183,16 @@ export interface IAppConfig {
|
||||
personalToken?: string
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 恋爱配置(2026-09-10 起插件端 loveEnabled 总开关下线,入口显隐由模块入口开关
|
||||
* (ourStory/lovePhoto/loveDaily.enabled)与 navList 统一管理,客户端按模块开关判定)
|
||||
*/
|
||||
loveConfig?: Record<string, unknown>
|
||||
imagesConfig?: IImagesConfig
|
||||
/**
|
||||
* 博主与社交(插件端 GeneralConfig.profile.blogger/social 重建;blogger 含
|
||||
* nickname/avatar/email/description/website(官网地址 2026-09-10 新增))
|
||||
*/
|
||||
authorConfig?: Record<string, unknown>
|
||||
appConfig?: Record<string, unknown>
|
||||
pluginConfig?: IPluginConfig
|
||||
|
||||
@@ -24,18 +24,20 @@ const emit = defineEmits<{
|
||||
const isShow = ref(false)
|
||||
const appConfigStore = useAppConfigStore()
|
||||
|
||||
/** 小程序申请信息(字段与 uh-links-mini-apply 表单一致;小程序信息读 linkInfo.miniInfo、作者信息读 linkInfo.authorInfo,字段名无映射) */
|
||||
/** 小程序申请信息(字段与 uh-links-mini-apply 表单一致;小程序信息读 linkInfo.miniInfo;
|
||||
* 作者信息 2026-09-10 起改读应用设置-博主资料 authorConfig.blogger(linkInfo.authorInfo 已下线),字段名无映射) */
|
||||
const miniInfo = computed(() => {
|
||||
const miniCfg = (appConfigStore.configs.pluginConfig?.linkInfo?.miniInfo || {}) as Record<string, unknown>
|
||||
const authorCfg = (appConfigStore.configs.pluginConfig?.linkInfo?.authorInfo || {}) as Record<string, unknown>
|
||||
const blogger = (appConfigStore.configs.authorConfig?.blogger || {}) as Record<string, unknown>
|
||||
const str = (cfg: Record<string, unknown>, key: string, fallback = '') => String(cfg[key] || fallback || '')
|
||||
return {
|
||||
displayName: str(miniCfg, 'displayName'),
|
||||
miniProgramCode: str(miniCfg, 'miniProgramCode'),
|
||||
link: str(miniCfg, 'link'),
|
||||
authorName: str(authorCfg, 'authorName'),
|
||||
avatar: str(authorCfg, 'avatar'),
|
||||
website: str(authorCfg, 'website'),
|
||||
// 作者信息:博主资料 nickname/avatar/website(官网地址 2026-09-10 新增)
|
||||
authorName: str(blogger, 'nickname'),
|
||||
avatar: str(blogger, 'avatar'),
|
||||
website: str(blogger, 'website'),
|
||||
description: str(miniCfg, 'description'),
|
||||
applyRemark: str(miniCfg, 'applyRemark'),
|
||||
email: str(miniCfg, 'email'),
|
||||
|
||||
@@ -26,7 +26,6 @@ const blogDetail = computed(() => (appConfigStore.configs.pluginConfig?.linkInfo
|
||||
url?: string
|
||||
logo?: string
|
||||
description?: string
|
||||
email?: string
|
||||
backlink?: string
|
||||
feedUrls?: string[]
|
||||
} | undefined) || {})
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
})
|
||||
|
||||
/* ---------------- 计算属性 ---------------- */
|
||||
const postDetailConfig = computed(() => (haloConfigs.value.basicConfig as { postDetailConfig ?: Record<string, unknown> } | undefined)?.postDetailConfig)
|
||||
const postDetailConfig = computed(() => (haloConfigs.value.pageConfig as { postDetailConfig ?: Record<string, unknown> } | undefined)?.postDetailConfig)
|
||||
|
||||
const bloggerInfo = computed(() => {
|
||||
const blogger = haloConfigs.value.authorConfig?.blogger as { nickname ?: string, avatar ?: string } | undefined
|
||||
|
||||
+139
-132
@@ -1,148 +1,155 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch, watchEffect } from 'vue'
|
||||
import { useAppConfigStore } from '@/store/appConfig'
|
||||
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
|
||||
import { checkAvatarUrl } from '@/utils/url'
|
||||
import { computed, onUnmounted, ref, watch, watchEffect } from 'vue'
|
||||
import { useAppConfigStore } from '@/store/appConfig'
|
||||
import { DataLoadingStatusEnum, useDataLoadingStatus } from '@/hooks/useDataLoadingStatus'
|
||||
import { checkAvatarUrl } from '@/utils/url'
|
||||
import { formatTime } from '@/utils/formatTime'
|
||||
import { markdownConfig } from '@/config/markdown'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '联系博主',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '联系博主',
|
||||
navigationStyle: 'custom',
|
||||
},
|
||||
})
|
||||
|
||||
const appConfigStore = useAppConfigStore()
|
||||
const authorConfig = computed(() => appConfigStore.configs.authorConfig)
|
||||
interface ISoical {
|
||||
key ?: string
|
||||
name : string
|
||||
content : string
|
||||
icon ?: string,
|
||||
color : string,
|
||||
bgColor : string,
|
||||
priority ?: number,
|
||||
visible ?: boolean,
|
||||
}
|
||||
|
||||
const bloggerInfo = computed(() => {
|
||||
const blogger = authorConfig.value?.blogger as { nickname?: string, avatar?: string, description?: string } | undefined
|
||||
return {
|
||||
nickname: blogger?.nickname || '',
|
||||
avatar: checkAvatarUrl(blogger?.avatar),
|
||||
description: blogger?.description || '',
|
||||
}
|
||||
})
|
||||
const appConfigStore = useAppConfigStore()
|
||||
const authorConfig = computed(() => appConfigStore.configs.authorConfig)
|
||||
|
||||
const socialConfig = computed(() => (authorConfig.value?.social as Record<string, unknown> | undefined) || {})
|
||||
const bloggerInfo = computed(() => {
|
||||
const blogger = authorConfig.value?.blogger as { nickname ?: string, avatar ?: string, description ?: string, intro ?: string } | undefined
|
||||
return {
|
||||
nickname: blogger?.nickname || '',
|
||||
avatar: checkAvatarUrl(blogger?.avatar),
|
||||
description: blogger?.description || '',
|
||||
intro: blogger?.intro || '',
|
||||
}
|
||||
})
|
||||
|
||||
/** 联系方式列表(从配置填充) */
|
||||
const result = ref<{ key: string, name: string, value: string }[]>([
|
||||
{ key: 'qq', name: '企鹅号', value: '' },
|
||||
{ key: 'wechat', name: '微信号', value: '' },
|
||||
{ key: 'github', name: 'Github', value: '' },
|
||||
{ key: 'gitee', name: 'Gitee', value: '' },
|
||||
{ key: 'bilibili', name: 'Bilibili', value: '' },
|
||||
{ key: 'csdn', name: 'CSDN', value: '' },
|
||||
{ key: 'blog', name: '博客地址', value: '' },
|
||||
{ key: 'juejin', name: '掘金地址', value: '' },
|
||||
{ key: 'weibo', name: '微博地址', value: '' },
|
||||
{ key: 'email', name: '邮箱地址', value: '' },
|
||||
])
|
||||
const socialConfig = computed(() => (authorConfig.value?.social as { items ?: ISoical[] } | undefined) || {})
|
||||
|
||||
/** 平台瓦片(品牌色 + 字符);未覆盖的平台回退中性色与名称首字 */
|
||||
const platformMeta: Record<string, { color: string, letter: string }> = {
|
||||
qq: { color: '#12b7f5', letter: 'Q' },
|
||||
wechat: { color: '#07c160', letter: '微' },
|
||||
github: { color: '#24292f', letter: 'G' },
|
||||
gitee: { color: '#c71d23', letter: 'G' },
|
||||
bilibili: { color: '#fb7299', letter: 'B' },
|
||||
csdn: { color: '#fc5531', letter: 'C' },
|
||||
blog: { color: '#3f51b5', letter: '博' },
|
||||
juejin: { color: '#1e80ff', letter: '掘' },
|
||||
weibo: { color: '#e6162d', letter: 'W' },
|
||||
email: { color: '#f57c00', letter: '@' },
|
||||
}
|
||||
const socialList = computed<Array<ISoical>>(() => {
|
||||
const configured = socialConfig.value.items
|
||||
if (!configured || !configured.length) {
|
||||
return []
|
||||
}
|
||||
return [...configured]
|
||||
.filter(item => item.visible !== false)
|
||||
.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0))
|
||||
})
|
||||
|
||||
const calcIsNotEmpty = computed(() => result.value.some(item => item.value !== ''))
|
||||
const isNotEmpty = computed(() => socialList.value.some(item => item.visible && !!item.content))
|
||||
|
||||
/* ---------------- 加载状态机(本地配置,状态直接推导) ---------------- */
|
||||
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
|
||||
watchEffect(() => {
|
||||
updateLoadingStatus(calcIsNotEmpty.value ? DataLoadingStatusEnum.Success : DataLoadingStatusEnum.Empty)
|
||||
})
|
||||
const { loadingStatus, updateLoadingStatus } = useDataLoadingStatus()
|
||||
watchEffect(() => {
|
||||
updateLoadingStatus(isNotEmpty.value ? DataLoadingStatusEnum.Success : DataLoadingStatusEnum.Empty)
|
||||
})
|
||||
|
||||
function handleGetData() {
|
||||
for (const key in socialConfig.value) {
|
||||
if (key === 'enabled')
|
||||
continue
|
||||
const item = result.value.find(x => x.key === key)
|
||||
if (item) {
|
||||
item.value = String(socialConfig.value[key] || '')
|
||||
}
|
||||
}
|
||||
}
|
||||
function handleOnClick(item : ISoical) {
|
||||
if (!item.content) { return }
|
||||
uni.setClipboardData({
|
||||
data: item.content,
|
||||
showToast: false,
|
||||
success: () => {
|
||||
uni.showToast({ icon: 'none', title: `${item.name} 已复制!` })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function handleOnClick(item: { value: string, name: string }) {
|
||||
if (!item.value)
|
||||
return
|
||||
uni.setClipboardData({
|
||||
data: item.value,
|
||||
showToast: false,
|
||||
success: () => {
|
||||
uni.showToast({ icon: 'none', title: `${item.name} 已复制!` })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
watch(socialConfig, () => {
|
||||
handleGetData()
|
||||
}, { deep: true, immediate: true })
|
||||
/* ---------------- 实时时间(收信日期,formatTime 工具每秒刷新) ---------------- */
|
||||
const nowText = ref('')
|
||||
let clock : ReturnType<typeof setInterval> | null = null
|
||||
function refreshNowText() {
|
||||
nowText.value = formatTime({ d: Date.now(), f: 'yyyy年MM月dd日' })
|
||||
}
|
||||
refreshNowText()
|
||||
clock = setInterval(refreshNowText, 1000)
|
||||
onUnmounted(() => {
|
||||
if (clock) {
|
||||
clearInterval(clock)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="box-border min-h-screen w-screen overflow-hidden bg-page px-4 pb-10 pt-2">
|
||||
<!-- 自定义导航 -->
|
||||
<uh-navbar default-title="联系博主" title-color="text-gray-900" />
|
||||
<view class="box-border min-h-screen w-screen overflow-hidden bg-page px-4 pb-10 pt-2">
|
||||
<!-- 自定义导航 -->
|
||||
<uh-navbar default-title="联系博主" title-color="text-gray-900" />
|
||||
|
||||
<!-- Hero 名片卡(主题色光斑透过毛玻璃形成柔和渐变) -->
|
||||
<view class="hero-wrap relative">
|
||||
<view class="absolute h-[220rpx] w-[220rpx] rounded-full bg-[rgba(185,228,36,0.32)] -right-8 -top-8" />
|
||||
<view class="absolute top-[150rpx] h-[180rpx] w-[180rpx] rounded-full bg-[rgba(215,249,76,0.45)] -left-10" />
|
||||
<view class="hero-card uh-global-card-glass uh-shadow-xs relative flex flex-col items-center rounded-2xl px-6 pb-6 pt-10">
|
||||
<view class="avatar relative box-border h-[150rpx] w-[150rpx] overflow-hidden border-5 border-white/90 rounded-full shadow-lg">
|
||||
<image class="avatar-img h-full w-full" :src="bloggerInfo.avatar" mode="aspectFill" />
|
||||
</view>
|
||||
<view class="nickname mt-4 text-[38rpx] text-gray-900 font-bold">
|
||||
{{ bloggerInfo.nickname }}
|
||||
</view>
|
||||
<view class="desc mt-2 px-4 text-center text-[26rpx] text-gray-500 leading-relaxed">
|
||||
{{ bloggerInfo.description || '这个博主很懒,竟然没写介绍~' }}
|
||||
</view>
|
||||
<view class="mt-5 w-full flex items-center justify-center gap-2 border-t border-black/5 pt-4">
|
||||
<wd-icon name="copy" size="26rpx" color="#a8a294" />
|
||||
<text class="text-2xs text-gray-400">点击卡片即可复制对应内容</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="fixed -right-8 top-8 h-28 w-28 rounded-full bg-[rgba(185,228,36,0.32)] blur-xl" />
|
||||
<view class="fixed -left-10 top-36 h-24 w-24 rounded-full bg-[rgba(215,249,76,0.45)] blur-xl" />
|
||||
|
||||
<!-- 联系方式(状态机:无联系方式 → empty 态) -->
|
||||
<uh-data-loading
|
||||
v-if="loadingStatus !== 'success'" :loading-status="loadingStatus" min-height="30vh"
|
||||
empty-text="暂无联系方式" empty-sub-text="" @refresh="handleGetData"
|
||||
/>
|
||||
<template v-else>
|
||||
<uh-section-title class="mb-3 mt-6">
|
||||
联系方式
|
||||
</uh-section-title>
|
||||
<view class="flex flex-col gap-3">
|
||||
<view
|
||||
v-for="item in result.filter(i => i.value)"
|
||||
:key="item.key"
|
||||
class="box-border uh-global-card-glass uh-shadow-xs flex items-center gap-3 rounded-2xl px-4 py-3"
|
||||
@click="handleOnClick(item)"
|
||||
>
|
||||
<view class="tile h-[76rpx] w-[76rpx] flex shrink-0 items-center justify-center rounded-xl" :style="{ backgroundColor: platformMeta[item.key]?.color || '#8a8a7a' }">
|
||||
<text class="text-[30rpx] text-white font-bold">{{ platformMeta[item.key]?.letter || item.name.slice(0, 1) }}</text>
|
||||
</view>
|
||||
<view class="min-w-0 flex flex-1 flex-col">
|
||||
<text class="text-xs text-gray-500">{{ item.name }}</text>
|
||||
<view class="mt-1 break-all text-[26rpx] text-gray-900 leading-snug">
|
||||
{{ item.value }}
|
||||
</view>
|
||||
</view>
|
||||
<wd-icon name="copy" size="28rpx" class="shrink-0 text-gray-400" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</template>
|
||||
<view
|
||||
class="box-border relative flex items-center justify-between uh-global-card-glass uh-shadow-xs rounded-xl p-4">
|
||||
<view class="flex-1 relative flex flex-col rounded-2xl">
|
||||
<view class="flex items-center gap-x-2 text-xs text-primary">
|
||||
TO <text class="text-sm">·</text> 收
|
||||
</view>
|
||||
<view class="mt-2 text-md text-gray-900 tracking-wider">
|
||||
素未谋面的朋友
|
||||
</view>
|
||||
<view class="mt-2 text-xs text-black/60">
|
||||
展信安,见字如面。
|
||||
</view>
|
||||
<view class="mt-3 text-xs text-black/60">
|
||||
自 · {{ bloggerInfo.nickname }}
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="shrink-0 uh-global-card-glass uh-shadow-xs border relative rotate-3 box-border w-22 overflow-hidden flex flex-col gap-y-1 items-center justify-center p-2">
|
||||
<image class="h-18 w-full" :src="bloggerInfo.avatar" mode="aspectFill" />
|
||||
<text class="text-xs font-normal truncate">{{ bloggerInfo.nickname }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mt-4 flex flex-col gap-y-6 box-border uh-global-card-glass uh-shadow-xs rounded-xl px-4 py-3">
|
||||
<uh-data-loading v-if="loadingStatus !== 'success'" :loading-status="loadingStatus" min-height="65vh"
|
||||
empty-text="暂无联系方式" empty-sub-text="" />
|
||||
<template v-else>
|
||||
<view class="w-full flex flex-col gap-y-3">
|
||||
<view class="text-md text-gray-900">
|
||||
展信安:
|
||||
</view>
|
||||
<mp-html v-if="bloggerInfo.intro" lazy-load :domain="markdownConfig.domain ?? ''"
|
||||
:loading-img="markdownConfig.loadingGif" scroll-table selectable
|
||||
:tag-style="markdownConfig.tagStyle" :container-style="markdownConfig.containStyle"
|
||||
:content="bloggerInfo.intro" :markdown="true" :show-line-number="true"
|
||||
:show-language-name="true" copy-by-long-press />
|
||||
<view class="mt-2 w-full flex items-end flex-col gap-y-3">
|
||||
<text class="text-md font-semibold text-gray-900">{{ bloggerInfo.nickname }}</text>
|
||||
<text class="text-xs text-black/60">{{ nowText }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="box-border pt-4 w-full flex flex-col gap-y-2 border-t border-t-dashed border-gray-200">
|
||||
<view v-for="item in socialList" :key="item.name" class="flex items-center gap-3"
|
||||
@click="handleOnClick(item)">
|
||||
<view
|
||||
class="uh-global-card-glass uh-shadow-xs border h-10 w-10 flex shrink-0 items-center justify-center rounded-xl"
|
||||
:style="{ backgroundColor: item.bgColor ,color:item.color }">
|
||||
<text class="text-sm text-gray-500 font-bold"> {{ item.icon }} </text>
|
||||
</view>
|
||||
<view class="min-w-0 flex flex-1 flex-col">
|
||||
<text class="text-xs text-gray-500">{{ item.name }}</text>
|
||||
<view class="mt-1 break-all text-sm text-gray-900 leading-snug">
|
||||
{{ item.content }}
|
||||
</view>
|
||||
</view>
|
||||
<wd-icon name="copy" size="28rpx" class="shrink-0 text-primary" />
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
@@ -16,8 +16,8 @@
|
||||
const haloConfigs = computed(() => appConfigStore.configs)
|
||||
|
||||
const disclaimersContent = computed(() => {
|
||||
const basicConfig = haloConfigs.value.basicConfig as { disclaimers ?: { content ?: string } } | undefined
|
||||
return basicConfig?.disclaimers?.content || ''
|
||||
const pageConfig = haloConfigs.value.pageConfig as { disclaimers ?: { content ?: string } } | undefined
|
||||
return pageConfig?.disclaimers?.content || ''
|
||||
})
|
||||
|
||||
const bloggerInfo = computed(() => {
|
||||
|
||||
@@ -68,6 +68,9 @@ async function handleMiniPluginRefresh() {
|
||||
handleGetMiniProgramLinks()
|
||||
}
|
||||
|
||||
/** 是否开放公开提交申请(通用配置-友链信息-基本配置 submissionEnabled,2026-09-11 由 setting linkConfig 迁入;默认 true) */
|
||||
const submissionEnabled = computed(() => (haloPluginConfigs.value?.linkInfo?.submissionEnabled ?? true))
|
||||
|
||||
/* ---------------- tabs ---------------- */
|
||||
const activeTabIndex = ref(0)
|
||||
|
||||
@@ -575,8 +578,8 @@ onReachBottom(() => {
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部悬浮操作栏(通用组件) -->
|
||||
<uh-links-actions :actions="['apply', 'info']" @apply="handleOpenApply" @info="handleOpenMiniInfo" />
|
||||
<!-- 底部悬浮操作栏(通用组件);提交申请入口按 submissionEnabled 显隐(2026-09-11) -->
|
||||
<uh-links-actions :actions="submissionEnabled ? ['apply', 'info'] : ['info']" @apply="handleOpenApply" @info="handleOpenMiniInfo" />
|
||||
</view>
|
||||
|
||||
<!-- 小程序详情弹窗 -->
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
/**
|
||||
* 友链申请页(源自旧项目 pagesA/submit-link,新建复刻)
|
||||
* 提交友链交换信息表单
|
||||
*/
|
||||
import { computed, ref } from 'vue'
|
||||
import { onLoad } from '@dcloudio/uni-app'
|
||||
import { useAppConfigStore } from '@/store/appConfig'
|
||||
import { checkAvatarUrl } from '@/utils/url'
|
||||
|
||||
definePage({
|
||||
style: {
|
||||
navigationBarTitleText: '友链申请',
|
||||
},
|
||||
})
|
||||
|
||||
const appConfigStore = useAppConfigStore()
|
||||
const haloPluginConfigs = computed(() => appConfigStore.configs.pluginConfig)
|
||||
|
||||
const blogDetail = computed(() => (haloPluginConfigs.value?.linkInfo?.siteInfo as {
|
||||
displayName?: string
|
||||
url?: string
|
||||
logo?: string
|
||||
description?: string
|
||||
email?: string
|
||||
backlink?: string
|
||||
feedUrls?: string[]
|
||||
} | undefined) || {})
|
||||
|
||||
const blogDetailPoupShow = ref(false)
|
||||
|
||||
const form = ref({
|
||||
url: '',
|
||||
name: '',
|
||||
logo: '',
|
||||
linkPageUrl: '',
|
||||
email: '',
|
||||
rssUrl: '',
|
||||
description: '',
|
||||
})
|
||||
|
||||
const calcBlogContent = computed(() => `
|
||||
博客名称:${blogDetail.value.displayName}
|
||||
博客地址:${blogDetail.value.url}
|
||||
博客logo:${checkAvatarUrl(blogDetail.value.logo)}
|
||||
博客简介:${blogDetail.value.description}
|
||||
`)
|
||||
|
||||
function calcSiteThumbnail(val?: string): string {
|
||||
if (!val)
|
||||
return ''
|
||||
const _val = val.endsWith('/') ? val : `${val}/`
|
||||
return `https://image.thum.io/get/width/1000/crop/800/${_val}`
|
||||
}
|
||||
|
||||
function checkIsUrl(url: string): boolean {
|
||||
return /^https?:\/\//i.test(url)
|
||||
}
|
||||
|
||||
function checkIsEmail(email: string): boolean {
|
||||
return /^[\w.-]+@[\w-]+(?:\.[\w-]+)+$/.test(email)
|
||||
}
|
||||
|
||||
async function handleHandle() {
|
||||
if (!form.value.name.trim()) {
|
||||
uni.showToast({ icon: 'none', title: '请输入网站名称!' })
|
||||
return
|
||||
}
|
||||
if (!checkIsUrl(form.value.url)) {
|
||||
uni.showToast({ icon: 'none', title: '请输入正确的网站地址!' })
|
||||
return
|
||||
}
|
||||
if (form.value.logo && !checkIsUrl(form.value.logo)) {
|
||||
uni.showToast({ icon: 'none', title: '请输入正确的Logo地址!' })
|
||||
return
|
||||
}
|
||||
if (form.value.email && !checkIsEmail(form.value.email)) {
|
||||
uni.showToast({ icon: 'none', title: '请输入正确的邮箱地址!' })
|
||||
return
|
||||
}
|
||||
|
||||
// linksSubmitPlugin 已弃用(2026-09-08),第三方友链自助提交暂未开放;
|
||||
// 后续可对接 Halo 官方 plugin-links link-applications 接口
|
||||
uni.showToast({ icon: 'none', title: '友链申请功能暂未开放,请联系站长' })
|
||||
}
|
||||
|
||||
function handleCopyLink() {
|
||||
uni.setClipboardData({
|
||||
data: calcBlogContent.value,
|
||||
showToast: false,
|
||||
success: () => {
|
||||
uni.showToast({ icon: 'none', title: '复制成功!' })
|
||||
},
|
||||
fail: () => {
|
||||
uni.showToast({ icon: 'none', title: '复制失败!' })
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onLoad(() => {
|
||||
uni.setNavigationBarTitle({ title: '友链申请' })
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="app-page box-border min-h-screen w-screen bg-[#fafafd] p-8">
|
||||
<!-- 博客详情卡片 -->
|
||||
<view class="blog-coupon mb-6 flex items-center rounded-xl p-6" style="background: linear-gradient(135deg, #2196f3, #64b5f6);" @click="blogDetailPoupShow = true">
|
||||
<image class="coupon-img h-[80rpx] w-[80rpx] shrink-0 rounded-xl" :src="checkAvatarUrl(blogDetail.logo)" mode="aspectFill" />
|
||||
<view class="coupon-info ml-5 flex-1">
|
||||
<view class="coupon-title text-[30rpx] text-white font-bold">
|
||||
{{ blogDetail.displayName }}
|
||||
</view>
|
||||
<view class="coupon-desc mt-1 text-[24rpx] text-white/80">
|
||||
{{ blogDetail.description }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="coupon-btn border-2 border-white/60 rounded-3xl px-5 py-1 text-[24rpx] text-white">
|
||||
友链详情
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 友链信息提交表单 -->
|
||||
<view class="form-wrap rounded-xl bg-white p-6 shadow-sm">
|
||||
<view class="form-title mb-6 text-[26rpx] font-bold">
|
||||
友链信息提交
|
||||
</view>
|
||||
|
||||
<view class="form-item flex items-center border-b-2 border-[#f5f5f5] py-5">
|
||||
<text class="label w-[160rpx] shrink-0 text-[26rpx] text-[#333]">名称</text>
|
||||
<input v-model="form.name" class="input flex-1 text-[26rpx]" placeholder="请输入网站名称">
|
||||
</view>
|
||||
<view class="form-item flex items-center border-b-2 border-[#f5f5f5] py-5">
|
||||
<text class="label w-[160rpx] shrink-0 text-[26rpx] text-[#333]">网址</text>
|
||||
<input v-model="form.url" class="input flex-1 text-[26rpx]" placeholder="请输入网站地址">
|
||||
</view>
|
||||
<view class="form-item flex items-center border-b-2 border-[#f5f5f5] py-5">
|
||||
<text class="label w-[160rpx] shrink-0 text-[26rpx] text-[#333]">Logo</text>
|
||||
<input v-model="form.logo" class="input flex-1 text-[26rpx]" placeholder="请输入网站Logo">
|
||||
</view>
|
||||
<view class="form-item flex items-center border-b-2 border-[#f5f5f5] py-5">
|
||||
<text class="label w-[160rpx] shrink-0 text-[26rpx] text-[#333]">邮箱</text>
|
||||
<input v-model="form.email" class="input flex-1 text-[26rpx]" placeholder="请输入邮箱">
|
||||
</view>
|
||||
<view class="form-item flex items-center border-b-2 border-[#f5f5f5] py-5">
|
||||
<text class="label w-[160rpx] shrink-0 text-[26rpx] text-[#333]">友链页面</text>
|
||||
<input v-model="form.linkPageUrl" class="input flex-1 text-[26rpx]" placeholder="请输入友链页面地址">
|
||||
</view>
|
||||
<view class="form-tip py-1 pl-[160rpx] text-[22rpx] text-[#999]">
|
||||
(贵站友情链接页面地址,即包含本站链接的页面)
|
||||
</view>
|
||||
<view class="form-item flex items-center border-b-2 border-[#f5f5f5] py-5">
|
||||
<text class="label w-[160rpx] shrink-0 text-[26rpx] text-[#333]">RSS地址</text>
|
||||
<input v-model="form.rssUrl" class="input flex-1 text-[26rpx]" placeholder="请输入RSS地址">
|
||||
</view>
|
||||
<view class="form-tip py-1 pl-[160rpx] text-[22rpx] text-[#999]">
|
||||
(用于抓取文章)
|
||||
</view>
|
||||
<view class="form-item flex items-center border-b-2 border-[#f5f5f5] py-5">
|
||||
<text class="label w-[160rpx] shrink-0 text-[26rpx] text-[#333]">网站描述</text>
|
||||
<textarea v-model="form.description" class="textarea h-[100rpx] flex-1 text-[26rpx]" :maxlength="30" placeholder="请输入网站描述,不超过30字符" />
|
||||
</view>
|
||||
|
||||
<view class="submit-btn mt-6">
|
||||
<wd-button type="primary" block size="medium" @click="handleHandle">
|
||||
提交数据
|
||||
</wd-button>
|
||||
<view class="submit-tip py-8 text-center text-[24rpx] text-[#999]">
|
||||
友链申请
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 博客详情弹窗 -->
|
||||
<wd-popup v-model="blogDetailPoupShow" position="center" custom-style="width:640rpx;border-radius:12rpx;">
|
||||
<view class="poup p-9">
|
||||
<view class="info flex">
|
||||
<image class="poup-logo h-[140rpx] w-[140rpx] rounded-3xl" :src="checkAvatarUrl(blogDetail.logo)" mode="aspectFill" />
|
||||
<view class="info-detail ml-6 flex flex-1 flex-col justify-center">
|
||||
<view class="poup-name text-[34rpx] font-bold">
|
||||
{{ blogDetail.displayName }}
|
||||
</view>
|
||||
<view class="poup-tag mt-2.5 text-[24rpx] text-[#999]">
|
||||
{{ blogDetail.description }}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="poup-desc mt-6 whitespace-pre-wrap text-[28rpx] text-[#555] leading-[1.8]">
|
||||
<text>{{ calcBlogContent }}</text>
|
||||
</view>
|
||||
<image v-if="blogDetail.url" class="poup-img mt-6 h-[320rpx] w-[568rpx] rounded-xl" :src="calcSiteThumbnail(blogDetail.url)" mode="aspectFill" />
|
||||
<view class="poup-link my-6 flex justify-center gap-6">
|
||||
<wd-button size="small" plain type="primary" @click="handleCopyLink">
|
||||
复制友链交换信息
|
||||
</wd-button>
|
||||
<wd-button size="small" plain @click="blogDetailPoupShow = false">
|
||||
关闭
|
||||
</wd-button>
|
||||
</view>
|
||||
</view>
|
||||
</wd-popup>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.app-page {
|
||||
/* 布局全部由 UnoCSS 原子类实现 */
|
||||
}
|
||||
</style>
|
||||
@@ -53,7 +53,7 @@
|
||||
})
|
||||
|
||||
const pageConfig = computed(() => haloConfigs.value.pageConfig?.aboutConfig as
|
||||
| { bgImageUrl ?: string, waveImageUrl ?: string }
|
||||
| { bgImageUrl ?: string, waveImageUrl ?: string, copyrightConfig ?: { enabled ?: boolean, content ?: string } }
|
||||
| undefined)
|
||||
|
||||
const calcProfileStyle = computed(() => ({
|
||||
@@ -62,18 +62,26 @@
|
||||
|
||||
const calcWaveUrl = computed(() => checkImageUrl(pageConfig.value?.waveImageUrl))
|
||||
|
||||
const basicConfig = computed(() => haloConfigs.value.basicConfig as
|
||||
| {
|
||||
copyrightConfig ?: { enabled ?: boolean, content ?: string }
|
||||
disclaimers ?: { enabled ?: boolean }
|
||||
showAboutSystem ?: boolean
|
||||
}
|
||||
| undefined)
|
||||
/** 免责声明(2026-09-10 起插件端从 basicConfig 迁入 pageConfig.disclaimers,无 enabled 开关,按内容判定入口显隐) */
|
||||
const pageDisclaimers = computed(() => (haloConfigs.value.pageConfig as { disclaimers ?: { content ?: string } } | undefined)?.disclaimers)
|
||||
|
||||
const copyrightConfig = computed(() => basicConfig.value?.copyrightConfig)
|
||||
/** 页脚版权(2026-09-10 起插件端从 basicConfig.copyrightConfig 迁入 pageConfig.aboutConfig.copyrightConfig) */
|
||||
const copyrightConfig = computed(() => pageConfig.value?.copyrightConfig)
|
||||
|
||||
const loveEnabled = computed(() => !!(haloConfigs.value.loveConfig as { loveEnabled ?: boolean } | undefined)?.loveEnabled)
|
||||
const socialEnabled = computed(() => !!(haloConfigs.value.authorConfig?.social as { enabled ?: boolean } | undefined)?.enabled)
|
||||
/** 恋爱入口:总开关 loveEnabled 2026-09-10 下线,改按模块入口开关判定(任一开启即展示) */
|
||||
const loveEnabled = computed(() => {
|
||||
const love = haloConfigs.value.loveConfig as
|
||||
| { ourStory ?: { enabled ?: boolean }, lovePhoto ?: { enabled ?: boolean }, loveDaily ?: { enabled ?: boolean } }
|
||||
| undefined
|
||||
return !!(love?.ourStory?.enabled || love?.lovePhoto?.enabled || love?.loveDaily?.enabled)
|
||||
})
|
||||
/** 社交信息入口:2026-09-10 起去 enabled 开关,改按社交项列表判定(存在可见且有内容的社交项即展示) */
|
||||
const socialEnabled = computed(() => {
|
||||
const social = haloConfigs.value.authorConfig?.social as
|
||||
| { items ?: Array<{ visible ?: boolean, content ?: string }> }
|
||||
| undefined
|
||||
return !!(social?.items?.some(item => item.visible !== false && !!item.content))
|
||||
})
|
||||
|
||||
/* ---------------- 状态 ---------------- */
|
||||
const statisticsShowMore = ref(false)
|
||||
@@ -280,7 +288,8 @@
|
||||
bgColor: 'rgba(121, 85, 72, 0.95)',
|
||||
rightText: '博客内容免责声明',
|
||||
path: '/pages-blog/disclaimers/disclaimers',
|
||||
show: !!basicConfig.value?.disclaimers?.enabled,
|
||||
// 2026-09-10 起无 enabled 开关,按内容非空判定入口显隐
|
||||
show: !!pageDisclaimers.value?.content,
|
||||
// show: true,
|
||||
group: 'more',
|
||||
},
|
||||
@@ -304,7 +313,8 @@
|
||||
bgColor: 'rgba(96, 125, 139, 0.95)',
|
||||
rightText: '小莫唐尼开源项目',
|
||||
path: '/pages-blog/about/about',
|
||||
show: !!basicConfig.value?.showAboutSystem,
|
||||
// showAboutSystem 2026-09-10 下线(入口由插件端功能入口 myPageConfig 统一管理),默认展示
|
||||
show: true,
|
||||
// show: true,
|
||||
group: 'more',
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user