feat: 用户管理对接真实 API
后端: 新增 /api/v1/users CRUD + 重置密码接口
前端: user model/repository/provider + 设置页用户 tab 改为真实数据,
支持新增、编辑、启停、重置密码
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/wangjia/jiu/backend/internal/middleware"
|
||||
"github.com/wangjia/jiu/backend/internal/model"
|
||||
)
|
||||
|
||||
type UserHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewUserHandler(db *gorm.DB) *UserHandler {
|
||||
return &UserHandler{db: db}
|
||||
}
|
||||
|
||||
// List GET /api/v1/users
|
||||
func (h *UserHandler) List(c *gin.Context) {
|
||||
shopID := middleware.GetShopID(c)
|
||||
var users []model.User
|
||||
h.db.Where("shop_id = ? AND deleted_at IS NULL", shopID).
|
||||
Order("id ASC").Find(&users)
|
||||
c.JSON(http.StatusOK, gin.H{"data": users})
|
||||
}
|
||||
|
||||
// Create POST /api/v1/users
|
||||
func (h *UserHandler) Create(c *gin.Context) {
|
||||
shopID := middleware.GetShopID(c)
|
||||
var req struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
RealName string `json:"real_name"`
|
||||
Phone string `json:"phone"`
|
||||
Role string `json:"role"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "密码加密失败"})
|
||||
return
|
||||
}
|
||||
role := req.Role
|
||||
if role == "" {
|
||||
role = "operator"
|
||||
}
|
||||
u := model.User{
|
||||
TenantBase: model.TenantBase{ShopID: shopID},
|
||||
Username: req.Username,
|
||||
PasswordHash: string(hash),
|
||||
RealName: req.RealName,
|
||||
Phone: req.Phone,
|
||||
Role: role,
|
||||
IsActive: true,
|
||||
}
|
||||
if err := h.db.Create(&u).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "用户名已存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusCreated, gin.H{"data": u})
|
||||
}
|
||||
|
||||
// Update PUT /api/v1/users/:id
|
||||
func (h *UserHandler) Update(c *gin.Context) {
|
||||
shopID := middleware.GetShopID(c)
|
||||
var u model.User
|
||||
if err := h.db.Where("id = ? AND shop_id = ? AND deleted_at IS NULL", c.Param("id"), shopID).
|
||||
First(&u).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
RealName string `json:"real_name"`
|
||||
Phone string `json:"phone"`
|
||||
Role string `json:"role"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
c.ShouldBindJSON(&req)
|
||||
if req.RealName != "" {
|
||||
u.RealName = req.RealName
|
||||
}
|
||||
if req.Phone != "" {
|
||||
u.Phone = req.Phone
|
||||
}
|
||||
if req.Role != "" {
|
||||
u.Role = req.Role
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
u.IsActive = *req.IsActive
|
||||
}
|
||||
h.db.Save(&u)
|
||||
c.JSON(http.StatusOK, gin.H{"data": u})
|
||||
}
|
||||
|
||||
// ResetPassword PUT /api/v1/users/:id/reset-password
|
||||
func (h *UserHandler) ResetPassword(c *gin.Context) {
|
||||
shopID := middleware.GetShopID(c)
|
||||
var u model.User
|
||||
if err := h.db.Where("id = ? AND shop_id = ? AND deleted_at IS NULL", c.Param("id"), shopID).
|
||||
First(&u).Error; err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "密码加密失败"})
|
||||
return
|
||||
}
|
||||
h.db.Model(&u).Update("password_hash", string(hash))
|
||||
c.JSON(http.StatusOK, gin.H{"message": "密码已重置"})
|
||||
}
|
||||
@@ -25,6 +25,7 @@ func Setup(r *gin.Engine, db *gorm.DB) {
|
||||
stockOutH := handler.NewStockOutHandler(db, stockSvc)
|
||||
inventoryH := handler.NewInventoryHandler(db)
|
||||
importH := handler.NewImportHandler(db)
|
||||
userH := handler.NewUserHandler(db)
|
||||
|
||||
v1 := r.Group("/api/v1")
|
||||
|
||||
@@ -105,6 +106,15 @@ func Setup(r *gin.Engine, db *gorm.DB) {
|
||||
inventory.GET("/checks/:id", inventoryH.GetCheck)
|
||||
}
|
||||
|
||||
// 用户管理
|
||||
users := api.Group("/users")
|
||||
{
|
||||
users.GET("", userH.List)
|
||||
users.POST("", userH.Create)
|
||||
users.PUT("/:id", userH.Update)
|
||||
users.PUT("/:id/reset-password", userH.ResetPassword)
|
||||
}
|
||||
|
||||
// 导入
|
||||
imp := api.Group("/import")
|
||||
{
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
class AppUser {
|
||||
final int id;
|
||||
final String username;
|
||||
final String? realName;
|
||||
final String? phone;
|
||||
final String role; // admin | operator | readonly
|
||||
final bool isActive;
|
||||
final String? createdAt;
|
||||
|
||||
const AppUser({
|
||||
required this.id,
|
||||
required this.username,
|
||||
this.realName,
|
||||
this.phone,
|
||||
required this.role,
|
||||
required this.isActive,
|
||||
this.createdAt,
|
||||
});
|
||||
|
||||
String get roleLabel {
|
||||
switch (role) {
|
||||
case 'admin':
|
||||
return '管理员';
|
||||
case 'operator':
|
||||
return '操作员';
|
||||
case 'readonly':
|
||||
return '只读';
|
||||
default:
|
||||
return role;
|
||||
}
|
||||
}
|
||||
|
||||
factory AppUser.fromJson(Map<String, dynamic> json) => AppUser(
|
||||
id: (json['id'] as num).toInt(),
|
||||
username: json['username'] as String,
|
||||
realName: json['real_name'] as String?,
|
||||
phone: json['phone'] as String?,
|
||||
role: json['role'] as String? ?? 'operator',
|
||||
isActive: json['is_active'] as bool? ?? true,
|
||||
createdAt: json['created_at'] as String?,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../core/api/api_client.dart';
|
||||
import '../core/auth/auth_state.dart';
|
||||
import '../models/user.dart';
|
||||
import '../repositories/user_repository.dart';
|
||||
|
||||
final userRepositoryProvider = Provider<UserRepository>((ref) {
|
||||
return UserRepository(ref.watch(apiClientProvider));
|
||||
});
|
||||
|
||||
final userListProvider =
|
||||
AsyncNotifierProvider<UserListNotifier, List<AppUser>>(
|
||||
UserListNotifier.new,
|
||||
);
|
||||
|
||||
class UserListNotifier extends AsyncNotifier<List<AppUser>> {
|
||||
@override
|
||||
Future<List<AppUser>> build() {
|
||||
ref.watch(authStateProvider.select((s) => s.user?.shopId));
|
||||
return ref.read(userRepositoryProvider).list();
|
||||
}
|
||||
|
||||
Future<void> reload() async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(
|
||||
() => ref.read(userRepositoryProvider).list());
|
||||
}
|
||||
|
||||
Future<void> createUser(Map<String, dynamic> data) async {
|
||||
await ref.read(userRepositoryProvider).create(data);
|
||||
await reload();
|
||||
}
|
||||
|
||||
Future<void> updateUser(int id, Map<String, dynamic> data) async {
|
||||
await ref.read(userRepositoryProvider).update(id, data);
|
||||
await reload();
|
||||
}
|
||||
|
||||
Future<void> resetPassword(int id, String newPassword) async {
|
||||
await ref.read(userRepositoryProvider).resetPassword(id, newPassword);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import '../core/api/api_client.dart';
|
||||
import '../models/user.dart';
|
||||
|
||||
class UserRepository {
|
||||
final ApiClient _client;
|
||||
UserRepository(this._client);
|
||||
|
||||
Future<List<AppUser>> list() async {
|
||||
final resp = await _client.get('/users');
|
||||
final data = resp.data['data'] as List;
|
||||
return data.map((e) => AppUser.fromJson(e as Map<String, dynamic>)).toList();
|
||||
}
|
||||
|
||||
Future<AppUser> create(Map<String, dynamic> data) async {
|
||||
final resp = await _client.post('/users', data: data);
|
||||
return AppUser.fromJson(resp.data['data'] as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
Future<AppUser> update(int id, Map<String, dynamic> data) async {
|
||||
final resp = await _client.put('/users/$id', data: data);
|
||||
return AppUser.fromJson(resp.data['data'] as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
Future<void> resetPassword(int id, String newPassword) async {
|
||||
await _client.put('/users/$id/reset-password',
|
||||
data: {'password': newPassword});
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../models/user.dart';
|
||||
import '../../models/warehouse.dart';
|
||||
import '../../providers/user_provider.dart';
|
||||
import '../../providers/warehouse_provider.dart';
|
||||
|
||||
class SettingsScreen extends ConsumerStatefulWidget {
|
||||
@@ -53,13 +55,7 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
}
|
||||
|
||||
Widget _buildUsersTab() {
|
||||
final users = [
|
||||
{'name': '张三', 'username': 'zhangsan', 'role': '管理员', 'status': true, 'lastLogin': '2026-04-04 09:15'},
|
||||
{'name': '李四', 'username': 'lisi', 'role': '操作员', 'status': true, 'lastLogin': '2026-04-04 08:30'},
|
||||
{'name': '王五', 'username': 'wangwu', 'role': '操作员', 'status': true, 'lastLogin': '2026-04-03 17:45'},
|
||||
{'name': '赵六', 'username': 'zhaoliu', 'role': '只读', 'status': false, 'lastLogin': '2026-03-28 10:20'},
|
||||
];
|
||||
|
||||
final asyncUsers = ref.watch(userListProvider);
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
@@ -78,66 +74,86 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: DataTable(
|
||||
headingRowColor: WidgetStateProperty.all(const Color(0xFFF0F4FF)),
|
||||
columns: const [
|
||||
DataColumn(label: Text('姓名')),
|
||||
DataColumn(label: Text('用户名')),
|
||||
DataColumn(label: Text('角色')),
|
||||
DataColumn(label: Text('状态')),
|
||||
DataColumn(label: Text('最后登录')),
|
||||
DataColumn(label: Text('操作')),
|
||||
],
|
||||
rows: users
|
||||
.map((u) => DataRow(cells: [
|
||||
DataCell(Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 14,
|
||||
backgroundColor: AppTheme.primary.withOpacity(0.15),
|
||||
child: Text(
|
||||
(u['name'] as String).substring(0, 1),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.primary,
|
||||
fontWeight: FontWeight.w600),
|
||||
child: asyncUsers.when(
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (e, _) => Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text('加载失败:$e',
|
||||
style: const TextStyle(color: AppTheme.danger)),
|
||||
const SizedBox(height: 12),
|
||||
ElevatedButton(
|
||||
onPressed: () => ref.read(userListProvider.notifier).reload(),
|
||||
child: const Text('重试'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
data: (users) => SingleChildScrollView(
|
||||
child: DataTable(
|
||||
headingRowColor: WidgetStateProperty.all(const Color(0xFFF0F4FF)),
|
||||
columns: const [
|
||||
DataColumn(label: Text('姓名')),
|
||||
DataColumn(label: Text('用户名')),
|
||||
DataColumn(label: Text('角色')),
|
||||
DataColumn(label: Text('状态')),
|
||||
DataColumn(label: Text('操作')),
|
||||
],
|
||||
rows: users
|
||||
.map((u) => DataRow(cells: [
|
||||
DataCell(Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircleAvatar(
|
||||
radius: 14,
|
||||
backgroundColor:
|
||||
AppTheme.primary.withOpacity(0.15),
|
||||
child: Text(
|
||||
(u.realName ?? u.username)
|
||||
.substring(0, 1),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.primary,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(u['name'] as String),
|
||||
],
|
||||
)),
|
||||
DataCell(Text(u['username'] as String,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'monospace', fontSize: 12))),
|
||||
DataCell(_RoleBadge(u['role'] as String)),
|
||||
DataCell(Switch(
|
||||
value: u['status'] as bool,
|
||||
onChanged: (_) {},
|
||||
materialTapTargetSize:
|
||||
MaterialTapTargetSize.shrinkWrap,
|
||||
)),
|
||||
DataCell(Text(u['lastLogin'] as String,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppTheme.textSecondary))),
|
||||
DataCell(Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
const SizedBox(width: 8),
|
||||
Text(u.realName ?? u.username),
|
||||
],
|
||||
)),
|
||||
DataCell(Text(u.username,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'monospace', fontSize: 12))),
|
||||
DataCell(_RoleBadge(u.roleLabel)),
|
||||
DataCell(Switch(
|
||||
value: u.isActive,
|
||||
onChanged: (v) => ref
|
||||
.read(userListProvider.notifier)
|
||||
.updateUser(u.id, {'is_active': v}),
|
||||
materialTapTargetSize:
|
||||
MaterialTapTargetSize.shrinkWrap,
|
||||
)),
|
||||
DataCell(Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
_showEditUserDialog(context, u),
|
||||
child: const Text('编辑',
|
||||
style: TextStyle(fontSize: 12))),
|
||||
TextButton(
|
||||
onPressed: () {},
|
||||
style: TextStyle(fontSize: 12)),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () =>
|
||||
_showResetPasswordDialog(context, u),
|
||||
child: const Text('重置密码',
|
||||
style: TextStyle(fontSize: 12))),
|
||||
],
|
||||
)),
|
||||
]))
|
||||
.toList(),
|
||||
style: TextStyle(fontSize: 12)),
|
||||
),
|
||||
],
|
||||
)),
|
||||
]))
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -417,50 +433,28 @@ class _SettingsScreenState extends ConsumerState<SettingsScreen> {
|
||||
void _showAddUserDialog(BuildContext context) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('新增用户'),
|
||||
content: SizedBox(
|
||||
width: 400,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const TextField(
|
||||
decoration: InputDecoration(labelText: '姓名', hintText: '请输入姓名'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const TextField(
|
||||
decoration: InputDecoration(labelText: '用户名', hintText: '请输入登录用户名'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const TextField(
|
||||
obscureText: true,
|
||||
decoration: InputDecoration(labelText: '初始密码', hintText: '请设置初始密码'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
DropdownButtonFormField<String>(
|
||||
value: '操作员',
|
||||
items: ['管理员', '操作员', '只读']
|
||||
.map((r) => DropdownMenuItem(value: r, child: Text(r)))
|
||||
.toList(),
|
||||
onChanged: (_) {},
|
||||
decoration: const InputDecoration(labelText: '角色'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('确定'),
|
||||
),
|
||||
],
|
||||
builder: (ctx) => _UserFormDialog(
|
||||
onSaved: () => ref.read(userListProvider.notifier).reload(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showEditUserDialog(BuildContext context, AppUser user) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => _UserFormDialog(
|
||||
user: user,
|
||||
onSaved: () => ref.read(userListProvider.notifier).reload(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showResetPasswordDialog(BuildContext context, AppUser user) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => _ResetPasswordDialog(user: user),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WarehouseFormDialog extends ConsumerStatefulWidget {
|
||||
@@ -590,6 +584,228 @@ class _WarehouseFormDialogState extends ConsumerState<_WarehouseFormDialog> {
|
||||
}
|
||||
}
|
||||
|
||||
// ── 新增/编辑用户弹窗 ─────────────────────────────────────
|
||||
class _UserFormDialog extends ConsumerStatefulWidget {
|
||||
final AppUser? user;
|
||||
final VoidCallback onSaved;
|
||||
const _UserFormDialog({this.user, required this.onSaved});
|
||||
|
||||
@override
|
||||
ConsumerState<_UserFormDialog> createState() => _UserFormDialogState();
|
||||
}
|
||||
|
||||
class _UserFormDialogState extends ConsumerState<_UserFormDialog> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
late final TextEditingController _usernameCtrl;
|
||||
late final TextEditingController _realNameCtrl;
|
||||
late final TextEditingController _phoneCtrl;
|
||||
late final TextEditingController _passwordCtrl;
|
||||
late String _role;
|
||||
bool _saving = false;
|
||||
|
||||
bool get _isEdit => widget.user != null;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_usernameCtrl = TextEditingController(text: widget.user?.username ?? '');
|
||||
_realNameCtrl = TextEditingController(text: widget.user?.realName ?? '');
|
||||
_phoneCtrl = TextEditingController(text: widget.user?.phone ?? '');
|
||||
_passwordCtrl = TextEditingController();
|
||||
_role = widget.user?.role ?? 'operator';
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_usernameCtrl.dispose();
|
||||
_realNameCtrl.dispose();
|
||||
_phoneCtrl.dispose();
|
||||
_passwordCtrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
setState(() => _saving = true);
|
||||
try {
|
||||
final notifier = ref.read(userListProvider.notifier);
|
||||
if (_isEdit) {
|
||||
await notifier.updateUser(widget.user!.id, {
|
||||
'real_name': _realNameCtrl.text.trim(),
|
||||
'phone': _phoneCtrl.text.trim(),
|
||||
'role': _role,
|
||||
});
|
||||
} else {
|
||||
await notifier.createUser({
|
||||
'username': _usernameCtrl.text.trim(),
|
||||
'password': _passwordCtrl.text,
|
||||
'real_name': _realNameCtrl.text.trim(),
|
||||
'phone': _phoneCtrl.text.trim(),
|
||||
'role': _role,
|
||||
});
|
||||
}
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
widget.onSaved();
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text(_isEdit ? '用户更新成功' : '用户创建成功'),
|
||||
backgroundColor: AppTheme.success,
|
||||
));
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text('保存失败:$e'),
|
||||
backgroundColor: AppTheme.danger,
|
||||
));
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _saving = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text(_isEdit ? '编辑用户' : '新增用户'),
|
||||
content: SizedBox(
|
||||
width: 400,
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (!_isEdit)
|
||||
TextFormField(
|
||||
controller: _usernameCtrl,
|
||||
decoration: const InputDecoration(labelText: '用户名'),
|
||||
validator: (v) => (v == null || v.isEmpty) ? '不能为空' : null,
|
||||
),
|
||||
if (!_isEdit) const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _realNameCtrl,
|
||||
decoration: const InputDecoration(labelText: '姓名'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
controller: _phoneCtrl,
|
||||
decoration: const InputDecoration(labelText: '手机号'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (!_isEdit) ...[
|
||||
TextFormField(
|
||||
controller: _passwordCtrl,
|
||||
obscureText: true,
|
||||
decoration: const InputDecoration(labelText: '初始密码'),
|
||||
validator: (v) => (v == null || v.isEmpty) ? '不能为空' : null,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
DropdownButtonFormField<String>(
|
||||
value: _role,
|
||||
items: const [
|
||||
DropdownMenuItem(value: 'admin', child: Text('管理员')),
|
||||
DropdownMenuItem(value: 'operator', child: Text('操作员')),
|
||||
DropdownMenuItem(value: 'readonly', child: Text('只读')),
|
||||
],
|
||||
onChanged: (v) => setState(() => _role = v ?? 'operator'),
|
||||
decoration: const InputDecoration(labelText: '角色'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: _saving ? null : _save,
|
||||
child: _saving
|
||||
? const SizedBox(
|
||||
width: 16, height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
|
||||
: const Text('保存'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 重置密码弹窗 ──────────────────────────────────────────
|
||||
class _ResetPasswordDialog extends ConsumerStatefulWidget {
|
||||
final AppUser user;
|
||||
const _ResetPasswordDialog({required this.user});
|
||||
|
||||
@override
|
||||
ConsumerState<_ResetPasswordDialog> createState() => _ResetPasswordDialogState();
|
||||
}
|
||||
|
||||
class _ResetPasswordDialogState extends ConsumerState<_ResetPasswordDialog> {
|
||||
final _ctrl = TextEditingController();
|
||||
bool _saving = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_ctrl.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _save() async {
|
||||
if (_ctrl.text.isEmpty) return;
|
||||
setState(() => _saving = true);
|
||||
try {
|
||||
await ref.read(userListProvider.notifier).resetPassword(widget.user.id, _ctrl.text);
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content: Text('密码已重置'),
|
||||
backgroundColor: AppTheme.success,
|
||||
));
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text('重置失败:$e'),
|
||||
backgroundColor: AppTheme.danger,
|
||||
));
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _saving = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('重置密码 — ${widget.user.username}'),
|
||||
content: SizedBox(
|
||||
width: 360,
|
||||
child: TextField(
|
||||
controller: _ctrl,
|
||||
obscureText: true,
|
||||
decoration: const InputDecoration(labelText: '新密码'),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: _saving ? null : _save,
|
||||
child: _saving
|
||||
? const SizedBox(
|
||||
width: 16, height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
|
||||
: const Text('确认重置'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RoleBadge extends StatelessWidget {
|
||||
final String role;
|
||||
const _RoleBadge(this.role);
|
||||
|
||||
Reference in New Issue
Block a user