Files
jiu/client/lib/screens/about/about_screen.dart
T
wangjia 6238b86dcb feat(client): 登录/注册页照原型重建,ds 真相源组件族统一全部屏
- 登录/注册(login.html/register.html 1:1):两栏卡片+品牌渐变面板+主题小衣服
  pill(onSurface 变体)+记住我(记录并预填最近账号)+原型式 toast 校验;
  登录 fidelity 1.4–2.1% 三主题全绿;注册暂不入闸(少 门店编号/兑换券 字段,
  已记 CONTRACT,screens.mjs 留存根)
- ds 原子补齐:DsToast(.toast 单例)/DsCheck(.check/.agree)/DsButton lg 档/
  DsSelect 替换全部旧 DropdownButton/DsField label 在上/DsInput 后缀与密码形态
- 全屏统一:对话框按钮全 DsButton、盒式输入主题钉死(visualDensity.standard、
  h38、InputDecorationTheme 渗漏修复)、图标全 lucide、JetBrains Mono 三端同源、
  BrandMark 真相源 logo、只读模式写操作全量守卫(WriteGuard+DsToast)
- 出入库列表:版式对齐原型(卡片对齐+搜索框居中)、KPI 近30天滚动、结清后
  失效财务应收应付表;商品编辑抽屉介绍库改搜索下拉、图片双击全屏预览
- 删除旧 UI 死代码:DataTableCard/FormDialog/PageScaffold/SearchChip/
  SelectProductDialog/tabStateProvider
- golden/fidelity:stock-in/out 补注册(9%)、login 入册(8%)、goldens 全量重打;
  修复 pubspec flutter_web_plugins 非法声明(CI pub get 阻断)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1g8XV1YhhmHRzhwWEW7o
2026-07-03 09:58:14 +08:00

