ops-console v1.0.2 (versionCode 8) 源码入库
This commit is contained in:
358
lib/screens/admin/app/app_version_page.dart
Normal file
358
lib/screens/admin/app/app_version_page.dart
Normal file
@@ -0,0 +1,358 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../models/app_version.dart';
|
||||
import '../../../services/app_service.dart';
|
||||
import '../../../services/api_client.dart';
|
||||
import '../../../utils/formatters.dart';
|
||||
|
||||
/// 应用版本管理页。
|
||||
class AppVersionPage extends ConsumerStatefulWidget {
|
||||
const AppVersionPage({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<AppVersionPage> createState() => _AppVersionPageState();
|
||||
}
|
||||
|
||||
class _AppVersionPageState extends ConsumerState<AppVersionPage> {
|
||||
final _searchController = TextEditingController();
|
||||
final _scrollController = ScrollController();
|
||||
|
||||
List<AppVersion> _items = [];
|
||||
bool _loading = false;
|
||||
bool _hasMore = true;
|
||||
int _page = 1;
|
||||
String? _error;
|
||||
String _keyword = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController.addListener(_onScroll);
|
||||
_loadData();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
_scrollController.removeListener(_onScroll);
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
if (_scrollController.position.pixels >=
|
||||
_scrollController.position.maxScrollExtent - 200 &&
|
||||
!_loading &&
|
||||
_hasMore) {
|
||||
_loadMore();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadData() async {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_error = null;
|
||||
});
|
||||
try {
|
||||
final list = await AppService().getAppVersionList(page: 1, limit: 20);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_items = list;
|
||||
_loading = false;
|
||||
_hasMore = list.length >= 20;
|
||||
_page = 1;
|
||||
});
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_loading = false;
|
||||
_error = _friendlyError(e);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadMore() async {
|
||||
if (_loading) return;
|
||||
setState(() => _loading = true);
|
||||
try {
|
||||
final list = await AppService().getAppVersionList(page: _page + 1, limit: 20);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_items.addAll(list);
|
||||
_loading = false;
|
||||
_hasMore = list.length >= 20;
|
||||
_page++;
|
||||
});
|
||||
} catch (_) {
|
||||
if (!mounted) return;
|
||||
setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refresh() async {
|
||||
_page = 1;
|
||||
_hasMore = true;
|
||||
await _loadData();
|
||||
}
|
||||
|
||||
String _friendlyError(Object e) {
|
||||
if (e is ApiException) return e.message;
|
||||
final msg = e.toString();
|
||||
if (msg.contains('404') ||
|
||||
msg.contains('DioException') ||
|
||||
msg.contains('SocketException')) {
|
||||
return '应用版本接口暂不可用,请联系管理员确认权限或接口地址';
|
||||
}
|
||||
return '加载失败:$msg';
|
||||
}
|
||||
|
||||
void _showForm({AppVersion? editing}) {
|
||||
final isEdit = editing != null;
|
||||
final appNameCtrl = TextEditingController(text: editing?.appName ?? '');
|
||||
final packageCtrl = TextEditingController(text: editing?.packageName ?? '');
|
||||
final versionNameCtrl = TextEditingController(text: editing?.versionName ?? '');
|
||||
final versionCodeCtrl =
|
||||
TextEditingController(text: editing?.versionCode.toString() ?? '');
|
||||
final apkUrlCtrl = TextEditingController(text: editing?.apkUrl ?? '');
|
||||
final orgIdCtrl = TextEditingController(text: editing?.orgId ?? '');
|
||||
final groupIdCtrl = TextEditingController(text: editing?.groupId ?? '');
|
||||
int statusValue = editing?.status ?? 0;
|
||||
|
||||
showModalBottomSheet(
|
||||
useSafeArea: true,
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
builder: (ctx, setSheet) => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(ctx).viewInsets.bottom,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(isEdit ? '编辑版本' : '新增版本',
|
||||
style: Theme.of(ctx).textTheme.titleMedium),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: appNameCtrl,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '应用名称', border: OutlineInputBorder())),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: packageCtrl,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '包名', border: OutlineInputBorder())),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: versionNameCtrl,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '版本名', border: OutlineInputBorder())),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: versionCodeCtrl,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '版本号', border: OutlineInputBorder())),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: apkUrlCtrl,
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'APK下载地址', border: OutlineInputBorder())),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: orgIdCtrl,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '组织ID', border: OutlineInputBorder())),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: groupIdCtrl,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '分组ID', border: OutlineInputBorder())),
|
||||
const SizedBox(height: 12),
|
||||
DropdownButtonFormField<int>(
|
||||
value: statusValue,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '状态', border: OutlineInputBorder()),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 0, child: Text('正常')),
|
||||
DropdownMenuItem(value: 1, child: Text('禁用')),
|
||||
],
|
||||
onChanged: (v) {
|
||||
if (v != null) setSheet(() => statusValue = v);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
final data = <String, dynamic>{
|
||||
if (isEdit) 'id': editing!.id,
|
||||
'appName': appNameCtrl.text,
|
||||
'packageName': packageCtrl.text,
|
||||
'versionName': versionNameCtrl.text,
|
||||
'versionCode': int.tryParse(versionCodeCtrl.text) ?? 0,
|
||||
'apkUrl': apkUrlCtrl.text,
|
||||
'orgId': orgIdCtrl.text,
|
||||
'groupId': groupIdCtrl.text,
|
||||
'status': statusValue,
|
||||
};
|
||||
try {
|
||||
if (isEdit) {
|
||||
await AppService().updateAppVersion(data);
|
||||
} else {
|
||||
await AppService().saveAppVersion(data);
|
||||
}
|
||||
if (!ctx.mounted) return;
|
||||
Navigator.pop(ctx);
|
||||
_refresh();
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('保存成功')),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!ctx.mounted) return;
|
||||
ScaffoldMessenger.of(ctx).showSnackBar(
|
||||
SnackBar(content: Text(e is ApiException ? e.message : '保存失败')),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Text('保存'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _delete(String id) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('确认删除'),
|
||||
content: const Text('确定要删除此版本吗?'),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')),
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('删除')),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true) return;
|
||||
try {
|
||||
await AppService().deleteAppVersion([int.tryParse(id) ?? 0]);
|
||||
_refresh();
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('删除成功')));
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(e is ApiException ? e.message : '删除失败')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(automaticallyImplyLeading: false, title: const Text('应用版本管理')),
|
||||
body: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: '搜索应用名称',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
suffixIcon: _keyword.isEmpty
|
||||
? null
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
setState(() => _keyword = '');
|
||||
},
|
||||
),
|
||||
),
|
||||
onSubmitted: (v) => setState(() => _keyword = v),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _error != null
|
||||
? Center(child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(_error!),
|
||||
const SizedBox(height: 8),
|
||||
FilledButton.tonal(onPressed: _refresh, child: const Text('重试')),
|
||||
],
|
||||
))
|
||||
: RefreshIndicator(
|
||||
onRefresh: _refresh,
|
||||
child: _items.isEmpty && !_loading
|
||||
? const Center(child: Text('暂无数据'))
|
||||
: ListView.builder(
|
||||
controller: _scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
itemCount: _items.length + (_loading ? 1 : 0),
|
||||
itemBuilder: (ctx, i) {
|
||||
if (i >= _items.length) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
final item = _items[i];
|
||||
if (_keyword.isNotEmpty &&
|
||||
!item.appName.toLowerCase().contains(_keyword.toLowerCase())) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Card(
|
||||
child: ListTile(
|
||||
title: Text(item.appName,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
Text('包名: ${item.packageName}'),
|
||||
Text('版本: ${item.versionName} (${item.versionCode})'),
|
||||
if (item.createTime != null)
|
||||
Text('创建: ${Formatters.formatDateTime(item.createTime!)}'),
|
||||
const SizedBox(height: 4),
|
||||
Chip(
|
||||
label: Text(item.status == 0 ? '正常' : '禁用'),
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: PopupMenuButton<String>(
|
||||
onSelected: (v) {
|
||||
if (v == 'edit') _showForm(editing: item);
|
||||
if (v == 'delete') _delete(item.id);
|
||||
},
|
||||
itemBuilder: (_) => [
|
||||
const PopupMenuItem(value: 'edit', child: Text('编辑')),
|
||||
const PopupMenuItem(value: 'delete', child: Text('删除')),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => _showForm(),
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
346
lib/screens/admin/app/tv_app_page.dart
Normal file
346
lib/screens/admin/app/tv_app_page.dart
Normal file
@@ -0,0 +1,346 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../models/tv_app.dart';
|
||||
import '../../../services/app_service.dart';
|
||||
import '../../../services/api_client.dart';
|
||||
|
||||
/// TV端应用管理页。
|
||||
class TvAppPage extends ConsumerStatefulWidget {
|
||||
const TvAppPage({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<TvAppPage> createState() => _TvAppPageState();
|
||||
}
|
||||
|
||||
class _TvAppPageState extends ConsumerState<TvAppPage> {
|
||||
final _searchController = TextEditingController();
|
||||
final _scrollController = ScrollController();
|
||||
|
||||
List<TvApp> _items = [];
|
||||
bool _loading = false;
|
||||
bool _hasMore = true;
|
||||
int _page = 1;
|
||||
String? _error;
|
||||
String _keyword = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController.addListener(_onScroll);
|
||||
_loadData();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
_scrollController.removeListener(_onScroll);
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
if (_scrollController.position.pixels >=
|
||||
_scrollController.position.maxScrollExtent - 200 &&
|
||||
!_loading &&
|
||||
_hasMore) {
|
||||
_loadMore();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadData() async {
|
||||
setState(() {
|
||||
_loading = true;
|
||||
_error = null;
|
||||
});
|
||||
try {
|
||||
final kw = _keyword.trim().isEmpty ? null : _keyword.trim();
|
||||
final list = await AppService().getTvAppList(page: 1, limit: 20, appName: kw);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_items = list;
|
||||
_loading = false;
|
||||
_hasMore = list.length >= 20;
|
||||
_page = 1;
|
||||
});
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_loading = false;
|
||||
_error = e is ApiException ? e.message : '加载失败';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadMore() async {
|
||||
if (_loading) return;
|
||||
setState(() => _loading = true);
|
||||
try {
|
||||
final kw = _keyword.trim().isEmpty ? null : _keyword.trim();
|
||||
final list = await AppService().getTvAppList(page: _page + 1, limit: 20, appName: kw);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_items.addAll(list);
|
||||
_loading = false;
|
||||
_hasMore = list.length >= 20;
|
||||
_page++;
|
||||
});
|
||||
} catch (_) {
|
||||
if (!mounted) return;
|
||||
setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _refresh() async {
|
||||
_page = 1;
|
||||
_hasMore = true;
|
||||
await _loadData();
|
||||
}
|
||||
|
||||
void _showForm({TvApp? editing}) {
|
||||
final isEdit = editing != null;
|
||||
final nameCtrl = TextEditingController(text: editing?.appName ?? '');
|
||||
final pkgCtrl = TextEditingController(text: editing?.packageName ?? '');
|
||||
final catCtrl = TextEditingController(text: editing?.appCategory ?? '');
|
||||
final iconCtrl = TextEditingController(text: editing?.iconUrl ?? '');
|
||||
final orgIdCtrl = TextEditingController(text: editing?.orgId ?? '');
|
||||
final groupIdCtrl = TextEditingController(text: editing?.groupId ?? '');
|
||||
int statusValue = editing?.status ?? 0;
|
||||
|
||||
showModalBottomSheet(
|
||||
useSafeArea: true,
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (ctx) => StatefulBuilder(
|
||||
builder: (ctx, setSheet) => Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(ctx).viewInsets.bottom),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(isEdit ? '编辑应用' : '新增应用',
|
||||
style: Theme.of(ctx).textTheme.titleMedium),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: nameCtrl,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '应用名称', border: OutlineInputBorder())),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: pkgCtrl,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '包名', border: OutlineInputBorder())),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: catCtrl,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '分类', border: OutlineInputBorder())),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: iconCtrl,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '图标地址', border: OutlineInputBorder())),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: orgIdCtrl,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '组织ID', border: OutlineInputBorder())),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: groupIdCtrl,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '分组ID', border: OutlineInputBorder())),
|
||||
const SizedBox(height: 12),
|
||||
DropdownButtonFormField<int>(
|
||||
value: statusValue,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '状态', border: OutlineInputBorder()),
|
||||
items: const [
|
||||
DropdownMenuItem(value: 0, child: Text('正常')),
|
||||
DropdownMenuItem(value: 1, child: Text('禁用')),
|
||||
],
|
||||
onChanged: (v) {
|
||||
if (v != null) setSheet(() => statusValue = v);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
final data = <String, dynamic>{
|
||||
if (isEdit) 'id': editing!.id,
|
||||
'appName': nameCtrl.text,
|
||||
'packageName': pkgCtrl.text,
|
||||
'appCategory': catCtrl.text,
|
||||
'iconUrl': iconCtrl.text,
|
||||
'orgId': orgIdCtrl.text,
|
||||
'groupId': groupIdCtrl.text,
|
||||
'status': statusValue,
|
||||
};
|
||||
try {
|
||||
await AppService().saveTvApp(data);
|
||||
if (!ctx.mounted) return;
|
||||
Navigator.pop(ctx);
|
||||
_refresh();
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('保存成功')),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!ctx.mounted) return;
|
||||
ScaffoldMessenger.of(ctx).showSnackBar(
|
||||
SnackBar(content: Text(e is ApiException ? e.message : '保存失败')),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: const Text('保存'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _delete(String id) async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: const Text('确认删除'),
|
||||
content: const Text('确定要删除此应用吗?'),
|
||||
actions: [
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, false), child: const Text('取消')),
|
||||
TextButton(onPressed: () => Navigator.pop(ctx, true), child: const Text('删除')),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed != true) return;
|
||||
try {
|
||||
await AppService().deleteTvApp([int.tryParse(id) ?? 0]);
|
||||
_refresh();
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('删除成功')));
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(e is ApiException ? e.message : '删除失败')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(automaticallyImplyLeading: false, title: const Text('TV端应用管理')),
|
||||
body: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: '搜索应用名称',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
suffixIcon: _keyword.isEmpty
|
||||
? null
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
setState(() => _keyword = '');
|
||||
_refresh();
|
||||
},
|
||||
),
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
onSubmitted: (v) {
|
||||
setState(() => _keyword = v);
|
||||
_refresh();
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _error != null
|
||||
? Center(child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(_error!),
|
||||
const SizedBox(height: 8),
|
||||
FilledButton.tonal(onPressed: _refresh, child: const Text('重试')),
|
||||
],
|
||||
))
|
||||
: RefreshIndicator(
|
||||
onRefresh: _refresh,
|
||||
child: _items.isEmpty && !_loading
|
||||
? const Center(child: Text('暂无数据'))
|
||||
: ListView.builder(
|
||||
controller: _scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
itemCount: _items.length + (_loading ? 1 : 0),
|
||||
itemBuilder: (ctx, i) {
|
||||
if (i >= _items.length) {
|
||||
return const Padding(
|
||||
padding: EdgeInsets.all(16),
|
||||
child: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
final item = _items[i];
|
||||
return Card(
|
||||
child: ListTile(
|
||||
leading: item.iconUrl.isNotEmpty
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.network(
|
||||
item.iconUrl,
|
||||
width: 40,
|
||||
height: 40,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, __, ___) =>
|
||||
const Icon(Icons.apps, size: 40),
|
||||
),
|
||||
)
|
||||
: const CircleAvatar(child: Icon(Icons.apps)),
|
||||
title: Text(item.appName,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600)),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
Text('包名: ${item.packageName}'),
|
||||
if (item.appCategory.isNotEmpty)
|
||||
Text('分类: ${item.appCategory}'),
|
||||
const SizedBox(height: 4),
|
||||
Chip(
|
||||
label: Text(item.status == 0 ? '正常' : '禁用'),
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: PopupMenuButton<String>(
|
||||
onSelected: (v) {
|
||||
if (v == 'edit') _showForm(editing: item);
|
||||
if (v == 'delete') _delete(item.id);
|
||||
},
|
||||
itemBuilder: (_) => [
|
||||
const PopupMenuItem(value: 'edit', child: Text('编辑')),
|
||||
const PopupMenuItem(value: 'delete', child: Text('删除')),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: () => _showForm(),
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user