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,65 @@
import { CARD_VIEW_HISTORY_KEY } from '../constants/storage';
import type { CardViewRecord, Lawyer } from '../types/card';
function normalizeLawyerSnapshot(input: unknown): Lawyer | undefined {
if (!input || typeof input !== 'object') {
return undefined;
}
const lawyer = input as Partial<Lawyer>;
if (typeof lawyer.id !== 'string' || !lawyer.id) {
return undefined;
}
return {
id: lawyer.id,
name: typeof lawyer.name === 'string' ? lawyer.name : '',
title: typeof lawyer.title === 'string' ? lawyer.title : '',
office: typeof lawyer.office === 'string' ? lawyer.office : '',
phone: typeof lawyer.phone === 'string' ? lawyer.phone : '',
email: typeof lawyer.email === 'string' ? lawyer.email : '',
address: typeof lawyer.address === 'string' ? lawyer.address : '',
avatar: typeof lawyer.avatar === 'string' ? lawyer.avatar : '',
coverImage: typeof lawyer.coverImage === 'string' ? lawyer.coverImage : '',
specialties: Array.isArray(lawyer.specialties)
? lawyer.specialties.filter((item): item is string => typeof item === 'string')
: [],
bio: typeof lawyer.bio === 'string' ? lawyer.bio : '',
wechatQrImage: typeof lawyer.wechatQrImage === 'string' ? lawyer.wechatQrImage : '',
};
}
export function getCardViewHistory(): CardViewRecord[] {
const raw = wx.getStorageSync(CARD_VIEW_HISTORY_KEY);
if (!Array.isArray(raw)) {
return [];
}
return raw
.map((item) => {
const record = item as { lawyerId?: unknown; viewedAt?: unknown; lawyer?: unknown };
const lawyerId = typeof record.lawyerId === 'string' ? record.lawyerId : '';
const viewedAt = typeof record.viewedAt === 'number' ? record.viewedAt : 0;
const lawyer = normalizeLawyerSnapshot(record.lawyer);
return { lawyerId, viewedAt, lawyer };
})
.filter((item) => Boolean(item.lawyerId) && item.viewedAt > 0)
.sort((a, b) => b.viewedAt - a.viewedAt);
}
export function appendCardViewRecord(lawyer: Lawyer): void {
if (!lawyer.id) {
return;
}
const current = getCardViewHistory().filter((item) => item.lawyerId !== lawyer.id);
current.unshift({
lawyerId: lawyer.id,
viewedAt: Date.now(),
lawyer,
});
wx.setStorageSync(CARD_VIEW_HISTORY_KEY, current);
}
export function clearCardViewHistory(): void {
wx.removeStorageSync(CARD_VIEW_HISTORY_KEY);
}

View File

@@ -0,0 +1,75 @@
import { API_BASE_URL_OVERRIDE_KEY, tenantRuntimeConfig } from '../config/runtime';
import { getMiniappAppId, getMiniappEnvVersion } from './miniapp';
interface ApiResponse<T> {
code: string;
message: string;
data: T;
}
interface RequestOptions {
url: string;
method?: 'GET' | 'POST';
data?: WechatMiniprogram.IAnyObject;
}
function normalizeBaseUrl(baseUrl: string): string {
return baseUrl.trim().replace(/\/+$/, '');
}
function getApiBaseUrl(): string {
const override = wx.getStorageSync(API_BASE_URL_OVERRIDE_KEY);
if (typeof override === 'string' && override.trim()) {
return normalizeBaseUrl(override);
}
const envVersion = getMiniappEnvVersion();
const baseUrl = tenantRuntimeConfig.apiBaseUrlByEnv[envVersion];
if (!baseUrl || !baseUrl.trim()) {
throw new Error(`未配置 ${envVersion} 环境的接口域名`);
}
const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
if (envVersion !== 'develop' && !normalizedBaseUrl.startsWith('https://')) {
throw new Error(`${envVersion} 环境接口域名必须使用 HTTPS`);
}
return normalizedBaseUrl;
}
export function request<T>(options: RequestOptions): Promise<T> {
const appId = getMiniappAppId();
if (!appId) {
return Promise.reject(new Error('未获取到小程序 AppID'));
}
let apiBaseUrl = '';
try {
apiBaseUrl = getApiBaseUrl();
} catch (error) {
return Promise.reject(error instanceof Error ? error : new Error('接口域名配置无效'));
}
return new Promise((resolve, reject) => {
wx.request({
url: `${apiBaseUrl}${options.url}`,
method: options.method || 'GET',
data: options.data,
header: {
'X-Miniapp-Appid': appId,
},
success: (response) => {
const payload = response.data as ApiResponse<T> | undefined;
if (response.statusCode >= 200 && response.statusCode < 300 && payload && payload.code === '0') {
resolve(payload.data);
return;
}
const message = payload && payload.message ? payload.message : '请求失败';
reject(new Error(message));
},
fail: () => {
reject(new Error('网络异常,请稍后重试'));
},
});
});
}

View File

@@ -0,0 +1,29 @@
import type { MiniProgramEnvVersion } from '../config/runtime';
const APPID_OVERRIDE_KEY = 'miniapp_appid_override';
export function getMiniappAppId(): string {
const override = wx.getStorageSync(APPID_OVERRIDE_KEY);
if (typeof override === 'string' && override.trim()) {
return override.trim();
}
try {
const accountInfo = wx.getAccountInfoSync();
return accountInfo.miniProgram.appId || '';
} catch {
return '';
}
}
export function getMiniappEnvVersion(): MiniProgramEnvVersion {
try {
const envVersion = wx.getAccountInfoSync().miniProgram.envVersion;
if (envVersion === 'trial' || envVersion === 'release') {
return envVersion;
}
return 'develop';
} catch {
return 'develop';
}
}

View File

@@ -0,0 +1,19 @@
export const formatTime = (date: Date) => {
const year = date.getFullYear()
const month = date.getMonth() + 1
const day = date.getDate()
const hour = date.getHours()
const minute = date.getMinutes()
const second = date.getSeconds()
return (
[year, month, day].map(formatNumber).join('/') +
' ' +
[hour, minute, second].map(formatNumber).join(':')
)
}
const formatNumber = (n: number) => {
const s = n.toString()
return s[1] ? s : '0' + s
}