feat: 搭建微信小程序展示端

- 初始化小程序工程配置与类型声明

- 增加首页、律所、律师列表、详情与历史页面

- 补充公共组件、运行时配置与示例素材
This commit is contained in:
2026-03-20 12:44:31 +08:00
parent 86c321e832
commit 9605384edc
87 changed files with 26373 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
{
"usingComponents": {
"app-header": "/components/app-header/app-header"
}
}

View File

@@ -0,0 +1,182 @@
.firm-page {
background: var(--bg-page);
}
.hero-section {
position: relative;
height: 520rpx;
width: 100%;
display: flex;
flex-direction: column;
justify-content: flex-end;
padding-bottom: 80rpx;
/* Space for overlap */
}
.hero-bg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
.hero-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
/* Updated gradient to match Burgundy Red */
background: linear-gradient(to bottom, rgba(142, 34, 48, 0.4), rgba(92, 13, 21, 0.9));
z-index: 1;
}
.hero-content {
position: relative;
z-index: 2;
padding: 0 var(--spacing-lg);
display: flex;
flex-direction: column;
margin-bottom: 72rpx;
}
.firm-stats {
display: inline-flex;
align-items: center;
width: fit-content;
padding: 14rpx 22rpx;
border-radius: 20rpx;
border: 1rpx solid rgba(255, 255, 255, 0.32);
background: linear-gradient(120deg, rgba(10, 21, 34, 0.62), rgba(19, 38, 56, 0.36));
box-shadow: 0 10rpx 28rpx rgba(7, 15, 27, 0.35), inset 0 1rpx 0 rgba(255, 255, 255, 0.24);
backdrop-filter: blur(10rpx);
-webkit-backdrop-filter: blur(10rpx);
}
.stat-item {
display: flex;
flex-direction: column;
min-width: 120rpx;
}
.stat-num {
font-size: 44rpx;
font-weight: 700;
color: #fff;
line-height: 1.2;
text-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.35);
}
.stat-label {
font-size: 24rpx;
color: rgba(242, 247, 252, 0.92);
margin-top: 4rpx;
text-shadow: 0 1rpx 8rpx rgba(0, 0, 0, 0.28);
}
.stat-divider {
width: 2rpx;
height: 40rpx;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.52), rgba(255, 255, 255, 0.12));
margin: 0 24rpx;
}
.content-container {
position: relative;
z-index: 3;
margin-top: -60rpx;
background: var(--bg-page);
border-radius: 32rpx 32rpx 0 0;
padding: var(--spacing-lg) var(--spacing-md);
min-height: 50vh;
}
.address-card {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-md);
margin-bottom: var(--spacing-lg);
background: #fff;
box-shadow: var(--shadow-base);
}
.address-info {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.address-label {
font-size: 24rpx;
color: var(--text-tertiary);
}
.address-text {
font-size: 28rpx;
color: var(--text-main);
font-weight: 500;
}
.address-icon {
font-size: 32rpx;
color: var(--text-tertiary);
}
.section {
margin-bottom: var(--spacing-xl);
}
.intro-text {
font-size: 28rpx;
color: var(--text-secondary);
line-height: 1.8;
text-align: justify;
}
.tags-wrapper {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.area-tag {
font-size: 26rpx;
color: var(--primary-color);
background: rgba(142, 34, 48, 0.08);
/* Primary color opacity */
padding: 10rpx 24rpx;
border-radius: 32rpx;
font-weight: 500;
}
.bottom-bar {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background: #fff;
padding: 20rpx var(--spacing-md);
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.05);
z-index: 10;
box-sizing: border-box;
}
.cta-btn {
background: var(--primary-color);
color: #fff;
font-size: 32rpx;
font-weight: 500;
border-radius: 50rpx;
height: 88rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 20rpx rgba(142, 34, 48, 0.3);
}
.cta-btn::after {
border: none;
}

View File

@@ -0,0 +1,71 @@
import { getFirmProfile } from '../../api/open';
import type { FirmInfo } from '../../types/card';
function createEmptyFirm(): FirmInfo {
return {
id: '',
name: '',
logo: '',
intro: '',
hotlinePhone: '',
hqAddress: '',
hqLatitude: 0,
hqLongitude: 0,
officeCount: 0,
lawyerCount: 0,
heroImage: '',
officeList: [],
practiceAreas: [],
};
}
Page({
data: {
firm: createEmptyFirm(),
loading: false,
},
onLoad() {
this.loadFirmProfile();
},
async loadFirmProfile() {
this.setData({ loading: true });
try {
const firm = await getFirmProfile();
this.setData({ firm });
} catch (error) {
const message = error instanceof Error ? error.message : '加载事务所信息失败';
wx.showToast({
title: message,
icon: 'none',
});
} finally {
this.setData({ loading: false });
}
},
goLawyerList() {
wx.navigateTo({
url: '/pages/lawyer-list/index',
});
},
openLocation() {
const { firm } = this.data;
if (!firm.hqLatitude || !firm.hqLongitude) {
wx.showToast({
title: '暂未配置地图位置',
icon: 'none',
});
return;
}
wx.openLocation({
latitude: firm.hqLatitude,
longitude: firm.hqLongitude,
name: firm.name,
address: firm.hqAddress,
scale: 18,
fail: () => {
wx.showToast({ title: '打开地图失败', icon: 'none' });
},
});
},
});

View File

@@ -0,0 +1,60 @@
<view class="container-page firm-page">
<!-- Custom Navigation Bar with transparent background initially if possible, or just standard -->
<app-header title="{{firm.name}}" back="{{false}}" background="rgba(255,255,255,0.9)"></app-header>
<scroll-view class="page-content" scroll-y="true" type="list">
<!-- Hero Section -->
<view class="hero-section">
<image wx:if="{{firm.heroImage}}" class="hero-bg" mode="aspectFill" src="{{firm.heroImage}}"></image>
<view wx:if="{{!firm.heroImage}}" class="hero-overlay"></view>
<view class="hero-content">
<view class="firm-stats">
<view class="stat-item">
<text class="stat-num">{{firm.officeCount}}</text>
<text class="stat-label">办公机构</text>
</view>
<view class="stat-divider"></view>
<view class="stat-item">
<text class="stat-num">{{firm.lawyerCount}}</text>
<text class="stat-label">专业律师</text>
</view>
</view>
</view>
</view>
<!-- Content Container (Overlapping) -->
<view class="content-container">
<!-- Address Card -->
<view class="card address-card" bindtap="openLocation">
<view class="address-info">
<text class="address-label">总部地址</text>
<text class="address-text">{{firm.hqAddress}}</text>
</view>
<text class="address-icon">></text>
</view>
<!-- Intro Section -->
<view class="section">
<view class="section-title">律所简介</view>
<text class="intro-text">{{firm.intro}}</text>
</view>
<!-- Practice Areas -->
<view class="section">
<view class="section-title">专业领域</view>
<view class="tags-wrapper">
<view class="area-tag" wx:for="{{firm.practiceAreas}}" wx:key="*this">{{item}}</view>
</view>
</view>
<view class="safe-area-bottom" style="height: 120rpx;"></view>
</view>
</scroll-view>
<!-- Bottom CTA -->
<view class="bottom-bar safe-area-bottom">
<button class="cta-btn" bindtap="goLawyerList">查找专业律师</button>
</view>
</view>