本节指标
- strapi + graphql 插件 + docker 装置
- strapi 治理数据结构、内容
- flutter + graphql 插件 实现查问
视频
https://www.bilibili.com/vide...
代码
https://github.com/ducafecat/...
注释
后盾开发步骤
采纳 strapi + nodejs + 网关 的计划
1. strapi 装置
1.1 docker-compose 形式装置
- .env
PASSWORD=123456
- docker-compose.yml
version: "3"services: mongo: image: mongo container_name: mongo restart: always ports: - 27017:27017 environment: - TZ=Asia/Shanghai - MONGO_INITDB_ROOT_USERNAME=root - MONGO_INITDB_ROOT_PASSWORD=${PASSWORD} volumes: - ./docker-data/mongo:/data/db networks: docker_net: ipv4_address: 172.22.0.11 # starpi # admin / 123456 / admin@ducafecat.tech strapi-app: image: strapi/strapi container_name: strapi-app restart: always ports: - 1337:1337 # command: strapi build # command: strapi start environment: - TZ=Asia/Shanghai - DATABASE_CLIENT=mongo - DATABASE_HOST=mongo - DATABASE_PORT=27017 - DATABASE_NAME=strapi - DATABASE_USERNAME=root - DATABASE_PASSWORD=${PASSWORD} - DATABASE_AUTHENTICATION_DATABASE=strapi # - NODE_ENV=production depends_on: - mongo volumes: - ./docker-data/strapi-app:/srv/app networks: docker_net: ipv4_address: 172.22.0.12networks: docker_net: driver: bridge ipam: config: - subnet: 172.22.0.0/16
http://localhost:1337/admin
1.2 装置 graphql 插件
2. 构建新闻数据结构
2.1 创立数据类型
- 增加类型
- 增加字段
- 字段列表
2.2 调整数据编辑界面
2.3 调整数据列表界面
2.4 保护数据
- 列表
- 增加
3. 调试 graphql 申请
3.3 graphql 语法
类型
- query 查问
- mutate 操作
3.4 调试新闻列表
http://localhost:1337/graphql
4. 编写 flutter 代码
4.1 退出 graphql 插件
https://pub.flutter-io.cn/pac...
- pubspec.yaml
dependencies: # graphql graphql: ^3.0.2
4.2 封装 graphql client 工具类
- lib/common/utils/graphql_client.dart
import 'package:flutter/material.dart';import 'package:flutter_ducafecat_news/common/values/values.dart';import 'package:flutter_ducafecat_news/common/widgets/widgets.dart';import 'package:graphql/client.dart';class GraphqlClientUtil { static OptimisticCache cache = OptimisticCache( dataIdFromObject: typenameDataIdFromObject, ); static client() { HttpLink _httpLink = HttpLink( uri: '$SERVER_STRAPI_GRAPHQL_URL/graphql', ); // final AuthLink _authLink = AuthLink( // getToken: () => // 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjVlZmMzNDdhYzgzOTVjMDAwY2ViYzE5NyIsImlhdCI6MTU5MzY1NDcwNiwiZXhwIjoxNTk2MjQ2NzA2fQ.RYDmNSDJxcZLLPHAf4u59IER7Bs5VoWfBo1_t-TR5yY', // ); // final Link _link = _authLink.concat(_httpLink); return GraphQLClient( cache: cache, link: _httpLink, ); } // 查问 static Future query({ @required BuildContext context, @required String schema, Map<String, dynamic> variables, }) async { QueryOptions options = QueryOptions( documentNode: gql(schema), variables: variables, ); QueryResult result = await client().query(options); if (result.hasException) { toastInfo(msg: result.exception.toString()); throw result.exception; } return result; } // 操作 static Future mutate({ @required BuildContext context, @required String schema, Map<String, dynamic> variables, }) async { QueryOptions options = QueryOptions( documentNode: gql(schema), variables: variables, ); QueryResult result = await client().mutate(options); if (result.hasException) { toastInfo(msg: result.exception.toString()); throw result.exception; } return result; }}
4.3 编写 graphql 查问申请
- lib/common/graphql/news_content.dart
const String GQL_NEWS_LIST = r''' query News { newsContents { title category author url addtime thumbnail { url } } }''';
4.4 编写数据实体
lib/common/entitys/gql_news.dart
class GqlNewsResponseEntity { GqlNewsResponseEntity({ this.id, this.title, this.category, this.author, this.url, this.addtime, this.thumbnail, }); String id; String title; String category; String author; String url; DateTime addtime; Thumbnail thumbnail; factory GqlNewsResponseEntity.fromJson(Map<String, dynamic> json) => GqlNewsResponseEntity( id: json["id"], title: json["title"], category: json["category"], author: json["author"], url: json["url"], addtime: DateTime.parse(json["addtime"]), thumbnail: Thumbnail.fromJson(json["thumbnail"]), ); Map<String, dynamic> toJson() => { "id": id, "title": title, "category": category, "author": author, "url": url, "addtime": "${addtime.year.toString().padLeft(4, '0')}-${addtime.month.toString().padLeft(2, '0')}-${addtime.day.toString().padLeft(2, '0')}", "thumbnail": thumbnail.toJson(), };}class Thumbnail { Thumbnail({ this.url, }); String url; factory Thumbnail.fromJson(Map<String, dynamic> json) => Thumbnail( url: json["url"], ); Map<String, dynamic> toJson() => { "url": url, };}
4.5 编写 API 拜访
- lib/common/apis/gql_news.dart
import 'package:flutter/material.dart';import 'package:flutter_ducafecat_news/common/entitys/entitys.dart';import 'package:flutter_ducafecat_news/common/graphql/graphql.dart';import 'package:flutter_ducafecat_news/common/utils/utils.dart';import 'package:graphql/client.dart';/// 新闻class GqlNewsAPI { /// 翻页 static Future<List<GqlNewsResponseEntity>> newsPageList({ @required BuildContext context, Map<String, dynamic> params, }) async { QueryResult response = await GraphqlClientUtil.query(context: context, schema: GQL_NEWS_LIST); return response.data['newsContents'] .map<GqlNewsResponseEntity>( (item) => GqlNewsResponseEntity.fromJson(item)) .toList(); }}
4.6 批改新闻列表页
- lib/pages/main/main.dart
import 'dart:async';import 'package:flutter/material.dart';import 'package:flutter_ducafecat_news/common/apis/apis.dart';import 'package:flutter_ducafecat_news/common/entitys/entitys.dart';import 'package:flutter_ducafecat_news/common/utils/utils.dart';import 'package:flutter_ducafecat_news/common/values/values.dart';import 'package:flutter_ducafecat_news/common/widgets/widgets.dart';import 'package:flutter_ducafecat_news/pages/main/ad_widget.dart';import 'package:flutter_ducafecat_news/pages/main/categories_widget.dart';import 'package:flutter_ducafecat_news/pages/main/channels_widget.dart';import 'package:flutter_ducafecat_news/pages/main/news_item_widget.dart';import 'package:flutter_ducafecat_news/pages/main/newsletter_widget.dart';import 'package:flutter_ducafecat_news/pages/main/recommend_widget.dart';import 'package:flutter_easyrefresh/easy_refresh.dart';class MainPage extends StatefulWidget { MainPage({Key key}) : super(key: key); @override _MainPageState createState() => _MainPageState();}class _MainPageState extends State<MainPage> { EasyRefreshController _controller; // EasyRefresh控制器 // NewsPageListResponseEntity _newsPageList; // 新闻翻页 List<GqlNewsResponseEntity> _newsPageList; // 新闻翻页 NewsItem _newsRecommend; // 新闻举荐 List<CategoryResponseEntity> _categories; // 分类 List<ChannelResponseEntity> _channels; // 频道 String _selCategoryCode; // 选中的分类Code @override void initState() { super.initState(); _controller = EasyRefreshController(); _loadAllData(); _loadLatestWithDiskCache(); } // 如果有磁盘缓存,提早3秒拉取更新档案 _loadLatestWithDiskCache() { if (CACHE_ENABLE == true) { var cacheData = StorageUtil().getJSON(STORAGE_INDEX_NEWS_CACHE_KEY); if (cacheData != null) { Timer(Duration(seconds: 3), () { _controller.callRefresh(); }); } } } // 读取所有数据 _loadAllData() async { _categories = await NewsAPI.categories( context: context, cacheDisk: true, ); _channels = await NewsAPI.channels( context: context, cacheDisk: true, ); // _newsRecommend = await NewsAPI.newsRecommend( // context: context, // cacheDisk: true, // ); // _newsPageList = await NewsAPI.newsPageList( // context: context, // cacheDisk: true, // ); _newsPageList = await GqlNewsAPI.newsPageList( context: context, ); _selCategoryCode = _categories.first.code; if (mounted) { setState(() {}); } } // 拉取举荐、新闻 _loadNewsData( categoryCode, { bool refresh = false, }) async { _selCategoryCode = categoryCode; _newsRecommend = await NewsAPI.newsRecommend( context: context, params: NewsRecommendRequestEntity(categoryCode: categoryCode), refresh: refresh, cacheDisk: true, ); // _newsPageList = await NewsAPI.newsPageList( // context: context, // params: NewsPageListRequestEntity(categoryCode: categoryCode), // refresh: refresh, // cacheDisk: true, // ); _newsPageList = await GqlNewsAPI.newsPageList( context: context, ); if (mounted) { setState(() {}); } } // 分类菜单 Widget _buildCategories() { return _categories == null ? Container() : newsCategoriesWidget( categories: _categories, selCategoryCode: _selCategoryCode, onTap: (CategoryResponseEntity item) { _loadNewsData(item.code); }, ); } // 举荐浏览 Widget _buildRecommend() { return _newsRecommend == null // 数据没到位,能够用骨架图展现 ? Container() : recommendWidget(_newsRecommend); } // 频道 Widget _buildChannels() { return _channels == null ? Container() : newsChannelsWidget( channels: _channels, onTap: (ChannelResponseEntity item) {}, ); } // 新闻列表 Widget _buildNewsList() { return _newsPageList == null ? Container( height: duSetHeight(161 * 5 + 100.0), ) : Column( children: _newsPageList.map((item) { // 新闻行 List<Widget> widgets = <Widget>[ newsItem(item), Divider(height: 1), ]; // 每 5 条 显示广告 int index = _newsPageList.indexOf(item); if (((index + 1) % 5) == 0) { widgets.addAll(<Widget>[ adWidget(), Divider(height: 1), ]); } // 返回 return Column( children: widgets, ); }).toList(), ); } // ad 广告条 // 邮件订阅 Widget _buildEmailSubscribe() { return newsletterWidget(); } @override Widget build(BuildContext context) { return _newsPageList == null ? cardListSkeleton() : EasyRefresh( enableControlFinishRefresh: true, controller: _controller, header: ClassicalHeader(), onRefresh: () async { await _loadNewsData( _selCategoryCode, refresh: true, ); _controller.finishRefresh(); }, child: SingleChildScrollView( child: Column( children: <Widget>[ _buildCategories(), Divider(height: 1), _buildRecommend(), Divider(height: 1), _buildChannels(), Divider(height: 1), _buildNewsList(), Divider(height: 1), _buildEmailSubscribe(), ], ), ), ); }}
4.7 批改新闻详情页
- lib/pages/main/news_item_widget.dart
import 'package:auto_route/auto_route.dart';import 'package:flutter/material.dart';import 'package:flutter_ducafecat_news/common/entitys/entitys.dart';import 'package:flutter_ducafecat_news/common/utils/utils.dart';import 'package:flutter_ducafecat_news/common/values/values.dart';import 'package:flutter_ducafecat_news/common/widgets/widgets.dart';import 'package:flutter_ducafecat_news/common/router/router.gr.dart';/// 新闻行 ItemWidget newsItem(GqlNewsResponseEntity item) { return Container( height: duSetHeight(161), padding: EdgeInsets.all(duSetWidth(20)), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ // 图 InkWell( onTap: () { ExtendedNavigator.rootNavigator.pushNamed( Routes.detailsPageRoute, arguments: DetailsPageArguments(item: item), ); }, child: imageCached( '$SERVER_STRAPI_GRAPHQL_URL${item.thumbnail.url}', width: duSetWidth(121), height: duSetWidth(121), ), ), // 右侧 SizedBox( width: duSetWidth(194), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ // 作者 Container( margin: EdgeInsets.all(0), child: Text( item.author, style: TextStyle( fontFamily: 'Avenir', fontWeight: FontWeight.normal, color: AppColors.thirdElementText, fontSize: duSetFontSize(14), height: 1, ), ), ), // 题目 InkWell( onTap: () { ExtendedNavigator.rootNavigator.pushNamed( Routes.detailsPageRoute, arguments: DetailsPageArguments(item: item), ); }, child: Container( margin: EdgeInsets.only(top: duSetHeight(10)), child: Text( item.title, style: TextStyle( fontFamily: 'Montserrat', fontWeight: FontWeight.w500, color: AppColors.primaryText, fontSize: duSetFontSize(16), height: 1, ), overflow: TextOverflow.clip, maxLines: 3, ), ), ), // Spacer Spacer(), // 一行 3 列 Container( child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: <Widget>[ // 分类 ConstrainedBox( constraints: BoxConstraints( maxWidth: duSetWidth(60), ), child: Text( item.category, style: TextStyle( fontFamily: 'Avenir', fontWeight: FontWeight.normal, color: AppColors.secondaryElementText, fontSize: duSetFontSize(14), height: 1, ), overflow: TextOverflow.clip, maxLines: 1, ), ), // 增加工夫 Container( width: duSetWidth(15), ), ConstrainedBox( constraints: BoxConstraints( maxWidth: duSetWidth(100), ), child: Text( '• ${duTimeLineFormat(item.addtime)}', style: TextStyle( fontFamily: 'Avenir', fontWeight: FontWeight.normal, color: AppColors.thirdElementText, fontSize: duSetFontSize(14), height: 1, ), overflow: TextOverflow.clip, maxLines: 1, ), ), // 更多 Spacer(), InkWell( child: Icon( Icons.more_horiz, color: AppColors.primaryText, size: 24, ), onTap: () {}, ), ], ), ), ], ), ), ], ), );}
资源
设计稿蓝湖预览
https://lanhuapp.com/url/lYuz1
明码: gSKl
蓝湖当初免费了,所以查看标记还请本人上传 xd 设计稿
商业设计稿文件不好间接分享, 能够加微信分割 ducafecat
参考
- https://pub.flutter-io.cn/pac...
- https://strapi.io/documentati...
© 猫哥
https://ducafecat.tech