我还不知道这个叫什么

本贴最后更新于 983 天前,其中的信息可能已经斗转星移

ng-alain

转载请注明原出处,勘误请发送电子邮件

1.什么是 ng-alain?

ng-alain 是来自中国作者 卡色 开源出的一个企业级中后台前端/设计解决方案脚手架,秉承 Ant Design 的设计价值观,让 Angular 快速落地于企业生产实践中的一个高集成性框架。

其中提供了非常丰富的功能诸如

  • Dynamic Form
  • Antv/Echarts
  • Acl
  • Auth
  • Theme
  • Cache
  • Mock
  • Util (DeepCopy/TreeToArray 等等)

假如你对 Angular 有一些使用基础,那么使用起来会相当的得心应手。

假如你对 Angular 只有一些略微了解,那么也可以通过 ng-alain 提供的 CLI 快速的建立起一个可运行 Project 用来快速上手。

2.ng-alain 解析

  • 生成一个 ng-alain 项目首先需要安装 AngularCli
  • npm install @angular/cli@12.2.0
  • 其次使用 ng new my-project --style less --routing 创建名为 my-project 的纯 Angular 项目
  • 然后使用 ng-alainCLI 生成项目
  • ng add ng-alain
  • 最后使用 npm run start 启动项目
  • 或者直接前往预览地址

3.ng-alain 项目

