feat: 出入库登记功能完成
|
@ -3,6 +3,11 @@ plugins {
|
|||
id "kotlin-android"
|
||||
id "dev.flutter.flutter-gradle-plugin"
|
||||
}
|
||||
def keystoreProperties = new Properties()
|
||||
def keystorePropertiesFile = rootProject.file('key.properties')
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
def localProperties = new Properties()
|
||||
def localPropertiesFile = rootProject.file('local.properties')
|
||||
|
@ -50,12 +55,19 @@ android {
|
|||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
keyAlias keystoreProperties['keyAlias']
|
||||
keyPassword keystoreProperties['keyPassword']
|
||||
storeFile file(keystoreProperties['storeFile'])
|
||||
storePassword keystoreProperties['storePassword']
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig signingConfigs.debug
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,4 +31,18 @@
|
|||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_PHONE_STATE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.INTERNET" />
|
||||
<uses-permission
|
||||
android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.CAMERA" />
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
</manifest>
|
After Width: | Height: | Size: 26 KiB |
|
@ -1,6 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<bitmap android:gravity="fill" android:src="@drawable/background"/>
|
||||
</item>
|
||||
</layer-list>
|
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 41 KiB |
After Width: | Height: | Size: 78 KiB |
After Width: | Height: | Size: 121 KiB |
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 121 KiB |
After Width: | Height: | Size: 41 KiB |
After Width: | Height: | Size: 78 KiB |
After Width: | Height: | Size: 121 KiB |
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 121 KiB |
|
@ -6,6 +6,8 @@
|
|||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
|
||||
<item name="android:windowSplashScreenIconBackgroundColor">#ffffff</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
<item name="android:windowFullscreen">false</item>
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
|
||||
<item name="android:windowSplashScreenIconBackgroundColor">#ffffff</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
|
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 57 KiB |
After Width: | Height: | Size: 40 KiB |
|
@ -1,8 +0,0 @@
|
|||
flutter_native_splash:
|
||||
background_image: 'assets/images/launch_image.jpg'
|
||||
android: true
|
||||
ios: true
|
||||
landscape:
|
||||
image: assets/images/launch_image_landscape.jpg
|
||||
android: true
|
||||
ios: true
|
Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 121 KiB |
|
@ -17,7 +17,7 @@
|
|||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleToFill" image="LaunchBackground" translatesAutoresizingMaskIntoConstraints="NO" id="tWc-Dq-wcI"/>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"></imageView>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleAspectFill" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4"></imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
|
|
|
@ -47,5 +47,7 @@
|
|||
<true/>
|
||||
<key>UIStatusBarHidden</key>
|
||||
<false/>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -19,7 +19,7 @@ class Api {
|
|||
// 获取个人信息
|
||||
static Future<Response> getUserInfo() {
|
||||
return DioService.dio.get(
|
||||
Urls.getUserInfo,
|
||||
Urls.userInfo,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -64,6 +64,12 @@ class Api {
|
|||
queryParameters: {'page': 1, 'pageSize': 10, ...(params)});
|
||||
}
|
||||
|
||||
// 分页获取用户
|
||||
static Future<Response<PaginationData>> getUsers(Map params) {
|
||||
return DioService.dio.get<PaginationData>(Urls.sysUser,
|
||||
queryParameters: {'page': 1, 'pageSize': 10, ...(params)});
|
||||
}
|
||||
|
||||
// 一次性获取所有的字典类型(不分页)
|
||||
static Future<Response> getDictTypeAll(Map params) {
|
||||
return DioService.dio.post(Urls.getDictType, data: params);
|
||||
|
@ -81,11 +87,6 @@ class Api {
|
|||
);
|
||||
}
|
||||
|
||||
static Future<Response> getAppConfig() {
|
||||
Map<String, dynamic> query = {'ver': 0};
|
||||
return DioService.dio.get(Urls.getAppConfig, queryParameters: query);
|
||||
}
|
||||
|
||||
static Future<Response> saveUserInfo(Map<String, dynamic> data) {
|
||||
return DioService.dio.post(Urls.saveUserInfo, data: data);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
/// Global config
|
||||
class GloablConfig {
|
||||
// static const BASE_URL = "http://10.0.2.2:7001/api/";
|
||||
static const BASE_URL = "http://192.168.60.220:7001/api/";
|
||||
static const OSS_URL = "http://192.168.60.220:7001";
|
||||
// static const BASE_URL = "http://144.123.43.138:3001/api/";
|
||||
// static const OSS_URL = "http://144.123.43.138:3001";
|
||||
static const BASE_URL = "http://192.168.60.220:8001/api/";
|
||||
static const OSS_URL = "http://192.168.60.220:8001";
|
||||
static const DOMAIN_NAME = "山矿通";
|
||||
static const DEBUG = true;
|
||||
static const PRIVACY_POLICY = 'http://h5.heeru.xyz/privacyPolicy.html';
|
||||
static const TERM_OF_USER = 'http://h5.heeru.xyz/termConditions.html';
|
||||
static const APPLE_STORE_PAGE = 'https://apps.apple.com/cn/app';
|
||||
static const DIO_TIMEOUT = 10;
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
class Urls {
|
||||
static String getAppConfig = 'config/getAppConfig';
|
||||
static String login = 'auth/login';
|
||||
static String isValidToken = 'security/isValidToken';
|
||||
static String logout = 'security/logout';
|
||||
static String deleteAccount = 'user/deleteAccount';
|
||||
static String saveUserInfo = 'user/saveUserInfo';
|
||||
static String getUserInfo = 'account/profile';
|
||||
static String sysUser = 'system/users';
|
||||
static String userInfo = 'account/profile';
|
||||
static String projects = 'project';
|
||||
static String products = 'product';
|
||||
static String inventoryInout = 'materials-in-out';
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
// import 'package:sentry_flutter/sentry_flutter.dart';
|
||||
import 'package:sk_base_mobile/global.dart';
|
||||
import 'package:sk_base_mobile/index.dart';
|
||||
import 'package:sk_base_mobile/util/logger_util.dart';
|
||||
|
@ -10,5 +14,23 @@ Future<void> main() async {
|
|||
} catch (e) {
|
||||
LoggerUtil().error('Init failed, please try again.${e}');
|
||||
}
|
||||
runApp(const IndexPage());
|
||||
bool isProduction = kReleaseMode;
|
||||
// await SentryFlutter.init(
|
||||
// (options) {
|
||||
// options.environment = isProduction ? 'production' : 'development';
|
||||
// // options.beforeSend = (event, {hint}) {
|
||||
// // if (!isProduction) {
|
||||
// // return null;
|
||||
// // }
|
||||
// // return event;
|
||||
// // };
|
||||
// options.dsn =
|
||||
// 'https://ce4f75d3cd9120a1bd9f1a807e573de1@o1078619.ingest.us.sentry.io/4506981258952704';
|
||||
// // Set tracesSampleRate to 1.0 to capture 100% of transactions for performance monitoring.
|
||||
// // We recommend adjusting this value in production.
|
||||
// options.tracesSampleRate = 1.0;
|
||||
// },
|
||||
// appRunner: () => runApp(const IndexPage()),
|
||||
// );
|
||||
return runApp(const IndexPage());
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
class DeptModel {
|
||||
DeptModel({
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.name,
|
||||
required this.orderNo,
|
||||
});
|
||||
|
||||
final int? id;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final String? name;
|
||||
final int? orderNo;
|
||||
|
||||
factory DeptModel.fromJson(Map<String, dynamic> json) {
|
||||
return DeptModel(
|
||||
id: json["id"],
|
||||
createdAt: DateTime.tryParse(json["createdAt"] ?? ""),
|
||||
updatedAt: DateTime.tryParse(json["updatedAt"] ?? ""),
|
||||
name: json["name"],
|
||||
orderNo: json["orderNo"],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"createdAt": createdAt?.toIso8601String(),
|
||||
"updatedAt": updatedAt?.toIso8601String(),
|
||||
"name": name,
|
||||
"orderNo": orderNo,
|
||||
};
|
||||
}
|
|
@ -1,40 +1,45 @@
|
|||
class RoleModel {
|
||||
int? id;
|
||||
String? createdAt;
|
||||
String? updatedAt;
|
||||
String? name;
|
||||
String? value;
|
||||
String? remark;
|
||||
int? status;
|
||||
RoleModel({
|
||||
required this.id,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.name,
|
||||
required this.value,
|
||||
required this.remark,
|
||||
required this.status,
|
||||
required this.roleDefault,
|
||||
});
|
||||
|
||||
RoleModel(
|
||||
{this.id,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.name,
|
||||
this.value,
|
||||
this.remark,
|
||||
this.status});
|
||||
final int? id;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final String? name;
|
||||
final String? value;
|
||||
final String? remark;
|
||||
final int? status;
|
||||
final dynamic roleDefault;
|
||||
|
||||
RoleModel.fromJson(Map<String, dynamic> json) {
|
||||
id = json['id'];
|
||||
createdAt = json['createdAt'];
|
||||
updatedAt = json['updatedAt'];
|
||||
name = json['name'];
|
||||
value = json['value'];
|
||||
remark = json['remark'];
|
||||
status = json['status'];
|
||||
factory RoleModel.fromJson(Map<String, dynamic> json) {
|
||||
return RoleModel(
|
||||
id: json["id"],
|
||||
createdAt: DateTime.tryParse(json["createdAt"] ?? ""),
|
||||
updatedAt: DateTime.tryParse(json["updatedAt"] ?? ""),
|
||||
name: json["name"],
|
||||
value: json["value"],
|
||||
remark: json["remark"],
|
||||
status: json["status"],
|
||||
roleDefault: json["default"],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['id'] = id;
|
||||
data['createdAt'] = createdAt;
|
||||
data['updatedAt'] = updatedAt;
|
||||
data['name'] = name;
|
||||
data['value'] = value;
|
||||
data['remark'] = remark;
|
||||
data['status'] = status;
|
||||
return data;
|
||||
}
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"createdAt": createdAt?.toIso8601String(),
|
||||
"updatedAt": updatedAt?.toIso8601String(),
|
||||
"name": name,
|
||||
"value": value,
|
||||
"remark": remark,
|
||||
"status": status,
|
||||
"default": roleDefault,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,24 +1,13 @@
|
|||
import 'package:sk_base_mobile/models/dept.model.dart';
|
||||
import 'package:sk_base_mobile/models/role.model.dart';
|
||||
|
||||
class UserInfoModel {
|
||||
int? id;
|
||||
String? createdAt;
|
||||
String? updatedAt;
|
||||
String? username;
|
||||
String? nickname;
|
||||
String? avatar;
|
||||
String? qq;
|
||||
String? email;
|
||||
String? phone;
|
||||
String? remark;
|
||||
int? status;
|
||||
List<RoleModel>? roles;
|
||||
|
||||
UserInfoModel(
|
||||
{this.id,
|
||||
UserInfoModel({
|
||||
this.id,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
this.username,
|
||||
this.psalt,
|
||||
this.nickname,
|
||||
this.avatar,
|
||||
this.qq,
|
||||
|
@ -26,44 +15,61 @@ class UserInfoModel {
|
|||
this.phone,
|
||||
this.remark,
|
||||
this.status,
|
||||
this.roles});
|
||||
|
||||
UserInfoModel.fromJson(Map<String, dynamic> json) {
|
||||
id = json['id'];
|
||||
createdAt = json['createdAt'];
|
||||
updatedAt = json['updatedAt'];
|
||||
username = json['username'];
|
||||
nickname = json['nickname'];
|
||||
avatar = json['avatar'];
|
||||
qq = json['qq'];
|
||||
email = json['email'];
|
||||
phone = json['phone'];
|
||||
remark = json['remark'];
|
||||
status = json['status'];
|
||||
if (json['roles'] != null) {
|
||||
roles = <RoleModel>[];
|
||||
json['roles'].forEach((v) {
|
||||
roles!.add(RoleModel.fromJson(v));
|
||||
this.dept,
|
||||
this.roles = const [],
|
||||
});
|
||||
}
|
||||
|
||||
final int? id;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
final String? username;
|
||||
final String? psalt;
|
||||
final String? nickname;
|
||||
final String? avatar;
|
||||
final String? qq;
|
||||
final String? email;
|
||||
final String? phone;
|
||||
final String? remark;
|
||||
final int? status;
|
||||
final DeptModel? dept;
|
||||
List<RoleModel> roles;
|
||||
|
||||
factory UserInfoModel.fromJson(Map<String, dynamic> json) {
|
||||
return UserInfoModel(
|
||||
id: json["id"],
|
||||
createdAt: DateTime.tryParse(json["createdAt"] ?? ""),
|
||||
updatedAt: DateTime.tryParse(json["updatedAt"] ?? ""),
|
||||
username: json["username"],
|
||||
psalt: json["psalt"],
|
||||
nickname: json["nickname"],
|
||||
avatar: json["avatar"],
|
||||
qq: json["qq"],
|
||||
email: json["email"],
|
||||
phone: json["phone"],
|
||||
remark: json["remark"],
|
||||
status: json["status"],
|
||||
dept: json["dept"] == null ? null : DeptModel.fromJson(json["dept"]),
|
||||
roles: json["roles"] == null
|
||||
? []
|
||||
: List<RoleModel>.from(
|
||||
json["roles"]!.map((x) => RoleModel.fromJson(x))),
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = <String, dynamic>{};
|
||||
data['id'] = id;
|
||||
data['createdAt'] = createdAt;
|
||||
data['updatedAt'] = updatedAt;
|
||||
data['username'] = username;
|
||||
data['nickname'] = nickname;
|
||||
data['avatar'] = avatar;
|
||||
data['qq'] = qq;
|
||||
data['email'] = email;
|
||||
data['phone'] = phone;
|
||||
data['remark'] = remark;
|
||||
data['status'] = status;
|
||||
if (roles != null) {
|
||||
data['roles'] = roles!.map((v) => v.toJson()).toList();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
Map<String, dynamic> toJson() => {
|
||||
"id": id,
|
||||
"createdAt": createdAt?.toIso8601String(),
|
||||
"updatedAt": updatedAt?.toIso8601String(),
|
||||
"username": username,
|
||||
"psalt": psalt,
|
||||
"nickname": nickname,
|
||||
"avatar": avatar,
|
||||
"qq": qq,
|
||||
"email": email,
|
||||
"phone": phone,
|
||||
"remark": remark,
|
||||
"status": status,
|
||||
"dept": dept?.toJson(),
|
||||
"roles": roles.map((x) => x?.toJson()).toList(),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ class CustomAppBar extends StatelessWidget {
|
|||
),
|
||||
Obx(
|
||||
() => Text(
|
||||
AuthStore.to.userInfo.value.username ?? '',
|
||||
AuthStore.to.userInfo.value.nickname ?? '',
|
||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(
|
||||
color: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
|
@ -59,17 +59,6 @@ class LoginScreen extends StatelessWidget {
|
|||
)
|
||||
],
|
||||
),
|
||||
// Row(
|
||||
// mainAxisAlignment: MainAxisAlignment.center,
|
||||
// children: [
|
||||
// Text(
|
||||
// '山矿通',
|
||||
// style: TextStyle(
|
||||
// letterSpacing: ScreenAdaper.sp(10),
|
||||
// fontSize: ScreenAdaper.sp(70)),
|
||||
// )
|
||||
// ],
|
||||
// ),
|
||||
SizedBox(height: ScreenAdaper.height(50)),
|
||||
buildForm(),
|
||||
SizedBox(height: ScreenAdaper.height(50)),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import 'package:flutter/widgets.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../store/auth.store.dart';
|
||||
// import 'package:sentry/sentry.dart';
|
||||
|
||||
class LoginController extends GetxController {
|
||||
final isAgreeTerm = RxBool(false);
|
||||
|
@ -13,6 +14,7 @@ class LoginController extends GetxController {
|
|||
if (!formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 拿出form中的数据
|
||||
AuthStore.to.login(username: username, password: password);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,201 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
import 'package:sk_base_mobile/apis/index.dart';
|
||||
import 'package:sk_base_mobile/app_theme.dart';
|
||||
import 'package:sk_base_mobile/constants/bg_color.dart';
|
||||
import 'package:sk_base_mobile/models/index.dart';
|
||||
import 'package:sk_base_mobile/models/inventory.model.dart';
|
||||
import 'package:sk_base_mobile/models/user_info.model.dart';
|
||||
import 'package:sk_base_mobile/util/debouncer.dart';
|
||||
import 'package:sk_base_mobile/util/screen_adaper_util.dart';
|
||||
import 'package:sk_base_mobile/widgets/empty.dart';
|
||||
import 'package:sk_base_mobile/widgets/fade_in_cache_image.dart';
|
||||
|
||||
class AgentSearch extends StatelessWidget {
|
||||
Function(UserInfoModel)? onSelected;
|
||||
Function(UserInfoModel)? beforeSelectedCheck;
|
||||
AgentSearch({super.key, this.onSelected, this.beforeSelectedCheck});
|
||||
final controller = Get.put(AgentSearchController());
|
||||
final listTitleTextStyle =
|
||||
TextStyle(fontSize: ScreenAdaper.sp(20), fontWeight: FontWeight.w600);
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: ScreenAdaper.width(20),
|
||||
vertical: ScreenAdaper.height(20)),
|
||||
child: Column(
|
||||
children: [
|
||||
buildSearchBar(),
|
||||
SizedBox(
|
||||
height: ScreenAdaper.height(defaultPadding) / 2,
|
||||
),
|
||||
Expanded(child: buildList())
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildSearchBar() {
|
||||
final doSearch = debouncer((String value) {
|
||||
controller.searchKey.value = value;
|
||||
controller.onRefresh();
|
||||
}, delayTime: 500);
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 5,
|
||||
child: TextField(
|
||||
controller: controller.searchBarTextConroller,
|
||||
onChanged: (value) => doSearch(value),
|
||||
decoration: InputDecoration(
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
vertical: ScreenAdaper.height(10),
|
||||
horizontal: ScreenAdaper.width(10)),
|
||||
hintText: '姓名',
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
// 当searchBarController有值时不显示
|
||||
suffixIcon: Obx(() => controller.searchKey.value.isEmpty
|
||||
? const SizedBox()
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
controller.searchKey.value = '';
|
||||
controller.searchBarTextConroller.clear();
|
||||
doSearch('');
|
||||
},
|
||||
)),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
))),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildList() {
|
||||
final textStyle = TextStyle(fontSize: ScreenAdaper.sp(22));
|
||||
return Obx(() => SmartRefresher(
|
||||
enablePullDown: true,
|
||||
enablePullUp: true,
|
||||
controller: controller.refreshController,
|
||||
onLoading: controller.onLoading,
|
||||
onRefresh: controller.onRefresh,
|
||||
child: controller.refreshController.isLoading
|
||||
? SizedBox()
|
||||
: controller.list.isEmpty
|
||||
? Center(
|
||||
child: Empty(text: '暂无数据'),
|
||||
)
|
||||
: ListView.separated(
|
||||
separatorBuilder: (context, index) => Divider(
|
||||
color: AppTheme.dividerColor,
|
||||
),
|
||||
itemCount: controller.list.length,
|
||||
itemBuilder: (_, index) => buildItem(index))));
|
||||
}
|
||||
|
||||
Widget buildItem(int index) {
|
||||
return InkWell(
|
||||
onTap: () => {
|
||||
if (beforeSelectedCheck != null)
|
||||
{
|
||||
if (beforeSelectedCheck!(controller.list[index]))
|
||||
{onSelected!(controller.list[index])}
|
||||
}
|
||||
else
|
||||
{onSelected!(controller.list[index])}
|
||||
},
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: ScreenAdaper.width(5),
|
||||
vertical: ScreenAdaper.height(10)),
|
||||
child: Row(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(ScreenAdaper.sp(15)),
|
||||
child: FadeInCacheImage(
|
||||
url: controller.list[index].avatar ?? '',
|
||||
width: ScreenAdaper.width(60),
|
||||
height: ScreenAdaper.width(60),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: ScreenAdaper.width(20),
|
||||
),
|
||||
Text(
|
||||
'${controller.list[index].nickname}',
|
||||
style: TextStyle(fontSize: ScreenAdaper.sp(25)),
|
||||
),
|
||||
if (controller.list[index].dept != null) ...[
|
||||
SizedBox(
|
||||
width: ScreenAdaper.width(20),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: ScreenAdaper.width(10),
|
||||
vertical: ScreenAdaper.height(5)),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[300],
|
||||
borderRadius: BorderRadius.circular(ScreenAdaper.sp(10))),
|
||||
child: Text('${controller.list[index].dept?.name}'),
|
||||
)
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AgentSearchController extends GetxController {
|
||||
RxList<UserInfoModel> list = RxList([]);
|
||||
RxString searchKey = ''.obs;
|
||||
final searchBarTextConroller = TextEditingController();
|
||||
RefreshController refreshController = RefreshController(initialRefresh: true);
|
||||
int page = 1;
|
||||
int limit = 15;
|
||||
int total = 0;
|
||||
Future<List<UserInfoModel>> getData({bool isRefresh = false}) async {
|
||||
if (isRefresh == true) {
|
||||
page = 1;
|
||||
} else {
|
||||
page++;
|
||||
}
|
||||
final res = await Api.getUsers({
|
||||
'page': page,
|
||||
'pageSize': 15,
|
||||
'keyword': searchKey.value,
|
||||
});
|
||||
List<UserInfoModel> newList =
|
||||
res.data!.items.map((e) => UserInfoModel.fromJson(e)).toList();
|
||||
isRefresh == true ? list.assignAll(newList) : list.addAll(newList);
|
||||
|
||||
return newList;
|
||||
}
|
||||
|
||||
Future<void> onRefresh() async {
|
||||
await getData(isRefresh: true).then((_) {
|
||||
refreshController.refreshCompleted(resetFooterState: true);
|
||||
}).catchError((_) {
|
||||
refreshController.refreshFailed();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> onLoading() async {
|
||||
await getData().then((_) {
|
||||
if (_.isEmpty) {
|
||||
refreshController.loadNoData();
|
||||
} else {
|
||||
refreshController.loadComplete();
|
||||
}
|
||||
}).catchError((_) {
|
||||
refreshController.loadFailed();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -7,6 +7,8 @@ import 'package:sk_base_mobile/constants/constants.dart';
|
|||
import 'package:sk_base_mobile/constants/dict_enum.dart';
|
||||
import 'package:sk_base_mobile/models/index.dart';
|
||||
import 'package:sk_base_mobile/models/inventory.model.dart';
|
||||
import 'package:sk_base_mobile/models/user_info.model.dart';
|
||||
import 'package:sk_base_mobile/screens/new_inventory_inout/components/agent_search.dart';
|
||||
import 'package:sk_base_mobile/screens/new_inventory_inout/components/inventory_search.dart';
|
||||
import 'package:sk_base_mobile/screens/new_inventory_inout/components/product_search.dart';
|
||||
import 'package:sk_base_mobile/store/dict.store.dart';
|
||||
|
@ -287,11 +289,49 @@ class NewInventoryInout extends StatelessWidget {
|
|||
|
||||
/// 经办人
|
||||
Widget buildAgent() {
|
||||
return ZtTextInput(
|
||||
textController: controller.agentTextController,
|
||||
labelText: '经办人',
|
||||
isRequired: true,
|
||||
);
|
||||
return TextFormField(
|
||||
controller: controller.agentTextController,
|
||||
decoration: InputDecoration(
|
||||
hintText: '请选择经办人',
|
||||
label: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
"*",
|
||||
style: TextStyle(
|
||||
color: Colors.red, fontSize: ScreenAdaper.sp(30)),
|
||||
),
|
||||
Text(
|
||||
'经办人',
|
||||
style: TextStyle(fontSize: ScreenAdaper.sp(30)),
|
||||
),
|
||||
]),
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always),
|
||||
readOnly: true,
|
||||
onTap: () {
|
||||
if (inOrOut == InventoryInOrOutEnum.In) {
|
||||
ModalUtil.showGeneralDialog(content: AgentSearch(
|
||||
onSelected: (UserInfoModel userInfo) {
|
||||
Get.back();
|
||||
controller.payload['agent'] = userInfo.nickname;
|
||||
controller.agentTextController.text = userInfo.nickname!;
|
||||
},
|
||||
));
|
||||
} else {
|
||||
ModalUtil.showGeneralDialog(
|
||||
content: AgentSearch(
|
||||
onSelected: (UserInfoModel userInfo) {
|
||||
Get.back();
|
||||
controller.payload['agent'] = userInfo.nickname;
|
||||
controller.agentTextController.text =
|
||||
userInfo.nickname!;
|
||||
},
|
||||
),
|
||||
width: Get.width - ScreenAdaper.width(50))
|
||||
.then((value) => Get.delete<AgentSearchController>());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 备注
|
||||
|
|
|
@ -95,7 +95,7 @@ class WorkBenchPage extends StatelessWidget {
|
|||
Text(
|
||||
works[index].title,
|
||||
style: TextStyle(
|
||||
letterSpacing: ScreenAdaper.width(5),
|
||||
letterSpacing: ScreenAdaper.width(10),
|
||||
fontSize: ScreenAdaper.sp(40),
|
||||
fontWeight: FontWeight.bold,
|
||||
foreground: Paint()
|
||||
|
@ -106,7 +106,7 @@ class WorkBenchPage extends StatelessWidget {
|
|||
Text(
|
||||
works[index].title,
|
||||
style: TextStyle(
|
||||
letterSpacing: ScreenAdaper.width(5),
|
||||
letterSpacing: ScreenAdaper.width(10),
|
||||
color: Colors.white,
|
||||
fontSize: ScreenAdaper.sp(40),
|
||||
fontWeight: FontWeight.bold),
|
||||
|
|
|
@ -82,20 +82,20 @@ class AppInfoService extends GetxService {
|
|||
}
|
||||
|
||||
Future<void> getAppConfig() async {
|
||||
if (StorageService.to.getString(CacheKeys.token, isWithUser: false) !=
|
||||
null) {
|
||||
final response = await Api.getAppConfig();
|
||||
if (response.data != null) {
|
||||
appConfig = AppConfig.fromJson(response.data['data']);
|
||||
await StorageService.to.setString(
|
||||
CacheKeys.appConfig, jsonEncode(appConfig.items!),
|
||||
isWithUser: false);
|
||||
if (appConfig.ver != null) {
|
||||
await StorageService.to
|
||||
.setString(CacheKeys.ver, appConfig.ver!, isWithUser: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
// if (StorageService.to.getString(CacheKeys.token, isWithUser: false) !=
|
||||
// null) {
|
||||
// final response = await Api.getAppConfig();
|
||||
// if (response.data != null) {
|
||||
// appConfig = AppConfig.fromJson(response.data['data']);
|
||||
// await StorageService.to.setString(
|
||||
// CacheKeys.appConfig, jsonEncode(appConfig.items!),
|
||||
// isWithUser: false);
|
||||
// if (appConfig.ver != null) {
|
||||
// await StorageService.to
|
||||
// .setString(CacheKeys.ver, appConfig.ver!, isWithUser: false);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
// need token
|
||||
|
|
|
@ -16,7 +16,7 @@ class DioService extends Get.GetxService {
|
|||
static late Dio _dio;
|
||||
List<String> whiteList = [Urls.login];
|
||||
BaseOptions dioBaseOptions = BaseOptions(
|
||||
connectTimeout: const Duration(seconds: 20),
|
||||
connectTimeout: const Duration(seconds: GloablConfig.DIO_TIMEOUT),
|
||||
baseUrl: '${GloablConfig.BASE_URL}',
|
||||
followRedirects: true);
|
||||
|
||||
|
@ -62,10 +62,10 @@ class DioService extends Get.GetxService {
|
|||
e.response?.data['message'],
|
||||
);
|
||||
} else if (e.response?.statusCode == 401) {
|
||||
await SnackBarUtil().error('请重新登录');
|
||||
// dio.lock();
|
||||
LoggerUtil().error('[Service-dio] logout:${e.response}');
|
||||
await AuthStore().logout(force: true);
|
||||
SnackBarUtil().error('请重新登录');
|
||||
} else if (e.response?.statusCode == 403) {
|
||||
await SnackBarUtil().error(
|
||||
'${e.response?.data['message']}.',
|
||||
|
|
|
@ -25,7 +25,7 @@ class AuthStore extends GetxService {
|
|||
|
||||
if (token != null) {
|
||||
if (preUserInfo != null) {
|
||||
userInfo(UserInfoModel.fromJson(jsonDecode(preUserInfo)));
|
||||
await getUserInfo();
|
||||
LoggerUtil().info('[Store-Auth] userId: ${userInfo.value.id}');
|
||||
}
|
||||
}
|
||||
|
@ -56,19 +56,11 @@ class AuthStore extends GetxService {
|
|||
Future<void> logout({bool force = false}) async {
|
||||
await StorageService.to.remove(CacheKeys.token, isWithUser: false);
|
||||
await StorageService.to.remove(CacheKeys.userInfo, isWithUser: false);
|
||||
try {
|
||||
// final response = await Api.logout();
|
||||
// if (response.data != null) {
|
||||
LoggerUtil().info('[Store-Auth] Logout succeed.');
|
||||
// 如果当前已经在login界面 不需要跳转
|
||||
if (Get.currentRoute != RouteConfig.login) {
|
||||
Get.offAllNamed(RouteConfig.login);
|
||||
}
|
||||
} catch (e) {
|
||||
print(e);
|
||||
} finally {
|
||||
LoadingUtil.to.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> login(
|
||||
|
@ -94,11 +86,11 @@ class AuthStore extends GetxService {
|
|||
.setString(CacheKeys.token, auth.token!, isWithUser: false);
|
||||
}
|
||||
await getUserInfo();
|
||||
await getCommonInfo();
|
||||
Get.offNamed(RouteConfig.home);
|
||||
getCommonInfo();
|
||||
}
|
||||
} catch (e) {
|
||||
await SnackBarUtil().error('账号密码错误');
|
||||
await SnackBarUtil().error('账号密码错误$e');
|
||||
LoggerUtil().error(e);
|
||||
} finally {
|
||||
LoadingUtil.to.dismiss();
|
||||
|
@ -112,7 +104,6 @@ class AuthStore extends GetxService {
|
|||
}
|
||||
|
||||
Future<void> getUserInfo() async {
|
||||
await LoadingUtil.to.show();
|
||||
try {
|
||||
final response = await Api.getUserInfo();
|
||||
if (response.data != null) {
|
||||
|
@ -121,9 +112,7 @@ class AuthStore extends GetxService {
|
|||
}
|
||||
} catch (e) {
|
||||
SnackBarUtil().error('$e');
|
||||
} finally {
|
||||
LoadingUtil.to.dismiss();
|
||||
}
|
||||
} finally {}
|
||||
}
|
||||
|
||||
Future<void> saveUserInfo(Map<String, dynamic> data) async {
|
||||
|
|
|
@ -42,12 +42,7 @@ class _FadeInCacheImageState extends State<FadeInCacheImage> {
|
|||
theContext = context;
|
||||
|
||||
if ((widget.url == null || widget.url == '' || widget.url == 'null')) {
|
||||
return SizedBox(
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
child: Icon(Icons.image_not_supported,
|
||||
size: ScreenAdaper.sp(100), color: AppTheme.grey),
|
||||
);
|
||||
return defaultImg();
|
||||
}
|
||||
|
||||
return buildImg(widget.url);
|
||||
|
@ -68,12 +63,20 @@ class _FadeInCacheImageState extends State<FadeInCacheImage> {
|
|||
decoration: const BoxDecoration(color: AppTheme.grey),
|
||||
child: const CupertinoActivityIndicator(),
|
||||
),
|
||||
errorWidget: (context, error, stackTrace) => SizedBox(
|
||||
errorWidget: (context, error, stackTrace) => defaultImg(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget defaultImg() {
|
||||
return Container(
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(), borderRadius: BorderRadius.circular(15)),
|
||||
width: widget.width,
|
||||
height: widget.height,
|
||||
child: Icon(Icons.image_not_supported,
|
||||
size: ScreenAdaper.sp(100), color: AppTheme.grey),
|
||||
),
|
||||
size: ScreenAdaper.sp((widget.width ?? 200) * 3 / 4),
|
||||
color: AppTheme.grey),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@ import 'package:sk_base_mobile/util/loading_util.dart';
|
|||
import 'package:sk_base_mobile/util/media_util.dart';
|
||||
import 'package:sk_base_mobile/util/screen_adaper_util.dart';
|
||||
import 'package:sk_base_mobile/util/snack_bar.util.dart';
|
||||
import 'package:sk_base_mobile/widgets/fade_in_cache_image.dart';
|
||||
import 'package:sk_base_mobile/widgets/loading_indicator.dart';
|
||||
|
||||
class ImagePreivew extends StatefulWidget {
|
||||
|
|
16
pubspec.lock
|
@ -181,10 +181,10 @@ packages:
|
|||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
sha256: "49af28382aefc53562459104f64d16b9dfd1e8ef68c862d5af436cc8356ce5a8"
|
||||
sha256: "50fec96118958b97c727d0d8f67255d3683f16cc1f90d9bc917b5d4fe3abeca9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.4.1"
|
||||
version: "5.4.2"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -330,10 +330,10 @@ packages:
|
|||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
|
||||
sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
version: "3.0.2"
|
||||
flutter_native_splash:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
|
@ -516,10 +516,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
|
||||
sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
version: "3.0.0"
|
||||
loading_animation_widget:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
@ -652,10 +652,10 @@ packages:
|
|||
dependency: transitive
|
||||
description:
|
||||
name: permission_handler_apple
|
||||
sha256: bdafc6db74253abb63907f4e357302e6bb786ab41465e8635f362ee71fd8707b
|
||||
sha256: e9ad66020b89ff1b63908f247c2c6f931c6e62699b756ef8b3c4569350cd8662
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "9.4.0"
|
||||
version: "9.4.4"
|
||||
permission_handler_html:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
16
pubspec.yaml
|
@ -62,6 +62,7 @@ dependencies:
|
|||
image_gallery_saver: ^2.0.3
|
||||
|
||||
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
@ -71,11 +72,24 @@ dev_dependencies:
|
|||
# activated in the `analysis_options.yaml` file located at the root of your
|
||||
# package. See that file for information about deactivating specific lint
|
||||
# rules and activating additional ones.
|
||||
flutter_lints: ^2.0.0
|
||||
flutter_lints: ^3.0.2
|
||||
flutter_launcher_icons: ^0.13.1
|
||||
flutter_native_splash: ^2.3.11
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
|
||||
flutter_native_splash:
|
||||
background_image: 'assets/images/launch_image.jpg'
|
||||
android: true
|
||||
ios: true
|
||||
android_gravity: scaleAspectFill
|
||||
ios_content_mode: scaleAspectFill
|
||||
android_12:
|
||||
image: assets/images/launch_image.jpg
|
||||
icon_background_color: "#ffffff"
|
||||
android_gravity: scaleAspectFill
|
||||
ios_content_mode: scaleAspectFill
|
||||
|
||||
flutter_launcher_icons:
|
||||
android: 'launcher_icon'
|
||||
ios: true
|
||||
|
|
100
web/index.html
|
@ -1,6 +1,4 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<!DOCTYPE html><html><head>
|
||||
<!--
|
||||
If you are serving your web app in a path other than the root, change the
|
||||
href value below to reflect the base path you are serving from.
|
||||
|
@ -27,7 +25,7 @@
|
|||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||
|
||||
<!-- Favicon -->
|
||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||
<link rel="icon" type="image/png" href="favicon.png">
|
||||
|
||||
<title>sk_base_mobile</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
|
@ -37,7 +35,94 @@
|
|||
const serviceWorkerVersion = null;
|
||||
</script>
|
||||
<!-- This script adds the flutter initialization JS code -->
|
||||
<script src="flutter.js" defer></script>
|
||||
<script src="flutter.js" defer=""></script>
|
||||
|
||||
|
||||
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<style id="splash-screen-style">
|
||||
html {
|
||||
height: 100%
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100%;
|
||||
background-color: #ffffff;
|
||||
background-image: url("splash/img/light-background.png");
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.center {
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
-ms-transform: translate(-50%, -50%);
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.contain {
|
||||
display:block;
|
||||
width:100%; height:100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.stretch {
|
||||
display:block;
|
||||
width:100%; height:100%;
|
||||
}
|
||||
|
||||
.cover {
|
||||
display:block;
|
||||
width:100%; height:100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
-ms-transform: translate(-50%, 0);
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
.bottomLeft {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.bottomRight {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
||||
<script id="splash-screen-script">
|
||||
function removeSplashFromWeb() {
|
||||
document.getElementById("splash")?.remove();
|
||||
document.getElementById("splash-branding")?.remove();
|
||||
document.body.style.background = "transparent";
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
|
@ -55,5 +140,6 @@
|
|||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
</body></html>
|
After Width: | Height: | Size: 121 KiB |