feat: add miniapp reference assets and primitives
|
After Width: | Height: | Size: 116 KiB |
|
After Width: | Height: | Size: 872 B |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 106 KiB |
@@ -1,6 +1,6 @@
|
|||||||
<view class="ui-empty">
|
<view class="ui-empty">
|
||||||
<view class="ui-empty__illustration ui-empty__illustration--{{illustration}}">
|
<view class="ui-empty__illustration ui-empty__illustration--{{illustration}}">
|
||||||
<view class="ui-empty__icon"></view>
|
<image class="ui-empty__icon" src="/assets/icons/ui/empty.png" mode="aspectFit" />
|
||||||
</view>
|
</view>
|
||||||
<text class="ui-empty__title">{{title}}</text>
|
<text class="ui-empty__title">{{title}}</text>
|
||||||
<text wx:if="{{description}}" class="ui-empty__desc">{{description}}</text>
|
<text wx:if="{{description}}" class="ui-empty__desc">{{description}}</text>
|
||||||
|
|||||||
@@ -16,29 +16,9 @@
|
|||||||
margin-bottom: var(--spacing-4);
|
margin-bottom: var(--spacing-4);
|
||||||
}
|
}
|
||||||
.ui-empty__icon {
|
.ui-empty__icon {
|
||||||
width: 60rpx;
|
width: 72rpx;
|
||||||
height: 60rpx;
|
height: 72rpx;
|
||||||
background: var(--color-text-disabled);
|
opacity: 0.72;
|
||||||
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 {
|
.ui-empty__title {
|
||||||
font-size: var(--font-size-lg);
|
font-size: var(--font-size-lg);
|
||||||
|
|||||||
@@ -1 +1,11 @@
|
|||||||
Component({properties:{title:{type:String,value:'加载失败'},description:{type:String,value:'请稍后重试'}},methods:{onRetry(){this.triggerEvent('retry')}}})
|
Component({
|
||||||
|
properties: {
|
||||||
|
title: { type: String, value: '加载失败' },
|
||||||
|
description: { type: String, value: '请稍后重试' },
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onRetry() {
|
||||||
|
this.triggerEvent('retry')
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|||||||
@@ -1 +1,6 @@
|
|||||||
<view class="error-state"><view class="error-icon">!</view><text class="error-title">{{title}}</text><text class="error-desc">{{description}}</text><button class="retry-btn" bindtap="onRetry">重新加载</button></view>
|
<view class="error-state">
|
||||||
|
<image class="error-icon" src="/assets/icons/ui/warning.png" mode="aspectFit" />
|
||||||
|
<text class="error-title">{{title}}</text>
|
||||||
|
<text class="error-desc">{{description}}</text>
|
||||||
|
<button class="retry-btn" bindtap="onRetry">重新加载</button>
|
||||||
|
</view>
|
||||||
|
|||||||
@@ -1 +1,45 @@
|
|||||||
.error-state{padding:64rpx 32rpx;text-align:center;background:var(--color-bg-card);border-radius:var(--radius-lg)}.error-icon{width:72rpx;height:72rpx;margin:0 auto 20rpx;display:flex;align-items:center;justify-content:center;border-radius:50%;background:var(--color-danger-light);color:var(--color-danger);font-weight:700}.error-title,.error-desc{display:block}.error-title{font-size:30rpx;font-weight:600}.error-desc{margin-top:8rpx;font-size:24rpx;color:var(--color-text-tertiary)}.retry-btn{width:220rpx;height:80rpx;line-height:80rpx;margin-top:24rpx;border:0;border-radius:var(--radius-full);background:var(--color-primary);color:var(--color-text-inverse);font-size:26rpx}.retry-btn::after{border:0}
|
.error-state {
|
||||||
|
padding: 64rpx 32rpx;
|
||||||
|
text-align: center;
|
||||||
|
background: var(--color-bg-card);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-icon {
|
||||||
|
display: block;
|
||||||
|
width: 72rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
margin: 0 auto 20rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-title,
|
||||||
|
.error-desc {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-title {
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-desc {
|
||||||
|
margin-top: 8rpx;
|
||||||
|
color: var(--color-text-tertiary);
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-btn {
|
||||||
|
width: 220rpx;
|
||||||
|
height: 80rpx;
|
||||||
|
margin-top: 24rpx;
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
background: var(--color-primary);
|
||||||
|
color: var(--color-text-inverse);
|
||||||
|
font-size: 26rpx;
|
||||||
|
line-height: 80rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.retry-btn::after {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<view class="ui-loading {{inline ? 'ui-loading--inline' : 'ui-loading--block'}}">
|
<view class="ui-loading {{inline ? 'ui-loading--inline' : 'ui-loading--block'}}">
|
||||||
<block wx:if="{{mode === 'spinner'}}">
|
<block wx:if="{{mode === 'spinner'}}">
|
||||||
<view class="ui-loading__spinner">
|
<view class="ui-loading__spinner">
|
||||||
<view class="ui-loading__spinner-ring"></view>
|
<image class="ui-loading__spinner-icon" src="/assets/icons/ui/loading.png" mode="aspectFit" />
|
||||||
</view>
|
</view>
|
||||||
<text wx:if="{{text}}" class="ui-loading__text">{{text}}</text>
|
<text wx:if="{{text}}" class="ui-loading__text">{{text}}</text>
|
||||||
</block>
|
</block>
|
||||||
|
|||||||
@@ -16,12 +16,9 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
.ui-loading__spinner-ring {
|
.ui-loading__spinner-icon {
|
||||||
width: 48rpx;
|
width: 48rpx;
|
||||||
height: 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;
|
animation: ui-loading-spin 0.8s linear infinite;
|
||||||
}
|
}
|
||||||
@keyframes ui-loading-spin {
|
@keyframes ui-loading-spin {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<view class="ui-nav ui-nav--{{bg}}" style="{{containerStyle}}">
|
<view class="ui-nav ui-nav--{{bg}}" style="{{containerStyle}}">
|
||||||
<view class="ui-nav__bar">
|
<view class="ui-nav__bar">
|
||||||
<view wx:if="{{showBack}}" class="ui-nav__back" bindtap="onBack">
|
<view wx:if="{{showBack}}" class="ui-nav__back" bindtap="onBack">
|
||||||
<view class="ui-nav__back-arrow"></view>
|
<image class="ui-nav__back-icon" src="/assets/icons/ui/back.png" mode="aspectFit" />
|
||||||
</view>
|
</view>
|
||||||
<view wx:else class="ui-nav__placeholder"></view>
|
<view wx:else class="ui-nav__placeholder"></view>
|
||||||
<view class="ui-nav__title">{{title}}</view>
|
<view class="ui-nav__title">{{title}}</view>
|
||||||
|
|||||||
@@ -28,12 +28,9 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
.ui-nav__back-arrow {
|
.ui-nav__back-icon {
|
||||||
width: 20rpx;
|
width: 36rpx;
|
||||||
height: 20rpx;
|
height: 36rpx;
|
||||||
border-left: 4rpx solid var(--color-text-primary);
|
|
||||||
border-bottom: 4rpx solid var(--color-text-primary);
|
|
||||||
transform: rotate(45deg);
|
|
||||||
}
|
}
|
||||||
.ui-nav__title {
|
.ui-nav__title {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
@@ -5,6 +5,6 @@
|
|||||||
</view>
|
</view>
|
||||||
<view wx:if="{{action}}" class="ui-section__action" bindtap="onAction">
|
<view wx:if="{{action}}" class="ui-section__action" bindtap="onAction">
|
||||||
<text>{{action}}</text>
|
<text>{{action}}</text>
|
||||||
<text class="ui-section__arrow">›</text>
|
<image class="ui-section__arrow" src="/assets/icons/ui/chevron-right.png" mode="aspectFit" />
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|||||||
@@ -26,6 +26,6 @@
|
|||||||
}
|
}
|
||||||
.ui-section__arrow {
|
.ui-section__arrow {
|
||||||
margin-left: 4rpx;
|
margin-left: 4rpx;
|
||||||
font-size: var(--font-size-md);
|
width: 24rpx;
|
||||||
line-height: 1;
|
height: 24rpx;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
Component({
|
||||||
|
options: { addGlobalClass: true },
|
||||||
|
properties: {
|
||||||
|
eyebrow: { type: String, value: '' },
|
||||||
|
title: { type: String, value: '' },
|
||||||
|
subtitle: { type: String, value: '' },
|
||||||
|
imageSrc: { type: String, value: '' },
|
||||||
|
tone: { type: String, value: 'rose' },
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"component": true,
|
||||||
|
"usingComponents": {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<view class="service-hero service-hero--{{tone}}">
|
||||||
|
<image class="service-hero__image" src="{{imageSrc}}" mode="aspectFill" />
|
||||||
|
<view class="service-hero__veil"></view>
|
||||||
|
<view class="service-hero__content">
|
||||||
|
<text wx:if="{{eyebrow}}" class="service-hero__eyebrow">{{eyebrow}}</text>
|
||||||
|
<text class="service-hero__title">{{title}}</text>
|
||||||
|
<text wx:if="{{subtitle}}" class="service-hero__subtitle">{{subtitle}}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
.service-hero {
|
||||||
|
position: relative;
|
||||||
|
height: 300rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 0 0 32rpx 32rpx;
|
||||||
|
background: #fff4f7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-hero__image,
|
||||||
|
.service-hero__veil {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-hero__veil {
|
||||||
|
background: linear-gradient(90deg, rgba(255, 250, 252, 0.98) 0%, rgba(255, 250, 252, 0.82) 38%, rgba(255, 250, 252, 0.08) 70%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-hero--night .service-hero__veil {
|
||||||
|
background: linear-gradient(90deg, rgba(18, 24, 83, 0.92) 0%, rgba(34, 31, 111, 0.64) 42%, rgba(34, 31, 111, 0.08) 72%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-hero__content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
width: 54%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 36rpx;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-hero__eyebrow {
|
||||||
|
margin-bottom: 10rpx;
|
||||||
|
color: var(--color-primary);
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: 2rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-hero__title {
|
||||||
|
color: #24305e;
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.28;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-hero__subtitle {
|
||||||
|
margin-top: 14rpx;
|
||||||
|
color: #737c96;
|
||||||
|
font-size: 24rpx;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-hero--night .service-hero__eyebrow,
|
||||||
|
.service-hero--night .service-hero__title,
|
||||||
|
.service-hero--night .service-hero__subtitle {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
@@ -6,14 +6,14 @@ const { authStore, appStore } = require('../../stores')
|
|||||||
const api = require('../../utils/api')
|
const api = require('../../utils/api')
|
||||||
|
|
||||||
const ENTRY_VISUALS = {
|
const ENTRY_VISUALS = {
|
||||||
find_object: { tone: 'rose', iconAsset: '/assets/tabbar/discover-active.png' },
|
find_object: { tone: 'rose', iconAsset: '/assets/icons/home/find-object.png' },
|
||||||
matchmaker: { tone: 'purple', iconAsset: '/assets/brand/logo-mark.png' },
|
matchmaker: { tone: 'purple', iconAsset: '/assets/icons/home/matchmaker.png' },
|
||||||
activities: { tone: 'blue', iconAsset: '/assets/tabbar/activity-active.png' },
|
activities: { tone: 'blue', iconAsset: '/assets/icons/home/activity.png' },
|
||||||
counseling: { tone: 'pink', iconAsset: '/assets/tabbar/message-active.png' },
|
counseling: { tone: 'pink', iconAsset: '/assets/icons/home/counseling.png' },
|
||||||
classroom: { tone: 'amber', iconAsset: '/assets/covers/classroom-default.jpg' },
|
classroom: { tone: 'amber', iconAsset: '/assets/icons/home/classroom.png' },
|
||||||
premarital: { tone: 'indigo', iconAsset: '/assets/tabbar/home-active.png' },
|
premarital: { tone: 'indigo', iconAsset: '/assets/icons/home/premarital.png' },
|
||||||
anti_fraud: { tone: 'red', iconAsset: '/assets/covers/anti-fraud-default.jpg' },
|
anti_fraud: { tone: 'red', iconAsset: '/assets/icons/home/anti-fraud.png' },
|
||||||
service_station: { tone: 'green', iconAsset: '/assets/covers/station-default.jpg' },
|
service_station: { tone: 'green', iconAsset: '/assets/icons/home/service-station.png' },
|
||||||
}
|
}
|
||||||
|
|
||||||
const QUICK_ENTRIES = [
|
const QUICK_ENTRIES = [
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import assert from 'node:assert/strict'
|
import assert from 'node:assert/strict'
|
||||||
import { existsSync, readFileSync } from 'node:fs'
|
import { existsSync, readFileSync } from 'node:fs'
|
||||||
import { resolve } from 'node:path'
|
import { resolve } from 'node:path'
|
||||||
|
import { createHash } from 'node:crypto'
|
||||||
|
|
||||||
const root = resolve(import.meta.dirname, '..')
|
const root = resolve(import.meta.dirname, '..')
|
||||||
const assets = [
|
const assets = [
|
||||||
@@ -16,10 +17,55 @@ const assets = [
|
|||||||
'miniapp/assets/covers/station-default.jpg',
|
'miniapp/assets/covers/station-default.jpg',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const miniappHeroAssets = [
|
||||||
|
'miniapp/assets/brand/home-hero-v2.jpg',
|
||||||
|
'miniapp/assets/service/matchmaker-hero.jpg',
|
||||||
|
'miniapp/assets/service/counseling-hero.jpg',
|
||||||
|
'miniapp/assets/service/premarital-hero.jpg',
|
||||||
|
'miniapp/assets/service/anti-fraud-hero.jpg',
|
||||||
|
'miniapp/assets/service/station-hero.jpg',
|
||||||
|
]
|
||||||
|
|
||||||
|
const homeIconAssets = [
|
||||||
|
'find-object.png',
|
||||||
|
'matchmaker.png',
|
||||||
|
'activity.png',
|
||||||
|
'counseling.png',
|
||||||
|
'classroom.png',
|
||||||
|
'premarital.png',
|
||||||
|
'anti-fraud.png',
|
||||||
|
'service-station.png',
|
||||||
|
].map((name) => `miniapp/assets/icons/home/${name}`)
|
||||||
|
|
||||||
|
const uiIconAssets = [
|
||||||
|
'back.png',
|
||||||
|
'chevron-right.png',
|
||||||
|
'warning.png',
|
||||||
|
'empty.png',
|
||||||
|
'loading.png',
|
||||||
|
'search.png',
|
||||||
|
].map((name) => `miniapp/assets/icons/ui/${name}`)
|
||||||
|
|
||||||
for (const file of assets) {
|
for (const file of assets) {
|
||||||
const path = resolve(root, file)
|
const path = resolve(root, file)
|
||||||
assert.ok(existsSync(path), `缺少品牌资产 ${file}`)
|
assert.ok(existsSync(path), `缺少品牌资产 ${file}`)
|
||||||
assert.ok(readFileSync(path).length > 4096, `品牌资产为空或过小 ${file}`)
|
assert.ok(readFileSync(path).length > 4096, `品牌资产为空或过小 ${file}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const file of miniappHeroAssets) {
|
||||||
|
const path = resolve(root, file)
|
||||||
|
assert.ok(existsSync(path), `缺少小程序 Hero 素材 ${file}`)
|
||||||
|
assert.ok(readFileSync(path).length > 16 * 1024, `小程序 Hero 素材为空或过小 ${file}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const homeIconHashes = new Set()
|
||||||
|
for (const file of [...homeIconAssets, ...uiIconAssets]) {
|
||||||
|
const path = resolve(root, file)
|
||||||
|
assert.ok(existsSync(path), `缺少小程序图标 ${file}`)
|
||||||
|
const bytes = readFileSync(path)
|
||||||
|
assert.ok(bytes.length > 256, `小程序图标为空或过小 ${file}`)
|
||||||
|
if (homeIconAssets.includes(file)) homeIconHashes.add(createHash('sha256').update(bytes).digest('hex'))
|
||||||
|
}
|
||||||
|
assert.equal(homeIconHashes.size, homeIconAssets.length, '首页 8 个入口不得复用同一图标')
|
||||||
|
|
||||||
console.log('Brand asset contracts passed')
|
console.log('Brand asset contracts passed')
|
||||||
|
|||||||