init: 初始化
99
CLAUDE.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
|
||||||
|
普拉提工作室预约与会员管理的微信小程序。TypeScript monorepo,包含三个包:
|
||||||
|
|
||||||
|
- **packages/server** — NestJS 后端(REST API、Prisma ORM、PostgreSQL)
|
||||||
|
- **packages/app** — Vue 3 + Pinia 前端,基于 Uni-app(目标平台 mp-weixin)
|
||||||
|
- **packages/shared** — 前后端共用的 TypeScript 类型、枚举和常量
|
||||||
|
|
||||||
|
## 常用命令
|
||||||
|
|
||||||
|
### 开发
|
||||||
|
```bash
|
||||||
|
pnpm dev:server # NestJS watch 模式 (localhost:3000)
|
||||||
|
pnpm dev:app # 微信小程序开发服务器
|
||||||
|
pnpm build:shared # 必须先构建 shared,再构建 server/app
|
||||||
|
```
|
||||||
|
|
||||||
|
### 测试(仅 server)
|
||||||
|
```bash
|
||||||
|
cd packages/server
|
||||||
|
pnpm test # 运行全部测试
|
||||||
|
pnpm test -- auth.service.spec # 运行单个测试文件
|
||||||
|
pnpm test:watch # watch 模式
|
||||||
|
pnpm test:cov # 覆盖率报告
|
||||||
|
```
|
||||||
|
|
||||||
|
Jest 配置内联在 `packages/server/package.json`。测试文件位于 `__tests__/` 子目录(如 `src/auth/__tests__/auth.service.spec.ts`),匹配模式:`*.spec.ts`。
|
||||||
|
|
||||||
|
### 数据库
|
||||||
|
```bash
|
||||||
|
cd packages/server
|
||||||
|
pnpm prisma:generate # schema 变更后重新生成 Prisma Client
|
||||||
|
pnpm prisma:migrate # 运行迁移(交互式)
|
||||||
|
pnpm prisma:seed # 填充种子数据
|
||||||
|
```
|
||||||
|
|
||||||
|
### 代码检查
|
||||||
|
```bash
|
||||||
|
pnpm lint # 所有包的 ESLint 检查
|
||||||
|
```
|
||||||
|
|
||||||
|
## 架构
|
||||||
|
|
||||||
|
### 数据流
|
||||||
|
```
|
||||||
|
微信小程序 → Uni-app (Vue 3) → REST API (NestJS) → Prisma → PostgreSQL
|
||||||
|
↕ ↕
|
||||||
|
Pinia stores @nestjs/schedule (定时任务)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 后端模块结构
|
||||||
|
每个功能是一个 NestJS 模块,遵循 controller → service → Prisma 模式。核心模块:
|
||||||
|
- **auth** — 微信 OAuth 登录(code2Session)、JWT 令牌、手机号绑定
|
||||||
|
- **booking** — 创建/取消预约,含会员卡验证和容量检查
|
||||||
|
- **time-slot** — 课程时段管理;`SlotGeneratorService` 根据 `WeekTemplate` 自动生成
|
||||||
|
- **membership** — 基于卡的会员制(TIMES 次卡、DURATION 时效卡、TRIAL 体验卡)
|
||||||
|
- **payment** — 微信支付集成,用于购卡
|
||||||
|
- **scheduler** — 定时任务:02:00 自动生成时段,02:30 清理过期时段
|
||||||
|
|
||||||
|
### 前端结构
|
||||||
|
- **pages/** — 按路由组织的页面(home、booking、card、profile、admin)
|
||||||
|
- **stores/** — Pinia 状态管理(user、booking、studio、admin)
|
||||||
|
- **utils/request.ts** — 封装 `uni.request` 的 HTTP 客户端,自动携带 JWT
|
||||||
|
- **utils/auth.ts** — 微信登录流程:uni.login → 服务端 /auth/login → 存储 token
|
||||||
|
|
||||||
|
### Shared 包
|
||||||
|
所有 API 类型、DTO、枚举和业务常量定义在 `packages/shared/src/`,前后端通过 `@mp-pilates/shared` 引用。路径别名配置在 `tsconfig.base.json` 和 Jest 的 `moduleNameMapper` 中。
|
||||||
|
|
||||||
|
### 数据库 Schema
|
||||||
|
Prisma schema 位于 `packages/server/prisma/schema.prisma`,关键约定:
|
||||||
|
- Model 用 PascalCase,表名用 snake_case(`@@map`)
|
||||||
|
- 字段用 camelCase,列名用 snake_case(`@map`)
|
||||||
|
- 所有 ID 为 UUID
|
||||||
|
- 金额字段使用 `Decimal(10, 0)`
|
||||||
|
- 关键唯一约束:`TimeSlot` 的 `@@unique([date, startTime, endTime])`,`Booking` 的 `@@unique([userId, timeSlotId])`
|
||||||
|
|
||||||
|
### 核心业务规则
|
||||||
|
- 预约需要有效的会员卡(剩余次数或有效期内)
|
||||||
|
- 取消预约需在课程开始前 `cancelHoursLimit` 小时(默认 2 小时,可在 StudioConfig 中配置)
|
||||||
|
- 时段根据 WeekTemplate 自动生成未来 14 天的课程
|
||||||
|
- 默认时段容量为 1(私教课)
|
||||||
|
|
||||||
|
## 环境配置
|
||||||
|
|
||||||
|
需要 Node 20+(.nvmrc)、pnpm 8+、PostgreSQL。复制 `packages/server/.env.example` 为 `.env.local`,需配置 DATABASE_URL、JWT_SECRET 及微信相关凭证(APPID、SECRET、MCH_ID、MCH_KEY、证书路径)。
|
||||||
|
|
||||||
|
## 开发约定
|
||||||
|
|
||||||
|
- **API 前缀**:所有路由在 `/api` 下(setGlobalPrefix)
|
||||||
|
- **参数校验**:全局 ValidationPipe,启用 whitelist + forbidNonWhitelisted + transform
|
||||||
|
- **鉴权守卫**:受保护路由使用 `@UseGuards(JwtAuthGuard)`,通过 `@Req()` 从 JWT 载荷提取用户
|
||||||
|
- **角色**:MEMBER 和 ADMIN;管理员路由使用自定义角色守卫
|
||||||
|
- **异常处理**:使用 NestJS 内置异常(BadRequestException、NotFoundException 等)
|
||||||
|
- **分页**:统一使用 `PaginatedResponse<T>`,包含 data、total、page、limit
|
||||||
|
- **pnpm**:使用 `shamefully-hoist=true`(.npmrc),为 Uni-app 兼容所需
|
||||||
@@ -1,33 +1,42 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="brand-banner" :style="bannerStyle">
|
<view class="brand-banner">
|
||||||
|
<!-- Background image layer -->
|
||||||
|
<image
|
||||||
|
v-if="studioInfo?.bannerUrl"
|
||||||
|
class="banner-bg"
|
||||||
|
:src="studioInfo.bannerUrl"
|
||||||
|
mode="aspectFill"
|
||||||
|
/>
|
||||||
|
<!-- Dark overlay for readability -->
|
||||||
|
<view class="banner-overlay" />
|
||||||
|
|
||||||
<!-- Status bar spacer -->
|
<!-- Status bar spacer -->
|
||||||
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }" />
|
<view class="status-bar" :style="{ height: statusBarHeight + 'px' }" />
|
||||||
|
|
||||||
<!-- Nav area -->
|
<!-- Centered content -->
|
||||||
<view class="nav-bar">
|
<view class="banner-content">
|
||||||
<view class="studio-name-row">
|
<!-- Circular logo -->
|
||||||
|
<view class="logo-circle">
|
||||||
<image
|
<image
|
||||||
v-if="studioInfo?.logo"
|
v-if="studioInfo?.logo"
|
||||||
class="logo"
|
class="logo-img"
|
||||||
:src="studioInfo.logo"
|
:src="studioInfo.logo"
|
||||||
mode="aspectFit"
|
mode="aspectFit"
|
||||||
/>
|
/>
|
||||||
<text class="studio-name">{{ studioInfo?.name || '普拉提工作室' }}</text>
|
<text v-else class="logo-placeholder">FC</text>
|
||||||
</view>
|
|
||||||
<text class="studio-slogan">专业 · 精致 · 健康</text>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- Decorative circles -->
|
<!-- Studio name -->
|
||||||
<view class="deco-circle deco-circle--1" />
|
<text class="studio-name">{{ studioInfo?.name || 'Focus Core' }}</text>
|
||||||
<view class="deco-circle deco-circle--2" />
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import type { StudioConfig } from '@mp-pilates/shared'
|
import type { StudioConfig } from '@mp-pilates/shared'
|
||||||
|
|
||||||
const props = defineProps<{
|
defineProps<{
|
||||||
studioInfo: StudioConfig | null
|
studioInfo: StudioConfig | null
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
@@ -37,82 +46,79 @@ onMounted(() => {
|
|||||||
const sysInfo = uni.getSystemInfoSync()
|
const sysInfo = uni.getSystemInfoSync()
|
||||||
statusBarHeight.value = sysInfo.statusBarHeight ?? 20
|
statusBarHeight.value = sysInfo.statusBarHeight ?? 20
|
||||||
})
|
})
|
||||||
|
|
||||||
const bannerStyle = computed(() => {
|
|
||||||
if (props.studioInfo?.bannerUrl) {
|
|
||||||
return {
|
|
||||||
backgroundImage: `url(${props.studioInfo.bannerUrl})`,
|
|
||||||
backgroundSize: 'cover',
|
|
||||||
backgroundPosition: 'center',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {}
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.brand-banner {
|
.brand-banner {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-height: 300rpx;
|
height: 580rpx;
|
||||||
background: linear-gradient(135deg, #1a1a2e 0%, #2d2d5e 50%, #1a1a2e 100%);
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding-bottom: 40rpx;
|
background: #2a2a2a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-bar {
|
.banner-bg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-bar {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
padding: 16rpx 40rpx 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.studio-name-row {
|
.banner-content {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding-top: 40rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-circle {
|
||||||
|
width: 200rpx;
|
||||||
|
height: 200rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #ffffff;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 16rpx;
|
justify-content: center;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 8rpx 40rpx rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
.logo-img {
|
||||||
width: 64rpx;
|
width: 160rpx;
|
||||||
height: 64rpx;
|
height: 160rpx;
|
||||||
border-radius: 12rpx;
|
}
|
||||||
|
|
||||||
|
.logo-placeholder {
|
||||||
|
font-size: 64rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #333;
|
||||||
|
letter-spacing: 4rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.studio-name {
|
.studio-name {
|
||||||
font-size: 44rpx;
|
margin-top: 28rpx;
|
||||||
|
font-size: 40rpx;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
letter-spacing: 2rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.studio-slogan {
|
|
||||||
display: block;
|
|
||||||
margin-top: 10rpx;
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: #c9a87c;
|
|
||||||
letter-spacing: 6rpx;
|
letter-spacing: 6rpx;
|
||||||
}
|
text-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.3);
|
||||||
|
|
||||||
/* Decorative blurred circles */
|
|
||||||
.deco-circle {
|
|
||||||
position: absolute;
|
|
||||||
border-radius: 50%;
|
|
||||||
opacity: 0.12;
|
|
||||||
background: #c9a87c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.deco-circle--1 {
|
|
||||||
width: 300rpx;
|
|
||||||
height: 300rpx;
|
|
||||||
top: -80rpx;
|
|
||||||
right: -60rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.deco-circle--2 {
|
|
||||||
width: 180rpx;
|
|
||||||
height: 180rpx;
|
|
||||||
bottom: -40rpx;
|
|
||||||
right: 120rpx;
|
|
||||||
opacity: 0.08;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,84 +1,61 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="card-shop">
|
<view class="card-shop">
|
||||||
|
<!-- Section header -->
|
||||||
<view class="section-header">
|
<view class="section-header">
|
||||||
<text class="section-title">会员卡</text>
|
<text class="section-title">会员卡</text>
|
||||||
<text class="section-subtitle">选择适合您的套餐</text>
|
<text class="section-action" @tap="goToAllCards">全部</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- Loading skeleton -->
|
<!-- Loading skeleton -->
|
||||||
<scroll-view
|
<view v-if="loading" class="card-list">
|
||||||
v-if="loading"
|
|
||||||
scroll-x
|
|
||||||
class="cards-scroll"
|
|
||||||
:show-scrollbar="false"
|
|
||||||
>
|
|
||||||
<view class="cards-row">
|
|
||||||
<view
|
<view
|
||||||
v-for="i in 3"
|
v-for="i in 3"
|
||||||
:key="i"
|
:key="i"
|
||||||
class="card-item skeleton-card"
|
class="card-row skeleton-row"
|
||||||
/>
|
>
|
||||||
|
<view class="skeleton-thumb" />
|
||||||
|
<view class="skeleton-info">
|
||||||
|
<view class="skeleton-line skeleton-line--title" />
|
||||||
|
<view class="skeleton-line skeleton-line--sub" />
|
||||||
|
<view class="skeleton-line skeleton-line--price" />
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
|
||||||
|
|
||||||
<!-- Card list -->
|
<!-- Card list -->
|
||||||
<scroll-view
|
<view v-else-if="cardTypes.length" class="card-list">
|
||||||
v-else-if="cardTypes.length"
|
|
||||||
scroll-x
|
|
||||||
class="cards-scroll"
|
|
||||||
:show-scrollbar="false"
|
|
||||||
>
|
|
||||||
<view class="cards-row">
|
|
||||||
<view
|
<view
|
||||||
v-for="card in cardTypes"
|
v-for="card in cardTypes"
|
||||||
:key="card.id"
|
:key="card.id"
|
||||||
class="card-item"
|
class="card-row"
|
||||||
:class="cardItemClass(card)"
|
|
||||||
@tap="goToDetail(card.id)"
|
@tap="goToDetail(card.id)"
|
||||||
>
|
>
|
||||||
<!-- Card header band -->
|
<!-- Thumbnail -->
|
||||||
<view class="card-header" :class="cardHeaderClass(card)">
|
<view class="card-thumb" :class="thumbClass(card)">
|
||||||
<text class="card-type-label">{{ typeLabel(card) }}</text>
|
<view class="thumb-fallback">
|
||||||
|
<text class="thumb-name">{{ truncate(card.name, 8) }}</text>
|
||||||
|
<text class="thumb-price">¥{{ formatPrice(card.price) }}</text>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- Card name -->
|
<!-- Card info -->
|
||||||
|
<view class="card-info">
|
||||||
<text class="card-name">{{ card.name }}</text>
|
<text class="card-name">{{ card.name }}</text>
|
||||||
|
<text class="card-validity">有效期:{{ card.durationDays }} 天</text>
|
||||||
<!-- Pricing -->
|
|
||||||
<view class="price-row">
|
<view class="price-row">
|
||||||
<text class="price-current">¥{{ formatPrice(card.price) }}</text>
|
<text class="price-label">价格:</text>
|
||||||
|
<text class="price-symbol">¥</text>
|
||||||
|
<text class="price-current">{{ formatPrice(card.price) }}</text>
|
||||||
<text
|
<text
|
||||||
v-if="card.originalPrice && card.originalPrice > card.price"
|
v-if="card.originalPrice && card.originalPrice > card.price"
|
||||||
class="price-original"
|
class="price-original"
|
||||||
>
|
>
|
||||||
¥{{ formatPrice(card.originalPrice) }}
|
原价:¥{{ formatPrice(card.originalPrice) }}
|
||||||
</text>
|
</text>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- Description -->
|
|
||||||
<text v-if="card.description" class="card-desc">
|
|
||||||
{{ truncate(card.description, 40) }}
|
|
||||||
</text>
|
|
||||||
|
|
||||||
<!-- Duration / Times -->
|
|
||||||
<view class="card-meta">
|
|
||||||
<view v-if="card.totalTimes" class="meta-item">
|
|
||||||
<text class="meta-value">{{ card.totalTimes }}</text>
|
|
||||||
<text class="meta-label">次</text>
|
|
||||||
</view>
|
|
||||||
<view class="meta-item">
|
|
||||||
<text class="meta-value">{{ card.durationDays }}</text>
|
|
||||||
<text class="meta-label">天有效</text>
|
|
||||||
</view>
|
|
||||||
</view>
|
|
||||||
|
|
||||||
<!-- Buy button -->
|
|
||||||
<view class="buy-btn">
|
|
||||||
<text class="buy-btn-text">立即购买</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</scroll-view>
|
|
||||||
|
|
||||||
<!-- Empty -->
|
<!-- Empty -->
|
||||||
<view v-else class="empty-state">
|
<view v-else class="empty-state">
|
||||||
@@ -120,25 +97,15 @@ function goToDetail(id: string) {
|
|||||||
uni.navigateTo({ url: `/pages/card/detail?id=${id}` })
|
uni.navigateTo({ url: `/pages/card/detail?id=${id}` })
|
||||||
}
|
}
|
||||||
|
|
||||||
function typeLabel(card: CardType): string {
|
function goToAllCards() {
|
||||||
const map: Record<CardTypeCategory, string> = {
|
// Navigate to all cards page or scroll behavior
|
||||||
[CardTypeCategory.TIMES]: '次卡',
|
uni.navigateTo({ url: '/pages/card/detail?showAll=1' })
|
||||||
[CardTypeCategory.DURATION]: '月卡',
|
|
||||||
[CardTypeCategory.TRIAL]: '体验',
|
|
||||||
}
|
|
||||||
return map[card.type] ?? '会员卡'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function cardItemClass(card: CardType): string {
|
function thumbClass(card: CardType): string {
|
||||||
if (card.type === CardTypeCategory.TRIAL) return 'card-item--trial'
|
if (card.type === CardTypeCategory.TRIAL) return 'thumb--trial'
|
||||||
if (card.type === CardTypeCategory.DURATION) return 'card-item--duration'
|
if (card.type === CardTypeCategory.DURATION) return 'thumb--duration'
|
||||||
return ''
|
return 'thumb--times'
|
||||||
}
|
|
||||||
|
|
||||||
function cardHeaderClass(card: CardType): string {
|
|
||||||
if (card.type === CardTypeCategory.TRIAL) return 'header--trial'
|
|
||||||
if (card.type === CardTypeCategory.DURATION) return 'header--duration'
|
|
||||||
return 'header--times'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function truncate(str: string, maxLen: number): string {
|
function truncate(str: string, maxLen: number): string {
|
||||||
@@ -148,166 +115,208 @@ function truncate(str: string, maxLen: number): string {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.card-shop {
|
.card-shop {
|
||||||
margin: 24rpx 0 0;
|
background: #ffffff;
|
||||||
padding-bottom: 40rpx;
|
margin-top: 16rpx;
|
||||||
|
padding-bottom: 20rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Section header ── */
|
||||||
.section-header {
|
.section-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: center;
|
||||||
gap: 16rpx;
|
justify-content: space-between;
|
||||||
padding: 0 24rpx;
|
padding: 32rpx 32rpx 20rpx;
|
||||||
margin-bottom: 20rpx;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-title {
|
.section-title {
|
||||||
font-size: 32rpx;
|
font-size: 36rpx;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #1a1a2e;
|
color: #222;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-subtitle {
|
.section-action {
|
||||||
font-size: 24rpx;
|
font-size: 26rpx;
|
||||||
color: #999;
|
color: #999;
|
||||||
|
padding: 8rpx 24rpx;
|
||||||
|
border: 1rpx solid #ddd;
|
||||||
|
border-radius: 24rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cards-scroll {
|
/* ── Card list ── */
|
||||||
width: 100%;
|
.card-list {
|
||||||
|
padding: 0 32rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.cards-row {
|
.card-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
align-items: center;
|
||||||
gap: 20rpx;
|
gap: 24rpx;
|
||||||
padding: 8rpx 24rpx 16rpx;
|
padding: 24rpx 0;
|
||||||
width: max-content;
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Individual Card --- */
|
/* ── Thumbnail ── */
|
||||||
.card-item {
|
.card-thumb {
|
||||||
width: 280rpx;
|
width: 200rpx;
|
||||||
background: #ffffff;
|
height: 140rpx;
|
||||||
border-radius: 16rpx;
|
border-radius: 12rpx;
|
||||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.10);
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb-fallback {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-shrink: 0;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
&--trial {
|
gap: 8rpx;
|
||||||
border: 2rpx solid #c9a87c;
|
padding: 12rpx;
|
||||||
}
|
|
||||||
|
|
||||||
&--duration {
|
|
||||||
border: 2rpx solid #9b59b6;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.skeleton-card {
|
.thumb--times .thumb-fallback {
|
||||||
height: 360rpx;
|
background: linear-gradient(135deg, #3a3a3a, #555);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb--duration .thumb-fallback {
|
||||||
|
background: linear-gradient(135deg, #6c3483, #9b59b6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb--trial .thumb-fallback {
|
||||||
|
background: linear-gradient(135deg, #7d6608, #c9a87c);
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb-name {
|
||||||
|
font-size: 22rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #ffffff;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.3;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb-price {
|
||||||
|
font-size: 24rpx;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Card info ── */
|
||||||
|
.card-info {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-name {
|
||||||
|
display: block;
|
||||||
|
font-size: 30rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #222;
|
||||||
|
margin-bottom: 8rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-validity {
|
||||||
|
display: block;
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-label {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #e53935;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-symbol {
|
||||||
|
font-size: 24rpx;
|
||||||
|
color: #e53935;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-current {
|
||||||
|
font-size: 40rpx;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #e53935;
|
||||||
|
}
|
||||||
|
|
||||||
|
.price-original {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #bbb;
|
||||||
|
text-decoration: line-through;
|
||||||
|
margin-left: 12rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Skeleton ── */
|
||||||
|
.skeleton-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 24rpx;
|
||||||
|
padding: 24rpx 0;
|
||||||
|
border-bottom: 1rpx solid #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-thumb {
|
||||||
|
width: 200rpx;
|
||||||
|
height: 140rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
background-size: 400% 100%;
|
background-size: 400% 100%;
|
||||||
animation: shimmer 1.4s infinite;
|
animation: shimmer 1.4s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.skeleton-info {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-line {
|
||||||
|
height: 24rpx;
|
||||||
|
border-radius: 6rpx;
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 400% 100%;
|
||||||
|
animation: shimmer 1.4s infinite;
|
||||||
|
|
||||||
|
&--title {
|
||||||
|
width: 70%;
|
||||||
|
height: 30rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--sub {
|
||||||
|
width: 40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--price {
|
||||||
|
width: 50%;
|
||||||
|
height: 36rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes shimmer {
|
@keyframes shimmer {
|
||||||
0% { background-position: 100% 0; }
|
0% { background-position: 100% 0; }
|
||||||
100% { background-position: -100% 0; }
|
100% { background-position: -100% 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header {
|
/* ── Empty state ── */
|
||||||
padding: 12rpx 20rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header--times { background: linear-gradient(90deg, #1a1a2e, #2d2d5e); }
|
|
||||||
.header--duration { background: linear-gradient(90deg, #6c3483, #9b59b6); }
|
|
||||||
.header--trial { background: linear-gradient(90deg, #7d6608, #c9a87c); }
|
|
||||||
|
|
||||||
.card-type-label {
|
|
||||||
font-size: 22rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #ffffff;
|
|
||||||
letter-spacing: 2rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-name {
|
|
||||||
font-size: 30rpx;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #1a1a2e;
|
|
||||||
padding: 20rpx 20rpx 8rpx;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
gap: 12rpx;
|
|
||||||
padding: 0 20rpx 12rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-current {
|
|
||||||
font-size: 40rpx;
|
|
||||||
font-weight: 800;
|
|
||||||
color: #c9a87c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.price-original {
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: #bbb;
|
|
||||||
text-decoration: line-through;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-desc {
|
|
||||||
font-size: 22rpx;
|
|
||||||
color: #888;
|
|
||||||
padding: 0 20rpx 16rpx;
|
|
||||||
line-height: 1.5;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-meta {
|
|
||||||
display: flex;
|
|
||||||
gap: 20rpx;
|
|
||||||
padding: 0 20rpx 20rpx;
|
|
||||||
flex: 1;
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
gap: 4rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-value {
|
|
||||||
font-size: 28rpx;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #1a1a2e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.meta-label {
|
|
||||||
font-size: 22rpx;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buy-btn {
|
|
||||||
margin: 0 20rpx 24rpx;
|
|
||||||
background: #1a1a2e;
|
|
||||||
border-radius: 40rpx;
|
|
||||||
padding: 16rpx 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buy-btn-text {
|
|
||||||
font-size: 26rpx;
|
|
||||||
font-weight: 600;
|
|
||||||
color: #c9a87c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
padding: 60rpx;
|
padding: 80rpx;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|||||||
@@ -1,57 +1,34 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="studio-info card">
|
<view class="studio-info">
|
||||||
<!-- Photo Swiper -->
|
<!-- Horizontal photo strip -->
|
||||||
<swiper
|
<scroll-view
|
||||||
v-if="studioInfo?.photos?.length"
|
v-if="studioInfo?.photos?.length"
|
||||||
class="photo-swiper"
|
scroll-x
|
||||||
:indicator-dots="studioInfo.photos.length > 1"
|
class="photo-strip"
|
||||||
:autoplay="true"
|
:show-scrollbar="false"
|
||||||
:interval="4000"
|
|
||||||
:duration="500"
|
|
||||||
indicator-color="rgba(255,255,255,0.5)"
|
|
||||||
indicator-active-color="#c9a87c"
|
|
||||||
circular
|
|
||||||
>
|
>
|
||||||
<swiper-item
|
<view class="photo-strip-inner">
|
||||||
|
<image
|
||||||
v-for="(photo, idx) in studioInfo.photos"
|
v-for="(photo, idx) in studioInfo.photos"
|
||||||
:key="idx"
|
:key="idx"
|
||||||
>
|
class="strip-photo"
|
||||||
<image
|
|
||||||
class="photo"
|
|
||||||
:src="photo"
|
:src="photo"
|
||||||
mode="aspectFill"
|
mode="aspectFill"
|
||||||
|
@tap="previewPhoto(idx)"
|
||||||
/>
|
/>
|
||||||
</swiper-item>
|
|
||||||
</swiper>
|
|
||||||
|
|
||||||
<!-- Placeholder when no photos -->
|
|
||||||
<view v-else class="photo-placeholder">
|
|
||||||
<text class="placeholder-icon">🏃</text>
|
|
||||||
<text class="placeholder-text">专业普拉提工作室</text>
|
|
||||||
</view>
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
|
||||||
<!-- Info rows -->
|
<!-- Address + Phone row -->
|
||||||
<view class="info-rows">
|
<view class="location-row">
|
||||||
<!-- Address -->
|
<view class="location-left" @tap="handleAddressTap">
|
||||||
<view class="info-row" @tap="handleAddressTap">
|
<text class="location-icon">📍</text>
|
||||||
<view class="icon-wrap">
|
<text class="location-text">
|
||||||
<text class="iconfont">📍</text>
|
|
||||||
</view>
|
|
||||||
<text class="info-text address-text">
|
|
||||||
{{ studioInfo?.address || '地址加载中…' }}
|
{{ studioInfo?.address || '地址加载中…' }}
|
||||||
</text>
|
</text>
|
||||||
<text class="info-action">
|
|
||||||
{{ studioInfo?.latitude ? '导航' : '复制' }}
|
|
||||||
</text>
|
|
||||||
</view>
|
</view>
|
||||||
|
<view class="phone-btn" @tap="handlePhoneTap">
|
||||||
<!-- Phone -->
|
<text class="phone-icon">📞</text>
|
||||||
<view class="info-row" @tap="handlePhoneTap">
|
|
||||||
<view class="icon-wrap">
|
|
||||||
<text class="iconfont">📞</text>
|
|
||||||
</view>
|
|
||||||
<text class="info-text">{{ studioInfo?.phone || '—' }}</text>
|
|
||||||
<text class="info-action">拨打</text>
|
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -64,6 +41,14 @@ const props = defineProps<{
|
|||||||
studioInfo: StudioConfig | null
|
studioInfo: StudioConfig | null
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
function previewPhoto(index: number) {
|
||||||
|
if (!props.studioInfo?.photos?.length) return
|
||||||
|
uni.previewImage({
|
||||||
|
current: index,
|
||||||
|
urls: props.studioInfo.photos,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function handleAddressTap() {
|
function handleAddressTap() {
|
||||||
if (!props.studioInfo) return
|
if (!props.studioInfo) return
|
||||||
|
|
||||||
@@ -73,8 +58,8 @@ function handleAddressTap() {
|
|||||||
uni.openLocation({
|
uni.openLocation({
|
||||||
latitude,
|
latitude,
|
||||||
longitude,
|
longitude,
|
||||||
name: name || '普拉提工作室',
|
name: name || 'Focus Core',
|
||||||
address: address,
|
address,
|
||||||
fail() {
|
fail() {
|
||||||
copyAddress()
|
copyAddress()
|
||||||
},
|
},
|
||||||
@@ -109,95 +94,73 @@ function handlePhoneTap() {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.studio-info {
|
.studio-info {
|
||||||
margin: 24rpx 24rpx 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
border-radius: 16rpx;
|
|
||||||
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.photo-swiper {
|
/* ── Photo strip ── */
|
||||||
|
.photo-strip {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 360rpx;
|
padding: 24rpx 0;
|
||||||
border-radius: 16rpx 16rpx 0 0;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.photo {
|
.photo-strip-inner {
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.photo-placeholder {
|
|
||||||
width: 100%;
|
|
||||||
height: 280rpx;
|
|
||||||
background: linear-gradient(135deg, #f0f0f0, #e8e8e8);
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 16rpx;
|
gap: 16rpx;
|
||||||
border-radius: 16rpx 16rpx 0 0;
|
padding: 0 24rpx;
|
||||||
|
width: max-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.placeholder-icon {
|
.strip-photo {
|
||||||
font-size: 80rpx;
|
width: 240rpx;
|
||||||
}
|
height: 160rpx;
|
||||||
|
border-radius: 12rpx;
|
||||||
.placeholder-text {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-rows {
|
|
||||||
padding: 16rpx 32rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 20rpx 0;
|
|
||||||
border-bottom: 1rpx solid #f0f0f0;
|
|
||||||
gap: 16rpx;
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.icon-wrap {
|
|
||||||
width: 48rpx;
|
|
||||||
height: 48rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconfont {
|
/* ── Location row ── */
|
||||||
font-size: 36rpx;
|
.location-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 24rpx 32rpx 28rpx;
|
||||||
|
gap: 24rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-text {
|
.location-left {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12rpx;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-icon {
|
||||||
font-size: 28rpx;
|
font-size: 28rpx;
|
||||||
color: #333;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.address-text {
|
|
||||||
font-size: 26rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info-action {
|
|
||||||
font-size: 24rpx;
|
|
||||||
color: #c9a87c;
|
|
||||||
padding: 6rpx 16rpx;
|
|
||||||
border: 1rpx solid #c9a87c;
|
|
||||||
border-radius: 24rpx;
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
margin-top: 4rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-text {
|
||||||
|
font-size: 26rpx;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1.5;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-btn {
|
||||||
|
width: 72rpx;
|
||||||
|
height: 72rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #f5f5f5;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-icon {
|
||||||
|
font-size: 36rpx;
|
||||||
|
color: #4CAF50;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -100,19 +100,19 @@
|
|||||||
"list": [
|
"list": [
|
||||||
{
|
{
|
||||||
"pagePath": "pages/home/index",
|
"pagePath": "pages/home/index",
|
||||||
"text": "首页",
|
"text": "场馆首页",
|
||||||
"iconPath": "static/tab/home.png",
|
"iconPath": "static/tab/home.png",
|
||||||
"selectedIconPath": "static/tab/home-active.png"
|
"selectedIconPath": "static/tab/home-active.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pagePath": "pages/booking/index",
|
"pagePath": "pages/booking/index",
|
||||||
"text": "预约",
|
"text": "课程预约",
|
||||||
"iconPath": "static/tab/booking.png",
|
"iconPath": "static/tab/booking.png",
|
||||||
"selectedIconPath": "static/tab/booking-active.png"
|
"selectedIconPath": "static/tab/booking-active.png"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pagePath": "pages/profile/index",
|
"pagePath": "pages/profile/index",
|
||||||
"text": "我的",
|
"text": "个人中心",
|
||||||
"iconPath": "static/tab/profile.png",
|
"iconPath": "static/tab/profile.png",
|
||||||
"selectedIconPath": "static/tab/profile-active.png"
|
"selectedIconPath": "static/tab/profile-active.png"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,19 +9,22 @@
|
|||||||
@refresherrefresh="handleRefresh"
|
@refresherrefresh="handleRefresh"
|
||||||
@refresherrestore="refreshing = false"
|
@refresherrestore="refreshing = false"
|
||||||
>
|
>
|
||||||
<!-- ① Brand Banner (custom nav) -->
|
<!-- ① Brand Banner (hero with bg image + centered logo) -->
|
||||||
<BrandBanner :studio-info="studioStore.studioInfo" />
|
<BrandBanner :studio-info="studioStore.studioInfo" />
|
||||||
|
|
||||||
<!-- ② Studio Info (swiper + address + phone) -->
|
<!-- ② Studio Info (photo strip + address/phone) -->
|
||||||
<StudioInfo :studio-info="studioStore.studioInfo" />
|
<StudioInfo :studio-info="studioStore.studioInfo" />
|
||||||
|
|
||||||
|
<!-- Divider -->
|
||||||
|
<view class="section-divider" />
|
||||||
|
|
||||||
<!-- ③ Quick Entry (login / trial / book / renew) -->
|
<!-- ③ Quick Entry (login / trial / book / renew) -->
|
||||||
<QuickEntry @scroll-to-card-shop="scrollToCardShop" />
|
<QuickEntry @scroll-to-card-shop="scrollToCardShop" />
|
||||||
|
|
||||||
<!-- ④ Upcoming Bookings -->
|
<!-- ④ Upcoming Bookings -->
|
||||||
<UpcomingBooking />
|
<UpcomingBooking />
|
||||||
|
|
||||||
<!-- ⑤ Card Shop (horizontal scroll) -->
|
<!-- ⑤ Card Shop (vertical list) -->
|
||||||
<view :id="cardShopAnchorId">
|
<view :id="cardShopAnchorId">
|
||||||
<CardShop ref="cardShopRef" />
|
<CardShop ref="cardShopRef" />
|
||||||
</view>
|
</view>
|
||||||
@@ -102,6 +105,11 @@ function scrollToCardShop() {
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-divider {
|
||||||
|
height: 16rpx;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
.bottom-padding {
|
.bottom-padding {
|
||||||
height: 120rpx;
|
height: 120rpx;
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 738 B After Width: | Height: | Size: 378 B |
|
Before Width: | Height: | Size: 679 B After Width: | Height: | Size: 371 B |
|
Before Width: | Height: | Size: 722 B After Width: | Height: | Size: 387 B |
|
Before Width: | Height: | Size: 573 B After Width: | Height: | Size: 382 B |
|
Before Width: | Height: | Size: 728 B After Width: | Height: | Size: 460 B |
|
Before Width: | Height: | Size: 622 B After Width: | Height: | Size: 455 B |
@@ -2,7 +2,6 @@
|
|||||||
"extends": "../../tsconfig.base.json",
|
"extends": "../../tsconfig.base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"rootDir": "src",
|
|
||||||
"module": "CommonJS",
|
"module": "CommonJS",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"emitDecoratorMetadata": true,
|
"emitDecoratorMetadata": true,
|
||||||
|
|||||||