807 lines
28 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// screens/about/about_screen.dart — 关于我们(照原型 about.html 重建)。
// 居中 col(max760) 四卡:Hero(渐变+链接+反馈) / 产品信息(2×2 cell) /
// 授权信息(3 cell, licenseProvider 实数据) / 更新日志(timeline, /public/release
// 与官网同源,默认 2 条可展开)。旧版多余卡(帮助文档/扫码防伪/系统信息)删除,
// 构建号并入版本 cell 小字(差异记 design/CONTRACT.md)。
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import 'package:lucide_icons_flutter/lucide_icons.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../core/config/app_config.dart';
import '../../core/config/app_info.dart';
import '../../core/responsive/responsive.dart';
import '../../core/theme/app_dims.g.dart';
import '../../core/theme/context_tokens.dart';
import '../../core/update/app_updater.dart';
import '../../core/utils/clock.dart';
import '../../core/utils/dialog_util.dart';
import '../../providers/changelog_provider.dart';
import '../../providers/feedback_provider.dart';
import '../../providers/license_provider.dart';
import '../../providers/update_provider.dart';
import '../../core/theme/app_fonts.dart';
import '../../widgets/brand_mark.dart';
import '../../widgets/ds/ds_atoms.dart';
import '../../widgets/ds/ds_toast.dart';
class AboutScreen extends ConsumerStatefulWidget {
const AboutScreen({super.key});
@override
ConsumerState<AboutScreen> createState() => _AboutScreenState();
}
class _AboutScreenState extends ConsumerState<AboutScreen> {
bool _logExpanded = false;
static const _logShown = 2; // 原型 SHOWN=2,其余折叠
@override
Widget build(BuildContext context) {
final t = context.tokens;
return Container(
color: t.bg,
child: SingleChildScrollView(
padding: context.isMobile
? const EdgeInsets.all(AppDims.sp4)
: const EdgeInsets.fromLTRB(26, 22, 26, 22),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 760),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_heroCard(t),
_productCard(t),
_licenseCard(t),
_changelogCard(t),
// 页脚
Padding(
padding: const EdgeInsets.only(top: 4, bottom: 12),
child: Text('© 2026 岩美科技 · 保留所有权利',
textAlign: TextAlign.center,
style: TextStyle(fontSize: AppDims.fsXs, color: t.faint)),
),
],
),
),
),
),
);
}
/// 原型 .cardsurface / border / r-lg / pad 20 22 / mb18。
Widget _card(dynamic t, Widget child, {Gradient? gradient}) => Container(
margin: const EdgeInsets.only(bottom: 18),
padding: const EdgeInsets.fromLTRB(22, 20, 22, 20),
decoration: BoxDecoration(
color: gradient == null ? t.surface : null,
gradient: gradient,
border: Border.all(color: t.border),
borderRadius: BorderRadius.circular(AppDims.rLg),
),
child: child,
);
/// 卡标题(.card-h 17/700 + primary 图标)。
Widget _cardH(dynamic t, IconData icon, String title) => Padding(
padding: const EdgeInsets.only(bottom: 14),
child: Row(children: [
Icon(icon, size: 18, color: t.primary),
const SizedBox(width: 8),
Text(title,
style: TextStyle(
fontSize: AppDims.fsH2,
fontWeight: FontWeight.w700,
color: t.heading)),
]),
);
// ── 卡1Hero(渐变 + 链接 2×2 + 意见反馈)──────────────────
Widget _heroCard(dynamic t) {
final mobile = context.isMobile;
return _card(
t,
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [t.brand50, t.surface],
),
Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// .hero-top62×62 logo(真相源 BrandMark,镜像原型 hero SVG+ 名称/by
Row(children: [
const BrandMark(size: 62),
const SizedBox(width: 18),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('岩美酒库',
style: TextStyle(
fontSize: AppDims.fsH1,
fontWeight: FontWeight.w800,
color: t.heading)),
const SizedBox(height: 4),
Text('酒水进销存 · 财务对账 · 防伪溯源一体化 · 岩美科技出品',
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
],
),
),
]),
// .hero-links2×2 链接
Container(
margin: const EdgeInsets.only(top: 16),
padding: const EdgeInsets.only(top: 14),
decoration: BoxDecoration(
border: Border(top: BorderSide(color: t.borderSubtle))),
child: mobile
? Column(children: [
for (final l in _links())
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: _linkRow(t, l),
),
])
: Column(children: [
for (var row = 0; row < 2; row++)
Padding(
padding: EdgeInsets.only(bottom: row == 0 ? 10 : 0),
child: Row(children: [
for (var col = 0; col < 2; col++) ...[
if (col > 0) const SizedBox(width: 10),
Expanded(
child: _linkRow(t, _links()[row * 2 + col])),
],
]),
),
]),
),
// .hero-acts:意见反馈
Container(
margin: const EdgeInsets.only(top: 16),
padding: const EdgeInsets.only(top: 14),
decoration: BoxDecoration(
border: Border(top: BorderSide(color: t.borderSubtle))),
child: Row(children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('意见反馈',
style: TextStyle(
fontSize: AppDims.fsBody,
fontWeight: FontWeight.w700,
color: t.heading)),
const SizedBox(height: 2),
Text('遇到 Bug 或有功能建议,欢迎告诉我们',
style:
TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
],
),
),
DsFeedbackButtons(onOpen: _openFeedback),
]),
),
],
),
);
}
List<({IconData icon, String label, VoidCallback onTap})> _links() => [
(
icon: LucideIcons.globe,
label: '官网',
onTap: () => _open(AppInfo.website),
),
(
icon: LucideIcons.messageCircle,
label: '微信客服',
onTap: () {
showDsToast(context,
'微信客服:${AppInfo.wechat.isNotEmpty ? AppInfo.wechat : '请见官网'}');
},
),
(
// 原型 i-ic03 = lock
icon: LucideIcons.lock,
label: '隐私政策',
onTap: () => _open(AppInfo.privacyUrl.isNotEmpty
? AppInfo.privacyUrl
: '${AppInfo.website}/privacy/'),
),
(
icon: LucideIcons.fileText,
label: '服务条款',
onTap: () => _open(AppInfo.termsUrl.isNotEmpty
? AppInfo.termsUrl
: '${AppInfo.website}/terms/'),
),
];
Future<void> _open(String url) async {
if (url.isEmpty) return;
final uri = Uri.parse(url);
if (await canLaunchUrl(uri)) launchUrl(uri);
}
/// 原型 .link-rowicon + 文字 + ›。
Widget _linkRow(
dynamic t, ({IconData icon, String label, VoidCallback onTap}) l) {
return InkWell(
onTap: l.onTap,
borderRadius: BorderRadius.circular(AppDims.rMd),
child: Container(
height: 40,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: t.surface,
border: Border.all(color: t.borderSubtle),
borderRadius: BorderRadius.circular(AppDims.rMd),
),
child: Row(children: [
Icon(l.icon, size: 15, color: t.primary),
const SizedBox(width: 9),
Expanded(
child: Text(l.label,
style: TextStyle(fontSize: AppDims.fsBody, color: t.text)),
),
Text('',
style: TextStyle(fontSize: AppDims.fsTitle, color: t.faint)),
]),
),
);
}
void _openFeedback({required bool isBug}) {
showAppDialog(
context: context,
builder: (_) => _FeedbackDialog(type: isBug ? 'bug' : 'suggestion'),
);
}
// ── 卡2:产品信息(2×2 cell)───────────────────────────────
Widget _productCard(dynamic t) {
final appVersion = ref.watch(appVersionProvider).valueOrNull ?? 'v1.0.0';
final mobile = context.isMobile;
final cells = [
_infoCell(
t,
'当前版本',
Row(children: [
// 原型 .iv.monofs-title 15 / w700 / mono
Text(appVersion,
style: TextStyle(
fontSize: AppDims.fsTitle,
fontWeight: FontWeight.w700,
fontFamily: AppFonts.mono,
fontFamilyFallback: AppFonts.monoFallback,
color: t.heading)),
const SizedBox(width: 6),
FutureBuilder<PackageInfo>(
future: PackageInfo.fromPlatform(),
builder: (_, snap) => Text(
snap.hasData ? 'build ${snap.data!.buildNumber}' : '',
style: TextStyle(fontSize: AppDims.fsXs, color: t.faint)),
),
const Spacer(),
DsSmallGhostButton(
label: '检查更新', icon: LucideIcons.refreshCw, onTap: _checkUpdate),
]),
),
_infoCell(t, '出品方', _iv(t, '岩美科技 · YANMEI TECH')),
_infoCell(t, '多端覆盖', _iv(t, 'Web · macOS · Windows · iOS · 安卓')),
_infoCell(t, '产品定位', _iv(t, '酒水进销存 / 财务 / 防伪溯源')),
];
return _card(
t,
Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
_cardH(t, LucideIcons.box, '产品信息'),
if (mobile)
Column(children: [
for (final c in cells)
Padding(padding: const EdgeInsets.only(bottom: 10), child: c),
])
else
Column(children: [
for (var row = 0; row < 2; row++)
Padding(
padding: EdgeInsets.only(bottom: row == 0 ? 14 : 0),
child: IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
for (var col = 0; col < 2; col++) ...[
if (col > 0) const SizedBox(width: 14),
Expanded(child: cells[row * 2 + col]),
],
],
),
),
),
]),
]),
);
}
Widget _iv(dynamic t, String v) => Text(v,
style: TextStyle(
fontSize: AppDims.fsBody,
fontWeight: FontWeight.w600,
color: t.heading));
/// 原型 .info-cellbg 底 / border-subtle / r-md / pad 14 16。
Widget _infoCell(dynamic t, String label, Widget value) => Container(
padding: const EdgeInsets.fromLTRB(16, 14, 16, 14),
decoration: BoxDecoration(
color: t.bg,
border: Border.all(color: t.borderSubtle),
borderRadius: BorderRadius.circular(AppDims.rMd),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label,
style: TextStyle(fontSize: AppDims.fsXs, color: t.muted)),
const SizedBox(height: 6),
value,
],
),
);
Future<void> _checkUpdate() async {
showDsToast(context, '正在检查更新…');
await ref.read(updateProvider.notifier).forceCheck();
if (!mounted) return;
final r = ref.read(updateProvider).valueOrNull;
if (r == null) {
showDsToast(context, '检查更新失败,请检查网络');
} else if (r.hasUpdate) {
showDsToast(context, '发现新版本 v${r.latestVersion}');
startInAppUpdate(context, r);
} else {
showDsToast(context, '已是最新版 v${r.latestVersion}');
}
}
// ── 卡3:授权信息(3 cell)─────────────────────────────────
Widget _licenseCard(dynamic t) {
final lic = ref.watch(licenseProvider).valueOrNull;
final expires = lic?.expiresAt != null
? DateFormat('yyyy-MM-dd').format(lic!.expiresAt!)
: '';
// 状态/剩余天数走可注入时钟(golden 确定化)
final active = lic?.isActive == true &&
(lic?.expiresAt == null || lic!.expiresAt!.isAfter(appNow()));
final days = lic?.expiresAt != null
? lic!.expiresAt!.difference(appNow()).inDays.clamp(0, 99999)
: null;
final mobile = context.isMobile;
final cells = [
_licCell(t, '授权状态', active ? '已授权' : '未授权',
color: active ? t.success : t.warn),
_licCell(t, '到期日', expires),
_licCell(t, '剩余天数', days != null ? '$days' : ''),
];
return _card(
t,
Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
_cardH(t, LucideIcons.shieldCheck, '授权信息'),
if (mobile)
Column(children: [
for (final c in cells)
Padding(padding: const EdgeInsets.only(bottom: 10), child: c),
])
else
Row(children: [
for (var i = 0; i < cells.length; i++) ...[
if (i > 0) const SizedBox(width: 14),
Expanded(child: cells[i]),
],
]),
]),
);
}
/// 原型 .lic-celllk 11 muted + lv 17/700/mono。
Widget _licCell(dynamic t, String label, String value, {Color? color}) =>
Container(
padding: const EdgeInsets.fromLTRB(16, 14, 16, 14),
decoration: BoxDecoration(
color: t.bg,
border: Border.all(color: t.borderSubtle),
borderRadius: BorderRadius.circular(AppDims.rMd),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label,
style: TextStyle(fontSize: AppDims.fsXs, color: t.muted)),
const SizedBox(height: 6),
Text(value,
style: TextStyle(
fontSize: AppDims.fsH2,
fontWeight: FontWeight.w700,
fontFamily: AppFonts.mono,
fontFamilyFallback: AppFonts.monoFallback,
color: color ?? t.heading)),
],
),
);
// ── 卡4:更新日志(timeline,默认 2 条 + 展开)───────────────
Widget _changelogCard(dynamic t) {
final appVersion = ref.watch(appVersionProvider).valueOrNull ?? '';
final entries = ref.watch(changelogProvider).valueOrNull ?? const [];
final shown = _logExpanded ? entries : entries.take(_logShown).toList();
return _card(
t,
Column(crossAxisAlignment: CrossAxisAlignment.stretch, children: [
_cardH(t, LucideIcons.history, '更新日志'),
if (entries.isEmpty)
Padding(
padding: const EdgeInsets.only(bottom: 4),
child: Text(appVersion.isEmpty ? '暂无更新记录' : '当前版本 $appVersion',
style: TextStyle(fontSize: AppDims.fsSm, color: t.muted)),
)
else ...[
// .timeline:左竖线 + 圆点
for (var i = 0; i < shown.length; i++)
_tlItem(t, shown[i], current: i == 0, last: i == shown.length - 1),
if (entries.length > _logShown)
Padding(
padding: const EdgeInsets.only(top: 10),
child: Align(
alignment: Alignment.centerLeft,
child: DsSmallGhostButton(
label: _logExpanded ? '收起' : '更多历史更新',
onTap: () => setState(() => _logExpanded = !_logExpanded),
),
),
),
],
]),
);
}
Widget _tlItem(dynamic t, ChangelogEntry e,
{required bool current, required bool last}) {
return IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 竖线 + 圆点
SizedBox(
width: 24,
child: Column(children: [
Container(
width: 9,
height: 9,
margin: const EdgeInsets.only(top: 4),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: current ? t.primary : t.surface,
border: Border.all(color: t.primary, width: 1.5),
),
),
if (!last)
Expanded(
child: Container(width: 1.5, color: t.borderSubtle),
),
]),
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(children: [
Text('v${e.version}',
style: TextStyle(
fontWeight: FontWeight.w700,
fontFamily: AppFonts.mono,
fontFamilyFallback: AppFonts.monoFallback,
fontSize: AppDims.fsBody,
color: t.heading)),
const SizedBox(width: 8),
Text(e.date,
style:
TextStyle(fontSize: AppDims.fsXs, color: t.faint)),
]),
for (final s in e.sections) ...[
const SizedBox(height: 8),
_tlTag(t, s.type),
for (final item in s.items)
Padding(
padding: const EdgeInsets.only(top: 4, left: 2),
child: Text('· $item',
style: TextStyle(
fontSize: AppDims.fsSm, color: t.text)),
),
],
],
),
),
),
],
),
);
}
/// 原型 .tl-tagfeat=success / impr=info(primary) / fix=warn。
Widget _tlTag(dynamic t, String type) {
final (label, color) = switch (type) {
'feat' || '新功能' => ('新功能', t.success),
'impr' || 'improve' || '改进' => ('改进', t.primary),
'fix' || '修复' => ('修复', t.warn),
_ => (type, t.muted),
};
return Text(label,
style: TextStyle(
fontSize: AppDims.fsXs, fontWeight: FontWeight.w700, color: color));
}
}
/// hero-acts 的两个反馈按钮(原型 .btn.primary 反馈 Bug + .btn.ghost 功能建议)。
class DsFeedbackButtons extends StatelessWidget {
final void Function({required bool isBug}) onOpen;
const DsFeedbackButtons({super.key, required this.onOpen});
@override
Widget build(BuildContext context) {
final t = context.tokens;
Widget btn(String label, IconData icon, bool primary) => InkWell(
onTap: () => onOpen(isBug: primary),
borderRadius: BorderRadius.circular(AppDims.rMd),
child: Container(
height: 38,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: primary ? t.primary : t.surface,
border: Border.all(color: primary ? t.primary : t.border),
borderRadius: BorderRadius.circular(AppDims.rMd),
),
child: Row(mainAxisSize: MainAxisSize.min, children: [
Icon(icon, size: 16, color: primary ? t.onPrimary : t.text),
const SizedBox(width: 7),
Text(label,
style: TextStyle(
fontSize: AppDims.fsBody,
fontWeight: FontWeight.w600,
color: primary ? t.onPrimary : t.text)),
]),
),
);
return Row(mainAxisSize: MainAxisSize.min, children: [
btn('反馈 Bug', LucideIcons.bug, true),
const SizedBox(width: 10),
btn('功能建议', LucideIcons.lightbulb, false),
]);
}
}
/// 小号 ghost 按钮(.btn.ghost.sm,可带前导图标)。
class DsSmallGhostButton extends StatelessWidget {
final String label;
final IconData? icon;
final VoidCallback onTap;
const DsSmallGhostButton(
{super.key, required this.label, this.icon, required this.onTap});
@override
Widget build(BuildContext context) {
final t = context.tokens;
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(AppDims.rMd),
child: Container(
height: 32,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: t.surface,
border: Border.all(color: t.border),
borderRadius: BorderRadius.circular(AppDims.rMd),
),
alignment: Alignment.center,
child: Row(mainAxisSize: MainAxisSize.min, children: [
if (icon != null) ...[
Icon(icon, size: 14, color: t.text),
const SizedBox(width: 6),
],
Text(label,
style: TextStyle(
fontSize: AppDims.fsSm,
fontWeight: FontWeight.w600,
color: t.text)),
]),
),
);
}
}
/// 反馈表单弹窗:文字 + 附图,直接提交后台(沿用原实现)。
class _FeedbackDialog extends ConsumerStatefulWidget {
final String type; // bug / suggestion
const _FeedbackDialog({required this.type});
@override
ConsumerState<_FeedbackDialog> createState() => _FeedbackDialogState();
}
class _FeedbackDialogState extends ConsumerState<_FeedbackDialog> {
final _ctrl = TextEditingController();
final List<String> _images = []; // 已上传的图片 URL
bool _uploading = false;
bool _submitting = false;
static const _maxImages = 9;
@override
void dispose() {
_ctrl.dispose();
super.dispose();
}
Future<void> _addImages() async {
final result = await FilePicker.platform.pickFiles(
type: FileType.image,
allowMultiple: true,
withData: true,
);
if (result == null) return;
setState(() => _uploading = true);
try {
final repo = ref.read(feedbackRepositoryProvider);
for (final f in result.files) {
if (_images.length >= _maxImages) break;
if (f.bytes == null) continue;
final url = await repo.uploadImage(bytes: f.bytes!, filename: f.name);
if (!mounted) return;
setState(() => _images.add(url));
}
} catch (e) {
if (mounted) {
showDsToast(context, '图片上传失败:$e');
}
} finally {
if (mounted) setState(() => _uploading = false);
}
}
Future<void> _submit() async {
if (_ctrl.text.trim().isEmpty && _images.isEmpty) {
showDsToast(context, '请先填写反馈内容');
return;
}
setState(() => _submitting = true);
try {
await ref.read(feedbackRepositoryProvider).submit(
type: widget.type,
content: _ctrl.text.trim(),
images: _images,
);
if (!mounted) return;
Navigator.pop(context);
showDsToast(context, '已提交反馈,感谢你的反馈 ✓');
} catch (e) {
if (!mounted) return;
setState(() => _submitting = false);
showDsToast(context, '提交失败:$e');
}
}
@override
Widget build(BuildContext context) {
final t = context.tokens;
final isBug = widget.type == 'bug';
return AlertDialog(
title: Text(isBug ? '反馈 Bug' : '功能建议'),
content: SizedBox(
width: context.dialogWidth(480),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
controller: _ctrl,
maxLines: 6,
decoration: InputDecoration(
hintText: isBug ? '请描述你遇到的问题或建议,我们会尽快跟进…' : '请描述您希望增加的功能…',
border: const OutlineInputBorder(),
),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (int i = 0; i < _images.length; i++) _thumb(t, i),
if (_images.length < _maxImages) _addButton(t),
],
),
if (_uploading)
const Padding(
padding: EdgeInsets.only(top: 8),
child: Row(children: [
SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(strokeWidth: 2)),
SizedBox(width: 8),
Text('图片上传中…', style: TextStyle(fontSize: 12)),
]),
),
],
),
),
actions: [
DsButton('取消',
onPressed: _submitting ? null : () => Navigator.pop(context)),
DsButton(
_submitting ? '提交中…' : '提交反馈',
variant: DsBtnVariant.primary,
onPressed: (_submitting || _uploading) ? null : _submit,
),
],
);
}
Widget _thumb(dynamic t, int i) {
return Stack(
clipBehavior: Clip.none,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(AppDims.rMd),
child: Image.network(
'${AppConfig.baseUrl}${_images[i]}',
width: 64,
height: 64,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Container(
width: 64,
height: 64,
color: t.borderSubtle,
child: Icon(LucideIcons.imageOff, size: 20, color: t.muted),
),
),
),
Positioned(
right: -8,
top: -8,
child: GestureDetector(
onTap: () => setState(() => _images.removeAt(i)),
child: Container(
padding: const EdgeInsets.all(2),
decoration:
BoxDecoration(color: t.danger, shape: BoxShape.circle),
child: Icon(LucideIcons.x, size: 12, color: t.onPrimary),
),
),
),
],
);
}
Widget _addButton(dynamic t) {
return InkWell(
onTap: _uploading ? null : _addImages,
borderRadius: BorderRadius.circular(AppDims.rMd),
child: Container(
width: 64,
height: 64,
decoration: BoxDecoration(
border: Border.all(color: t.border),
borderRadius: BorderRadius.circular(AppDims.rMd),
color: t.bg,
),
child: Icon(LucideIcons.imagePlus, size: 20, color: t.muted),
),
);
}
}