escort_publish.dart 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  1. import 'package:flutter/material.dart';
  2. import 'dart:io';
  3. import 'package:image_picker/image_picker.dart';
  4. class EscortPublish extends StatefulWidget {
  5. @override
  6. State<EscortPublish> createState() => _EscortPublishState();
  7. }
  8. class _EscortPublishState extends State<EscortPublish> {
  9. final _nameController = TextEditingController();
  10. final _phoneController = TextEditingController();
  11. final _addressController = TextEditingController();
  12. final List<String> levels = ['平价', '中端', '高端', '顶级'];
  13. final List<String> services = ['工作室', '上门', '空降', '伴游'];
  14. final List<String> taboos = ['DU品', '吃YAO', 'WU套', '醉9'];
  15. final List<String> tags = [
  16. '学生',
  17. '良家',
  18. 'OL',
  19. '网红',
  20. '模特',
  21. '明星',
  22. '新人',
  23. '兼职',
  24. '短期',
  25. '原装',
  26. 'S身材',
  27. '高颜值',
  28. '三点粉',
  29. '微胖',
  30. '水嫩',
  31. '紧致',
  32. '清纯',
  33. '轻熟',
  34. '气质',
  35. '纯欲'
  36. ];
  37. final List<String> packages = [
  38. '双飞',
  39. '3P',
  40. '口爆',
  41. '胸推',
  42. '足交',
  43. '毒龙',
  44. '做爱',
  45. '69',
  46. '无套内射',
  47. '舌吻',
  48. '水床',
  49. '制服',
  50. '冰火',
  51. '环游',
  52. '调教',
  53. '喷水',
  54. '洗澡',
  55. '剧情',
  56. 'SM',
  57. '三通'
  58. ];
  59. String? selectedLevel;
  60. String? selectedCity;
  61. String? selectedYear;
  62. String? selectedWeight;
  63. String? selectedHeight;
  64. String? selectedCup;
  65. List<String> selectedServices = [];
  66. List<String> selectedTaboos = [];
  67. List<String> selectedTags = [];
  68. List<String> selectedPackages = [];
  69. List<XFile> imageFiles = [];
  70. XFile? videoFile;
  71. bool hideFromPublic = false;
  72. final TextEditingController _descController = TextEditingController();
  73. final ImagePicker _picker = ImagePicker();
  74. @override
  75. Widget build(BuildContext context) {
  76. return Scaffold(
  77. appBar: AppBar(
  78. title: const Text.rich(
  79. TextSpan(
  80. text: '发布外围',
  81. children: [
  82. TextSpan(
  83. text: '(请保证妹子信息准确)',
  84. style: TextStyle(fontSize: 14, color: Colors.grey),
  85. )
  86. ],
  87. ),
  88. ),
  89. leading: const BackButton(),
  90. ),
  91. body: SingleChildScrollView(
  92. padding: const EdgeInsets.all(16),
  93. child: Column(
  94. children: [
  95. buildTextField("外围花名", "(必填)", "请输入外围花名,不超过10个字", _nameController),
  96. buildDropdownField("所在城市", "(必选)", selectedCity, ['北京', '上海', '广州'],
  97. (val) {
  98. setState(() => selectedCity = val);
  99. }),
  100. buildTextField("手机号码", "(必填,不会自动发送给用户)", "请输入妹子联系方式,不超过50个字",
  101. _phoneController),
  102. buildTextField("详细地址", "(必填,不会自动发送给用户)", "请输入妹子工作室地址,不超过50个字",
  103. _addressController),
  104. buildDropdownField("出生年份", "(必选)", selectedYear,
  105. List.generate(25, (i) => '${2000 - i}'), (val) {
  106. setState(() => selectedYear = val);
  107. }),
  108. buildDropdownField(
  109. "体重", "(必选)", selectedWeight, ['40kg', '45kg', '50kg', '55kg'],
  110. (val) {
  111. setState(() => selectedWeight = val);
  112. }),
  113. buildDropdownField(
  114. "身高", "(必选)", selectedHeight, ['150cm', '160cm', '170cm'],
  115. (val) {
  116. setState(() => selectedHeight = val);
  117. }),
  118. buildDropdownField(
  119. "罩杯", "(必选)", selectedCup, ['A', 'B', 'C', 'D', 'E'], (val) {
  120. setState(() => selectedCup = val);
  121. }),
  122. const SizedBox(height: 20),
  123. Align(
  124. alignment: Alignment.centerLeft,
  125. child: RichText(
  126. text: const TextSpan(
  127. style: TextStyle(fontSize: 16, color: Colors.black),
  128. children: [
  129. TextSpan(text: '请选择妹子档次'),
  130. TextSpan(text: '(单选)', style: TextStyle(color: Colors.red)),
  131. ],
  132. ),
  133. ),
  134. ),
  135. const SizedBox(height: 10),
  136. Wrap(
  137. spacing: 12,
  138. children: levels.map((level) {
  139. final selected = level == selectedLevel;
  140. return ChoiceChip(
  141. label: Text(level),
  142. selected: selected,
  143. onSelected: (_) {
  144. setState(() => selectedLevel = level);
  145. },
  146. selectedColor: Colors.blue,
  147. labelStyle:
  148. TextStyle(color: selected ? Colors.white : Colors.black),
  149. );
  150. }).toList(),
  151. ),
  152. const SizedBox(height: 20),
  153. Align(
  154. alignment: Alignment.centerLeft,
  155. child: RichText(
  156. text: const TextSpan(
  157. style: TextStyle(fontSize: 16, color: Colors.black),
  158. children: [
  159. TextSpan(text: '服务套餐'),
  160. TextSpan(
  161. text: '(最少选填1项)', style: TextStyle(color: Colors.red)),
  162. ],
  163. ),
  164. ),
  165. ),
  166. const SizedBox(height: 10),
  167. buildSelectableChips(
  168. options: services,
  169. selectedList: selectedServices,
  170. maxSelect: services.length,
  171. isSingle: false,
  172. onTap: (_) {},
  173. ),
  174. const SizedBox(height: 12),
  175. buildPackageInputFields(),
  176. const SizedBox(height: 20),
  177. Align(
  178. alignment: Alignment.centerLeft,
  179. child: RichText(
  180. text: const TextSpan(
  181. style: TextStyle(fontSize: 16, color: Colors.black),
  182. children: [
  183. TextSpan(text: '禁忌'),
  184. TextSpan(text: '(多选)', style: TextStyle(color: Colors.red)),
  185. ],
  186. ),
  187. ),
  188. ),
  189. const SizedBox(height: 10),
  190. buildSelectableChips(
  191. options: taboos,
  192. selectedList: selectedTaboos,
  193. maxSelect: taboos.length,
  194. isSingle: false,
  195. onTap: (_) {},
  196. ),
  197. const SizedBox(height: 20),
  198. Align(
  199. alignment: Alignment.centerLeft,
  200. child: RichText(
  201. text: const TextSpan(
  202. style: TextStyle(fontSize: 16, color: Colors.black),
  203. children: [
  204. TextSpan(text: '外圈标签'),
  205. TextSpan(
  206. text: '(必选,最多3个)', style: TextStyle(color: Colors.red)),
  207. ],
  208. ),
  209. ),
  210. ),
  211. const SizedBox(height: 10),
  212. buildSelectableChips(
  213. options: tags,
  214. selectedList: selectedTags,
  215. maxSelect: 3,
  216. isSingle: false,
  217. onTap: (_) {},
  218. ),
  219. Align(
  220. alignment: Alignment.centerLeft,
  221. child: RichText(
  222. text: const TextSpan(
  223. style: TextStyle(fontSize: 16, color: Colors.black),
  224. children: [
  225. TextSpan(text: '服务项目'),
  226. TextSpan(
  227. text: '(必选,最多20个)',
  228. style: TextStyle(color: Colors.red)),
  229. ],
  230. ),
  231. ),
  232. ),
  233. const SizedBox(height: 10),
  234. buildSelectableChips(
  235. options: packages,
  236. selectedList: selectedPackages,
  237. maxSelect: 20,
  238. isSingle: false,
  239. onTap: (_) {},
  240. ),
  241. const SizedBox(height: 20),
  242. Align(
  243. alignment: Alignment.centerLeft,
  244. child: RichText(
  245. text: const TextSpan(
  246. style: TextStyle(fontSize: 16, color: Colors.black),
  247. children: [
  248. TextSpan(text: '外圈介绍'),
  249. TextSpan(text: '(选填)', style: TextStyle(color: Colors.red)),
  250. ],
  251. ),
  252. ),
  253. ),
  254. const SizedBox(height: 10),
  255. TextField(
  256. controller: _descController,
  257. maxLength: 200,
  258. maxLines: 4,
  259. decoration: const InputDecoration(
  260. hintText: '请输入外圈介绍,不超过200字',
  261. border: OutlineInputBorder(),
  262. ),
  263. ),
  264. const SizedBox(height: 20),
  265. Align(
  266. alignment: Alignment.centerLeft,
  267. child: RichText(
  268. text: const TextSpan(
  269. style: TextStyle(fontSize: 16, color: Colors.black),
  270. children: [
  271. TextSpan(text: '上传图片'),
  272. TextSpan(
  273. text: '(最少1张,最多9张)',
  274. style: TextStyle(color: Colors.red)),
  275. ],
  276. ),
  277. ),
  278. ),
  279. const SizedBox(height: 10),
  280. buildImageUploader(),
  281. const SizedBox(height: 20),
  282. Row(
  283. children: [
  284. Align(
  285. alignment: Alignment.centerLeft,
  286. child: RichText(
  287. text: const TextSpan(
  288. style: TextStyle(fontSize: 16, color: Colors.black),
  289. children: [
  290. TextSpan(text: '官方认证视频'),
  291. TextSpan(
  292. text: '(选填,不超过100M)',
  293. style: TextStyle(color: Colors.red)),
  294. ],
  295. ),
  296. ),
  297. ),
  298. Spacer(),
  299. Row(
  300. children: [
  301. Radio<bool>(
  302. value: true,
  303. groupValue: hideFromPublic,
  304. onChanged: (_) => setState(() => hideFromPublic = true),
  305. ),
  306. const Text("不对外展示", style: TextStyle(fontSize: 12)),
  307. ],
  308. ),
  309. ],
  310. ),
  311. const SizedBox(height: 8),
  312. GestureDetector(
  313. onTap: pickVideo,
  314. child: Container(
  315. height: 160,
  316. color: Colors.grey[100],
  317. child: Center(
  318. child: videoFile == null
  319. ? const Icon(Icons.videocam, size: 40, color: Colors.grey)
  320. : const Icon(Icons.check_circle,
  321. color: Colors.green, size: 40),
  322. ),
  323. ),
  324. ),
  325. const SizedBox(height: 8),
  326. const Text("① 视频需包含本人露脸、当前日期、所在城市/名媛会",
  327. style: TextStyle(color: Colors.grey)),
  328. const Text("例如:3月3日,名媛会露脸在苏州等你",
  329. style: TextStyle(color: Colors.grey)),
  330. const Text("② 审核通过率高,通过后可获得官方认证标识",
  331. style: TextStyle(color: Colors.grey)),
  332. const Text("③ 选择对外展示可额外获得官方流量扶持+佣金优惠",
  333. style: TextStyle(color: Colors.grey)),
  334. const SizedBox(height: 30),
  335. SizedBox(
  336. width: double.infinity,
  337. child: ElevatedButton(
  338. onPressed: () {},
  339. style: ElevatedButton.styleFrom(
  340. padding: const EdgeInsets.symmetric(vertical: 16),
  341. shape: RoundedRectangleBorder(
  342. borderRadius: BorderRadius.circular(6)),
  343. backgroundColor: Colors.blue,
  344. ),
  345. child: const Text("提交", style: TextStyle(fontSize: 16)),
  346. ),
  347. ),
  348. ],
  349. ),
  350. ),
  351. );
  352. }
  353. Widget buildTextField(String label, String hint, String placeholder,
  354. TextEditingController controller) {
  355. return Padding(
  356. padding: const EdgeInsets.symmetric(vertical: 8),
  357. child: Column(
  358. crossAxisAlignment: CrossAxisAlignment.start,
  359. children: [
  360. buildLabel(label, hint),
  361. const SizedBox(height: 6),
  362. TextField(
  363. controller: controller,
  364. decoration: InputDecoration(
  365. hintText: placeholder,
  366. border: OutlineInputBorder(),
  367. contentPadding:
  368. const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
  369. ),
  370. ),
  371. ],
  372. ),
  373. );
  374. }
  375. Widget buildDropdownField(String label, String hint, String? value,
  376. List<String> options, ValueChanged<String?> onChanged) {
  377. return Padding(
  378. padding: const EdgeInsets.symmetric(vertical: 8),
  379. child: Column(
  380. crossAxisAlignment: CrossAxisAlignment.start,
  381. children: [
  382. buildLabel(label, hint),
  383. const SizedBox(height: 6),
  384. DropdownButtonFormField<String>(
  385. value: value,
  386. items: options
  387. .map((e) => DropdownMenuItem(value: e, child: Text(e)))
  388. .toList(),
  389. onChanged: onChanged,
  390. decoration: InputDecoration(
  391. border: OutlineInputBorder(),
  392. contentPadding:
  393. const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
  394. ),
  395. ),
  396. ],
  397. ),
  398. );
  399. }
  400. Widget buildLabel(String title, String hint) {
  401. return RichText(
  402. text: TextSpan(
  403. text: title,
  404. style: const TextStyle(fontSize: 16, color: Colors.black),
  405. children: [
  406. TextSpan(text: ' $hint', style: const TextStyle(color: Colors.red)),
  407. ],
  408. ),
  409. );
  410. }
  411. Widget buildSelectableChips({
  412. required List<String> options,
  413. required List<String> selectedList,
  414. required int maxSelect,
  415. required bool isSingle,
  416. required Function(String) onTap,
  417. }) {
  418. return Wrap(
  419. spacing: 8,
  420. runSpacing: 8,
  421. children: options.map((option) {
  422. final bool selected = selectedList.contains(option);
  423. return ChoiceChip(
  424. label: Text(option),
  425. selected: selected,
  426. onSelected: (_) {
  427. setState(() {
  428. if (isSingle) {
  429. selectedList
  430. ..clear()
  431. ..add(option);
  432. } else {
  433. if (selected) {
  434. selectedList.remove(option);
  435. } else if (selectedList.length < maxSelect) {
  436. selectedList.add(option);
  437. }
  438. }
  439. onTap(option);
  440. });
  441. },
  442. );
  443. }).toList(),
  444. );
  445. }
  446. Widget buildPackageInputFields() {
  447. return Column(
  448. children: List.generate(4, (i) {
  449. return Row(
  450. children: [
  451. Expanded(
  452. child: TextField(
  453. decoration: InputDecoration(
  454. hintText: '请输入服务套餐${i + 1}${i == 0 ? ',必填' : ''}',
  455. border: OutlineInputBorder(),
  456. contentPadding:
  457. const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
  458. ),
  459. ),
  460. ),
  461. const SizedBox(width: 8),
  462. Expanded(
  463. child: TextField(
  464. decoration: InputDecoration(
  465. hintText: '请输入套餐${i + 1}价格${i == 0 ? ',必填' : ''}',
  466. border: OutlineInputBorder(),
  467. contentPadding:
  468. const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
  469. ),
  470. keyboardType: TextInputType.number,
  471. ),
  472. ),
  473. ],
  474. );
  475. }),
  476. );
  477. }
  478. Widget buildImageUploader() {
  479. return Wrap(
  480. spacing: 8,
  481. runSpacing: 8,
  482. children: List.generate(9, (index) {
  483. if (index < imageFiles.length) {
  484. return Stack(
  485. children: [
  486. Image.file(
  487. File(imageFiles[index].path),
  488. width: 100,
  489. height: 100,
  490. fit: BoxFit.cover,
  491. ),
  492. Positioned(
  493. top: 0,
  494. right: 0,
  495. child: GestureDetector(
  496. onTap: () {
  497. setState(() {
  498. imageFiles.removeAt(index);
  499. });
  500. },
  501. child: const Icon(Icons.cancel, color: Colors.red, size: 20),
  502. ),
  503. ),
  504. ],
  505. );
  506. } else if (index == imageFiles.length) {
  507. return GestureDetector(
  508. onTap: pickImages,
  509. child: Container(
  510. width: 100,
  511. height: 100,
  512. color: Colors.grey[200],
  513. child: const Icon(Icons.add),
  514. ),
  515. );
  516. } else {
  517. return Container(
  518. width: 100,
  519. height: 100,
  520. color: Colors.grey[100],
  521. );
  522. }
  523. }),
  524. );
  525. }
  526. Future<void> pickVideo() async {
  527. final XFile? picked = await _picker.pickVideo(
  528. source: ImageSource.gallery,
  529. maxDuration: const Duration(minutes: 5),
  530. );
  531. if (picked != null) {
  532. setState(() {
  533. videoFile = picked;
  534. });
  535. }
  536. }
  537. Future<void> pickImages() async {
  538. final List<XFile>? images = await _picker.pickMultiImage();
  539. if (images != null && images.isNotEmpty) {
  540. setState(() {
  541. final remainingSlots = 9 - imageFiles.length;
  542. imageFiles.addAll(images.take(remainingSlots));
  543. });
  544. }
  545. }
  546. }