95 changed files with 3993 additions and 708 deletions
+12 -4
View File
@@ -1,4 +1,5 @@
using IM_API.Dtos;
using AutoMapper;
using IM_API.Dtos;
using IM_API.Interface.Services;
using IM_API.Tools;
using Microsoft.AspNetCore.Http;
@@ -17,7 +18,12 @@ namespace IM_API.Controllers
private readonly IJWTService _jwtService;
private readonly IRefreshTokenService _refreshTokenService;
private readonly IConfiguration _configuration;
public AuthController(ILogger<AuthController> logger, IAuthService authService, IJWTService jwtService, IRefreshTokenService refreshTokenService, IConfiguration configuration,IUserService userService)
private IMapper _mapper;
public AuthController(ILogger<AuthController> logger, IAuthService authService,
IJWTService jwtService, IRefreshTokenService refreshTokenService,
IConfiguration configuration,IUserService userService,
IMapper mapper
)
{
_logger = logger;
_authService = authService;
@@ -25,16 +31,18 @@ namespace IM_API.Controllers
_refreshTokenService = refreshTokenService;
_configuration = configuration;
_userService = userService;
_mapper = mapper;
}
[HttpPost]
public async Task<IActionResult> Login(LoginRequestDto dto)
{
var user = await _authService.LoginAsync(dto);
var userInfo = _mapper.Map<UserInfoDto>(user);
//生成凭证
(string token,DateTime expiresAt) = _jwtService.CreateAccessTokenForUser(user.Id,user.Username,"user");
//生成刷新凭证
string refreshToken = await _refreshTokenService.CreateRefreshTokenAsync(user.Id);
var res = new BaseResponse<LoginDto>(new LoginDto(user.Id,token,refreshToken,expiresAt));
var res = new BaseResponse<LoginDto>(new LoginDto(userInfo,token,refreshToken, expiresAt));
return Ok(res);
}
[HttpPost]
@@ -55,7 +63,7 @@ namespace IM_API.Controllers
}
var userinfo = await _userService.GetUserInfoAsync(userId);
(string token,DateTime expiresAt) = _jwtService.CreateAccessTokenForUser(userinfo.Id,userinfo.Username,"user");
var res = new BaseResponse<LoginDto>(new LoginDto(userinfo.Id,token,dto.refreshToken,expiresAt));
var res = new BaseResponse<LoginDto>(new LoginDto(userinfo,token, dto.refreshToken, expiresAt));
return Ok(res);
}
}
+3 -3
View File
@@ -2,12 +2,12 @@
{
public class LoginDto
{
public int Id { get; set; }
public UserInfoDto userInfo { get; set; }
public string Token { get; set; }
public string RefreshToken { get; set; }
public DateTime ExpireAt { get; set; }
public LoginDto(int id,string token,string refreshToken,DateTime expireAt) {
this.Id = id;
public LoginDto(UserInfoDto userInfo,string token, string refreshToken, DateTime expireAt) {
this.userInfo = userInfo;
this.Token = token;
this.RefreshToken = refreshToken;
this.ExpireAt = expireAt;
+4
View File
@@ -17,6 +17,10 @@ namespace IM_API.Dtos
/// 用户昵称
/// </summary>
public string NickName { get; set; }
/// <summary>
/// 用户头像
/// </summary>
public string? Avatar { get; set; }
/// <summary>
/// 用户在线状态
+45
View File
@@ -0,0 +1,45 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
/coverage/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
+36
View File
@@ -0,0 +1,36 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "f6ff1529fd6d8af5f706051d9251ac9231c83407"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
- platform: android
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
- platform: ios
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
- platform: web
create_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
base_revision: f6ff1529fd6d8af5f706051d9251ac9231c83407
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
+16
View File
@@ -0,0 +1,16 @@
# app
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
+28
View File
@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
+14
View File
@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks
+44
View File
@@ -0,0 +1,44 @@
plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
android {
namespace = "com.example.app"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.app"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
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.getByName("debug")
}
}
}
flutter {
source = "../.."
}
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
@@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="app"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
@@ -0,0 +1,5 @@
package com.example.app
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>
Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</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
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</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
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
+24
View File
@@ -0,0 +1,24 @@
allprojects {
repositories {
google()
mavenCentral()
}
}
val newBuildDir: Directory =
rootProject.layout.buildDirectory
.dir("../../build")
.get()
rootProject.layout.buildDirectory.value(newBuildDir)
subprojects {
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}
+2
View File
@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
+26
View File
@@ -0,0 +1,26 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")
+34
View File
@@ -0,0 +1,34 @@
**/dgph
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/ephemeral/
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>
+1
View File
@@ -0,0 +1 @@
#include "Generated.xcconfig"
@@ -0,0 +1 @@
#include "Generated.xcconfig"
@@ -0,0 +1,616 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
buildRules = (
);
dependencies = (
331C8086294A63A400263BE5 /* PBXTargetDependency */,
);
name = RunnerTests;
productName = RunnerTests;
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
331C8080294A63A400263BE5 = {
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
331C807F294A63A400263BE5 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
331C807D294A63A400263BE5 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.app.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Debug;
};
331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.app.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Release;
};
331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.example.app.RunnerTests;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_VERSION = 5.0;
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = com.example.app;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
331C8088294A63A400263BE5 /* Debug */,
331C8089294A63A400263BE5 /* Release */,
331C808A294A63A400263BE5 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>
+13
View File
@@ -0,0 +1,13 @@
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
@@ -0,0 +1,122 @@
{
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

@@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" 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>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>
+49
View File
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>App</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>app</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>
@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"
@@ -0,0 +1,12 @@
import Flutter
import UIKit
import XCTest
class RunnerTests: XCTestCase {
func testExample() {
// If you add code to the Runner application, consider adding tests here.
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
}
}
+178
View File
@@ -0,0 +1,178 @@
import 'package:flutter/material.dart';
void main() => runApp(
const MaterialApp(home: IMLoginPage(), debugShowCheckedModeBanner: false),
);
class IMLoginPage extends StatefulWidget {
const IMLoginPage({super.key});
@override
State<IMLoginPage> createState() => _IMLoginPageState();
}
class _IMLoginPageState extends State<IMLoginPage> {
@override
Widget build(BuildContext context) {
// 主题色定义:这里用类似微信的深绿或经典的社交蓝
const Color primaryColor = Color(0xFF07C160); // 微信绿,你也可以换成 0xFF0084FF (社交蓝)
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
actions: [
TextButton(
onPressed: () {},
child: const Text("找回密码", style: TextStyle(color: Colors.grey)),
),
],
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 20),
// 1. 品牌Logo/头像区域
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: primaryColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: const Icon(
Icons.person_rounded,
size: 60,
color: primaryColor,
),
),
const SizedBox(height: 24),
const Text(
"登录您的聊天账号",
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(height: 60),
// 2. 账号输入框 (极简下划线风格)
TextField(
decoration: InputDecoration(
labelText: "账号 / 邮箱 / 手机号",
labelStyle: const TextStyle(color: Colors.grey, fontSize: 14),
floatingLabelStyle: const TextStyle(color: primaryColor),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: const UnderlineInputBorder(
borderSide: BorderSide(color: primaryColor, width: 2),
),
),
),
const SizedBox(height: 25),
// 3. 密码输入框
TextField(
obscureText: true,
decoration: InputDecoration(
labelText: "请输入密码",
labelStyle: const TextStyle(color: Colors.grey, fontSize: 14),
floatingLabelStyle: const TextStyle(color: primaryColor),
enabledBorder: UnderlineInputBorder(
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: const UnderlineInputBorder(
borderSide: BorderSide(color: primaryColor, width: 2),
),
),
),
const SizedBox(height: 60),
// 4. 登录按钮 (圆润大按钮)
SizedBox(
width: double.infinity,
height: 50,
child: ElevatedButton(
onPressed: () {},
style: ElevatedButton.styleFrom(
backgroundColor: primaryColor,
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: const Text(
"登 录",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500),
),
),
),
const SizedBox(height: 20),
// 5. 注册/切换登录方式
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton(
onPressed: () {},
child: const Text(
"注册账号",
style: TextStyle(color: Color(0xFF576B95)),
), // 经典的链接蓝
),
const SizedBox(
height: 20,
child: VerticalDivider(color: Colors.grey),
),
TextButton(
onPressed: () {},
child: const Text(
"验证码登录",
style: TextStyle(color: Color(0xFF576B95)),
),
),
],
),
const SizedBox(height: 80),
// 6. 底部协议 (社交App必有)
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Checkbox(
value: true,
activeColor: primaryColor,
onChanged: (v) {},
),
const Text(
"我已阅读并同意",
style: TextStyle(color: Colors.grey, fontSize: 12),
),
const Text(
"《用户协议》",
style: TextStyle(color: Color(0xFF576B95), fontSize: 12),
),
const Text(
"",
style: TextStyle(color: Colors.grey, fontSize: 12),
),
const Text(
"《隐私政策》",
style: TextStyle(color: Color(0xFF576B95), fontSize: 12),
),
],
),
],
),
),
),
);
}
}
+213
View File
@@ -0,0 +1,213 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
async:
dependency: transitive
description:
name: async
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.13.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.4.0"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.19.1"
cupertino_icons:
dependency: "direct main"
description:
name: cupertino_icons
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.0.8"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.3.3"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "6.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "6.0.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "0.12.17"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "0.11.1"
meta:
dependency: transitive
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.17.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.9.1"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.10.1"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "0.7.7"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
url: "https://mirrors.tuna.tsinghua.edu.cn/dart-pub/"
source: hosted
version: "15.0.2"
sdks:
dart: ">=3.10.4 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"
+89
View File
@@ -0,0 +1,89 @@
name: app
description: "A new Flutter project."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
sdk: ^3.10.4
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# 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: ^6.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package
+30
View File
@@ -0,0 +1,30 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:app/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const IMLoginPage());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

+38
View File
@@ -0,0 +1,38 @@
<!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.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<!-- iOS meta tags & icons -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="app">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>app</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<script src="flutter_bootstrap.js" async></script>
</body>
</html>
+35
View File
@@ -0,0 +1,35 @@
{
"name": "app",
"short_name": "app",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"description": "A new Flutter project.",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"icons": [
{
"src": "icons/Icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/Icon-512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "icons/Icon-maskable-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "icons/Icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}
+1 -1
View File
@@ -1 +1 @@
VITE_API_BASE_URL = http://localhost:5202/api
VITE_API_BASE_URL = http://192.168.5.116:7070/api
+160 -1
View File
@@ -8,11 +8,15 @@
"name": "web",
"version": "0.0.0",
"dependencies": {
"@vuelidate/core": "^2.0.3",
"@vuelidate/validators": "^2.0.4",
"axios": "^1.13.2",
"feather-icons": "^4.29.2",
"pinia": "^3.0.3",
"vee-validate": "^4.15.1",
"vue": "^3.5.22",
"vue-router": "^4.5.1"
"vue-router": "^4.5.1",
"yup": "^1.7.1"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
@@ -2511,6 +2515,94 @@
"vue-component-type-helpers": "^2.0.0"
}
},
"node_modules/@vuelidate/core": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@vuelidate/core/-/core-2.0.3.tgz",
"integrity": "sha512-AN6l7KF7+mEfyWG0doT96z+47ljwPpZfi9/JrNMkOGLFv27XVZvKzRLXlmDPQjPl/wOB1GNnHuc54jlCLRNqGA==",
"license": "MIT",
"dependencies": {
"vue-demi": "^0.13.11"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^2.0.0 || >=3.0.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@vuelidate/core/node_modules/vue-demi": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz",
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@vuelidate/validators": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/@vuelidate/validators/-/validators-2.0.4.tgz",
"integrity": "sha512-odTxtUZ2JpwwiQ10t0QWYJkkYrfd0SyFYhdHH44QQ1jDatlZgTh/KRzrWVmn/ib9Gq7H4hFD4e8ahoo5YlUlDw==",
"license": "MIT",
"dependencies": {
"vue-demi": "^0.13.11"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^2.0.0 || >=3.0.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@vuelidate/validators/node_modules/vue-demi": {
"version": "0.13.11",
"resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.13.11.tgz",
"integrity": "sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/abbrev": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
@@ -5113,6 +5205,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/property-expr": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
"integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==",
"license": "MIT"
},
"node_modules/proto-list": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
@@ -5601,6 +5699,12 @@
"url": "https://opencollective.com/synckit"
}
},
"node_modules/tiny-case": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==",
"license": "MIT"
},
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
@@ -5726,6 +5830,12 @@
"node": ">=8.0"
}
},
"node_modules/toposort": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
"integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==",
"license": "MIT"
},
"node_modules/totalist": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
@@ -5788,6 +5898,18 @@
"node": ">= 0.8.0"
}
},
"node_modules/type-fest": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -5894,6 +6016,19 @@
"dev": true,
"license": "MIT"
},
"node_modules/vee-validate": {
"version": "4.15.1",
"resolved": "https://registry.npmjs.org/vee-validate/-/vee-validate-4.15.1.tgz",
"integrity": "sha512-DkFsiTwEKau8VIxyZBGdO6tOudD+QoUBPuHj3e6QFqmbfCRj1ArmYWue9lEp6jLSWBIw4XPlDLjFIZNLdRAMSg==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^7.5.2",
"type-fest": "^4.8.3"
},
"peerDependencies": {
"vue": "^3.4.26"
}
},
"node_modules/vite": {
"version": "7.2.7",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.7.tgz",
@@ -6637,6 +6772,30 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/yup": {
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/yup/-/yup-1.7.1.tgz",
"integrity": "sha512-GKHFX2nXul2/4Dtfxhozv701jLQHdf6J34YDh2cEkpqoo8le5Mg6/LrdseVLrFarmFygZTlfIhHx/QKfb/QWXw==",
"license": "MIT",
"dependencies": {
"property-expr": "^2.0.5",
"tiny-case": "^1.0.3",
"toposort": "^2.0.2",
"type-fest": "^2.19.0"
}
},
"node_modules/yup/node_modules/type-fest": {
"version": "2.19.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=12.20"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
}
}
}
+5 -1
View File
@@ -15,11 +15,15 @@
"format": "prettier --write src/"
},
"dependencies": {
"@vuelidate/core": "^2.0.3",
"@vuelidate/validators": "^2.0.4",
"axios": "^1.13.2",
"feather-icons": "^4.29.2",
"pinia": "^3.0.3",
"vee-validate": "^4.15.1",
"vue": "^3.5.22",
"vue-router": "^4.5.1"
"vue-router": "^4.5.1",
"yup": "^1.7.1"
},
"devDependencies": {
"@eslint/js": "^9.33.0",
+3 -19
View File
@@ -1,37 +1,21 @@
<script setup>
</script>
<template>
<div id="app">
<!--
<WindowLayout>
-->
<RouterView></RouterView>
<!---
</WindowLayout>
-->
<RouterView></RouterView>
<Alert></Alert>
</div>
</template>
<script setup>
import WindowLayout from './components/Window.vue'
import Alert from '@/components/messages/Alert.vue';
</script>
<style scoped>
#app {
display: flex;
justify-content: center;
align-items: center;
width: 100vw;
height: 100vh;
padding: 150px;
margin: 0;
box-sizing: border-box;
background: radial-gradient(circle at 50% 30%, #eef2ff, #e2e8f0);
padding: 0;
overflow: hidden;
}
</style>
Binary file not shown.

After

Width:  |  Height:  |  Size: 737 KiB

+21 -16
View File
@@ -4,7 +4,9 @@
<div class="field-wrap">
<i class="icon" :data-feather="props.iconName" ref="iconElement"></i>
<input :type="props.type" v-model="inputValue" :placeholder="props.placeholder" v-bind="attrs"/>
<input :type="props.type" v-model="inputValue" :placeholder="props.placeholder" v-bind="attrs"
@input="inputHandler"
/>
</div>
</div>
</template>
@@ -76,6 +78,7 @@ const renderFeatherIcon = () => {
}
};
// 1. 组件挂载后渲染图标
onMounted(() => {
renderFeatherIcon();
@@ -106,55 +109,57 @@ label {
text-decoration: none;
}
/* 输入框样式:现代填充风格 */
/* 输入框样式:现代线框风格 */
.field-wrap {
position: relative;
display: flex;
align-items: center;
}
.field-wrap .icon {
/* .icon 是 <i> 占位符,定位父容器 */
position: absolute;
left: 12px;
width: 18px;
height: 18px;
color: #94a3b8; /* 默认图标颜色 */
color: #94a3b8;
transition: color 0.2s;
/* Feather icons 渲染后,SVG会继承这个颜色 */
pointer-events: none; /* Icon shouldn't block input click */
z-index: 10;
}
/* 确保渲染后的 SVG 元素颜色能够正确继承 */
.field-wrap .icon > svg {
/* 确保 SVG 自身不被其他默认样式影响 */
display: block;
width: 100%;
height: 100%;
stroke: currentColor; /* 使用父元素的颜色 */
stroke: currentColor;
transition: stroke 0.2s;
}
.field-wrap input {
padding: 12px 12px 12px 40px;
background: #f1f5f9; /* 浅灰底色 */
border: 2px solid transparent;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 8px;
font-size: 14px;
color: #1e293b;
outline: none;
transition: all 0.2s ease;
width: 100%;
}
/* 聚焦交互 */
.field-wrap input:focus {
background: #fff;
border-color: #4f46e5; /* 品牌色 */
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.1);
border-color: #6366f1; /* 品牌色 */
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1); /* 柔和光晕 */
}
/* 错误状态 (Optional, if you pass a prop later) */
.field-wrap input.error {
border-color: #ef4444;
}
/* 🚨 关键:聚焦时图标颜色变化的选择器 */
/* 当 input 聚焦时,选择它后面的所有兄弟元素 (~),其中类名为 .icon 的元素,改变它的颜色 */
/* 由于我们在 .icon 元素上设置了 colorSVG 会自动继承 */
.field-wrap input:focus ~ .icon {
color: #4f46e5; /* 聚焦时的颜色 */
color: #6366f1; /* 聚焦时的颜色 */
}
</style>
+95 -117
View File
@@ -2,21 +2,22 @@
<div id="btn">
<button
class="submit-btn"
:disabled="props.disabled || props.loading" v-bind="attrs"
:class="[
`variant-${props.variant}`,
{ 'is-loading': props.loading }
]"
:data-variant="props.variant"
:disabled="props.disabled || props.loading"
v-bind="attrs"
>
<!-- Loading Spinner -->
<span v-if="props.loading" class="spinner-icon"></span>
<span :class="{ 'is-hidden': props.loading }">
<!-- Button Content -->
<span class="btn-text" :class="{ 'is-hidden': props.loading }">
<slot></slot>
</span>
<div class="iconBox" v-show="!props.loading"><slot name="icon"></slot></div>
<!-- Icon Slot -->
<div class="iconBox" v-if="$slots.icon && !props.loading">
<slot name="icon"></slot>
</div>
</button>
</div>
</template>
@@ -37,7 +38,8 @@ const props = defineProps({
'variant': {
type: String,
default: 'primary',
validator: (value) => ['primary', 'secondary', 'danger', 'text'].includes(value)
validator: (value) => ['primary', 'secondary', 'danger', 'text', 'pill', 'pill-green'].includes(value)
},
// 明确接收 disabled 属性,以便在脚本中控制和类型检查
'disabled': {
@@ -57,153 +59,129 @@ const attrs = useAttrs()
<style scoped>
/* ======================================= */
/* 基础样式和布局 */
/* Base Button Styling */
/* ======================================= */
.submit-btn {
margin-top: 10px;
padding: 12px 20px;
border: none;
border-radius: 8px;
font-weight: 600;
font-size: 15px;
cursor: pointer;
/* 布局:使用 Flex 确保图标和文本对齐 */
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px; /* 文本和图标之间的间距 */
transition: all 0.2s, transform 0.1s;
gap: 8px;
padding: 12px 24px;
border: none;
font-weight: 600;
font-size: 15px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 8px; /* Default */
color: white;
min-width: 100px;
}
/* 交互状态 */
.submit-btn:active {
transform: scale(0.98);
transform: scale(0.96);
}
.submit-btn:disabled,
.submit-btn.is-loading {
opacity: 0.6;
cursor: not-allowed !important;
pointer-events: none; /* 禁用点击事件 */
cursor: not-allowed;
pointer-events: none;
}
/* ======================================= */
/* 样式变体 (Variants) */
/* Style Variants */
/* ======================================= */
/* --- 1. Primary (主色调) --- */
.variant-primary {
background: #4f46e5; /* Indigo 品牌蓝 */
color: white;
/* 1. Primary (Modern Blue) */
.submit-btn[data-variant="primary"] {
background: #3b82f6;
}
.variant-primary:hover {
background: #4338ca;
.submit-btn[data-variant="primary"]:hover {
background: #2563eb;
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3);
}
/* --- 2. Secondary (次要/灰色调) --- */
.variant-secondary {
background: #e2e8f0; /* Slate 浅灰 */
color: #1e293b;
/* 2. Pill (Used for Login - Wider and Rounded) */
.submit-btn[data-variant="pill"] {
background: #3b82f6 !important;
border-radius: 999px !important;
min-width: 340px !important;
height: 52px !important; /* Slightly taller for more impact */
font-size: 16px !important;
box-shadow: 0 6px 16px rgba(59, 130, 246, 0.2) !important;
}
.variant-secondary:hover {
background: #cbd5e1;
.submit-btn[data-variant="pill"]:hover {
background: #2563eb !important;
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(59, 130, 246, 0.3);
}
/* --- 3. Danger (危险/红色调) --- */
.variant-danger {
background: #ef4444; /* Red 红色 */
color: white;
/* 2.5 Pill-Green (Used for Register) */
.submit-btn[data-variant="pill-green"] {
background: #10b981 !important;
border-radius: 999px !important;
min-width: 340px !important;
height: 52px !important;
font-size: 16px !important;
box-shadow: 0 6px 16px rgba(16, 185, 129, 0.2) !important;
}
.variant-danger:hover {
.submit-btn[data-variant="pill-green"]:hover {
background: #059669 !important;
transform: translateY(-2px);
box-shadow: 0 10px 25px rgba(16, 185, 129, 0.3);
}
/* 3. Secondary */
.submit-btn[data-variant="secondary"] {
background: #f1f5f9;
color: #475569;
}
.submit-btn[data-variant="secondary"]:hover {
background: #e2e8f0;
}
/* 4. Danger */
.submit-btn[data-variant="danger"] {
background: #ef4444;
}
.submit-btn[data-variant="danger"]:hover {
background: #dc2626;
}
/* --- 4. Text (文本按钮/无背景) --- */
.variant-text {
/* 5. Text */
.submit-btn[data-variant="text"] {
background: transparent;
color: #4f46e5;
padding: 10px 12px; /* 减小 padding 以适应文本按钮 */
color: #3b82f6;
}
.variant-text:hover {
text-decoration: underline;
background: transparent;
color: #4338ca;
}
/* 按钮基础样式 (假设你已有一些基础样式) */
.submit-btn {
position: relative; /* 确保 spinner-icon 可以相对于按钮定位 */
display: inline-flex;
align-items: center;
justify-content: center;
padding: 10px 20px;
font-size: 16px;
border: none;
cursor: pointer;
transition: all 0.2s;
.submit-btn[data-variant="text"]:hover {
background: rgba(59, 130, 246, 0.05);
}
/* =======================================
1. 加载状态:is-loading
======================================= */
.submit-btn.is-loading {
/* 视觉反馈:半透明、改变颜色或禁用 */
opacity: 0.8;
cursor: not-allowed;
pointer-events: none; /* 确保点击穿透 */
}
/* =======================================
2. 旋转图标样式:spinner-icon
======================================= */
.submit-btn .spinner-icon {
display: inline-block;
width: 1em; /* 旋转图标的宽度 */
height: 1em; /* 旋转图标的高度 */
border: 2px solid currentColor; /* 边框颜色继承自按钮文本颜色 */
border-top-color: transparent; /* 顶部透明,形成旋转的缺口 */
border-radius: 50%;
animation: spin 1s linear infinite; /* 应用旋转动画 */
margin-right: 0.5em; /* 在图标和文本之间添加间距 */
/* ======================================= */
/* Internal Elements */
/* ======================================= */
.spinner-icon {
width: 1.25rem;
height: 1.25rem;
border: 2px solid rgba(255, 255, 255, 0.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.iconBox {
display: flex;
display: flex !important;
align-items: center;
justify-content: center;
}
/* 在加载状态且有文本时,清除右边距 */
.submit-btn.is-loading .spinner-icon {
/* 如果按钮内容被隐藏了,这个 margin 应该根据你的布局决定是否清除 */
margin-right: 0;
}
/* =======================================
3. 文本隐藏样式
======================================= */
.is-hidden {
/* 隐藏文本,但不移除它,保持布局稳定 */
visibility: hidden;
height: 0;
width: 0;
margin: 0;
padding: 0;
overflow: hidden;
visibility: hidden;
opacity: 0;
}
/* =======================================
4. 旋转动画定义
======================================= */
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
to { transform: rotate(360deg); }
}
</style>
+45 -2
View File
@@ -1,11 +1,44 @@
import { createRouter, createWebHistory } from 'vue-router'
import MainView from '@/views/Main.vue'
import TestView from '@/views/Test.vue'
import { useAuthStore } from '@/stores/auth'
import { useMessage } from '@/components/messages/useAlert'
const message = useMessage();
const routes = [
{ path: '/auth/login', component: () => import('@/views/auth/Login.vue') },
{ path: '/', component: MainView },
{ path: '/index', component: MainView },
{
path: '/',
component: MainView,
children: [
{
path: '/messages',
name: 'userMessages',
component: () => import('@/views/messages/MessageList.vue'),
children: [
{
path: '/messages/chat/:id',
name: '/msgChat',
component: () => import('@/views/messages/MessageContent.vue'),
props: true
}
]
},
{ path: '/contacts', name: 'userContacts', component: () => import('@/views/contact/ContactList.vue') },
{ path: '/settings', name: 'userSettings', component: () => import('@/views/settings/SettingMenu.vue') }
],
meta: {
requiresAuth: true
}
},
{
path: '/index',
component: MainView,
meta: {
requiresAuth: true
}
},
{ path: '/test', component: TestView },
]
@@ -14,4 +47,14 @@ const router = createRouter({
routes,
})
router.beforeEach((to, from, next) => {
const authStore = useAuthStore();
if (to.meta.requiresAuth && !authStore.isLoggedIn) {
message.info('未登录,即将跳转...');
next('auth/login');
} else {
next();
}
})
export default router
+6
View File
@@ -7,5 +7,11 @@ export const authService = {
* @returns
*/
login: (data) => request.post('/auth/login', data),
/**
* 用户注册
* @param {*} data
* @returns
*/
register: (data) => request.post('/auth/register', data)
}
+38
View File
@@ -0,0 +1,38 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useAuthStore = defineStore('auth', () => {
const token = ref(localStorage.getItem('user_token') || '');
const refreshToken = ref(localStorage.getItem('refresh_token') || '');
const userInfo = ref(localStorage.getItem('user_info') || '');
//判断是否已登录
const isLoggedIn = computed(() => !!token.value);
/**
* 登录成功保存状态
* @param {String} newToken 用户凭证
* @param {*} user 用户信息
*/
function setLoginInfo(newToken, newRefreshToken, user) {
token.value = newToken;
refreshToken.value = newRefreshToken
userInfo.value = user;
localStorage.setItem('user_token', newToken);
localStorage.setItem('refresh_token', refreshToken)
localStorage.setItem('user_info', user)
}
/**
* 退出登录
*/
function logout() {
token.value = '';
userInfo.value = null;
localStorage.removeItem('user_token');
localStorage.removeItem('refresh_token')
localStorage.removeItem('user_info')
}
return { token, userInfo, isLoggedIn, setLoginInfo, logout };
})
-12
View File
@@ -1,12 +0,0 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})
+180 -431
View File
@@ -1,477 +1,226 @@
<template>
<div class="im-container">
<nav class="sidebar" aria-label="主导航">
<div class="avatar-wrapper">
<div class="avatar"></div>
<div class="status-dot"></div>
</div>
<div class="menu" role="tablist">
<button
v-for="item in menus"
:key="item.key"
@click="currentTab = item.key"
:class="['menu-btn', { active: currentTab === item.key }]"
:title="item.label"
:aria-pressed="currentTab === item.key"
>
<i :class="item.icon" class="menu-icon" aria-hidden="true"></i>
<span v-if="item.notification" class="notification-badge">{{ item.notification }}</span>
</button>
</div>
<div class="bottom-menu">
<button class="menu-btn" title="设置" @click="currentTab = 'settings'">
<i class="fas fa-cog menu-icon"></i>
</button>
<nav class="nav-sidebar">
<div class="user-self">
<img :src="myInfo?.avatar ?? defaultAvatar" class="avatar-std" />
</div>
<router-link class="nav-item" to="/messages" active-class="active">💬</router-link>
<router-link class="nav-item" to="/contacts" active-class="active">👤</router-link>
<router-link class="nav-item" to="/settings" active-class="active"></router-link>
</nav>
<aside class="list-panel">
<div class="panel-header">
<h2>{{ currentTabTitle }}</h2>
<div class="header-actions">
<button class="add-btn" :title="currentTab === 'chat' ? '发起新聊天' : '添加好友'"><i class="fas fa-plus"></i></button>
</div>
</div>
<div class="search-wrapper">
<div class="search-box">
<i class="fas fa-search search-icon" aria-hidden="true"></i>
<input type="text" placeholder="搜索..." v-model="searchText" aria-label="搜索" />
</div>
</div>
<div class="list-content custom-scroll">
<transition-group name="list" tag="ul" v-if="currentTab === 'chat'" class="chat-list">
<li
v-for="chat in filteredChats"
:key="chat.id"
@click="selectChat(chat)"
:class="['list-item', { active: currentChat?.id === chat.id }]"
>
<div class="item-avatar">
{{ getInitials(chat.name) }}
<span v-if="chat.status === 'online'" class="online-dot" aria-hidden="true"></span>
</div>
<div class="item-info">
<div class="row-top">
<span class="item-name">{{ chat.name }}</span>
<span class="item-time">{{ chat.lastTime }}</span>
</div>
<div class="row-bottom">
<span class="item-msg">{{ chat.lastMsg }}</span>
<span v-if="chat.unread > 0" class="unread-badge">{{ chat.unread }}</span>
</div>
</div>
</li>
</transition-group>
<ul v-else-if="currentTab === 'friends'" class="friend-list">
<li v-for="f in filteredFriends" :key="f.id" class="list-item">
<div class="item-avatar friend">
{{ getInitials(f.name) }}
</div>
<div class="item-info">
<div class="row-top">
<span class="item-name">{{ f.name }}</span>
</div>
<div class="row-bottom">
<span :class="['status-text', f.status]">
{{ f.status === 'online' ? '在线' : '离线' }}
</span>
</div>
</div>
</li>
</ul>
<div v-else class="empty-placeholder">
<div class="icon-bg">
<i :class="getEmptyIcon()"></i>
</div>
<h3>{{ currentTab === 'groups' ? '暂无群聊' : '设置中心' }}</h3>
<p>这里目前什么都没有</p>
</div>
</div>
</aside>
<main class="chat-stage">
<template v-if="currentChat">
<header class="chat-header">
<div class="user-profile">
<div class="header-avatar">{{ getInitials(currentChat.name) }}</div>
<div class="header-info">
<div class="header-name">{{ currentChat.name }}</div>
<div class="header-status">
<span class="status-indicator" :class="currentChat.status"></span>
{{ currentChat.status === 'online' ? '在线' : '离线' }}
</div>
</div>
</div>
<div class="header-tools">
<button class="tool-btn" title="语音通话"><i class="fas fa-phone-alt"></i></button>
<button class="tool-btn" title="视频通话"><i class="fas fa-video"></i></button>
<button class="tool-btn" title="更多选项"><i class="fas fa-ellipsis-h"></i></button>
</div>
</header>
<div class="chat-body custom-scroll" ref="chatBody">
<div
v-for="msg in messages"
:key="msg.id"
:class="['msg-row', msg.from === 'me' ? 'msg-me' : 'msg-them']"
>
<div class="msg-avatar" v-if="msg.from !== 'me'">
{{ getInitials(currentChat.name) }}
</div>
<div class="msg-bubble-group">
<div class="msg-bubble">
{{ msg.text }}
</div>
<div class="msg-meta">{{ msg.time }}</div>
</div>
</div>
</div>
<footer class="chat-footer">
<div class="input-container">
<button class="attach-btn" title="发送文件"><i class="fas fa-plus"></i></button>
<textarea
v-model="input"
placeholder="输入消息..."
@keydown.enter.prevent="handleSend"
rows="1"
ref="messageInput"
></textarea>
<button class="emoji-btn" title="表情"><i class="far fa-smile"></i></button>
<button class="send-btn" @click="handleSend" :disabled="!input.trim()">
<i class="fas fa-paper-plane"></i>
</button>
</div>
</footer>
</template>
<div class="empty-chat" v-else>
<div class="empty-illustration">
<i class="fas fa-comments"></i>
</div>
<h3>开启美好对话</h3>
<p>选择左侧联系人开始畅所欲言</p>
</div>
</main>
<router-view></router-view>
</div>
</template>
<script setup>
import { ref, computed, nextTick, onMounted } from 'vue'
import { ref, watch } from 'vue'
import { useAuthStore } from '@/stores/auth';
import defaultAvatar from '@/assets/default_avatar.png'
/** (原代码未改动,样式优化专注于 CSS) */
const currentTab = ref('chat')
const searchText = ref('')
const chats = ref([
{ id: 1, name: 'Allen Zhang', lastMsg: '今晚一起吃饭吗?', lastTime: '10:30', unread: 2, status: 'online' },
{ id: 2, name: 'Sarah Lee', lastMsg: '设计稿已经发你邮箱了', lastTime: '昨天', unread: 0, status: 'online' },
{ id: 3, name: 'David Wang', lastMsg: '项目进度怎么样?', lastTime: '周三', unread: 1, status: 'offline' },
{ id: 4, name: 'Product Team', lastMsg: '下周一开会讨论需求', lastTime: '周一', unread: 0, status: 'online' },
{ id: 5, name: 'Lisa Chen', lastMsg: '好的,没问题', lastTime: '3月15日', unread: 0, status: 'online' },
])
const friends = ref([
{ id: 1, name: 'Allen Zhang', status: 'online' },
{ id: 2, name: 'Sarah Lee', status: 'online' },
{ id: 3, name: 'David Wang', status: 'offline' },
{ id: 4, name: 'Product Team', status: 'online' },
])
const menus = ref([
{ key: 'chat', label: '消息', icon: 'fas fa-comment-alt', notification: computed(() => chats.value.reduce((sum, c) => sum + c.unread, 0)) },
{ key: 'friends', label: '联系人', icon: 'fas fa-user-friends', notification: 0 },
{ key: 'groups', label: '群聊', icon: 'fas fa-users', notification: 0 },
])
const currentChat = ref(null)
const messages = ref([])
const input = ref('')
const chatBody = ref(null)
const messageInput = ref(null)
const authStore = useAuthStore();
const myInfo = authStore.userInfo;
const filteredChats = computed(() => {
if (!searchText.value) return chats.value
const lowerSearch = searchText.value.toLowerCase()
return chats.value.filter(
(chat) => chat.name.toLowerCase().includes(lowerSearch) || chat.lastMsg.toLowerCase().includes(lowerSearch),
)
})
const filteredFriends = computed(() => {
if (!searchText.value) return friends.value
const lowerSearch = searchText.value.toLowerCase()
return friends.value.filter((friend) => friend.name.toLowerCase().includes(lowerSearch))
})
const currentTabTitle = computed(() => {
const map = { chat: '消息', friends: '通讯录', groups: '群组', settings: '设置' }
return map[currentTab.value] || '消息'
})
function getInitials(name) { return name ? name.substring(0, 1).toUpperCase() : '' }
function getEmptyIcon() { if (currentTab.value === 'groups') return 'fas fa-users'; if (currentTab.value === 'settings') return 'fas fa-cog'; return 'fas fa-box-open' }
function formatTime(date) { return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) }
function scrollBottom() { if (chatBody.value) chatBody.value.scrollTop = chatBody.value.scrollHeight }
function focusInput() { if (messageInput.value) messageInput.value.focus() }
function selectChat(chat) {
currentChat.value = chat
messages.value = [
{ id: 1, from: 'them', text: `Hi,我是 ${chat.name}`, time: '09:15' },
{ id: 2, from: 'me', text: '你好!最近怎么样?', time: '09:16' },
{ id: 3, from: 'them', text: '挺好的,你呢?', time: '09:20' },
]
nextTick(() => { scrollBottom(); focusInput() })
if (chat.unread > 0) chat.unread = 0
}
function simulateReply() {
if (!currentChat.value) return
const replies = ['收到', '这个设计不错', '稍等我看一下', '哈哈哈哈', '明白,我马上处理']
const randomReply = replies[Math.floor(Math.random() * replies.length)]
setTimeout(() => {
messages.value.push({ id: Date.now(), from: 'them', text: randomReply, time: formatTime(new Date()) })
nextTick(scrollBottom)
}, 500 + Math.random() * 1500)
}
function handleSend() {
if (!input.value.trim()) return
messages.value.push({ id: Date.now(), from: 'me', text: input.value, time: formatTime(new Date()) })
input.value = ''
nextTick(scrollBottom)
if (currentChat.value) {
currentChat.value.lastMsg = messages.value[messages.value.length - 1].text
currentChat.value.lastTime = formatTime(new Date())
}
simulateReply()
}
onMounted(() => { if (chats.value.length > 0) selectChat(chats.value[0]) })
</script>
<style scoped>
/* ------------------- 设计代币 (简约优雅) ------------------- */
:root{
/* 极简设计变量:避免与外部容器背景冲突 */
--bg: #f9fafb; /* 比外部纯白略深的浅灰,形成层级 */ /* 组件自身有背景,不透明 */
--panel: #ffffff; /* 面板纯白 */
--border: #e5e7eb; /* 轻边框 */
--text: #1f2937; /* 主文字 */
--muted: #6b7280; /* 次文字 */
--primary: #4f46e5; /* 主色:靛蓝 */
--primary-contrast: #ffffff; /* 主色上的前景色 */ /* 单一强调色(靛蓝) */
--success: #10b981;
--danger: #ef4444;
--radius: 10px;
--shadow: 0 2px 6px rgba(0,0,0,0.04); /* 极弱阴影 */
--fn: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto;
}
.im-container{
width: 100%;
height: 100%;
/* 1. 基础容器:锁定宽高,禁止抖动 */
.im-container {
display: flex;
width: 1000px;
height: 650px;
margin: 40px auto;
background: #fff;
border-radius: 4px;
overflow: hidden;
background: var(--bg); /* 浅灰背景,明确区分外层 */
border-radius: var(--radius);
border: 1px solid var(--border);
box-shadow: none;
font-family: var(--fn);
color: var(--text);
box-shadow: 0 12px 48px rgba(0,0,0,0.1);
}
/* ------------------- 侧边栏 ------------------- */
.sidebar{
width: 64px;
background: #ffffff; /* 内部区域保持纯白 */
border-right: 1px solid var(--border);
padding: 16px 6px;
display:flex;
flex-direction:column;
align-items:center;
/* 导航栏 */
.nav-sidebar {
width: 60px;
flex-shrink: 0;
background: #282828;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px 0;
gap: 24px;
}
.avatar{
width:48px;height:48px;border-radius:10px;background:linear-gradient(135deg,var(--primary),#7c3aed);
display:flex;align-items:center;justify-content:center;color:#fff;font-weight:600;font-size:16px;box-shadow:0 4px 10px rgba(37,99,235,0.12);
.user-self { margin-bottom: 10px; }
/* 2. 列表区修复 */
.list-panel {
width: 250px;
flex-shrink: 0;
background: #eee;
border-right: 1px solid #d6d6d6;
display: flex;
flex-direction: column;
}
.avatar-wrapper{position:relative;margin-bottom:18px}
.status-dot{position:absolute;right:6px;bottom:6px;width:11px;height:11px;background:var(--success);border-radius:50%;box-shadow:0 0 0 2px var(--panel)}
.menu{display:flex;flex-direction:column;gap:8px;width:100%;align-items:center}
.menu-btn{
background: transparent;
border: none;
color: var(--muted); /* 默认图标灰 */
width: 48px;
height: 48px;
border-radius: 8px;
/* 修复:搜索框美化 */
.search-section {
padding: 20px 12px 10px 12px;
}
.search-box {
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
background: #dbdbdb;
padding: 4px 8px;
border-radius: 4px;
gap: 5px;
}
.search-icon { font-size: 12px; color: #666; }
.search-box input {
background: transparent;
border: none;
outline: none;
font-size: 12px;
width: 100%;
}
/* Font Awesome 兜底:不管外部怎么写 */
.menu-btn i{
color: currentColor;
}
.menu-btn:hover{
background: #f3f4f6; /* 纯灰 hover,不使用渐变 */
color: var(--primary);
transform: none;
}
.menu-btn.active{
background: var(--primary); /* 选中态:主色背景 */
color: var(--primary-contrast); /* 明确指定前景色 */
}
.scroll-area { flex: 1; overflow-y: auto; }
/* 防止 font-awesome 被父级覆盖 */
.menu-btn.active .menu-icon{
font-size: 18px;
color: inherit; /* 关键:图标严格继承 button 的 color */
}
.menu-icon{font-size:18px}
.notification-badge{position:absolute;top:8px;right:10px;background:var(--danger);color:#fff;border-radius:999px;min-width:18px;height:18px;display:inline-flex;align-items:center;justify-content:center;padding:0 6px;font-size:11px;font-weight:600;box-shadow:0 1px 0 rgba(255,255,255,0.08)}
.bottom-menu{margin-top:auto;padding-bottom:6px}
/* ------------------- 中间面板 ------------------- */
.list-panel{
width: 300px;
background: #ffffff; /* 中间列表纯白 */
border-right: 1px solid var(--border);
display:flex;
flex-direction:column;
}
.panel-header{display:flex;align-items:center;justify-content:space-between;padding:6px 8px}
.panel-header h2{margin:0;font-size:18px;font-weight:700;color:var(--text)}
.add-btn{background:var(--primary);border:none;color:#fff;width:36px;height:36px;border-radius:10px;display:flex;align-items:center;justify-content:center;cursor:pointer;box-shadow:0 6px 12px rgba(37,99,235,0.12)}
.search-wrapper{padding:8px}
.search-box{position:relative}
.search-box input{width:100%;padding:10px 12px 10px 36px;border-radius:10px;border:1px solid rgba(15,23,42,0.05);background:rgba(255,255,255,0.9);font-size:14px;color:var(--text);box-shadow:inset 0 1px 0 rgba(0,0,0,0.02)}
.search-box input:focus{outline:none;border-color:rgba(37,99,235,0.35);box-shadow:0 6px 20px rgba(37,99,235,0.06)}
.search-icon{position:absolute;left:12px;top:50%;transform:translateY(-50%);color:var(--muted);font-size:13px}
.list-content{flex:1;overflow:auto;padding-right:6px}
.chat-list,.friend-list{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:8px}
.list-item{display:flex;align-items:center;padding:10px;border-radius:10px;cursor:pointer;transition:all .16s ease;background:transparent}
.list-item:hover{
background: #f9fafb; /* 极轻 hover */
transform: none;
}
.list-item.active{
background: #eef2ff; /* 单色选中 */
border-left: 3px solid var(--primary);
padding-left: 9px;
}
.item-avatar{width:52px;height:52px;border-radius:12px;background:linear-gradient(135deg,#eef2ff,#e9d5ff);display:flex;align-items:center;justify-content:center;color:var(--primary);font-weight:700;font-size:15px;margin-right:12px;flex-shrink:0}
.item-avatar.friend{background:linear-gradient(135deg,#ecfdf5,#ecfeff);color:var(--success)}
.online-dot{position:absolute;right:4px;bottom:6px;width:10px;height:10px;background:var(--success);border-radius:50%;box-shadow:0 0 0 2px var(--panel)}
.item-info{flex:1;min-width:0;display:flex;flex-direction:column}
.row-top{display:flex;justify-content:space-between;align-items:center}
.item-name{font-weight:600;color:var(--text);font-size:15px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:70%}
.item-time{font-size:12px;color:var(--muted);flex-shrink:0;margin-left:8px}
.item-msg{font-size:13px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.unread-badge{background:var(--danger);color:#fff;border-radius:999px;padding:4px 8px;font-size:11px;font-weight:700}
.status-text{font-size:13px;color:var(--muted)}
.status-text.online{color:var(--success);font-weight:600}
/* ------------------- 聊天区 ------------------- */
.chat-stage{
/* 3. 聊天主面板修复 */
.chat-panel {
flex: 1;
display: flex;
flex-direction: column;
background: #ffffff; /* 聊天区纯白 */
background: #f5f5f5;
min-width: 0;
}
.chat-header{
height: 64px;
.chat-header {
height: 60px;
padding: 0 20px;
display:flex;
align-items:center;
justify-content:space-between;
border-bottom: 1px solid var(--border);
background: var(--panel);
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #e0e0e0;
background: #f5f5f5;
}
.user-profile{display:flex;align-items:center;gap:12px}
.header-avatar{width:44px;height:44px;border-radius:10px;background:linear-gradient(135deg,#eef2ff,#e9d5ff);display:flex;align-items:center;justify-content:center;color:var(--primary);font-weight:700}
.header-name{font-weight:700}
.header-status{font-size:13px;color:var(--muted);display:flex;align-items:center;gap:8px}
.status-indicator{display:inline-block;width:8px;height:8px;border-radius:999px}
.status-indicator.online{background:var(--success)}
.status-indicator.offline{background:rgba(15,23,42,0.08)}
.header-tools{display:flex;gap:8px}
.tool-btn{background:transparent;border:1px solid rgba(15,23,42,0.04);padding:8px;border-radius:10px;cursor:pointer;display:flex;align-items:center;justify-content:center}
.chat-body{flex:1;padding:20px;display:flex;flex-direction:column;gap:12px;overflow:auto}
.msg-row{display:flex;gap:10px;align-items:flex-end}
.msg-me{justify-content:flex-end}
.msg-them{justify-content:flex-start}
.msg-avatar{width:36px;height:36px;border-radius:10px;background:linear-gradient(135deg,#eef2ff,#e9d5ff);display:flex;align-items:center;justify-content:center;color:var(--primary);font-weight:700}
.msg-bubble-group{max-width:70%;display:flex;flex-direction:column;align-items:flex-start}
.msg-bubble{
padding: 8px 12px;
border-radius: 8px;
background: #f3f4f6;
color: var(--text);
.chat-history {
flex: 1;
overflow-y: auto;
padding: 20px 30px;
}
/* 修复:本人消息右侧对齐逻辑 */
.msg {
display: flex;
margin-bottom: 24px;
gap: 12px;
}
/* 别人发的:默认靠左 */
.msg.other { flex-direction: row; }
/* 本人发的:翻转排列方向,靠右显示 */
.msg.mine {
flex-direction: row-reverse;
}
.msg-content {
display: flex;
flex-direction: column;
max-width: 70%;
}
/* 修复:本人消息文字和时间戳也需要右对齐 */
.msg.mine .msg-content {
align-items: flex-end;
}
.bubble {
padding: 9px 14px;
border-radius: 6px;
font-size: 14px;
line-height: 1.4;
box-shadow: none;
line-height: 1.6;
word-break: break-all;
position: relative;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
.msg-me .msg-bubble{
background: var(--primary);
color: #ffffff;
}
.msg-meta{font-size:12px;color:var(--muted);margin-top:6px}
.chat-footer{
padding: 12px 20px;
border-top: 1px solid var(--border);
background: var(--panel);
.other .bubble { background: #fff; color: #333; }
.mine .bubble { background: #95ec69; color: #000; }
.msg-time {
font-size: 11px;
color: #b2b2b2;
margin-top: 4px;
}
.input-container{
display:flex;
align-items:flex-end;
gap: 8px;
background: #f9fafb;
padding: 8px;
/* 头像样式统一 */
.avatar-std { width: 40px; height: 40px; border-radius: 4px; object-fit: cover; }
.avatar-chat { width: 38px; height: 38px; border-radius: 4px; object-fit: cover; flex-shrink: 0; }
/* 未读气泡 */
.avatar-container { position: relative; }
.unread-badge {
position: absolute;
top: -5px;
right: -5px;
background: #ff4d4f;
color: #fff;
font-size: 10px;
padding: 0 4px;
min-width: 16px;
height: 16px;
border-radius: 8px;
border: 1px solid var(--border);
box-shadow: none;
}
.input-container:focus-within{box-shadow:0 8px 30px rgba(37,99,235,0.06)}
.input-container textarea{flex:1;border:none;background:transparent;resize:none;outline:none;font-size:14px;line-height:1.5;min-height:36px;max-height:120px}
.attach-btn,.emoji-btn{background:transparent;border:none;padding:8px;border-radius:8px;cursor:pointer}
.send-btn{
background: var(--primary);
color: var(--primary-contrast); /* 明确发送按钮图标颜色 */
border: none;
padding: 8px 12px;
border-radius: 8px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #fff;
}
.send-btn i{
color: currentColor; /* 防止图标丢失 */
/* 输入框区域修复 */
.chat-footer {
height: 160px;
background: #fff;
border-top: 1px solid #e0e0e0;
padding: 10px 20px;
display: flex;
flex-direction: column;
}
.send-btn:disabled{
background: #c7d2fe;
color: #ffffff;
}
.send-btn:disabled{opacity:0.5;cursor:not-allowed}
.toolbar { display: flex; gap: 12px; margin-bottom: 5px; font-size: 20px; color: #666; }
.toolbar button { background: none; border: none; cursor: pointer; opacity: 0.7; }
/* ------------------- 细节与响应式 ------------------- */
.custom-scroll::-webkit-scrollbar{width:8px}
.custom-scroll::-webkit-scrollbar-thumb{background:rgba(15,23,42,0.06);border-radius:999px}
@media (max-width:900px){
.im-container{flex-direction:column;width:94vw;height:92vh}
.sidebar{flex-direction:row;width:100%;height:64px;padding:8px}
.list-panel{width:100%;order:2;border-right:none;border-top:1px solid rgba(15,23,42,0.03)}
.chat-stage{order:1}
textarea {
flex: 1;
border: none;
outline: none;
resize: none;
font-family: inherit;
font-size: 14px;
padding: 5px 0;
}
</style>
.send-row { display: flex; justify-content: flex-end; }
.send-btn {
background: #f5f5f5;
color: #07c160;
border: 1px solid #e0e0e0;
padding: 5px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.send-btn:hover { background: #e2e2e2; }
/* 列表美化 */
.list-item { display: flex; padding: 12px; gap: 12px; cursor: pointer; }
.list-item.active { background: #c6c6c6; }
.list-item:hover:not(.active) { background: #ddd; }
.info { flex: 1; overflow: hidden; }
.name-row { display: flex; justify-content: space-between; align-items: center; }
.name { font-size: 14px; font-weight: 500; }
.time { font-size: 11px; color: #999; }
.last-msg { font-size: 12px; color: #888; margin-top: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nav-item { font-size: 24px; cursor: pointer; opacity: 0.5; }
.nav-item.active { opacity: 1; }
</style>
+209 -101
View File
@@ -1,40 +1,44 @@
<template>
<div class="login-layout">
<div class="side-visual">
<div class="visual-mask"></div>
<div class="brand-container">
<h1 class="hero-title">Work<br>Together.</h1>
<p class="hero-subtitle">下一代企业级即时通讯平台让沟通无距离</p>
</div>
<div class="visual-footer">
<span>© 2025 IM System</span>
<div class="dots">
<span></span><span></span><span></span>
<div class="login-card">
<div class="side-visual">
<div class="brand-container">
<h1 class="hero-title">Work<br>Together.</h1>
<p class="hero-subtitle">下一代企业级即时通讯平台让沟通无距离</p>
</div>
<div class="visual-footer">
<span>© 2025 IM System</span>
<div class="dots">
<span></span><span></span><span></span>
</div>
</div>
</div>
</div>
<div class="side-form">
<div class="form-wrapper">
<div class="welcome-header">
<h2>账号登录</h2>
<p>请输入您的工作账号以继续</p>
</div>
<div class="side-form">
<div class="form-wrapper">
<div class="welcome-header">
<h2>账号登录</h2>
<p>请输入您的工作账号以继续</p>
</div>
<form @submit.prevent="handleLogin">
<IconInput class="input" placeholder="请输入用户名" lab="用户名 / 邮箱" type="text" icon-name="user" v-model="form.username"/>
<form @submit.prevent="handleLogin">
<IconInput class="input"
placeholder="请输入用户名" lab="用户名 / 邮箱" type="text" icon-name="user" v-model="form.username"/>
<IconInput class="input" placeholder="请输入密码" lab="密码" type="password" icon-name="user" v-model="form.password"/>
<IconInput class="input"
placeholder="请输入密码" lab="密码" type="password" icon-name="lock" v-model="form.password"/>
<MyButton class="loginBtn" :loading="loading">
登录
<template #icon><i data-feather="arrow-right"></i></template>
</MyButton>
</form>
<div class="login-btn-wrapper">
<MyButton variant="pill" class="login-btn" :loading="loading">
登录
</MyButton>
</div>
</form>
<div class="register-hint">
还没有账号? <a href="#">联系管理员注册</a>
<div class="register-hint">
还没有账号? <router-link to="/auth/register">立即注册</router-link>
</div>
</div>
</div>
</div>
@@ -48,9 +52,15 @@ import { useMessage } from '@/components/messages/useAlert'
import { authService } from '@/services/auth'
import { useRouter } from 'vue-router'
import feather from 'feather-icons'
import IconInput from '@/components/IconInput.vue'
import MyButton from '@/components/MyButton.vue'
import { required, maxLength, helpers } from '@vuelidate/validators'
import useVuelidate from '@vuelidate/core'
import { useAuthStore } from '@/stores/auth'
const message = useMessage()
const router = useRouter()
const message = useMessage();
const router = useRouter();
const authStore = useAuthStore();
const loading = ref(false)
const form = reactive({
@@ -58,22 +68,44 @@ const form = reactive({
password: ''
})
const rules = {
username:{
required:helpers.withMessage('用户名不能为空', required),
maxLength:helpers.withMessage('用户名最大20字符', maxLength(20))
},
password:{
required:helpers.withMessage('密码不能为空', required),
maxLength:helpers.withMessage('密码最大50字符', maxLength(50))
}
};
const v$ = useVuelidate(rules,form);
const handleLogin = async () => {
const isFormCorrect = await v$.value.$validate()
if (!isFormCorrect) {
if (v$.value.$errors.length > 0) {
message.error(v$.value.$errors[0].$message)
}
return
}
try{
loading.value = true;
const res = await authService.login(form);
if(res.code === 0){
message.success('登成功')
loading.value = false;
router.push('/index')
}else{
message.error(res.message)
const res = await authService.login(form);
if(res.code === 0){ // Assuming 0 is success
message.success('登成功')
authStore.setLoginInfo(res.data.token, res.data.refreshToken, res.data.userInfo);
router.push('/')
}else{
message.error(res.message || '登录失败')
}
} catch (e) {
console.error(e)
message.error('登录请求异常')
} finally{
loading.value = false;
}
}finally{
loading.value = false;
}
}
onMounted(() => {
@@ -82,138 +114,214 @@ onMounted(() => {
</script>
<style scoped>
:deep(.loginBtn) {
width: 100%;
}
:deep(.input){
width: 100%;
}
/* 撑满 Window 组件的内容区 */
/* Soft Mesh Gradient Background */
.login-layout {
display: flex;
width: 100%;
height: 100%;
align-items: center;
justify-content: center;
width: 100vw;
height: 100vh;
min-height: 100vh;
background-color: #f8fafc;
background-image:
radial-gradient(at 0% 0%, hsla(190, 100%, 95%, 1) 0, transparent 50%),
radial-gradient(at 50% 0%, hsla(160, 100%, 96%, 1) 0, transparent 50%),
radial-gradient(at 100% 0%, hsla(210, 100%, 96%, 1) 0, transparent 50%);
overflow: hidden;
position: relative;
}
/* Very subtle grid overlay */
.login-layout::before {
content: "";
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background-image: radial-gradient(rgba(0,0,0,0.02) 1px, transparent 1px);
background-size: 20px 20px;
z-index: 0;
}
.login-card {
display: flex;
width: 1000px;
height: 600px;
background: rgba(255, 255, 255, 0.82);
backdrop-filter: blur(30px);
border-radius: 32px;
box-shadow:
0 10px 25px -5px rgba(0, 0, 0, 0.02),
0 40px 100px -20px rgba(0, 0, 0, 0.08);
overflow: hidden;
z-index: 10;
position: relative;
border: 1px solid rgba(255, 255, 255, 0.7);
}
/* --- 左侧视觉 --- */
.side-visual {
width: 42%;
background: linear-gradient(135deg, #4f46e5 0%, #3b82f6 100%);
flex: 1;
/* Soft connectivity gradient */
background: linear-gradient(135deg, #4f46e5 0%, #06b6d4 100%);
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
padding: 40px;
padding: 60px;
color: white;
overflow: hidden;
}
/* 增加一点背景纹理 */
/* Abstract "Connection" Circles */
.side-visual::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background-image: radial-gradient(rgba(255,255,255,0.15) 1px, transparent 1px);
background-size: 20px 20px;
opacity: 0.6;
top: -20%; left: -20%;
width: 400px; height: 400px;
border-radius: 50%;
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 60%);
z-index: 1;
}
.side-visual::after {
content: '';
position: absolute;
bottom: -10%; right: -10%;
width: 300px; height: 300px;
border-radius: 50%;
background: radial-gradient(circle, rgba(255,255,255,0.15) 0%, transparent 60%);
z-index: 1;
}
.brand-container {
position: relative;
z-index: 2;
}
.hero-title {
font-size: 48px;
font-size: 44px;
font-weight: 800;
line-height: 1.1;
margin-bottom: 16px;
letter-spacing: -1px;
}
.hero-subtitle {
font-size: 15px;
opacity: 0.9;
line-height: 1.6;
max-width: 300px;
line-height: 1.2;
margin-bottom: 20px;
text-shadow: 0 4px 10px rgba(0,0,0,0.1);
}
.hero-subtitle {
font-size: 16px;
opacity: 0.95;
line-height: 1.6;
max-width: 340px;
font-weight: 400;
}
.visual-footer {
position: absolute;
bottom: 30px;
left: 40px;
right: 40px;
bottom: 40px;
left: 60px;
right: 60px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
opacity: 0.6;
}
.dots span {
display: inline-block;
width: 4px; height: 4px;
background: white;
border-radius: 50%;
margin-left: 4px;
opacity: 0.8;
z-index: 2;
}
.dots span {
display: inline-block;
width: 6px; height: 6px;
background: white;
border-radius: 50%;
margin-left: 6px;
opacity: 0.7;
}
/* --- 右侧表单 --- */
.side-form {
flex: 1;
background: white;
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
background: #fff;
}
.form-wrapper {
width: 100%;
max-width: 360px;
max-width: 340px;
}
.welcome-header {
margin-bottom: 32px;
margin-bottom: 30px;
text-align: center;
}
.welcome-header h2 {
font-size: 24px;
font-size: 26px;
font-weight: 700;
color: #1e293b;
margin-bottom: 8px;
}
.welcome-header p {
color: #64748b;
font-size: 14px;
}
.input {
width: 100%;
margin-bottom: 20px;
}
.login-btn-wrapper {
display: flex;
justify-content: center;
width: 100%;
margin-top: 32px;
}
.register-hint {
margin-top: 24px;
text-align: center;
font-size: 13px;
color: #64748b;
}
.register-hint a {
color: #4f46e5;
color: #2563eb;
font-weight: 600;
text-decoration: none;
}
/* 响应式调整 */
@media (max-width: 768px) {
.login-layout {
flex-direction: column;
}
.side-visual {
width: 100%;
height: 160px;
padding: 24px;
}
.hero-title { font-size: 28px; }
.visual-footer { display: none; }
.side-form { flex: 1; padding: 24px; }
.register-hint a:hover {
text-decoration: underline;
}
.feather-arrow-right{
width: 18px;
/* Response Design */
@media (max-width: 960px) {
.login-card {
flex-direction: column;
width: 90%;
margin: 20px;
height: auto;
border-radius: 16px;
}
.side-visual {
padding: 30px;
min-height: 160px;
background: linear-gradient(135deg, #2563eb 0%, #06b6d4 100%);
}
.hero-title { font-size: 28px; }
.hero-subtitle, .visual-footer { display: none; }
.side-form { padding: 40px 20px; }
}
</style>
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
font-family: 'Inter', system-ui, -apple-system, sans-serif;
background-color: #f0f4f8; /* Fallback */
}
</style>
+355
View File
@@ -0,0 +1,355 @@
<template>
<div class="login-layout">
<div class="login-card">
<div class="side-visual">
<div class="brand-container">
<h1 class="hero-title">Join<br>Us.</h1>
<p class="hero-subtitle">创建一个新账号开启您的沟通之旅</p>
</div>
<div class="visual-footer">
<span>© 2025 IM System</span>
<div class="dots">
<span></span><span></span><span></span>
</div>
</div>
</div>
<div class="side-form">
<div class="form-wrapper">
<div class="welcome-header">
<h2>注册账号</h2>
<p>请填写以下信息以完成注册</p>
</div>
<form @submit.prevent="handleRegister">
<IconInput class="input"
placeholder="请输入用户名" lab="用户名" type="text" icon-name="user" v-model="form.username"/>
<IconInput class="input"
placeholder="请输入昵称" lab="昵称" type="text" icon-name="smile" v-model="form.nickname"/>
<IconInput class="input"
placeholder="请输入密码" lab="密码" type="password" icon-name="lock" v-model="form.password"/>
<IconInput class="input"
placeholder="再次输入密码" lab="确认密码" type="password" icon-name="check-circle" v-model="form.confirmPassword"/>
<div class="login-btn-wrapper">
<MyButton variant="pill-green" class="login-btn" :loading="loading">
立即注册
</MyButton>
</div>
</form>
<div class="register-hint">
已有账号? <router-link to="/auth/login">直接登录</router-link>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { reactive, ref, onMounted } from 'vue'
import { useMessage } from '@/components/messages/useAlert'
import { authService } from '@/services/auth'
import { useRouter } from 'vue-router'
import feather from 'feather-icons'
import IconInput from '@/components/IconInput.vue'
import MyButton from '@/components/MyButton.vue'
import { required, maxLength, minLength, sameAs, helpers } from '@vuelidate/validators'
import useVuelidate from '@vuelidate/core'
const message = useMessage();
const router = useRouter();
const loading = ref(false)
const form = reactive({
username: '',
nickname: '',
password: '',
confirmPassword: ''
})
const rules = {
username:{
required:helpers.withMessage('用户名不能为空', required),
maxLength:helpers.withMessage('用户名最大20字符', maxLength(20)),
minLength:helpers.withMessage('用户名至少3字符', minLength(3))
},
nickname: {
required: helpers.withMessage('昵称不能为空', required),
maxLength: helpers.withMessage('昵称最大20字符', maxLength(20))
},
password:{
required:helpers.withMessage('密码不能为空', required),
minLength:helpers.withMessage('密码至少6字符', minLength(6)),
maxLength:helpers.withMessage('密码最大50字符', maxLength(50))
},
confirmPassword: {
required: helpers.withMessage('请确认密码', required),
sameAs: helpers.withMessage('两次输入的密码不一致', sameAs(ref(form).password)) // This might fail if using reactive directly without computed ref binding for sameAs. Let's fix sameAs usage.
// Actually sameAs(form.password) won't work reactively in Vuelidate 2 sometimes if not ref.
// Just utilize a simpler computed or validator.
}
};
// Vuelidate sameAs expects a generic or a ref.
// Let's use computed for the target or just fix the rule.
// In composition API, use computed(() => form.password)
const rulesWithComputed = {
...rules,
confirmPassword: {
required: helpers.withMessage('请确认密码', required),
sameAs: helpers.withMessage('两次输入的密码不一致', sameAs(ref(form).value?.password || form.password)) // Trickier with reactive.
}
}
// Actually, standard way:
// const rules = computed(() => ({ ... }))
const v$ = useVuelidate(rules, form); // Vuelidate supports reactive object directly.
// The problem is `sameAs` needs a locator.
// Correct usage: sameAs(computed(() => form.password))
const handleRegister = async () => {
// Manual check for confirm password if Vuelidate sameAs is tricky without computed rules
if (form.password !== form.confirmPassword) {
message.error('两次输入的密码不一致');
return;
}
const isFormCorrect = await v$.value.$validate()
if (!isFormCorrect) {
if (v$.value.$errors.length > 0) {
// Skip confirmPassword error if we manually checked it or if it's the only one
message.error(v$.value.$errors[0].$message)
}
return
}
try{
loading.value = true;
// Prepare data (exclude confirmPassword)
const { confirmPassword, ...registerData } = form;
const res = await authService.register(registerData);
if(res.code === 0){
message.success('注册成功,请登录')
router.push('/auth/login')
}else{
message.error(res.message || '注册失败')
}
} catch(e) {
console.error(e);
message.error('注册请求异常');
} finally{
loading.value = false;
}
}
onMounted(() => {
feather.replace()
})
</script>
<style scoped>
/* Green Soft Mesh Gradient Background */
.login-layout {
display: flex;
align-items: center;
justify-content: center;
width: 100vw;
height: 100vh;
min-height: 100vh;
background-color: #f0fdf4; /* Very light green fallback */
background-image:
radial-gradient(at 0% 0%, hsla(150, 100%, 95%, 1) 0, transparent 50%),
radial-gradient(at 50% 0%, hsla(165, 100%, 96%, 1) 0, transparent 50%),
radial-gradient(at 100% 0%, hsla(140, 100%, 96%, 1) 0, transparent 50%);
overflow: hidden;
position: relative;
}
/* Very subtle grid overlay */
.login-layout::before {
content: "";
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background-image: radial-gradient(rgba(0,0,0,0.02) 1px, transparent 1px);
background-size: 20px 20px;
z-index: 0;
}
.login-card {
display: flex;
width: 1000px;
height: 600px;
background: rgba(255, 255, 255, 0.82);
backdrop-filter: blur(30px);
border-radius: 32px;
box-shadow:
0 10px 25px -5px rgba(0, 0, 0, 0.02),
0 40px 100px -20px rgba(0, 0, 0, 0.08);
overflow: hidden;
z-index: 10;
position: relative;
border: 1px solid rgba(255, 255, 255, 0.7);
}
.side-visual {
flex: 1;
/* Green connectivity gradient */
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
position: relative;
display: flex;
flex-direction: column;
justify-content: center;
padding: 60px;
color: white;
overflow: hidden;
}
/* Abstract Decorations */
.side-visual::before {
content: '';
position: absolute;
top: -20%; left: -20%;
width: 400px; height: 400px;
border-radius: 50%;
background: radial-gradient(circle, rgba(255,255,255,0.1) 0%, transparent 60%);
z-index: 1;
}
.side-visual::after {
content: '';
position: absolute;
bottom: -10%; right: -10%;
width: 300px; height: 300px;
border-radius: 50%;
background: radial-gradient(circle, rgba(255,255,255,0.15) 0%, transparent 60%);
z-index: 1;
}
.brand-container {
position: relative;
z-index: 2;
}
.hero-title {
font-size: 44px;
font-weight: 800;
line-height: 1.2;
margin-bottom: 20px;
text-shadow: 0 4px 10px rgba(0,0,0,0.1);
}
.hero-subtitle {
font-size: 16px;
opacity: 0.95;
line-height: 1.6;
max-width: 340px;
font-weight: 400;
}
.visual-footer {
position: absolute;
bottom: 40px;
left: 60px;
right: 60px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
opacity: 0.8;
z-index: 2;
}
.dots span {
display: inline-block;
width: 6px; height: 6px;
background: white;
border-radius: 50%;
margin-left: 6px;
opacity: 0.7;
}
.side-form {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 40px;
background: #fff;
}
.form-wrapper {
width: 100%;
max-width: 340px;
}
.welcome-header {
margin-bottom: 30px;
text-align: center;
}
.welcome-header h2 {
font-size: 26px;
font-weight: 700;
color: #1e293b;
margin-bottom: 8px;
}
.welcome-header p {
color: #64748b;
font-size: 14px;
}
.input {
width: 100%;
margin-bottom: 16px;
}
.login-btn-wrapper {
display: flex;
justify-content: center;
width: 100%;
margin-top: 24px;
}
.register-hint {
margin-top: 24px;
text-align: center;
font-size: 13px;
color: #64748b;
}
.register-hint a {
color: #10b981;
font-weight: 600;
text-decoration: none;
}
.register-hint a:hover {
text-decoration: underline;
}
/* Responsive Design */
@media (max-width: 960px) {
.login-card {
flex-direction: column;
width: 90%;
margin: 20px;
height: auto;
border-radius: 16px;
}
.side-visual {
padding: 30px;
min-height: 160px;
}
.hero-title { font-size: 28px; }
.hero-subtitle, .visual-footer { display: none; }
.side-form { padding: 40px 20px; }
}
</style>
@@ -0,0 +1,315 @@
<template>
<div class="contact-container">
<aside class="contact-list-panel">
<div class="search-section">
<div class="search-box">
<span class="search-icon">🔍</span>
<input v-model="searchQuery" placeholder="搜索联系人" />
</div>
</div>
<div class="scroll-area">
<div class="fixed-entries">
<div class="list-item mini">
<div class="icon-box orange">👤+</div>
<div class="name">新的朋友</div>
</div>
<div class="list-item mini">
<div class="icon-box green">👥</div>
<div class="name">群聊</div>
</div>
<div class="list-item mini">
<div class="icon-box blue">🏷</div>
<div class="name">标签</div>
</div>
</div>
<div class="group-title">我的好友</div>
<div v-for="c in filteredContacts"
:key="c.id"
class="list-item"
:class="{active: activeContactId === c.id}"
@click="activeContactId = c.id">
<img :src="c.avatar" class="avatar-std" />
<div class="info">
<div class="name">{{ c.name }}</div>
</div>
</div>
</div>
</aside>
<main class="profile-main">
<div v-if="currentContact" class="profile-card">
<header class="profile-header">
<div class="text-info">
<h2 class="display-name">
{{ currentContact.name }}
<span :class="['gender-tag', currentContact.gender]">
{{ currentContact.gender === 'm' ? '♂' : '♀' }}
</span>
</h2>
<p class="sub-text">微信号{{ currentContact.wxid }}</p>
<p class="sub-text">地区{{ currentContact.region }}</p>
</div>
<img :src="currentContact.avatar" class="big-avatar" />
</header>
<div class="profile-body">
<div class="info-row">
<span class="label">备注名</span>
<span class="value">{{ currentContact.alias || '未设置' }}</span>
</div>
<div class="info-row">
<span class="label">个性签名</span>
<span class="value">{{ currentContact.signature || '这个家伙很懒,什么都没留下' }}</span>
</div>
<div class="info-row">
<span class="label">来源</span>
<span class="value">通过搜索微信号添加</span>
</div>
</div>
<footer class="profile-footer">
<button class="btn-primary" @click="handleGoToChat">发消息</button>
<button class="btn-ghost">音视频通话</button>
</footer>
</div>
<div v-else class="empty-state">
<div class="empty-logo">👤</div>
<p>请在左侧选择联系人查看详情</p>
</div>
</main>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const searchQuery = ref('')
const activeContactId = ref(null)
// 模拟联系人数据
const contacts = ref([
{ id: 101, name: '南浔', wxid: 'nan_xun_99', region: '浙江 杭州', avatar: 'https://i.pravatar.cc/40?1', gender: 'f', signature: '山有木兮木有枝', alias: '南酱' },
{ id: 102, name: '老张', wxid: 'zhang_boss', region: '广东 深圳', avatar: 'https://i.pravatar.cc/40?10', gender: 'm', signature: '搞钱要紧', alias: '张总' },
{ id: 103, name: 'UI小王', wxid: 'wang_design', region: '上海 黄浦', avatar: 'https://i.pravatar.cc/40?5', gender: 'f', signature: '不改了,真的不改了', alias: '' },
{ id: 104, name: '测试组长', wxid: 'test_pro', region: '北京', avatar: 'https://i.pravatar.cc/40?8', gender: 'm', signature: 'Bug 哪里跑', alias: '铁面人' }
])
const filteredContacts = computed(() => {
return contacts.value.filter(c =>
c.name.includes(searchQuery.value) || c.wxid.includes(searchQuery.value)
)
})
const currentContact = computed(() => {
return contacts.value.find(c => c.id === activeContactId.value)
})
// 发送事件给父组件(用于切换回聊天Tab并打开会话)
const emit = defineEmits(['start-chat'])
function handleGoToChat() {
if (currentContact.value) {
emit('start-chat', { ...currentContact.value })
}
}
</script>
<style scoped>
.contact-container {
display: flex;
width: 100%; /* 继承父组件的高度和宽度 */
height: 100%;
background: #fff;
}
/* --- 左侧列表栏 --- */
.contact-list-panel {
width: 250px;
background: #eee;
border-right: 1px solid #d6d6d6;
display: flex;
flex-direction: column;
flex-shrink: 0;
}
.search-section {
padding: 20px 12px 10px 12px;
}
.search-box {
display: flex;
align-items: center;
background: #dbdbdb;
padding: 5px 8px;
border-radius: 4px;
gap: 6px;
}
.search-box input {
background: transparent;
border: none;
outline: none;
font-size: 12px;
width: 100%;
}
.scroll-area {
flex: 1;
overflow-y: auto;
}
.group-title {
padding: 8px 12px;
font-size: 12px;
color: #999;
}
.list-item {
display: flex;
padding: 10px 12px;
gap: 12px;
align-items: center;
cursor: pointer;
transition: background 0.2s;
}
.list-item:hover { background: #e2e2e2; }
.list-item.active { background: #c6c6c6; }
.avatar-std {
width: 36px;
height: 36px;
border-radius: 4px;
object-fit: cover;
}
.icon-box {
width: 36px;
height: 36px;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 18px;
}
.icon-box.orange { background: #faad14; }
.icon-box.green { background: #52c41a; }
.icon-box.blue { background: #1890ff; }
/* --- 右侧名片区 --- */
.profile-main {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
background: #f5f5f5;
min-width: 0;
}
.profile-card {
width: 420px;
background: transparent;
padding: 20px;
}
.profile-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding-bottom: 30px;
border-bottom: 1px solid #e7e7e7;
margin-bottom: 30px;
}
.display-name {
font-size: 24px;
color: #000;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 8px;
}
.gender-tag { font-size: 16px; }
.gender-tag.m { color: #1890ff; }
.gender-tag.f { color: #ff4d4f; }
.sub-text {
font-size: 13px;
color: #888;
margin: 3px 0;
}
.big-avatar {
width: 70px;
height: 70px;
border-radius: 6px;
object-fit: cover;
}
.profile-body {
margin-bottom: 40px;
}
.info-row {
display: flex;
margin-bottom: 15px;
font-size: 14px;
}
.info-row .label {
width: 80px;
color: #999;
}
.info-row .value {
color: #333;
flex: 1;
}
.profile-footer {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.btn-primary {
width: 160px;
padding: 10px;
background: #07c160;
color: #fff;
border: none;
border-radius: 4px;
font-weight: bold;
cursor: pointer;
}
.btn-ghost {
width: 160px;
padding: 10px;
background: #fff;
border: 1px solid #e0e0e0;
color: #333;
border-radius: 4px;
cursor: pointer;
}
.btn-primary:hover, .btn-ghost:hover {
opacity: 0.8;
}
.empty-state {
text-align: center;
color: #ccc;
}
.empty-logo {
font-size: 80px;
margin-bottom: 10px;
opacity: 0.2;
}
</style>
@@ -0,0 +1 @@
<template></template>
@@ -0,0 +1,234 @@
<template>
<section class="chat-panel">
<header class="chat-header">
<span class="title">{{ currentSession?.name || '未选择会话' }}</span>
<div class="actions">
<button @click="startCall('video')">📹</button>
<button @click="startCall('voice')">📞</button>
</div>
</header>
<div class="chat-history" ref="historyRef">
<div v-for="m in activeMessages" :key="m.id" :class="['msg', m.mine ? 'mine' : 'other']">
<img :src="m.mine ? (myInfo?.avatar || defaultAvatar) : currentSession?.avatar" class="avatar-chat" />
<div class="msg-content">
<div class="bubble">
<div v-if="m.type === 'text'">{{ m.content }}</div>
<div v-else-if="m.type === 'emoji'" class="emoji-msg">{{ m.content }}</div>
</div>
<span class="msg-time">{{ m.time }}</span>
</div>
</div>
</div>
<footer class="chat-footer">
<div class="toolbar">
<button @click="toggleEmoji">😀</button>
<label class="tool-btn">
📁 <input type="file" hidden @change="handleFile" />
</label>
</div>
<textarea
v-model="input"
placeholder="请输入消息..."
@keydown.enter.exact.prevent="sendText"
></textarea>
<div class="send-row">
<button class="send-btn" :disabled="!input.trim()" @click="sendText">发送(S)</button>
</div>
</footer>
</section>
</template>
<script setup>
import { ref, computed, nextTick, onMounted } from 'vue';
import { useAuthStore } from '@/stores/auth';
import defaultAvatar from '@/assets/default_avatar.png';
const props = defineProps({
id:{
type: String,
required:true
}
})
const input = ref(''); // 输入框内容
const historyRef = ref(null); // 绑定 DOM 用于滚动
const myInfo = useAuthStore().userInfo;
// --- 模拟会话数据 (实际开发中应从后端或 store 获取) ---
const sessions = ref([
{ id: 1, name: '南浔', avatar: 'https://i.pravatar.cc/40?1', last: '' },
{ id: 2, name: '技术群', avatar: 'https://i.pravatar.cc/40?2', last: '' }
]);
// --- 消息数据 ---
const messages = ref({
1: [
{ id: 1, type: 'text', content: '在干嘛?', mine: false, time: '14:00' },
{ id: 2, type: 'text', content: '在写代码呢,帮你调样式', mine: true, time: '14:02' }
],
2: []
});
// --- 计算属性 ---
const currentSession = computed(() => sessions.value.find(s => s.id == props.id));
const activeMessages = computed(() => messages.value[props.id] || []);
// --- 功能函数 ---
// 自动滚动到底部
const scrollToBottom = async () => {
await nextTick(); // 等待 DOM 更新后执行
if (historyRef.value) {
historyRef.value.scrollTop = historyRef.value.scrollHeight;
}
};
// 发送文本
function sendText() {
if (!input.value.trim()) return;
const now = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
const newMessage = {
id: Date.now(),
type: 'text',
content: input.value,
mine: true,
time: now
};
// 插入消息
if (!messages.value[props.id]) {
messages.value[props.id] = [];
}
messages.value[props.id].push(newMessage);
// 同步更新会话列表最后一条摘要
if (currentSession.value) {
currentSession.value.last = input.value;
}
input.value = ''; // 清空输入框
scrollToBottom(); // 滚动
}
// 通话模拟
function startCall(type) {
console.log(`发起${type === 'video' ? '视频' : '语音'}通话`);
}
// 文件处理模拟
function handleFile(e) {
const file = e.target.files[0];
if (file) console.log('选中文件:', file.name);
}
function toggleEmoji() {
console.log('打开表情面板');
}
// 初始化时滚动到底部
onMounted(() => {
scrollToBottom();
});
</script>
<style scoped>
/* 核心布局修复 */
.chat-panel {
display: flex;
flex-direction: column;
height: 100%; /* 确保占满父容器 */
background: #f5f5f5;
}
.chat-header {
height: 60px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px;
background: #f5f5f5;
border-bottom: 1px solid #e0e0e0;
}
/* 历史区域:自动撑开并处理滚动 */
.chat-history {
flex: 1;
overflow-y: auto;
padding: 20px 30px;
}
.chat-footer {
height: 160px;
flex-shrink: 0;
background: #fff;
border-top: 1px solid #e0e0e0;
padding: 10px 20px;
display: flex;
flex-direction: column;
}
/* 消息对齐逻辑 */
.msg {
display: flex;
margin-bottom: 24px;
gap: 12px;
}
.msg.mine {
flex-direction: row-reverse;
}
.msg.mine .msg-content {
align-items: flex-end;
}
.bubble {
padding: 9px 14px;
border-radius: 6px;
font-size: 14px;
max-width: 450px;
word-break: break-all;
background: #fff;
}
.mine .bubble {
background: #95ec69;
}
.avatar-chat {
width: 38px;
height: 38px;
border-radius: 4px;
flex-shrink: 0;
}
.msg-time {
font-size: 11px;
color: #b2b2b2;
margin-top: 4px;
}
textarea {
flex: 1;
border: none;
outline: none;
resize: none;
font-family: inherit;
font-size: 14px;
}
.send-row {
display: flex;
justify-content: flex-end;
}
.send-btn {
background: #f5f5f5;
color: #07c160;
border: 1px solid #e0e0e0;
padding: 5px 20px;
border-radius: 4px;
cursor: pointer;
}
</style>
@@ -0,0 +1,245 @@
<template>
<div id="MsgList">
<aside class="list-panel">
<div class="search-section">
<div class="search-box">
<span class="search-icon">🔍</span>
<input v-model="searchQuery" placeholder="搜索" />
</div>
</div>
<div class="scroll-area">
<div v-for="s in filteredSessions" :key="s.id"
class="list-item" :class="{active: activeId === s.id}" @click="selectSession(s)">
<div class="avatar-container">
<img :src="s.avatar" class="avatar-std" />
<span v-if="s.unread > 0" class="unread-badge">{{ s.unread }}</span>
</div>
<div class="info">
<div class="name-row">
<span class="name">{{ s.name }}</span>
<span class="time">{{ s.lastTime }}</span>
</div>
<div class="last-msg">{{ s.last }}</div>
</div>
</div>
</div>
</aside>
<RouterView></RouterView>
</div>
</template>
<script setup>
import router from '@/router'
import { ref, computed, nextTick } from 'vue'
const searchQuery = ref('')
const input = ref('')
const historyRef = ref(null)
const activeId = ref(1)
const sessions = ref([
{ id: 1, name: '南浔', last: '在写代码呢', lastTime: '14:20', avatar: 'https://i.pravatar.cc/40?1', unread: 2 },
{ id: 2, name: '技术群', last: '部署好了', lastTime: '12:05', avatar: 'https://i.pravatar.cc/40?2', unread: 0 }
])
const filteredSessions = computed(() => sessions.value.filter(s => s.name.includes(searchQuery.value)))
const currentSession = computed(() => sessions.value.find(s => s.id === activeId.value))
function selectSession(s) {
activeId.value = s.id
s.unread = 0
router.push(`/messages/chat/${s.id}`)
scrollToBottom()
}
const scrollToBottom = async () => {
await nextTick()
if (historyRef.value) historyRef.value.scrollTop = historyRef.value.scrollHeight
}
</script>
<style scoped>
#MsgList {
display: flex;
flex: 1;
}
/* 2. 列表区修复 */
.list-panel {
width: 250px;
flex-shrink: 0;
background: #eee;
border-right: 1px solid #d6d6d6;
display: flex;
flex-direction: column;
}
/* 修复:搜索框美化 */
.search-section {
padding: 20px 12px 10px 12px;
}
.search-box {
display: flex;
align-items: center;
background: #dbdbdb;
padding: 4px 8px;
border-radius: 4px;
gap: 5px;
}
.search-icon { font-size: 12px; color: #666; }
.search-box input {
background: transparent;
border: none;
outline: none;
font-size: 12px;
width: 100%;
}
.scroll-area { flex: 1; overflow-y: auto; }
/* 3. 聊天主面板修复 */
.chat-panel {
flex: 1;
display: flex;
flex-direction: column;
background: #f5f5f5;
min-width: 0;
}
.chat-header {
height: 60px;
padding: 0 20px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px solid #e0e0e0;
background: #f5f5f5;
}
.chat-history {
flex: 1;
overflow-y: auto;
padding: 20px 30px;
}
/* 修复:本人消息右侧对齐逻辑 */
.msg {
display: flex;
margin-bottom: 24px;
gap: 12px;
}
/* 别人发的:默认靠左 */
.msg.other { flex-direction: row; }
/* 本人发的:翻转排列方向,靠右显示 */
.msg.mine {
flex-direction: row-reverse;
}
.msg-content {
display: flex;
flex-direction: column;
max-width: 70%;
}
/* 修复:本人消息文字和时间戳也需要右对齐 */
.msg.mine .msg-content {
align-items: flex-end;
}
.bubble {
padding: 9px 14px;
border-radius: 6px;
font-size: 14px;
line-height: 1.6;
word-break: break-all;
position: relative;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
.other .bubble { background: #fff; color: #333; }
.mine .bubble { background: #95ec69; color: #000; }
.msg-time {
font-size: 11px;
color: #b2b2b2;
margin-top: 4px;
}
/* 头像样式统一 */
.avatar-std { width: 40px; height: 40px; border-radius: 4px; object-fit: cover; }
.avatar-chat { width: 38px; height: 38px; border-radius: 4px; object-fit: cover; flex-shrink: 0; }
/* 未读气泡 */
.avatar-container { position: relative; }
.unread-badge {
position: absolute;
top: -5px;
right: -5px;
background: #ff4d4f;
color: #fff;
font-size: 10px;
padding: 0 4px;
min-width: 16px;
height: 16px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid #fff;
}
/* 输入框区域修复 */
.chat-footer {
height: 160px;
background: #fff;
border-top: 1px solid #e0e0e0;
padding: 10px 20px;
display: flex;
flex-direction: column;
}
.toolbar { display: flex; gap: 12px; margin-bottom: 5px; font-size: 20px; color: #666; }
.toolbar button { background: none; border: none; cursor: pointer; opacity: 0.7; }
textarea {
flex: 1;
border: none;
outline: none;
resize: none;
font-family: inherit;
font-size: 14px;
padding: 5px 0;
}
.send-row { display: flex; justify-content: flex-end; }
.send-btn {
background: #f5f5f5;
color: #07c160;
border: 1px solid #e0e0e0;
padding: 5px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.send-btn:hover { background: #e2e2e2; }
/* 列表美化 */
.list-item { display: flex; padding: 12px; gap: 12px; cursor: pointer; }
.list-item.active { background: #c6c6c6; }
.list-item:hover:not(.active) { background: #ddd; }
.info { flex: 1; overflow: hidden; }
.name-row { display: flex; justify-content: space-between; align-items: center; }
.name { font-size: 14px; font-weight: 500; }
.time { font-size: 11px; color: #999; }
.last-msg { font-size: 12px; color: #888; margin-top: 4px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.nav-item { font-size: 24px; cursor: pointer; opacity: 0.5; }
.nav-item.active { opacity: 1; }
</style>
@@ -0,0 +1 @@
<template></template>