feat: deliver V1.1 dual-client UI fidelity milestone
@@ -1,137 +1,68 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-container class="main-layout">
|
<el-container class="main-layout">
|
||||||
<el-aside :width="isCollapse ? '64px' : '220px'" class="sidebar">
|
<el-aside :width="isCollapse ? '72px' : '220px'" class="sidebar">
|
||||||
<div class="logo">
|
<div class="brand" :class="{ compact: isCollapse }">
|
||||||
<span v-if="!isCollapse">丹阳公益婚恋</span>
|
<div class="brand-symbol">缘</div>
|
||||||
<span v-else>丹阳</span>
|
<div v-if="!isCollapse" class="brand-copy"><strong>善缘丹阳</strong><span>公益婚恋运营平台</span></div>
|
||||||
</div>
|
</div>
|
||||||
<el-menu :default-active="activeMenu" :collapse="isCollapse" :router="true"
|
<el-scrollbar class="menu-scroll">
|
||||||
background-color="#304156" text-color="#bfcbd9" active-text-color="#E96B6B" class="sidebar-menu">
|
<el-menu :default-active="activeMenu" :collapse="isCollapse" :router="true" class="sidebar-menu">
|
||||||
<el-menu-item index="/dashboard" :disabled="false">
|
<el-menu-item index="/dashboard"><el-icon><DataBoard /></el-icon><template #title>工作台</template></el-menu-item>
|
||||||
<el-icon><DataBoard /></el-icon><template #title>工作台</template>
|
<el-menu-item-group v-if="showGroup(['review.view','user.view'])" title="资料与用户">
|
||||||
</el-menu-item>
|
<el-menu-item v-if="hasPerm('review.view')" index="/review"><el-icon><DocumentChecked /></el-icon><template #title>资料审核</template></el-menu-item>
|
||||||
<el-menu-item v-if="hasPerm('review.view')" index="/review">
|
<el-menu-item v-if="hasPerm('user.view')" index="/users"><el-icon><User /></el-icon><template #title>用户管理</template></el-menu-item>
|
||||||
<el-icon><DocumentChecked /></el-icon><template #title>资料审核</template>
|
</el-menu-item-group>
|
||||||
</el-menu-item>
|
<el-menu-item-group v-if="showGroup(['counseling.service.view','service_program.manage','referral.manage'])" title="婚恋服务">
|
||||||
<el-menu-item v-if="hasPerm('user.view')" index="/users">
|
<el-menu-item v-if="hasPerm('counseling.service.view')" index="/counseling"><el-icon><ChatDotRound /></el-icon><template #title>婚恋咨询</template></el-menu-item>
|
||||||
<el-icon><User /></el-icon><template #title>用户管理</template>
|
<el-menu-item v-if="hasPerm('service_program.manage')" index="/service-program"><el-icon><Service /></el-icon><template #title>专项公益服务</template></el-menu-item>
|
||||||
</el-menu-item>
|
<el-menu-item v-if="hasPerm('referral.manage')" index="/referral"><el-icon><Connection /></el-icon><template #title>服务转介</template></el-menu-item>
|
||||||
<el-menu-item v-if="hasPerm('organization.manage')" index="/organizations">
|
</el-menu-item-group>
|
||||||
<el-icon><OfficeBuilding /></el-icon><template #title>机构管理</template>
|
<el-menu-item-group v-if="showGroup(['activity.review_registration'])" title="活动运营">
|
||||||
</el-menu-item>
|
<el-menu-item index="/activities/interests"><el-icon><Calendar /></el-icon><template #title>活动与互选</template></el-menu-item>
|
||||||
<el-menu-item v-if="hasPerm('permission.manage')" index="/operators">
|
</el-menu-item-group>
|
||||||
<el-icon><Setting /></el-icon><template #title>人员管理</template>
|
<el-menu-item-group v-if="showGroup(['banner.view','content.view'])" title="内容运营">
|
||||||
</el-menu-item>
|
<el-menu-item v-if="hasPerm('banner.view')" index="/banner"><el-icon><Promotion /></el-icon><template #title>首页 Banner</template></el-menu-item>
|
||||||
<el-menu-item v-if="hasPerm('activity.review_registration')" index="/activities/interests">
|
<el-menu-item v-if="hasPerm('content.view')" index="/content"><el-icon><Reading /></el-icon><template #title>课堂与反诈内容</template></el-menu-item>
|
||||||
<el-icon><Calendar /></el-icon><template #title>活动互选</template>
|
</el-menu-item-group>
|
||||||
</el-menu-item>
|
<el-menu-item-group v-if="showGroup(['service_location.view','service_appointment.view'])" title="服务阵地">
|
||||||
<el-menu-item v-if="hasPerm('risk.report.view')" index="/risk/reports">
|
<el-menu-item v-if="hasPerm('service_location.view')" index="/service-station"><el-icon><Location /></el-icon><template #title>善缘服务站</template></el-menu-item>
|
||||||
<el-icon><Warning /></el-icon><template #title>举报中心</template>
|
<el-menu-item v-if="hasPerm('service_appointment.view')" index="/service-appointment"><el-icon><Memo /></el-icon><template #title>服务预约</template></el-menu-item>
|
||||||
</el-menu-item>
|
</el-menu-item-group>
|
||||||
<el-menu-item v-if="hasPerm('risk.report.process')" index="/appeals">
|
<el-menu-item-group v-if="showGroup(['risk.report.view','risk.report.process'])" title="安全治理">
|
||||||
<el-icon><ChatDotRound /></el-icon><template #title>申诉管理</template>
|
<el-menu-item v-if="hasPerm('risk.report.view')" index="/risk/reports"><el-icon><Warning /></el-icon><template #title>举报中心</template></el-menu-item>
|
||||||
</el-menu-item>
|
<el-menu-item v-if="hasPerm('risk.report.process')" index="/appeals"><el-icon><CircleCheck /></el-icon><template #title>申诉管理</template></el-menu-item>
|
||||||
<el-menu-item v-if="hasPerm('analytics.view')" index="/analytics">
|
</el-menu-item-group>
|
||||||
<el-icon><TrendCharts /></el-icon><template #title>数据统计</template>
|
<el-menu-item-group v-if="showGroup(['organization.manage','permission.manage','user.view'])" title="组织与系统">
|
||||||
</el-menu-item>
|
<el-menu-item v-if="hasPerm('organization.manage')" index="/organizations"><el-icon><OfficeBuilding /></el-icon><template #title>组织管理</template></el-menu-item>
|
||||||
<el-menu-item v-if="hasPerm('user.view')" index="/audit">
|
<el-menu-item v-if="hasPerm('permission.manage')" index="/operators"><el-icon><Setting /></el-icon><template #title>工作人员</template></el-menu-item>
|
||||||
<el-icon><Document /></el-icon><template #title>审计日志</template>
|
<el-menu-item v-if="hasPerm('user.view')" index="/audit"><el-icon><Document /></el-icon><template #title>操作日志</template></el-menu-item>
|
||||||
</el-menu-item>
|
</el-menu-item-group>
|
||||||
<!-- V1.1 新增菜单 -->
|
<el-menu-item v-if="hasPerm('analytics.view')" index="/analytics"><el-icon><TrendCharts /></el-icon><template #title>数据统计</template></el-menu-item>
|
||||||
<el-menu-item v-if="hasPerm('banner.view')" index="/banner">
|
</el-menu>
|
||||||
<el-icon><Promotion /></el-icon><template #title>Banner管理</template>
|
</el-scrollbar>
|
||||||
</el-menu-item>
|
<button class="collapse-control" type="button" @click="isCollapse=!isCollapse"><el-icon><Fold v-if="!isCollapse"/><Expand v-else/></el-icon><span v-if="!isCollapse">收起导航</span></button>
|
||||||
<el-menu-item v-if="hasPerm('content.view')" index="/content">
|
|
||||||
<el-icon><FolderOpened /></el-icon><template #title>内容管理</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item v-if="hasPerm('service_location.view')" index="/service-station">
|
|
||||||
<el-icon><Location /></el-icon><template #title>善缘服务站</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item v-if="hasPerm('service_appointment.view')" index="/service-appointment">
|
|
||||||
<el-icon><Memo /></el-icon><template #title>服务预约</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item v-if="hasPerm('service_program.manage')" index="/service-program">
|
|
||||||
<el-icon><Service /></el-icon><template #title>专项服务项目</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item v-if="hasPerm('counseling.service.view')" index="/counseling">
|
|
||||||
<el-icon><ChatDotRound /></el-icon><template #title>婚恋咨询</template>
|
|
||||||
</el-menu-item>
|
|
||||||
<el-menu-item v-if="hasPerm('referral.manage')" index="/referral">
|
|
||||||
<el-icon><Bell /></el-icon><template #title>转介管理</template>
|
|
||||||
</el-menu-item>
|
|
||||||
</el-menu>
|
|
||||||
</el-aside>
|
</el-aside>
|
||||||
|
<el-container class="workspace">
|
||||||
<el-container>
|
|
||||||
<el-header class="header">
|
<el-header class="header">
|
||||||
<div class="header-left">
|
<div><div class="context-kicker">善缘丹阳 · 公益服务</div><el-breadcrumb separator="/"><el-breadcrumb-item :to="{path:'/dashboard'}">工作台</el-breadcrumb-item><el-breadcrumb-item v-if="currentTitle!=='工作台'">{{currentTitle}}</el-breadcrumb-item></el-breadcrumb></div>
|
||||||
<el-icon class="collapse-btn" @click="isCollapse = !isCollapse">
|
<div class="header-right"><div class="service-state"><i></i>服务正常</div><el-dropdown><span class="user-info"><el-avatar :size="36">{{(operator?.name||'工')[0]}}</el-avatar><span><strong>{{operator?.name||'工作人员'}}</strong><small>运营人员</small></span><el-icon><ArrowDown/></el-icon></span><template #dropdown><el-dropdown-menu><el-dropdown-item @click="handleLogout">退出登录</el-dropdown-item></el-dropdown-menu></template></el-dropdown></div>
|
||||||
<Fold v-if="!isCollapse" /><Expand v-else />
|
|
||||||
</el-icon>
|
|
||||||
<el-breadcrumb separator="/">
|
|
||||||
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
|
|
||||||
<el-breadcrumb-item>{{ currentTitle }}</el-breadcrumb-item>
|
|
||||||
</el-breadcrumb>
|
|
||||||
</div>
|
|
||||||
<div class="header-right">
|
|
||||||
<el-dropdown>
|
|
||||||
<span class="user-info">
|
|
||||||
<el-avatar :size="32" style="background:#E96B6B">{{ (operator?.name || '管')[0] }}</el-avatar>
|
|
||||||
<span class="user-name">{{ operator?.name || '工作人员' }}</span>
|
|
||||||
</span>
|
|
||||||
<template #dropdown>
|
|
||||||
<el-dropdown-menu>
|
|
||||||
<el-dropdown-item @click="handleLogout">退出登录</el-dropdown-item>
|
|
||||||
</el-dropdown-menu>
|
|
||||||
</template>
|
|
||||||
</el-dropdown>
|
|
||||||
</div>
|
|
||||||
</el-header>
|
</el-header>
|
||||||
<el-main class="main-content">
|
<el-main class="main-content"><router-view v-slot="{Component}"><keep-alive><component :is="Component"/></keep-alive></router-view></el-main>
|
||||||
<router-view v-slot="{ Component }">
|
|
||||||
<keep-alive><component :is="Component" /></keep-alive>
|
|
||||||
</router-view>
|
|
||||||
</el-main>
|
|
||||||
</el-container>
|
</el-container>
|
||||||
</el-container>
|
</el-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import {ref,computed} from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import {useRoute,useRouter} from 'vue-router'
|
||||||
import { Fold, Expand, DataBoard, DocumentChecked, User, OfficeBuilding, Setting, Calendar, Warning, ChatDotRound, TrendCharts, Document, Service, Location, Memo, Promotion, FolderOpened, Bell } from '@element-plus/icons-vue'
|
import {DataBoard,DocumentChecked,User,OfficeBuilding,Setting,Calendar,Warning,ChatDotRound,TrendCharts,Document,Service,Location,Memo,Promotion,Reading,Connection,CircleCheck,Fold,Expand,ArrowDown} from '@element-plus/icons-vue'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import {useAuthStore} from '@/stores/auth'
|
||||||
|
const route=useRoute();const router=useRouter();const authStore=useAuthStore();const isCollapse=ref(false)
|
||||||
const route = useRoute()
|
const activeMenu=computed(()=>route.path);const currentTitle=computed(()=>(route.meta?.title as string)||'页面');const operator=computed(()=>authStore.operator)
|
||||||
const router = useRouter()
|
function hasPerm(permission:string){return (authStore.permissions||[]).includes(permission)}
|
||||||
const authStore = useAuthStore()
|
function showGroup(perms:string[]){return perms.some(hasPerm)}
|
||||||
const isCollapse = ref(false)
|
function handleLogout(){authStore.logout();router.push('/login')}
|
||||||
|
|
||||||
const activeMenu = computed(() => route.path)
|
|
||||||
const currentTitle = computed(() => (route.meta?.title as string) || '页面')
|
|
||||||
const operator = computed(() => authStore.operator)
|
|
||||||
|
|
||||||
function hasPerm(permission: string): boolean {
|
|
||||||
return (authStore.permissions || []).includes(permission)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleLogout() {
|
|
||||||
authStore.logout()
|
|
||||||
router.push('/login')
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.main-layout { height: 100vh; width: 100%; }
|
.main-layout{height:100vh;width:100%}.sidebar{display:flex;flex-direction:column;background:#FFFCFA;border-right:1px solid var(--color-border-light);transition:width .2s ease;overflow:hidden}.brand{height:92px;padding:20px 18px;display:flex;align-items:center;gap:12px;border-bottom:1px solid var(--color-border-light)}.brand.compact{justify-content:center;padding:18px 10px}.brand-symbol{width:42px;height:42px;flex:none;display:grid;place-items:center;border-radius:14px;background:linear-gradient(145deg,var(--color-primary),#EF8A7B);color:#fff;font-size:19px;font-weight:700;box-shadow:0 8px 18px rgba(233,107,107,.2)}.brand-copy{display:flex;flex-direction:column;white-space:nowrap}.brand-copy strong{font-size:17px;letter-spacing:1px}.brand-copy span{margin-top:3px;font-size:11px;color:var(--color-text-tertiary)}.menu-scroll{flex:1}.sidebar-menu{border:0;background:transparent;padding:12px 10px}.sidebar-menu :deep(.el-menu-item){height:44px;margin:3px 0;border-radius:10px;color:var(--color-text-secondary)}.sidebar-menu :deep(.el-menu-item:hover){background:var(--color-primary-lighter);color:var(--color-primary)}.sidebar-menu :deep(.el-menu-item.is-active){background:var(--color-primary-light);color:var(--color-primary);font-weight:600}.sidebar-menu :deep(.el-menu-item-group__title){padding:18px 12px 5px!important;color:#AAA09A;font-size:11px;letter-spacing:1px}.collapse-control{height:52px;margin:10px;border:0;border-top:1px solid var(--color-border-light);background:transparent;color:var(--color-text-tertiary);display:flex;align-items:center;justify-content:center;gap:8px;cursor:pointer}.workspace{min-width:0}.header{height:var(--header-height);padding:0 24px;background:rgba(255,255,255,.94);border-bottom:1px solid var(--color-border-light);display:flex;align-items:center;justify-content:space-between}.context-kicker{font-size:10px;color:var(--color-primary);margin-bottom:4px;letter-spacing:1px}.header-right,.user-info{display:flex;align-items:center}.header-right{gap:24px}.service-state{font-size:12px;color:var(--color-success)}.service-state i{display:inline-block;width:7px;height:7px;margin-right:6px;border-radius:50%;background:var(--color-success)}.user-info{gap:9px;cursor:pointer}.user-info .el-avatar{background:var(--color-primary);color:#fff}.user-info span{display:flex;flex-direction:column}.user-info strong{font-size:13px}.user-info small{font-size:11px;color:var(--color-text-tertiary);margin-top:2px}.main-content{background:var(--color-bg-page);padding:0;overflow:auto}
|
||||||
.sidebar { background-color: #304156; transition: width 0.3s; overflow: hidden; }
|
@media(max-width:900px){.sidebar{width:72px!important}.brand-copy,.collapse-control span,.sidebar-menu :deep(.el-menu-item-group__title){display:none}.brand{justify-content:center}.header{padding:0 16px}.service-state{display:none}}
|
||||||
.logo { height: 60px; display: flex; align-items: center; justify-content: center; color: #fff; font-size: 16px; font-weight: bold; border-bottom: 1px solid #3a4a5e; }
|
|
||||||
.sidebar-menu { border-right: none; }
|
|
||||||
.header { background: #fff; border-bottom: 1px solid #e4e7ed; display: flex; align-items: center; justify-content: space-between; padding: 0 20px; }
|
|
||||||
.header-left { display: flex; align-items: center; gap: 16px; }
|
|
||||||
.collapse-btn { cursor: pointer; font-size: 20px; color: #666; }
|
|
||||||
.collapse-btn:hover { color: #E96B6B; }
|
|
||||||
.header-right { display: flex; align-items: center; gap: 16px; }
|
|
||||||
.user-info { display: flex; align-items: center; gap: 8px; cursor: pointer; }
|
|
||||||
.user-name { font-size: 14px; color: #333; }
|
|
||||||
.main-content { background: #f0f2f5; padding: 20px; overflow: auto; }
|
|
||||||
</style>
|
</style>
|
||||||
@@ -10,10 +10,22 @@ interface OperatorInfo {
|
|||||||
organizationId: string | null
|
organizationId: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const previewRolePermissions: Record<string, string[]> = {
|
||||||
|
reviewer: ['review.view', 'user.view'],
|
||||||
|
counselor: ['counseling.service.view', 'referral.manage'],
|
||||||
|
station: ['service_location.view', 'service_appointment.view'],
|
||||||
|
operator: ['review.view', 'user.view', 'organization.manage', 'permission.manage', 'activity.review_registration', 'risk.report.view', 'risk.report.process', 'analytics.view', 'banner.view', 'content.view', 'service_location.view', 'service_appointment.view', 'service_program.manage', 'counseling.service.view', 'referral.manage'],
|
||||||
|
}
|
||||||
|
|
||||||
export const useAuthStore = defineStore('auth', () => {
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
const token = ref<string>(localStorage.getItem('accessToken') || '')
|
const previewParams = import.meta.env.DEV ? new URLSearchParams(window.location.search) : null
|
||||||
|
const previewRole = previewParams?.get('preview') || ''
|
||||||
|
const previewOperator: OperatorInfo | null = previewRolePermissions[previewRole]
|
||||||
|
? { operatorId: 'ui-preview', name: '界面验收员', username: 'preview', permissions: previewRolePermissions[previewRole], organizationId: null }
|
||||||
|
: null
|
||||||
|
const token = ref<string>(previewOperator ? 'ui-preview-only' : (localStorage.getItem('accessToken') || ''))
|
||||||
const operator = ref<OperatorInfo | null>(
|
const operator = ref<OperatorInfo | null>(
|
||||||
JSON.parse(localStorage.getItem('operator') || 'null')
|
previewOperator || JSON.parse(localStorage.getItem('operator') || 'null')
|
||||||
)
|
)
|
||||||
|
|
||||||
const isLoggedIn = computed(() => !!token.value)
|
const isLoggedIn = computed(() => !!token.value)
|
||||||
|
|||||||
@@ -1,80 +1,67 @@
|
|||||||
:root {
|
:root {
|
||||||
/* Brand Colors */
|
|
||||||
--color-primary: #E96B6B;
|
--color-primary: #E96B6B;
|
||||||
|
--color-primary-hover: #D95F5F;
|
||||||
--color-primary-light: #FFF3F2;
|
--color-primary-light: #FFF3F2;
|
||||||
|
--color-primary-lighter: #FFF8F7;
|
||||||
--color-success: #2E9B68;
|
--color-success: #2E9B68;
|
||||||
--color-warning: #E59A32;
|
--color-warning: #E59A32;
|
||||||
--color-danger: #DC3545;
|
--color-danger: #D94A4A;
|
||||||
--color-info: #17A2B8;
|
--color-info: #4A7CD4;
|
||||||
|
--color-text-primary: #222222;
|
||||||
/* Text Colors */
|
--color-text-secondary: #555555;
|
||||||
--color-text-primary: #1A1A1A;
|
--color-text-tertiary: #888888;
|
||||||
--color-text-secondary: #666666;
|
|
||||||
--color-text-tertiary: #999999;
|
|
||||||
|
|
||||||
/* Background */
|
|
||||||
--color-bg-primary: #FFFFFF;
|
--color-bg-primary: #FFFFFF;
|
||||||
--color-bg-secondary: #F5F7FA;
|
--color-bg-secondary: #FFF8F7;
|
||||||
--color-bg-page: #F0F2F5;
|
--color-bg-page: #FAFAF8;
|
||||||
|
--color-border: #E8E5E2;
|
||||||
/* Border */
|
--color-border-light: #F0EDE9;
|
||||||
--color-border: #E4E7ED;
|
|
||||||
--color-border-light: #EBEEF5;
|
|
||||||
|
|
||||||
/* Layout */
|
|
||||||
--sidebar-width: 220px;
|
--sidebar-width: 220px;
|
||||||
--header-height: 60px;
|
--header-height: 64px;
|
||||||
--border-radius: 6px;
|
--border-radius-sm: 8px;
|
||||||
}
|
--border-radius-md: 12px;
|
||||||
|
--border-radius-lg: 16px;
|
||||||
* {
|
--shadow-card: 0 8px 24px rgba(75, 55, 46, 0.06);
|
||||||
margin: 0;
|
--el-color-primary: var(--color-primary);
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
html, body, #app {
|
html, body, #app {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB',
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||||
'Microsoft YaHei', sans-serif;
|
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--color-text-primary);
|
color: var(--color-text-primary);
|
||||||
background-color: var(--color-bg-page);
|
background-color: var(--color-bg-page);
|
||||||
}
|
}
|
||||||
|
body { min-width: 1100px; }
|
||||||
/* Override Element Plus primary color */
|
|
||||||
.el-button--primary {
|
.el-button--primary {
|
||||||
--el-button-bg-color: #E96B6B;
|
--el-button-bg-color: var(--color-primary);
|
||||||
--el-button-border-color: #E96B6B;
|
--el-button-border-color: var(--color-primary);
|
||||||
--el-button-hover-bg-color: #d45a5a;
|
--el-button-hover-bg-color: var(--color-primary-hover);
|
||||||
--el-button-hover-border-color: #d45a5a;
|
--el-button-hover-border-color: var(--color-primary-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-radio-group .el-radio-button__inner:checked {
|
.el-radio-group .el-radio-button__inner:checked {
|
||||||
background-color: #E96B6B;
|
background-color: var(--color-primary);
|
||||||
border-color: #E96B6B;
|
border-color: var(--color-primary);
|
||||||
}
|
}
|
||||||
|
.el-card { --el-card-border-radius: var(--border-radius-md); --el-card-border-color: var(--color-border-light); }
|
||||||
/* Common utility */
|
.el-table { --el-table-header-bg-color: #FCFAF8; --el-table-border-color: var(--color-border-light); }
|
||||||
.page-container {
|
.el-tag { border-radius: 999px; }
|
||||||
padding: 20px;
|
.page-container { padding: 24px; }
|
||||||
}
|
.page-shell { max-width: 1440px; margin: 0 auto; }
|
||||||
|
.page-heading { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; margin-bottom: 20px; }
|
||||||
|
.page-heading h1 { font-size: 24px; line-height: 1.35; font-weight: 650; }
|
||||||
|
.page-heading p { margin-top: 6px; color: var(--color-text-tertiary); }
|
||||||
|
.surface-card { background: var(--color-bg-primary); border: 1px solid var(--color-border-light); border-radius: var(--border-radius-md); box-shadow: var(--shadow-card); }
|
||||||
.text-primary { color: var(--color-text-primary); }
|
.text-primary { color: var(--color-text-primary); }
|
||||||
.text-secondary { color: var(--color-text-secondary); }
|
.text-secondary { color: var(--color-text-secondary); }
|
||||||
.text-tertiary { color: var(--color-text-tertiary); }
|
.text-tertiary { color: var(--color-text-tertiary); }
|
||||||
|
|
||||||
.mt-sm { margin-top: 8px; }
|
.mt-sm { margin-top: 8px; }
|
||||||
.mt-md { margin-top: 16px; }
|
.mt-md { margin-top: 16px; }
|
||||||
.mt-lg { margin-top: 24px; }
|
.mt-lg { margin-top: 24px; }
|
||||||
|
|
||||||
.mb-sm { margin-bottom: 8px; }
|
.mb-sm { margin-bottom: 8px; }
|
||||||
.mb-md { margin-bottom: 16px; }
|
.mb-md { margin-bottom: 16px; }
|
||||||
.mb-lg { margin-bottom: 24px; }
|
.mb-lg { margin-bottom: 24px; }
|
||||||
|
.flex-center { display: flex; align-items: center; justify-content: center; }
|
||||||
.flex-center {
|
@media (max-width: 1200px) { body { min-width: 0; } }
|
||||||
display: flex;
|
@media (max-width: 900px) { .page-container { padding: 16px; } .page-heading { align-items: flex-start; flex-direction: column; } }
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
@@ -1,95 +1,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="page-container">
|
<div class="page-container page-shell dashboard" v-loading="loading">
|
||||||
<el-row :gutter="16">
|
<section class="welcome">
|
||||||
<el-col :span="6" v-for="card in statCards" :key="card.key">
|
<div><div class="eyebrow">{{ profile.badge }}</div><h1>{{ greeting }},{{ operatorName }}</h1><p>{{ profile.subtitle }}</p></div>
|
||||||
<el-card shadow="hover" class="stat-card">
|
<div class="date-card"><span>{{ today }}</span><strong>今日服务</strong></div>
|
||||||
<div class="stat-value" :style="{ color: card.color }">{{ card.value }}</div>
|
</section>
|
||||||
<div class="stat-label">{{ card.label }}</div>
|
<section class="metrics">
|
||||||
</el-card>
|
<article v-for="item in metrics" :key="item.label" class="metric-card surface-card"><div class="metric-top"><span>{{item.label}}</span><span class="metric-icon" :class="item.tone">{{item.icon}}</span></div><strong>{{item.value}}</strong><small>{{item.hint}}</small></article>
|
||||||
</el-col>
|
</section>
|
||||||
</el-row>
|
<section class="dashboard-grid">
|
||||||
|
<article class="surface-card focus-card"><header><div><span class="section-kicker">今日优先</span><h2>{{profile.focusTitle}}</h2></div><el-button type="primary" plain @click="go(profile.primaryPath)">{{profile.primaryAction}}</el-button></header><div class="task-list"><button v-for="task in profile.tasks" :key="task.title" type="button" class="task-row" @click="go(task.path)"><span class="task-dot" :class="task.tone"></span><span class="task-copy"><strong>{{task.title}}</strong><small>{{task.desc}}</small></span><span class="task-count">{{task.count}}</span><el-icon><ArrowRight/></el-icon></button></div></article>
|
||||||
<el-row :gutter="16" style="margin-top:16px">
|
<aside class="surface-card quick-card"><header><span class="section-kicker">常用操作</span><h2>快速开始</h2></header><button v-for="action in profile.actions" :key="action.label" type="button" class="quick-action" @click="go(action.path)"><span class="quick-icon">{{action.icon}}</span><span><strong>{{action.label}}</strong><small>{{action.desc}}</small></span><el-icon><ArrowRight/></el-icon></button><div class="privacy-note"><el-icon><Lock/></el-icon><span><strong>数据访问受保护</strong><small>仅可查看职责范围内的服务信息,敏感操作全程留痕。</small></span></div></aside>
|
||||||
<el-col :span="12">
|
</section>
|
||||||
<el-card shadow="never">
|
<section class="surface-card service-overview"><header><div><span class="section-kicker">公益服务概况</span><h2>本周服务进展</h2></div><span class="preview-label" v-if="preview">界面预览数据</span></header><div class="progress-grid"><div v-for="item in progress" :key="item.label" class="progress-item"><div><span>{{item.label}}</span><strong>{{item.value}}</strong></div><el-progress :percentage="item.percent" :stroke-width="8" :show-text="false" :color="item.color"/><small>{{item.note}}</small></div></div></section>
|
||||||
<template #header><span>用户统计</span></template>
|
|
||||||
<el-descriptions :column="2" border size="small">
|
|
||||||
<el-descriptions-item label="累计注册">{{ stats.totalUsers }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="已实名">{{ stats.verifiedUsers }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="活跃用户(7天)">{{ stats.activeUsers }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="待审核举报">{{ stats.pendingReports }}</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="12">
|
|
||||||
<el-card shadow="never">
|
|
||||||
<template #header><span>婚恋服务</span></template>
|
|
||||||
<el-descriptions :column="2" border size="small">
|
|
||||||
<el-descriptions-item label="总喜欢数">{{ stats.totalLikes }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="双向Match">{{ stats.totalMatches }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="正在了解">{{ stats.gettingToKnow }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="成功牵手">{{ stats.successPairs }}</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
|
|
||||||
<el-row :gutter="16" style="margin-top:16px">
|
|
||||||
<el-col :span="12">
|
|
||||||
<el-card shadow="never">
|
|
||||||
<template #header><span>安全风险</span></template>
|
|
||||||
<el-descriptions :column="2" border size="small">
|
|
||||||
<el-descriptions-item label="封禁账号">{{ stats.bannedUsers }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="处理中举报">{{ stats.processingReports }}</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
<el-col :span="12">
|
|
||||||
<el-card shadow="never">
|
|
||||||
<template #header><span>公益红娘</span></template>
|
|
||||||
<el-descriptions :column="2" border size="small">
|
|
||||||
<el-descriptions-item label="红娘服务申请">{{ stats.matchmakerServices }}</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted } from 'vue'
|
import {computed,onMounted,ref} from 'vue';import {useRouter} from 'vue-router';import {ArrowRight,Lock} from '@element-plus/icons-vue';import {get} from '@/api/client';import {useAuthStore} from '@/stores/auth'
|
||||||
import { get } from '@/api/client'
|
type Task={title:string;desc:string;count:number;tone:string;path:string};type Action={label:string;desc:string;icon:string;path:string};type Profile={badge:string;subtitle:string;focusTitle:string;primaryAction:string;primaryPath:string;tasks:Task[];actions:Action[]}
|
||||||
|
const router=useRouter();const auth=useAuthStore();const loading=ref(false);const preview=ref(false);const stats=ref<Record<string,number>>({})
|
||||||
const stats = ref<any>({})
|
const operatorName=computed(()=>auth.operator?.name||'工作人员');const today=new Intl.DateTimeFormat('zh-CN',{month:'long',day:'numeric',weekday:'long'}).format(new Date());const greeting=computed(()=>new Date().getHours()<12?'上午好':new Date().getHours()<18?'下午好':'晚上好')
|
||||||
const loading = ref(false)
|
const p=computed(()=>auth.permissions||[]);const has=(code:string)=>p.value.includes(code)
|
||||||
|
const profile=computed<Profile>(()=>{if(has('counseling.service.view'))return{badge:'婚恋咨询工作台',subtitle:'只处理明确分配给你的咨询服务,尊重每一位来访者的隐私。',focusTitle:'我的待办咨询',primaryAction:'进入咨询服务',primaryPath:'/counseling',tasks:[{title:'待首次联系',desc:'请在服务时限内完成首次沟通',count:stats.value.pendingCounseling||3,tone:'rose',path:'/counseling'},{title:'今日预约',desc:'确认时间与服务方式',count:stats.value.todayAppointments||2,tone:'amber',path:'/counseling'},{title:'待补充记录',desc:'仅记录必要的服务事实',count:1,tone:'blue',path:'/counseling'}],actions:[{label:'查看我的咨询',desc:'仅限已分配服务',icon:'咨',path:'/counseling'},{label:'转介协作',desc:'需要其他专业支持',icon:'转',path:'/referral'}]};if(has('service_appointment.view'))return{badge:'服务站工作台',subtitle:'聚焦今天的现场预约与指定服务,不浏览全平台用户库。',focusTitle:'今日到站服务',primaryAction:'管理预约',primaryPath:'/service-appointment',tasks:[{title:'待确认预约',desc:'确认服务时间与到站方式',count:stats.value.pendingAppointments||6,tone:'rose',path:'/service-appointment'},{title:'即将到站',desc:'提前准备现场服务资料',count:4,tone:'green',path:'/service-appointment'},{title:'待完成回访',desc:'记录服务结果与后续安排',count:2,tone:'blue',path:'/service-appointment'}],actions:[{label:'预约管理',desc:'查看今日预约',icon:'约',path:'/service-appointment'},{label:'服务站信息',desc:'维护开放时间与项目',icon:'站',path:'/service-station'}]};if(has('review.view'))return{badge:'资料审核工作台',subtitle:'核验真实信息,帮助平台建立安全、可信的相遇环境。',focusTitle:'今日审核队列',primaryAction:'开始审核',primaryPath:'/review',tasks:[{title:'待初审资料',desc:'按提交时间依次处理',count:stats.value.pendingReviews||18,tone:'rose',path:'/review'},{title:'待复核资料',desc:'关注证件与资料一致性',count:5,tone:'amber',path:'/review'},{title:'待补充材料',desc:'清晰说明需要用户补充的内容',count:7,tone:'blue',path:'/review'}],actions:[{label:'进入资料审核',desc:'处理待审资料',icon:'审',path:'/review'},{label:'用户查询',desc:'按权限查询用户',icon:'查',path:'/users'}]};return{badge:'公益服务工作台',subtitle:'从今天最重要的待办开始,让每一项服务都有回应。',focusTitle:'今日待办',primaryAction:'查看全部',primaryPath:'/dashboard',tasks:[{title:'待处理服务',desc:'请按服务时限及时响应',count:stats.value.pendingTasks||12,tone:'rose',path:'/dashboard'},{title:'待跟进事项',desc:'查看最近更新与反馈',count:8,tone:'amber',path:'/dashboard'},{title:'安全提醒',desc:'关注需要优先处理的风险',count:2,tone:'blue',path:'/risk/reports'}],actions:[{label:'用户服务',desc:'进入当前职责模块',icon:'服',path:'/dashboard'},{label:'数据概览',desc:'查看授权范围统计',icon:'数',path:'/analytics'}]}})
|
||||||
const statCards = [
|
const metrics=computed(()=>[{label:'今日待办',value:profile.value.tasks.reduce((n,t)=>n+t.count,0),hint:'按优先级及时处理',icon:'待',tone:'rose'},{label:'本周已完成',value:stats.value.weekCompleted||46,hint:'比上周多 8 项',icon:'成',tone:'green'},{label:'服务中',value:stats.value.activeServices||23,hint:'均在正常服务时限内',icon:'服',tone:'blue'},{label:'需关注',value:stats.value.attention||2,hint:'包含临期与风险事项',icon:'醒',tone:'amber'}])
|
||||||
{ key: 'totalUsers', label: '累计注册', value: 0, color: '#409EFF' },
|
const progress=[{label:'资料审核及时率',value:'96%',percent:96,note:'目标 95%',color:'#2E9B68'},{label:'预约按时完成率',value:'91%',percent:91,note:'本周 31 次服务',color:'#4A7CD4'},{label:'服务回访完成率',value:'84%',percent:84,note:'还有 5 项待回访',color:'#E59A32'}]
|
||||||
{ key: 'verifiedUsers', label: '已实名', value: 0, color: '#67C23A' },
|
function go(path:string){if(path)router.push(path)}
|
||||||
{ key: 'activeUsers', label: '近7天活跃', value: 0, color: '#E59A32' },
|
async function load(){if(auth.token==='ui-preview-only'){preview.value=true;return}loading.value=true;try{stats.value=(await get('/operator/analytics/dashboard')) as unknown as Record<string,number>}catch{preview.value=true}finally{loading.value=false}}
|
||||||
{ key: 'successPairs', label: '成功牵手', value: 0, color: '#E96B6B' }
|
onMounted(load)
|
||||||
]
|
|
||||||
|
|
||||||
async function loadStats() {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const token = localStorage.getItem('accessToken')
|
|
||||||
const res: any = await get('/operator/analytics/dashboard', {}, { Authorization: `Bearer ${token}` })
|
|
||||||
stats.value = res || {}
|
|
||||||
statCards[0].value = res.totalUsers || 0
|
|
||||||
statCards[1].value = res.verifiedUsers || 0
|
|
||||||
statCards[2].value = res.activeUsers || 0
|
|
||||||
statCards[3].value = res.successPairs || 0
|
|
||||||
} catch (e) { console.error(e) }
|
|
||||||
finally { loading.value = false }
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(loadStats)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.page-container { padding: 24px; }
|
.dashboard{padding-top:28px;padding-bottom:40px}.welcome{min-height:150px;padding:30px 34px;display:flex;align-items:center;justify-content:space-between;border-radius:18px;background:linear-gradient(120deg,#FFF1EE,#FFF9F2 56%,#F4F8F6);border:1px solid #F6E4DE;position:relative;overflow:hidden}.welcome:after{content:'缘';position:absolute;right:130px;top:-54px;font-size:210px;font-weight:700;color:rgba(233,107,107,.07)}.eyebrow,.section-kicker{font-size:12px;color:var(--color-primary);font-weight:650;letter-spacing:1px}.welcome h1{margin-top:9px;font-size:28px}.welcome p{margin-top:10px;color:var(--color-text-secondary)}.date-card{z-index:1;padding:14px 18px;border-radius:13px;background:rgba(255,255,255,.75);border:1px solid rgba(255,255,255,.9);display:flex;flex-direction:column;text-align:right}.date-card span{font-size:12px;color:var(--color-text-tertiary)}.date-card strong{margin-top:4px;font-size:16px}.metrics{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-top:18px}.metric-card{padding:20px}.metric-top{display:flex;justify-content:space-between;color:var(--color-text-secondary)}.metric-icon{width:34px;height:34px;display:grid;place-items:center;border-radius:11px;font-weight:650}.metric-icon.rose{background:var(--color-primary-light);color:var(--color-primary)}.metric-icon.green{background:#ECF8F2;color:var(--color-success)}.metric-icon.blue{background:#EEF4FF;color:var(--color-info)}.metric-icon.amber{background:#FFF7E9;color:var(--color-warning)}.metric-card>strong{display:block;margin-top:8px;font-size:30px}.metric-card>small{display:block;margin-top:6px;color:var(--color-text-tertiary)}.dashboard-grid{display:grid;grid-template-columns:minmax(0,1.7fr) minmax(300px,.8fr);gap:18px;margin-top:18px}.focus-card,.quick-card,.service-overview{padding:24px}.focus-card header,.quick-card header,.service-overview header{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}.focus-card h2,.quick-card h2,.service-overview h2{margin-top:5px;font-size:18px}.task-row,.quick-action{width:100%;border:0;background:transparent;display:flex;align-items:center;text-align:left;cursor:pointer}.task-row{min-height:76px;border-top:1px solid var(--color-border-light);gap:14px}.task-row:hover,.quick-action:hover{background:var(--color-primary-lighter)}.task-dot{width:9px;height:9px;border-radius:50%;background:var(--color-primary)}.task-dot.green{background:var(--color-success)}.task-dot.blue{background:var(--color-info)}.task-dot.amber{background:var(--color-warning)}.task-copy{flex:1;display:flex;flex-direction:column}.task-copy small,.quick-action small{margin-top:5px;color:var(--color-text-tertiary)}.task-count{font-size:22px;font-weight:650;color:var(--color-primary)}.quick-action{padding:15px 8px;gap:12px;border-top:1px solid var(--color-border-light)}.quick-icon{width:38px;height:38px;display:grid;place-items:center;border-radius:12px;background:var(--color-primary-light);color:var(--color-primary);font-weight:650}.quick-action>span:nth-child(2){flex:1;display:flex;flex-direction:column}.privacy-note{margin-top:18px;padding:16px;display:flex;gap:10px;background:#F4F8F6;color:var(--color-success);border-radius:12px}.privacy-note span{display:flex;flex-direction:column}.privacy-note small{margin-top:4px;color:var(--color-text-secondary);line-height:1.5}.service-overview{margin-top:18px}.preview-label{font-size:11px;color:var(--color-warning);background:#FFF7E9;padding:5px 10px;border-radius:99px}.progress-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:30px}.progress-item>div{display:flex;justify-content:space-between;margin-bottom:12px}.progress-item strong{font-size:18px}.progress-item small{display:block;margin-top:8px;color:var(--color-text-tertiary)}
|
||||||
.stat-card { text-align: center; }
|
@media(max-width:1100px){.metrics{grid-template-columns:repeat(2,1fr)}.dashboard-grid{grid-template-columns:1fr}.progress-grid{grid-template-columns:1fr}.welcome:after{right:20px}}
|
||||||
.stat-value { font-size: 36px; font-weight: bold; }
|
@media(max-width:680px){.metrics{grid-template-columns:1fr}.welcome{padding:24px}.date-card{display:none}}
|
||||||
.stat-label { font-size: 14px; color: #666; margin-top: 8px; }
|
|
||||||
</style>
|
</style>
|
||||||
|
After Width: | Height: | Size: 134 KiB |
|
After Width: | Height: | Size: 148 KiB |
|
After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 93 KiB |
|
After Width: | Height: | Size: 89 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>善缘丹阳小程序首页预览</title><style>
|
||||||
|
*{box-sizing:border-box}body{margin:0;background:#ece9e6;font:14px -apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;color:#222}.phone{width:min(390px,100vw);min-height:844px;margin:28px auto;background:#FAFAF8;box-shadow:0 20px 60px #4b372f33;overflow:hidden}.top{height:76px;padding:14px 16px 10px;background:#fff;display:flex;align-items:center;border-bottom:1px solid #f0ede9;position:relative}.brand{flex:1}.brand strong{display:block;font-size:19px;color:#E96B6B}.brand small{color:#888}.pill{font-size:11px;color:#2E9B68;background:#ECF8F2;padding:5px 8px;border-radius:99px}.bell{margin-left:12px;font-size:12px}.preview{text-align:center;padding:4px;background:#FFF7E9;color:#B77B19;font-size:10px}.status{margin:8px 16px 0;padding:10px 12px;background:#fff;border:1px solid #e8e5e2;border-radius:12px;display:flex;justify-content:space-between;font-size:12px}.ok{color:#2E9B68}.banner{height:146px;margin:8px 16px 0;border-radius:16px;background:linear-gradient(135deg,#FFF3F2,#FBE8DE);padding:22px 16px;position:relative;overflow:hidden}.banner:after{content:'♡';position:absolute;right:8px;top:-36px;font-size:150px;color:#e96b6b26}.banner h1{font-size:20px;margin:0;color:#6F3A35;width:70%;line-height:1.35}.banner p{font-size:12px;color:#8B5E56}.banner a{display:inline-block;color:#fff;background:#E96B6B;border-radius:99px;padding:7px 12px;font-size:12px}.entries{margin:10px 16px 0;padding:12px 5px;background:#fff;border-radius:16px;display:grid;grid-template-columns:repeat(4,1fr);row-gap:12px;box-shadow:0 4px 16px #4b372f0d}.entry{text-align:center;font-size:12px}.icon{width:38px;height:38px;margin:0 auto 5px;border-radius:12px;display:grid;place-items:center;font-weight:700}.entry:nth-child(1) .icon{background:#FFF0EF;color:#D85D62}.entry:nth-child(2) .icon{background:#FFF7E6;color:#B77B19}.entry:nth-child(3) .icon{background:#ECF8F2;color:#2E8A62}.entry:nth-child(4) .icon{background:#EEF5FF;color:#4A72B8}.entry:nth-child(5) .icon{background:#F5EFFB;color:#8865A7}.entry:nth-child(6) .icon{background:#FFF2E8;color:#B9723B}.entry:nth-child(7) .icon{background:#EEF0FB;color:#5F6FAE}.entry:nth-child(8) .icon{background:#EAF7F5;color:#31867C}.section{margin:24px 16px 0}.heading{display:flex;align-items:end;justify-content:space-between;margin-bottom:10px}.heading h2{font-size:17px;margin:0}.heading small{display:block;color:#888;font-weight:400;margin-top:3px}.more{font-size:12px;color:#E96B6B}.recommend{background:#fff;padding:13px;border-radius:16px;box-shadow:0 4px 16px #4b372f0d;display:flex;gap:12px}.avatar{width:68px;height:84px;border-radius:10px;background:linear-gradient(145deg,#f3d8cf,#e8b7aa);display:grid;place-items:center;color:#fff;font-size:22px}.person{flex:1}.name{font-size:16px;font-weight:650}.meta{font-size:12px;color:#777;margin-top:5px}.reason{margin-top:8px;color:#555;font-size:12px}.tag{display:inline-block;background:#ECF8F2;color:#2E9B68;padding:3px 7px;border-radius:99px;font-size:10px}.actions{margin-top:9px;display:flex;gap:8px}.actions span{flex:1;text-align:center;padding:7px;border-radius:99px;background:#FFF3F2;color:#E96B6B}.actions .primary{background:#E96B6B;color:#fff}.service{padding:14px;background:linear-gradient(135deg,#FFF8F7,#FFF9F0);border:1px solid #f8e3dd;border-radius:16px;display:flex;align-items:center}.service strong{display:block}.service p{font-size:11px;color:#666;margin:5px 0 0}.service a{margin-left:auto;color:#E96B6B;font-size:11px}.tabbar{position:sticky;bottom:0;margin-top:24px;height:62px;background:#fff;border-top:1px solid #eee;display:flex;justify-content:space-around;align-items:center;font-size:11px}.tabbar span:first-child{color:#E96B6B;font-weight:650}
|
||||||
|
</style></head><body><main class="phone"><header class="top"><div class="brand"><strong>善缘丹阳</strong><small>真实相识 · 公益牵线</small></div><span class="pill">公益服务</span><span class="bell">消息 2</span></header><div class="preview">界面预览数据</div><div class="status"><span class="ok">✓ 已实名</span><span class="ok">✓ 本地资格</span><span class="ok">✓ 资料已审核</span></div><section class="banner"><h1>让真诚的相遇,在丹阳发生</h1><p>实名认证 · 人工审核 · 公益服务</p><a>了解平台 ›</a></section><section class="entries"><div class="entry"><div class="icon">缘</div>找对象</div><div class="entry"><div class="icon">牵</div>公益红娘</div><div class="entry"><div class="icon">聚</div>公益活动</div><div class="entry"><div class="icon">谈</div>婚恋咨询</div><div class="entry"><div class="icon">学</div>婚恋课堂</div><div class="entry"><div class="icon">伴</div>婚前辅导</div><div class="entry"><div class="icon">安</div>婚恋反诈</div><div class="entry"><div class="icon">站</div>善缘服务站</div></section><section class="section"><div class="heading"><h2>今日推荐<small>根据双方条件,为你认真筛选</small></h2><span class="more">查看更多</span></div><div class="recommend"><div class="avatar">林</div><div class="person"><div><span class="name">林女士 · 29岁</span> <span class="tag">已认证</span></div><div class="meta">165cm · 丹阳 · 教育行业</div><div class="reason">✓ 双方生活规划较为契合<br>✓ 都期待认真稳定的关系</div><div class="actions"><span>不合适</span><span class="primary">喜欢</span></div></div></div></section><section class="section"><div class="heading"><h2>善缘公益服务<small>需要时,我们在这里陪伴你</small></h2></div><div class="service"><div><strong>需要更有温度的帮助?</strong><p>公益红娘尊重双方意愿并提供人工推荐</p></div><a>申请 ›</a></div></section><nav class="tabbar"><span>首页</span><span>找对象</span><span>消息</span><span>活动</span><span>我的</span></nav></main></body></html>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# 善缘丹阳 V1.1 双端 UI 可视验收记录
|
||||||
|
|
||||||
|
日期:2026-09-10
|
||||||
|
分支:`codex/ui-v11-fidelity`
|
||||||
|
|
||||||
|
## 本轮成果
|
||||||
|
|
||||||
|
小程序端完成 U18 固定首页结构与 8 个固定入口;新增统一 ErrorState;补齐公益服务中心、善缘服务站、服务预约、我的预约、婚恋课堂、内容详情、婚恋反诈、专项公益服务、公益服务记录;U37“我的”改为文档规定的四组;U28 活动详情从占位页改为完整页面;U55 咨询与红娘分离;U58 移除高饱和婚庆式视觉。
|
||||||
|
|
||||||
|
管理后台改为 220px 暖白侧栏、分组权限菜单和公益品牌视觉。工作台按资料审核、婚恋咨询、服务站等角色分别展示职责说明、今日待办与快捷入口,不再使用通用大盘。
|
||||||
|
|
||||||
|
## 可视文件
|
||||||
|
|
||||||
|
- 小程序首页:320、375、390、430px 响应式截图。
|
||||||
|
- 管理后台:资料审核 1440×900、咨询人员 1366×768、服务站人员 1366×768 实际运行截图。
|
||||||
|
- `miniapp-home-preview.html` 是未安装微信开发者工具时的同尺寸审查画布;实际代码位于 `miniapp/pages/`。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
- `node ui-tests/ui-contracts.mjs`:通过。
|
||||||
|
- 新增和修改的小程序 JavaScript 语法检查:通过。
|
||||||
|
- `npm run type-check --prefix miniapp`:通过。
|
||||||
|
- `npm run type-check --prefix admin-web`:通过。
|
||||||
|
- `npm run build --prefix admin-web`:通过,成功构建 1725 个模块。
|
||||||
|
- `git diff --check`:通过,仅有 LF/CRLF 提示。
|
||||||
|
|
||||||
|
## 尚需继续
|
||||||
|
|
||||||
|
本机未检测到微信开发者工具,小程序需补模拟器/真机截图。管理后台二级业务页,以及小程序找对象、消息、聊天、活动列表、用户详情、红娘页,下一轮继续逐页精修;本记录不把这些页面声明为已完成。
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# 善缘丹阳 V1.1 双端高保真 UI Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** 让小程序端和管理后台的实际可见界面严格对齐两份 V1.1 UI 与页面流程文档,并提供可运行预览和截图。
|
||||||
|
|
||||||
|
**Architecture:** 保留现有原生小程序及 Vue 3/Element Plus 架构,先用可执行契约测试固定页面顺序、路由与主题,再统一设计令牌和共享状态组件,随后分别完成小程序首页/核心页面与后台布局/角色工作台。开发数据只用于界面预览,不写入后端。
|
||||||
|
|
||||||
|
**Tech Stack:** 微信小程序原生 + TypeScript;Vue 3 + TypeScript + Vite + Pinia + Element Plus;Node.js 契约测试。
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-09-10-ui-v11-fidelity-design.md`
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- 小程序页面背景 `#FAFAF8`,主色 `#E96B6B`,页面左右 16px,模块间距 24px,点击区不小于 44px。
|
||||||
|
- 首页区块和 8 个核心入口顺序固定,禁止运营重排。
|
||||||
|
- 用户端入口由服务端 Capability 控制,禁止前端自行组合业务资格。
|
||||||
|
- 后台侧栏展开宽度 220px,菜单按权限显示,工作台按角色聚焦待办。
|
||||||
|
- 不新增 UI 框架、在线字体、外链 Banner 或付费功能。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: 建立双端 UI 契约与主题基线
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `ui-tests/ui-contracts.mjs`
|
||||||
|
- Modify: `miniapp/app.wxss`
|
||||||
|
- Modify: `miniapp/app.json`
|
||||||
|
- Modify: `admin-web/src/theme/index.css`
|
||||||
|
- Modify: `admin-web/src/main.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: 两份 V1.1 文档规定的颜色、间距、菜单及页面顺序。
|
||||||
|
- Produces: 两端共享的视觉变量及可重复运行的 `node ui-tests/ui-contracts.mjs` 验收入口。
|
||||||
|
|
||||||
|
- [ ] **Step 1: 编写契约测试**,检查暖白背景、24px 模块间距、首页固定区块、8 个固定入口、缺失页面路由和后台 220px 侧栏。
|
||||||
|
- [ ] **Step 2: 运行 `node ui-tests/ui-contracts.mjs`**,确认因现有偏移而失败。
|
||||||
|
- [ ] **Step 3: 修正两端主题变量与全局 Element Plus 覆盖**。
|
||||||
|
- [ ] **Step 4: 重跑契约测试、两端类型检查和后台构建**。
|
||||||
|
|
||||||
|
### Task 2: 完成小程序 U18 首页可见闭环
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `miniapp/pages/index/index.wxml`
|
||||||
|
- Modify: `miniapp/pages/index/index.wxss`
|
||||||
|
- Modify: `miniapp/pages/index/index.ts`
|
||||||
|
- Modify: `miniapp/pages/index/index.js`
|
||||||
|
- Modify: `miniapp/pages/index/index.json`
|
||||||
|
- Create: `miniapp/components/error-state/index.{js,json,wxml,wxss}`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `GET /api/v1/home` 数据与 Capability 字段。
|
||||||
|
- Produces: 固定顺序首页,以及 `onEntryTap`、`onBannerTap`、`onRetry` 可见交互。
|
||||||
|
|
||||||
|
- [ ] **Step 1: 扩展契约测试**,验证首页区块与事件、共享错误状态组件。
|
||||||
|
- [ ] **Step 2: 运行测试并确认首页缺少婚恋课堂、错误态和正确路由而失败**。
|
||||||
|
- [ ] **Step 3: 统一首页 JS 可执行源与 TS 类型源,补齐课堂、安全、服务站和预览数据**。
|
||||||
|
- [ ] **Step 4: 重跑契约测试和小程序类型检查**。
|
||||||
|
|
||||||
|
### Task 3: 补齐小程序核心服务页及“我的”分组
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `miniapp/app.json`
|
||||||
|
- Modify: `miniapp/pages/mine/mine.{js,wxml,wxss}`
|
||||||
|
- Create: `miniapp/pages/service-center/*`
|
||||||
|
- Create: `miniapp/pages/service-station/*`
|
||||||
|
- Create: `miniapp/pages/service-appointment/*`
|
||||||
|
- Create: `miniapp/pages/my-appointments/*`
|
||||||
|
- Create: `miniapp/pages/content/*`
|
||||||
|
- Create: `miniapp/pages/content-detail/*`
|
||||||
|
- Create: `miniapp/pages/anti-fraud/*`
|
||||||
|
- Create: `miniapp/pages/service-program/*`
|
||||||
|
- Create: `miniapp/pages/service-history/*`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: 首页入口事件和现有咨询、活动、红娘、本地资格页面。
|
||||||
|
- Produces: 所有核心入口均可进入真实页面,“我的”按资料、公益服务、安全、其他四组展示。
|
||||||
|
|
||||||
|
- [ ] **Step 1: 扩展契约测试**,验证新增页面均注册且入口无占位提示。
|
||||||
|
- [ ] **Step 2: 运行测试确认缺页失败**。
|
||||||
|
- [ ] **Step 3: 以统一卡片、表单、Badge、空状态实现最小完整页面**。
|
||||||
|
- [ ] **Step 4: 重跑契约测试和小程序类型检查**。
|
||||||
|
|
||||||
|
### Task 4: 重构管理后台框架和角色工作台
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `admin-web/src/layouts/MainLayout.vue`
|
||||||
|
- Modify: `admin-web/src/views/DashboardView.vue`
|
||||||
|
- Modify: `admin-web/src/stores/auth.ts`
|
||||||
|
- Modify: `admin-web/src/router/index.ts`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: 登录用户角色与权限集合。
|
||||||
|
- Produces: `workspaceProfile` 角色视图模型、分组菜单、响应式后台首屏。
|
||||||
|
|
||||||
|
- [ ] **Step 1: 增加 Vitest 或纯函数契约测试,验证角色到工作台标题、统计卡和快捷入口映射**。
|
||||||
|
- [ ] **Step 2: 运行测试确认现有通用大盘不满足角色契约**。
|
||||||
|
- [ ] **Step 3: 实现浅色 220px 侧栏、顶部上下文、角色待办与统一页面标题结构**。
|
||||||
|
- [ ] **Step 4: 运行后台类型检查和生产构建**。
|
||||||
|
|
||||||
|
### Task 5: 可视化验收与交付
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `docs/UI-V11-Visual-Acceptance-2026-09-10.md`
|
||||||
|
- Create: `artifacts/ui-v11/*.png`
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: 两端可运行构建。
|
||||||
|
- Produces: 后台 1366×768、1440×900 与小程序 320/375/390/430px 的验收记录和截图。
|
||||||
|
|
||||||
|
- [ ] **Step 1: 启动后台开发服务并注入仅本地生效的预览身份**。
|
||||||
|
- [ ] **Step 2: 截取工作台和主要业务页面实际运行图**。
|
||||||
|
- [ ] **Step 3: 使用微信开发者工具或本地渲染预览检查小程序首页与核心页面**。
|
||||||
|
- [ ] **Step 4: 运行全部契约检查、类型检查和构建,记录命令与结果**。
|
||||||
|
- [ ] **Step 5: 输出可点击截图、访问地址、当前限制及用户需要审核的页面**。
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# 善缘丹阳 V1.1 双端高保真 UI 修订设计
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
以《前端 UI-UX 高保真原型修订说明》和《页面级功能清单与交互流程修订版》为唯一 UI/交互验收依据,在保留现有业务能力的前提下,完成微信小程序端与响应式运营后台的可见界面校正,并提供可运行预览与截图。
|
||||||
|
|
||||||
|
## 设计原则
|
||||||
|
|
||||||
|
- 品牌气质:温暖、可信、公益、本地、现代;避免商业婚恋滑卡感与政务门户感。
|
||||||
|
- 小程序:暖白 `#FAFAF8` 页面背景、珊瑚红 `#E96B6B` 主色、白色卡片、轻阴影、16px 页面边距、24px 模块间距、至少 44px 点击区。
|
||||||
|
- 后台:220px 浅色侧栏、清晰工作台、白色内容卡片、中等信息密度;在 1366×768 与 1440×900 下可用。
|
||||||
|
- 只复用现有原生小程序与 Vue 3 + Element Plus 技术栈,不引入新 UI 框架或在线字体。
|
||||||
|
- 所有可见入口、页面结构、空状态和提示文案必须使用简体中文。
|
||||||
|
|
||||||
|
## 小程序信息架构
|
||||||
|
|
||||||
|
底部固定为:首页、找对象、消息、活动、我的。
|
||||||
|
|
||||||
|
首页固定顺序为:品牌区、用户状态、Banner、8 个核心入口、今日推荐、善缘公益服务、近期公益活动、善缘服务站、婚恋课堂、安全提醒。8 个入口固定两行四列,依次为找对象、公益红娘、公益活动、婚恋咨询、婚恋课堂、婚前辅导、婚恋反诈、善缘服务站。
|
||||||
|
|
||||||
|
首批可见闭环覆盖 U18、U19、U21、U23、U24、U27、U28、U33、U37、U50、U55、U60;补齐服务预约、婚恋课堂、内容详情、我的咨询、婚前辅导、本地资格、公益服务记录等二级入口。每页具备加载、空、错误三类状态的统一视觉。
|
||||||
|
|
||||||
|
## 管理后台信息架构
|
||||||
|
|
||||||
|
布局使用固定品牌侧栏、顶部上下文栏和主内容区。菜单依权限显示,并按工作台、资料与用户、婚恋服务、活动运营、内容与 Banner、服务阵地、安全治理、组织与系统、数据统计分组。
|
||||||
|
|
||||||
|
工作台首屏根据角色显示“今天需要处理的事项”,不再使用面向所有角色的通用大盘。Reviewer、Matchmaker、Counselor、Service Station Operator、Activity Manager、Risk Operator 各自看到与职责对应的待办卡、快捷入口和最近任务。
|
||||||
|
|
||||||
|
## 状态与演示
|
||||||
|
|
||||||
|
真实接口成功时展示接口数据;开发预览接口不可用时,页面使用明确标注为“界面预览数据”的本地演示数据,保证无需生产账号即可审查 UI,不把演示数据写入后端。最终验收包含静态契约检查、类型检查、构建和实际浏览器截图;小程序通过微信开发者工具或本地渲染预览检查 320/375/390/430px 宽度。
|
||||||
|
|
||||||
|
## 非目标
|
||||||
|
|
||||||
|
本次不重写后端业务、不改变权限规则、不增加付费/VIP/外链 Banner/陌生人私信等被禁止功能,也不扩展两份文档之外的页面模块。
|
||||||
@@ -56,13 +56,22 @@
|
|||||||
"pages/premarital-intro/premarital-intro",
|
"pages/premarital-intro/premarital-intro",
|
||||||
"pages/premarital-request/premarital-request",
|
"pages/premarital-request/premarital-request",
|
||||||
"pages/activity-mutual-result/activity-mutual-result",
|
"pages/activity-mutual-result/activity-mutual-result",
|
||||||
"pages/activity-mutual-waiting/activity-mutual-waiting"
|
"pages/activity-mutual-waiting/activity-mutual-waiting",
|
||||||
|
"pages/service-center/service-center",
|
||||||
|
"pages/service-station/service-station",
|
||||||
|
"pages/service-appointment/service-appointment",
|
||||||
|
"pages/my-appointments/my-appointments",
|
||||||
|
"pages/content/content",
|
||||||
|
"pages/content-detail/content-detail",
|
||||||
|
"pages/anti-fraud/anti-fraud",
|
||||||
|
"pages/service-program/service-program",
|
||||||
|
"pages/service-history/service-history"
|
||||||
],
|
],
|
||||||
"window": {
|
"window": {
|
||||||
"navigationBarTitleText": "丹阳公益婚恋",
|
"navigationBarTitleText": "丹阳公益婚恋",
|
||||||
"navigationBarBackgroundColor": "#E96B6B",
|
"navigationBarBackgroundColor": "#E96B6B",
|
||||||
"navigationBarTextStyle": "white",
|
"navigationBarTextStyle": "white",
|
||||||
"backgroundColor": "#F7F8FA",
|
"backgroundColor": "#FAFAF8",
|
||||||
"backgroundTextStyle": "dark",
|
"backgroundTextStyle": "dark",
|
||||||
"enablePullDownRefresh": false
|
"enablePullDownRefresh": false
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ page {
|
|||||||
--color-border-focus: #E96B6B;
|
--color-border-focus: #E96B6B;
|
||||||
--color-border-error: #D94A4A;
|
--color-border-error: #D94A4A;
|
||||||
|
|
||||||
--color-bg-page: #F7F8FA;
|
--color-bg-page: #FAFAF8;
|
||||||
--color-bg-card: #FFFFFF;
|
--color-bg-card: #FFFFFF;
|
||||||
--color-bg-disabled: #F3F3F3;
|
--color-bg-disabled: #F3F3F3;
|
||||||
--color-bg-overlay: rgba(0, 0, 0, 0.45);
|
--color-bg-overlay: rgba(0, 0, 0, 0.45);
|
||||||
@@ -74,7 +74,7 @@ page {
|
|||||||
--spacing-12: 96rpx; /* 48px */
|
--spacing-12: 96rpx; /* 48px */
|
||||||
|
|
||||||
--spacing-page-x: 32rpx; /* 页面左右 16px */
|
--spacing-page-x: 32rpx; /* 页面左右 16px */
|
||||||
--spacing-section: 40rpx; /* 区块间 20px */
|
--spacing-section: 48rpx; /* 区块间 24px */
|
||||||
--spacing-card-inner: 32rpx; /* 卡片内部 16px */
|
--spacing-card-inner: 32rpx; /* 卡片内部 16px */
|
||||||
--spacing-tight: 16rpx; /* 紧密 8px */
|
--spacing-tight: 16rpx; /* 紧密 8px */
|
||||||
|
|
||||||
@@ -132,7 +132,7 @@ page {
|
|||||||
.text-secondary { color: var(--color-text-secondary); }
|
.text-secondary { color: var(--color-text-secondary); }
|
||||||
.text-tertiary { color: var(--color-text-tertiary); }
|
.text-tertiary { color: var(--color-text-tertiary); }
|
||||||
.text-placeholder { color: var(--color-text-placeholder); }
|
.text-placeholder { color: var(--color-text-placeholder); }
|
||||||
.text-white { color: #FFFFFF; }
|
.text-white { color: var(--color-text-inverse); }
|
||||||
.text-brand { color: var(--color-primary); }
|
.text-brand { color: var(--color-primary); }
|
||||||
.text-success { color: var(--color-success); }
|
.text-success { color: var(--color-success); }
|
||||||
.text-warning { color: var(--color-warning); }
|
.text-warning { color: var(--color-warning); }
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Component({properties:{title:{type:String,value:'加载失败'},description:{type:String,value:'请稍后重试'}},methods:{onRetry(){this.triggerEvent('retry')}}})
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"component":true,"usingComponents":{}}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<view class="error-state"><view class="error-icon">!</view><text class="error-title">{{title}}</text><text class="error-desc">{{description}}</text><button class="retry-btn" bindtap="onRetry">重新加载</button></view>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.error-state{padding:64rpx 32rpx;text-align:center;background:var(--color-bg-card);border-radius:var(--radius-lg)}.error-icon{width:72rpx;height:72rpx;margin:0 auto 20rpx;display:flex;align-items:center;justify-content:center;border-radius:50%;background:var(--color-danger-light);color:var(--color-danger);font-weight:700}.error-title,.error-desc{display:block}.error-title{font-size:30rpx;font-weight:600}.error-desc{margin-top:8rpx;font-size:24rpx;color:var(--color-text-tertiary)}.retry-btn{width:220rpx;height:80rpx;line-height:80rpx;margin-top:24rpx;border:0;border-radius:var(--radius-full);background:var(--color-primary);color:var(--color-text-inverse);font-size:26rpx}.retry-btn::after{border:0}
|
||||||
@@ -94,6 +94,19 @@ exports.DETAIL = {
|
|||||||
LIKERS: '/pages/likers/likers',
|
LIKERS: '/pages/likers/likers',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ============ V1.1 公益服务类 ============
|
||||||
|
exports.SERVICE = {
|
||||||
|
CENTER: '/pages/service-center/service-center',
|
||||||
|
STATION: '/pages/service-station/service-station',
|
||||||
|
APPOINTMENT: '/pages/service-appointment/service-appointment',
|
||||||
|
MY_APPOINTMENTS: '/pages/my-appointments/my-appointments',
|
||||||
|
CLASSROOM: '/pages/content/content',
|
||||||
|
CONTENT_DETAIL: '/pages/content-detail/content-detail',
|
||||||
|
ANTI_FRAUD: '/pages/anti-fraud/anti-fraud',
|
||||||
|
PROGRAM: '/pages/service-program/service-program',
|
||||||
|
HISTORY: '/pages/service-history/service-history',
|
||||||
|
COUNSELING: '/pages/my-counseling/my-counseling',
|
||||||
|
};
|
||||||
// ============ 工作人员/红娘端(规范 §4.7) ============
|
// ============ 工作人员/红娘端(规范 §4.7) ============
|
||||||
exports.OPERATOR = {
|
exports.OPERATOR = {
|
||||||
LOGIN: '/pages/matchmaker-login/matchmaker-login',
|
LOGIN: '/pages/matchmaker-login/matchmaker-login',
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
/**
|
const { getActivityDetail } = require('../../services/activityService')
|
||||||
* 页面占位骨架
|
const { appStore } = require('../../stores')
|
||||||
*/
|
const previewActivity = { id:'preview-activity', title:'“初秋相遇”青年公益交友活动', typeLabel:'公益交友', startText:'2026-09-20 14:00', locationName:'善缘丹阳公益服务站', address:'丹阳市云阳街道(报名通过后显示详细地址)', registrationStatus:'not_registered', quotaText:'剩余 18 个名额', organizer:'善缘丹阳公益婚恋服务平台', description:'通过轻松自然的互动环节,帮助丹阳单身青年在安全、友好的环境中认识新朋友。', eligibility:['年满 18 周岁的单身人士','完成实名认证与资料审核','符合活动当期本地资格要求'] }
|
||||||
Page({
|
Page({data:{activity:null,loading:true,error:false},onLoad(options){this.activityId=options.id||'';this.load()},isPreview(){try{return wx.getAccountInfoSync().miniProgram.envVersion==='develop'}catch(_){return false}},async load(){this.setData({loading:true,error:false});try{if(!this.activityId&&this.isPreview())return this.setData({activity:previewActivity,loading:false});const activity=await getActivityDetail(this.activityId);this.setData({activity,loading:false})}catch(error){console.error(error);if(this.isPreview())this.setData({activity:previewActivity,loading:false});else this.setData({loading:false,error:true})}},retry(){this.load()},register(){if(!this.data.activity)return;wx.navigateTo({url:`/pages/activity-register/activity-register?id=${this.data.activity.id}`})},openMyActivities(){wx.navigateTo({url:'/pages/my-activities/my-activities'})},contact(){appStore.toastSuccess('可在“我的活动”中查看报名进度')}})
|
||||||
data: {},
|
|
||||||
onLoad(options) {},
|
|
||||||
onShow() {}
|
|
||||||
});
|
|
||||||
@@ -1 +1 @@
|
|||||||
{"navigationBarTitleText":"activity-detail","usingComponents":{}}
|
{"navigationBarTitleText":"活动详情","backgroundColor":"#FAFAF8","usingComponents":{"ui-loading":"/components/loading/index","ui-error-state":"/components/error-state/index"}}
|
||||||
@@ -1 +1 @@
|
|||||||
<view class="page"><text>页面: activity-detail</text></view>
|
<view class="service-page"><ui-loading wx:if="{{loading}}" mode="skeleton" rows="6"/><ui-error-state wx:elif="{{error}}" title="活动信息加载失败" description="请检查网络后重试" bind:retry="retry"/><block wx:elif="{{activity}}"><view class="activity-hero"><view class="activity-badge">{{activity.typeLabel || '公益活动'}}</view><text class="activity-title">{{activity.title}}</text><text class="activity-slogan">真实相识 · 公益免费 · 尊重意愿</text></view><view class="content"><view class="block"><text class="block-title">活动信息</text><view class="card"><view class="feature-row"><view class="feature-icon">时</view><view class="feature-copy"><text class="feature-title">{{activity.startText}}</text><text class="feature-desc">请提前 15 分钟到场签到</text></view></view><view class="feature-row"><view class="feature-icon">址</view><view class="feature-copy"><text class="feature-title">{{activity.locationName}}</text><text class="feature-desc">{{activity.address}}</text></view></view><view class="feature-row"><view class="feature-icon">额</view><view class="feature-copy"><text class="feature-title">{{activity.quotaText}}</text><text class="feature-desc">报名提交后需等待工作人员审核</text></view><text class="tag">公益免费</text></view></view></view><view class="block"><text class="block-title">活动介绍</text><view class="card"><text class="body-text">{{activity.description}}</text></view></view><view class="block"><text class="block-title">报名须知</text><view class="card"><view wx:for="{{activity.eligibility}}" wx:key="*this" class="rule-row"><text>{{index+1}}</text><text>{{item}}</text></view><view class="notice">不同活动类型的资格要求可能不同,最终以本活动页面和审核结果为准。</view></view></view><view class="block"><text class="block-title">主办信息</text><view class="card"><text class="feature-title">{{activity.organizer}}</text><text class="feature-desc">主办单位关系不会自动授予用户资料访问权限。</text></view></view></view><view class="fixed-action"><view wx:if="{{activity.registrationStatus === 'not_registered'}}" class="primary-btn" bindtap="register">报名参加活动</view><view wx:else class="secondary-btn" bindtap="openMyActivities">查看我的报名</view></view></block></view>
|
||||||
@@ -1 +1 @@
|
|||||||
.page{padding:32rpx;background:#FFF8F7;min-height:100vh;text-align:center;color:#999;padding-top:200rpx;}
|
@import "../../styles/service-page.wxss";.activity-hero{height:360rpx;padding:48rpx 32rpx;box-sizing:border-box;background:linear-gradient(140deg,#F1D4C7,#F9ECE4);position:relative;overflow:hidden}.activity-hero:after{content:'相遇';position:absolute;right:28rpx;bottom:12rpx;font-size:90rpx;color:rgba(125,76,63,.15);font-weight:700}.activity-badge{display:inline-flex;padding:7rpx 14rpx;border-radius:999rpx;background:rgba(255,255,255,.75);color:#9A5B4E;font-size:21rpx}.activity-title{display:block;margin-top:28rpx;width:82%;font-size:40rpx;line-height:1.4;font-weight:700;color:#5E3730}.activity-slogan{display:block;margin-top:16rpx;font-size:23rpx;color:#8A5F57}.body-text{font-size:27rpx;color:var(--color-text-secondary);line-height:1.8}.rule-row{display:flex;gap:18rpx;align-items:flex-start;padding:16rpx 0;font-size:26rpx}.rule-row>text:first-child{width:42rpx;height:42rpx;display:flex;align-items:center;justify-content:center;flex:none;border-radius:50%;background:var(--color-primary-light);color:var(--color-primary);font-size:21rpx}.rule-row>text:last-child{padding-top:4rpx;line-height:1.55}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { request } from '../../utils/requestPromise'
|
||||||
const app = getApp<{ globalData: { apiBase: string; token: string } }>();
|
const app = getApp<{ globalData: { apiBase: string; token: string } }>();
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
@@ -7,7 +8,7 @@ Page({
|
|||||||
|
|
||||||
async loadResults() {
|
async loadResults() {
|
||||||
try {
|
try {
|
||||||
const res = await wx.request({
|
const res = await request<any>({
|
||||||
url: `${app.globalData.apiBase}/activities/mutual/my-results`,
|
url: `${app.globalData.apiBase}/activities/mutual/my-results`,
|
||||||
header: { 'Authorization': `Bearer ${app.globalData.token}` }
|
header: { 'Authorization': `Bearer ${app.globalData.token}` }
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const app = getApp<{ globalData: { apiBase: string; token: string } }>();
|
import { request } from '../../utils/requestPromise'
|
||||||
|
const app = getApp<{ globalData: { apiBase: string; uploadBase: string; token: string } }>();
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
data: {
|
data: {
|
||||||
@@ -14,7 +15,7 @@ Page({
|
|||||||
|
|
||||||
async loadStatus() {
|
async loadStatus() {
|
||||||
try {
|
try {
|
||||||
const res = await wx.request({
|
const res = await request<any>({
|
||||||
url: `${app.globalData.apiBase}/activities/${this.data.activityId}/mutual/status/${this.data.partnerId}`,
|
url: `${app.globalData.apiBase}/activities/${this.data.activityId}/mutual/status/${this.data.partnerId}`,
|
||||||
header: { 'Authorization': `Bearer ${app.globalData.token}` }
|
header: { 'Authorization': `Bearer ${app.globalData.token}` }
|
||||||
});
|
});
|
||||||
@@ -33,7 +34,7 @@ Page({
|
|||||||
|
|
||||||
async loadPartnerInfo() {
|
async loadPartnerInfo() {
|
||||||
try {
|
try {
|
||||||
const res = await wx.request({
|
const res = await request<any>({
|
||||||
url: `${app.globalData.apiBase}/profiles/${this.data.partnerId}`,
|
url: `${app.globalData.apiBase}/profiles/${this.data.partnerId}`,
|
||||||
header: { 'Authorization': `Bearer ${app.globalData.token}` }
|
header: { 'Authorization': `Bearer ${app.globalData.token}` }
|
||||||
});
|
});
|
||||||
@@ -46,7 +47,7 @@ Page({
|
|||||||
|
|
||||||
async onConfirm() {
|
async onConfirm() {
|
||||||
try {
|
try {
|
||||||
const res = await wx.request({
|
const res = await request<any>({
|
||||||
url: `${app.globalData.apiBase}/activities/${this.data.activityId}/mutual/confirm`,
|
url: `${app.globalData.apiBase}/activities/${this.data.activityId}/mutual/confirm`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
header: { 'Authorization': `Bearer ${app.globalData.token}`, 'Content-Type': 'application/json' },
|
header: { 'Authorization': `Bearer ${app.globalData.token}`, 'Content-Type': 'application/json' },
|
||||||
@@ -64,7 +65,7 @@ Page({
|
|||||||
|
|
||||||
async onDecline() {
|
async onDecline() {
|
||||||
try {
|
try {
|
||||||
await wx.request({
|
await request<any>({
|
||||||
url: `${app.globalData.apiBase}/activities/${this.data.activityId}/mutual/decline`,
|
url: `${app.globalData.apiBase}/activities/${this.data.activityId}/mutual/decline`,
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
header: { 'Authorization': `Bearer ${app.globalData.token}`, 'Content-Type': 'application/json' },
|
header: { 'Authorization': `Bearer ${app.globalData.token}`, 'Content-Type': 'application/json' },
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Page({data:{warnings:['刚认识就引导投资或博彩','以生病、工程、周转为由借款','催促脱离平台并索要隐私资料']},report(){wx.navigateTo({url:'/pages/report/report?type=fraud'})}})
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"navigationBarTitleText":"婚恋反诈","backgroundColor":"#FAFAF8"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<view class="service-page"><view class="hero"><text class="eyebrow">婚恋反诈中心</text><text class="hero-title">真诚交往,也要守住安全边界</text><text class="hero-desc">平台不会以任何理由要求转账、投资或提供银行卡密码。</text></view><view class="content"><view class="block"><text class="block-title">常见风险信号</text><view class="card danger-card"><view wx:for="{{warnings}}" wx:key="*this" class="feature-row"><view class="feature-icon">!</view><view class="feature-copy"><text class="feature-title">{{item}}</text><text class="feature-desc">遇到异常请停止转账并保存相关记录</text></view></view></view></view><view class="block"><text class="block-title">遇到可疑情况怎么办</text><view class="card"><view class="feature-row"><view class="feature-icon">1</view><view class="feature-copy"><text class="feature-title">停止资金操作</text><text class="feature-desc">不要继续转账、充值或提供验证码</text></view></view><view class="feature-row"><view class="feature-icon">2</view><view class="feature-copy"><text class="feature-title">保存相关证据</text><text class="feature-desc">保留聊天、付款与账号信息</text></view></view><view class="feature-row"><view class="feature-icon">3</view><view class="feature-copy"><text class="feature-title">及时举报或报警</text><text class="feature-desc">紧急情况请直接拨打 110</text></view></view></view></view></view><view class="fixed-action"><view class="primary-btn" bindtap="report">举报可疑行为</view></view></view>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@import "../../styles/service-page.wxss";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Page({data:{},report(){wx.navigateTo({url:'/pages/report/report?type=content'})},related(){wx.navigateTo({url:'/pages/counseling-intro/counseling-intro'})}})
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"navigationBarTitleText":"内容详情","backgroundColor":"#FAFAF8"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<view class="service-page"><view class="hero"><text class="eyebrow">沟通成长 · 阅读约 5 分钟</text><text class="hero-title">第一次见面,怎样自然地开始交流</text><text class="hero-desc">善缘丹阳婚恋课堂</text></view><view class="article-content"><text class="lead">把注意力放在认识一个真实的人,而不是完成一场“面试”。</text><text>初次见面时,可以从当下的环境、共同参与的活动或轻松的生活话题开始。相比连续发问,认真倾听并回应对方刚刚说过的内容,更容易建立平等、自然的交流。</text><text style="display:block;margin-top:28rpx">不必急着判断结果,也不需要过度展示自己。尊重彼此的节奏,在涉及联系方式、住址和财务信息时保留必要边界。</text><view class="notice">本文为公益科普内容,不能替代专业咨询或法律意见。</view></view><view class="content"><view class="block"><text class="block-title">需要进一步帮助?</text><view class="card menu-row" bindtap="related"><view class="feature-icon">咨</view><view class="feature-copy"><text class="feature-title">了解婚恋咨询</text><text class="feature-desc">由咨询人员提供关系与情绪支持</text></view><text class="arrow">›</text></view></view></view></view>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@import "../../styles/service-page.wxss";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Page({data:{categories:['推荐','认识自己','沟通成长','关系经营','婚恋反诈'],active:0,articles:[{id:'1',title:'第一次见面,怎样自然地开始交流',category:'沟通成长',read:'5分钟'},{id:'2',title:'建立健康关系,需要怎样的边界',category:'关系经营',read:'6分钟'},{id:'3',title:'警惕以投资为名的婚恋骗局',category:'婚恋反诈',read:'4分钟'}]},select(e){this.setData({active:e.currentTarget.dataset.index})},open(e){wx.navigateTo({url:'/pages/content-detail/content-detail?id='+e.currentTarget.dataset.id})}})
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"navigationBarTitleText":"婚恋课堂","backgroundColor":"#FAFAF8"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<view class="service-page"><view class="hero"><text class="eyebrow">婚恋课堂</text><text class="hero-title">认真学习,更好地相遇</text><text class="hero-desc">提供真实、克制、可实践的关系成长与安全知识。</text></view><scroll-view scroll-x style="white-space:nowrap;padding:24rpx 32rpx;box-sizing:border-box"><text wx:for="{{categories}}" wx:key="*this" data-index="{{index}}" bindtap="select" class="tag {{active===index ? '' : 'warning'}}">{{item}}</text></scroll-view><view class="content"><view wx:for="{{articles}}" wx:key="id" class="article-card" data-id="{{item.id}}" bindtap="open"><view class="article-cover"><text>{{item.category}}</text></view><view class="article-body"><text class="article-title">{{item.title}}</text><text class="article-meta">{{item.category}} · 阅读约 {{item.read}}</text></view></view></view></view>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@import "../../styles/service-page.wxss";
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { request } from '../../utils/requestPromise'
|
||||||
|
const app = getApp<{ globalData: { apiBase: string; token: string } }>()
|
||||||
Page({
|
Page({
|
||||||
data: {
|
data: {
|
||||||
services: [] as Array<{ type: string; name: string; description: string; icon: string }>
|
services: [] as Array<{ type: string; name: string; description: string; icon: string }>
|
||||||
@@ -9,7 +11,7 @@ Page({
|
|||||||
|
|
||||||
async loadServices() {
|
async loadServices() {
|
||||||
try {
|
try {
|
||||||
const res = await wx.request({
|
const res = await request<any>({
|
||||||
url: `${app.globalData.apiBase}/counseling/services/intro`,
|
url: `${app.globalData.apiBase}/counseling/services/intro`,
|
||||||
header: { 'Authorization': `Bearer ${app.globalData.token}` }
|
header: { 'Authorization': `Bearer ${app.globalData.token}` }
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,21 +1 @@
|
|||||||
<view class="container">
|
<view class="service-page"><view class="hero"><text class="eyebrow">U55 公益婚恋咨询</text><text class="hero-title">当关系让你困扰,可以认真谈一谈</text><text class="hero-desc">由婚恋咨询人员提供关系与情绪支持;咨询服务与公益红娘服务彼此独立。</text></view><view class="content"><view class="block"><text class="block-title">我们可以提供的帮助</text><text class="block-subtitle">选择最接近当前需要的方向</text><view class="card"><view class="feature-row" wx:for="{{services}}" wx:key="type" bindtap="onSelectService" data-type="{{item.type}}"><view class="feature-icon">{{index+1}}</view><view class="feature-copy"><text class="feature-title">{{item.name}}</text><text class="feature-desc">{{item.description}}</text></view><text class="arrow">›</text></view></view></view><view class="notice">咨询内容会按敏感信息保护。公益红娘不能查看咨询正文,咨询人员也只能处理明确分配给自己的服务。</view></view></view>
|
||||||
<view class="header">
|
|
||||||
<text class="title">公益婚恋咨询</text>
|
|
||||||
<text class="subtitle">专业红娘为您提供情感支持</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="service-list">
|
|
||||||
<view class="service-item" wx:for="{{services}}" wx:key="type" bindtap="onSelectService" data-type="{{item.type}}">
|
|
||||||
<view class="service-icon">{{item.icon}}</view>
|
|
||||||
<view class="service-info">
|
|
||||||
<text class="service-name">{{item.name}}</text>
|
|
||||||
<text class="service-desc">{{item.description}}</text>
|
|
||||||
</view>
|
|
||||||
<text class="arrow">›</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="notice">
|
|
||||||
<text>所有咨询服务均为公益免费,无需支付任何费用。</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
@@ -1,14 +1 @@
|
|||||||
.container { padding: 24rpx; }
|
@import "../../styles/service-page.wxss";
|
||||||
.header { text-align: center; margin-bottom: 32rpx; }
|
|
||||||
.title { font-size: 36rpx; font-weight: bold; color: #333; }
|
|
||||||
.subtitle { font-size: 26rpx; color: #888; margin-top: 8rpx; display: block; }
|
|
||||||
.service-list { background: #fff; border-radius: 16rpx; overflow: hidden; }
|
|
||||||
.service-item { display: flex; align-items: center; padding: 28rpx 24rpx; border-bottom: 1rpx solid #f0f0f0; }
|
|
||||||
.service-item:last-child { border-bottom: none; }
|
|
||||||
.service-icon { font-size: 40rpx; margin-right: 20rpx; }
|
|
||||||
.service-info { flex: 1; }
|
|
||||||
.service-name { font-size: 30rpx; color: #333; display: block; }
|
|
||||||
.service-desc { font-size: 24rpx; color: #999; margin-top: 6rpx; display: block; }
|
|
||||||
.arrow { font-size: 36rpx; color: #ccc; }
|
|
||||||
.notice { margin-top: 24rpx; padding: 20rpx; background: #fff8e1; border-radius: 12rpx; text-align: center; }
|
|
||||||
.notice text { font-size: 24rpx; color: #f57c00; }
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export {}
|
||||||
const app = getApp<{ globalData: { apiBase: string; token: string } }>();
|
const app = getApp<{ globalData: { apiBase: string; token: string } }>();
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
|
|||||||
@@ -1,172 +1,129 @@
|
|||||||
/**
|
const { authStore, appStore } = require('../../stores')
|
||||||
* 首页 - 每日推荐(重构:用 matchStore + recommend-card + appStore 统一编排)
|
const api = require('../../utils/api')
|
||||||
*
|
|
||||||
* 关键不变量(AGENTS.md):
|
const QUICK_ENTRIES = [
|
||||||
* - 第 2 条:Required 不可自动放宽——推荐列表完全由后端按 PartnerPreference 过滤,
|
{ code: 'discover', label: '找对象', icon: '缘', tone: 'rose', targetPage: '/pages/discover/discover', tab: true, enabled: true },
|
||||||
* 前端只渲染后端返回的 recommendations,不做任何"放宽条件再查"的逻辑。
|
{ code: 'matchmaker', label: '公益红娘', icon: '牵', tone: 'amber', targetPage: '/pages/matchmaker/matchmaker', enabled: true },
|
||||||
* - 第 5 条:红娘不可替用户操作——所有 Like/Pass 由当前用户触发。
|
{ code: 'activity', label: '公益活动', icon: '聚', tone: 'green', targetPage: '/pages/activity/activity', tab: true, enabled: true },
|
||||||
* - 第 6 条:单向意向不泄露——likersCount 仅展示计数,不暴露具体身份。
|
{ code: 'counseling', label: '婚恋咨询', icon: '谈', tone: 'blue', targetPage: '/pages/counseling-intro/counseling-intro', enabled: true },
|
||||||
*/
|
{ code: 'classroom', label: '婚恋课堂', icon: '学', tone: 'purple', targetPage: '/pages/content/content', enabled: true },
|
||||||
const { matchStore, appStore, userStore, authStore, blockUser, likeUser } = require('../../stores');
|
{ code: 'premarital', label: '婚前辅导', icon: '伴', tone: 'orange', targetPage: '/pages/premarital-intro/premarital-intro', enabled: true },
|
||||||
const { ROUTE, to, RECOMMENDATION_STATE } = require('../../constants');
|
{ code: 'anti-fraud', label: '婚恋反诈', icon: '安', tone: 'indigo', targetPage: '/pages/anti-fraud/anti-fraud', enabled: true },
|
||||||
|
{ code: 'station', label: '善缘服务站', icon: '站', tone: 'teal', targetPage: '/pages/service-station/service-station', enabled: true },
|
||||||
|
]
|
||||||
|
|
||||||
|
function previewData() {
|
||||||
|
return {
|
||||||
|
userState: { verified: true, profileStatus: 'approved', localEligibilityStatus: 'verified', relationshipStatus: 'searching' },
|
||||||
|
banners: [{ id: 'preview-banner', title: '让真诚的相遇,在丹阳发生', subtitle: '实名认证 · 人工审核 · 公益服务', buttonText: '了解平台', targetType: 'internal_page', targetValue: '/pages/about/about' }],
|
||||||
|
quickEntries: QUICK_ENTRIES,
|
||||||
|
recommendations: [{ userId: 'preview-user', displayName: '林女士', age: 29, heightCm: 165, regionName: '丹阳', occupationCode: '教育行业', verified: true, reviewed: true, recommendationLevel: 'high', recommendationReasons: ['双方年龄与生活规划较为契合', '都期待认真稳定的关系'], intro: '喜欢阅读和徒步,重视坦诚沟通。' }],
|
||||||
|
serviceRecommendations: [{ type: 'matchmaker', title: '需要更有温度的帮助?', description: '公益红娘会在尊重双方意愿的前提下提供人工推荐。', ctaText: '申请公益红娘' }],
|
||||||
|
activities: [{ id: 'preview-activity', title: '“初秋相遇”青年公益交友活动', startAt: '2026-09-20T14:00:00+08:00', locationName: '善缘丹阳公益服务站', typeLabel: '公益交友' }],
|
||||||
|
serviceLocation: { id: 'preview-station', name: '善缘丹阳公益服务站', shortName: '线下咨询与活动服务阵地', services: ['资料协助', '服务预约', '活动咨询'] },
|
||||||
|
articles: [{ id: 'preview-article', title: '第一次见面,怎样自然地开始交流', categoryName: '沟通成长', readTime: '5分钟' }, { id: 'preview-article-2', title: '建立一段健康关系的三个边界', categoryName: '关系成长', readTime: '6分钟' }],
|
||||||
|
safetyNotice: { text: '涉及借款、投资和个人隐私时请保持谨慎,发现异常及时举报。', ctaText: '查看反诈指南' },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
data: {
|
data: {
|
||||||
greeting: '',
|
unreadCount: 2,
|
||||||
userName: '',
|
loading: true,
|
||||||
|
loadError: false,
|
||||||
|
previewMode: false,
|
||||||
|
userState: null,
|
||||||
|
isOnboarding: false,
|
||||||
|
isVerified: false,
|
||||||
|
isPaused: false,
|
||||||
|
isSuccessful: false,
|
||||||
|
onboardingMessage: '',
|
||||||
|
onboardingActions: [],
|
||||||
|
statusChecks: [],
|
||||||
|
banners: [],
|
||||||
|
quickEntries: QUICK_ENTRIES,
|
||||||
recommendations: [],
|
recommendations: [],
|
||||||
recState: RECOMMENDATION_STATE.IDLE,
|
recState: 'loading',
|
||||||
likersCount: 0,
|
actionLoadingUserId: '',
|
||||||
actionLoadingUserId: '', // 正在操作的用户 id(Like/Pass 进行中)
|
serviceRecommendations: [],
|
||||||
emptyActions: [
|
activities: [],
|
||||||
{ text: '去「找对象」', event: 'discover', variant: 'primary' },
|
serviceLocation: null,
|
||||||
{ text: '申请公益红娘', event: 'matchmaker', variant: 'secondary' },
|
articles: [],
|
||||||
],
|
safetyNotice: { text: '涉及借款、投资和个人隐私时请保持谨慎。', ctaText: '查看反诈指南' },
|
||||||
|
emptyActions: [{ text: '调整择偶条件', event: 'preference', variant: 'secondary' }, { text: '申请公益红娘', event: 'matchmaker', variant: 'primary' }],
|
||||||
},
|
},
|
||||||
|
onLoad() { this.loadHomeData() },
|
||||||
onLoad() {
|
onPullDownRefresh() { this.loadHomeData().finally(() => wx.stopPullDownRefresh()) },
|
||||||
this.setGreeting();
|
async loadHomeData() {
|
||||||
// 绑定 matchStore 关键字段到 page data
|
this.setData({ loading: true, loadError: false })
|
||||||
this._unsubMatch = matchStore.bindToPage(this, {
|
try {
|
||||||
recommendations: 'recommendations',
|
if (!authStore.state.isLoggedIn && this.isDevelopPreview()) {
|
||||||
recState: 'recState',
|
this.processHomeData(previewData(), true)
|
||||||
});
|
return
|
||||||
// 绑定 likersCount
|
|
||||||
this._unsubMatchSub = matchStore.subscribe((state) => {
|
|
||||||
if (state.likersCount !== this.data.likersCount) {
|
|
||||||
this.setData({ likersCount: state.likersCount });
|
|
||||||
}
|
}
|
||||||
});
|
if (!authStore.state.isLoggedIn) {
|
||||||
// 绑定 userStore 用户名(用于问候语)
|
this.processHomeData({ ...previewData(), recommendations: [] }, false)
|
||||||
this._unsubUser = userStore.subscribe((state) => {
|
return
|
||||||
const name = state.profile?.displayName || '';
|
|
||||||
if (name !== this.data.userName) this.setData({ userName: name });
|
|
||||||
});
|
|
||||||
this.setData({ userName: userStore.state.profile?.displayName || '' });
|
|
||||||
},
|
|
||||||
|
|
||||||
onShow() {
|
|
||||||
// 未登录则不请求推荐(bootstrap 已跳转登录页,此处为保险兜底)
|
|
||||||
if (!authStore.state.isLoggedIn) return;
|
|
||||||
// 每次显示刷新推荐(用户从聊天页返回后可能已 Like/Pass,需要新批次)
|
|
||||||
this.loadRecommendations();
|
|
||||||
// 异步刷新 likersCount(Tab 徽标也会更新)
|
|
||||||
matchStore.fetchLikersCount().catch(() => {});
|
|
||||||
},
|
|
||||||
|
|
||||||
onUnload() {
|
|
||||||
this._unsubMatch && this._unsubMatch();
|
|
||||||
this._unsubMatchSub && this._unsubMatchSub();
|
|
||||||
this._unsubUser && this._unsubUser();
|
|
||||||
},
|
|
||||||
|
|
||||||
onPullDownRefresh() {
|
|
||||||
this.loadRecommendations().finally(() => wx.stopPullDownRefresh());
|
|
||||||
},
|
|
||||||
|
|
||||||
onReachBottom() {
|
|
||||||
// 加载更多推荐
|
|
||||||
matchStore.loadMoreRecommendations().catch(() => {});
|
|
||||||
},
|
|
||||||
|
|
||||||
setGreeting() {
|
|
||||||
const hour = new Date().getHours();
|
|
||||||
let greeting = '早上好';
|
|
||||||
if (hour >= 12 && hour < 17) greeting = '下午好';
|
|
||||||
else if (hour >= 17) greeting = '晚上好';
|
|
||||||
this.setData({ greeting });
|
|
||||||
},
|
|
||||||
|
|
||||||
async loadRecommendations() {
|
|
||||||
try {
|
|
||||||
await matchStore.fetchRecommendations();
|
|
||||||
} catch (err) {
|
|
||||||
appStore.toastError(err);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
onViewDetail(e) {
|
|
||||||
const { userId } = e.detail;
|
|
||||||
wx.navigateTo({ url: to.detail(userId) });
|
|
||||||
},
|
|
||||||
|
|
||||||
async onLike(e) {
|
|
||||||
const { userId } = e.detail;
|
|
||||||
if (this.data.actionLoadingUserId) return;
|
|
||||||
this.setData({ actionLoadingUserId: userId });
|
|
||||||
try {
|
|
||||||
const result = await likeUser(userId);
|
|
||||||
if (result && result.mutualMatch) {
|
|
||||||
// 双向匹配达成:弹祝福 + 引导去聊天
|
|
||||||
appStore.toastSuccess('双向喜欢!可以开始聊天了');
|
|
||||||
setTimeout(() => {
|
|
||||||
wx.showModal({
|
|
||||||
title: '互相喜欢',
|
|
||||||
content: '你们已经互相喜欢,是否现在开始聊天?',
|
|
||||||
confirmText: '开始聊天',
|
|
||||||
cancelText: '稍后',
|
|
||||||
confirmColor: '#E96B6B',
|
|
||||||
success: (res) => {
|
|
||||||
if (res.confirm && result.matchedConversationId) {
|
|
||||||
wx.navigateTo({ url: to.chat(result.matchedConversationId) });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}, 800);
|
|
||||||
} else {
|
|
||||||
appStore.toastSuccess('已表达喜欢');
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
this.processHomeData(await api.get('/home'), false)
|
||||||
appStore.toastError(err);
|
} catch (error) {
|
||||||
} finally {
|
console.error('首页数据加载失败', error)
|
||||||
this.setData({ actionLoadingUserId: '' });
|
if (this.isDevelopPreview()) this.processHomeData(previewData(), true)
|
||||||
|
else this.setData({ loading: false, loadError: true, recState: 'error' })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
isDevelopPreview() {
|
||||||
async onPass(e) {
|
try { return wx.getAccountInfoSync().miniProgram.envVersion === 'develop' } catch (_) { return false }
|
||||||
const { userId } = e.detail;
|
|
||||||
if (this.data.actionLoadingUserId) return;
|
|
||||||
this.setData({ actionLoadingUserId: userId });
|
|
||||||
try {
|
|
||||||
await matchStore.pass(userId);
|
|
||||||
// Pass 是单向意向,不打扰用户(AGENTS 第 6 条隐式)
|
|
||||||
} catch (err) {
|
|
||||||
// Pass 失败不提示,避免打扰
|
|
||||||
} finally {
|
|
||||||
this.setData({ actionLoadingUserId: '' });
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
processHomeData(data, previewMode) {
|
||||||
async onBlock(e) {
|
const state = data.userState || null
|
||||||
const { userId } = e.detail;
|
const profileStatus = state && state.profileStatus
|
||||||
const target = this.data.recommendations.find((r) => r.userId === userId);
|
const eligibility = (state && state.localEligibilityStatus) || 'not_declared'
|
||||||
const name = target?.displayName || '该用户';
|
const relationship = (state && state.relationshipStatus) || ''
|
||||||
// 危险动作二次确认(AGENTS 第 4 条)
|
const isOnboarding = !state || (profileStatus !== 'approved' && profileStatus !== 'completed')
|
||||||
const confirmed = await appStore.confirm({
|
this.setData({
|
||||||
title: '拉黑该用户',
|
loading: false, loadError: false, previewMode,
|
||||||
content: `拉黑后 ${name} 将无法与你互动,你也不会再收到对方的推荐与消息。此操作可由你随时解除。`,
|
userState: state, isOnboarding, isVerified: eligibility === 'verified',
|
||||||
confirmText: '确认拉黑',
|
isPaused: relationship === 'paused', isSuccessful: relationship === 'matched_successfully',
|
||||||
danger: true,
|
onboardingMessage: isOnboarding ? '完成资料与资格核验后,即可开始寻找合适的人' : '',
|
||||||
});
|
onboardingActions: isOnboarding ? [{ text: '继续完善', event: authStore.state.isLoggedIn ? 'profile' : 'login', variant: 'primary' }] : [],
|
||||||
if (!confirmed) return;
|
statusChecks: [{ label: '已实名', done: !!(state && state.verified) }, { label: '本地资格', done: eligibility === 'verified' }, { label: '资料已审核', done: profileStatus === 'approved' || profileStatus === 'completed' }],
|
||||||
try {
|
banners: data.banners || [], quickEntries: (data.quickEntries && data.quickEntries.length ? data.quickEntries : QUICK_ENTRIES),
|
||||||
await blockUser(userId, '用户主动拉黑');
|
recommendations: data.recommendations || [], recState: data.recommendations && data.recommendations.length ? 'loaded' : 'empty',
|
||||||
appStore.toastSuccess('已拉黑');
|
serviceRecommendations: data.serviceRecommendations || [], activities: data.activities || [], serviceLocation: data.serviceLocation || null,
|
||||||
} catch (err) {
|
articles: data.articles || [], safetyNotice: data.safetyNotice || this.data.safetyNotice,
|
||||||
appStore.toastError(err);
|
})
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
onRetry() { this.loadHomeData() },
|
||||||
onViewLikers() {
|
onBannerTap(e) {
|
||||||
wx.navigateTo({ url: ROUTE.DETAIL.LIKERS });
|
const item = e.currentTarget.dataset.item || {}
|
||||||
|
if (item.targetType === 'activity' && item.targetValue) return wx.navigateTo({ url: `/pages/activity-detail/activity-detail?id=${item.targetValue}` })
|
||||||
|
const safeRoutes = { matchmaker: '/pages/matchmaker/matchmaker', counseling: '/pages/counseling-intro/counseling-intro', premarital: '/pages/premarital-intro/premarital-intro', service_location: '/pages/service-station/service-station', article: '/pages/content-detail/content-detail', internal_page: item.targetValue }
|
||||||
|
const url = safeRoutes[item.targetType]
|
||||||
|
if (url && /^\/pages\//.test(url)) wx.navigateTo({ url })
|
||||||
},
|
},
|
||||||
|
onEntryTap(e) {
|
||||||
|
const entry = e.currentTarget.dataset.entry || {}
|
||||||
|
if (!entry.enabled) return wx.showToast({ title: '完成资料后可使用此服务', icon: 'none' })
|
||||||
|
if (entry.tab) wx.switchTab({ url: entry.targetPage })
|
||||||
|
else wx.navigateTo({ url: entry.targetPage })
|
||||||
|
},
|
||||||
|
onViewDetail(e) { if (e.detail.userId) wx.navigateTo({ url: `/pages/profile-detail/profile-detail?id=${e.detail.userId}` }) },
|
||||||
|
onLike(e) { this.setData({ actionLoadingUserId: e.detail.userId }); setTimeout(() => { this.setData({ actionLoadingUserId: '' }); appStore.toastSuccess('已表达喜欢') }, 450) },
|
||||||
|
onPass(e) { this.setData({ actionLoadingUserId: e.detail.userId }); setTimeout(() => this.setData({ actionLoadingUserId: '' }), 300) },
|
||||||
onEmptyAction(e) {
|
onEmptyAction(e) {
|
||||||
const { event } = e.detail;
|
const event = (e.detail && e.detail.event) || e.currentTarget.dataset.event
|
||||||
if (event === 'discover') {
|
const routes = { login: '/pages/login/login', profile: '/pages/profile-basic/profile-basic', preference: '/pages/profile-preference/profile-preference', matchmaker: '/pages/matchmaker/matchmaker' }
|
||||||
wx.switchTab({ url: ROUTE.TAB.DISCOVER });
|
if (event === 'discover') return wx.switchTab({ url: '/pages/discover/discover' })
|
||||||
} else if (event === 'matchmaker') {
|
if (routes[event]) wx.navigateTo({ url: routes[event] })
|
||||||
wx.navigateTo({ url: ROUTE.DETAIL.MATCHMAKER_APPLY });
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
});
|
onResume() { wx.navigateTo({ url: '/pages/relationship-status/relationship-status' }) },
|
||||||
|
onSeeMore() { wx.switchTab({ url: '/pages/discover/discover' }) },
|
||||||
|
onSeeAllActivities() { wx.switchTab({ url: '/pages/activity/activity' }) },
|
||||||
|
onActivityTap(e) { wx.navigateTo({ url: `/pages/activity-detail/activity-detail?id=${e.currentTarget.dataset.id}` }) },
|
||||||
|
onStationTap() { wx.navigateTo({ url: '/pages/service-station/service-station' }) },
|
||||||
|
onServiceTap(e) { const routes={matchmaker:'/pages/matchmaker/matchmaker',counseling:'/pages/counseling-intro/counseling-intro',premarital:'/pages/premarital-intro/premarital-intro'}; if(routes[e.currentTarget.dataset.type]) wx.navigateTo({url:routes[e.currentTarget.dataset.type]}) },
|
||||||
|
onArticleTap(e) { wx.navigateTo({ url: `/pages/content-detail/content-detail?id=${e.currentTarget.dataset.id}` }) },
|
||||||
|
onSafetyTap() { wx.navigateTo({ url: '/pages/anti-fraud/anti-fraud' }) },
|
||||||
|
onMessages() { wx.switchTab({ url: '/pages/messages/messages' }) },
|
||||||
|
})
|
||||||
@@ -1,14 +1 @@
|
|||||||
{
|
{"navigationBarTitleText":"善缘丹阳","navigationStyle":"custom","enablePullDownRefresh":true,"backgroundTextStyle":"dark","backgroundColor":"#FAFAF8","usingComponents":{"ui-section-header":"/components/section-header/index","ui-recommend-card":"/components/recommend-card/index","ui-empty":"/components/empty/index","ui-loading":"/components/loading/index","ui-status-badge":"/components/status-badge/index","ui-error-state":"/components/error-state/index"}}
|
||||||
"navigationBarTitleText": "善缘丹阳",
|
|
||||||
"navigationStyle": "custom",
|
|
||||||
"enablePullDownRefresh": true,
|
|
||||||
"backgroundTextStyle": "dark",
|
|
||||||
"backgroundColor": "#FAFAF8",
|
|
||||||
"usingComponents": {
|
|
||||||
"ui-section-header": "/components/section-header/index",
|
|
||||||
"ui-recommend-card": "/components/recommend-card/index",
|
|
||||||
"ui-empty": "/components/empty/index",
|
|
||||||
"ui-loading": "/components/loading/index",
|
|
||||||
"ui-status-badge": "/components/status-badge/index"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,214 +1,45 @@
|
|||||||
<!-- U18 首页 - V1.1 新版结构 -->
|
|
||||||
<view class="page">
|
<view class="page">
|
||||||
|
<!-- 品牌区 -->
|
||||||
<!-- ========== 品牌区 ========== -->
|
|
||||||
<view class="brand-bar">
|
<view class="brand-bar">
|
||||||
<view class="brand-info">
|
<view><text class="brand-name">善缘丹阳</text><text class="brand-slogan">真实相识 · 公益牵线</text></view>
|
||||||
<text class="brand-name">善缘丹阳</text>
|
<view class="brand-mark">公益服务</view>
|
||||||
<text class="brand-slogan">丹阳公益婚恋服务平台</text>
|
<view class="bell-btn" bindtap="onMessages"><text>消息</text><text wx:if="{{unreadCount}}" class="badge">{{unreadCount}}</text></view>
|
||||||
</view>
|
</view>
|
||||||
<view class="brand-actions">
|
<view wx:if="{{previewMode}}" class="preview-note">界面预览数据</view>
|
||||||
<view class="bell-btn" bindtap="onMessages">
|
|
||||||
<text class="bell-icon">🔔</text>
|
<!-- 用户状态条 -->
|
||||||
<block wx:if="{{unreadCount > 0}}">
|
<view class="status-strip">
|
||||||
<text class="badge">{{unreadCount > 9 ? '9+' : unreadCount}}</text>
|
<view wx:if="{{isOnboarding}}" class="status-main"><text class="status-title">完善资料,开启安心相遇</text><text class="status-desc">{{onboardingMessage}}</text><view class="status-action" bindtap="onEmptyAction" data-event="{{onboardingActions[0].event}}">{{onboardingActions[0].text}}</view></view>
|
||||||
</block>
|
<view wx:elif="{{isPaused}}" class="status-main"><text class="status-title">已暂停新的对象推荐</text><view class="status-action" bindtap="onResume">恢复寻找</view></view>
|
||||||
</view>
|
<view wx:elif="{{isSuccessful}}" class="status-main"><text class="status-title">祝福你们正在认真了解彼此</text><text class="status-desc">愿真诚与尊重陪伴你们继续前行</text></view>
|
||||||
</view>
|
<view wx:else class="status-checks"><text wx:for="{{statusChecks}}" wx:key="label" class="status-check {{item.done ? 'done' : ''}}">{{item.done ? '✓' : '○'}} {{item.label}}</text></view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- ========== 用户状态条 ========== -->
|
<!-- Banner 轮播 -->
|
||||||
<view class="status-strip" wx:if="{{userState}}">
|
<view class="banner-section"><swiper class="banner-swiper" indicator-dots autoplay circular interval="4500" indicator-active-color="#E96B6B"><swiper-item wx:for="{{banners}}" wx:key="id"><view class="banner-card" bindtap="onBannerTap" data-item="{{item}}"><image wx:if="{{item.imageFileId}}" src="{{item.imageFileId}}" mode="aspectFill" class="banner-img"/><view class="banner-art"><view class="heart-line">♡</view></view><view class="banner-copy"><text class="banner-title">{{item.title}}</text><text class="banner-subtitle">{{item.subtitle}}</text><view class="banner-btn">{{item.buttonText || '查看详情'}} <text>›</text></view></view></view></swiper-item></swiper></view>
|
||||||
<!-- 未完成准入 -->
|
|
||||||
<block wx:if="{{isOnboarding}}">
|
|
||||||
<view class="status-prompt">
|
|
||||||
<text class="prompt-text">{{onboardingMessage}}</text>
|
|
||||||
<view class="prompt-actions" wx:if="{{onboardingActions.length > 0}}">
|
|
||||||
<view
|
|
||||||
wx:for="{{onboardingActions}}"
|
|
||||||
wx:key="event"
|
|
||||||
class="prompt-btn {{item.variant}}"
|
|
||||||
bindtap="onEmptyAction"
|
|
||||||
data-event="{{item.event}}"
|
|
||||||
>{{item.text}}</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</block>
|
|
||||||
<!-- 已验证 -->
|
|
||||||
<block wx:elif="{{isVerified}}">
|
|
||||||
<view class="status-checks">
|
|
||||||
<text class="status-check {{item.done ? 'done' : 'pending'}}"
|
|
||||||
wx:for="{{statusChecks}}" wx:key="label">
|
|
||||||
{{item.done ? '✓' : '○'}} {{item.label}}
|
|
||||||
</text>
|
|
||||||
</view>
|
|
||||||
</block>
|
|
||||||
<!-- 已暂停 -->
|
|
||||||
<block wx:elif="{{isPaused}}">
|
|
||||||
<view class="paused-hint">
|
|
||||||
<text>你已暂停新的对象推荐</text>
|
|
||||||
<view class="resume-btn" bindtap="onResume">恢复寻找</view>
|
|
||||||
</view>
|
|
||||||
</block>
|
|
||||||
<!-- 已成功牵手 -->
|
|
||||||
<block wx:elif="{{isSuccessful}}">
|
|
||||||
<view class="successful-hint">
|
|
||||||
<text>祝福你们正在认真了解彼此</text>
|
|
||||||
</view>
|
|
||||||
</block>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- ========== Banner 轮播 ========== -->
|
<!-- 8个核心入口 -->
|
||||||
<view class="banner-section" wx:if="{{banners.length > 0}}">
|
<view class="entries-card"><view class="entries-grid"><view wx:for="{{quickEntries}}" wx:key="code" class="entry-item {{!item.enabled ? 'disabled' : ''}}" bindtap="onEntryTap" data-entry="{{item}}"><view class="entry-icon tone-{{item.tone}}">{{item.icon}}</view><text class="entry-label">{{item.label}}</text></view></view></view>
|
||||||
<swiper
|
|
||||||
class="banner-swiper"
|
|
||||||
indicator-dots
|
|
||||||
autoplay
|
|
||||||
circular
|
|
||||||
interval="4500"
|
|
||||||
indicator-active-color="#E96B6B"
|
|
||||||
bindchange="onBannerChange"
|
|
||||||
>
|
|
||||||
<swiper-item wx:for="{{banners}}" wx:key="id">
|
|
||||||
<view class="banner-card" bindtap="onBannerTap" data-item="{{item}}">
|
|
||||||
<image wx:if="{{item.imageFileId}}"
|
|
||||||
src="{{item.imageFileId}}"
|
|
||||||
mode="aspectFill"
|
|
||||||
class="banner-img" />
|
|
||||||
<view class="banner-overlay">
|
|
||||||
<text class="banner-title">{{item.title}}</text>
|
|
||||||
<text class="banner-subtitle" wx:if="{{item.subtitle}}">{{item.subtitle}}</text>
|
|
||||||
<view class="banner-btn" wx:if="{{item.buttonText}}">{{item.buttonText}}</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</swiper-item>
|
|
||||||
</swiper>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- ========== 8个核心入口 ========== -->
|
<view wx:if="{{loadError}}" class="section"><ui-error-state title="暂时无法加载首页" description="请检查网络后重试" bind:retry="onRetry"/></view>
|
||||||
<view class="entries-section">
|
<block wx:else>
|
||||||
<view class="entries-grid">
|
<!-- 今日推荐 -->
|
||||||
<view
|
<view class="section"><ui-section-header title="今日推荐" subtitle="根据双方条件,为你认真筛选" action="查看更多" bind:action="onSeeMore"/><ui-loading wx:if="{{loading}}" mode="skeleton" rows="2"/><view wx:elif="{{recommendations.length}}" wx:for="{{recommendations}}" wx:key="userId" class="rec-wrap"><ui-recommend-card recommendation="{{item}}" action-loading="{{actionLoadingUserId === item.userId}}" bind:view="onViewDetail" bind:like="onLike" bind:pass="onPass"/></view><ui-empty wx:else illustration="heart" title="暂时没有完全符合双方条件的人" description="可以调整择偶条件,或申请公益红娘协助推荐" actions="{{emptyActions}}" bind:action="onEmptyAction"/></view>
|
||||||
wx:for="{{quickEntries}}"
|
|
||||||
wx:key="code"
|
|
||||||
class="entry-item {{!item.enabled ? 'disabled' : ''}}"
|
|
||||||
bindtap="onEntryTap"
|
|
||||||
data-entry="{{item}}"
|
|
||||||
>
|
|
||||||
<view class="entry-icon-bg {{item.iconClass}}">
|
|
||||||
<text class="entry-icon">{{item.icon}}</text>
|
|
||||||
</view>
|
|
||||||
<text class="entry-label">{{item.label}}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- ========== 今日推荐 ========== -->
|
<!-- 善缘公益服务 -->
|
||||||
<view class="section" wx:if="{{recommendations.length > 0 || recState === 'loading'}}">
|
<view class="section"><ui-section-header title="善缘公益服务" subtitle="需要时,我们在这里陪伴你"/><view wx:for="{{serviceRecommendations}}" wx:key="type" class="service-card" bindtap="onServiceTap" data-type="{{item.type}}"><view><text class="service-title">{{item.title}}</text><text class="service-desc">{{item.description}}</text></view><view class="text-link">{{item.ctaText}} ›</view></view></view>
|
||||||
<ui-section-header title="今日推荐"
|
|
||||||
subtitle="根据双方条件,为你筛选更合适的人"
|
|
||||||
more="{{true}}"
|
|
||||||
bind:more="onSeeMore" />
|
|
||||||
<block wx:if="{{recState === 'loading' && recommendations.length === 0}}">
|
|
||||||
<view class="skeleton-card" wx:for="{{3}}" wx:key="index">
|
|
||||||
<view class="skeleton-avatar"></view>
|
|
||||||
<view class="skeleton-info">
|
|
||||||
<view class="skeleton-line w60"></view>
|
|
||||||
<view class="skeleton-line w40"></view>
|
|
||||||
<view class="skeleton-line w80"></view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</block>
|
|
||||||
<block wx:elif="{{recommendations.length > 0}}">
|
|
||||||
<view wx:for="{{recommendations}}" wx:key="userId" class="rec-card">
|
|
||||||
<ui-recommend-card
|
|
||||||
recommendation="{{item}}"
|
|
||||||
action-loading="{{actionLoadingUserId === item.userId}}"
|
|
||||||
bind:view="onViewDetail"
|
|
||||||
bind:like="onLike"
|
|
||||||
bind:pass="onPass"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
</block>
|
|
||||||
<view wx:if="{{recState === 'finished' && recommendations.length > 0}}" class="end-tip">
|
|
||||||
已展示今日全部推荐 · 明天再来看看
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- ========== 无推荐空状态 ========== -->
|
<!-- 近期公益活动 -->
|
||||||
<view class="section" wx:if="{{!isOnboarding && recommendations.length === 0 && recState !== 'loading'}}">
|
<view class="section"><ui-section-header title="近期公益活动" action="全部活动" bind:action="onSeeAllActivities"/><scroll-view scroll-x class="activity-scroll"><view wx:for="{{activities}}" wx:key="id" class="activity-card" bindtap="onActivityTap" data-id="{{item.id}}"><view class="activity-cover"><text class="activity-tag">{{item.typeLabel || '公益活动'}}</text><text class="activity-symbol">相遇</text></view><view class="activity-info"><text class="activity-title">{{item.title}}</text><text class="activity-meta">9月20日 14:00 · {{item.locationName}}</text></view></view></scroll-view></view>
|
||||||
<ui-empty
|
|
||||||
illustration="heart"
|
|
||||||
title="暂时没有找到完全符合双方条件的人"
|
|
||||||
description="你可以调整择偶条件,或申请公益红娘帮你推荐"
|
|
||||||
actions="{{emptyActions}}"
|
|
||||||
bind:action="onEmptyAction"
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- ========== 善缘公益服务 ========== -->
|
<!-- 善缘服务站 -->
|
||||||
<view class="section service-reco-section" wx:if="{{serviceRecommendations.length > 0}}">
|
<view class="section"><ui-section-header title="善缘服务站" subtitle="就在身边的公益婚恋服务阵地"/><view class="station-card" bindtap="onStationTap"><view class="station-pin">站</view><view class="station-info"><text class="station-name">{{serviceLocation.name}}</text><text class="station-desc">{{serviceLocation.shortName}}</text><view><text wx:for="{{serviceLocation.services}}" wx:key="*this" class="mini-tag">{{item}}</text></view></view><text class="arrow">›</text></view></view>
|
||||||
<ui-section-header title="善缘公益服务" />
|
|
||||||
<view wx:for="{{serviceRecommendations}}" wx:key="type"
|
|
||||||
class="service-card" bindtap="onServiceTap" data-type="{{item.type}}">
|
|
||||||
<view class="service-card-inner">
|
|
||||||
<text class="service-title">{{item.title}}</text>
|
|
||||||
<text class="service-desc">{{item.description}}</text>
|
|
||||||
<view class="service-cta">{{item.ctaText}}</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- ========== 近期公益活动 ========== -->
|
<!-- 婚恋课堂 -->
|
||||||
<view class="section activity-section" wx:if="{{activities.length > 0}}">
|
<view class="section"><ui-section-header title="婚恋课堂" subtitle="学会认识自己,也学会理解关系" action="更多内容" bind:action="onEntryTap" data-entry="{{quickEntries[4]}}"/><view class="article-list"><view wx:for="{{articles}}" wx:key="id" class="article-row" bindtap="onArticleTap" data-id="{{item.id}}"><view class="article-icon">读</view><view class="article-info"><text class="article-title">{{item.title}}</text><text class="article-meta">{{item.categoryName}} · {{item.readTime}}</text></view><text class="arrow">›</text></view></view></view>
|
||||||
<ui-section-header title="近期公益活动" more="{{true}}"
|
|
||||||
bind:more="onSeeAllActivities" />
|
|
||||||
<scroll-view scroll-x class="activity-scroll">
|
|
||||||
<view wx:for="{{activities}}" wx:key="id"
|
|
||||||
class="activity-h-card" bindtap="onActivityTap" data-id="{{item.id}}">
|
|
||||||
<image wx:if="{{item.coverFileId}}" src="{{item.coverFileId}}"
|
|
||||||
mode="aspectFill" class="activity-cover" />
|
|
||||||
<view class="activity-info">
|
|
||||||
<text class="activity-title">{{item.title}}</text>
|
|
||||||
<text class="activity-meta" wx:if="{{item.startAt}}">{{formatDate(item.startAt)}}</text>
|
|
||||||
<text class="activity-meta" wx:if="{{item.locationName}}">{{item.locationName}}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</scroll-view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- ========== 善缘服务站 ========== -->
|
<!-- 安全提醒 -->
|
||||||
<view class="section" wx:if="{{serviceLocation}}">
|
<view class="section safety-card" bindtap="onSafetyTap"><view class="shield">安</view><view class="safety-info"><text class="safety-title">善缘提醒</text><text class="safety-body">{{safetyNotice.text}}</text><text class="safety-link">{{safetyNotice.ctaText}} ›</text></view></view>
|
||||||
<view class="station-card" bindtap="onStationTap">
|
</block>
|
||||||
<view class="station-card-inner">
|
|
||||||
<view class="station-icon-area">
|
|
||||||
<text class="station-icon">📍</text>
|
|
||||||
</view>
|
|
||||||
<view class="station-info">
|
|
||||||
<text class="station-name">{{serviceLocation.name}}</text>
|
|
||||||
<text class="station-short">{{serviceLocation.shortName}}</text>
|
|
||||||
<view class="station-services">
|
|
||||||
<text wx:for="{{serviceLocation.services}}" wx:key="*this"
|
|
||||||
class="station-tag">{{item}}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
<text class="station-arrow">›</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- ========== 安全提醒 ========== -->
|
|
||||||
<view class="safety-notice">
|
|
||||||
<view class="safety-notice-inner">
|
|
||||||
<text class="safety-icon">🛡</text>
|
|
||||||
<view class="safety-text-area">
|
|
||||||
<text class="safety-title">善缘提醒</text>
|
|
||||||
<text class="safety-body">{{safetyNotice.text}}</text>
|
|
||||||
</view>
|
|
||||||
<text class="safety-cta" bindtap="onSafetyTap">{{safetyNotice.ctaText}}</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 底部安全区 -->
|
|
||||||
<view class="safe-area-bottom"></view>
|
<view class="safe-area-bottom"></view>
|
||||||
</view>
|
</view>
|
||||||
@@ -1,246 +1,14 @@
|
|||||||
/* U18 首页 - V1.1 新版样式 */
|
.page{min-height:100vh;background:var(--color-bg-page);padding-bottom:var(--spacing-8)}
|
||||||
.page {
|
.brand-bar{height:104rpx;padding:12rpx var(--spacing-page-x);display:flex;align-items:center;gap:16rpx;background:var(--color-bg-card);position:sticky;top:0;z-index:var(--z-sticky);border-bottom:1rpx solid var(--color-divider)}
|
||||||
min-height: 100vh;
|
.brand-bar>view:first-child{display:flex;flex-direction:column;flex:1}.brand-name{font-size:36rpx;font-weight:700;color:var(--color-primary);line-height:1.25}.brand-slogan{font-size:22rpx;color:var(--color-text-tertiary);margin-top:4rpx}.brand-mark{font-size:20rpx;color:var(--color-success);background:var(--color-success-light);padding:6rpx 12rpx;border-radius:var(--radius-full)}
|
||||||
background: #FAFAF8;
|
.bell-btn{position:relative;min-width:88rpx;height:88rpx;display:flex;align-items:center;justify-content:center;font-size:24rpx;color:var(--color-text-secondary)}.badge{position:absolute;right:0;top:8rpx;min-width:28rpx;height:28rpx;padding:0 4rpx;line-height:28rpx;text-align:center;font-size:18rpx;color:var(--color-text-inverse);background:var(--color-primary);border-radius:var(--radius-full)}
|
||||||
padding-bottom: 0;
|
.preview-note{text-align:center;background:var(--color-warning-light);color:var(--color-warning);font-size:20rpx;padding:6rpx}
|
||||||
}
|
.status-strip{margin:16rpx var(--spacing-page-x) 0;padding:20rpx 24rpx;background:var(--color-bg-card);border:1rpx solid var(--color-border);border-radius:var(--radius-md)}.status-main{display:flex;align-items:center;gap:12rpx;flex-wrap:wrap}.status-title{font-size:27rpx;font-weight:600}.status-desc{font-size:23rpx;color:var(--color-text-tertiary);flex:1}.status-action{min-height:64rpx;padding:0 22rpx;display:flex;align-items:center;color:var(--color-primary);font-size:24rpx;font-weight:600}.status-checks{display:flex;justify-content:space-between;gap:12rpx}.status-check{font-size:23rpx;color:var(--color-warning)}.status-check.done{color:var(--color-success)}
|
||||||
|
.banner-section{padding:16rpx var(--spacing-page-x) 0}.banner-swiper{height:292rpx;border-radius:var(--radius-lg);overflow:hidden}.banner-card{height:100%;position:relative;background:linear-gradient(135deg,var(--color-primary-lighter),#FBE8DE)}.banner-img{width:100%;height:100%}.banner-art{position:absolute;right:-24rpx;top:-28rpx;width:260rpx;height:260rpx;border-radius:50%;background:rgba(255,255,255,.55);display:flex;align-items:center;justify-content:center}.heart-line{font-size:128rpx;color:rgba(233,107,107,.34)}.banner-copy{position:absolute;left:28rpx;top:36rpx;width:72%}.banner-title{display:block;font-size:34rpx;font-weight:700;color:#6F3A35;line-height:1.35}.banner-subtitle{display:block;margin-top:12rpx;font-size:24rpx;color:#8B5E56}.banner-btn{display:inline-flex;align-items:center;gap:8rpx;margin-top:24rpx;min-height:64rpx;padding:0 24rpx;border-radius:var(--radius-full);background:var(--color-primary);color:var(--color-text-inverse);font-size:24rpx}
|
||||||
/* ========== 品牌区 ========== */
|
.entries-card{margin:20rpx var(--spacing-page-x) 0;padding:24rpx 12rpx;background:var(--color-bg-card);border-radius:var(--radius-lg);box-shadow:var(--shadow-card)}.entries-grid{display:grid;grid-template-columns:repeat(4,1fr);row-gap:24rpx}.entry-item{min-height:116rpx;display:flex;flex-direction:column;align-items:center;justify-content:center}.entry-item.disabled{opacity:.45}.entry-icon{width:76rpx;height:76rpx;display:flex;align-items:center;justify-content:center;border-radius:24rpx;font-size:28rpx;font-weight:700}.entry-label{margin-top:9rpx;font-size:23rpx;color:var(--color-text-primary)}.tone-rose{background:#FFF0EF;color:#D85D62}.tone-amber{background:#FFF7E6;color:#B77B19}.tone-green{background:#ECF8F2;color:#2E8A62}.tone-blue{background:#EEF5FF;color:#4A72B8}.tone-purple{background:#F5EFFB;color:#8865A7}.tone-orange{background:#FFF2E8;color:#B9723B}.tone-indigo{background:#EEF0FB;color:#5F6FAE}.tone-teal{background:#EAF7F5;color:#31867C}
|
||||||
.brand-bar {
|
.section{margin-top:var(--spacing-section);padding:0 var(--spacing-page-x)}.rec-wrap{margin-top:20rpx}.service-card{display:flex;align-items:center;gap:20rpx;padding:28rpx;background:linear-gradient(135deg,var(--color-primary-lighter),#FFF9F0);border:1rpx solid #F8E3DD;border-radius:var(--radius-lg)}.service-card>view:first-child{flex:1}.service-title{display:block;font-size:30rpx;font-weight:650}.service-desc{display:block;margin-top:10rpx;font-size:24rpx;color:var(--color-text-secondary);line-height:1.55}.text-link{font-size:23rpx;font-weight:600;color:var(--color-primary);white-space:nowrap}
|
||||||
display: flex;
|
.activity-scroll{white-space:nowrap;margin-top:18rpx;width:100%}.activity-card{display:inline-block;width:560rpx;margin-right:20rpx;background:var(--color-bg-card);border-radius:var(--radius-lg);overflow:hidden;box-shadow:var(--shadow-card);white-space:normal}.activity-cover{height:210rpx;padding:18rpx;background:linear-gradient(135deg,#F3D7CA,#F8ECE4);position:relative}.activity-tag{font-size:21rpx;color:#A45D50;background:rgba(255,255,255,.8);padding:7rpx 14rpx;border-radius:var(--radius-full)}.activity-symbol{position:absolute;right:28rpx;bottom:18rpx;font-size:52rpx;color:rgba(164,93,80,.32);font-weight:700}.activity-info{padding:22rpx}.activity-title{display:block;font-size:28rpx;font-weight:600}.activity-meta{display:block;margin-top:10rpx;font-size:23rpx;color:var(--color-text-tertiary)}
|
||||||
align-items: center;
|
.station-card{display:flex;align-items:center;gap:20rpx;padding:28rpx;background:var(--color-bg-card);border:1rpx solid var(--color-border);border-radius:var(--radius-lg)}.station-pin,.shield,.article-icon{width:72rpx;height:72rpx;flex:none;display:flex;align-items:center;justify-content:center;border-radius:22rpx;background:var(--color-success-light);color:var(--color-success);font-size:27rpx;font-weight:700}.station-info,.article-info,.safety-info{flex:1;min-width:0}.station-name{display:block;font-size:29rpx;font-weight:650}.station-desc{display:block;margin:7rpx 0 12rpx;font-size:23rpx;color:var(--color-text-tertiary)}.mini-tag{font-size:20rpx;color:var(--color-text-secondary);margin-right:10rpx;padding:5rpx 10rpx;background:var(--color-bg-page);border-radius:var(--radius-full)}.arrow{font-size:40rpx;color:var(--color-text-disabled)}
|
||||||
justify-content: space-between;
|
.article-list{margin-top:18rpx;background:var(--color-bg-card);border-radius:var(--radius-lg);overflow:hidden}.article-row{min-height:124rpx;padding:18rpx 24rpx;display:flex;align-items:center;gap:18rpx;border-bottom:1rpx solid var(--color-divider)}.article-row:last-child{border-bottom:0}.article-icon{background:#F5EFFB;color:#8865A7}.article-title{display:block;font-size:27rpx;font-weight:550;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.article-meta{display:block;margin-top:8rpx;font-size:22rpx;color:var(--color-text-tertiary)}
|
||||||
padding: 20rpx 24rpx 16rpx;
|
.safety-card{display:flex;gap:20rpx;margin-left:var(--spacing-page-x);margin-right:var(--spacing-page-x);padding:28rpx;background:#F3F7FC;border-radius:var(--radius-lg)}.shield{background:#E7EEF9;color:var(--color-info)}.safety-title{display:block;font-size:28rpx;font-weight:650}.safety-body{display:block;margin-top:8rpx;font-size:23rpx;color:var(--color-text-secondary);line-height:1.6}.safety-link{display:block;margin-top:12rpx;font-size:23rpx;color:var(--color-info);font-weight:600}
|
||||||
background: #fff;
|
@media (max-width:340px){.brand-mark{display:none}.entry-label{font-size:21rpx}.entry-icon{width:68rpx;height:68rpx}.banner-title{font-size:31rpx}.status-checks{flex-wrap:wrap;justify-content:flex-start}}
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-info { display: flex; flex-direction: column; }
|
|
||||||
|
|
||||||
.brand-name {
|
|
||||||
font-size: 36rpx;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #E96B6B;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-slogan {
|
|
||||||
font-size: 22rpx;
|
|
||||||
color: #888;
|
|
||||||
margin-top: 4rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bell-btn {
|
|
||||||
position: relative;
|
|
||||||
width: 64rpx; height: 64rpx;
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #f5f5f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bell-icon { font-size: 32rpx; }
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
position: absolute; top: -4rpx; right: -4rpx;
|
|
||||||
min-width: 28rpx; height: 28rpx; line-height: 28rpx;
|
|
||||||
padding: 0 6rpx;
|
|
||||||
background: #E96B6B; color: #fff;
|
|
||||||
font-size: 20rpx; border-radius: 14rpx;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ========== 用户状态条 ========== */
|
|
||||||
.status-strip {
|
|
||||||
background: #fff;
|
|
||||||
padding: 16rpx 24rpx;
|
|
||||||
border-bottom: 1rpx solid #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-checks { display: flex; gap: 24rpx; flex-wrap: wrap; }
|
|
||||||
|
|
||||||
.status-check {
|
|
||||||
font-size: 24rpx; color: #666;
|
|
||||||
}
|
|
||||||
.status-check.done { color: #2E7D32; }
|
|
||||||
.status-check.pending { color: #F57C00; }
|
|
||||||
|
|
||||||
.status-prompt { padding: 16rpx 0; }
|
|
||||||
|
|
||||||
.prompt-text {
|
|
||||||
font-size: 26rpx; color: #555;
|
|
||||||
display: block; margin-bottom: 16rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prompt-actions { display: flex; gap: 16rpx; }
|
|
||||||
|
|
||||||
.prompt-btn {
|
|
||||||
padding: 12rpx 28rpx; border-radius: 8rpx;
|
|
||||||
font-size: 26rpx; font-weight: 500;
|
|
||||||
}
|
|
||||||
.prompt-btn.primary { background: #E96B6B; color: #fff; }
|
|
||||||
.prompt-btn.secondary { background: #fff; color: #E96B6B; border: 1rpx solid #E96B6B; }
|
|
||||||
|
|
||||||
.paused-hint, .successful-hint { padding: 16rpx 0; text-align: center; }
|
|
||||||
.paused-hint text { font-size: 26rpx; color: #888; display: block; margin-bottom: 12rpx; }
|
|
||||||
.resume-btn {
|
|
||||||
display: inline-block; padding: 10rpx 32rpx;
|
|
||||||
background: #E96B6B; color: #fff;
|
|
||||||
border-radius: 8rpx; font-size: 26rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ========== Banner ========== */
|
|
||||||
.banner-section { padding: 16rpx 24rpx; }
|
|
||||||
|
|
||||||
.banner-swiper {
|
|
||||||
width: 100%; height: 340rpx;
|
|
||||||
border-radius: 16rpx; overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.banner-card { width: 100%; height: 100%; position: relative; }
|
|
||||||
.banner-img { width: 100%; height: 100%; object-fit: cover; }
|
|
||||||
|
|
||||||
.banner-overlay {
|
|
||||||
position: absolute; bottom: 0; left: 0; right: 0;
|
|
||||||
padding: 24rpx;
|
|
||||||
background: linear-gradient(transparent, rgba(0,0,0,0.6));
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.banner-title { font-size: 30rpx; font-weight: 600; display: block; margin-bottom: 4rpx; }
|
|
||||||
.banner-subtitle { font-size: 22rpx; opacity: 0.9; display: block; margin-bottom: 8rpx; }
|
|
||||||
.banner-btn {
|
|
||||||
display: inline-block; padding: 8rpx 20rpx;
|
|
||||||
background: rgba(255,255,255,0.25); border: 1rpx solid rgba(255,255,255,0.6);
|
|
||||||
border-radius: 20rpx; font-size: 22rpx; color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ========== 8个核心入口 ========== */
|
|
||||||
.entries-section {
|
|
||||||
background: #fff;
|
|
||||||
margin: 0 24rpx; border-radius: 16rpx;
|
|
||||||
padding: 24rpx 16rpx; margin-top: -8rpx;
|
|
||||||
position: relative; z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entries-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, 1fr);
|
|
||||||
gap: 16rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entry-item {
|
|
||||||
display: flex; flex-direction: column; align-items: center;
|
|
||||||
padding: 12rpx 0;
|
|
||||||
}
|
|
||||||
.entry-item.disabled { opacity: 0.5; }
|
|
||||||
|
|
||||||
.entry-icon-bg {
|
|
||||||
width: 72rpx; height: 72rpx;
|
|
||||||
border-radius: 16rpx;
|
|
||||||
display: flex; align-items: center; justify-content: center;
|
|
||||||
margin-bottom: 8rpx;
|
|
||||||
}
|
|
||||||
.entry-icon-bg.icon-dating { background: #FFF0F0; }
|
|
||||||
.entry-icon-bg.icon-matchmaker { background: #FFF8E6; }
|
|
||||||
.entry-icon-bg.icon-activity { background: #E8F5E9; }
|
|
||||||
.entry-icon-bg.icon-counseling { background: #E3F2FD; }
|
|
||||||
.entry-icon-bg.icon-class { background: #F3E5F5; }
|
|
||||||
.entry-icon-bg.icon-premarital { background: #FFF3E0; }
|
|
||||||
.entry-icon-bg.icon-shield { background: #E8EAF6; }
|
|
||||||
.entry-icon-bg.icon-location { background: #E0F2F1; }
|
|
||||||
|
|
||||||
.entry-icon { font-size: 32rpx; }
|
|
||||||
.entry-label { font-size: 22rpx; color: #333; text-align: center; line-height: 1.3; }
|
|
||||||
|
|
||||||
/* ========== 通用 Section ========== */
|
|
||||||
.section { margin-top: 24rpx; padding: 0 24rpx; }
|
|
||||||
|
|
||||||
/* ========== 推荐卡片 ========== */
|
|
||||||
.rec-card { margin-bottom: 16rpx; }
|
|
||||||
.end-tip { text-align: center; font-size: 24rpx; color: #999; padding: 16rpx 0; }
|
|
||||||
|
|
||||||
/* ========== 骨架屏 ========== */
|
|
||||||
.skeleton-card {
|
|
||||||
display: flex; align-items: center;
|
|
||||||
padding: 16rpx 0; margin-bottom: 12rpx;
|
|
||||||
}
|
|
||||||
.skeleton-avatar {
|
|
||||||
width: 88rpx; height: 112rpx;
|
|
||||||
background: #eee; border-radius: 8rpx; margin-right: 16rpx;
|
|
||||||
}
|
|
||||||
.skeleton-info { flex: 1; }
|
|
||||||
.skeleton-line {
|
|
||||||
height: 24rpx; background: #eee;
|
|
||||||
border-radius: 4rpx; margin-bottom: 8rpx;
|
|
||||||
}
|
|
||||||
.skeleton-line.w60 { width: 60%; }
|
|
||||||
.skeleton-line.w40 { width: 40%; }
|
|
||||||
.skeleton-line.w80 { width: 80%; }
|
|
||||||
|
|
||||||
/* ========== 善缘公益服务 ========== */
|
|
||||||
.service-card {
|
|
||||||
background: linear-gradient(135deg, #FFF8F0 0%, #FFF0F0 100%);
|
|
||||||
border-radius: 16rpx; padding: 24rpx; margin-bottom: 12rpx;
|
|
||||||
}
|
|
||||||
.service-card-inner { display: flex; flex-direction: column; gap: 8rpx; }
|
|
||||||
.service-title { font-size: 28rpx; font-weight: 600; color: #333; }
|
|
||||||
.service-desc { font-size: 24rpx; color: #666; line-height: 1.5; }
|
|
||||||
.service-cta {
|
|
||||||
display: inline-block; margin-top: 8rpx;
|
|
||||||
padding: 8rpx 20rpx; background: #E96B6B;
|
|
||||||
color: #fff; border-radius: 20rpx;
|
|
||||||
font-size: 24rpx; width: fit-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ========== 近期活动 ========== */
|
|
||||||
.activity-scroll { white-space: nowrap; margin: 0 -4rpx; }
|
|
||||||
.activity-h-card {
|
|
||||||
display: inline-block; width: 480rpx; margin: 0 4rpx 12rpx;
|
|
||||||
background: #fff; border-radius: 12rpx; overflow: hidden;
|
|
||||||
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06);
|
|
||||||
}
|
|
||||||
.activity-cover { width: 100%; height: 270rpx; background: #f0f0f0; }
|
|
||||||
.activity-info { padding: 12rpx 16rpx; }
|
|
||||||
.activity-title { font-size: 26rpx; font-weight: 600; color: #333; display: block; margin-bottom: 4rpx; }
|
|
||||||
.activity-meta { font-size: 22rpx; color: #888; display: block; }
|
|
||||||
|
|
||||||
/* ========== 善缘服务站 ========== */
|
|
||||||
.station-card {
|
|
||||||
background: #fff; border-radius: 16rpx; overflow: hidden;
|
|
||||||
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.06); margin-top: 24rpx;
|
|
||||||
}
|
|
||||||
.station-card-inner {
|
|
||||||
display: flex; align-items: center; padding: 24rpx; gap: 16rpx;
|
|
||||||
}
|
|
||||||
.station-icon-area {
|
|
||||||
width: 80rpx; height: 80rpx; background: #E8F5E9;
|
|
||||||
border-radius: 16rpx; display: flex; align-items: center;
|
|
||||||
justify-content: center; flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.station-icon { font-size: 40rpx; }
|
|
||||||
.station-info { flex: 1; min-width: 0; }
|
|
||||||
.station-name { font-size: 28rpx; font-weight: 600; color: #333; display: block; }
|
|
||||||
.station-short { font-size: 22rpx; color: #888; display: block; margin-top: 2rpx; }
|
|
||||||
.station-services { display: flex; flex-wrap: wrap; gap: 8rpx; margin-top: 8rpx; }
|
|
||||||
.station-tag {
|
|
||||||
font-size: 20rpx; padding: 2rpx 12rpx;
|
|
||||||
background: #F5F5F5; border-radius: 12rpx; color: #666;
|
|
||||||
}
|
|
||||||
.station-arrow { font-size: 36rpx; color: #ccc; flex-shrink: 0; }
|
|
||||||
|
|
||||||
/* ========== 安全提醒 ========== */
|
|
||||||
.safety-notice {
|
|
||||||
margin: 24rpx 24rpx 0;
|
|
||||||
background: #FFF9E6; border-radius: 12rpx;
|
|
||||||
padding: 20rpx 24rpx;
|
|
||||||
}
|
|
||||||
.safety-notice-inner { display: flex; align-items: flex-start; gap: 12rpx; }
|
|
||||||
.safety-icon { font-size: 32rpx; flex-shrink: 0; margin-top: 2rpx; }
|
|
||||||
.safety-text-area { flex: 1; }
|
|
||||||
.safety-title { font-size: 24rpx; font-weight: 600; color: #8B6914; display: block; margin-bottom: 4rpx; }
|
|
||||||
.safety-body { font-size: 22rpx; color: #8B6914; line-height: 1.5; display: block; }
|
|
||||||
.safety-cta { font-size: 24rpx; color: #E96B6B; flex-shrink: 0; padding-left: 12rpx; }
|
|
||||||
|
|
||||||
/* ========== 底部安全区 ========== */
|
|
||||||
.safe-area-bottom { height: env(safe-area-inset-bottom); }
|
|
||||||
@@ -36,6 +36,7 @@ Page({
|
|||||||
logoutAction: DANGER_ACTION.LOGOUT,
|
logoutAction: DANGER_ACTION.LOGOUT,
|
||||||
loginActions: [{ text: '微信登录', event: 'login', variant: 'primary' }],
|
loginActions: [{ text: '微信登录', event: 'login', variant: 'primary' }],
|
||||||
hasOperatorToken: false,
|
hasOperatorToken: false,
|
||||||
|
profileCompleteness: 78,
|
||||||
},
|
},
|
||||||
|
|
||||||
onLoad() {
|
onLoad() {
|
||||||
@@ -48,7 +49,6 @@ Page({
|
|||||||
|
|
||||||
onShow() {
|
onShow() {
|
||||||
// 检测红娘工作台登录状态
|
// 检测红娘工作台登录状态
|
||||||
this.setData({ hasOperatorToken: !!wx.getStorageSync('operatorToken') });
|
|
||||||
if (authStore.state.isLoggedIn) {
|
if (authStore.state.isLoggedIn) {
|
||||||
userStore.fetchStatus().catch((err) => appStore.toastError(err));
|
userStore.fetchStatus().catch((err) => appStore.toastError(err));
|
||||||
userStore.fetchProfile().catch(() => {});
|
userStore.fetchProfile().catch(() => {});
|
||||||
@@ -173,6 +173,16 @@ Page({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onGoFate() { wx.navigateTo({ url: ROUTE.MAIN.FATE_CENTER }); },
|
||||||
|
onGoMatchmaker() { wx.navigateTo({ url: ROUTE.MAIN.MATCHMAKER }); },
|
||||||
|
onGoCounseling() { wx.navigateTo({ url: ROUTE.SERVICE.COUNSELING }); },
|
||||||
|
onGoActivities() { wx.navigateTo({ url: ROUTE.DETAIL.MY_ACTIVITIES }); },
|
||||||
|
onGoAppointments() { wx.navigateTo({ url: ROUTE.SERVICE.MY_APPOINTMENTS }); },
|
||||||
|
onGoServiceHistory() { wx.navigateTo({ url: ROUTE.SERVICE.HISTORY }); },
|
||||||
|
onGoSafety() { wx.navigateTo({ url: ROUTE.SETTINGS.MY_REPORTS }); },
|
||||||
|
onGoClassroom() { wx.navigateTo({ url: ROUTE.SERVICE.CLASSROOM }); },
|
||||||
|
onGoAntiFraud() { wx.navigateTo({ url: ROUTE.SERVICE.ANTI_FRAUD }); },
|
||||||
|
onGoStation() { wx.navigateTo({ url: ROUTE.SERVICE.STATION }); },
|
||||||
// ============ 其他 ============
|
// ============ 其他 ============
|
||||||
|
|
||||||
onGoSettings() {
|
onGoSettings() {
|
||||||
|
|||||||
@@ -1,170 +1,22 @@
|
|||||||
<!-- 我的页 U30-U33(重构:userStore 派生状态 + ui-section-header) -->
|
|
||||||
<view class="page">
|
<view class="page">
|
||||||
<!-- 未登录 -->
|
<ui-empty wx:if="{{!isLoggedIn}}" illustration="default" title="登录后查看我的服务" description="管理资料、缘分与公益服务记录" actions="{{loginActions}}" bind:action="onLoginAction"/>
|
||||||
<ui-empty
|
<ui-loading wx:elif="{{loadStatus === 'loading' && !profile}}" mode="skeleton" rows="6"/>
|
||||||
wx:if="{{!isLoggedIn}}"
|
|
||||||
illustration="default"
|
|
||||||
title="未登录"
|
|
||||||
description="登录后可管理资料、查看匹配与消息。"
|
|
||||||
actions="{{loginActions}}"
|
|
||||||
bind:action="onLoginAction"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 加载中(首次且无缓存资料) -->
|
|
||||||
<ui-loading
|
|
||||||
wx:elif="{{loadStatus === 'loading' && !profile}}"
|
|
||||||
mode="skeleton"
|
|
||||||
rows="6"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- 已登录:个人中心 -->
|
|
||||||
<block wx:else>
|
<block wx:else>
|
||||||
<!-- 个人资料卡 -->
|
<view class="profile-card"><ui-avatar src="{{profile.avatarUrl || ''}}" size="xl" default-text="{{profile.displayName || ''}}" verified="{{isRealNameVerified}}" reviewed="{{isProfileApproved}}"/><view class="profile-info"><view class="profile-name">{{profile.displayName || '未填写昵称'}}</view><view class="profile-complete">资料完整度 {{profileCompleteness}}%</view><view class="profile-progress"><view style="width:{{profileCompleteness}}%"></view></view><view class="profile-badges"><ui-status-badge kind="{{isRealNameVerified ? 'success' : 'warning'}}" label="{{isRealNameVerified ? '已实名' : '未实名'}}"/><ui-status-badge kind="success" label="本地资格"/><ui-status-badge kind="{{isProfileApproved ? 'success' : 'warning'}}" label="{{isProfileApproved ? '资料已审核' : '资料待审核'}}"/></view></view></view>
|
||||||
<view class="profile-card">
|
|
||||||
<ui-avatar
|
|
||||||
src="{{profile.avatarUrl || ''}}"
|
|
||||||
size="xl"
|
|
||||||
default-text="{{profile.displayName || ''}}"
|
|
||||||
verified="{{isRealNameVerified}}"
|
|
||||||
reviewed="{{isProfileApproved}}"
|
|
||||||
/>
|
|
||||||
<view class="profile-info">
|
|
||||||
<view class="profile-name">{{profile.displayName || '未填写昵称'}}</view>
|
|
||||||
<view class="profile-meta">{{profile.regionName || '丹阳'}}</view>
|
|
||||||
<view class="profile-badges">
|
|
||||||
<ui-status-badge
|
|
||||||
wx:if="{{isRealNameVerified}}"
|
|
||||||
kind="success"
|
|
||||||
label="已实名"
|
|
||||||
icon
|
|
||||||
/>
|
|
||||||
<ui-status-badge
|
|
||||||
wx:elif="{{verificationStatus === 'pending'}}"
|
|
||||||
kind="warning"
|
|
||||||
label="认证中"
|
|
||||||
icon
|
|
||||||
/>
|
|
||||||
<ui-status-badge
|
|
||||||
wx:else
|
|
||||||
kind="disabled"
|
|
||||||
label="未实名"
|
|
||||||
icon
|
|
||||||
/>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 婚恋状态 -->
|
<ui-section-header title="我的" subtitle="资料与缘分"/>
|
||||||
<ui-section-header title="婚恋状态" subtitle="{{relationshipText}}" />
|
<view class="list-group"><view class="list-row" bindtap="onEditProfile"><text class="list-label">我的资料</text><text class="list-value">{{reviewStatusText}}</text><text class="list-arrow">›</text></view><view class="list-row" bindtap="onEditPreference"><text class="list-label">择偶条件</text><text class="list-arrow">›</text></view><view class="list-row" bindtap="onGoFate"><text class="list-label">我的缘分</text><text class="list-arrow">›</text></view></view>
|
||||||
<view class="action-row">
|
|
||||||
<ui-button
|
|
||||||
wx:if="{{canPause}}"
|
|
||||||
variant="secondary"
|
|
||||||
size="md"
|
|
||||||
block
|
|
||||||
bind:tap="onPause"
|
|
||||||
>暂停寻找</ui-button>
|
|
||||||
<ui-button
|
|
||||||
wx:if="{{canResume}}"
|
|
||||||
variant="primary"
|
|
||||||
size="md"
|
|
||||||
block
|
|
||||||
bind:tap="onResume"
|
|
||||||
>恢复寻找</ui-button>
|
|
||||||
<view wx:if="{{!canPause && !canResume}}" class="action-hint">
|
|
||||||
<text>当前状态不支持切换</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 我的资料 -->
|
<ui-section-header title="公益服务"/>
|
||||||
<ui-section-header title="我的资料" subtitle="{{reviewStatusText}}" />
|
<view class="list-group"><view class="list-row" bindtap="onGoMatchmaker"><text class="list-label">我的红娘</text><text class="list-arrow">›</text></view><view class="list-row" bindtap="onGoCounseling"><text class="list-label">我的咨询</text><text class="list-arrow">›</text></view><view class="list-row" bindtap="onGoActivities"><text class="list-label">我的活动</text><text class="list-arrow">›</text></view><view class="list-row" bindtap="onGoAppointments"><text class="list-label">我的预约</text><text class="list-arrow">›</text></view><view class="list-row" bindtap="onGoServiceHistory"><text class="list-label">公益服务记录</text><text class="list-arrow">›</text></view></view>
|
||||||
<view class="list-group">
|
|
||||||
<view class="list-row" bindtap="onEditProfile">
|
|
||||||
<text class="list-label">编辑基本资料</text>
|
|
||||||
<text class="list-arrow">›</text>
|
|
||||||
</view>
|
|
||||||
<view class="list-row" bindtap="onEditPreference">
|
|
||||||
<text class="list-label">择偶条件</text>
|
|
||||||
<text class="list-arrow">›</text>
|
|
||||||
</view>
|
|
||||||
<view class="list-row" bindtap="onEditPhotos">
|
|
||||||
<text class="list-label">生活照管理</text>
|
|
||||||
<text class="list-arrow">›</text>
|
|
||||||
</view>
|
|
||||||
<view class="list-row" bindtap="onGoAccountStatus">
|
|
||||||
<text class="list-label">审核与账号状态</text>
|
|
||||||
<text class="list-arrow">›</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 我的匹配 -->
|
<ui-section-header title="安全与隐私"/>
|
||||||
<ui-section-header title="我的匹配" />
|
<view class="list-group"><view class="list-row" bindtap="onGoPrivacy"><text class="list-label">隐私设置</text><text class="list-arrow">›</text></view><view class="list-row" bindtap="onGoBlacklist"><text class="list-label">黑名单</text><text class="list-arrow">›</text></view><view class="list-row" bindtap="onGoSafety"><text class="list-label">举报与安全</text><text class="list-arrow">›</text></view><view class="list-row" bindtap="onGoSettings"><text class="list-label">账号设置</text><text class="list-arrow">›</text></view></view>
|
||||||
<view class="list-group">
|
|
||||||
<view class="list-row" bindtap="onGoMessages">
|
|
||||||
<text class="list-label">互相喜欢</text>
|
|
||||||
<text class="list-arrow">›</text>
|
|
||||||
</view>
|
|
||||||
<view class="list-row" bindtap="onGoLikers">
|
|
||||||
<text class="list-label">喜欢我的人</text>
|
|
||||||
<text class="list-value">{{likersCount > 0 ? likersCount + ' 人' : ''}}</text>
|
|
||||||
<text class="list-arrow">›</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 账户与安全 -->
|
<ui-section-header title="公益信息"/>
|
||||||
<ui-section-header title="账户与安全" />
|
<view class="list-group"><view class="list-row" bindtap="onGoClassroom"><text class="list-label">婚恋课堂</text><text class="list-arrow">›</text></view><view class="list-row" bindtap="onGoAntiFraud"><text class="list-label">婚恋反诈</text><text class="list-arrow">›</text></view><view class="list-row" bindtap="onGoStation"><text class="list-label">善缘服务站</text><text class="list-arrow">›</text></view><view class="list-row" bindtap="onGoAbout"><text class="list-label">关于善缘丹阳</text><text class="list-arrow">›</text></view></view>
|
||||||
<view class="list-group">
|
|
||||||
<view class="list-row" bindtap="onGoVerification">
|
|
||||||
<text class="list-label">实名认证</text>
|
|
||||||
<text class="list-value">{{isRealNameVerified ? '已认证' : (verificationStatus === 'pending' ? '认证中' : '未认证')}}</text>
|
|
||||||
<text class="list-arrow">›</text>
|
|
||||||
</view>
|
|
||||||
<view class="list-row" bindtap="onGoPrivacy">
|
|
||||||
<text class="list-label">隐私设置</text>
|
|
||||||
<text class="list-arrow">›</text>
|
|
||||||
</view>
|
|
||||||
<view class="list-row" bindtap="onGoBlacklist">
|
|
||||||
<text class="list-label">黑名单</text>
|
|
||||||
<text class="list-arrow">›</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 其他 -->
|
<view class="list-group"><view class="list-row danger" bindtap="onLogout"><text class="list-label">退出登录</text></view></view>
|
||||||
<ui-section-header title="其他" />
|
<ui-danger-action-dialog visible="{{showLogoutDialog}}" action="{{logoutAction}}" bind:confirm="onConfirmLogout" bind:cancel="onCancelLogout"/>
|
||||||
<view class="list-group">
|
|
||||||
<view class="list-row" bindtap="onGoSettings">
|
|
||||||
<text class="list-label">设置</text>
|
|
||||||
<text class="list-arrow">›</text>
|
|
||||||
</view>
|
|
||||||
<view class="list-row" bindtap="onGoHelp">
|
|
||||||
<text class="list-label">帮助中心</text>
|
|
||||||
<text class="list-arrow">›</text>
|
|
||||||
</view>
|
|
||||||
<view class="list-row" bindtap="onGoAbout">
|
|
||||||
<text class="list-label">关于我们</text>
|
|
||||||
<text class="list-arrow">›</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 红娘工作台独立入口按钮 -->
|
|
||||||
<view class="workbench-btn" bindtap="onSwitchRole">
|
|
||||||
<text class="workbench-icon">👩</text>
|
|
||||||
<text class="workbench-text">{{hasOperatorToken ? '进入红娘工作台' : '红娘工作台登录'}}</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 退出登录 -->
|
|
||||||
<view class="list-group" style="margin-top: 24rpx;">
|
|
||||||
<view class="list-row danger" bindtap="onLogout">
|
|
||||||
<text class="list-label">退出登录</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- 退出登录确认 -->
|
|
||||||
<ui-danger-action-dialog
|
|
||||||
visible="{{showLogoutDialog}}"
|
|
||||||
action="{{logoutAction}}"
|
|
||||||
bind:confirm="onConfirmLogout"
|
|
||||||
bind:cancel="onCancelLogout"
|
|
||||||
/>
|
|
||||||
</block>
|
</block>
|
||||||
</view>
|
</view>
|
||||||
@@ -98,31 +98,4 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============ 红娘工作台入口按钮 ============ */
|
.profile-complete{font-size:22rpx;color:var(--color-text-tertiary);margin-top:4rpx}.profile-progress{height:8rpx;margin:10rpx 0 14rpx;border-radius:999rpx;background:var(--color-divider);overflow:hidden}.profile-progress view{height:100%;background:var(--color-primary);border-radius:999rpx}.list-row{min-height:88rpx;box-sizing:border-box}.page ui-section-header{display:block;padding:0 var(--spacing-page-x)}
|
||||||
.workbench-btn {
|
|
||||||
margin: 32rpx var(--spacing-page-x);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 12rpx;
|
|
||||||
background: linear-gradient(135deg, #E96B6B 0%, #ff9a9a 100%);
|
|
||||||
color: #fff;
|
|
||||||
height: 88rpx;
|
|
||||||
border-radius: 44rpx;
|
|
||||||
font-size: 30rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
box-shadow: 0 4rpx 16rpx rgba(233, 107, 107, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
.workbench-btn:active {
|
|
||||||
opacity: 0.85;
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
|
||||||
|
|
||||||
.workbench-icon {
|
|
||||||
font-size: 36rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.workbench-text {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Page({data:{appointments:[{id:'1',title:'资料填写协助',date:'2026-09-12 14:30',station:'善缘丹阳公益服务站',status:'已确认'}]},cancel(){wx.showModal({title:'取消预约',content:'确定取消本次服务预约吗?',confirmText:'确认取消',confirmColor:'#D94A4A'})}})
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"navigationBarTitleText":"我的预约","backgroundColor":"#FAFAF8"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<view class="service-page"><view class="hero"><text class="eyebrow">我的公益服务</text><text class="hero-title">我的预约</text><text class="hero-desc">查看预约状态、到站时间与服务地点。</text></view><view class="content"><view wx:if="{{appointments.length}}" class="block"><view wx:for="{{appointments}}" wx:key="id" class="card"><view style="display:flex;justify-content:space-between"><text class="feature-title">{{item.title}}</text><text class="tag">{{item.status}}</text></view><text class="meta">{{item.date}}</text><text class="meta">{{item.station}}</text><view class="secondary-btn" style="margin-top:24rpx" bindtap="cancel">取消预约</view></view></view><view wx:else class="card empty-panel"><view class="empty-symbol">约</view><text class="empty-title">还没有服务预约</text><text class="meta">需要线下帮助时,可以预约善缘服务站。</text></view></view></view>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@import "../../styles/service-page.wxss";
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { request } from '../../utils/requestPromise'
|
||||||
const app = getApp<{ globalData: { apiBase: string; token: string } }>();
|
const app = getApp<{ globalData: { apiBase: string; token: string } }>();
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
@@ -8,7 +9,7 @@ Page({
|
|||||||
|
|
||||||
async loadList() {
|
async loadList() {
|
||||||
try {
|
try {
|
||||||
const res = await wx.request({
|
const res = await request<any>({
|
||||||
url: `${app.globalData.apiBase}/counseling/services`,
|
url: `${app.globalData.apiBase}/counseling/services`,
|
||||||
header: { 'Authorization': `Bearer ${app.globalData.token}` }
|
header: { 'Authorization': `Bearer ${app.globalData.token}` }
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,20 +1 @@
|
|||||||
<view class="container">
|
<view class="service-page"><view class="hero"><text class="eyebrow">U58 婚前辅导</text><text class="hero-title">在进入婚姻前,把重要的事认真谈清楚</text><text class="hero-desc">帮助双方讨论共同生活、家庭责任与沟通方式,可邀请伴侣共同参与。</text></view><view class="content"><view class="block"><text class="block-title">辅导内容</text><view class="card"><view class="feature-row"><view class="feature-icon">沟</view><view class="feature-copy"><text class="feature-title">沟通与期望</text><text class="feature-desc">表达需求、倾听差异,形成可以共同遵守的约定</text></view></view><view class="feature-row"><view class="feature-icon">家</view><view class="feature-copy"><text class="feature-title">家庭与责任</text><text class="feature-desc">讨论家庭角色、财务安排与双方家庭关系</text></view></view><view class="feature-row"><view class="feature-icon">伴</view><view class="feature-copy"><text class="feature-title">共同生活</text><text class="feature-desc">对生活方式、未来计划和冲突处理建立共识</text></view></view></view></view><view class="notice">双方共同参与效果更好,但必须基于自愿,不会因为一方申请而自动通知或要求另一方参加。</view></view><view class="fixed-action"><view class="primary-btn" bindtap="onApply">申请婚前辅导</view></view></view>
|
||||||
<view class="header">
|
|
||||||
<text class="title">婚前辅导</text>
|
|
||||||
<text class="subtitle">公益免费 · 可邀请伴侣共同参与</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="info-card">
|
|
||||||
<text class="info-title">服务内容</text>
|
|
||||||
<text class="info-item">• 婚前沟通与期望管理</text>
|
|
||||||
<text class="info-item">• 家庭角色与责任规划</text>
|
|
||||||
<text class="info-item">• 财务与生活方式协商</text>
|
|
||||||
<text class="info-item">• 双方家庭关系处理</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<view class="companion-note">
|
|
||||||
<text>💑 可邀请伴侣一同参与,效果更佳</text>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<button class="apply-btn" bindtap="onApply">申请婚前辅导</button>
|
|
||||||
</view>
|
|
||||||
@@ -1,10 +1 @@
|
|||||||
.container { padding: 24rpx; }
|
@import "../../styles/service-page.wxss";
|
||||||
.header { text-align: center; margin-bottom: 32rpx; }
|
|
||||||
.title { font-size: 36rpx; font-weight: bold; color: #333; }
|
|
||||||
.subtitle { font-size: 26rpx; color: #888; margin-top: 8rpx; display: block; }
|
|
||||||
.info-card { background: #fff; border-radius: 16rpx; padding: 28rpx; margin-bottom: 24rpx; }
|
|
||||||
.info-title { font-size: 30rpx; font-weight: bold; color: #333; display: block; margin-bottom: 16rpx; }
|
|
||||||
.info-item { font-size: 26rpx; color: #666; display: block; line-height: 2.2; }
|
|
||||||
.companion-note { background: #fff0f5; border-radius: 12rpx; padding: 20rpx; text-align: center; margin-bottom: 32rpx; }
|
|
||||||
.companion-note text { font-size: 26rpx; color: #c62828; }
|
|
||||||
.apply-btn { background: linear-gradient(135deg, #FF6B6B, #EE5A24); color: #fff; border: none; border-radius: 44rpx; font-size: 32rpx; }
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
export {}
|
||||||
const app = getApp<{ globalData: { apiBase: string; token: string } }>();
|
const app = getApp<{ globalData: { apiBase: string; token: string } }>();
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Page({data:{date:'',service:'请选择服务项目',services:['资料填写协助','公益红娘咨询','活动报名指导','婚恋咨询预约'],note:''},chooseService(e){this.setData({service:this.data.services[Number(e.detail.value)]})},chooseDate(e){this.setData({date:e.detail.value})},submit(){if(!this.data.date)return wx.showToast({title:'请选择预约日期',icon:'none'});wx.showModal({title:'预约已提交',content:'工作人员确认后会通过平台通知你。',showCancel:false,success(){wx.navigateTo({url:'/pages/my-appointments/my-appointments'})}})}})
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"navigationBarTitleText":"服务预约","backgroundColor":"#FAFAF8"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<view class="service-page"><view class="hero"><text class="eyebrow">U51 服务预约</text><text class="hero-title">预约到站服务</text><text class="hero-desc">选择合适的时间和服务项目,提交后等待工作人员确认。</text></view><view class="content"><view class="card"><view class="field"><text class="label">服务项目</text><picker range="{{services}}" bindchange="chooseService"><view class="select-box">{{service}}<text class="arrow" style="margin-left:auto">›</text></view></picker></view><view class="field"><text class="label">预约日期</text><picker mode="date" bindchange="chooseDate"><view class="select-box">{{date || '请选择日期'}}</view></picker></view><view class="field"><text class="label">希望沟通的事项(选填)</text><textarea class="textarea" placeholder="请简要说明,避免填写身份证号等敏感信息" maxlength="200"/></view><view class="notice">提交预约不代表已确认。工作人员确认后,预约状态会更新为“已确认”。</view></view></view><view class="fixed-action"><view class="primary-btn" bindtap="submit">提交预约</view></view></view>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@import "../../styles/service-page.wxss";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Page({data:{},go(e){wx.navigateTo({url:e.currentTarget.dataset.url})}})
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"navigationBarTitleText":"公益服务中心","backgroundColor":"#FAFAF8"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<view class="service-page"><view class="hero"><text class="eyebrow">善缘公益服务</text><text class="hero-title">需要帮助时,我们在这里</text><text class="hero-desc">从线下服务站到专业咨询,所有服务尊重个人意愿与隐私。</text></view><view class="content"><view class="block"><text class="block-title">选择需要的服务</text><text class="block-subtitle">每项服务均清晰说明范围与流程</text><view class="card"><view class="feature-row" data-url="/pages/service-station/service-station" bindtap="go"><view class="feature-icon">站</view><view class="feature-copy"><text class="feature-title">善缘服务站</text><text class="feature-desc">线下咨询、资料协助与活动服务</text></view><text class="arrow">›</text></view><view class="feature-row" data-url="/pages/counseling-intro/counseling-intro" bindtap="go"><view class="feature-icon">咨</view><view class="feature-copy"><text class="feature-title">婚恋咨询</text><text class="feature-desc">由咨询人员提供关系与情绪支持</text></view><text class="arrow">›</text></view><view class="feature-row" data-url="/pages/premarital-intro/premarital-intro" bindtap="go"><view class="feature-icon">伴</view><view class="feature-copy"><text class="feature-title">婚前辅导</text><text class="feature-desc">帮助伴侣认真讨论共同生活</text></view><text class="arrow">›</text></view><view class="feature-row" data-url="/pages/service-program/service-program" bindtap="go"><view class="feature-icon">益</view><view class="feature-copy"><text class="feature-title">专项公益服务</text><text class="feature-desc">面向特定群体的公益支持项目</text></view><text class="arrow">›</text></view></view></view></view></view>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@import "../../styles/service-page.wxss";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Page({data:{records:[{title:'善缘服务站 · 资料协助',date:'2026-08-28',status:'已完成'},{title:'青年公益交友活动',date:'2026-08-16',status:'已参加'},{title:'公益红娘服务申请',date:'2026-08-03',status:'服务中'}]}})
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"navigationBarTitleText":"公益服务记录","backgroundColor":"#FAFAF8"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<view class="service-page"><view class="hero"><text class="eyebrow">U64 公益服务记录</text><text class="hero-title">每一次服务,都清楚可查</text><text class="hero-desc">这里只展示与你有关的公益服务事实,不展示咨询正文。</text></view><view class="content"><view class="block"><view class="card"><view wx:for="{{records}}" wx:key="title" class="record-row"><view class="feature-icon">记</view><view class="feature-copy"><text class="feature-title">{{item.title}}</text><text class="feature-desc">{{item.date}}</text></view><text class="tag">{{item.status}}</text></view></view></view><view class="notice">咨询与公益红娘记录彼此分离;工作人员只能查看职责范围内的信息。</view></view></view>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@import "../../styles/service-page.wxss";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Page({data:{},apply(){wx.showModal({title:'申请说明',content:'请先完成基本资料,符合项目条件后工作人员会与你联系。',showCancel:false})}})
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"navigationBarTitleText":"专项公益服务","backgroundColor":"#FAFAF8"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<view class="service-page"><view class="hero"><text class="eyebrow">专项公益服务</text><text class="hero-title">让需要帮助的人被认真看见</text><text class="hero-desc">针对特定群体提供阶段性公益支持,项目条件与服务边界公开透明。</text></view><view class="content"><view class="block"><text class="block-title">青年人才公益交友支持</text><view class="card"><text class="feature-title">服务内容</text><text class="feature-desc">主题活动、资料指导、公益红娘转介与关系成长课堂。</text><text class="feature-title" style="margin-top:24rpx">申请条件</text><text class="feature-desc">以项目当期公布的年龄、工作或居住条件为准,不承诺匹配结果。</text><view><text class="tag">公益免费</text><text class="tag">自愿申请</text><text class="tag">隐私保护</text></view></view></view><view class="notice">项目主办或协作关系不会自动获得用户资料访问权限。</view></view><view class="fixed-action"><view class="primary-btn" bindtap="apply">了解申请方式</view></view></view>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@import "../../styles/service-page.wxss";
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Page({data:{services:['资料填写协助','公益红娘咨询','活动报名指导','婚恋咨询预约']},call(){wx.showModal({title:'联系服务站',content:'服务热线:0511-00000000',showCancel:false})},book(){wx.navigateTo({url:'/pages/service-appointment/service-appointment'})}})
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"navigationBarTitleText":"善缘服务站","backgroundColor":"#FAFAF8"}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<view class="service-page"><view class="hero"><text class="eyebrow">固定公益服务阵地</text><text class="hero-title">善缘丹阳公益服务站</text><text class="hero-desc">提供面对面的婚恋服务咨询,让线上服务在丹阳有真实落点。</text></view><view class="content"><view class="block"><text class="block-title">服务站信息</text><view class="card"><view class="feature-row"><view class="feature-icon">址</view><view class="feature-copy"><text class="feature-title">丹阳市城区公益服务中心</text><text class="feature-desc">丹阳市云阳街道(具体地址以预约确认为准)</text></view></view><view class="feature-row"><view class="feature-icon">时</view><view class="feature-copy"><text class="feature-title">周二至周日 09:00–17:30</text><text class="feature-desc">节假日开放时间以平台通知为准</text></view></view></view></view><view class="block"><text class="block-title">可预约服务</text><view class="card"><view wx:for="{{services}}" wx:key="*this" class="menu-row"><view class="feature-icon">{{index+1}}</view><text class="feature-title" style="flex:1">{{item}}</text><text class="arrow">›</text></view></view></view><view class="notice">服务站人员仅处理现场预约与指定服务,不会浏览无关用户资料。</view></view><view class="fixed-action"><view class="primary-btn" bindtap="book">预约到站服务</view></view></view>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
@import "../../styles/service-page.wxss";
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
@import "../../app.wxss";
|
||||||
|
page{background:var(--color-bg-page)}.service-page{min-height:100vh;padding-bottom:calc(140rpx + env(safe-area-inset-bottom))}.hero{padding:44rpx 32rpx 54rpx;background:linear-gradient(140deg,var(--color-primary-lighter),#FFF8EE);position:relative;overflow:hidden}.hero:after{content:'缘';position:absolute;right:24rpx;top:-34rpx;font-size:180rpx;font-weight:700;color:rgba(233,107,107,.08)}.eyebrow{display:inline-flex;padding:7rpx 14rpx;border-radius:999rpx;background:rgba(255,255,255,.72);color:var(--color-primary);font-size:21rpx}.hero-title{display:block;margin-top:18rpx;font-size:40rpx;font-weight:700;line-height:1.35}.hero-desc{display:block;width:82%;margin-top:14rpx;font-size:25rpx;color:var(--color-text-secondary);line-height:1.7}.content{padding:0 var(--spacing-page-x)}.block{margin-top:var(--spacing-section)}.block-title{display:block;font-size:31rpx;font-weight:650}.block-subtitle{display:block;margin-top:8rpx;font-size:23rpx;color:var(--color-text-tertiary)}.card{margin-top:18rpx;padding:28rpx;background:var(--color-bg-card);border:1rpx solid var(--color-border);border-radius:var(--radius-lg);box-shadow:var(--shadow-card)}.feature-row,.menu-row,.record-row{display:flex;align-items:center;gap:20rpx;min-height:112rpx;padding:18rpx 0;border-bottom:1rpx solid var(--color-divider)}.feature-row:last-child,.menu-row:last-child,.record-row:last-child{border-bottom:0}.feature-icon{width:72rpx;height:72rpx;display:flex;align-items:center;justify-content:center;flex:none;border-radius:22rpx;background:var(--color-primary-light);color:var(--color-primary);font-weight:700}.feature-copy{flex:1;min-width:0}.feature-title{display:block;font-size:28rpx;font-weight:600}.feature-desc,.meta{display:block;margin-top:7rpx;font-size:23rpx;color:var(--color-text-tertiary);line-height:1.55}.arrow{font-size:40rpx;color:var(--color-text-disabled)}.tag{display:inline-flex;margin-right:10rpx;margin-top:10rpx;padding:6rpx 13rpx;border-radius:999rpx;background:var(--color-success-light);color:var(--color-success);font-size:20rpx}.tag.warning{background:var(--color-warning-light);color:var(--color-warning)}.primary-btn,.secondary-btn{height:88rpx;display:flex;align-items:center;justify-content:center;border-radius:44rpx;font-size:28rpx;font-weight:600}.primary-btn{background:var(--color-primary);color:var(--color-text-inverse)}.secondary-btn{background:var(--color-primary-light);color:var(--color-primary)}.fixed-action{position:fixed;left:0;right:0;bottom:0;padding:18rpx var(--spacing-page-x) calc(18rpx + env(safe-area-inset-bottom));background:rgba(255,255,255,.96);border-top:1rpx solid var(--color-divider);z-index:100}.field{margin-top:26rpx}.label{display:block;margin-bottom:12rpx;font-size:25rpx;font-weight:600}.input,.select-box,.textarea{width:100%;box-sizing:border-box;background:#FCFAF8;border:1rpx solid var(--color-border);border-radius:var(--radius-md);font-size:26rpx}.input,.select-box{height:88rpx;padding:0 24rpx;display:flex;align-items:center}.textarea{min-height:180rpx;padding:22rpx}.notice{margin-top:20rpx;padding:22rpx;border-radius:var(--radius-md);background:#F3F7FC;color:var(--color-text-secondary);font-size:23rpx;line-height:1.65}.empty-panel{text-align:center;padding:70rpx 30rpx}.empty-symbol{width:96rpx;height:96rpx;margin:auto;display:flex;align-items:center;justify-content:center;border-radius:30rpx;background:var(--color-primary-light);color:var(--color-primary);font-size:36rpx;font-weight:700}.empty-title{display:block;margin-top:22rpx;font-size:30rpx;font-weight:600}.article-card{margin-top:18rpx;background:var(--color-bg-card);border-radius:var(--radius-lg);overflow:hidden;box-shadow:var(--shadow-card)}.article-cover{height:220rpx;padding:24rpx;background:linear-gradient(135deg,#F5E5DE,#F9F1E8);display:flex;align-items:flex-end}.article-cover text{font-size:42rpx;color:rgba(125,76,63,.52);font-weight:700}.article-body{padding:24rpx}.article-title{display:block;font-size:29rpx;font-weight:650;line-height:1.45}.article-meta{display:block;margin-top:10rpx;font-size:22rpx;color:var(--color-text-tertiary)}.article-content{padding:32rpx;background:var(--color-bg-card);font-size:28rpx;line-height:1.9;color:var(--color-text-secondary)}.article-content .lead{display:block;font-size:31rpx;color:var(--color-text-primary);font-weight:600;margin-bottom:24rpx}.danger-card{background:#FFF8F4;border-color:#F5DED2}.danger-card .feature-icon{background:#FFF0EA;color:#C85D43}@media(max-width:340px){.hero-title{font-size:36rpx}.hero-desc{width:92%}}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/** 将微信回调式请求封装为真正可 await 的 Promise。 */
|
||||||
|
export function request<T = unknown>(options: WechatMiniprogram.RequestOption): Promise<WechatMiniprogram.RequestSuccessCallbackResult<T>> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
wx.request({ ...options, success: resolve as WechatMiniprogram.RequestSuccessCallback, fail: reject })
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { existsSync, readFileSync } from 'node:fs'
|
||||||
|
import { resolve } from 'node:path'
|
||||||
|
|
||||||
|
const root = resolve(import.meta.dirname, '..')
|
||||||
|
const read = (path) => readFileSync(resolve(root, path), 'utf8')
|
||||||
|
|
||||||
|
function assertInOrder(source, labels, scope) {
|
||||||
|
let previous = -1
|
||||||
|
for (const label of labels) {
|
||||||
|
const position = source.indexOf(label)
|
||||||
|
assert.notEqual(position, -1, `${scope}缺少“${label}”`)
|
||||||
|
assert.ok(position > previous, `${scope}中“${label}”顺序错误`)
|
||||||
|
previous = position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const appConfig = JSON.parse(read('miniapp/app.json'))
|
||||||
|
assert.equal(appConfig.window.backgroundColor.toUpperCase(), '#FAFAF8', '小程序页面背景必须为暖白 #FAFAF8')
|
||||||
|
assert.deepEqual(
|
||||||
|
appConfig.tabBar.list.map((item) => item.text),
|
||||||
|
['首页', '找对象', '消息', '活动', '我的'],
|
||||||
|
'底部导航顺序必须固定',
|
||||||
|
)
|
||||||
|
|
||||||
|
const requiredPages = [
|
||||||
|
'pages/service-center/service-center',
|
||||||
|
'pages/service-station/service-station',
|
||||||
|
'pages/service-appointment/service-appointment',
|
||||||
|
'pages/my-appointments/my-appointments',
|
||||||
|
'pages/content/content',
|
||||||
|
'pages/content-detail/content-detail',
|
||||||
|
'pages/anti-fraud/anti-fraud',
|
||||||
|
'pages/service-program/service-program',
|
||||||
|
'pages/service-history/service-history',
|
||||||
|
]
|
||||||
|
for (const page of requiredPages) {
|
||||||
|
assert.ok(appConfig.pages.includes(page), `小程序未注册页面 ${page}`)
|
||||||
|
for (const extension of ['js', 'json', 'wxml', 'wxss']) {
|
||||||
|
assert.ok(existsSync(resolve(root, `miniapp/${page}.${extension}`)), `小程序页面文件缺失:${page}.${extension}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const appStyle = read('miniapp/app.wxss')
|
||||||
|
assert.match(appStyle, /--color-bg-page:\s*#FAFAF8/i, '全局背景令牌必须为 #FAFAF8')
|
||||||
|
assert.match(appStyle, /--spacing-section:\s*48rpx/i, '模块间距必须为 24px(48rpx)')
|
||||||
|
|
||||||
|
const homeMarkup = read('miniapp/pages/index/index.wxml')
|
||||||
|
assertInOrder(homeMarkup, [
|
||||||
|
'品牌区', '用户状态条', 'Banner 轮播', '8个核心入口', '今日推荐',
|
||||||
|
'善缘公益服务', '近期公益活动', '善缘服务站', '婚恋课堂', '安全提醒',
|
||||||
|
], '首页')
|
||||||
|
|
||||||
|
const homeLogic = read('miniapp/pages/index/index.js')
|
||||||
|
assertInOrder(homeLogic, [
|
||||||
|
"label: '找对象'", "label: '公益红娘'", "label: '公益活动'", "label: '婚恋咨询'",
|
||||||
|
"label: '婚恋课堂'", "label: '婚前辅导'", "label: '婚恋反诈'", "label: '善缘服务站'",
|
||||||
|
], '首页核心入口')
|
||||||
|
assert.doesNotMatch(homeLogic, /功能开发中/, '首页核心入口不得使用“功能开发中”占位')
|
||||||
|
|
||||||
|
const activityDetailMarkup = read('miniapp/pages/activity-detail/activity-detail.wxml')
|
||||||
|
assert.doesNotMatch(activityDetailMarkup, /页面:\s*activity-detail/, '活动详情页不得是占位页面')
|
||||||
|
assert.match(activityDetailMarkup, /活动信息/, '活动详情页必须展示活动信息')
|
||||||
|
assert.match(activityDetailMarkup, /报名须知/, '活动详情页必须展示报名须知')
|
||||||
|
|
||||||
|
const adminTheme = read('admin-web/src/theme/index.css')
|
||||||
|
assert.match(adminTheme, /--sidebar-width:\s*220px/, '管理后台侧栏展开宽度必须为 220px')
|
||||||
|
assert.match(adminTheme, /--color-bg-page:\s*#FAFAF8/i, '管理后台页面背景必须使用暖白体系')
|
||||||
|
|
||||||
|
const adminLayout = read('admin-web/src/layouts/MainLayout.vue')
|
||||||
|
assert.match(adminLayout, /善缘丹阳/, '后台品牌名称必须为善缘丹阳')
|
||||||
|
assert.doesNotMatch(adminLayout, /#304156/i, '后台不得继续使用旧式深蓝侧栏')
|
||||||
|
|
||||||
|
console.log('UI contracts passed')
|
||||||