我使用 Google Sheets 作为数据源,并在 Google Sheets 中使用 Google Apps 脚本设置了 Google Sheets API。使用Postman测试时,数据成功进入Google Sheets。但是,运行 Flutter 应用程序时,数据不会记录在 Google Sheets 中,并且会显示一条提及“XMLHTTPRequest”的错误。
应用程序脚本中的代码:
// Function to handle GET requests`your text`
function doGet(e) {
return ContentService.createTextOutput('GET request received. Please use POST method to send data.')
.setMimeType(ContentService.MimeType.TEXT);
}
var sheet = SpreadsheetApp.openByUrl('https://docs.google.com/spreadsheets.......').getSheetByName('Sheet1');
// Function to handle POST requests
function doPost(e) {
function doPost(e) {
var response = {status: 'success', message: 'Data received'};
return ContentService.createTextOutput(JSON.stringify(response))
.setMimeType(ContentService.MimeType.JSON)
.setHeader('Access-Control-Allow-Origin', '*')
.setHeader('Access-Control-Allow-Methods', 'POST')
.setHeader('Access-Control-Allow-Headers', 'Content-Type');
}
if (!e || !e.postData || !e.postData.contents) {
return ContentService.createTextOutput(JSON.stringify({status: 'error', message: 'Invalid POST request'}))
.setMimeType(ContentService.MimeType.JSON);
}
try {
// Parse the JSON data from the POST request
var jsonData = JSON.parse(e.postData.contents);
// Extract data (assuming the JSON object has the correct keys)
var branch = jsonData.branch || '';
var shop = jsonData.shop || '';
var deliveryMode = jsonData.deliveryMode || '';
var creditDays = jsonData.creditDays || '';
var discountPercentage = jsonData.discountPercentage || '';
var remarks = jsonData.remarks || '';
var items = jsonData.items || [];
var totalQuantity = jsonData.totalQuantity || '';
var subtotal = jsonData.subtotal || '';
var discountAmount = jsonData.discountAmount || '';
var netAmount = jsonData.netAmount || '';
items.forEach(function(item) {
var itemName = item.itemName || '';
var quantity = item.quantity || '';
var rate = item.rate || '';
var total = item.total || '';
// Append data to the sheet
sheet.appendRow([branch, shop, deliveryMode, creditDays, discountPercentage, remarks, itemName, quantity, rate, total,totalQuantity,subtotal,discountAmount,netAmount]);
});
// Return a success response
return ContentService.createTextOutput(JSON.stringify({status: 'success'}))
.setMimeType(ContentService.MimeType.JSON);
} catch (error) {
return ContentService.createTextOutput(JSON.stringify({status: 'error', message: error.toString()}))
.setMimeType(ContentService.MimeType.JSON);
}
}
可视化代码中main.dart中的代码:
import 'package:flutter/material.dart';
import 'dart:convert';
import 'package:http/http.dart' as http;
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'SOB',
theme: ThemeData(
primarySwatch: Colors.blue,
primaryColor: Colors.blue,
colorScheme: ColorScheme.fromSwatch().copyWith(secondary: Colors.orange),
fontFamily: 'Roboto',
),
home: HomePage(),
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Home Page'),
),
body: Center(
child: ElevatedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SalesOrderForm()),
);
},
child: Text('Go to Sales Order Form'),
),
),
);
}
}
class SalesOrderForm extends StatefulWidget {
@override
_SalesOrderFormState createState() => _SalesOrderFormState();
}
class _SalesOrderFormState extends State<SalesOrderForm> {
String? _selectedBranch;
String? _selectedShop;
String? _selectedDeliveryMode;
List<String> _branches = ['Branch A', 'Branch B', 'Branch C'];
List<String> _shops = ['Shop 1', 'Shop 2', 'Shop 3'];
List<String> _deliveryModes = ['Standard', 'Express', 'Next Day'];
final TextEditingController _creditDaysController = TextEditingController();
final TextEditingController _discountPercentageController = TextEditingController();
final TextEditingController _remarksController = TextEditingController();
List<Item> _items = [];
final _formKey = GlobalKey<FormState>();
double get subtotal {
return _items.fold(0.0, (sum, item) => sum + item.total);
}
double get discountAmount {
double discountPercentage = double.tryParse(_discountPercentageController.text) ?? 0.0;
return subtotal * (discountPercentage / 100);
}
double get netAmount {
return subtotal - discountAmount;
}
int get totalQuantity {
return _items.fold(0, (sum, item) => sum + item.quantity);
}
@override
void dispose() {
_creditDaysController.dispose();
_discountPercentageController.dispose();
_remarksController.dispose();
super.dispose();
}
void _updateUI() {
setState(() {});
}
Future<void> _submitForm() async {
if (_formKey.currentState!.validate()) {
try {
final response = await http.post(
Uri.parse('https://script.google.com/macros/s/...../exec'),
headers: <String, String>{
'Content-Type': 'application/json; charset=UTF-8',
},
body: jsonEncode(<String, dynamic>{
'branch': _selectedBranch,
'shop': _selectedShop,
'deliveryMode': _selectedDeliveryMode,
'creditDays': _creditDaysController.text,
'discountPercentage': _discountPercentageController.text,
'remarks': _remarksController.text,
'items': _items.map((item) => item.toMap()).toList(),
'totalQuantity': totalQuantity.toString(),
'subtotal': subtotal.toString(),
'discountAmount': discountAmount.toString(),
'netAmount': netAmount.toString(),
}),
);
if (response.statusCode == 200) {
final result = json.decode(response.body);
if (result['status'] == 'success') {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Data updated successfully')));
} else {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to update data: ${result['message']}')));
}
} else {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Failed to update data')));
}
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Error: ${e.toString()}')));
}
}
}
void _addItem() {
setState(() {
_items.add(Item());
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Sales Order Form'),
centerTitle: true,
),
body: Padding(
padding: EdgeInsets.all(16.0),
child: SingleChildScrollView(
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(
'Branch Name',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
DropdownButtonFormField<String>(
decoration: InputDecoration(
filled: true,
fillColor: Colors.white24,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10.0),
),
),
value: _selectedBranch,
onChanged: (newValue) {
setState(() {
_selectedBranch = newValue;
});
},
items: _branches.map((branch) {
return DropdownMenuItem<String>(
value: branch,
child: Text(branch),
);
}).toList(),
validator: (value) => value == null ? 'Please select a branch' : null,
),
SizedBox(height: 20),
Text(
'Shop Name',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
DropdownButtonFormField<String>(
decoration: InputDecoration(
filled: true,
fillColor: Colors.white24,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10.0),
),
),
value: _selectedShop,
onChanged: (newValue) {
setState(() {
_selectedShop = newValue;
});
},
items: _shops.map((shop) {
return DropdownMenuItem<String>(
value: shop,
child: Text(shop),
);
}).toList(),
validator: (value) => value == null ? 'Please select a shop' : null,
),
SizedBox(height: 20),
Text(
'Delivery Mode',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
DropdownButtonFormField<String>(
decoration: InputDecoration(
filled: true,
fillColor: Colors.white24,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10.0),
),
),
value: _selectedDeliveryMode,
onChanged: (newValue) {
setState(() {
_selectedDeliveryMode = newValue;
});
},
items: _deliveryModes.map((mode) {
return DropdownMenuItem<String>(
value: mode,
child: Text(mode),
);
}).toList(),
validator: (value) => value == null ? 'Please select a delivery mode' : null,
),
SizedBox(height: 20),
Text(
'Credit Days',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
TextFormField(
controller: _creditDaysController,
decoration: InputDecoration(
filled: true,
fillColor: Colors.white24,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10.0),
),
),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter credit days';
}
if (int.tryParse(value) == null) {
return 'Please enter a valid number';
}
return null;
},
),
SizedBox(height: 20),
Text(
'Discount Percentage',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
TextFormField(
controller: _discountPercentageController,
decoration: InputDecoration(
filled: true,
fillColor: Colors.white24,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10.0),
),
),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return 'Please enter discount percentage';
}
if (double.tryParse(value) == null) {
return 'Please enter a valid number';
}
return null;
},
),
SizedBox(height: 20),
Text(
'Remarks',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
TextFormField(
controller: _remarksController,
decoration: InputDecoration(
filled: true,
fillColor: Colors.white24,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10.0),
),
),
keyboardType: TextInputType.multiline,
maxLines: 3,
),
SizedBox(height: 20),
Text(
'Items',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
ListView.builder(
shrinkWrap: true,
itemCount: _items.length,
itemBuilder: (context, index) {
return ItemWidget(
item: _items[index],
onDelete: () {
setState(() {
_items.removeAt(index);
});
},
onUpdate: _updateUI,
);
},
),
TextButton.icon(
onPressed: _addItem,
icon: Icon(Icons.add),
label: Text('Add Item'),
),
SizedBox(height: 20),
Text(
'Total Quantity: $totalQuantity',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
Text(
'Subtotal: \$${subtotal.toStringAsFixed(2)}',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
Text(
'Discount Amount: \$${discountAmount.toStringAsFixed(2)}',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
Text(
'Net Amount: \$${netAmount.toStringAsFixed(2)}',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _submitForm,
child: Text('Submit'),
),
],
),
),
),
),
);
}
}
class Item {
String itemName;
int quantity;
double rate;
Item({this.itemName = '', this.quantity = 0, this.rate = 0.0});
double get total => quantity * rate;
Map<String, dynamic> toMap() {
return {
'itemName': itemName,
'quantity': quantity,
'rate': rate,
'total': total,
};
}
}
class ItemWidget extends StatelessWidget {
final Item item;
final VoidCallback onDelete;
final VoidCallback onUpdate;
const ItemWidget({
required this.item,
required this.onDelete,
required this.onUpdate,
});
@override
Widget build(BuildContext context) {
final TextEditingController itemNameController = TextEditingController(text: item.itemName);
final TextEditingController quantityController = TextEditingController(text: item.quantity.toString());
final TextEditingController rateController = TextEditingController(text: item.rate.toString());
return Card(
margin: EdgeInsets.symmetric(vertical: 8.0),
child: Padding(
padding: EdgeInsets.all(8.0),
child: Column(
children: [
TextFormField(
controller: itemNameController,
decoration: InputDecoration(labelText: 'Item Name'),
onChanged: (value) {
item.itemName = value;
onUpdate();
},
),
TextFormField(
controller: quantityController,
decoration: InputDecoration(labelText: 'Quantity'),
keyboardType: TextInputType.number,
onChanged: (value) {
item.quantity = int.tryParse(value) ?? 0;
onUpdate();
},
),
TextFormField(
controller: rateController,
decoration: InputDecoration(labelText: 'Rate'),
keyboardType: TextInputType.number,
onChanged: (value) {
item.rate = double.tryParse(value) ?? 0.0;
onUpdate();
},
),
SizedBox(height: 8.0),
Text('Total: \$${item.total.toStringAsFixed(2)}'),
SizedBox(height: 8.0),
TextButton.icon(
onPressed: onDelete,
icon: Icon(Icons.delete),
label: Text('Delete'),
style: TextButton.styleFrom(foregroundColor: Colors.red),
),
],
),
),
);
}
}
函数
doPost
也声明了一个名为doPost
的内部函数,但是这个内部doPost函数没有被调用。
可能还有其他问题,但您应该从解决这个问题开始。