chore: establish V1.1 recovery baseline
@@ -0,0 +1,28 @@
|
||||
// 丹阳公益婚恋服务平台 - 小程序入口
|
||||
const stores = require('./stores');
|
||||
|
||||
App({
|
||||
globalData: {
|
||||
// 平台配置(静态信息)
|
||||
platformName: '丹阳公益婚恋服务平台',
|
||||
slogan: '真实相识 · 公益牵线',
|
||||
},
|
||||
|
||||
onLaunch() {
|
||||
// 触发 7 个 Store 的冷启动初始化(规范 §131)
|
||||
// - 同步从 storage 恢复登录态/用户状态/未读数(避免白屏)
|
||||
// - 异步拉取服务端 App 配置(不阻塞)
|
||||
// - 若已登录异步刷新 /me/status(不阻塞)
|
||||
stores.bootstrap();
|
||||
},
|
||||
|
||||
onError(err) {
|
||||
console.error('[App onError]', err);
|
||||
},
|
||||
|
||||
onPageNotFound(res) {
|
||||
wx.redirectTo({
|
||||
url: '/pages/index/index'
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"sitemapLocation": "sitemap.json",
|
||||
"style": "v2",
|
||||
"pages": [
|
||||
"pages/index/index",
|
||||
"pages/discover/discover",
|
||||
"pages/messages/messages",
|
||||
"pages/activity/activity",
|
||||
"pages/mine/mine",
|
||||
"pages/login/login",
|
||||
"pages/bind-phone/bind-phone",
|
||||
"pages/verification/verification",
|
||||
"pages/profile-basic/profile-basic",
|
||||
"pages/profile-lifestyle/profile-lifestyle",
|
||||
"pages/profile-photos/profile-photos",
|
||||
"pages/profile-interests/profile-interests",
|
||||
"pages/profile-intent/profile-intent",
|
||||
"pages/profile-intro/profile-intro",
|
||||
"pages/profile-preference/profile-preference",
|
||||
"pages/profile-confirm/profile-confirm",
|
||||
"pages/review-pending/review-pending",
|
||||
"pages/review-rejected/review-rejected",
|
||||
"pages/profile-detail/profile-detail",
|
||||
"pages/fate-center/fate-center",
|
||||
"pages/chat/chat",
|
||||
"pages/activity-detail/activity-detail",
|
||||
"pages/activity-register/activity-register",
|
||||
"pages/my-activities/my-activities",
|
||||
"pages/checkin/checkin",
|
||||
"pages/activity-feedback/activity-feedback",
|
||||
"pages/activity-matches/activity-matches",
|
||||
"pages/matchmaker/matchmaker",
|
||||
"pages/matchmaker-apply/matchmaker-apply",
|
||||
"pages/matchmaker-progress/matchmaker-progress",
|
||||
"pages/settings/settings",
|
||||
"pages/privacy-settings/privacy-settings",
|
||||
"pages/relationship-status/relationship-status",
|
||||
"pages/blacklist/blacklist",
|
||||
"pages/notifications/notifications",
|
||||
"pages/report/report",
|
||||
"pages/my-reports/my-reports",
|
||||
"pages/help-center/help-center",
|
||||
"pages/about/about",
|
||||
"pages/account-status-page/account-status-page",
|
||||
"pages/account-logout/account-logout",
|
||||
"pages/account-status/account-status",
|
||||
"pages/appeal/appeal",
|
||||
"pages/matchmaker-login/matchmaker-login",
|
||||
"pages/matchmaker-workbench/matchmaker-workbench",
|
||||
"pages/local-eligibility/local-eligibility",
|
||||
"pages/local-eligibility-supplement/local-eligibility-supplement",
|
||||
"pages/counseling-intro/counseling-intro",
|
||||
"pages/counseling-request/counseling-request",
|
||||
"pages/my-counseling/my-counseling",
|
||||
"pages/premarital-intro/premarital-intro",
|
||||
"pages/premarital-request/premarital-request",
|
||||
"pages/activity-mutual-result/activity-mutual-result",
|
||||
"pages/activity-mutual-waiting/activity-mutual-waiting"
|
||||
],
|
||||
"window": {
|
||||
"navigationBarTitleText": "丹阳公益婚恋",
|
||||
"navigationBarBackgroundColor": "#E96B6B",
|
||||
"navigationBarTextStyle": "white",
|
||||
"backgroundColor": "#F7F8FA",
|
||||
"backgroundTextStyle": "dark",
|
||||
"enablePullDownRefresh": false
|
||||
},
|
||||
"tabBar": {
|
||||
"color": "#999999",
|
||||
"selectedColor": "#E96B6B",
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"borderStyle": "black",
|
||||
"list": [
|
||||
{
|
||||
"pagePath": "pages/index/index",
|
||||
"text": "首页",
|
||||
"iconPath": "assets/tabbar/home.png",
|
||||
"selectedIconPath": "assets/tabbar/home-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/discover/discover",
|
||||
"text": "找对象",
|
||||
"iconPath": "assets/tabbar/discover.png",
|
||||
"selectedIconPath": "assets/tabbar/discover-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/messages/messages",
|
||||
"text": "消息",
|
||||
"iconPath": "assets/tabbar/message.png",
|
||||
"selectedIconPath": "assets/tabbar/message-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/activity/activity",
|
||||
"text": "活动",
|
||||
"iconPath": "assets/tabbar/activity.png",
|
||||
"selectedIconPath": "assets/tabbar/activity-active.png"
|
||||
},
|
||||
{
|
||||
"pagePath": "pages/mine/mine",
|
||||
"text": "我的",
|
||||
"iconPath": "assets/tabbar/mine.png",
|
||||
"selectedIconPath": "assets/tabbar/mine-active.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* 丹阳公益婚恋服务平台 - 全局 Design Token
|
||||
*
|
||||
* 依据《前端组件库与 Design Token 规范 V1.0》
|
||||
* 单位说明:rpx(750rpx = 屏幕宽度),规范 px 值按 2x 映射为 rpx
|
||||
* 禁止页面层写死颜色/间距/圆角,必须引用 var(--token)
|
||||
*/
|
||||
|
||||
page {
|
||||
/* ============ 品牌主色(规范 §3) ============ */
|
||||
--color-primary: #E96B6B;
|
||||
--color-primary-active: #D95F5F;
|
||||
--color-primary-light: #FFF3F2;
|
||||
--color-primary-lighter: #FFF8F7;
|
||||
|
||||
/* ============ 功能色(规范 §4) ============ */
|
||||
--color-success: #2E9B68;
|
||||
--color-success-light: #ECF8F2;
|
||||
--color-warning: #E59A32;
|
||||
--color-warning-light: #FFF7E9;
|
||||
--color-danger: #D94A4A;
|
||||
--color-danger-light: #FFF0F0;
|
||||
--color-info: #4A7CD4;
|
||||
--color-info-light: #EEF4FF;
|
||||
|
||||
/* ============ 中性色(规范 §5-6) ============ */
|
||||
--color-text-primary: #222222;
|
||||
--color-text-secondary: #444444;
|
||||
--color-text-tertiary: #777777;
|
||||
--color-text-placeholder: #999999;
|
||||
--color-text-disabled: #B5B5B5;
|
||||
--color-text-inverse: #FFFFFF; /* 用于 primary/success 等深色背景上的文字 */
|
||||
|
||||
--color-border: #EEEEEE;
|
||||
--color-divider: #F0F0F0;
|
||||
--color-border-focus: #E96B6B;
|
||||
--color-border-error: #D94A4A;
|
||||
|
||||
--color-bg-page: #F7F8FA;
|
||||
--color-bg-card: #FFFFFF;
|
||||
--color-bg-disabled: #F3F3F3;
|
||||
--color-bg-overlay: rgba(0, 0, 0, 0.45);
|
||||
|
||||
/* ============ 字体(规范 §7-11) ============ */
|
||||
--font-family-cn: -apple-system, "PingFang SC", "Noto Sans SC", "Hiragino Sans GB", "Microsoft YaHei", system-ui, sans-serif;
|
||||
|
||||
--font-size-xs: 24rpx; /* 12px */
|
||||
--font-size-sm: 26rpx; /* 13px */
|
||||
--font-size-md: 28rpx; /* 14px */
|
||||
--font-size-base: 32rpx; /* 16px */
|
||||
--font-size-lg: 36rpx; /* 18px */
|
||||
--font-size-xl: 40rpx; /* 20px */
|
||||
--font-size-2xl: 48rpx; /* 24px */
|
||||
--font-size-3xl: 56rpx; /* 28px */
|
||||
|
||||
--font-weight-regular: 400;
|
||||
--font-weight-medium: 500;
|
||||
--font-weight-semibold: 600;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
--line-height-tight: 1.25;
|
||||
--line-height-normal: 1.5;
|
||||
--line-height-relaxed: 1.7;
|
||||
|
||||
/* ============ 间距(规范 §12,4px 栅格) ============ */
|
||||
--spacing-1: 8rpx; /* 4px */
|
||||
--spacing-2: 16rpx; /* 8px */
|
||||
--spacing-3: 24rpx; /* 12px */
|
||||
--spacing-4: 32rpx; /* 16px */
|
||||
--spacing-5: 40rpx; /* 20px */
|
||||
--spacing-6: 48rpx; /* 24px */
|
||||
--spacing-8: 64rpx; /* 32px */
|
||||
--spacing-10: 80rpx; /* 40px */
|
||||
--spacing-12: 96rpx; /* 48px */
|
||||
|
||||
--spacing-page-x: 32rpx; /* 页面左右 16px */
|
||||
--spacing-section: 40rpx; /* 区块间 20px */
|
||||
--spacing-card-inner: 32rpx; /* 卡片内部 16px */
|
||||
--spacing-tight: 16rpx; /* 紧密 8px */
|
||||
|
||||
/* ============ 圆角(规范 §14) ============ */
|
||||
--radius-sm: 16rpx; /* 8px */
|
||||
--radius-md: 24rpx; /* 12px */
|
||||
--radius-lg: 32rpx; /* 16px */
|
||||
--radius-xl: 40rpx; /* 20px */
|
||||
--radius-full: 9999rpx;
|
||||
|
||||
/* ============ 阴影(规范 §15,极轻) ============ */
|
||||
--shadow-card: 0 8rpx 32rpx rgba(0, 0, 0, 0.04);
|
||||
--shadow-popup: 0 16rpx 64rpx rgba(0, 0, 0, 0.10);
|
||||
|
||||
/* ============ 边框(规范 §16) ============ */
|
||||
--border-width: 1rpx;
|
||||
|
||||
/* ============ 动效(规范 §17) ============ */
|
||||
--motion-fast: 120ms;
|
||||
--motion-normal: 200ms;
|
||||
--motion-slow: 300ms;
|
||||
--ease-standard: ease-out;
|
||||
|
||||
/* ============ 层级 z-index(规范 §90) ============ */
|
||||
--z-base: 1;
|
||||
--z-sticky: 100;
|
||||
--z-tabbar: 200;
|
||||
--z-dropdown: 500;
|
||||
--z-modal: 1000;
|
||||
--z-toast: 2000;
|
||||
|
||||
/* ============ 页面基础样式 ============ */
|
||||
background-color: var(--color-bg-page);
|
||||
font-family: var(--font-family-cn);
|
||||
font-size: var(--font-size-base);
|
||||
color: var(--color-text-primary);
|
||||
line-height: var(--line-height-normal);
|
||||
}
|
||||
|
||||
/* ============ 通用工具类 ============ */
|
||||
.container {
|
||||
padding: var(--spacing-page-x);
|
||||
}
|
||||
|
||||
.flex { display: flex; }
|
||||
.flex-column { display: flex; flex-direction: column; }
|
||||
.flex-center { display: flex; align-items: center; justify-content: center; }
|
||||
.flex-between { display: flex; align-items: center; justify-content: space-between; }
|
||||
.flex-start { display: flex; align-items: center; justify-content: flex-start; }
|
||||
.flex-1 { flex: 1; min-width: 0; }
|
||||
.flex-shrink-0 { flex-shrink: 0; }
|
||||
.wrap { flex-wrap: wrap; }
|
||||
|
||||
.text-primary { color: var(--color-text-primary); }
|
||||
.text-secondary { color: var(--color-text-secondary); }
|
||||
.text-tertiary { color: var(--color-text-tertiary); }
|
||||
.text-placeholder { color: var(--color-text-placeholder); }
|
||||
.text-white { color: #FFFFFF; }
|
||||
.text-brand { color: var(--color-primary); }
|
||||
.text-success { color: var(--color-success); }
|
||||
.text-warning { color: var(--color-warning); }
|
||||
.text-danger { color: var(--color-danger); }
|
||||
.text-info { color: var(--color-info); }
|
||||
|
||||
.font-xs { font-size: var(--font-size-xs); }
|
||||
.font-sm { font-size: var(--font-size-sm); }
|
||||
.font-md { font-size: var(--font-size-md); }
|
||||
.font-base { font-size: var(--font-size-base); }
|
||||
.font-lg { font-size: var(--font-size-lg); }
|
||||
.font-xl { font-size: var(--font-size-xl); }
|
||||
.font-2xl { font-size: var(--font-size-2xl); }
|
||||
.font-3xl { font-size: var(--font-size-3xl); }
|
||||
|
||||
.font-regular { font-weight: var(--font-weight-regular); }
|
||||
.font-medium { font-weight: var(--font-weight-medium); }
|
||||
.font-semibold { font-weight: var(--font-weight-semibold); }
|
||||
.font-bold { font-weight: var(--font-weight-bold); }
|
||||
|
||||
.mt-1 { margin-top: var(--spacing-1); }
|
||||
.mt-2 { margin-top: var(--spacing-2); }
|
||||
.mt-3 { margin-top: var(--spacing-3); }
|
||||
.mt-4 { margin-top: var(--spacing-4); }
|
||||
.mt-5 { margin-top: var(--spacing-5); }
|
||||
.mt-6 { margin-top: var(--spacing-6); }
|
||||
.mb-1 { margin-bottom: var(--spacing-1); }
|
||||
.mb-2 { margin-bottom: var(--spacing-2); }
|
||||
.mb-3 { margin-bottom: var(--spacing-3); }
|
||||
.mb-4 { margin-bottom: var(--spacing-4); }
|
||||
.mb-5 { margin-bottom: var(--spacing-5); }
|
||||
.mb-6 { margin-bottom: var(--spacing-6); }
|
||||
|
||||
/* 文本省略 */
|
||||
.ellipsis {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.ellipsis-2 {
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 安全区底部留白(避开 Home Indicator) */
|
||||
.safe-area-bottom {
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
/* 页面卡片基础样式(中低密度留白) */
|
||||
.card-base {
|
||||
background: var(--color-bg-card);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-card-inner);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
|
||||
/* 分割线 */
|
||||
.divider {
|
||||
height: var(--border-width);
|
||||
background: var(--color-divider);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
# 丹阳公益婚恋平台 - 底部导航栏图标设计规范
|
||||
|
||||
## 概述
|
||||
|
||||
为小程序 5 个底部导航 Tab 设计的圆润线性风格图标,包含未选中(灰色)和选中(玫瑰红)两种状态,适配三种屏幕密度(1x/2x/3x)。
|
||||
|
||||
## 设计规范
|
||||
|
||||
| 属性 | 规范值 |
|
||||
|------|--------|
|
||||
| 图标尺寸 | 81 × 81 px (基础) |
|
||||
| 安全区域 | 56 × 56 px (边距 12px) |
|
||||
| 线宽 | 2px |
|
||||
| 圆角 | 端点圆角 + 转折圆角 |
|
||||
| 填充 | 无(纯线性) |
|
||||
|
||||
## 颜色规范
|
||||
|
||||
| 状态 | 颜色值 | 用途 |
|
||||
|------|--------|------|
|
||||
| 未选中 | `#999999` | 默认状态图标 |
|
||||
| 选中 | `#E96B6B` | 激活状态图标 |
|
||||
| 背景 | `#FFFFFF` | 透明背景(PNG 支持 alpha) |
|
||||
|
||||
## 图标清单
|
||||
|
||||
### 1. 首页 (home)
|
||||
- **意象**: 温馨小屋
|
||||
- **元素**: 屋顶三角 + 房屋主体 + 门 + 烟囱
|
||||
- **文件**: `home.png` / `home-active.png`
|
||||
|
||||
### 2. 找对象 (discover)
|
||||
- **意象**: 双心相连,象征缘分匹配
|
||||
- **元素**: 两个相交的爱心
|
||||
- **文件**: `discover.png` / `discover-active.png`
|
||||
|
||||
### 3. 消息 (message)
|
||||
- **意象**: 对话沟通
|
||||
- **元素**: 圆角气泡 + 小三角 + 文字线条
|
||||
- **文件**: `message.png` / `message-active.png`
|
||||
|
||||
### 4. 活动 (activity)
|
||||
- **意象**: 日程安排
|
||||
- **元素**: 日历主体 + 挂环 + 日期标记点
|
||||
- **文件**: `activity.png` / `activity-active.png`
|
||||
|
||||
### 5. 我的 (mine)
|
||||
- **意象**: 个人用户
|
||||
- **元素**: 圆形头部 + 半圆身体轮廓
|
||||
- **文件**: `mine.png` / `mine-active.png`
|
||||
|
||||
## 导出尺寸
|
||||
|
||||
| 尺寸 | 像素 | 适用场景 |
|
||||
|------|------|---------|
|
||||
| 1x | 81 × 81 | 标准屏设备 |
|
||||
| 2x | 162 × 162 | Retina 屏设备 |
|
||||
| 3x | 243 × 243 | 超高清晰屏设备 |
|
||||
|
||||
文件名约定:`{icon-name}.png` / `{icon-name}-active.png` / `{icon-name}_2x.png` / `{icon-name}_3x.png`
|
||||
|
||||
## SVG 源文件
|
||||
|
||||
所有图标均提供可编辑的 SVG 版本,位于 `svg/` 目录:
|
||||
- 可直接在 Figma 中导入编辑
|
||||
- 可调整颜色、粗细、圆角等参数
|
||||
- 颜色替换:将 `stroke="#999999"` 改为 `stroke="#E96B6B"` 即可切换激活状态
|
||||
|
||||
## 文件位置
|
||||
|
||||
```
|
||||
miniapp/assets/tabbar/
|
||||
├── svg/ # SVG 源文件(可编辑)
|
||||
│ ├── home.svg / home-active.svg
|
||||
│ ├── discover.svg / discover-active.svg
|
||||
│ ├── message.svg / message-active.svg
|
||||
│ ├── activity.svg / activity-active.svg
|
||||
│ └── mine.svg / mine-active.svg
|
||||
├── home.png / home-active.png
|
||||
├── home_2x.png / home-active_2x.png
|
||||
├── home_3x.png / home-active_3x.png
|
||||
├── discover.png / discover-active.png
|
||||
├── discover_2x.png / discover-active_2x.png
|
||||
├── discover_3x.png / discover-active_3x.png
|
||||
├── message.png / message-active.png
|
||||
├── message_2x.png / message-active_2x.png
|
||||
├── message_3x.png / message-active_3x.png
|
||||
├── activity.png / activity-active.png
|
||||
├── activity_2x.png / activity-active_2x.png
|
||||
├── activity_3x.png / activity-active_3x.png
|
||||
├── mine.png / mine-active.png
|
||||
├── mine_2x.png / mine-active_2x.png
|
||||
└── mine_3x.png / mine-active_3x.png
|
||||
```
|
||||
|
||||
## 更新说明
|
||||
|
||||
小程序 TabBar 配置无需修改,文件名与原有配置保持一致。
|
||||
|
||||
原配置 (`app.json`):
|
||||
```json
|
||||
{
|
||||
"tabBar": {
|
||||
"list": [
|
||||
{ "pagePath": "pages/index/index", "iconPath": "assets/tabbar/home.png", "selectedIconPath": "assets/tabbar/home-active.png" },
|
||||
{ "pagePath": "pages/discover/discover", "iconPath": "assets/tabbar/discover.png", "selectedIconPath": "assets/tabbar/discover-active.png" },
|
||||
{ "pagePath": "pages/messages/messages", "iconPath": "assets/tabbar/message.png", "selectedIconPath": "assets/tabbar/message-active.png" },
|
||||
{ "pagePath": "pages/activity/activity", "iconPath": "assets/tabbar/activity.png", "selectedIconPath": "assets/tabbar/activity-active.png" },
|
||||
{ "pagePath": "pages/mine/mine", "iconPath": "assets/tabbar/mine.png", "selectedIconPath": "assets/tabbar/mine-active.png" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
After Width: | Height: | Size: 436 B |
|
After Width: | Height: | Size: 741 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 433 B |
|
After Width: | Height: | Size: 737 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 274 B |
|
After Width: | Height: | Size: 528 B |
|
After Width: | Height: | Size: 819 B |
|
After Width: | Height: | Size: 274 B |
|
After Width: | Height: | Size: 527 B |
|
After Width: | Height: | Size: 816 B |
|
After Width: | Height: | Size: 457 B |
|
After Width: | Height: | Size: 865 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 452 B |
|
After Width: | Height: | Size: 858 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 405 B |
|
After Width: | Height: | Size: 719 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 401 B |
|
After Width: | Height: | Size: 714 B |
|
After Width: | Height: | Size: 1.0 KiB |
|
After Width: | Height: | Size: 419 B |
|
After Width: | Height: | Size: 826 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 413 B |
|
After Width: | Height: | Size: 821 B |
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,12 @@
|
||||
<!-- 活动 - 日历图标 (选中状态) -->
|
||||
<svg width="81" height="81" viewBox="0 0 81 81" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="25" cy="18" r="5" stroke="#E96B6B" stroke-width="2"/>
|
||||
<circle cx="56" cy="18" r="5" stroke="#E96B6B" stroke-width="2"/>
|
||||
<rect x="12" y="26" width="57" height="40" rx="8" stroke="#E96B6B" stroke-width="2"/>
|
||||
<line x1="16" y1="36" x2="65" y2="36" stroke="#E96B6B" stroke-width="2"/>
|
||||
<circle cx="25" cy="46" r="3" fill="#E96B6B"/>
|
||||
<circle cx="40.5" cy="46" r="3" fill="#E96B6B"/>
|
||||
<circle cx="56" cy="46" r="3" fill="#E96B6B"/>
|
||||
<circle cx="33" cy="56" r="3" fill="#E96B6B"/>
|
||||
<circle cx="48" cy="56" r="3" fill="#E96B6B"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 696 B |
@@ -0,0 +1,16 @@
|
||||
<!-- 活动 - 日历图标 -->
|
||||
<svg width="81" height="81" viewBox="0 0 81 81" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- 挂环 -->
|
||||
<circle cx="25" cy="18" r="5" stroke="#999999" stroke-width="2"/>
|
||||
<circle cx="56" cy="18" r="5" stroke="#999999" stroke-width="2"/>
|
||||
<!-- 日历主体 -->
|
||||
<rect x="12" y="26" width="57" height="40" rx="8" stroke="#999999" stroke-width="2"/>
|
||||
<!-- 分隔线 -->
|
||||
<line x1="16" y1="36" x2="65" y2="36" stroke="#999999" stroke-width="2"/>
|
||||
<!-- 日期点 -->
|
||||
<circle cx="25" cy="46" r="3" fill="#999999"/>
|
||||
<circle cx="40.5" cy="46" r="3" fill="#999999"/>
|
||||
<circle cx="56" cy="46" r="3" fill="#999999"/>
|
||||
<circle cx="33" cy="56" r="3" fill="#999999"/>
|
||||
<circle cx="48" cy="56" r="3" fill="#999999"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 765 B |
@@ -0,0 +1,5 @@
|
||||
<!-- 找对象 - 双心图标 (选中状态) -->
|
||||
<svg width="81" height="81" viewBox="0 0 81 81" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M28 32C28 26 32 22 36 22C38 22 40 23 41 25C42 23 44 22 46 22C50 22 54 26 54 32C54 38 48 44 41 50C34 44 28 38 28 32Z" stroke="#E96B6B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M42 36C42 30 46 26 50 26C52 26 54 27 55 29C56 27 58 26 60 26C64 26 68 30 68 36C68 42 62 48 55 54C48 48 42 42 42 36Z" stroke="#E96B6B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 574 B |
@@ -0,0 +1,7 @@
|
||||
<!-- 找对象 - 双心图标 -->
|
||||
<svg width="81" height="81" viewBox="0 0 81 81" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- 左心 -->
|
||||
<path d="M28 32C28 26 32 22 36 22C38 22 40 23 41 25C42 23 44 22 46 22C50 22 54 26 54 32C54 38 48 44 41 50C34 44 28 38 28 32Z" stroke="#999999" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<!-- 右心 -->
|
||||
<path d="M42 36C42 30 46 26 50 26C52 26 54 27 55 29C56 27 58 26 60 26C64 26 68 30 68 36C68 42 62 48 55 54C48 48 42 42 42 36Z" stroke="#999999" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 595 B |
@@ -0,0 +1,7 @@
|
||||
<!-- 首页 - 房子图标 (选中状态) -->
|
||||
<svg width="81" height="81" viewBox="0 0 81 81" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="58" y="18" width="8" height="14" rx="2" stroke="#E96B6B" stroke-width="2"/>
|
||||
<path d="M40.5 16L22 34H59L40.5 16Z" stroke="#E96B6B" stroke-width="2" stroke-linejoin="round"/>
|
||||
<rect x="24" y="34" width="33" height="22" rx="4" stroke="#E96B6B" stroke-width="2"/>
|
||||
<rect x="35" y="46" width="11" height="10" rx="2" stroke="#E96B6B" stroke-width="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 511 B |
@@ -0,0 +1,11 @@
|
||||
<!-- 首页 - 房子图标 -->
|
||||
<svg width="81" height="81" viewBox="0 0 81 81" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- 烟囱 -->
|
||||
<rect x="58" y="18" width="8" height="14" rx="2" stroke="#999999" stroke-width="2"/>
|
||||
<!-- 屋顶 -->
|
||||
<path d="M40.5 16L22 34H59L40.5 16Z" stroke="#999999" stroke-width="2" stroke-linejoin="round"/>
|
||||
<!-- 房屋主体 -->
|
||||
<rect x="24" y="34" width="33" height="22" rx="4" stroke="#999999" stroke-width="2"/>
|
||||
<!-- 门 -->
|
||||
<rect x="35" y="46" width="11" height="10" rx="2" stroke="#999999" stroke-width="2"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 571 B |
@@ -0,0 +1,8 @@
|
||||
<!-- 消息 - 对话气泡图标 (选中状态) -->
|
||||
<svg width="81" height="81" viewBox="0 0 81 81" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="12" y="18" width="57" height="36" rx="10" stroke="#E96B6B" stroke-width="2"/>
|
||||
<path d="M22 54L16 64L28 54" stroke="#E96B6B" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="20" y1="28" x2="61" y2="28" stroke="#E96B6B" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="20" y1="36" x2="53" y2="36" stroke="#E96B6B" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="20" y1="44" x2="45" y2="44" stroke="#E96B6B" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 655 B |
@@ -0,0 +1,11 @@
|
||||
<!-- 消息 - 对话气泡图标 -->
|
||||
<svg width="81" height="81" viewBox="0 0 81 81" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- 气泡主体 -->
|
||||
<rect x="12" y="18" width="57" height="36" rx="10" stroke="#999999" stroke-width="2"/>
|
||||
<!-- 小三角 -->
|
||||
<path d="M22 54L16 64L28 54" stroke="#999999" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<!-- 文字线条 -->
|
||||
<line x1="20" y1="28" x2="61" y2="28" stroke="#999999" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="20" y1="36" x2="53" y2="36" stroke="#999999" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="20" y1="44" x2="45" y2="44" stroke="#999999" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 709 B |
@@ -0,0 +1,7 @@
|
||||
<!-- 我的 - 用户图标 (选中状态) -->
|
||||
<svg width="81" height="81" viewBox="0 0 81 81" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="40.5" cy="28" r="14" stroke="#E96B6B" stroke-width="2"/>
|
||||
<path d="M22 62C22 50 30 44 40.5 44C51 44 59 50 59 62" stroke="#E96B6B" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="22" y1="52" x2="22" y2="62" stroke="#E96B6B" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="59" y1="52" x2="59" y2="62" stroke="#E96B6B" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 534 B |
@@ -0,0 +1,10 @@
|
||||
<!-- 我的 - 用户图标 -->
|
||||
<svg width="81" height="81" viewBox="0 0 81 81" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- 头部 -->
|
||||
<circle cx="40.5" cy="28" r="14" stroke="#999999" stroke-width="2"/>
|
||||
<!-- 身体 -->
|
||||
<path d="M22 62C22 50 30 44 40.5 44C51 44 59 50 59 62" stroke="#999999" stroke-width="2" stroke-linecap="round"/>
|
||||
<!-- 身体两侧 -->
|
||||
<line x1="22" y1="52" x2="22" y2="62" stroke="#999999" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="59" y1="52" x2="59" y2="62" stroke="#999999" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 579 B |
@@ -0,0 +1,20 @@
|
||||
/**
|
||||
* ActivityCard 活动卡片(规范 §43-46)
|
||||
*
|
||||
* 不变量(AGENTS.md 第 1 条):feeType=paid 仅作展示字段,前端不实现任何支付入口。
|
||||
* "公益免费"作为主要视觉标识突出展示。
|
||||
*/
|
||||
Component({
|
||||
options: { addGlobalClass: true },
|
||||
properties: {
|
||||
activity: { type: Object, value: null },
|
||||
showStatusBadge: { type: Boolean, value: true },
|
||||
compact: { type: Boolean, value: false }, // 紧凑模式(首页推荐位)
|
||||
},
|
||||
methods: {
|
||||
onTap() {
|
||||
if (!this.data.activity) return;
|
||||
this.triggerEvent('tap', { activityId: this.data.activity.activityId });
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"ui-status-badge": "../status-badge/index"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<view class="ui-act {{compact ? 'ui-act--compact' : ''}}" bindtap="onTap">
|
||||
<view class="ui-act__cover">
|
||||
<image
|
||||
wx:if="{{activity.coverUrl}}"
|
||||
class="ui-act__cover-img"
|
||||
src="{{activity.coverUrl}}"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view wx:else class="ui-act__cover-placeholder">
|
||||
<view class="ui-act__cover-icon"></view>
|
||||
</view>
|
||||
<view class="ui-act__fee-badge {{activity.feeType === 'paid' ? 'ui-act__fee-badge--paid' : 'ui-act__fee-badge--free'}}">
|
||||
{{activity.feeType === 'paid' ? '付费' : '公益免费'}}
|
||||
</view>
|
||||
</view>
|
||||
<view class="ui-act__body">
|
||||
<view class="ui-act__title">{{activity.title}}</view>
|
||||
<view class="ui-act__meta">
|
||||
<text class="ui-act__time">{{activity.startAtText || activity.startAt}}</text>
|
||||
<text wx:if="{{activity.location}}"> · {{activity.location}}</text>
|
||||
</view>
|
||||
<view wx:if="{{activity.organizer}}" class="ui-act__meta ui-act__organizer">
|
||||
主办:{{activity.organizer}}
|
||||
</view>
|
||||
<view wx:if="{{activity.suitableFor}}" class="ui-act__meta">
|
||||
适合:{{activity.suitableFor}}
|
||||
</view>
|
||||
<view wx:if="{{!compact}}" class="ui-act__intro">{{activity.intro}}</view>
|
||||
<view class="ui-act__footer">
|
||||
<view class="ui-act__capacity">
|
||||
<text wx:if="{{activity.capacity}}">容量 {{activity.capacity}}</text>
|
||||
</view>
|
||||
<view wx:if="{{showStatusBadge}}">
|
||||
<ui-status-badge
|
||||
wx:if="{{activity.status === 'registration_open'}}"
|
||||
kind="success"
|
||||
label="报名中"
|
||||
/>
|
||||
<ui-status-badge
|
||||
wx:elif="{{activity.status === 'registration_closed'}}"
|
||||
kind="warning"
|
||||
label="报名截止"
|
||||
/>
|
||||
<ui-status-badge
|
||||
wx:elif="{{activity.status === 'ongoing'}}"
|
||||
kind="info"
|
||||
label="进行中"
|
||||
/>
|
||||
<ui-status-badge
|
||||
wx:elif="{{activity.status === 'ended'}}"
|
||||
kind="disabled"
|
||||
label="已结束"
|
||||
/>
|
||||
<ui-status-badge
|
||||
wx:elif="{{activity.status === 'cancelled'}}"
|
||||
kind="danger"
|
||||
label="已取消"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,108 @@
|
||||
.ui-act {
|
||||
background: var(--color-bg-card);
|
||||
border-radius: var(--radius-lg);
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-card);
|
||||
margin-bottom: var(--spacing-3);
|
||||
}
|
||||
.ui-act__cover {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 240rpx;
|
||||
background: var(--color-bg-disabled);
|
||||
}
|
||||
.ui-act__cover-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.ui-act__cover-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-primary-light);
|
||||
}
|
||||
.ui-act__cover-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
background: var(--color-primary);
|
||||
opacity: 0.4;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
.ui-act__fee-badge {
|
||||
position: absolute;
|
||||
top: var(--spacing-2);
|
||||
right: var(--spacing-2);
|
||||
padding: 6rpx 14rpx;
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
.ui-act__fee-badge--free {
|
||||
background: var(--color-success);
|
||||
}
|
||||
.ui-act__fee-badge--paid {
|
||||
background: var(--color-text-tertiary);
|
||||
}
|
||||
.ui-act__body {
|
||||
padding: var(--spacing-3) var(--spacing-4);
|
||||
}
|
||||
.ui-act__title {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text-primary);
|
||||
line-height: var(--line-height-tight);
|
||||
margin-bottom: var(--spacing-1);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.ui-act__meta {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-tertiary);
|
||||
line-height: var(--line-height-normal);
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
.ui-act__organizer {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
.ui-act__intro {
|
||||
margin-top: var(--spacing-2);
|
||||
font-size: var(--font-size-md);
|
||||
color: var(--color-text-secondary);
|
||||
line-height: var(--line-height-relaxed);
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
}
|
||||
.ui-act__footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: var(--spacing-3);
|
||||
}
|
||||
.ui-act__capacity {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
.ui-act--compact {
|
||||
display: flex;
|
||||
height: 200rpx;
|
||||
}
|
||||
.ui-act--compact .ui-act__cover {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ui-act--compact .ui-act__body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: var(--spacing-2) var(--spacing-3);
|
||||
}
|
||||
.ui-act--compact .ui-act__intro {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Avatar 头像(规范 §22 用户卡片)
|
||||
*
|
||||
* 显示用户头像 + 已实名 / 已审核徽标。
|
||||
* 不变量(AGENTS 第 8 条):头像 URL 来自 OSS 公开链接,不携带任何敏感身份字段。
|
||||
* 用法:
|
||||
* <ui-avatar src="{{user.avatarUrl}}" size="lg"
|
||||
* verified="{{user.verified}}" reviewed="{{user.reviewed}}" />
|
||||
*/
|
||||
Component({
|
||||
options: { addGlobalClass: true },
|
||||
properties: {
|
||||
src: { type: String, value: '' },
|
||||
size: { type: String, value: 'md' }, // sm|md|lg|xl
|
||||
shape: { type: String, value: 'circle' }, // circle|square
|
||||
verified: { type: Boolean, value: false },
|
||||
reviewed: { type: Boolean, value: false },
|
||||
showBadge: { type: Boolean, value: true },
|
||||
// 兜底默认头像(不依赖外部资源)
|
||||
defaultText: { type: String, value: '' }, // 没图时显示首字(如"王")
|
||||
},
|
||||
data: {
|
||||
loadError: false,
|
||||
},
|
||||
methods: {
|
||||
onError() {
|
||||
this.setData({ loadError: true });
|
||||
this.triggerEvent('error', {});
|
||||
},
|
||||
onTap() {
|
||||
this.triggerEvent('tap', {});
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<view
|
||||
class="ui-avatar ui-avatar--{{size}} ui-avatar--{{shape}}"
|
||||
bindtap="onTap"
|
||||
>
|
||||
<image
|
||||
wx:if="{{src && !loadError}}"
|
||||
class="ui-avatar__img"
|
||||
src="{{src}}"
|
||||
mode="aspectFill"
|
||||
binderror="onError"
|
||||
/>
|
||||
<view wx:else class="ui-avatar__fallback">
|
||||
<text wx:if="{{defaultText}}" class="ui-avatar__fallback-text">{{defaultText}}</text>
|
||||
<view wx:else class="ui-avatar__fallback-icon"></view>
|
||||
</view>
|
||||
<view wx:if="{{showBadge && (verified || reviewed)}}" class="ui-avatar__badges">
|
||||
<view wx:if="{{verified}}" class="ui-avatar__badge ui-avatar__badge--verified">实名</view>
|
||||
<view wx:if="{{reviewed}}" class="ui-avatar__badge ui-avatar__badge--reviewed">审核</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,62 @@
|
||||
.ui-avatar {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
background: var(--color-bg-disabled);
|
||||
overflow: hidden;
|
||||
}
|
||||
.ui-avatar--circle { border-radius: 50%; }
|
||||
.ui-avatar--square { border-radius: var(--radius-md); }
|
||||
|
||||
/* 尺寸:sm 60 / md 88 / lg 120 / xl 160 */
|
||||
.ui-avatar--sm { width: 60rpx; height: 60rpx; }
|
||||
.ui-avatar--md { width: 88rpx; height: 88rpx; }
|
||||
.ui-avatar--lg { width: 120rpx; height: 120rpx; }
|
||||
.ui-avatar--xl { width: 160rpx; height: 160rpx; }
|
||||
|
||||
.ui-avatar__img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.ui-avatar__fallback {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--color-primary-light);
|
||||
}
|
||||
.ui-avatar__fallback-text {
|
||||
font-size: var(--font-size-lg);
|
||||
color: var(--color-primary);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
.ui-avatar__fallback-icon {
|
||||
width: 50%;
|
||||
height: 50%;
|
||||
border-radius: 50%;
|
||||
background: var(--color-primary);
|
||||
opacity: 0.6;
|
||||
}
|
||||
.ui-avatar__badges {
|
||||
position: absolute;
|
||||
bottom: -4rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 6rpx;
|
||||
}
|
||||
.ui-avatar__badge {
|
||||
font-size: 16rpx;
|
||||
line-height: 1;
|
||||
padding: 4rpx 8rpx;
|
||||
border-radius: var(--radius-full);
|
||||
color: #FFFFFF;
|
||||
font-weight: var(--font-weight-medium);
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ui-avatar__badge--verified { background: var(--color-success); }
|
||||
.ui-avatar__badge--reviewed { background: var(--color-info); }
|
||||
|
||||
/* 小尺寸时隐藏徽标避免溢出 */
|
||||
.ui-avatar--sm .ui-avatar__badges { display: none; }
|
||||
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Button 按钮(规范 §22 / Token §2 主色 + 功能色)
|
||||
*
|
||||
* 变体 variant:primary(主红)/ secondary(白底红边)/ ghost(无背景)/ danger / text
|
||||
* 尺寸 size:sm / md(默认)/ lg
|
||||
* 状态:disabled / loading
|
||||
* 用法:
|
||||
* <ui-button variant="primary" size="md" bindtap="onSubmit">提交</ui-button>
|
||||
* <ui-button variant="danger" block loading>删除</ui-button>
|
||||
*/
|
||||
Component({
|
||||
options: { addGlobalClass: true, multipleSlots: true },
|
||||
externalClasses: ['custom-class'],
|
||||
properties: {
|
||||
variant: { type: String, value: 'primary' }, // primary|secondary|ghost|danger|text
|
||||
size: { type: String, value: 'md' }, // sm|md|lg
|
||||
block: { type: Boolean, value: false }, // 是否撑满父容器
|
||||
disabled: { type: Boolean, value: false },
|
||||
loading: { type: Boolean, value: false },
|
||||
openType: { type: String, value: '' }, // 透传原生 open-type(getPhoneNumber 等)
|
||||
hoverClass: { type: String, value: 'ui-btn--hover' },
|
||||
},
|
||||
methods: {
|
||||
onTap(e) {
|
||||
if (this.data.disabled || this.data.loading) return;
|
||||
this.triggerEvent('tap', e.detail || {});
|
||||
},
|
||||
onGetPhoneNumber(e) {
|
||||
this.triggerEvent('getphonenumber', e.detail || {});
|
||||
},
|
||||
onError(e) {
|
||||
this.triggerEvent('error', e.detail || {});
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<view
|
||||
class="ui-btn ui-btn--{{variant}} ui-btn--{{size}} {{block ? 'ui-btn--block' : ''}} {{disabled ? 'ui-btn--disabled' : ''}} {{loading ? 'ui-btn--loading' : ''}} custom-class"
|
||||
hover-class="{{disabled || loading ? '' : hoverClass}}"
|
||||
hover-stay-time="100"
|
||||
bindtap="onTap"
|
||||
>
|
||||
<view wx:if="{{loading}}" class="ui-btn__spinner">
|
||||
<view class="ui-btn__spinner-dot"></view>
|
||||
</view>
|
||||
<slot></slot>
|
||||
</view>
|
||||
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Button 组件样式
|
||||
* 引用全局 Design Token(addGlobalClass: true)
|
||||
*/
|
||||
.ui-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
border-radius: var(--radius-full);
|
||||
font-family: var(--font-family-cn);
|
||||
font-weight: var(--font-weight-medium);
|
||||
text-align: center;
|
||||
transition: opacity var(--motion-fast) var(--ease-standard),
|
||||
transform var(--motion-fast) var(--ease-standard);
|
||||
user-select: none;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 尺寸 */
|
||||
.ui-btn--sm {
|
||||
padding: 14rpx 24rpx;
|
||||
font-size: var(--font-size-sm);
|
||||
min-height: 56rpx;
|
||||
}
|
||||
.ui-btn--md {
|
||||
padding: 20rpx 36rpx;
|
||||
font-size: var(--font-size-base);
|
||||
min-height: 80rpx;
|
||||
}
|
||||
.ui-btn--lg {
|
||||
padding: 26rpx 48rpx;
|
||||
font-size: var(--font-size-lg);
|
||||
min-height: 96rpx;
|
||||
}
|
||||
|
||||
/* 撑满 */
|
||||
.ui-btn--block {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 变体 */
|
||||
.ui-btn--primary {
|
||||
background: var(--color-primary);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
.ui-btn--secondary {
|
||||
background: var(--color-bg-card);
|
||||
color: var(--color-primary);
|
||||
border: 1rpx solid var(--color-primary);
|
||||
}
|
||||
.ui-btn--ghost {
|
||||
background: transparent;
|
||||
color: var(--color-text-primary);
|
||||
border: 1rpx solid var(--color-border);
|
||||
}
|
||||
.ui-btn--danger {
|
||||
background: var(--color-danger);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
.ui-btn--text {
|
||||
background: transparent;
|
||||
color: var(--color-primary);
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
/* 状态 */
|
||||
.ui-btn--disabled,
|
||||
.ui-btn--loading {
|
||||
opacity: 0.55;
|
||||
}
|
||||
.ui-btn--disabled.ui-btn--primary,
|
||||
.ui-btn--disabled.ui-btn--danger {
|
||||
background: var(--color-text-disabled);
|
||||
}
|
||||
.ui-btn--disabled.ui-btn--secondary,
|
||||
.ui-btn--disabled.ui-btn--ghost {
|
||||
color: var(--color-text-disabled);
|
||||
border-color: var(--color-text-disabled);
|
||||
}
|
||||
|
||||
.ui-btn--hover {
|
||||
opacity: 0.85;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* Loading spinner */
|
||||
.ui-btn__spinner {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
margin-right: 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.ui-btn__spinner-dot {
|
||||
width: 18rpx;
|
||||
height: 18rpx;
|
||||
border-radius: 50%;
|
||||
border: 3rpx solid currentColor;
|
||||
border-top-color: transparent;
|
||||
animation: ui-btn-spin 0.7s linear infinite;
|
||||
}
|
||||
@keyframes ui-btn-spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* ConversationItem 会话列表项(规范 §35)
|
||||
*
|
||||
* 不变量(AGENTS.md):
|
||||
* - 第 3 条:仅 Mutual Match 后才能聊天——canSendMessage=false 时显示"已停用"角标,
|
||||
* 点击进入后聊天页禁止输入。
|
||||
* - 第 4 条:Block 后会话立即停用,relationStatus=blocked 时灰显。
|
||||
*/
|
||||
Component({
|
||||
options: { addGlobalClass: true },
|
||||
properties: {
|
||||
conversation: { type: Object, value: null },
|
||||
showAvatar: { type: Boolean, value: true },
|
||||
},
|
||||
methods: {
|
||||
onTap() {
|
||||
if (!this.data.conversation) return;
|
||||
this.triggerEvent('tap', {
|
||||
conversationId: this.data.conversation.conversationId,
|
||||
targetUserId: this.data.conversation.targetUserId,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"ui-avatar": "../avatar/index"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<view
|
||||
class="ui-conv {{!conversation.canSendMessage ? 'ui-conv--disabled' : ''}}"
|
||||
bindtap="onTap"
|
||||
>
|
||||
<ui-avatar
|
||||
wx:if="{{showAvatar}}"
|
||||
src="{{conversation.targetAvatarUrl}}"
|
||||
size="lg"
|
||||
default-text="{{conversation.targetDisplayName}}"
|
||||
/>
|
||||
<view class="ui-conv__body">
|
||||
<view class="ui-conv__top">
|
||||
<text class="ui-conv__name">{{conversation.targetDisplayName}}</text>
|
||||
<text class="ui-conv__time">{{conversation.lastMessageAtText || ''}}</text>
|
||||
</view>
|
||||
<view class="ui-conv__bottom">
|
||||
<text class="ui-conv__last">{{conversation.lastMessage || '开始你们的对话吧'}}</text>
|
||||
<view wx:if="{{conversation.unreadCount > 0}}" class="ui-conv__badge">
|
||||
{{conversation.unreadCount > 99 ? '99+' : conversation.unreadCount}}
|
||||
</view>
|
||||
</view>
|
||||
<view wx:if="{{!conversation.canSendMessage}}" class="ui-conv__status-hint">
|
||||
{{conversation.relationStatus === 'blocked' ? '会话已停用(已拉黑)' : '会话已停用'}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,66 @@
|
||||
.ui-conv {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
background: var(--color-bg-card);
|
||||
padding: var(--spacing-3) var(--spacing-page-x);
|
||||
border-bottom: 1rpx solid var(--color-divider);
|
||||
}
|
||||
.ui-conv--disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
.ui-conv__body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-left: var(--spacing-3);
|
||||
}
|
||||
.ui-conv__top {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
.ui-conv__name {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.ui-conv__time {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-tertiary);
|
||||
flex-shrink: 0;
|
||||
margin-left: var(--spacing-2);
|
||||
}
|
||||
.ui-conv__bottom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: var(--spacing-1);
|
||||
}
|
||||
.ui-conv__last {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-tertiary);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
margin-right: var(--spacing-2);
|
||||
}
|
||||
.ui-conv__badge {
|
||||
min-width: 36rpx;
|
||||
height: 36rpx;
|
||||
padding: 0 10rpx;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--color-danger);
|
||||
color: #FFFFFF;
|
||||
font-size: var(--font-size-xs);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ui-conv__status-hint {
|
||||
margin-top: var(--spacing-1);
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* DangerActionDialog 危险动作确认弹窗(规范 §107 DANGER_ACTION)
|
||||
*
|
||||
* 用于:拉黑 / 解除匹配 / 暂停寻找 / 退出服务 / 关闭账号
|
||||
* 不变量(AGENTS.md):
|
||||
* - 第 4 条:Block 前必须二次确认,本弹窗为唯一确认入口;
|
||||
* - 第 6 条:解除匹配/退出服务这类不可逆动作同样需要确认。
|
||||
*
|
||||
* 用法:
|
||||
* <ui-danger-action-dialog
|
||||
* visible="{{showBlockDialog}}"
|
||||
* action="block"
|
||||
* target-name="王女士"
|
||||
* bind:confirm="onConfirmBlock"
|
||||
* bind:cancel="onCancelBlock" />
|
||||
*/
|
||||
const { DANGER_ACTION } = require('../../constants');
|
||||
|
||||
const ACTION_CONFIG = {
|
||||
[DANGER_ACTION.BLOCK]: {
|
||||
title: '拉黑该用户',
|
||||
confirmText: '确认拉黑',
|
||||
danger: true,
|
||||
buildContent: (name) => `拉黑后 ${name || '该用户'} 将无法与你互动,你也不会再收到对方的推荐与消息。此操作可由你随时解除。`,
|
||||
},
|
||||
[DANGER_ACTION.UNBLOCK]: {
|
||||
title: '解除拉黑',
|
||||
confirmText: '确认解除',
|
||||
danger: false,
|
||||
buildContent: (name) => `解除拉黑后,${name || '对方'} 将可以重新看到你并向你发起互动。请确认你已准备好重新与对方接触。`,
|
||||
},
|
||||
[DANGER_ACTION.UNMATCH]: {
|
||||
title: '解除匹配',
|
||||
confirmText: '确认解除',
|
||||
danger: true,
|
||||
buildContent: (name) => `解除与 ${name || '对方'} 的匹配后,你们的会话将立即停用,历史记录保留但不可继续发送消息。此操作不可撤销。`,
|
||||
},
|
||||
[DANGER_ACTION.PAUSE]: {
|
||||
title: '暂停寻找',
|
||||
confirmText: '暂停寻找',
|
||||
danger: false,
|
||||
buildContent: () => '暂停后你将不再被推荐给他人,也无法收到新的喜欢提示。可随时恢复。',
|
||||
},
|
||||
[DANGER_ACTION.SUCCESS_MATCH]: {
|
||||
title: '成功牵手',
|
||||
confirmText: '发起牵手请求',
|
||||
danger: false,
|
||||
buildContent: (name) => `发起牵手请求后,${name || '对方'} 需在 7 天内确认。双方均确认后关系将置为"成功牵手",平台将不再为你推荐其他人。`,
|
||||
},
|
||||
[DANGER_ACTION.LOGOUT]: {
|
||||
title: '退出登录',
|
||||
confirmText: '确认退出',
|
||||
danger: false,
|
||||
buildContent: () => '退出后需重新通过微信登录。本地未同步的草稿可能丢失。',
|
||||
},
|
||||
[DANGER_ACTION.CLOSE_ACCOUNT]: {
|
||||
title: '关闭账号',
|
||||
confirmText: '申请关闭',
|
||||
danger: true,
|
||||
buildContent: () => '关闭账号后你的资料将进入 30 天冷静期,期间可联系客服恢复;超过冷静期将永久删除并不可恢复。',
|
||||
},
|
||||
};
|
||||
|
||||
Component({
|
||||
options: { addGlobalClass: true },
|
||||
properties: {
|
||||
visible: { type: Boolean, value: false },
|
||||
action: { type: String, value: 'block' },
|
||||
targetName: { type: String, value: '' },
|
||||
},
|
||||
data: {
|
||||
config: null,
|
||||
},
|
||||
observers: {
|
||||
action(action) {
|
||||
this.setData({ config: ACTION_CONFIG[action] || ACTION_CONFIG[DANGER_ACTION.BLOCK] });
|
||||
},
|
||||
'targetName'(name) {
|
||||
// 重新计算 content
|
||||
const cfg = this.data.config;
|
||||
if (cfg && cfg.buildContent) {
|
||||
this.setData({
|
||||
config: { ...cfg, content: cfg.buildContent(name) },
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
lifetimes: {
|
||||
attached() {
|
||||
const action = this.data.action;
|
||||
const cfg = ACTION_CONFIG[action] || ACTION_CONFIG[DANGER_ACTION.BLOCK];
|
||||
this.setData({
|
||||
config: { ...cfg, content: cfg.buildContent(this.data.targetName) },
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onConfirm() {
|
||||
this.triggerEvent('confirm', { action: this.data.action });
|
||||
},
|
||||
onCancel() {
|
||||
this.triggerEvent('cancel', { action: this.data.action });
|
||||
},
|
||||
onStop() {
|
||||
// 阻止冒泡到 mask
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"ui-button": "../button/index"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<view wx:if="{{visible}}" class="ui-dialog-mask" catchtap="onCancel">
|
||||
<view class="ui-dialog" catchtap="onStop">
|
||||
<view class="ui-dialog__title">{{config.title}}</view>
|
||||
<view class="ui-dialog__content">{{config.content}}</view>
|
||||
<view class="ui-dialog__actions">
|
||||
<ui-button
|
||||
variant="ghost"
|
||||
size="md"
|
||||
class="ui-dialog__btn"
|
||||
bindtap="onCancel"
|
||||
>取消</ui-button>
|
||||
<ui-button
|
||||
variant="{{config.danger ? 'danger' : 'primary'}}"
|
||||
size="md"
|
||||
class="ui-dialog__btn"
|
||||
bindtap="onConfirm"
|
||||
>{{config.confirmText}}</ui-button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,47 @@
|
||||
.ui-dialog-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: var(--color-bg-overlay);
|
||||
z-index: var(--z-modal);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 var(--spacing-6);
|
||||
}
|
||||
.ui-dialog {
|
||||
width: 100%;
|
||||
max-width: 600rpx;
|
||||
background: var(--color-bg-card);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-5) var(--spacing-4) var(--spacing-4);
|
||||
box-shadow: var(--shadow-popup);
|
||||
animation: ui-dialog-in var(--motion-normal) var(--ease-standard);
|
||||
}
|
||||
@keyframes ui-dialog-in {
|
||||
from { transform: scale(0.92); opacity: 0; }
|
||||
to { transform: scale(1); opacity: 1; }
|
||||
}
|
||||
.ui-dialog__title {
|
||||
font-size: var(--font-size-xl);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text-primary);
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-3);
|
||||
}
|
||||
.ui-dialog__content {
|
||||
font-size: var(--font-size-md);
|
||||
color: var(--color-text-secondary);
|
||||
line-height: var(--line-height-relaxed);
|
||||
text-align: center;
|
||||
margin-bottom: var(--spacing-5);
|
||||
}
|
||||
.ui-dialog__actions {
|
||||
display: flex;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
.ui-dialog__btn {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
/**
|
||||
* Empty 空状态(规范 §22 推荐为空时引导去 Discover / 申请红娘)
|
||||
*
|
||||
* 用法:
|
||||
* <ui-empty title="暂时没有符合条件的人"
|
||||
* description="可以去「找对象」看看更多符合条件的人"
|
||||
* actions="{{[{text:'去「找对象', event:'discover'}, {text:'申请公益红娘', event:'matchmaker'}]}}"
|
||||
* bind:action="onEmptyAction" />
|
||||
*/
|
||||
Component({
|
||||
options: { addGlobalClass: true, multipleSlots: true },
|
||||
properties: {
|
||||
title: { type: String, value: '暂无内容' },
|
||||
description: { type: String, value: '' },
|
||||
// [{text, event, variant?: 'primary'|'secondary'}]
|
||||
actions: { type: Array, value: [] },
|
||||
// 图标变体(不依赖外部图片资源,纯 CSS 绘制)
|
||||
illustration: { type: String, value: 'default' }, // default|search|heart|activity|message
|
||||
},
|
||||
methods: {
|
||||
onAction(e) {
|
||||
const { event } = e.currentTarget.dataset;
|
||||
this.triggerEvent('action', { event });
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<view class="ui-empty">
|
||||
<view class="ui-empty__illustration ui-empty__illustration--{{illustration}}">
|
||||
<view class="ui-empty__icon"></view>
|
||||
</view>
|
||||
<text class="ui-empty__title">{{title}}</text>
|
||||
<text wx:if="{{description}}" class="ui-empty__desc">{{description}}</text>
|
||||
<view class="ui-empty__actions">
|
||||
<view
|
||||
wx:for="{{actions}}"
|
||||
wx:key="event"
|
||||
class="ui-empty__action ui-empty__action--{{item.variant || 'secondary'}}"
|
||||
data-event="{{item.event}}"
|
||||
bindtap="onAction"
|
||||
>{{item.text}}</view>
|
||||
</view>
|
||||
<slot></slot>
|
||||
</view>
|
||||
@@ -0,0 +1,80 @@
|
||||
.ui-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: var(--spacing-10) var(--spacing-page-x);
|
||||
text-align: center;
|
||||
}
|
||||
.ui-empty__illustration {
|
||||
width: 160rpx;
|
||||
height: 160rpx;
|
||||
border-radius: 50%;
|
||||
background: var(--color-bg-disabled);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: var(--spacing-4);
|
||||
}
|
||||
.ui-empty__icon {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
background: var(--color-text-disabled);
|
||||
opacity: 0.5;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.ui-empty__illustration--search .ui-empty__icon {
|
||||
border-radius: 50% 50% 50% 0;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
.ui-empty__illustration--heart .ui-empty__icon {
|
||||
clip-path: polygon(50% 100%, 0 38%, 18% 18%, 50% 38%, 82% 18%, 100% 38%);
|
||||
border-radius: 0;
|
||||
}
|
||||
.ui-empty__illustration--activity .ui-empty__icon {
|
||||
border-radius: var(--radius-sm);
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
}
|
||||
.ui-empty__illustration--message .ui-empty__icon {
|
||||
border-radius: var(--radius-md);
|
||||
width: 80rpx;
|
||||
height: 60rpx;
|
||||
}
|
||||
.ui-empty__title {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--color-text-primary);
|
||||
margin-bottom: var(--spacing-2);
|
||||
}
|
||||
.ui-empty__desc {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-tertiary);
|
||||
line-height: var(--line-height-relaxed);
|
||||
max-width: 480rpx;
|
||||
}
|
||||
.ui-empty__actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-2);
|
||||
margin-top: var(--spacing-6);
|
||||
width: 100%;
|
||||
max-width: 480rpx;
|
||||
}
|
||||
.ui-empty__action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 80rpx;
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
.ui-empty__action--primary {
|
||||
background: var(--color-primary);
|
||||
color: #FFFFFF;
|
||||
}
|
||||
.ui-empty__action--secondary {
|
||||
background: var(--color-bg-card);
|
||||
color: var(--color-primary);
|
||||
border: 1rpx solid var(--color-primary);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Input 输入框(规范 §18-19 表单)
|
||||
*
|
||||
* 支持 label、错误提示、字数计数(maxLength 触发)、清除按钮。
|
||||
* 用法:
|
||||
* <ui-input label="昵称" placeholder="请输入昵称" value="{{name}}"
|
||||
* maxlength="20" error="{{errors.name}}" bind:input="onInputName" />
|
||||
*/
|
||||
Component({
|
||||
options: { addGlobalClass: true },
|
||||
properties: {
|
||||
label: { type: String, value: '' },
|
||||
placeholder: { type: String, value: '' },
|
||||
value: { type: String, value: '' },
|
||||
type: { type: String, value: 'text' }, // text|number|idcard|digit|nickname
|
||||
maxlength: { type: Number, value: 140 }, // -1 表示不限
|
||||
showCount: { type: Boolean, value: false }, // 是否显示字数
|
||||
clearable: { type: Boolean, value: true }, // 是否显示清除按钮
|
||||
error: { type: String, value: '' }, // 错误文案,非空时显示错误态
|
||||
disabled: { type: Boolean, value: false },
|
||||
password: { type: Boolean, value: false },
|
||||
confirmType: { type: String, value: 'done' }, // send|search|next|go|done
|
||||
inputAlign: { type: String, value: 'left' }, // left|right
|
||||
},
|
||||
methods: {
|
||||
onInput(e) {
|
||||
this.triggerEvent('input', { value: e.detail.value });
|
||||
},
|
||||
onFocus(e) {
|
||||
this.triggerEvent('focus', e.detail);
|
||||
},
|
||||
onBlur(e) {
|
||||
this.triggerEvent('blur', { value: e.detail.value });
|
||||
},
|
||||
onConfirm(e) {
|
||||
this.triggerEvent('confirm', { value: e.detail.value });
|
||||
},
|
||||
onClear() {
|
||||
this.triggerEvent('input', { value: '' });
|
||||
this.triggerEvent('clear', {});
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<view class="ui-input {{error ? 'ui-input--error' : ''}} {{disabled ? 'ui-input--disabled' : ''}}">
|
||||
<view wx:if="{{label}}" class="ui-input__label">{{label}}</view>
|
||||
<view class="ui-input__field-wrap">
|
||||
<input
|
||||
class="ui-input__field"
|
||||
style="text-align: {{inputAlign}};"
|
||||
type="{{type}}"
|
||||
password="{{password}}"
|
||||
value="{{value}}"
|
||||
placeholder="{{placeholder}}"
|
||||
placeholder-class="ui-input__placeholder"
|
||||
maxlength="{{maxlength}}"
|
||||
disabled="{{disabled}}"
|
||||
confirm-type="{{confirmType}}"
|
||||
bindinput="onInput"
|
||||
bindfocus="onFocus"
|
||||
bindblur="onBlur"
|
||||
bindconfirm="onConfirm"
|
||||
/>
|
||||
<view
|
||||
wx:if="{{clearable && value && !disabled}}"
|
||||
class="ui-input__clear"
|
||||
catchtap="onClear"
|
||||
>×</view>
|
||||
</view>
|
||||
<view wx:if="{{showCount || error}}" class="ui-input__footer">
|
||||
<view wx:if="{{error}}" class="ui-input__error">{{error}}</view>
|
||||
<view wx:if="{{showCount}}" class="ui-input__count">
|
||||
{{value.length}}{{maxlength > 0 ? '/' + maxlength : ''}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,69 @@
|
||||
.ui-input {
|
||||
padding: var(--spacing-3) 0;
|
||||
border-bottom: 1rpx solid var(--color-border);
|
||||
position: relative;
|
||||
}
|
||||
.ui-input__label {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-tertiary);
|
||||
margin-bottom: var(--spacing-2);
|
||||
}
|
||||
.ui-input__field-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.ui-input__field {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: var(--font-size-base);
|
||||
color: var(--color-text-primary);
|
||||
padding: var(--spacing-2) 0;
|
||||
line-height: var(--line-height-normal);
|
||||
}
|
||||
.ui-input__placeholder {
|
||||
color: var(--color-text-placeholder);
|
||||
}
|
||||
.ui-input__clear {
|
||||
flex-shrink: 0;
|
||||
margin-left: var(--spacing-2);
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
border-radius: 50%;
|
||||
background: var(--color-text-disabled);
|
||||
color: #FFFFFF;
|
||||
font-size: 28rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
}
|
||||
.ui-input__footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-top: var(--spacing-1);
|
||||
min-height: 28rpx;
|
||||
}
|
||||
.ui-input__error {
|
||||
color: var(--color-danger);
|
||||
font-size: var(--font-size-xs);
|
||||
flex: 1;
|
||||
}
|
||||
.ui-input__count {
|
||||
color: var(--color-text-tertiary);
|
||||
font-size: var(--font-size-xs);
|
||||
margin-left: var(--spacing-2);
|
||||
}
|
||||
.ui-input--error .ui-input__field {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
.ui-input--error {
|
||||
border-bottom-color: var(--color-border-error);
|
||||
}
|
||||
.ui-input--disabled .ui-input__field {
|
||||
color: var(--color-text-disabled);
|
||||
background: var(--color-bg-disabled);
|
||||
border-radius: var(--radius-sm);
|
||||
padding-left: var(--spacing-2);
|
||||
padding-right: var(--spacing-2);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* LikersBadge 喜欢我的人计数徽标(规范 §22 / §107)
|
||||
*
|
||||
* 不变量(AGENTS.md 第 6 条):单向意向不泄露——
|
||||
* - 仅展示计数("3 人喜欢你"),不展示具体身份列表入口。
|
||||
* - 详情由用户主动进入"喜欢我的人"页查看,且后端对该列表做匿名化处理
|
||||
* (未 Mutual 前对方不会被告知你看过了谁)。
|
||||
*
|
||||
* 用法:
|
||||
* <ui-likers-badge count="{{likersCount}}" bind:tap="onViewLikers" />
|
||||
*/
|
||||
Component({
|
||||
options: { addGlobalClass: true },
|
||||
properties: {
|
||||
count: { type: Number, value: 0 },
|
||||
size: { type: String, value: 'md' }, // sm | md | lg
|
||||
},
|
||||
data: {
|
||||
displayText: '',
|
||||
},
|
||||
observers: {
|
||||
count(n) {
|
||||
let text;
|
||||
if (n <= 0) text = '还没有人喜欢你';
|
||||
else if (n === 1) text = '1 人喜欢你';
|
||||
else text = `${n} 人喜欢你`;
|
||||
this.setData({ displayText: text });
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onTap() {
|
||||
if (this.data.count <= 0) return;
|
||||
this.triggerEvent('tap', { count: this.data.count });
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<view
|
||||
class="ui-likers ui-likers--{{size}} {{count > 0 ? 'ui-likers--active' : ''}}"
|
||||
bindtap="onTap"
|
||||
>
|
||||
<view class="ui-likers__icon">
|
||||
<view class="ui-likers__heart"></view>
|
||||
</view>
|
||||
<text class="ui-likers__text">{{displayText}}</text>
|
||||
</view>
|
||||
@@ -0,0 +1,42 @@
|
||||
.ui-likers {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: var(--color-bg-card);
|
||||
border-radius: var(--radius-full);
|
||||
border: 1rpx solid var(--color-border);
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
.ui-likers--active {
|
||||
background: var(--color-primary-light);
|
||||
border-color: var(--color-primary-light);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.ui-likers--sm {
|
||||
padding: 6rpx 14rpx;
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
.ui-likers--md {
|
||||
padding: 8rpx 18rpx;
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
.ui-likers--lg {
|
||||
padding: 12rpx 24rpx;
|
||||
font-size: var(--font-size-md);
|
||||
}
|
||||
.ui-likers__icon {
|
||||
width: 24rpx;
|
||||
height: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 8rpx;
|
||||
}
|
||||
.ui-likers__heart {
|
||||
width: 20rpx;
|
||||
height: 18rpx;
|
||||
background: currentColor;
|
||||
clip-path: polygon(50% 100%, 0 38%, 18% 8%, 50% 28%, 82% 8%, 100% 38%);
|
||||
}
|
||||
.ui-likers__text {
|
||||
font-size: inherit;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Loading 加载(规范 §21 推荐列表加载状态)
|
||||
*
|
||||
* 两种形态:
|
||||
* - mode="spinner":圆形旋转(页面级首次加载)
|
||||
* - mode="skeleton":骨架屏(列表渐进加载,避免抖动)
|
||||
* 用法:
|
||||
* <ui-loading mode="spinner" text="正在为你挑选…" />
|
||||
* <ui-loading mode="skeleton" rows="3" />
|
||||
*/
|
||||
Component({
|
||||
options: { addGlobalClass: true },
|
||||
properties: {
|
||||
mode: { type: String, value: 'spinner' }, // spinner|skeleton
|
||||
text: { type: String, value: '' },
|
||||
rows: { type: Number, value: 3 }, // skeleton 模式行数
|
||||
inline: { type: Boolean, value: false }, // 是否行内显示
|
||||
},
|
||||
data: {
|
||||
rowList: [],
|
||||
},
|
||||
observers: {
|
||||
rows(n) {
|
||||
const arr = [];
|
||||
for (let i = 0; i < n; i++) arr.push(i);
|
||||
this.setData({ rowList: arr });
|
||||
},
|
||||
},
|
||||
lifetimes: {
|
||||
attached() {
|
||||
const arr = [];
|
||||
for (let i = 0; i < this.data.rows; i++) arr.push(i);
|
||||
this.setData({ rowList: arr });
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<view class="ui-loading {{inline ? 'ui-loading--inline' : 'ui-loading--block'}}">
|
||||
<block wx:if="{{mode === 'spinner'}}">
|
||||
<view class="ui-loading__spinner">
|
||||
<view class="ui-loading__spinner-ring"></view>
|
||||
</view>
|
||||
<text wx:if="{{text}}" class="ui-loading__text">{{text}}</text>
|
||||
</block>
|
||||
<block wx:elif="{{mode === 'skeleton'}}">
|
||||
<view class="ui-loading__skeleton">
|
||||
<view wx:for="{{rowList}}" wx:key="*this" class="ui-loading__skeleton-row">
|
||||
<view class="ui-loading__skeleton-avatar"></view>
|
||||
<view class="ui-loading__skeleton-content">
|
||||
<view class="ui-loading__skeleton-line ui-loading__skeleton-line--title"></view>
|
||||
<view class="ui-loading__skeleton-line ui-loading__skeleton-line--meta"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
@@ -0,0 +1,94 @@
|
||||
.ui-loading--block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-8) var(--spacing-page-x);
|
||||
}
|
||||
.ui-loading--inline {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
.ui-loading__spinner {
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.ui-loading__spinner-ring {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 50%;
|
||||
border: 4rpx solid var(--color-divider);
|
||||
border-top-color: var(--color-primary);
|
||||
animation: ui-loading-spin 0.8s linear infinite;
|
||||
}
|
||||
@keyframes ui-loading-spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.ui-loading__text {
|
||||
margin-top: var(--spacing-3);
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
|
||||
/* Skeleton */
|
||||
.ui-loading__skeleton {
|
||||
width: 100%;
|
||||
}
|
||||
.ui-loading__skeleton-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--spacing-3) var(--spacing-page-x);
|
||||
background: var(--color-bg-card);
|
||||
border-bottom: 1rpx solid var(--color-divider);
|
||||
}
|
||||
.ui-loading__skeleton-avatar {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 50%;
|
||||
background: var(--color-bg-disabled);
|
||||
margin-right: var(--spacing-3);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ui-loading__skeleton-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.ui-loading__skeleton-line {
|
||||
height: 24rpx;
|
||||
background: var(--color-bg-disabled);
|
||||
border-radius: var(--radius-sm);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.ui-loading__skeleton-line--title {
|
||||
width: 40%;
|
||||
margin-bottom: var(--spacing-2);
|
||||
}
|
||||
.ui-loading__skeleton-line--meta {
|
||||
width: 70%;
|
||||
}
|
||||
/* shimmer */
|
||||
.ui-loading__skeleton-line::after,
|
||||
.ui-loading__skeleton-avatar::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
transparent 0%,
|
||||
rgba(255, 255, 255, 0.55) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
animation: ui-loading-shimmer 1.4s ease-in-out infinite;
|
||||
}
|
||||
@keyframes ui-loading-shimmer {
|
||||
0% { transform: translateX(-100%); }
|
||||
100% { transform: translateX(100%); }
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* MatchmakerCard 红娘服务卡片(规范 §48-50)
|
||||
*
|
||||
* 展示我申请的红娘服务进度,含状态、所属机构、最近跟进时间。
|
||||
* 不变量(AGENTS.md 第 5 条):红娘不可替用户操作——本卡片不暴露任何
|
||||
* "代用户 Like/Pass"入口,红娘推荐列表走 matchStore.like/pass(userId)。
|
||||
* 不变量(AGENTS.md 第 1 条):用户永久免费——本卡片不展示任何付费红娘/会员红娘字段。
|
||||
*/
|
||||
Component({
|
||||
options: { addGlobalClass: true },
|
||||
properties: {
|
||||
service: { type: Object, value: null },
|
||||
showRecommendations: { type: Boolean, value: false },
|
||||
},
|
||||
data: {
|
||||
statusLabelMap: {
|
||||
submitted: '已提交申请',
|
||||
assigned: '红娘已分配',
|
||||
in_service: '服务中',
|
||||
waiting_user: '等待你的回应',
|
||||
completed: '服务完成',
|
||||
cancelled: '已取消',
|
||||
},
|
||||
statusKindMap: {
|
||||
submitted: 'info',
|
||||
assigned: 'info',
|
||||
in_service: 'success',
|
||||
waiting_user: 'warning',
|
||||
completed: 'disabled',
|
||||
cancelled: 'disabled',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onViewDetail() {
|
||||
if (!this.data.service) return;
|
||||
this.triggerEvent('view', { serviceId: this.data.service.serviceId });
|
||||
},
|
||||
onViewRecommendations() {
|
||||
if (!this.data.service) return;
|
||||
this.triggerEvent('recommendations', { serviceId: this.data.service.serviceId });
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"ui-status-badge": "../status-badge/index",
|
||||
"ui-button": "../button/index"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<view class="ui-mm" wx:if="{{service}}">
|
||||
<view class="ui-mm__header" bindtap="onViewDetail">
|
||||
<view class="ui-mm__title-row">
|
||||
<text class="ui-mm__title">公益红娘服务</text>
|
||||
<ui-status-badge
|
||||
kind="{{statusKindMap[service.status]}}"
|
||||
label="{{statusLabelMap[service.status]}}"
|
||||
/>
|
||||
</view>
|
||||
<view wx:if="{{service.matchmakerName}}" class="ui-mm__mm-info">
|
||||
<text>红娘:{{service.matchmakerName}}</text>
|
||||
<text wx:if="{{service.matchmakerOrg}}"> · {{service.matchmakerOrg}}</text>
|
||||
</view>
|
||||
<view class="ui-mm__req">
|
||||
<text class="ui-mm__req-label">你的需求:</text>
|
||||
<text class="ui-mm__req-content">{{service.requirement}}</text>
|
||||
</view>
|
||||
<view wx:if="{{service.lastFollowUpAt}}" class="ui-mm__follow-up">
|
||||
最近跟进:{{service.lastFollowUpAtText || '近期'}}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view wx:if="{{showRecommendations && service.status === 'in_service' || service.status === 'waiting_user'}}" class="ui-mm__footer">
|
||||
<ui-button
|
||||
variant="secondary"
|
||||
size="md"
|
||||
block
|
||||
bindtap="onViewRecommendations"
|
||||
>查看红娘推荐</ui-button>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,52 @@
|
||||
.ui-mm {
|
||||
background: var(--color-bg-card);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: var(--spacing-4);
|
||||
margin-bottom: var(--spacing-3);
|
||||
box-shadow: var(--shadow-card);
|
||||
}
|
||||
.ui-mm__header {
|
||||
display: block;
|
||||
}
|
||||
.ui-mm__title-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--spacing-2);
|
||||
}
|
||||
.ui-mm__title {
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.ui-mm__mm-info {
|
||||
font-size: var(--font-size-md);
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: var(--spacing-2);
|
||||
}
|
||||
.ui-mm__req {
|
||||
padding: var(--spacing-2) var(--spacing-3);
|
||||
background: var(--color-bg-disabled);
|
||||
border-radius: var(--radius-sm);
|
||||
margin-bottom: var(--spacing-2);
|
||||
}
|
||||
.ui-mm__req-label {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
.ui-mm__req-content {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-secondary);
|
||||
line-height: var(--line-height-normal);
|
||||
display: block;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
.ui-mm__follow-up {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
.ui-mm__footer {
|
||||
margin-top: var(--spacing-3);
|
||||
padding-top: var(--spacing-3);
|
||||
border-top: 1rpx solid var(--color-divider);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
/**
|
||||
* MessageBubble 聊天气泡(规范 §61-63)
|
||||
*
|
||||
* bubbleType: self / other / system
|
||||
* self:右对齐,主红气泡
|
||||
* other:左对齐,白色气泡
|
||||
* system:居中,灰色文字(如"成功牵手"系统提示)
|
||||
*
|
||||
* 失败状态:sendStatus='failed' 时显示重试图标。
|
||||
*/
|
||||
Component({
|
||||
options: { addGlobalClass: true },
|
||||
properties: {
|
||||
bubbleType: { type: String, value: 'other' },
|
||||
content: { type: String, value: '' },
|
||||
sentAt: { type: Number, value: 0 },
|
||||
sentAtText: { type: String, value: '' },
|
||||
avatarUrl: { type: String, value: '' },
|
||||
showAvatar: { type: Boolean, value: true },
|
||||
showTime: { type: Boolean, value: false },
|
||||
sendStatus: { type: String, value: 'sent' }, // sending|sent|failed
|
||||
},
|
||||
methods: {
|
||||
onRetry() {
|
||||
if (this.data.sendStatus !== 'failed') return;
|
||||
this.triggerEvent('retry', {});
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"ui-avatar": "../avatar/index"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<view class="ui-bubble ui-bubble--{{bubbleType}}">
|
||||
<view wx:if="{{showTime && sentAtText}}" class="ui-bubble__time">
|
||||
<text>{{sentAtText}}</text>
|
||||
</view>
|
||||
<block wx:if="{{bubbleType === 'system'}}">
|
||||
<view class="ui-bubble__system">{{content}}</view>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<view class="ui-bubble__row">
|
||||
<ui-avatar
|
||||
wx:if="{{bubbleType === 'other' && showAvatar}}"
|
||||
src="{{avatarUrl}}"
|
||||
size="sm"
|
||||
/>
|
||||
<view class="ui-bubble__content-wrap">
|
||||
<view class="ui-bubble__content">{{content}}</view>
|
||||
<view
|
||||
wx:if="{{bubbleType === 'self' && sendStatus === 'failed'}}"
|
||||
class="ui-bubble__retry"
|
||||
catchtap="onRetry"
|
||||
>重发</view>
|
||||
<view wx:if="{{bubbleType === 'self' && sendStatus === 'sending'}}" class="ui-bubble__sending">
|
||||
<view class="ui-bubble__sending-dot"></view>
|
||||
</view>
|
||||
</view>
|
||||
<ui-avatar
|
||||
wx:if="{{bubbleType === 'self' && showAvatar}}"
|
||||
src="{{avatarUrl}}"
|
||||
size="sm"
|
||||
/>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
@@ -0,0 +1,78 @@
|
||||
.ui-bubble {
|
||||
padding: var(--spacing-1) var(--spacing-page-x);
|
||||
}
|
||||
.ui-bubble__time {
|
||||
text-align: center;
|
||||
margin: var(--spacing-3) 0;
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
.ui-bubble__system {
|
||||
text-align: center;
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--color-text-tertiary);
|
||||
background: var(--color-bg-disabled);
|
||||
padding: var(--spacing-1) var(--spacing-3);
|
||||
border-radius: var(--radius-sm);
|
||||
display: inline-block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.ui-bubble__row {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.ui-bubble--other .ui-bubble__row {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.ui-bubble--self .ui-bubble__row {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.ui-bubble__content-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 70%;
|
||||
margin: 0 var(--spacing-2);
|
||||
}
|
||||
.ui-bubble--self .ui-bubble__content-wrap {
|
||||
align-items: flex-end;
|
||||
}
|
||||
.ui-bubble__content {
|
||||
padding: var(--spacing-2) var(--spacing-3);
|
||||
border-radius: var(--radius-lg);
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-normal);
|
||||
word-break: break-all;
|
||||
}
|
||||
.ui-bubble--other .ui-bubble__content {
|
||||
background: var(--color-bg-card);
|
||||
color: var(--color-text-primary);
|
||||
border-top-left-radius: var(--radius-sm);
|
||||
}
|
||||
.ui-bubble--self .ui-bubble__content {
|
||||
background: var(--color-primary);
|
||||
color: #FFFFFF;
|
||||
border-top-right-radius: var(--radius-sm);
|
||||
}
|
||||
.ui-bubble__retry {
|
||||
margin-top: var(--spacing-1);
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-danger);
|
||||
align-self: flex-end;
|
||||
}
|
||||
.ui-bubble__sending {
|
||||
margin-top: var(--spacing-1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.ui-bubble__sending-dot {
|
||||
width: 16rpx;
|
||||
height: 16rpx;
|
||||
border-radius: 50%;
|
||||
background: var(--color-text-disabled);
|
||||
animation: ui-bubble-pulse 1s ease-in-out infinite;
|
||||
}
|
||||
@keyframes ui-bubble-pulse {
|
||||
0%, 100% { opacity: 0.3; }
|
||||
50% { opacity: 1; }
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* NavBar 自定义导航栏(规范 §17)
|
||||
*
|
||||
* 用于非 tabBar 页面:返回按钮 + 标题 + 右侧操作 slot。
|
||||
* statusBarHeight / navBarHeight 由调用方 app.js 通过 wx.getSystemInfoSync 注入。
|
||||
* 用法:
|
||||
* <ui-nav-bar title="编辑资料" show-back bind:back="onBack" />
|
||||
*/
|
||||
Component({
|
||||
options: { addGlobalClass: true, multipleSlots: true },
|
||||
properties: {
|
||||
title: { type: String, value: '' },
|
||||
showBack: { type: Boolean, value: true },
|
||||
statusBarHeight: { type: Number, value: 20 },
|
||||
navBarHeight: { type: Number, value: 44 },
|
||||
bg: { type: String, value: 'card' }, // card | transparent
|
||||
},
|
||||
data: {
|
||||
containerStyle: '',
|
||||
},
|
||||
observers: {
|
||||
'statusBarHeight, navBarHeight'(s, n) {
|
||||
this.setData({
|
||||
containerStyle: `padding-top: ${s}px; height: ${s + n}px;`,
|
||||
});
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onBack() {
|
||||
this.triggerEvent('back', {});
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<view class="ui-nav ui-nav--{{bg}}" style="{{containerStyle}}">
|
||||
<view class="ui-nav__bar">
|
||||
<view wx:if="{{showBack}}" class="ui-nav__back" bindtap="onBack">
|
||||
<view class="ui-nav__back-arrow"></view>
|
||||
</view>
|
||||
<view wx:else class="ui-nav__placeholder"></view>
|
||||
<view class="ui-nav__title">{{title}}</view>
|
||||
<view class="ui-nav__right">
|
||||
<slot name="right"></slot>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,54 @@
|
||||
.ui-nav {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: var(--z-sticky);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.ui-nav--card {
|
||||
background: var(--color-bg-card);
|
||||
border-bottom: 1rpx solid var(--color-divider);
|
||||
}
|
||||
.ui-nav--transparent {
|
||||
background: transparent;
|
||||
}
|
||||
.ui-nav__bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 88rpx;
|
||||
padding: 0 var(--spacing-2);
|
||||
}
|
||||
.ui-nav__back,
|
||||
.ui-nav__placeholder {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ui-nav__back-arrow {
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
border-left: 4rpx solid var(--color-text-primary);
|
||||
border-bottom: 4rpx solid var(--color-text-primary);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.ui-nav__title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
color: var(--color-text-primary);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.ui-nav__right {
|
||||
flex-shrink: 0;
|
||||
min-width: 64rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* PhotoUploader 照片上传器(规范 §22, §115 PHOTO_STATUS)
|
||||
*
|
||||
* 多图上传 + 审核状态展示 + 排序 + 删除。
|
||||
* 不变量(AGENTS.md 第 8 条):本组件不接收身份证号/手机号等敏感字段,
|
||||
* 仅处理生活照 URL(已上传至 OSS 后由 Profile.photos 引用)。
|
||||
* 上传凭证由调用方通过 profileService.requestUploadToken 获取后注入。
|
||||
*
|
||||
* 用法:
|
||||
* <ui-photo-uploader photos="{{photos}}" max="{{6}}"
|
||||
* bind:add="onAddPhoto" bind:remove="onRemovePhoto" bind:tap="onPreviewPhoto" />
|
||||
*/
|
||||
Component({
|
||||
options: { addGlobalClass: true },
|
||||
properties: {
|
||||
photos: { type: Array, value: [] },
|
||||
max: { type: Number, value: 6 },
|
||||
readonly: { type: Boolean, value: false },
|
||||
// 是否显示审核状态徽标
|
||||
showReviewStatus: { type: Boolean, value: true },
|
||||
},
|
||||
data: {
|
||||
photoStatusKindMap: {
|
||||
local: 'warning',
|
||||
uploading: 'info',
|
||||
uploaded: 'success',
|
||||
failed: 'danger',
|
||||
review_pending: 'warning',
|
||||
},
|
||||
photoStatusLabelMap: {
|
||||
local: '本地',
|
||||
uploading: '上传中',
|
||||
uploaded: '已上传',
|
||||
failed: '失败',
|
||||
review_pending: '审核中',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onAdd() {
|
||||
if (this.data.readonly) return;
|
||||
if (this.data.photos.length >= this.data.max) return;
|
||||
this.triggerEvent('add', {});
|
||||
},
|
||||
onRemove(e) {
|
||||
if (this.data.readonly) return;
|
||||
const { index } = e.currentTarget.dataset;
|
||||
this.triggerEvent('remove', { index });
|
||||
},
|
||||
onTap(e) {
|
||||
const { index } = e.currentTarget.dataset;
|
||||
this.triggerEvent('tap', { index });
|
||||
},
|
||||
onRetry(e) {
|
||||
const { index } = e.currentTarget.dataset;
|
||||
this.triggerEvent('retry', { index });
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"ui-status-badge": "../status-badge/index"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<view class="ui-photo">
|
||||
<view class="ui-photo__list">
|
||||
<view
|
||||
wx:for="{{photos}}"
|
||||
wx:key="index"
|
||||
class="ui-photo__item"
|
||||
data-index="{{index}}"
|
||||
bindtap="onTap"
|
||||
>
|
||||
<image class="ui-photo__img" src="{{item.url}}" mode="aspectFill" />
|
||||
<view wx:if="{{item.status && item.status !== 'uploaded'}}" class="ui-photo__status">
|
||||
<ui-status-badge
|
||||
kind="{{photoStatusKindMap[item.status]}}"
|
||||
label="{{photoStatusLabelMap[item.status]}}"
|
||||
/>
|
||||
</view>
|
||||
<view
|
||||
wx:if="{{!readonly && item.status !== 'uploading'}}"
|
||||
class="ui-photo__remove"
|
||||
catchtap="onRemove"
|
||||
data-index="{{index}}"
|
||||
>×</view>
|
||||
<view
|
||||
wx:if="{{item.status === 'failed'}}"
|
||||
class="ui-photo__retry"
|
||||
catchtap="onRetry"
|
||||
data-index="{{index}}"
|
||||
>重试</view>
|
||||
<view wx:if="{{item.status === 'uploading'}}" class="ui-photo__mask">
|
||||
<view class="ui-photo__spinner"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
wx:if="{{!readonly && photos.length < max}}"
|
||||
class="ui-photo__add"
|
||||
bindtap="onAdd"
|
||||
>
|
||||
<view class="ui-photo__add-icon">+</view>
|
||||
<text class="ui-photo__add-text">{{photos.length}}/{{max}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="ui-photo__hint">
|
||||
<text>最多 {{max}} 张,建议真人生活照,避免过度美颜</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,99 @@
|
||||
.ui-photo__list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-2);
|
||||
}
|
||||
.ui-photo__item,
|
||||
.ui-photo__add {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
border-radius: var(--radius-md);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.ui-photo__item {
|
||||
background: var(--color-bg-disabled);
|
||||
}
|
||||
.ui-photo__img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.ui-photo__status {
|
||||
position: absolute;
|
||||
top: 8rpx;
|
||||
left: 8rpx;
|
||||
}
|
||||
.ui-photo__remove {
|
||||
position: absolute;
|
||||
top: 8rpx;
|
||||
right: 8rpx;
|
||||
width: 36rpx;
|
||||
height: 36rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
color: #FFFFFF;
|
||||
font-size: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
}
|
||||
.ui-photo__retry {
|
||||
position: absolute;
|
||||
bottom: 8rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: var(--radius-full);
|
||||
background: var(--color-danger);
|
||||
color: #FFFFFF;
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
.ui-photo__mask {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.ui-photo__spinner {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border: 3rpx solid rgba(255, 255, 255, 0.5);
|
||||
border-top-color: #FFFFFF;
|
||||
border-radius: 50%;
|
||||
animation: ui-photo-spin 0.8s linear infinite;
|
||||
}
|
||||
@keyframes ui-photo-spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.ui-photo__add {
|
||||
background: var(--color-bg-disabled);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2rpx dashed var(--color-text-disabled);
|
||||
}
|
||||
.ui-photo__add-icon {
|
||||
font-size: var(--font-size-2xl);
|
||||
color: var(--color-text-tertiary);
|
||||
line-height: 1;
|
||||
}
|
||||
.ui-photo__add-text {
|
||||
margin-top: var(--spacing-1);
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
.ui-photo__hint {
|
||||
margin-top: var(--spacing-2);
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-tertiary);
|
||||
line-height: var(--line-height-normal);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
/**
|
||||
* PreferenceEditor 择偶条件编辑器(规范 §30)
|
||||
*
|
||||
* 每条条件有两个 importance 等级:
|
||||
* - required:必须符合(AGENTS 第 2 条硬约束)
|
||||
* - preferred:最好符合(软约束,后端可适度放宽匹配)
|
||||
*
|
||||
* 不变量(AGENTS.md 第 2 条):用户已勾选"必须符合"的字段不可被前端自动放宽为
|
||||
* "最好符合",UI 必须显式提示用户此操作将影响匹配精度。
|
||||
*/
|
||||
Component({
|
||||
options: { addGlobalClass: true },
|
||||
properties: {
|
||||
preference: { type: Object, value: null },
|
||||
readonly: { type: Boolean, value: false },
|
||||
},
|
||||
data: {
|
||||
importanceLabelMap: {
|
||||
required: '必须符合',
|
||||
preferred: '最好符合',
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// 切换某字段的 importance:required ↔ preferred
|
||||
onToggleImportance(e) {
|
||||
if (this.data.readonly) return;
|
||||
const { field } = e.currentTarget.dataset;
|
||||
const pref = this.data.preference || {};
|
||||
const current = pref[field];
|
||||
if (!current) return;
|
||||
const next = current === 'required' ? 'preferred' : 'required';
|
||||
this.triggerEvent('change', {
|
||||
field,
|
||||
importance: next,
|
||||
// 必须符合 → 最好符合,需用户二次确认(AGENTS 第 2 条)
|
||||
needConfirmRelax: current === 'required',
|
||||
});
|
||||
},
|
||||
onAgeChange(e) {
|
||||
if (this.data.readonly) return;
|
||||
const { bound } = e.currentTarget.dataset;
|
||||
this.triggerEvent('change', {
|
||||
field: bound === 'min' ? 'ageMin' : 'ageMax',
|
||||
value: Number(e.detail.value),
|
||||
});
|
||||
},
|
||||
onHeightChange(e) {
|
||||
if (this.data.readonly) return;
|
||||
const { bound } = e.currentTarget.dataset;
|
||||
this.triggerEvent('change', {
|
||||
field: bound === 'min' ? 'heightMin' : 'heightMax',
|
||||
value: Number(e.detail.value) || null,
|
||||
});
|
||||
},
|
||||
onAcceptChildrenChange(e) {
|
||||
if (this.data.readonly) return;
|
||||
this.triggerEvent('change', {
|
||||
field: 'acceptChildren',
|
||||
value: e.detail.value,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"component": true,
|
||||
"usingComponents": {
|
||||
"ui-input": "../input/index",
|
||||
"ui-tag": "../tag/index"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<view class="ui-pref" wx:if="{{preference}}">
|
||||
<view class="ui-pref__hint">
|
||||
标记为"必须符合"的条件将作为硬约束,不会自动放宽;
|
||||
"最好符合"为软偏好,可能匹配到略不满足的对象。
|
||||
</view>
|
||||
|
||||
<!-- 年龄 -->
|
||||
<view class="ui-pref__row">
|
||||
<view class="ui-pref__label">年龄</view>
|
||||
<view class="ui-pref__range">
|
||||
<input
|
||||
class="ui-pref__num"
|
||||
type="number"
|
||||
value="{{preference.ageMin}}"
|
||||
placeholder="不限"
|
||||
data-bound="min"
|
||||
bindinput="onAgeChange"
|
||||
/>
|
||||
<text class="ui-pref__dash">~</text>
|
||||
<input
|
||||
class="ui-pref__num"
|
||||
type="number"
|
||||
value="{{preference.ageMax}}"
|
||||
placeholder="不限"
|
||||
data-bound="max"
|
||||
bindinput="onAgeChange"
|
||||
/>
|
||||
</view>
|
||||
<view
|
||||
class="ui-pref__importance ui-pref__importance--{{preference.ageImportance}}"
|
||||
data-field="ageImportance"
|
||||
bindtap="onToggleImportance"
|
||||
>{{importanceLabelMap[preference.ageImportance]}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 身高 -->
|
||||
<view class="ui-pref__row">
|
||||
<view class="ui-pref__label">身高</view>
|
||||
<view class="ui-pref__range">
|
||||
<input
|
||||
class="ui-pref__num"
|
||||
type="number"
|
||||
value="{{preference.heightMin}}"
|
||||
placeholder="不限"
|
||||
data-bound="min"
|
||||
bindinput="onHeightChange"
|
||||
/>
|
||||
<text class="ui-pref__dash">~</text>
|
||||
<input
|
||||
class="ui-pref__num"
|
||||
type="number"
|
||||
value="{{preference.heightMax}}"
|
||||
placeholder="不限"
|
||||
data-bound="max"
|
||||
bindinput="onHeightChange"
|
||||
/>
|
||||
</view>
|
||||
<view
|
||||
class="ui-pref__importance ui-pref__importance--{{preference.heightImportance}}"
|
||||
data-field="heightImportance"
|
||||
bindtap="onToggleImportance"
|
||||
>{{importanceLabelMap[preference.heightImportance]}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 地区 -->
|
||||
<view class="ui-pref__row">
|
||||
<view class="ui-pref__label">地区</view>
|
||||
<view class="ui-pref__tags">
|
||||
<ui-tag
|
||||
wx:for="{{preference.region}}"
|
||||
wx:key="*this"
|
||||
label="{{item}}"
|
||||
variant="primary"
|
||||
/>
|
||||
</view>
|
||||
<view
|
||||
class="ui-pref__importance ui-pref__importance--{{preference.regionImportance}}"
|
||||
data-field="regionImportance"
|
||||
bindtap="onToggleImportance"
|
||||
>{{importanceLabelMap[preference.regionImportance]}}</view>
|
||||
</view>
|
||||
|
||||
<!-- 婚姻状况 -->
|
||||
<view class="ui-pref__row">
|
||||
<view class="ui-pref__label">婚姻状况</view>
|
||||
<view class="ui-pref__tags">
|
||||
<ui-tag
|
||||
wx:for="{{preference.maritalStatusAccepted}}"
|
||||
wx:key="*this"
|
||||
label="{{item}}"
|
||||
variant="primary"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 学历 -->
|
||||
<view class="ui-pref__row">
|
||||
<view class="ui-pref__label">学历</view>
|
||||
<view class="ui-pref__tags">
|
||||
<ui-tag
|
||||
wx:for="{{preference.educationAccepted}}"
|
||||
wx:key="*this"
|
||||
label="{{item}}"
|
||||
variant="primary"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 是否接受有孩 -->
|
||||
<view class="ui-pref__row">
|
||||
<view class="ui-pref__label">子女</view>
|
||||
<picker
|
||||
class="ui-pref__picker"
|
||||
value="{{preference.acceptChildren}}"
|
||||
range="{{['any', 'accept', 'reject']}}"
|
||||
range-key=""
|
||||
bindchange="onAcceptChildrenChange"
|
||||
>
|
||||
<view class="ui-pref__picker-value">
|
||||
{{preference.acceptChildren === 'any' ? '不限' : preference.acceptChildren === 'accept' ? '可接受' : '不接受'}}
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,72 @@
|
||||
.ui-pref__hint {
|
||||
padding: var(--spacing-2) var(--spacing-3);
|
||||
background: var(--color-warning-light);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-warning);
|
||||
line-height: var(--line-height-normal);
|
||||
margin-bottom: var(--spacing-3);
|
||||
}
|
||||
.ui-pref__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--spacing-3) 0;
|
||||
border-bottom: 1rpx solid var(--color-divider);
|
||||
}
|
||||
.ui-pref__label {
|
||||
width: 140rpx;
|
||||
font-size: var(--font-size-md);
|
||||
color: var(--color-text-secondary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ui-pref__range {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.ui-pref__num {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
height: 64rpx;
|
||||
padding: 0 var(--spacing-2);
|
||||
border: 1rpx solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--font-size-md);
|
||||
text-align: center;
|
||||
}
|
||||
.ui-pref__dash {
|
||||
margin: 0 var(--spacing-2);
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||
.ui-pref__tags {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--spacing-1);
|
||||
}
|
||||
.ui-pref__picker {
|
||||
flex: 1;
|
||||
}
|
||||
.ui-pref__picker-value {
|
||||
padding: var(--spacing-2) 0;
|
||||
font-size: var(--font-size-md);
|
||||
color: var(--color-text-primary);
|
||||
}
|
||||
.ui-pref__importance {
|
||||
margin-left: var(--spacing-2);
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--font-size-xs);
|
||||
font-weight: var(--font-weight-medium);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.ui-pref__importance--required {
|
||||
background: var(--color-danger-light);
|
||||
color: var(--color-danger);
|
||||
}
|
||||
.ui-pref__importance--preferred {
|
||||
background: var(--color-bg-disabled);
|
||||
color: var(--color-text-tertiary);
|
||||
}
|
||||