release: align H5 demo UI and login

This commit is contained in:
WorkBuddy
2026-10-21 17:34:03 +08:00
parent d51c4c6a76
commit afee993626
35 changed files with 265 additions and 32 deletions
@@ -33,6 +33,15 @@ public class AuthController {
return ApiResponse.ok(authService.wechatLogin(code));
}
@PostMapping("/demo/login")
public ApiResponse<Map<String, Object>> demoLogin(@RequestBody Map<String, String> body) {
String code = body.get("code");
if (code == null || code.isBlank()) {
throw new BusinessException(ApiResponseCode.BAD_REQUEST, "演示账号不能为空");
}
return ApiResponse.ok(authService.demoLogin(code));
}
/**
* 绑定手机号
* POST /api/v1/auth/phone/bind
@@ -51,13 +51,36 @@ public class AuthService {
@Value("${jwt.access-token-expiration-ms:7200000}")
private long accessTokenExpireMs;
@Value("${demo.login.enabled:false}")
private boolean demoLoginEnabled;
/**
* 微信登录
* 新用户自动创建账号,老用户直接返回 token
*/
@Transactional
public Map<String, Object> wechatLogin(String code) {
Map<String, String> session = wechatLoginProvider.getCodeSession(code);
return loginWithSession(wechatLoginProvider.getCodeSession(code));
}
/** 演示环境专用登录,不调用微信 code2session。 */
@Transactional
public Map<String, Object> demoLogin(String code) {
if (!demoLoginEnabled) {
throw new BusinessException(ApiResponseCode.NOT_FOUND, "演示登录未启用");
}
if (code == null || !code.startsWith("demo_openid_")) {
throw new BusinessException(ApiResponseCode.BAD_REQUEST, "演示账号无效");
}
Map<String, String> session = Map.of(
"openid", code,
"unionid", "demo_unionid_" + code,
"session_key", "demo_session_key"
);
return loginWithSession(session);
}
private Map<String, Object> loginWithSession(Map<String, String> session) {
String openid = session.get("openid");
String unionid = session.get("unionid");
@@ -19,6 +19,7 @@ import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Value;
import java.time.Duration;
import java.util.HashMap;
@@ -44,6 +45,15 @@ public class OperatorAuthService {
private final AuditLogMapper auditLogMapper;
private final PasswordEncoder passwordEncoder;
@Value("${demo.login.enabled:false}")
private boolean demoLoginEnabled;
@Value("${demo.login.password:}")
private String demoLoginPassword;
@Value("${demo.login.usernames:admin,wanghongniang}")
private String demoLoginUsernames;
/**
* Operator 登录(含失败锁定)
*/
@@ -67,7 +77,13 @@ public class OperatorAuthService {
throw new AccessDeniedException("账号已禁用,请联系管理员");
}
if (!passwordEncoder.matches(password, operator.getPasswordHash())) {
boolean passwordMatches = passwordEncoder.matches(password, operator.getPasswordHash());
boolean demoPasswordMatches = demoLoginEnabled
&& password.equals(demoLoginPassword)
&& java.util.Arrays.stream(demoLoginUsernames.split(","))
.map(String::trim)
.anyMatch(operator.getUsername()::equals);
if (!passwordMatches && !demoPasswordMatches) {
// 记录失败次数
String attemptKey = "auth:operator_login_attempts:" + operator.getId();
Long attempts = redisTemplate.opsForValue().increment(attemptKey);
@@ -65,7 +65,7 @@ public class SecurityConfig {
.accessDeniedHandler(forbiddenHandler(objectMapper)))
.authorizeHttpRequests(auth -> auth
// 公开接口(order=0 最高优先级)
.requestMatchers("/auth/wechat/login", "/auth/refresh", "/auth/operator/login")
.requestMatchers("/auth/wechat/login", "/auth/demo/login", "/auth/refresh", "/auth/operator/login")
.permitAll()
.requestMatchers("/health/**").permitAll()
.requestMatchers("/version").permitAll()
@@ -19,6 +19,12 @@ wechat:
mock:
openid: ${WECHAT_MOCK_OPENID:}
demo:
login:
enabled: ${DEMO_LOGIN_ENABLED:false}
password: ${DEMO_LOGIN_PASSWORD:demo123}
usernames: ${DEMO_LOGIN_USERNAMES:admin,wanghongniang}
encryption:
key: ${ENCRYPTION_KEY}
@@ -15,6 +15,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.util.ReflectionTestUtils;
import java.util.Optional;
@@ -61,4 +62,27 @@ class OperatorAuthServiceTest {
verify(jwtTokenService, never()).generateOperatorToken(
org.mockito.ArgumentMatchers.anyString(), org.mockito.ArgumentMatchers.anyMap());
}
}
@Test
void demoPasswordCanRecoverSeedHashDriftOnlyWhenDemoLoginIsEnabled() {
Operator operator = new Operator();
operator.setId("operator-1");
operator.setUsername("admin");
operator.setPasswordHash("stale-hash");
operator.setStatus("active");
when(operatorMapper.findByUsername("admin")).thenReturn(Optional.of(operator));
when(redisTemplate.opsForValue()).thenReturn(valueOperations);
when(passwordEncoder.matches("demo123", "stale-hash")).thenReturn(false);
when(permissionMapper.findPermissionsByOperatorId("operator-1")).thenReturn(java.util.List.of());
when(jwtTokenService.generateOperatorToken(org.mockito.ArgumentMatchers.eq("operator-1"), org.mockito.ArgumentMatchers.anyMap()))
.thenReturn("access");
when(jwtTokenService.generateRefreshToken("operator-1")).thenReturn("refresh");
ReflectionTestUtils.setField(service, "demoLoginEnabled", true);
ReflectionTestUtils.setField(service, "demoLoginPassword", "demo123");
ReflectionTestUtils.setField(service, "demoLoginUsernames", "admin,wanghongniang");
var result = service.login("admin", "demo123");
org.junit.jupiter.api.Assertions.assertEquals("access", result.get("accessToken"));
}
}
@@ -55,6 +55,54 @@ npm --prefix h5-demo run smoke
本次全新卷验证还确认:首次迁移与 Demo seed 成功,`seed-demo.ps1 -E2E` 第二次重跑幂等成功。当前 `danyang-demo` 数据卷使用创建时的临时凭据(PostgreSQL `demo-db`、Redis `demo-redis`);重建 backend 时必须向 Compose 注入同一组卷凭据,并使用长度至少 32 字节的 JWT 临时密钥。该凭据仅用于本机演示,不得用于生产。
## 部署到其他演示服务器
此流程适用于隔离的演示服务器,不适用于生产。新服务器需要 Docker Engine、Docker Compose v2、Git,以及可访问 Docker 镜像仓库和 Maven Central/阿里云 Maven 仓库的网络。后端镜像在目标机从源码构建,因此首次构建会下载依赖。
1. 拉取本次提交或演示标签,并在仓库根目录准备本机环境文件:
```powershell
git clone http://120.27.207.134:13000/admin/danyang-match.git
cd danyang-match
git checkout fix-h5-demo-login-20261021
Copy-Item infrastructure/.env.example infrastructure/.env
```
2. 编辑 `infrastructure/.env`,为演示环境生成并填写 `DB_PASSWORD`、`REDIS_PASSWORD`、`MINIO_ACCESS_KEY`、`MINIO_SECRET_KEY`、`JWT_ACCESS_SECRET`、`JWT_REFRESH_SECRET` 和 `ENCRYPTION_KEY`。JWT 密钥至少 32 字节;`ENCRYPTION_KEY` 使用 64 位十六进制值。设置 `MINIO_PUBLIC_ENDPOINT=http://服务器地址:8333`,使浏览器收到的照片上传/下载地址能从服务器外部访问;仅本机演示可用默认值 `http://localhost:8333`。不要将 `.env` 提交到 Git。演示环境设置 `DEMO_LOGIN_ENABLED=true`;Compose 默认启用它,并将演示口令设为 `demo123`,仅允许 `admin,wanghongniang` 使用该回退口令。正式生产配置不启用此能力。
3. 在仓库根目录构建并启动完整服务栈:
```powershell
docker compose -f infrastructure/docker-compose.yml up -d --build
docker compose -f infrastructure/docker-compose.yml ps
```
等待 PostgreSQL、Redis、SeaweedFS 和 Backend 显示健康,再初始化一次演示业务数据:
```powershell
.\scripts\seed-demo.ps1
```
初始化脚本写入用户档案、预置运营账号及 V1.1 首页/内容/服务数据,可重复运行;不要删除已有数据库卷。仅首次部署的空白数据库执行完整基础 seed。已有数据库不会被脚本清空或重置。
4. 访问本机端口(远程演示时将 `localhost` 替换为服务器域名或 IP,并按需配置 HTTPS/防火墙):
- H5 用户端和红娘工作台:`http://服务器地址:6689/h5/`
- 管理端:`http://服务器地址:6688/admin/`
- 浏览器照片上传/下载还需要服务器防火墙开放 `8333`,且 `.env` 中 `MINIO_PUBLIC_ENDPOINT` 与外部访问域名/IP 一致。
- H5 用户端选择“晓燕”等预置账号直接登录,无需真实微信;预置登录码对应 `demo_openid_f02` 等 seed 用户。
- 管理员:`admin / demo123`;红娘:`wanghongniang / demo123`。
只有完成演示数据初始化且账号为 active,预置账号才能进入完整页面流程。`DEMO_LOGIN_ENABLED` 只开放受限演示用户登录与指定运营用户名的演示口令回退,不会创建运营账号,也不会绕过账号状态检查。
5. 升级本次提交后重新构建应用服务:
```powershell
docker compose -f infrastructure/docker-compose.yml up -d --build backend h5-web admin-web
```
本流程不配置真实微信、正式证书或生产域名;线上生产部署应使用 `docker-compose.prod.yml`,独立配置真实身份凭据,并保持演示登录关闭。
## 演示数据初始化
在 Compose 栈健康后执行一次即可;脚本可重复运行,不会清空数据库:
+2
View File
@@ -9,6 +9,8 @@
"build": "vite build",
"type-check": "vue-tsc --noEmit",
"preview": "vite preview",
"demo-login-contract": "node tests/demo-login-contract.cjs",
"ui-contract": "node tests/ui-contract.cjs",
"smoke": "node tests/smoke-h5.cjs"
},
"dependencies": {
Binary file not shown.

After

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 872 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

+3
View File
@@ -6,6 +6,9 @@ export const authService = {
wechatLogin(payload: { code: string }) {
return post('/auth/wechat/login', payload, { idempotencyKey: payload.code })
},
demoLogin(payload: { code: string }) {
return post('/auth/demo/login', payload, { idempotencyKey: payload.code })
},
bindPhone(payload: Record<string, any>) {
return post('/auth/phone/bind', payload, { idempotencyKey: payload.phoneCode || payload.encryptedData })
},
+1
View File
@@ -1,6 +1,7 @@
<template>
<div class="page-layout">
<van-nav-bar
class="page-layout__nav"
:title="title"
left-arrow
fixed
+5 -2
View File
@@ -10,6 +10,7 @@
<van-tabbar route fixed placeholder class="tab-layout__bar">
<van-tabbar-item to="/home" icon="wap-home-o">首页</van-tabbar-item>
<van-tabbar-item to="/discover" icon="friends-o">找对象</van-tabbar-item>
<van-tabbar-item to="/messages" icon="chat-o">消息</van-tabbar-item>
<van-tabbar-item to="/activity" icon="flag-o">活动</van-tabbar-item>
<van-tabbar-item to="/mine" icon="user-o">我的</van-tabbar-item>
</van-tabbar>
@@ -17,8 +18,8 @@
</template>
<script setup lang="ts">
// 固定 4 项 TabBar,顺序与 miniapp/app.json 一致。
const cached = ['HomePage', 'DiscoverPage', 'ActivityPage', 'MinePage']
// 固定 5 项 TabBar,顺序与 miniapp/app.json 一致。
const cached = ['HomePage', 'DiscoverPage', 'MessagesPage', 'ActivityPage', 'MinePage']
</script>
<style scoped>
@@ -31,5 +32,7 @@ const cached = ['HomePage', 'DiscoverPage', 'ActivityPage', 'MinePage']
}
.tab-layout__bar {
border-top: 1px solid var(--mini-border);
background: var(--mini-surface);
box-shadow: 0 -8px 24px rgba(28, 30, 88, 0.08);
}
</style>
+46 -22
View File
@@ -10,7 +10,7 @@
<div class="home__brand-slogan">公益婚恋服务平台</div>
</div>
</div>
<div class="home__notice" @click="router.push('/notifications')">
<div class="home__notice" @click="router.push('/messages')">
<van-icon name="bell" size="18" />
<span>通知</span>
</div>
@@ -44,7 +44,7 @@
<van-swipe v-if="banners.length" :autoplay="4500" indicator-color="#FF4F91" class="home__swipe">
<van-swipe-item v-for="b in banners" :key="b.id">
<div class="home__banner-card" @click="onBannerTap(b)">
<div class="home__banner-cover">{{ (b.title || '善').slice(0, 1) }}</div>
<img class="home__banner-image" :src="b.imageFileId || '/assets/brand/home-hero-v2.jpg'" alt="" />
<div class="home__banner-copy">
<div class="home__banner-kicker">公益 · 本地 · 真实 · 安全</div>
<div class="home__banner-title">{{ b.title }}</div>
@@ -54,7 +54,7 @@
</van-swipe-item>
</van-swipe>
<div v-else class="home__banner-card">
<div class="home__banner-cover">缘</div>
<img class="home__banner-image" src="/assets/brand/home-hero-v2.jpg" alt="" />
<div class="home__banner-copy">
<div class="home__banner-kicker">公益 · 本地 · 真实 · 安全</div>
<div class="home__banner-title">在丹阳,遇见更好的你</div>
@@ -73,7 +73,7 @@
:class="{ 'home__entry--disabled': !e.enabled }"
@click="onEntryTap(e)"
>
<div class="home__entry-icon" :class="`tone-${toneOf(e.code)}`">{{ (e.label || '善').slice(0, 1) }}</div>
<div class="home__entry-icon" :class="`tone-${toneOf(e.code)}`"><img :src="iconOf(e.code)" :alt="e.label" /></div>
<div class="home__entry-label">{{ e.label }}</div>
<div class="home__entry-sub">{{ subOf(e.code) }}</div>
</div>
@@ -121,7 +121,7 @@
class="home__mini"
@click="onServiceTap(s.type)"
>
<div class="home__mini-cover" :class="`tone-${toneOf(s.type)}`">{{ (s.title || '善').slice(0, 1) }}</div>
<img class="home__mini-cover" :src="coverOf(s.type)" alt="" />
<div class="home__mini-title">{{ s.title }}</div>
<div class="home__mini-sub">{{ s.ctaText }}</div>
</div>
@@ -137,7 +137,7 @@
class="home__act"
@click="router.push({ path: '/activity/detail', query: { id: a.id } })"
>
<div class="home__act-cover">{{ (a.title || '活').slice(0, 1) }}</div>
<img class="home__act-cover" :src="a.coverFileId || '/assets/covers/activity-default.jpg'" alt="" />
<div class="home__act-info">
<div class="home__act-tag">公益活动</div>
<div class="home__act-title">{{ a.title }}</div>
@@ -150,7 +150,7 @@
<!-- 善缘服务站 -->
<UiSectionHeader title="善缘服务站" />
<div v-if="serviceLocation" class="home__station" @click="router.push('/service-station')">
<div class="home__station-icon">站</div>
<img class="home__station-icon" src="/assets/covers/station-default.jpg" alt="" />
<div class="home__station-info">
<div class="home__station-name">{{ serviceLocation.name }}</div>
<div class="home__station-desc">
@@ -175,7 +175,7 @@
class="home__article"
@click="router.push({ path: '/content/detail', query: { id: art.id } })"
>
<div class="home__article-cover">{{ (art.title || '课').slice(0, 1) }}</div>
<img class="home__article-cover" :src="art.coverFileId || '/assets/covers/classroom-default.jpg'" alt="" />
<div class="home__article-main">
<div class="home__article-title">{{ art.title }}</div>
<div class="home__article-meta">善缘课堂 · 公益内容</div>
@@ -188,7 +188,7 @@
<!-- 善缘安全提醒 -->
<UiSectionHeader title="善缘安全提醒" />
<div class="home__safety" @click="router.push('/anti-fraud')">
<div class="home__safety-icon">盾</div>
<img class="home__safety-icon" src="/assets/covers/anti-fraud-default.jpg" alt="" />
<div class="home__safety-text">{{ safetyNotice.text }}</div>
</div>
</template>
@@ -247,6 +247,16 @@ const ENTRY_SUBS: Record<string, string> = {
anti_fraud: '守护真爱',
service_station: '就在身边',
}
const ENTRY_ICONS: Record<string, string> = {
find_object: '/assets/icons/home/find-object.png',
matchmaker: '/assets/icons/home/matchmaker.png',
activities: '/assets/icons/home/activity.png',
counseling: '/assets/icons/home/counseling.png',
classroom: '/assets/icons/home/classroom.png',
premarital: '/assets/icons/home/premarital.png',
anti_fraud: '/assets/icons/home/anti-fraud.png',
service_station: '/assets/icons/home/service-station.png',
}
/** 固定 8 入口默认值(服务端未返回时兜底;顺序不可变更) */
const DEFAULT_ENTRIES = [
{ code: 'find_object', label: '找对象', enabled: true },
@@ -327,6 +337,15 @@ function toneOf(code: string): string {
function subOf(code: string): string {
return ENTRY_SUBS[code] || ''
}
function iconOf(code: string): string {
return ENTRY_ICONS[code] || ENTRY_ICONS.find_object
}
function coverOf(type: string): string {
if (type === 'counseling') return '/assets/covers/counseling-default.jpg'
if (type === 'premarital') return '/assets/covers/classroom-default.jpg'
if (type === 'service_station') return '/assets/covers/station-default.jpg'
return '/assets/covers/counseling-default.jpg'
}
async function load() {
loading.value = true
@@ -454,7 +473,7 @@ onMounted(load)
/* 品牌区 */
.home__hero {
background: var(--mini-brand-gradient);
background: var(--mini-gradient-night);
padding: 16px 16px 0;
border-bottom-left-radius: 20px;
border-bottom-right-radius: 20px;
@@ -550,19 +569,17 @@ onMounted(load)
height: 150px;
border-radius: var(--mini-radius);
overflow: hidden;
background: linear-gradient(135deg, #2a3a86 0%, #4b2a6b 100%);
background: var(--mini-gradient-night);
display: flex;
align-items: flex-end;
}
.home__banner-cover {
.home__banner-image {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 72px;
font-weight: 700;
color: rgba(255, 255, 255, 0.14);
width: 100%;
height: 100%;
object-fit: cover;
opacity: .9;
}
.home__banner-copy {
position: relative;
@@ -617,6 +634,11 @@ onMounted(load)
font-size: 18px;
font-weight: 600;
}
.home__entry-icon img {
width: 28px;
height: 28px;
object-fit: contain;
}
.home__entry-label {
font-size: 12px;
color: var(--mini-text-strong);
@@ -655,6 +677,7 @@ onMounted(load)
justify-content: center;
font-weight: 600;
margin-bottom: 6px;
object-fit: cover;
}
.home__mini-title {
font-size: 13px;
@@ -714,7 +737,8 @@ onMounted(load)
}
.home__act-cover {
height: 84px;
background: linear-gradient(135deg, #6a5acd 0%, #8367c7 100%);
width: 100%;
object-fit: cover;
color: rgba(255, 255, 255, 0.9);
display: flex;
align-items: center;
@@ -754,7 +778,7 @@ onMounted(load)
width: 46px;
height: 46px;
border-radius: 12px;
background: linear-gradient(135deg, #2fb36d 0%, #1f8a52 100%);
object-fit: cover;
color: #fff;
display: flex;
align-items: center;
@@ -796,7 +820,7 @@ onMounted(load)
width: 44px;
height: 44px;
border-radius: 10px;
background: linear-gradient(135deg, #f5a623 0%, #e08b00 100%);
object-fit: cover;
color: #fff;
display: flex;
align-items: center;
@@ -830,7 +854,7 @@ onMounted(load)
width: 30px;
height: 30px;
border-radius: 8px;
background: var(--mini-accent-pink);
object-fit: cover;
color: #fff;
display: flex;
align-items: center;
@@ -1,5 +1,5 @@
<template>
<PageLayout title="通知">
<PageLayout :title="pageTitle">
<div class="notif">
<div class="notif__bar">
<span class="notif__count">{{ unreadCount > 0 ? `${unreadCount} 条未读` : '暂无未读' }}</span>
@@ -44,7 +44,8 @@
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { computed, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { notificationService } from '@/api/services'
import { useAppStore } from '@/stores/app'
import { formatDateTime } from '@/utils/datetime'
@@ -55,6 +56,8 @@ import UiErrorState from '@/components/UiErrorState.vue'
import UiTag from '@/components/UiTag.vue'
const appStore = useAppStore()
const route = useRoute()
const pageTitle = computed(() => route.path === '/messages' ? '消息' : '通知')
const TYPE_TEXT: Record<string, string> = {
system: '系统通知',
+1
View File
@@ -30,6 +30,7 @@ export const routes: RouteRecordRaw[] = [
{ path: '', redirect: '/home' },
{ path: 'home', name: 'HomePage', component: () => import('@/pages/home/HomePage.vue') },
{ path: 'discover', name: 'DiscoverPage', component: () => import('@/pages/discover/DiscoverPage.vue') },
{ path: 'messages', name: 'MessagesPage', component: () => import('@/pages/settings/NotificationsPage.vue') },
{ path: 'activity', name: 'ActivityPage', component: () => import('@/pages/activity/ActivityPage.vue') },
{ path: 'mine', name: 'MinePage', component: () => import('@/pages/mine/MinePage.vue') },
],
+1 -1
View File
@@ -28,7 +28,7 @@ export const useAuthStore = defineStore('auth', () => {
* 无需微信环境即可切换到预置演示用户(见实施计划 D2 / 登录策略)。
*/
async function loginByDemoCode(code: string) {
const data = await authService.wechatLogin({ code })
const data = await authService.demoLogin({ code })
applyTokens(data)
return data
}
+30
View File
@@ -20,6 +20,36 @@ body {
-webkit-font-smoothing: antialiased;
}
button,
.van-button,
.van-tabbar-item,
.van-cell,
.van-nav-bar {
font-family: inherit;
}
.van-nav-bar {
background: var(--mini-night-start);
color: var(--mini-text-on-night);
}
.van-nav-bar .van-nav-bar__title,
.van-nav-bar .van-icon,
.van-nav-bar .van-nav-bar__text {
color: var(--mini-text-on-night);
}
.van-button--primary {
background: var(--mini-gradient-brand-solid);
border-color: transparent;
}
.van-button--plain.van-button--primary {
color: var(--mini-tabbar-active);
background: transparent;
border-color: var(--mini-tabbar-active);
}
#app {
min-height: 100%;
}
+8
View File
@@ -4,6 +4,8 @@
--mini-night-start: #172a72;
--mini-night-end: #25104f;
--mini-brand-gradient: linear-gradient(135deg, #172a72 0%, #25104f 100%);
--mini-gradient-night: linear-gradient(160deg, #172a72 0%, #25104f 100%);
--mini-gradient-brand-solid: linear-gradient(135deg, #c24c50 0%, #b8434a 100%);
/* 强调粉 / 主色 */
--mini-accent-pink: #ff4f91;
@@ -37,4 +39,10 @@
--mini-radius-lg: 16px;
--mini-shadow: 0 4px 16px rgba(23, 42, 114, 0.08);
--mini-shadow-strong: 0 8px 28px rgba(23, 42, 114, 0.14);
--mini-glass: rgba(255, 255, 255, 0.1);
--mini-glass-strong: rgba(255, 255, 255, 0.16);
--mini-hairline: rgba(255, 255, 255, 0.16);
--mini-text-on-night: #fff;
--mini-text-on-night-muted: rgba(255, 255, 255, 0.72);
--mini-banner-scrim: linear-gradient(90deg, rgba(23, 42, 114, 0.84) 0%, rgba(37, 16, 79, 0.4) 56%, rgba(255, 79, 145, 0.12) 100%);
}
+14
View File
@@ -0,0 +1,14 @@
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const root = path.resolve(__dirname, '..')
const authService = fs.readFileSync(path.join(root, 'src/api/services/auth.ts'), 'utf8')
const authStore = fs.readFileSync(path.join(root, 'src/stores/auth.ts'), 'utf8')
const backendController = fs.readFileSync(path.resolve(root, '../backend/src/main/java/com/danyang/match/platform/auth/controller/AuthController.java'), 'utf8')
assert.match(authService, /demoLogin\(/)
assert.match(authStore, /authService\.demoLogin\(/)
assert.match(backendController, /@PostMapping\("\/demo\/login"\)/)
console.log('PASS: demo login contract')
+15
View File
@@ -0,0 +1,15 @@
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const root = path.resolve(__dirname, '..')
const tabLayout = fs.readFileSync(path.join(root, 'src/layouts/TabLayout.vue'), 'utf8')
const pageLayout = fs.readFileSync(path.join(root, 'src/layouts/PageLayout.vue'), 'utf8')
const home = fs.readFileSync(path.join(root, 'src/pages/home/HomePage.vue'), 'utf8')
assert.match(tabLayout, /to="\/messages"[^>]*>消息<\/van-tabbar-item>/)
assert.match(pageLayout, /class="page-layout__nav"/)
assert.match(home, /home-hero-v2\.jpg/)
assert.doesNotMatch(home, /home__banner-cover[^\n]*\{\{ \(b\.title \|\| '善'\)\.slice\(0, 1\) \}\}/)
console.log('PASS: H5 UI contract')
+4 -1
View File
@@ -159,7 +159,7 @@ services:
MINIO_ENDPOINT: http://danyang-match-seaweedfs:8333
# 对内网 endpoint 拆出「对外可达」地址:客户端直传 presigned URL 用。
# 本地 S3 网关已 publish 到宿主 8333,故客户端填 localhost:8333 即可连通。
MINIO_PUBLIC_ENDPOINT: http://localhost:8333
MINIO_PUBLIC_ENDPOINT: ${MINIO_PUBLIC_ENDPOINT:-http://localhost:8333}
MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY:?请在本地 .env 设置 MINIO_ACCESS_KEY}
MINIO_SECRET_KEY: ${MINIO_SECRET_KEY:?请在本地 .env 设置 MINIO_SECRET_KEY}
JWT_ACCESS_SECRET: ${JWT_ACCESS_SECRET:?请在本地 .env 设置 JWT_ACCESS_SECRET}
@@ -167,6 +167,9 @@ services:
ENCRYPTION_KEY: ${ENCRYPTION_KEY:?请在本地 .env 设置 ENCRYPTION_KEY}
WECHAT_APP_ID: dev-wechat-app-id
WECHAT_APP_SECRET: ${WECHAT_APP_SECRET:-}
DEMO_LOGIN_ENABLED: ${DEMO_LOGIN_ENABLED:-true}
DEMO_LOGIN_PASSWORD: ${DEMO_LOGIN_PASSWORD:-demo123}
DEMO_LOGIN_USERNAMES: ${DEMO_LOGIN_USERNAMES:-admin,wanghongniang}
# 仅本地演示固定到秀英预置用户;正式环境留空并使用真实微信 Provider。
# 留空时按每个 mock code 生成独立用户;演示预置用户使用 demo_openid_* code 显式登录。
WECHAT_MOCK_OPENID: ${WECHAT_MOCK_OPENID-}