feat: 添加有来技术公众号页面及相关功能优化

This commit is contained in:
Ray.Hao
2026-03-07 19:10:16 +08:00
parent 6caee74a86
commit 6ea8f2e7bc
9 changed files with 386 additions and 336 deletions

View File

@@ -1,29 +1,19 @@
<template>
<view class="home-page">
<custom-navbar title="首页" fixed placeholder :showBack="false">
<template #right>
<wd-search
v-model="searchValue"
class="navbar__search"
hide-cancel
disabled
@click="handleSearch"
/>
</template>
</custom-navbar>
<!-- 轮播图 -->
<wd-swiper
v-model:current="current"
custom-class="swiper-box"
:list="swiperList"
autoplay
@click="handleSwiperClick"
@change="handleSwiperChange"
/>
<view class="hero">
<wd-swiper
v-model:current="current"
custom-class="swiper-box"
:list="swiperList"
autoplay
@click="handleSwiperClick"
@change="handleSwiperChange"
/>
</view>
<!-- 快捷导航 -->
<view class="section">
<view class="section section--overlay">
<wd-grid clickable :column="4">
<wd-grid-item
v-for="(item, index) in navList"
@@ -118,9 +108,10 @@
* - 支持暗黑模式(通过 CSS 变量)
*/
import { reactive, ref } from "vue";
import { onReady } from "@dcloudio/uni-app";
import { dayjs } from "wot-design-uni";
import { useRouter } from "uni-mini-router";
import CustomNavbar from "@/components/custom-navbar/index.vue";
// ============================================================================
// 类型定义
@@ -154,7 +145,6 @@ const router = useRouter();
// ============================================================================
const current = ref(0);
const searchValue = ref("");
const recentDaysRange = ref(7);
const swiperList = ref(["https://www.youlai.tech/storage/blog/banner9.png"]);
@@ -268,10 +258,6 @@ function loadVisitTrendData() {
// 事件处理
// ============================================================================
function handleSearch() {
uni.showToast({ title: "搜索功能开发中", icon: "none" });
}
function handleNavClick(item: NavItem) {
router.push({ path: item.url });
}
@@ -317,6 +303,39 @@ onReady(() => {
background-color: var(--color-bg-secondary);
}
.hero {
position: relative;
}
.hero::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 120rpx;
background: linear-gradient(
to bottom,
rgba(245, 247, 250, 0) 0%,
rgba(245, 247, 250, 0.85) 60%,
rgba(245, 247, 250, 1) 100%
);
pointer-events: none;
z-index: 1;
}
:deep(.swiper-box) {
height: 420rpx;
overflow: hidden;
border-bottom-left-radius: 32rpx;
border-bottom-right-radius: 32rpx;
}
:deep(.swiper-box .wd-swiper__item),
:deep(.swiper-box image) {
height: 420rpx;
}
// ============================================================================
// 导航栏
// ============================================================================
@@ -369,6 +388,16 @@ onReady(() => {
margin: 24rpx;
}
.section--overlay {
position: relative;
z-index: 2;
margin-top: -72rpx;
padding: 16rpx 8rpx;
background: var(--color-bg);
border-radius: 24rpx;
box-shadow: 0 16rpx 36rpx rgba(0, 0, 0, 0.08);
}
// ============================================================================
// 快捷导航项
// ============================================================================

View File

@@ -106,7 +106,12 @@
</view>
<!-- 登录按钮 -->
<button class="btn-primary" :disabled="loading" @click="handleLogin">
<button
class="btn-primary"
:class="{ 'btn-primary--loading': loading }"
:disabled="loading"
@click="handleLogin"
>
{{ loading ? "登录中..." : "登 录" }}
</button>
@@ -866,6 +871,7 @@ const handleBack = () => {
// 主按钮
.btn-primary {
position: relative;
display: flex;
align-items: center;
justify-content: center;
@@ -878,14 +884,44 @@ const handleBack = () => {
border-radius: 12px;
box-shadow: 0 10px 15px -3px rgba(59, 130, 246, 0.3);
transition: all 0.2s;
overflow: hidden;
&:active {
transform: scale(0.98);
}
&:disabled {
box-shadow: none;
opacity: 0.5;
opacity: 0.9;
box-shadow: 0 8px 14px -6px rgba(59, 130, 246, 0.22);
transform: none;
}
}
.btn-primary--loading {
opacity: 0.92;
}
.btn-primary--loading::before {
position: absolute;
inset: 0;
content: "";
background: linear-gradient(
110deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.16) 30%,
rgba(255, 255, 255, 0) 60%
);
transform: translateX(-100%);
animation: btnShimmer 1.1s ease-in-out infinite;
}
@keyframes btnShimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}

View File

@@ -1,5 +1,5 @@
<template>
<view class="page-container">
<view class="page-container dark:text-[var(--wot-color-text)]">
<custom-navbar
title="我的主页"
bg-color="transparent"
@@ -20,16 +20,25 @@
:src="isLogin ? userInfo!.avatar : defaultAvatar"
mode="aspectFill"
/>
<view v-if="genderIconName" class="gender-icon" :class="genderIconClass">
<wd-icon :name="genderIconName" size="12" color="#fff" />
</view>
</view>
<view class="user-details">
<block v-if="isLogin">
<view class="nickname">
{{ userInfo!.nickname || "匿名用户" }}
<text v-if="genderLabel" class="gender-badge" :class="genderClass">
{{ genderLabel }}
</text>
</view>
<view class="user-id">ID: {{ userInfo?.username || "0000000" }}</view>
<view class="user-meta">
<view class="meta-row">
<wd-icon name="user" size="14" color="rgba(255, 255, 255, 0.92)" />
<text class="meta-value">{{ userInfo?.username || "0000000" }}</text>
</view>
<view v-if="deptNameText" class="meta-row">
<wd-icon name="home" size="14" color="rgba(255, 255, 255, 0.92)" />
<text class="meta-value">{{ deptNameText }}</text>
</view>
</view>
</block>
<block v-else>
<view class="login-prompt">立即登录获取更多功能</view>
@@ -45,55 +54,12 @@
</view>
<view class="header-actions">
<view class="action-btn relative" @click="navigateToNotifications">
<wd-icon name="notification" size="20" color="#fff" />
<view v-if="true" class="action-badge">2</view>
</view>
<view class="action-btn" @click="navigateToSettings">
<wd-icon name="setting" size="22" color="#fff" />
</view>
</view>
</view>
</view>
<view v-if="isLogin" class="section-card">
<view class="section-header">
<view class="section-title">
<wd-icon name="user" size="18" :color="currentThemeColor" />
<text class="title-text">资料卡片</text>
</view>
</view>
<view class="profile-grid">
<view class="profile-tile">
<view class="tile-icon" :style="{ backgroundColor: currentThemeColor + '15' }">
<wd-icon name="secured" size="20" :color="currentThemeColor" />
</view>
<view class="tile-content">
<text class="tile-label">角色</text>
<text class="tile-value">{{ roleText }}</text>
</view>
</view>
<view class="profile-tile">
<view class="tile-icon" :style="{ backgroundColor: currentThemeColor + '15' }">
<wd-icon name="goods" size="20" :color="currentThemeColor" />
</view>
<view class="tile-content">
<text class="tile-label">部门</text>
<text class="tile-value">{{ deptText }}</text>
</view>
</view>
<view class="profile-tile">
<view class="tile-icon" :style="{ backgroundColor: currentThemeColor + '15' }">
<wd-icon name="phone" size="20" :color="currentThemeColor" />
</view>
<view class="tile-content">
<text class="tile-label">手机</text>
<text class="tile-value">{{ mobileText }}</text>
</view>
</view>
<view class="profile-tile">
<view class="tile-icon" :style="{ backgroundColor: currentThemeColor + '15' }">
<wd-icon name="mail" size="20" :color="currentThemeColor" />
</view>
<view class="tile-content">
<text class="tile-label">邮箱</text>
<text class="tile-value">{{ emailText }}</text>
<wd-icon name="setting" size="20" color="#fff" />
</view>
</view>
</view>
@@ -102,31 +68,21 @@
<view class="section-card">
<view class="section-header">
<view class="section-title">
<wd-icon name="setting" size="18" :color="currentThemeColor" />
<text class="title-text">系统设置</text>
<wd-icon name="user" size="18" :color="currentThemeColor" />
<text class="title-text">个人中心</text>
</view>
</view>
<view class="menu-list">
<view class="menu-item" @click="navigateToUserAgreement">
<wd-icon name="note" size="20" color="#666" />
<text class="menu-text">用户协议</text>
<view class="menu-item" @click="navigateToProfile">
<wd-icon name="user" size="20" color="#666" />
<text class="menu-text">个人资料</text>
<wd-icon name="arrow-right" size="16" color="#ccc" />
</view>
<view class="menu-item" @click="navigateToAbout">
<wd-icon name="info-circle" size="20" color="#666" />
<text class="menu-text">关于我们</text>
<view class="menu-item" @click="navigateToAccount">
<wd-icon name="secured" size="20" color="#666" />
<text class="menu-text">账号和安全</text>
<wd-icon name="arrow-right" size="16" color="#ccc" />
</view>
<view class="menu-item" @click="navigateToNetworkTest">
<wd-icon name="wifi" size="20" color="#666" />
<text class="menu-text">网络测试</text>
<wd-icon name="arrow-right" size="16" color="#ccc" />
</view>
<view class="menu-item" @click="handleClearCache">
<wd-icon name="delete" size="20" color="#666" />
<text class="menu-text">清空缓存</text>
<text class="menu-value">{{ cacheSize }}</text>
</view>
</view>
</view>
@@ -138,11 +94,6 @@
</view>
</view>
<view class="menu-list">
<view class="menu-item" @click="navigateToFAQ">
<wd-icon name="question-circle" size="20" color="#666" />
<text class="menu-text">常见问题</text>
<wd-icon name="arrow-right" size="16" color="#ccc" />
</view>
<view class="menu-item" @click="handleQuestionFeedback">
<wd-icon name="edit" size="20" color="#666" />
<text class="menu-text">问题反馈</text>
@@ -151,22 +102,20 @@
</view>
</view>
<!-- 退出登录按钮 -->
<view v-if="isLogin" class="logout-section">
<view class="logout-btn" @click="handleLogout">
<text class="logout-text">退出登录</text>
<view class="section-card official-section">
<view class="official-card" @click="navigateToOfficialAccount">
<image class="official-avatar" src="/static/logo.png" mode="aspectFill" />
<view class="official-body">
<view class="official-title">有来技术</view>
<view class="official-desc">技术干货 · 开源社区 · 交流群</view>
</view>
<view class="official-action">
<wd-icon name="arrow-right" size="18" color="#9ca3af" />
</view>
</view>
</view>
<wd-toast />
<wd-loading
v-if="clearing"
v-model="clearing"
text="正在清理..."
mask
custom-class="loading-center"
/>
</view>
</template>
@@ -180,12 +129,10 @@
<script lang="ts" setup>
import { onLoad } from "@dcloudio/uni-app";
import { useToast } from "wot-design-uni";
import { useUserStore, useThemeStore } from "@/store";
import { useRouter } from "uni-mini-router";
import { useNavbar } from "@/composables/useNavbar";
const toast = useToast();
const userStore = useUserStore();
const themeStore = useThemeStore();
const currentThemeColor = computed(() => themeStore.themeVars.colorTheme);
@@ -205,64 +152,33 @@ const router = useRouter();
// 使用导航栏 HookTabBar 页面)
const navbar = useNavbar({ hasTabbar: true });
const roleText = computed(() => {
if (!userInfo.value) return "-";
return userInfo.value.roleName || userInfo.value.roles?.join("、") || "-";
});
const deptText = computed(() => userInfo.value?.deptName || "-");
const mobileText = computed(() => userInfo.value?.mobile || "-");
const emailText = computed(() => userInfo.value?.email || "-");
const genderValue = computed(() => (userInfo.value as any)?.gender);
const genderLabel = computed(() => {
if (genderValue.value === 1) return "♂";
if (genderValue.value === 2) return "♀";
return "";
const normalizedGender = computed(() => {
const v = genderValue.value;
const n = typeof v === "number" ? v : Number(v);
return Number.isFinite(n) ? n : 0;
});
const genderClass = computed(() => {
if (genderValue.value === 1) return "gender-badge--male";
if (genderValue.value === 2) return "gender-badge--female";
const deptNameText = computed(() => {
if (!isLogin.value) return "";
return (userInfo.value as any)?.deptName || "";
});
const genderIconName = computed(() => {
if (!isLogin.value) return "";
if (normalizedGender.value === 1) return "gender-male";
if (normalizedGender.value === 2) return "gender-female";
return "";
});
const clearing = ref(false);
const cacheSize = ref<any>("-");
const formatSize = (size: number) => {
if (size < 1024) {
return size + "B";
} else if (size < 1024 * 1024) {
return (size / 1024).toFixed(2) + "KB";
} else {
return (size / 1024 / 1024).toFixed(2) + "MB";
}
};
const getCacheSize = async () => {
try {
// #ifdef MP-WEIXIN
const res = await uni.getStorageInfo();
cacheSize.value = formatSize(res.currentSize);
// #endif
// #ifdef H5
cacheSize.value = formatSize(
Object.keys(localStorage).reduce((size, key) => size + localStorage[key].length, 0)
);
// #endif
if (!cacheSize.value) {
cacheSize.value = "0B";
}
} catch (error) {
console.error("获取缓存大小失败:", error);
cacheSize.value = "-";
}
};
onLoad(() => {
getCacheSize();
const genderIconClass = computed(() => {
if (normalizedGender.value === 1) return "gender-icon--male";
if (normalizedGender.value === 2) return "gender-icon--female";
return "";
});
onLoad(() => {});
// 登录
const navigateToLoginPage = () => {
const pages = getCurrentPages();
@@ -271,20 +187,6 @@ const navigateToLoginPage = () => {
router.push({ path: "/pages/login/index", query: { redirect: currentPagePath } });
};
// 退出登录
const handleLogout = () => {
uni.showModal({
title: "提示",
content: "确认退出登录吗?",
success: function (res) {
if (res.confirm) {
userStore.logout();
toast.show("已退出登录");
}
},
});
};
// 个人信息
const navigateToProfile = () => {
if (!isLogin.value) {
@@ -294,14 +196,20 @@ const navigateToProfile = () => {
router.push({ path: "/pages/mine/profile/index" });
};
// 常见问题
const navigateToFAQ = () => {
router.push({ path: "/pages/mine/faq/index" });
// 账号和安全
const navigateToAccount = () => {
if (!isLogin.value) {
navigateToLoginPage();
return;
}
router.push({ path: "/pages/mine/settings/account/index" });
};
// 关于我们
const navigateToAbout = () => {
router.push({ path: "/pages/mine/about/index" });
const navigateToNotifications = () => {
uni.showToast({
title: "功能开发中",
icon: "none",
});
};
// 设置
@@ -309,40 +217,19 @@ const navigateToSettings = () => {
router.push({ path: "/pages/mine/settings/index" });
};
const navigateToUserAgreement = () => {
router.push({ path: "/pages/mine/settings/agreement/index" });
};
const navigateToNetworkTest = () => {
router.push({ path: "/pages/mine/settings/network/index" });
};
// 问题反馈
const handleQuestionFeedback = () => {
router.push({ path: "/pages/mine/feedback/index" });
};
const handleClearCache = async () => {
if (clearing.value) return;
try {
clearing.value = true;
await new Promise((resolve) => setTimeout(resolve, 800));
await uni.clearStorage();
await getCacheSize();
toast.show("清理成功");
} catch {
toast.show("清理失败");
} finally {
clearing.value = false;
}
// 有来技术公众号
const navigateToOfficialAccount = () => {
router.push({ path: "/pages/mine/official/index" });
};
</script>
<style lang="scss" scoped>
.page-container {
background: #f5f7fa;
}
// page-container 使用全局样式,支持暗黑模式
// 用户信息卡片
.user-profile {
@@ -355,7 +242,7 @@ const handleClearCache = async () => {
right: 0;
left: 0;
z-index: 0;
height: 380rpx;
height: 460rpx;
border-bottom-right-radius: 48rpx;
border-bottom-left-radius: 48rpx;
}
@@ -365,19 +252,41 @@ const handleClearCache = async () => {
z-index: 1;
display: flex;
align-items: center;
padding: 32rpx 32rpx 40rpx;
padding: 48rpx 32rpx 56rpx;
padding-right: 140rpx;
.avatar-container {
position: relative;
.avatar {
width: 128rpx;
height: 128rpx;
width: 144rpx;
height: 144rpx;
border: 4rpx solid rgba(255, 255, 255, 0.9);
border-radius: 50%;
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
}
.gender-icon {
position: absolute;
right: 8rpx;
bottom: 8rpx;
display: flex;
align-items: center;
justify-content: center;
width: 28rpx;
height: 28rpx;
border: 2rpx solid rgba(255, 255, 255, 0.9);
border-radius: 50%;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.18);
}
.gender-icon--male {
background-color: #409eff;
}
.gender-icon--female {
background-color: #ff6384;
}
}
.user-details {
@@ -386,40 +295,33 @@ const handleClearCache = async () => {
.nickname {
margin-bottom: 10rpx;
font-size: 36rpx;
font-size: 40rpx;
font-weight: 700;
color: #fff;
.gender-badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 36rpx;
height: 36rpx;
padding: 0 12rpx;
margin-left: 12rpx;
font-size: 22rpx;
font-weight: 700;
line-height: 36rpx;
color: #fff;
background-color: rgba(255, 255, 255, 0.25);
border-radius: 999px;
}
.gender-badge--male {
background-color: rgba(64, 158, 255, 0.5);
}
.gender-badge--female {
background-color: rgba(255, 99, 132, 0.5);
}
}
.user-id {
font-size: 24rpx;
.user-meta {
margin-top: 4rpx;
display: flex;
flex-direction: column;
gap: 6rpx;
color: rgba(255, 255, 255, 0.85);
}
.meta-row {
display: flex;
align-items: center;
gap: 8rpx;
font-size: 24rpx;
line-height: 1.2;
}
.meta-value {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.login-prompt {
margin-bottom: 16rpx;
font-size: 28rpx;
@@ -432,6 +334,7 @@ const handleClearCache = async () => {
top: 50%;
right: 32rpx;
display: flex;
gap: 20rpx;
transform: translateY(-50%);
}
@@ -439,8 +342,8 @@ const handleClearCache = async () => {
display: flex;
align-items: center;
justify-content: center;
width: 72rpx;
height: 72rpx;
width: 64rpx;
height: 64rpx;
background-color: rgba(255, 255, 255, 0.28);
backdrop-filter: blur(8px);
border-radius: 50%;
@@ -494,62 +397,6 @@ const handleClearCache = async () => {
color: #1f2937;
}
// 资料卡片网格
.profile-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20rpx;
}
.profile-tile {
display: flex;
align-items: center;
padding: 20rpx;
background: #f9fafb;
border-radius: 14rpx;
transition: background 0.2s;
&:active {
background: #f3f4f6;
}
}
.tile-icon {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 48rpx;
height: 48rpx;
margin-right: 16rpx;
border-radius: 12rpx;
}
.tile-content {
flex: 1;
overflow: hidden;
}
.tile-label {
display: block;
margin-bottom: 6rpx;
overflow: hidden;
font-size: 22rpx;
color: #9ca3af;
text-overflow: ellipsis;
white-space: nowrap;
}
.tile-value {
display: block;
overflow: hidden;
font-size: 28rpx;
font-weight: 600;
color: #1f2937;
text-overflow: ellipsis;
white-space: nowrap;
}
// 菜单列表
.menu-list {
display: flex;
@@ -588,6 +435,54 @@ const handleClearCache = async () => {
color: #9ca3af;
}
.official-section {
margin-top: 16rpx;
}
.official-card {
display: flex;
align-items: center;
padding: 32rpx 28rpx;
background: #fff;
border-radius: 20rpx;
box-shadow: 0 2rpx 16rpx rgba(0, 0, 0, 0.04);
}
.official-avatar {
width: 100rpx;
height: 100rpx;
border-radius: 24rpx;
}
.official-body {
flex: 1;
min-width: 0;
margin-left: 28rpx;
}
.official-title {
font-size: 32rpx;
font-weight: 600;
color: #1f2937;
}
.official-desc {
margin-top: 12rpx;
font-size: 24rpx;
color: #9ca3af;
letter-spacing: 1rpx;
}
.official-action {
display: flex;
align-items: center;
justify-content: center;
width: 56rpx;
height: 56rpx;
background: #f3f4f6;
border-radius: 50%;
}
// 退出登录
.logout-section {
padding: 40rpx 28rpx 80rpx;

View File

@@ -0,0 +1,108 @@
<template>
<view class="page-container dark:text-[var(--wot-color-text)]">
<view class="page-content">
<view class="header">
<image class="logo" src="/static/logo.png" mode="aspectFill" />
<view class="header-info">
<view class="title">有来技术</view>
<view class="sub-title">关注公众号获取更多技术干货</view>
</view>
</view>
<view class="qr-card">
<image
class="qr-image"
src="/static/images/qrcode-official.png"
mode="widthFix"
show-menu-by-longpress
/>
<view class="qr-tip">长按识别二维码关注公众号</view>
<view class="group-tip">
如需加入技术交流群请添加微信 haoxianrui 并备注前端/后端/全栈邀您入群
</view>
</view>
</view>
</view>
</template>
<route lang="json">
{
"name": "official",
"style": {
"navigationBarTitleText": "有来技术"
}
}
</route>
<script lang="ts" setup></script>
<style lang="scss" scoped>
.page-content {
padding: 28rpx;
padding-bottom: calc(28rpx + env(safe-area-inset-bottom));
}
.header {
display: flex;
align-items: center;
gap: 20rpx;
padding: 28rpx;
background: #fff;
border-radius: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
.logo {
width: 96rpx;
height: 96rpx;
border-radius: 20rpx;
}
.header-info {
flex: 1;
min-width: 0;
}
.title {
font-size: 34rpx;
font-weight: 700;
color: #111827;
}
.sub-title {
margin-top: 8rpx;
font-size: 26rpx;
color: #6b7280;
}
.group-tip {
margin-top: 20rpx;
padding: 20rpx;
font-size: 26rpx;
line-height: 1.6;
color: #374151;
background: #f9fafb;
border-radius: 16rpx;
}
.qr-card {
margin-top: 24rpx;
padding: 28rpx;
background: #fff;
border-radius: 20rpx;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.04);
}
.qr-image {
width: 100%;
border-radius: 16rpx;
}
.qr-tip {
margin-top: 20rpx;
text-align: center;
font-size: 26rpx;
color: #6b7280;
}
</style>

View File

@@ -1,14 +1,6 @@
<template>
<view class="page-container dark:text-[var(--wot-color-text)]">
<wd-cell-group>
<wd-cell v-if="isLogin" title="个人资料" icon="user" is-link @click="navigateToProfile" />
<wd-cell
v-if="isLogin"
title="账号和安全"
icon="secured"
is-link
@click="navigateToAccount"
/>
<wd-cell title="主题设置" icon="setting1" is-link @click="navigateToTheme" />
<wd-cell title="用户协议" icon="user" is-link @click="navigateToUserAgreement" />
<wd-cell title="关于我们" icon="info-circle" is-link @click="navigateToAbout" />
@@ -42,30 +34,11 @@
<script lang="ts" setup>
import { useUserStore } from "@/store";
import { checkLogin } from "@/utils/auth";
import { onLoad } from "@dcloudio/uni-app";
const userStore = useUserStore();
const isLogin = computed(() => !!userStore.userInfo);
// 个人资料
const navigateToProfile = () => {
if (checkLogin()) {
uni.navigateTo({
url: "/pages/mine/profile/index",
});
}
};
// 账号和安全
const navigateToAccount = () => {
if (checkLogin()) {
uni.navigateTo({
url: "/pages/mine/settings/account/index",
});
}
};
// 主题设置
const navigateToTheme = () => {
uni.navigateTo({