“没有比浏览 angular.json 配置文件更能快速了解一个 angular 项目的方法了” -詹姆斯高斯林

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "ng-alain": {
      "projectType": "application",
      "root": "",
      "sourceRoot": "src",
      "prefix": "app",
      "schematics": {
        "@schematics/angular:component": {
          "style": "less"
        },
        "@schematics/angular:application": {
          "strict": true
        }
      },
      "architect": {
        "build": {
          "builder": "@angular-devkit/build-angular:browser",
          "options": {
            "outputPath": "dist",
            "index": "src/index.html",
            "main": "src/main.ts",
            "tsConfig": "tsconfig.app.json",
            "polyfills": "src/polyfills.ts",
            "assets": ["src/assets", "src/favicon.ico"],
            "styles": ["src/styles.less"],
            "scripts": [],
            "allowedCommonJsDependencies": ["ajv", "ajv-formats"]
          },
          "configurations": {
            "production": {
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ],
              "outputHashing": "all",
              "budgets": [
                {
                  "type": "initial",
                  "maximumWarning": "2mb",
                  "maximumError": "6mb"
                },
                {
                  "type": "anyComponentStyle",
                  "maximumWarning": "6kb",
                  "maximumError": "10kb"
                }
              ]
            },
            "development": {
              "buildOptimizer": false,
              "optimization": false,
              "vendorChunk": true,
              "extractLicenses": false,
              "sourceMap": true,
              "namedChunks": true
            }
          },
          "defaultConfiguration": "production"
        },
        "serve": {
          "builder": "@angular-devkit/build-angular:dev-server",
          "options": {
            "browserTarget": "ng-alain:build",
            "proxyConfig": "proxy.conf.json"
          },
          "configurations": {
            "production": {
              "browserTarget": "ng-alain:build:production"
            },
            "development": {
              "browserTarget": "ng-alain:build:development"
            }
          },
          "defaultConfiguration": "development"
        },
        "extract-i18n": {
          "builder": "@angular-devkit/build-angular:extract-i18n",
          "options": {
            "browserTarget": "ng-alain:build"
          }
        },
      ....

我们可以清晰的从 angular.json 中看出

  • build 部分
  • projects 中仅仅含有一个类型为 application 的项目 ng-alain
  • builder 编译器使用了默认的 @angular-devkit/build-angular:browser,当然还有许多其他的编译器,可以自行搜索使用。
  • builder 中的 options 指定了一些入口文件
  • 引入了 CommanJS: ajv,ajv-formatsajvJSON Schema 的一种验证器
  • build 中的 configuration 会使用 env.prod.ts 替换 env.ts
  • outputHashing 打包文件带有哈希值
  • serve 部分
  • serve 对应的命令为 ng serve,使用的 builder@angular-devkit/build-angular:dev-server,我们可以在 @angular-devikt 找到这个包,有一些配置参数和说明,刚刚 build 时使用的 browser,还有 app-shell 呦

找到了入口文件 index.html

<app-root></app-root>
  <div class="preloader">
    <div class="cs-loader">
      <div class="cs-loader-inner">
        <label> ●</label>
        <label> ●</label>
        <label> ●</label>
        <label> ●</label>
        <label> ●</label>
        <label> ●</label>
      </div>
    </div>
  </div>

比较重要的一部分

  • <app-root> ,这里是对 app.component.ts 的直接引用
  • 下面的 div 中使用 classcss 做了一个加载动画,可以理解成遮罩层,利用最先渲染 index.html,使用 css 将整个 webapp 遮盖住
  • index.html 结束

找到了入口文件 main.ts

preloaderFinished();

第九行调用了 preloaderFinished,这个函数是在 alain 的另一个基础项目 delon 中以基础包的形式出现的

export function preloaderFinished(): void {
  const body = document.querySelector('body')!;
  const preloader = document.querySelector('.preloader')!;

  body.style.overflow = 'hidden';

  function remove(): void {
    // preloader value null when running --hmr
    if (!preloader) return;
    preloader.addEventListener('transitionend', () => {
      preloader.className = 'preloader-hidden';
    });

    preloader.className += ' preloader-hidden-add preloader-hidden-add-active';
  }

  (window as NzSafeAny).appBootstrap = () => {
    setTimeout(() => {
      remove();
      body.style.overflow = '';
    }, 100);
  };
}
  • 写了之前说的遮罩层如何删除

  • 赋予 window 对象一个 appBootstrap 函数,用来进行调用 remove 删除掉 index.html 中的遮罩层

    if (environment.production) {
    enableProdMode();
    }

如果环境为 prod 模式那么开启 Prod 模式

platformBrowserDynamic()
  .bootstrapModule(AppModule, {
    defaultEncapsulation: ViewEncapsulation.Emulated,
    preserveWhitespaces: false
  })
  .then(res => {
    const win = window as NzSafeAny;
    if (win && win.appBootstrap) {
      win.appBootstrap();
    }
    return res;
  })
  .catch(err => console.error(err));

上面为 main.ts 中最后的内容

  • 引导 AppModule 启动
  • 启动完毕后调用 win 中存下的 remove 函数,删除遮罩层

@import '~@delon/theme/system/index';
@import '~@delon/abc/index';
@import '~@delon/chart/index';
@import '~@delon/theme/layout-default/style/index';
@import '~@delon/theme/layout-blank/style/index';

@import './styles/index';
@import './styles/theme';

style.less 中引入了一些 less 变量,以供全局 css 使用,这些 less 变量也是 @delon/theme 包中出现的,后续可能会看到


至此,除了 assets 静态资源目录,其余从 angular.json 找到的入口的启动过程梳理完毕,分别

  • 提供了模版
  • 提供了静态资源
  • 提供了 JS 启动模块
  • 提供了全局 CSS

3.1 项目配置

接下来看被 bootstrapAppModule

const LANG = {
  abbr: 'zh',
  ng: ngLang,
  zorro: zorroLang,
  date: dateLang,
  delon: delonLang
};
registerLocaleData(LANG.ng, LANG.abbr);
const LANG_PROVIDES = [
  { provide: LOCALE_ID, useValue: LANG.abbr },
  { provide: NZ_I18N, useValue: LANG.zorro },
  { provide: NZ_DATE_LOCALE, useValue: LANG.date },
  { provide: DELON_LOCALE, useValue: LANG.delon }
];

前面主要是本地化/i18n 的一些配置,通过 provide 键值对的方式注入到包里,使得第三方库使用 useValue 提供的值

const I18NSERVICE_PROVIDES = [{ provide: ALAIN_I18N_TOKEN, useClass: I18NService, multi: false }];

当然除了使用 useValue 的方式,还可以使用 useClass

export const ALAIN_I18N_TOKEN = new InjectionToken<AlainI18NService>('alainI18nToken', {
  providedIn: 'root',
  factory: () => new AlainI18NServiceFake()
});

上文的 ALAIN_I18N_TOKEN@delon/theme 中,接收一个 AlainI18nService类型 的 class

I18nService 继承自 AlainI18nBaseService,而 AlainI18nBaseService 又实现了 AlainI18NService 接口,所以类型匹配,这样我们就可以把自己的 class 传入第三方包中,第三方包注入 class 时调用的 function 就可以来自外部实际项目了


const GLOBAL_THIRD_MODULES: Array<Type<any>> = [BidiModule]

引入了一个左右布局切换的 Module,在 cdk 里,cdk@angular 提供的一个工具包


const INTERCEPTOR_PROVIDES = [
  { provide: HTTP_INTERCEPTORS, useClass: SimpleInterceptor, multi: true },
  { provide: HTTP_INTERCEPTORS, useClass: DefaultInterceptor, multi: true }
];

再往下就是这两个拦截器

  • Simple: 验证 token
  • Default: 处理错误信息

export function StartupServiceFactory(startupService: StartupService): () => Observable<void> {
  return () => startupService.load();
}
const APPINIT_PROVIDES = [
  StartupService,
  {
    provide: APP_INITIALIZER,
    useFactory: StartupServiceFactory,
    deps: [StartupService],
    multi: true
  }
];

重点,这里我称为 实际引导项目启动 的一个函数,这里作为整个项目声明周期的 初始化 周期,使用了 Angular 内置的 provide: APP_INITIALIZER 并返回了一个 Promise 函数 load

@Injectable()
export class StartupService {
  constructor(
    iconSrv: NzIconService,
    private menuService: MenuService,
    @Inject(ALAIN_I18N_TOKEN) private i18n: I18NService,
    private settingService: SettingsService,
    private aclService: ACLService,
    private titleService: TitleService,
    private httpClient: HttpClient
  ) {
    iconSrv.addIcon(...ICONS_AUTO, ...ICONS);
  }

  load(): Observable<void> {
    const defaultLang = this.i18n.defaultLang;
    return zip(this.i18n.loadLangData(defaultLang), this.httpClient.get('assets/tmp/app-data.json')).pipe(
      // 接收其他拦截器后产生的异常消息
      catchError(res => {
        console.warn(`StartupService.load: Network request failed`, res);
        return [];
      }),
      map(([langData, appData]: [Record<string, string>, NzSafeAny]) => {
        // setting language data
        this.i18n.use(defaultLang, langData);
        // 应用信息:包括站点名、描述、年份
        this.settingService.setApp(appData.app);
        // 用户信息:包括姓名、头像、邮箱地址
        this.settingService.setUser(appData.user);
        // ACL:设置权限为全量
        this.aclService.setFull(true);
        // 初始化菜单
        this.menuService.add(appData.menu);
        // 设置页面标题的后缀
        this.titleService.default = '';
        this.titleService.suffix = appData.app.name;
      })
    );
  }
}

以上代码初始化了整体项目必备的数据


@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    HttpClientModule,
    GlobalConfigModule.forRoot(),
    CoreModule,
    SharedModule,
    LayoutModule,
    RoutesModule,
    STWidgetModule,
    NzNotificationModule,
    ...GLOBAL_THIRD_MODULES,
    ...FORM_MODULES
  ],
  providers: [...LANG_PROVIDES, ...INTERCEPTOR_PROVIDES, ...I18NSERVICE_PROVIDES, ...APPINIT_PROVIDES],
  bootstrap: [AppComponent]
})
export class AppModule {}

