diff --git a/lib/features/commercial/commercial_page.dart b/lib/features/commercial/commercial_page.dart index 1563c4e..fdf09f3 100644 --- a/lib/features/commercial/commercial_page.dart +++ b/lib/features/commercial/commercial_page.dart @@ -1,10 +1,178 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; -class CommercialPage extends StatelessWidget { +import '../../core/auth/auth_provider.dart'; +import '../../shared/widgets/error_view.dart'; +import '../../shared/widgets/loading_view.dart'; + +class PartnerStoreInfo { + final int id; + final String name; + final int type; // 1 造型/发型店,2 服装店 + final String address; + final String commissionPolicy; + + const PartnerStoreInfo({ + required this.id, + required this.name, + required this.type, + required this.address, + required this.commissionPolicy, + }); +} + +class StoreNotifier extends AsyncNotifier> { + @override + Future> build() async { + final api = ref.read(apiClientProvider); + final list = await api.get>('/partner-store/list'); + return list + .map((e) => PartnerStoreInfo( + id: (e['id'] as num).toInt(), + name: e['name'] as String? ?? '', + type: (e['type'] as num?)?.toInt() ?? 1, + address: e['address'] as String? ?? '', + commissionPolicy: e['commission_policy'] as String? ?? '', + )) + .toList(); + } + + Future refresh() async { + state = const AsyncLoading(); + state = await AsyncValue.guard(build); + } +} + +final storeProvider = + AsyncNotifierProvider>( + StoreNotifier.new); + +const _storeTypeLabels = {1: '造型', 2: '服装'}; + +/// 商业化:订阅权益 + 合作门店(佣金导流) +class CommercialPage extends ConsumerStatefulWidget { const CommercialPage({super.key}); + @override + ConsumerState createState() => _CommercialPageState(); +} + +class _CommercialPageState extends ConsumerState { + int? _typeFilter; + @override Widget build(BuildContext context) { - return const Center(child: Text('门店电商(开发中)')); + final stores = ref.watch(storeProvider); + final scheme = Theme.of(context).colorScheme; + return Scaffold( + appBar: AppBar(title: const Text('门店与服务')), + body: ListView( + padding: const EdgeInsets.all(16), + children: [ + // 订阅卡片(MVP 占位,支付接入后启用) + Card( + color: scheme.primaryContainer, + child: Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Icon(Icons.workspace_premium, + size: 36, color: scheme.primary), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('形象会员', + style: TextStyle( + fontSize: 16, fontWeight: FontWeight.bold)), + const SizedBox(height: 4), + Text( + '会员专享:无限次效果图生成 · 优先 AI 方案 · 门店专属折扣(接入支付后开通)', + style: TextStyle( + fontSize: 12, color: scheme.primary), + ), + ], + ), + ), + ], + ), + ), + ), + const SizedBox(height: 16), + Row( + children: [ + Text('合作门店', + style: TextStyle( + fontSize: 15, fontWeight: FontWeight.bold)), + const Spacer(), + ChoiceChip( + label: const Text('全部'), + selected: _typeFilter == null, + onSelected: (_) => setState(() => _typeFilter = null), + ), + const SizedBox(width: 8), + for (final entry in _storeTypeLabels.entries) ...[ + ChoiceChip( + label: Text(entry.value), + selected: _typeFilter == entry.key, + onSelected: (_) => + setState(() => _typeFilter = entry.key), + ), + const SizedBox(width: 8), + ], + ], + ), + const SizedBox(height: 8), + stores.when( + loading: () => const Padding( + padding: EdgeInsets.only(top: 32), + child: LoadingView(text: '加载门店...')), + error: (e, _) => Padding( + padding: const EdgeInsets.only(top: 32), + child: ErrorView( + message: e.toString().replaceFirst('Exception: ', ''), + onRetry: () => ref.read(storeProvider.notifier).refresh(), + ), + ), + data: (list) { + final shown = _typeFilter == null + ? list + : list.where((s) => s.type == _typeFilter).toList(); + if (shown.isEmpty) { + return const Padding( + padding: EdgeInsets.only(top: 32), + child: Center( + child: Text('附近暂无可合作门店', + style: TextStyle(color: Colors.grey))), + ); + } + return Column( + children: [ + for (final s in shown) ...[ + Card( + child: ListTile( + leading: CircleAvatar( + backgroundColor: scheme.primaryContainer, + child: Icon(s.type == 1 + ? Icons.content_cut + : Icons.checkroom), + ), + title: Text(s.name), + subtitle: Text('${s.address}\n${s.commissionPolicy}'), + isThreeLine: true, + trailing: const Icon(Icons.chevron_right, + color: Colors.grey), + ), + ), + const SizedBox(height: 8), + ], + ], + ); + }, + ), + ], + ), + ); } } diff --git a/lib/shared/widgets/avatar_viewer.dart b/lib/shared/widgets/avatar_viewer.dart index 9dd33bb..b757589 100644 --- a/lib/shared/widgets/avatar_viewer.dart +++ b/lib/shared/widgets/avatar_viewer.dart @@ -1,19 +1,13 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_gl/flutter_gl.dart'; -import 'package:three_dart/three_dart.dart' as three; -import 'package:three_dart_jsm/three_dart_jsm.dart' as three_jsm; -import '../../core/config/app_config.dart'; - -/// 是否启用 three_dart 3D 渲染。 -/// MVP 阶段服务端尚无预烘焙 GLB 资产(/workspace/templates/*.glb 为构建产物), -/// 默认关闭展示信息占位;接入真实 GLB 资源后置为 true 即可启用。 -const bool kEnable3D = false; - -/// 3D 化身查看组件:three_dart 渲染 GLB,加载失败/未启用时展示信息占位 +/// 3D 化身查看组件(MVP 占位实现) +/// +/// v2 接入真实 3D 渲染的方式: +/// 1. 服务端提供预烘焙 GLB 资产(/workspace/templates/avatar_f{face}_b{body}_s{skin}.glb) +/// 2. pubspec 引入 three_dart ^0.0.16 + three_dart_jsm ^0.0.10 + flutter_gl ^0.0.20 +/// (注意 flutter_gl 0.0.21 在 Flutter 3.44 下无法编译,需锁 0.0.20) +/// 3. 用 flutter_gl 初始化 GL 上下文 + GLTFLoader 加载 GLB + 自动旋转渲染, +/// 参考 three_dart 0.0.16 的 example/lib/webgl_loader_glb.dart class AvatarViewer extends StatelessWidget { final String? glbUrl; final String title; @@ -28,182 +22,6 @@ class AvatarViewer extends StatelessWidget { this.height = 360, }); - @override - Widget build(BuildContext context) { - if (!kEnable3D || glbUrl == null || glbUrl!.isEmpty) { - return _AvatarPlaceholder( - title: title, subtitle: subtitle, height: height); - } - return _ThreeDAvatarViewer( - url: AppConfig.resolveUrl(glbUrl!), - title: title, - subtitle: subtitle, - height: height, - ); - } -} - -/// three_dart 实现:flutter_gl 初始化 + GLTFLoader 加载 + 自动旋转渲染 -class _ThreeDAvatarViewer extends StatefulWidget { - final String url; - final String title; - final String subtitle; - final double height; - - const _ThreeDAvatarViewer({ - required this.url, - required this.title, - required this.subtitle, - required this.height, - }); - - @override - State<_ThreeDAvatarViewer> createState() => _ThreeDAvatarViewerState(); -} - -class _ThreeDAvatarViewerState extends State<_ThreeDAvatarViewer> { - FlutterGlPlugin? _gl; - three.WebGLRenderer? _renderer; - three.Scene? _scene; - three.Camera? _camera; - three.Object3D? _model; - dynamic _sourceTexture; - double _width = 300; - double _dpr = 1; - bool _ready = false; - bool _failed = false; - bool _disposed = false; - Timer? _timer; - - @override - void initState() { - super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) => _init()); - } - - @override - void dispose() { - _disposed = true; - _timer?.cancel(); - super.dispose(); - } - - Future _init() async { - final mq = MediaQuery.of(context); - _width = mq.size.width; - _dpr = mq.devicePixelRatio; - try { - final gl = FlutterGlPlugin(); - await gl.initialize(options: { - 'antialias': true, - 'alpha': false, - 'width': _width.toInt(), - 'height': widget.height.toInt(), - 'dpr': _dpr, - }); - await gl.prepareContext(); - - final renderer = three.WebGLRenderer({ - 'width': _width, - 'height': widget.height, - 'gl': gl.gl, - 'antialias': true, - 'canvas': gl.element, - }); - renderer.setPixelRatio(_dpr); - renderer.setSize(_width, widget.height, false); - - final target = three.WebGLMultisampleRenderTarget( - (_width * _dpr).toInt(), (widget.height * _dpr).toInt(), - three.WebGLRenderTargetOptions({'format': three.RGBAFormat})); - target.samples = 4; - renderer.setRenderTarget(target); - final sourceTexture = renderer.getRenderTargetGLTexture(target); - - await _loadModel(gl, renderer); - - _gl = gl; - _renderer = renderer; - _sourceTexture = sourceTexture; - if (mounted) setState(() => _ready = true); - _timer = Timer.periodic( - const Duration(milliseconds: 33), (_) => _render()); - } catch (e) { - debugPrint('3D 渲染不可用,回退占位:$e'); - if (mounted) setState(() => _failed = true); - } - } - - Future _loadModel( - FlutterGlPlugin gl, three.WebGLRenderer renderer) async { - final scene = three.Scene(); - final camera = three.PerspectiveCamera(45, _width / widget.height, 0.1, 100); - camera.position.set(0, 1.4, 3.2); - scene.add(three.AmbientLight(0xffffff, 0.9)); - final keyLight = three.DirectionalLight(0xffffff, 0.9); - keyLight.position.set(2, 4, 3); - scene.add(keyLight); - scene.add(camera); - camera.lookAt(three.Vector3(0, 1, 0)); - - final loader = three_jsm.GLTFLoader(null); - final result = await loader.loadAsync(widget.url); - final model = result['scene'] as three.Object3D?; - if (model != null) { - model.rotation.y = 0.6; - scene.add(model); - } - - _scene = scene; - _camera = camera; - _model = model; - } - - void _render() { - if (_disposed || _renderer == null || _scene == null || _camera == null) { - return; - } - final model = _model; - if (model != null) { - model.rotation.y += 0.01; // 自动旋转 - } - _renderer!.render(_scene!, _camera!); - _gl!.gl.flush(); - if (!kIsWeb) { - _gl!.updateTexture(_sourceTexture); - } - } - - @override - Widget build(BuildContext context) { - if (_failed) { - return _AvatarPlaceholder( - title: widget.title, subtitle: widget.subtitle, height: widget.height); - } - return SizedBox( - height: widget.height, - width: double.infinity, - child: _ready - ? (kIsWeb - ? HtmlElementView(viewType: _gl!.textureId!.toString()) - : Texture(textureId: _gl!.textureId!)) - : const Center(child: CircularProgressIndicator()), - ); - } -} - -/// 占位:GLB 资产未接入或加载失败时展示化身信息 -class _AvatarPlaceholder extends StatelessWidget { - final String title; - final String subtitle; - final double height; - - const _AvatarPlaceholder({ - required this.title, - required this.subtitle, - required this.height, - }); - @override Widget build(BuildContext context) { final scheme = Theme.of(context).colorScheme; @@ -214,7 +32,10 @@ class _AvatarPlaceholder extends StatelessWidget { gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [scheme.primaryContainer, scheme.primary.withValues(alpha: 0.3)], + colors: [ + scheme.primaryContainer, + scheme.primary.withValues(alpha: 0.3), + ], ), borderRadius: BorderRadius.circular(16), ), diff --git a/pubspec.lock b/pubspec.lock index 11df863..ee95cb4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,14 +17,6 @@ packages: url: "https://pub.dev" source: hosted version: "12.1.0" - archive: - dependency: transitive - description: - name: archive - sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d - url: "https://pub.dev" - source: hosted - version: "3.6.1" args: dependency: transitive description: @@ -57,14 +49,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.1" - charcode: - dependency: transitive - description: - name: charcode - sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a - url: "https://pub.dev" - source: hosted - version: "1.4.0" cli_config: dependency: transitive description: @@ -121,14 +105,6 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.7" - csslib: - dependency: transitive - description: - name: csslib - sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e" - url: "https://pub.dev" - source: hosted - version: "1.0.2" cupertino_icons: dependency: "direct main" description: @@ -222,46 +198,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_gl: - dependency: "direct main" - description: - name: flutter_gl - sha256: de74c88f77228f47dd280e2092b50eb49fe1fc008de74047d195a27f2db0b491 - url: "https://pub.dev" - source: hosted - version: "0.0.21" - flutter_gl_macos: - dependency: transitive - description: - name: flutter_gl_macos - sha256: "62aa244d4aa9127115df651baec070893718dd27571c8dbca5374dbcb9fd4849" - url: "https://pub.dev" - source: hosted - version: "0.0.5" - flutter_gl_platform_interface: - dependency: transitive - description: - name: flutter_gl_platform_interface - sha256: "03062491fac26d0fda80703e4a01894ecc4fec4f7e5cec131ef021fbf46b2fda" - url: "https://pub.dev" - source: hosted - version: "0.0.4" - flutter_gl_web: - dependency: transitive - description: - name: flutter_gl_web - sha256: "005dc72618ee14659dff7dfe72d6b1fce0efdda745cfebfe4618ce7ac2044af9" - url: "https://pub.dev" - source: hosted - version: "0.0.5" - flutter_gl_windows: - dependency: transitive - description: - name: flutter_gl_windows - sha256: cd9259fb8178863de9e667129f73447fa174447a3c4abd16defa07af648850e4 - url: "https://pub.dev" - source: hosted - version: "0.0.4" flutter_lints: dependency: "direct dev" description: @@ -328,14 +264,6 @@ packages: url: "https://pub.dev" source: hosted version: "17.3.0" - html: - dependency: transitive - description: - name: html - sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602" - url: "https://pub.dev" - source: hosted - version: "0.15.6" http: dependency: transitive description: @@ -360,14 +288,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.2" - image: - dependency: transitive - description: - name: image - sha256: "8e9d133755c3e84c73288363e6343157c383a0c6c56fc51afcc5d4d7180306d6" - url: "https://pub.dev" - source: hosted - version: "3.3.0" image_picker: dependency: "direct main" description: @@ -528,14 +448,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.2" - opentype_dart: - dependency: transitive - description: - name: opentype_dart - sha256: "4bd96aeed494289a87e92bde20afe60f59648dcef253c0a7159b65ffa23899dc" - url: "https://pub.dev" - source: hosted - version: "0.0.1" package_config: dependency: transitive description: @@ -576,14 +488,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" - petitparser: - dependency: transitive - description: - name: petitparser - sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" - url: "https://pub.dev" - source: hosted - version: "7.0.2" platform: dependency: transitive description: @@ -805,22 +709,6 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.17" - three_dart: - dependency: "direct main" - description: - name: three_dart - sha256: "102ff2cc65c2dcb805166c7dc5fa00a9a3252b5aec34fb435cc558a7487dc40b" - url: "https://pub.dev" - source: hosted - version: "0.0.16" - three_dart_jsm: - dependency: "direct main" - description: - name: three_dart_jsm - sha256: "26a71aff4aa842ac8178d2ba8e64b997c434ce9f604fa4fa3ee49f343d2c84b4" - url: "https://pub.dev" - source: hosted - version: "0.0.10" typed_data: dependency: transitive description: @@ -829,30 +717,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" - typr_dart: - dependency: transitive - description: - name: typr_dart - sha256: e8aa717c1445ceccd77bd6ba471683c8e17a9b14797ac09db3bd52d6fbd2fe7b - url: "https://pub.dev" - source: hosted - version: "0.0.2" - universal_html: - dependency: transitive - description: - name: universal_html - sha256: c0bcae5c733c60f26c7dfc88b10b0fd27cbcc45cb7492311cdaa6067e21c9cd4 - url: "https://pub.dev" - source: hosted - version: "2.3.0" - universal_io: - dependency: transitive - description: - name: universal_io - sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2 - url: "https://pub.dev" - source: hosted - version: "2.3.1" uuid: dependency: transitive description: @@ -925,14 +789,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" - xml: - dependency: transitive - description: - name: xml - sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" - url: "https://pub.dev" - source: hosted - version: "6.6.1" yaml: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f8d429c..686a027 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,9 +40,8 @@ dependencies: image_picker: ^1.2.3 shared_preferences: ^2.5.5 fluttertoast: ^9.1.0 - three_dart: ^0.0.16 - three_dart_jsm: ^0.0.10 - flutter_gl: ^0.0.20 + # v2 3D 渲染接入时引入:three_dart ^0.0.16 + three_dart_jsm ^0.0.10 + flutter_gl ^0.0.20 + # (flutter_gl 0.0.21 在 Flutter 3.44 下无法编译:platformViewRegistry 已移除) dev_dependencies: flutter_test: