|
@@ -0,0 +1,561 @@
|
|
|
+import 'package:flutter/material.dart';
|
|
|
+import 'dart:io';
|
|
|
+import 'package:image_picker/image_picker.dart';
|
|
|
+
|
|
|
+class EscortPublish extends StatefulWidget {
|
|
|
+ @override
|
|
|
+ State<EscortPublish> createState() => _EscortPublishState();
|
|
|
+}
|
|
|
+
|
|
|
+class _EscortPublishState extends State<EscortPublish> {
|
|
|
+ final _nameController = TextEditingController();
|
|
|
+ final _phoneController = TextEditingController();
|
|
|
+ final _addressController = TextEditingController();
|
|
|
+
|
|
|
+ final List<String> levels = ['平价', '中端', '高端', '顶级'];
|
|
|
+ final List<String> services = ['工作室', '上门', '空降', '伴游'];
|
|
|
+ final List<String> taboos = ['DU品', '吃YAO', 'WU套', '醉9'];
|
|
|
+ final List<String> tags = [
|
|
|
+ '学生',
|
|
|
+ '良家',
|
|
|
+ 'OL',
|
|
|
+ '网红',
|
|
|
+ '模特',
|
|
|
+ '明星',
|
|
|
+ '新人',
|
|
|
+ '兼职',
|
|
|
+ '短期',
|
|
|
+ '原装',
|
|
|
+ 'S身材',
|
|
|
+ '高颜值',
|
|
|
+ '三点粉',
|
|
|
+ '微胖',
|
|
|
+ '水嫩',
|
|
|
+ '紧致',
|
|
|
+ '清纯',
|
|
|
+ '轻熟',
|
|
|
+ '气质',
|
|
|
+ '纯欲'
|
|
|
+ ];
|
|
|
+ final List<String> packages = [
|
|
|
+ '双飞',
|
|
|
+ '3P',
|
|
|
+ '口爆',
|
|
|
+ '胸推',
|
|
|
+ '足交',
|
|
|
+ '毒龙',
|
|
|
+ '做爱',
|
|
|
+ '69',
|
|
|
+ '无套内射',
|
|
|
+ '舌吻',
|
|
|
+ '水床',
|
|
|
+ '制服',
|
|
|
+ '冰火',
|
|
|
+ '环游',
|
|
|
+ '调教',
|
|
|
+ '喷水',
|
|
|
+ '洗澡',
|
|
|
+ '剧情',
|
|
|
+ 'SM',
|
|
|
+ '三通'
|
|
|
+ ];
|
|
|
+ String? selectedLevel;
|
|
|
+
|
|
|
+ String? selectedCity;
|
|
|
+ String? selectedYear;
|
|
|
+ String? selectedWeight;
|
|
|
+ String? selectedHeight;
|
|
|
+ String? selectedCup;
|
|
|
+
|
|
|
+ List<String> selectedServices = [];
|
|
|
+ List<String> selectedTaboos = [];
|
|
|
+ List<String> selectedTags = [];
|
|
|
+
|
|
|
+ List<String> selectedPackages = [];
|
|
|
+ List<XFile> imageFiles = [];
|
|
|
+ XFile? videoFile;
|
|
|
+ bool hideFromPublic = false;
|
|
|
+ final TextEditingController _descController = TextEditingController();
|
|
|
+ final ImagePicker _picker = ImagePicker();
|
|
|
+
|
|
|
+ @override
|
|
|
+ Widget build(BuildContext context) {
|
|
|
+ return Scaffold(
|
|
|
+ appBar: AppBar(
|
|
|
+ title: const Text.rich(
|
|
|
+ TextSpan(
|
|
|
+ text: '发布外围',
|
|
|
+ children: [
|
|
|
+ TextSpan(
|
|
|
+ text: '(请保证妹子信息准确)',
|
|
|
+ style: TextStyle(fontSize: 14, color: Colors.grey),
|
|
|
+ )
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ leading: const BackButton(),
|
|
|
+ ),
|
|
|
+ body: SingleChildScrollView(
|
|
|
+ padding: const EdgeInsets.all(16),
|
|
|
+ child: Column(
|
|
|
+ children: [
|
|
|
+ buildTextField("外围花名", "(必填)", "请输入外围花名,不超过10个字", _nameController),
|
|
|
+ buildDropdownField("所在城市", "(必选)", selectedCity, ['北京', '上海', '广州'],
|
|
|
+ (val) {
|
|
|
+ setState(() => selectedCity = val);
|
|
|
+ }),
|
|
|
+ buildTextField("手机号码", "(必填,不会自动发送给用户)", "请输入妹子联系方式,不超过50个字",
|
|
|
+ _phoneController),
|
|
|
+ buildTextField("详细地址", "(必填,不会自动发送给用户)", "请输入妹子工作室地址,不超过50个字",
|
|
|
+ _addressController),
|
|
|
+ buildDropdownField("出生年份", "(必选)", selectedYear,
|
|
|
+ List.generate(25, (i) => '${2000 - i}'), (val) {
|
|
|
+ setState(() => selectedYear = val);
|
|
|
+ }),
|
|
|
+ buildDropdownField(
|
|
|
+ "体重", "(必选)", selectedWeight, ['40kg', '45kg', '50kg', '55kg'],
|
|
|
+ (val) {
|
|
|
+ setState(() => selectedWeight = val);
|
|
|
+ }),
|
|
|
+ buildDropdownField(
|
|
|
+ "身高", "(必选)", selectedHeight, ['150cm', '160cm', '170cm'],
|
|
|
+ (val) {
|
|
|
+ setState(() => selectedHeight = val);
|
|
|
+ }),
|
|
|
+ buildDropdownField(
|
|
|
+ "罩杯", "(必选)", selectedCup, ['A', 'B', 'C', 'D', 'E'], (val) {
|
|
|
+ setState(() => selectedCup = val);
|
|
|
+ }),
|
|
|
+ const SizedBox(height: 20),
|
|
|
+ Align(
|
|
|
+ alignment: Alignment.centerLeft,
|
|
|
+ child: RichText(
|
|
|
+ text: const TextSpan(
|
|
|
+ style: TextStyle(fontSize: 16, color: Colors.black),
|
|
|
+ children: [
|
|
|
+ TextSpan(text: '请选择妹子档次'),
|
|
|
+ TextSpan(text: '(单选)', style: TextStyle(color: Colors.red)),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ const SizedBox(height: 10),
|
|
|
+ Wrap(
|
|
|
+ spacing: 12,
|
|
|
+ children: levels.map((level) {
|
|
|
+ final selected = level == selectedLevel;
|
|
|
+ return ChoiceChip(
|
|
|
+ label: Text(level),
|
|
|
+ selected: selected,
|
|
|
+ onSelected: (_) {
|
|
|
+ setState(() => selectedLevel = level);
|
|
|
+ },
|
|
|
+ selectedColor: Colors.blue,
|
|
|
+ labelStyle:
|
|
|
+ TextStyle(color: selected ? Colors.white : Colors.black),
|
|
|
+ );
|
|
|
+ }).toList(),
|
|
|
+ ),
|
|
|
+ const SizedBox(height: 20),
|
|
|
+ Align(
|
|
|
+ alignment: Alignment.centerLeft,
|
|
|
+ child: RichText(
|
|
|
+ text: const TextSpan(
|
|
|
+ style: TextStyle(fontSize: 16, color: Colors.black),
|
|
|
+ children: [
|
|
|
+ TextSpan(text: '服务套餐'),
|
|
|
+ TextSpan(
|
|
|
+ text: '(最少选填1项)', style: TextStyle(color: Colors.red)),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ const SizedBox(height: 10),
|
|
|
+ buildSelectableChips(
|
|
|
+ options: services,
|
|
|
+ selectedList: selectedServices,
|
|
|
+ maxSelect: services.length,
|
|
|
+ isSingle: false,
|
|
|
+ onTap: (_) {},
|
|
|
+ ),
|
|
|
+ const SizedBox(height: 12),
|
|
|
+ buildPackageInputFields(),
|
|
|
+ const SizedBox(height: 20),
|
|
|
+ Align(
|
|
|
+ alignment: Alignment.centerLeft,
|
|
|
+ child: RichText(
|
|
|
+ text: const TextSpan(
|
|
|
+ style: TextStyle(fontSize: 16, color: Colors.black),
|
|
|
+ children: [
|
|
|
+ TextSpan(text: '禁忌'),
|
|
|
+ TextSpan(text: '(多选)', style: TextStyle(color: Colors.red)),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ const SizedBox(height: 10),
|
|
|
+ buildSelectableChips(
|
|
|
+ options: taboos,
|
|
|
+ selectedList: selectedTaboos,
|
|
|
+ maxSelect: taboos.length,
|
|
|
+ isSingle: false,
|
|
|
+ onTap: (_) {},
|
|
|
+ ),
|
|
|
+ const SizedBox(height: 20),
|
|
|
+ Align(
|
|
|
+ alignment: Alignment.centerLeft,
|
|
|
+ child: RichText(
|
|
|
+ text: const TextSpan(
|
|
|
+ style: TextStyle(fontSize: 16, color: Colors.black),
|
|
|
+ children: [
|
|
|
+ TextSpan(text: '外圈标签'),
|
|
|
+ TextSpan(
|
|
|
+ text: '(必选,最多3个)', style: TextStyle(color: Colors.red)),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ const SizedBox(height: 10),
|
|
|
+ buildSelectableChips(
|
|
|
+ options: tags,
|
|
|
+ selectedList: selectedTags,
|
|
|
+ maxSelect: 3,
|
|
|
+ isSingle: false,
|
|
|
+ onTap: (_) {},
|
|
|
+ ),
|
|
|
+ Align(
|
|
|
+ alignment: Alignment.centerLeft,
|
|
|
+ child: RichText(
|
|
|
+ text: const TextSpan(
|
|
|
+ style: TextStyle(fontSize: 16, color: Colors.black),
|
|
|
+ children: [
|
|
|
+ TextSpan(text: '服务项目'),
|
|
|
+ TextSpan(
|
|
|
+ text: '(必选,最多20个)',
|
|
|
+ style: TextStyle(color: Colors.red)),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ const SizedBox(height: 10),
|
|
|
+ buildSelectableChips(
|
|
|
+ options: packages,
|
|
|
+ selectedList: selectedPackages,
|
|
|
+ maxSelect: 20,
|
|
|
+ isSingle: false,
|
|
|
+ onTap: (_) {},
|
|
|
+ ),
|
|
|
+ const SizedBox(height: 20),
|
|
|
+ Align(
|
|
|
+ alignment: Alignment.centerLeft,
|
|
|
+ child: RichText(
|
|
|
+ text: const TextSpan(
|
|
|
+ style: TextStyle(fontSize: 16, color: Colors.black),
|
|
|
+ children: [
|
|
|
+ TextSpan(text: '外圈介绍'),
|
|
|
+ TextSpan(text: '(选填)', style: TextStyle(color: Colors.red)),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ const SizedBox(height: 10),
|
|
|
+ TextField(
|
|
|
+ controller: _descController,
|
|
|
+ maxLength: 200,
|
|
|
+ maxLines: 4,
|
|
|
+ decoration: const InputDecoration(
|
|
|
+ hintText: '请输入外圈介绍,不超过200字',
|
|
|
+ border: OutlineInputBorder(),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ const SizedBox(height: 20),
|
|
|
+ Align(
|
|
|
+ alignment: Alignment.centerLeft,
|
|
|
+ child: RichText(
|
|
|
+ text: const TextSpan(
|
|
|
+ style: TextStyle(fontSize: 16, color: Colors.black),
|
|
|
+ children: [
|
|
|
+ TextSpan(text: '上传图片'),
|
|
|
+ TextSpan(
|
|
|
+ text: '(最少1张,最多9张)',
|
|
|
+ style: TextStyle(color: Colors.red)),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ const SizedBox(height: 10),
|
|
|
+ buildImageUploader(),
|
|
|
+ const SizedBox(height: 20),
|
|
|
+ Row(
|
|
|
+ children: [
|
|
|
+ Align(
|
|
|
+ alignment: Alignment.centerLeft,
|
|
|
+ child: RichText(
|
|
|
+ text: const TextSpan(
|
|
|
+ style: TextStyle(fontSize: 16, color: Colors.black),
|
|
|
+ children: [
|
|
|
+ TextSpan(text: '官方认证视频'),
|
|
|
+ TextSpan(
|
|
|
+ text: '(选填,不超过100M)',
|
|
|
+ style: TextStyle(color: Colors.red)),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ Spacer(),
|
|
|
+ Row(
|
|
|
+ children: [
|
|
|
+ Radio<bool>(
|
|
|
+ value: true,
|
|
|
+ groupValue: hideFromPublic,
|
|
|
+ onChanged: (_) => setState(() => hideFromPublic = true),
|
|
|
+ ),
|
|
|
+ const Text("不对外展示", style: TextStyle(fontSize: 12)),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ const SizedBox(height: 8),
|
|
|
+ GestureDetector(
|
|
|
+ onTap: pickVideo,
|
|
|
+ child: Container(
|
|
|
+ height: 160,
|
|
|
+ color: Colors.grey[100],
|
|
|
+ child: Center(
|
|
|
+ child: videoFile == null
|
|
|
+ ? const Icon(Icons.videocam, size: 40, color: Colors.grey)
|
|
|
+ : const Icon(Icons.check_circle,
|
|
|
+ color: Colors.green, size: 40),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ const SizedBox(height: 8),
|
|
|
+ const Text("① 视频需包含本人露脸、当前日期、所在城市/名媛会",
|
|
|
+ style: TextStyle(color: Colors.grey)),
|
|
|
+ const Text("例如:3月3日,名媛会露脸在苏州等你",
|
|
|
+ style: TextStyle(color: Colors.grey)),
|
|
|
+ const Text("② 审核通过率高,通过后可获得官方认证标识",
|
|
|
+ style: TextStyle(color: Colors.grey)),
|
|
|
+ const Text("③ 选择对外展示可额外获得官方流量扶持+佣金优惠",
|
|
|
+ style: TextStyle(color: Colors.grey)),
|
|
|
+ const SizedBox(height: 30),
|
|
|
+ SizedBox(
|
|
|
+ width: double.infinity,
|
|
|
+ child: ElevatedButton(
|
|
|
+ onPressed: () {},
|
|
|
+ style: ElevatedButton.styleFrom(
|
|
|
+ padding: const EdgeInsets.symmetric(vertical: 16),
|
|
|
+ shape: RoundedRectangleBorder(
|
|
|
+ borderRadius: BorderRadius.circular(6)),
|
|
|
+ backgroundColor: Colors.blue,
|
|
|
+ ),
|
|
|
+ child: const Text("提交", style: TextStyle(fontSize: 16)),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ Widget buildTextField(String label, String hint, String placeholder,
|
|
|
+ TextEditingController controller) {
|
|
|
+ return Padding(
|
|
|
+ padding: const EdgeInsets.symmetric(vertical: 8),
|
|
|
+ child: Column(
|
|
|
+ crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
+ children: [
|
|
|
+ buildLabel(label, hint),
|
|
|
+ const SizedBox(height: 6),
|
|
|
+ TextField(
|
|
|
+ controller: controller,
|
|
|
+ decoration: InputDecoration(
|
|
|
+ hintText: placeholder,
|
|
|
+ border: OutlineInputBorder(),
|
|
|
+ contentPadding:
|
|
|
+ const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ Widget buildDropdownField(String label, String hint, String? value,
|
|
|
+ List<String> options, ValueChanged<String?> onChanged) {
|
|
|
+ return Padding(
|
|
|
+ padding: const EdgeInsets.symmetric(vertical: 8),
|
|
|
+ child: Column(
|
|
|
+ crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
+ children: [
|
|
|
+ buildLabel(label, hint),
|
|
|
+ const SizedBox(height: 6),
|
|
|
+ DropdownButtonFormField<String>(
|
|
|
+ value: value,
|
|
|
+ items: options
|
|
|
+ .map((e) => DropdownMenuItem(value: e, child: Text(e)))
|
|
|
+ .toList(),
|
|
|
+ onChanged: onChanged,
|
|
|
+ decoration: InputDecoration(
|
|
|
+ border: OutlineInputBorder(),
|
|
|
+ contentPadding:
|
|
|
+ const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ Widget buildLabel(String title, String hint) {
|
|
|
+ return RichText(
|
|
|
+ text: TextSpan(
|
|
|
+ text: title,
|
|
|
+ style: const TextStyle(fontSize: 16, color: Colors.black),
|
|
|
+ children: [
|
|
|
+ TextSpan(text: ' $hint', style: const TextStyle(color: Colors.red)),
|
|
|
+ ],
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ Widget buildSelectableChips({
|
|
|
+ required List<String> options,
|
|
|
+ required List<String> selectedList,
|
|
|
+ required int maxSelect,
|
|
|
+ required bool isSingle,
|
|
|
+ required Function(String) onTap,
|
|
|
+ }) {
|
|
|
+ return Wrap(
|
|
|
+ spacing: 8,
|
|
|
+ runSpacing: 8,
|
|
|
+ children: options.map((option) {
|
|
|
+ final bool selected = selectedList.contains(option);
|
|
|
+ return ChoiceChip(
|
|
|
+ label: Text(option),
|
|
|
+ selected: selected,
|
|
|
+ onSelected: (_) {
|
|
|
+ setState(() {
|
|
|
+ if (isSingle) {
|
|
|
+ selectedList
|
|
|
+ ..clear()
|
|
|
+ ..add(option);
|
|
|
+ } else {
|
|
|
+ if (selected) {
|
|
|
+ selectedList.remove(option);
|
|
|
+ } else if (selectedList.length < maxSelect) {
|
|
|
+ selectedList.add(option);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ onTap(option);
|
|
|
+ });
|
|
|
+ },
|
|
|
+ );
|
|
|
+ }).toList(),
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ Widget buildPackageInputFields() {
|
|
|
+ return Column(
|
|
|
+ children: List.generate(4, (i) {
|
|
|
+ return Row(
|
|
|
+ children: [
|
|
|
+ Expanded(
|
|
|
+ child: TextField(
|
|
|
+ decoration: InputDecoration(
|
|
|
+ hintText: '请输入服务套餐${i + 1}${i == 0 ? ',必填' : ''}',
|
|
|
+ border: OutlineInputBorder(),
|
|
|
+ contentPadding:
|
|
|
+ const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ const SizedBox(width: 8),
|
|
|
+ Expanded(
|
|
|
+ child: TextField(
|
|
|
+ decoration: InputDecoration(
|
|
|
+ hintText: '请输入套餐${i + 1}价格${i == 0 ? ',必填' : ''}',
|
|
|
+ border: OutlineInputBorder(),
|
|
|
+ contentPadding:
|
|
|
+ const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
|
+ ),
|
|
|
+ keyboardType: TextInputType.number,
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ );
|
|
|
+ }),
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ Widget buildImageUploader() {
|
|
|
+ return Wrap(
|
|
|
+ spacing: 8,
|
|
|
+ runSpacing: 8,
|
|
|
+ children: List.generate(9, (index) {
|
|
|
+ if (index < imageFiles.length) {
|
|
|
+ return Stack(
|
|
|
+ children: [
|
|
|
+ Image.file(
|
|
|
+ File(imageFiles[index].path),
|
|
|
+ width: 100,
|
|
|
+ height: 100,
|
|
|
+ fit: BoxFit.cover,
|
|
|
+ ),
|
|
|
+ Positioned(
|
|
|
+ top: 0,
|
|
|
+ right: 0,
|
|
|
+ child: GestureDetector(
|
|
|
+ onTap: () {
|
|
|
+ setState(() {
|
|
|
+ imageFiles.removeAt(index);
|
|
|
+ });
|
|
|
+ },
|
|
|
+ child: const Icon(Icons.cancel, color: Colors.red, size: 20),
|
|
|
+ ),
|
|
|
+ ),
|
|
|
+ ],
|
|
|
+ );
|
|
|
+ } else if (index == imageFiles.length) {
|
|
|
+ return GestureDetector(
|
|
|
+ onTap: pickImages,
|
|
|
+ child: Container(
|
|
|
+ width: 100,
|
|
|
+ height: 100,
|
|
|
+ color: Colors.grey[200],
|
|
|
+ child: const Icon(Icons.add),
|
|
|
+ ),
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ return Container(
|
|
|
+ width: 100,
|
|
|
+ height: 100,
|
|
|
+ color: Colors.grey[100],
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ Future<void> pickVideo() async {
|
|
|
+ final XFile? picked = await _picker.pickVideo(
|
|
|
+ source: ImageSource.gallery,
|
|
|
+ maxDuration: const Duration(minutes: 5),
|
|
|
+ );
|
|
|
+ if (picked != null) {
|
|
|
+ setState(() {
|
|
|
+ videoFile = picked;
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ Future<void> pickImages() async {
|
|
|
+ final List<XFile>? images = await _picker.pickMultiImage();
|
|
|
+ if (images != null && images.isNotEmpty) {
|
|
|
+ setState(() {
|
|
|
+ final remainingSlots = 9 - imageFiles.length;
|
|
|
+ imageFiles.addAll(images.take(remainingSlots));
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|