回到 app.module 中,imports 一些模块,providers 一些服务,

其中对于整体项目比较重要的模块有

  • GlobalConfigModule.forRoot() 全局配置模块,主要配置 alain 中的一些组件功能/认证/等各方面的内容
  • LayoutModule 在用户端的一个布局模块
  • SharedModule 共享模块
  • RoutesModule 总体路由模块,比如 /app/v1 路径,要渲染哪个模块的那个组件,就是这个模块决定的,在这个项目里,也称为 业务模块
  • CoreModule core 模块,刚才提到的拦截器和 i18n 和 startup 就在这个模块中

const alainConfig: AlainConfig = {
  st: { modal: { size: 'lg' } },
  pageHeader: { homeI18n: 'home' },
  lodop: {
    license: `A59B099A586B3851E0F0D7FDBF37B603`,
    licenseA: `C94CEE276DB2187AE6B65D56B3FC2848`
  },
  auth: { login_url: '/passport/login' }
};

const alainModules: any[] = [AlainThemeModule.forRoot(), DelonACLModule.forRoot()];
const alainProvides = [{ provide: ALAIN_CONFIG, useValue: alainConfig }];

const ngZorroConfig: NzConfig = {};

const zorroProvides = [{ provide: NZ_CONFIG, useValue: ngZorroConfig }];

