feat: 完成 Flutter 版智能电网 IoT 管理应用初始化搭建
This commit is contained in:
commit
f61b7bf90b
120 files changed
+15229
No files matched your search
@@ -0,0 +1,338 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/theme/app_typography.dart';
|
||||
import '../../../core/utils/format_util.dart';
|
||||
import '../../../shared/widgets/app_feedback.dart';
|
||||
import '../../../shared/widgets/x_card.dart';
|
||||
import '../../../shared/widgets/x_chip.dart';
|
||||
import '../../../shared/widgets/x_common.dart';
|
||||
import '../../../shared/widgets/x_icon.dart';
|
||||
import '../../../shared/widgets/x_top_bar.dart';
|
||||
import '../../customer/data/customer_api.dart';
|
||||
import '../data/project_api.dart';
|
||||
import '../data/project_models.dart';
|
||||
|
||||
/// 项目详情页。
|
||||
///
|
||||
/// 源 `pages/project/detail.vue` 基于 mock 数据(团队 / 里程碑 / 设备概览等),
|
||||
/// 这里按用户确认改为直连后端 `/api/project-plans/:id`,保留信息卡 + 分组 + 明细列表的视觉语言。
|
||||
class ProjectDetailPage extends ConsumerStatefulWidget {
|
||||
const ProjectDetailPage({super.key, required this.id});
|
||||
|
||||
final int id;
|
||||
|
||||
@override
|
||||
ConsumerState<ProjectDetailPage> createState() => _ProjectDetailPageState();
|
||||
}
|
||||
|
||||
class _ProjectDetailPageState extends ConsumerState<ProjectDetailPage> {
|
||||
ProjectPlan? _plan;
|
||||
String? _customerName;
|
||||
bool _loading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_load();
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
setState(() => _loading = true);
|
||||
try {
|
||||
final res = await ref.read(projectApiProvider).detail(widget.id);
|
||||
if (!mounted) return;
|
||||
if (res.isOk && res.data != null) {
|
||||
setState(() => _plan = res.data);
|
||||
await _loadCustomer(res.data!.customerId);
|
||||
} else {
|
||||
showAppToast(res.message.isEmpty ? '方案不存在' : res.message);
|
||||
}
|
||||
} catch (_) {
|
||||
if (mounted) showAppToast('加载失败');
|
||||
} finally {
|
||||
if (mounted) setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadCustomer(int customerId) async {
|
||||
try {
|
||||
final res = await ref.read(customerApiProvider).detail(customerId);
|
||||
if (!mounted) return;
|
||||
if (res.isOk && res.data != null) {
|
||||
setState(() => _customerName = res.data!.customerName);
|
||||
}
|
||||
} catch (_) {
|
||||
// 客户名称获取失败时降级为「客户 #id」
|
||||
}
|
||||
}
|
||||
|
||||
String _customerLabel(int customerId) {
|
||||
final name = _customerName;
|
||||
return (name == null || name.isEmpty) ? '客户 #$customerId' : name;
|
||||
}
|
||||
|
||||
String _textOrDash(String? value) =>
|
||||
(value == null || value.isEmpty) ? '-' : value;
|
||||
|
||||
String _formatQty(num quantity) => quantity == quantity.roundToDouble()
|
||||
? quantity.toInt().toString()
|
||||
: quantity.toString();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final planNo = _plan?.planNo;
|
||||
return XPageShell(
|
||||
topBar: XTopBar.nav(
|
||||
title: (planNo == null || planNo.isEmpty) ? '方案详情' : planNo,
|
||||
onBack: () => context.pop(),
|
||||
),
|
||||
child: _buildBody(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildBody() {
|
||||
final p = _plan;
|
||||
if (p == null) {
|
||||
return XPlaceholder(text: _loading ? '加载中...' : '方案不存在');
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 60),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildHeaderCard(p),
|
||||
_buildInfoCard(p),
|
||||
_buildDetailSection(p),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeaderCard(ProjectPlan p) {
|
||||
return XCard(
|
||||
padding: const EdgeInsets.all(16),
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 6,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
_customerLabel(p.customerId),
|
||||
style: AppTypography.headlineMdMobile.copyWith(
|
||||
color: AppColors.onSurface,
|
||||
),
|
||||
),
|
||||
XChip(
|
||||
projectStatusText(p.status),
|
||||
tone: projectStatusTone(p.status),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'方案单号',
|
||||
style: AppTypography.bodyMd.copyWith(
|
||||
fontSize: 12,
|
||||
color: AppColors.textMuted,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
p.displayTitle,
|
||||
style: AppTypography.dataMono.copyWith(
|
||||
color: AppColors.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoCard(ProjectPlan p) {
|
||||
final items = <Widget>[
|
||||
_infoItem('person', '登记人', _textOrDash(p.registrar)),
|
||||
_infoItem('calendar', '登记时间', formatDate(p.registerTime)),
|
||||
_infoItem('clock', '创建时间', formatDateTime(p.createdAt)),
|
||||
_infoItem('category', '方案说明', _textOrDash(p.planDesc)),
|
||||
];
|
||||
|
||||
return XCard(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: Column(
|
||||
children: [
|
||||
for (var i = 0; i < items.length; i++) ...[
|
||||
if (i > 0) Container(height: 1, color: AppColors.borderRule),
|
||||
items[i],
|
||||
],
|
||||
if (p.planFileUrl != null && p.planFileUrl!.isNotEmpty) ...[
|
||||
Container(height: 1, color: AppColors.borderRule),
|
||||
_buildFileRow(p.planFileUrl!),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _infoItem(String icon, String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 13),
|
||||
child: Row(
|
||||
children: [
|
||||
XIcon(icon, size: 18, color: AppColors.textMuted),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: AppTypography.bodyMd.copyWith(
|
||||
fontSize: 12,
|
||||
color: AppColors.textMuted,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: AppTypography.bodyMd.copyWith(
|
||||
color: AppColors.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 方案文件:源页面用 uni 下载打开,Flutter 未引入 url_launcher,这里仅展示链接文本。
|
||||
Widget _buildFileRow(String url) {
|
||||
return GestureDetector(
|
||||
onTap: () => showAppToast(url),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 13),
|
||||
child: Row(
|
||||
children: [
|
||||
const XIcon('domain', size: 18, color: AppColors.textMuted),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'方案文件',
|
||||
style: AppTypography.bodyMd.copyWith(
|
||||
fontSize: 12,
|
||||
color: AppColors.textMuted,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
url,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: AppTypography.bodyMd.copyWith(
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const XIcon('forward', size: 16, color: AppColors.textPlaceholder),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailSection(ProjectPlan p) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 22, bottom: 10),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const XSectionTitle('设备明细', icon: 'category', fontSize: 16),
|
||||
Text(
|
||||
'合计 ${_formatQty(p.detailQuantityTotal)}',
|
||||
style: AppTypography.dataMono.copyWith(
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (p.details.isEmpty)
|
||||
const XPlaceholder(text: '暂无设备明细', dashed: true)
|
||||
else
|
||||
_buildDetailCard(p.details),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDetailCard(List<ProjectPlanDetail> details) {
|
||||
final items = <Widget>[];
|
||||
for (var i = 0; i < details.length; i++) {
|
||||
if (i > 0) items.add(Container(height: 1, color: AppColors.borderRule));
|
||||
items.add(_detailItem(details[i]));
|
||||
}
|
||||
return XCard(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: Column(children: items),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _detailItem(ProjectPlanDetail d) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 13),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
d.equipName.isEmpty ? '未命名设备' : d.equipName,
|
||||
style: AppTypography.bodyMd.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.onSurface,
|
||||
),
|
||||
),
|
||||
if (d.remark != null && d.remark!.isNotEmpty) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
d.remark!,
|
||||
style: AppTypography.bodyMd.copyWith(
|
||||
fontSize: 12,
|
||||
color: AppColors.textMuted,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
'×${_formatQty(d.quantity)}',
|
||||
style: AppTypography.dataMono.copyWith(color: AppColors.primary),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../app/tab_index.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import '../../../core/theme/app_dimens.dart';
|
||||
import '../../../core/theme/app_typography.dart';
|
||||
import '../../../core/utils/format_util.dart';
|
||||
import '../../../shared/widgets/app_feedback.dart';
|
||||
import '../../../shared/widgets/x_card.dart';
|
||||
import '../../../shared/widgets/x_chip.dart';
|
||||
import '../../../shared/widgets/x_common.dart';
|
||||
import '../../../shared/widgets/x_icon.dart';
|
||||
import '../../../shared/widgets/x_top_bar.dart';
|
||||
import '../../customer/data/customer_api.dart';
|
||||
import '../data/project_api.dart';
|
||||
import '../data/project_models.dart';
|
||||
|
||||
/// 项目 Tab。
|
||||
///
|
||||
/// 源 `components/tabs/ProjectTab.vue` 使用本地 mock(字段为 projectName / progress / stage 等),
|
||||
/// 这里按用户确认改为直连后端 `/api/project-plans`,视觉沿用原卡片式语言但字段换成后端真实字段。
|
||||
class ProjectTab extends ConsumerStatefulWidget {
|
||||
const ProjectTab({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ProjectTab> createState() => _ProjectTabState();
|
||||
}
|
||||
|
||||
class _ProjectTabState extends ConsumerState<ProjectTab> {
|
||||
static const _statusOptions = <({String label, int? value})>[
|
||||
(label: '全部', value: null),
|
||||
(label: '草稿', value: ProjectPlanStatus.draft),
|
||||
(label: '已确认', value: ProjectPlanStatus.confirmed),
|
||||
];
|
||||
|
||||
final _searchController = TextEditingController();
|
||||
|
||||
Timer? _searchTimer;
|
||||
List<ProjectPlan> _list = const [];
|
||||
Map<int, String> _customerNames = const {};
|
||||
bool _loading = false;
|
||||
int? _filterStatus;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_loadCustomers();
|
||||
_load();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchTimer?.cancel();
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// `/project-plans` 只返回 customerId,需要先建立 id → 客户名称 映射用于展示。
|
||||
Future<void> _loadCustomers() async {
|
||||
try {
|
||||
final res = await ref
|
||||
.read(customerApiProvider)
|
||||
.list(page: 1, pageSize: 200);
|
||||
if (!mounted) return;
|
||||
if (res.isOk && res.data != null) {
|
||||
setState(() {
|
||||
_customerNames = {
|
||||
for (final c in res.data!.list) c.id: c.customerName,
|
||||
};
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
// 客户名称映射失败时降级为「客户 #id」,不影响方案列表展示
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
setState(() => _loading = true);
|
||||
try {
|
||||
final keyword = _searchController.text.trim();
|
||||
final res = await ref
|
||||
.read(projectApiProvider)
|
||||
.list(
|
||||
page: 1,
|
||||
pageSize: 100,
|
||||
keyword: keyword.isEmpty ? null : keyword,
|
||||
status: _filterStatus,
|
||||
);
|
||||
if (!mounted) return;
|
||||
if (res.isOk && res.data != null) {
|
||||
setState(() => _list = res.data!.list);
|
||||
}
|
||||
} catch (_) {
|
||||
showAppToast('加载方案失败');
|
||||
} finally {
|
||||
if (mounted) setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _onSearchChanged(String _) {
|
||||
_searchTimer?.cancel();
|
||||
_searchTimer = Timer(const Duration(milliseconds: 350), _load);
|
||||
}
|
||||
|
||||
void _onStatusChip(int? value) {
|
||||
if (_filterStatus == value) return;
|
||||
setState(() => _filterStatus = value);
|
||||
_load();
|
||||
}
|
||||
|
||||
Future<void> _goDetail(ProjectPlan p) async {
|
||||
await context.push('/project/detail/${p.id}');
|
||||
if (mounted) _load();
|
||||
}
|
||||
|
||||
String _customerLabel(int customerId) {
|
||||
final name = _customerNames[customerId];
|
||||
return (name == null || name.isEmpty) ? '客户 #$customerId' : name;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ref.listen(tabIndexProvider, (previous, next) {
|
||||
if (next == AppTab.project) _load();
|
||||
});
|
||||
|
||||
return XPageShell(
|
||||
topBar: XTopBar.brand(
|
||||
title: '鑫潼智电',
|
||||
trailingColor: AppColors.primary,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildPageHead(),
|
||||
_buildSearchBar(),
|
||||
_buildFilters(),
|
||||
Expanded(child: _buildList()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPageHead() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('项目', style: AppTypography.headlineMd),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: const Text(
|
||||
'管理与跟踪智电改造方案',
|
||||
style: TextStyle(fontSize: 14, color: AppColors.textMuted),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSearchBar() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 14, 16, 8),
|
||||
child: XSearchField(
|
||||
controller: _searchController,
|
||||
hintText: '按方案单号或说明搜索...',
|
||||
onChanged: _onSearchChanged,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilters() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, top: 4, bottom: 8),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
for (final option in _statusOptions)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: XFilterChip(
|
||||
label: option.label,
|
||||
selected: _filterStatus == option.value,
|
||||
onTap: () => _onStatusChip(option.value),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildList() {
|
||||
if (_loading && _list.isEmpty) {
|
||||
return const XPlaceholder(text: '加载中...');
|
||||
}
|
||||
if (_list.isEmpty) {
|
||||
return const XPlaceholder(text: '暂无方案');
|
||||
}
|
||||
return RefreshIndicator(
|
||||
color: AppColors.primary,
|
||||
onRefresh: _load,
|
||||
child: ListView.builder(
|
||||
padding: const EdgeInsets.fromLTRB(16, 4, 16, 120),
|
||||
itemCount: _list.length,
|
||||
itemBuilder: (context, index) => _planCard(_list[index]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _planCard(ProjectPlan p) {
|
||||
final registrar = p.registrar;
|
||||
|
||||
return XCard(
|
||||
padding: EdgeInsets.zero,
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
onTap: () => _goDetail(p),
|
||||
child: Stack(
|
||||
children: [
|
||||
// 左侧状态色条贴边,圆角按卡片圆角裁出
|
||||
Positioned(
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: 4,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: projectStatusBarColor(p.status),
|
||||
borderRadius: const BorderRadius.horizontal(
|
||||
left: Radius.circular(AppRadius.lg),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
_customerLabel(p.customerId),
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
XChip(
|
||||
projectStatusText(p.status),
|
||||
tone: projectStatusTone(p.status),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
p.displayTitle,
|
||||
style: AppTypography.dataMono.copyWith(
|
||||
color: AppColors.textMuted,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
const XIcon(
|
||||
'person',
|
||||
size: 15,
|
||||
color: AppColors.textMuted,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
(registrar == null || registrar.isEmpty)
|
||||
? '未填写登记人'
|
||||
: registrar,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppColors.textMuted,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const XIcon('clock', size: 15, color: AppColors.textMuted),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
formatDate(p.registerTime),
|
||||
style: AppTypography.dataMono.copyWith(
|
||||
color: AppColors.textMuted,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (p.planDesc != null && p.planDesc!.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
p.planDesc!,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppColors.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user