// #endregion

@NgModule({
  imports: [...alainModules, ...(environment.modules || [])]
})
export class GlobalConfigModule {
  constructor(@Optional() @SkipSelf() parentModule: GlobalConfigModule) {
    throwIfAlreadyLoaded(parentModule, 'GlobalConfigModule');
  }

  static forRoot(): ModuleWithProviders<GlobalConfigModule> {
    return {
      ngModule: GlobalConfigModule,
      providers: [...alainProvides, ...zorroProvides]
    };
  }
}

配置了一些基于 AlainConfig 接口的配置,配置了一些 zorro 的配置,然后再次通过 providers 传入到 @delon 包中


@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    RouterModule,
    ReactiveFormsModule,
    AlainThemeModule.forChild(),
    DelonACLModule,
    DelonFormModule,
    ...SHARED_DELON_MODULES,
    ...SHARED_ZORRO_MODULES,
    // third libs
    ...THIRDMODULES
  ],
  declarations: [
    // your components
    ...COMPONENTS,
    ...DIRECTIVES
  ],
  exports: [
    CommonModule,
    FormsModule,
    ReactiveFormsModule,
    RouterModule,
    AlainThemeModule,
    DelonACLModule,
    DelonFormModule,
    ...SHARED_DELON_MODULES,
    ...SHARED_ZORRO_MODULES,
    // third libs
    ...THIRDMODULES,
    // your components
    ...COMPONENTS,
    ...DIRECTIVES
  ]
})

shared 模块存在的意义就是,统一管理每个 routes 模块所依赖的其他模块,新增业务模块时,只需要引入 SharedModule 就可以将其中的一大坨模块直接引入到你建立的模块中,至于文件体积和过多引用不用担心,打包时编译器会做优化的

export const SHARED_ZORRO_MODULES = [
  NzButtonModule,
  NzMessageModule,
  NzDropDownModule,
  NzGridModule,
  NzCheckboxModule,
  NzToolTipModule,
  NzPopoverModule,
  NzSelectModule,
  NzIconModule,
  NzBadgeModule,
  NzAlertModule,
  NzModalModule,
  NzTableModule,
  NzDrawerModule,
  NzTabsModule,
  NzInputModule,
  NzDatePickerModule,
  NzTimePickerModule,
  NzTagModule,
  NzInputNumberModule,
  NzBreadCrumbModule,
  NzListModule,
  NzSwitchModule,
  NzRadioModule,
  NzFormModule,
  NzAvatarModule,
  NzSpinModule,
  NzCardModule,
  NzDividerModule,
  NzProgressModule,
  NzPopconfirmModule,
  NzUploadModule
];

zorro 模块,将 zorro 中所有的模块封装起来,为了之后全量引入,不再做按需引入


接下来是布局,在 app.component 中的模版只有一个 router-outlet,这里直接为后续所有组件渲染放出了位置接口,所有其他的组件会渲染到 HTML 中的这个位置

const routes: Routes = [
  {
    path: '',
    component: LayoutBasicComponent,
    canActivate: [SimpleGuard],
    canActivateChild: [SimpleGuard],
    data: {},
    children: [
      { path: '', redirectTo: 'dashboard', pathMatch: 'full' },
      { path: 'dashboard', loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule) },
      {
        path: 'widgets',
        loadChildren: () => import('./widgets/widgets.module').then(m => m.WidgetsModule)
      },
      { path: 'style', loadChildren: () => import('./style/style.module').then(m => m.StyleModule) },
      { path: 'delon', loadChildren: () => import('./delon/delon.module').then(m => m.DelonModule) },
      { path: 'extras', loadChildren: () => import('./extras/extras.module').then(m => m.ExtrasModule) },
      { path: 'pro', loadChildren: () => import('./pro/pro.module').then(m => m.ProModule) }
    ]
  },
  // Blak Layout 空白布局
  {
    path: 'data-v',
    component: LayoutBlankComponent,
    children: [{ path: '', loadChildren: () => import('./data-v/data-v.module').then(m => m.DataVModule) }]
  },
  // passport
  { path: '', loadChildren: () => import('./passport/passport.module').then(m => m.PassportModule) },
  { path: 'exception', loadChildren: () => import('./exception/exception.module').then(m => m.ExceptionModule) },
  { path: '**', redirectTo: 'exception/404' }
];

然后看路由表,最外层的父路由中,指定了渲染的 component: LayoutBasicComponent,这个组件就是定义在 LayoutModule 中的

....
<ng-template #asideUserTpl>
        <div nz-dropdown nzTrigger="click" [nzDropdownMenu]="userMenu" class="alain-default__aside-user">
          <nz-avatar class="alain-default__aside-user-avatar" [nzSrc]="user.avatar"></nz-avatar>
          <div class="alain-default__aside-user-info">
            <strong>{{ user.name }}</strong>
            <p class="mb0">{{ user.email }}</p>
          </div>
        </div>
        <nz-dropdown-menu #userMenu="nzDropdownMenu">
          <ul nz-menu>
            <li nz-menu-item routerLink="/pro/account/center">{{ 'menu.account.center' | i18n }}</li>
            <li nz-menu-item routerLink="/pro/account/settings">{{ 'menu.account.settings' | i18n }}</li>
          </ul>
        </nz-dropdown-menu>
      </ng-template>


      <ng-template #contentTpl>
        <router-outlet></router-outlet>
      </ng-template>

.....

拿出一段 LayoutBasicComponent 的 HTML,其中又有一个 <router-outlet>

这里是为子路由对应的组件放出的渲染位置

假设我的访问路径为:/dashboard/

  • 首先会匹配到路由表中: '' 空路由,将 LayoutBasicComponent 渲染在 app.component 中的 router-outlet 上,又由于,index.html 在删除 preloader-div 后只剩下 app-root,所以 LayoutBasicComponent 占了整张页面
  • 其次匹配 dashboard 路由,这里 dashboard 指向的组件为 dashboard/dashboard.module 模块中的 DashboardV1Component,所以 DashboardV1Component 就会渲染在 已经渲染完 LayoutBasicComponent 组件中的 router-outlet 位置
  • 这样通过路由和子路由加上 router-outlet 完成了整体布局
  • PS: loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule) 这种加载方法被称为懒加载,在 DashboardModule 中还有一个子路由表我这里没贴出来,感兴趣可以自己去看一下

至此,大体上过了个差不多,你可以看到虽然 alain 是一个项目,但它又不仅仅是一个项目,下面我们分析一下 alain 项目的组成

4.alain 项目组成

直接看 package.json 吧,就不对照模块了,刚才涉及到的代码有很多都来自以下这些 npm

"ng-zorro-antd": "^12.0.1", 阿里ant组件库

  "@delon/abc": "^12.1.0",  delon包,也是最核心的包
  "@delon/acl": "^12.1.0",
  "@delon/auth": "^12.1.0",
  "@delon/cache": "^12.1.0",
  "@delon/chart": "^12.1.0",
  "@delon/form": "^12.1.0",
  "@delon/mock": "^12.1.0",
  "@delon/theme": "^12.1.0",
  "@delon/util": "^12.1.0",

  "ngx-tinymce": "^12.0.0", 两个编辑器包
   "ngx-ueditor": "^12.0.0",
   "screenfull": "^5.1.0",

   "ajv": "^8.6.2" jsonSchematicValidate包

  "ng-alain": "^12.1.0",  脚手架包

    "ng-alain-plugin-theme": "^12.0.0",  主题生成插件
    "ng-alain-sts": "^0.0.1",  构建 Swagger API 转换为列表、编辑页的命令行工具

5.delon

delon 的项目地址位于: https://github.com/ng-alain/delon

按照惯例先看 angular.json

"delon": {
      "root": "packages",
      "projectType": "library",
      "prefix": "",
      "architect": {
        "lint": {
          "builder": "@angular-eslint/builder:lint",
          "options": {
            "fix": true,
            "lintFilePatterns": [
              "packages/**/*.ts",
              "packages/**/*.html"
            ]
          }
        },
        "test": {
          "builder": "@angular-devkit/build-angular:karma",
          "options": {
            "main": "packages/test.ts",
            "karmaConfig": "packages/karma.conf.js",
            "polyfills": "packages/polyfills.ts",
            "tsConfig": "packages/tsconfig.spec.json",
            "scripts": ["node_modules/@antv/g2/dist/g2.min.js", "node_modules/@antv/data-set/dist/data-set.js"],
            "codeCoverageExclude": ["schematics/**", "packages/testing/**"]
          }
        }
      }
    }

可以看到这里 delon 的跟路径为 packages,应用类行为 package,并不是刚才的 application,说明它并不是一个应用,而是一个最终为 npm 包 形式的项目,所以自然没有 serve 启动项的配置

但是我们找到了入口 root 配置项配置的是 packages

5.1 delon/packages

5.2 delon build

5.3 delon schematics

5.4 sts

5.5 plugin-theme

plugin-theme 主要是用来生成主题配置的一个插件

plugin-theme 的项目在 https://github.com/ng-alain/plugin-theme.git

const cli = meow({
  help: `
  Usage
    ng-alain-plugin-theme
  Example
    ng-alain-plugin-theme -t=themeCss -c=ng-alain.json
  Options
    -t, --type    Can be set 'themeCss', 'colorLess'
    -c, --config  A filepath of NG-ALAIN config script
    -d, --debug   Debug mode
  `,
  flags: {
    type: {
      type: 'string',
      default: 'themeCss',
      alias: 't',
    },
    config: {
      type: 'string',
      default: 'ng-alain.json',
      alias: 'c',
    },
    debug: {
      type: 'boolean',
      default: false,
      alias: 'd',
    },
  },
});

这里使用了基于 nodejs 的 meow 包,构建了一个 CLI 工具,所谓 CLI 即为 Command Line Interface,其实就是命令行,这里给出了三个配置项

  • -t 文件类型,less/css
  • -c 自定义主题 config,详情见 ng-alalin 项目内的 ng-alain.json,也可以在主题切换中找到使用方式
  • -d 是否开启 debug 模式

let config: { theme: Config; colorLess: Config; [key: string]: Config };

传入 JSON 的格式

[key:string]:Config

意思是接口的任何属性,都要是 Config 格式

try {
  const configFile = resolve(process.cwd(), cli.flags.config);
  if (existsSync(configFile)) {
    config = getJSON(configFile);
  } else {
    console.error(`The config file '${cli.flags.config}' will not found`);
    process.exit(1);
  }
} catch (err) {
  console.error('Invalid config file', err);
  process.exit(1);
}

读取 cli 传入的 文件路径 然后转换成 JSON,赋值给 config

if (cli.flags.type === 'themeCss') {
  buildThemeCSS(config.theme);
} else if (cli.flags.type === 'colorLess') {
  genColorLess(config.colorLess);
} else {
  throw new Error(`Invalid type, can be set themeCss or colorLess value`);
}

假如 type 是 css 那么走 buildThemeCSS 函数

假如 type 是 less 那么走 genColorLess 函数


export async function genColorLess(config: ColorLessConfig): Promise<void> {
  config = fixConfig(config);

  if (existsSync(config.outputFilePath!)) {
    unlinkSync(config.outputFilePath!);
  }

  await generateTheme(config);
}

genColorLess 简单的处理 config 传入的 ng-alalin.json 转换成的对象,然后调用了 generateTheme 来生成主题,最后输出到项目的目录中,具体通过 json 来生成 theme 的函数规则较为复杂,概括一下就是 拿json传入的配置,对文件内容自动进行@import,替换等操作,最后生成目标文件 less,写入原项目中

5.6 scripts

5.7 site

5.8 changelog

5.9 publish

5.10 release

3 操作
someone66251 在 2021-08-16 11:05:15 更新了该帖
someone66251 在 2021-08-15 21:55:32 更新了该帖
someone66251 在 2021-08-15 21:53:51 更新了该帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...