add_patrol.vue 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  1. <template>
  2. <view class="page-container">
  3. <view class="store-info-card">
  4. <view class="card-header">
  5. <uni-icons type="shop-filled" size="20" color="#3c82f8"></uni-icons>
  6. <text class="store-name">{{ storeInfo.name }}</text>
  7. </view>
  8. <view class="info-row">
  9. <text class="info-label">店主名称:</text>
  10. <text class="info-value">{{ storeInfo.owner }}</text>
  11. </view>
  12. <view class="info-row">
  13. <text class="info-label">联系方式:</text>
  14. <text class="info-value">{{ storeInfo.contact }}</text>
  15. </view>
  16. <view class="info-row">
  17. <text class="info-label">门店地址:</text>
  18. <text class="info-value">{{ storeInfo.address }}</text>
  19. </view>
  20. </view>
  21. <view class="patrol-content-card">
  22. <view class="form-item">
  23. <view class="form-label required">门头照片</view>
  24. <uni-file-picker
  25. :value="formData.storefrontPhoto"
  26. file-mediatype="image"
  27. mode="grid"
  28. ref="mentou"
  29. :limit="1"
  30. />
  31. </view>
  32. <view class="form-item">
  33. <view class="form-label required">陈列照片</view>
  34. <uni-file-picker
  35. :value="formData.displayPhoto"
  36. file-mediatype="image"
  37. mode="grid"
  38. ref="chenlie"
  39. :limit="9"
  40. />
  41. </view>
  42. <view class="form-item">
  43. <view class="form-label required">获取位置</view>
  44. <view class="location-wrapper" @click="getLocation">
  45. <text class="location-text" :class="{'placeholder': !locationInfo.address}">
  46. {{ locationInfo.address || '获取当前位置' }}
  47. </text>
  48. <uni-icons v-if="!isLocating" type="location-filled" size="20" color="#3c82f8"></uni-icons>
  49. <view v-if="isLocating" class="loading-spinner"></view>
  50. </view>
  51. </view>
  52. <view class="form-item">
  53. <view class="form-label">备注</view>
  54. <textarea
  55. class="remark-textarea"
  56. v-model="formData.remarks"
  57. placeholder="请输入备注信息"
  58. placeholder-class="placeholder"
  59. auto-height
  60. />
  61. </view>
  62. </view>
  63. <view class="footer-save-button">
  64. <button class="save-btn" @click="submitForm">提 交</button>
  65. </view>
  66. </view>
  67. </template>
  68. <script>
  69. import { getRetailDetail, uploadImage,addPatrolRecord,parseLocation} from "@/api/hexiao";
  70. export default {
  71. data() {
  72. return {
  73. loadding:false,
  74. // 假设的门店信息,从上个页面传来
  75. storeInfo: {
  76. id: 1,
  77. name: '',
  78. owner: '',
  79. contact: '',
  80. address: ''
  81. },
  82. // 表单数据
  83. formData: {
  84. storefrontPhoto: [], // 用于 file-picker 显示
  85. storefrontPhotoUrl: '', // 门头照上传后的URL
  86. displayPhoto: [], // 用于 file-picker 显示
  87. displayPhotoUrl: '', // 陈列照上传后的URL
  88. remarks: ''
  89. },
  90. // 位置信息
  91. locationInfo: {
  92. latitude: null,
  93. longitude: null,
  94. address: ''
  95. },
  96. isLocating: false, // 是否正在定位中
  97. };
  98. },
  99. onLoad(opt){
  100. this.storeInfo.id = opt.id;
  101. getRetailDetail(opt.id).then(res=>{
  102. let data = res.data;
  103. this.storeInfo = {
  104. id: data.id,
  105. name: data.store_name,
  106. owner: data.contact_name,
  107. contact: data.contact_phone,
  108. address: data.address,
  109. }
  110. })
  111. },
  112. methods: {
  113. // 文件选择后自动上传
  114. async handleFileSelect(type, e) {
  115. const tempFile = e.tempFiles[0];
  116. try {
  117. const imageUrl = await this.uploadImageToServer(tempFile.path);
  118. if (type === 'storefrontPhoto') {
  119. this.formData.storefrontPhotoUrl = imageUrl;
  120. this.formData.storefrontPhoto = [tempFile];
  121. } else if (type === 'displayPhoto') {
  122. this.formData.displayPhotoUrl = imageUrl;
  123. this.formData.displayPhoto = [tempFile];
  124. }
  125. uni.showToast({ title: '上传成功', icon: 'success' });
  126. } catch (error) {
  127. uni.showToast({ title: '上传失败', icon: 'none' });
  128. }
  129. },
  130. // 删除图片
  131. handleFileDelete(type) {
  132. if (type === 'storefrontPhoto') {
  133. this.formData.storefrontPhoto = [];
  134. this.formData.storefrontPhotoUrl = '';
  135. } else if (type === 'displayPhoto') {
  136. this.formData.displayPhoto = [];
  137. this.formData.displayPhotoUrl = '';
  138. }
  139. },
  140. // 获取地理位置
  141. getLocation() {
  142. this.isLocating = true;
  143. let self = this;
  144. uni.getLocation({
  145. type: 'gcj02', // 'wgs84' GPS坐标, 'gcj02' 国测局坐标
  146. isHighAccuracy: true,
  147. geocode: true, // 获取带有地址信息(仅App和H5支持)
  148. success: (res) => {
  149. console.log('当前位置信息:', res);
  150. this.locationInfo.latitude = res.latitude;
  151. this.locationInfo.longitude = res.longitude;
  152. parseLocation(res.latitude, res.longitude).then(res=>{
  153. if("Success" === res.data.message){
  154. let formatted_addresses = res.data.result.formatted_addresses
  155. self.locationInfo.address = formatted_addresses.standard_address
  156. }else{
  157. uni.showToast({ title: '解析地址失败', icon: 'none' });
  158. }
  159. })
  160. },
  161. fail: (err) => {
  162. uni.showToast({ title: '获取位置失败', icon: 'none' });
  163. console.error(err);
  164. },
  165. complete: () => {
  166. this.isLocating = false;
  167. }
  168. });
  169. },
  170. // 提交表单
  171. async submitForm() {
  172. if(this.$refs.chenlie.files.length === 0){
  173. return uni.showToast({ title: '请上传门头照片', icon: 'none' });
  174. }
  175. if(this.$refs.mentou.files.length === 0){
  176. return uni.showToast({ title: '请上传陈列照片', icon: 'none' });
  177. }
  178. const chenlieArr = await uploadImage(this.$refs.chenlie.files);
  179. const mentouArr = await uploadImage(this.$refs.mentou.files);
  180. // 数据校验
  181. if(mentouArr.length === 0){
  182. uni.showToast({ title: '门店照片上传失败,请重试', icon: 'none' });
  183. return;
  184. }
  185. if(chenlieArr.length === 0){
  186. uni.showToast({ title: '陈列照片上传失败,请重试', icon: 'none' });
  187. return;
  188. }
  189. if (!this.locationInfo.address) {
  190. return uni.showToast({ title: '请获取当前位置', icon: 'none' });
  191. }
  192. if(this.loadding){
  193. return ;
  194. }
  195. this.loadding = true;
  196. uni.showLoading({ title: '正在提交...' ,mask:true});
  197. const finalData = {
  198. storeId: this.storeInfo.id,
  199. storeImg: mentouArr[0],
  200. displayImg: chenlieArr,
  201. latitude: this.locationInfo.latitude,
  202. longitude: this.locationInfo.longitude,
  203. address: this.locationInfo.address,
  204. remark: this.formData.remarks
  205. };
  206. let res = await addPatrolRecord(finalData)
  207. if(res.code == 0){
  208. uni.showToast({ title: '提交成功', icon: 'success' });
  209. }else{
  210. uni.showToast({ title: '提交失败', icon: 'error' });
  211. }
  212. uni.hideLoading();
  213. setTimeout(() => uni.navigateBack(), 1000);
  214. },
  215. // 上传图片到您的后端服务器(此方法保持不变)
  216. uploadImageToServer(tempFilePath) {
  217. return new Promise((resolve, reject) => {
  218. uni.uploadFile({
  219. url: 'https://您的服务器地址/api/upload', // 【重要】请替换
  220. filePath: tempFilePath,
  221. name: 'file',
  222. success: (res) => {
  223. const data = JSON.parse(res.data);
  224. if (data.code === 0) {
  225. resolve(data.data.url);
  226. } else {
  227. reject(new Error(data.msg || '上传失败'));
  228. }
  229. },
  230. fail: (err) => { reject(err); }
  231. });
  232. });
  233. }
  234. }
  235. }
  236. </script>
  237. <style lang="scss" scoped>
  238. .page-container {
  239. min-height: 100vh;
  240. background: linear-gradient(to bottom, #e4efff, #f5f6fa 40%);
  241. padding: 30rpx;
  242. box-sizing: border-box;
  243. padding-bottom: 160rpx; // 为底部按钮留出空间
  244. }
  245. .store-info-card, .patrol-content-card {
  246. background-color: #ffffff;
  247. border-radius: 20rpx;
  248. padding: 30rpx;
  249. box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
  250. margin-bottom: 30rpx;
  251. }
  252. .store-info-card {
  253. .card-header {
  254. display: flex;
  255. align-items: center;
  256. .store-name { font-size: 32rpx; font-weight: bold; margin-left: 15rpx; }
  257. }
  258. .info-row {
  259. margin-top: 20rpx;
  260. font-size: 28rpx;
  261. .info-label { color: #999; }
  262. .info-value { color: #333; margin-left: 20rpx; }
  263. }
  264. }
  265. .patrol-content-card {
  266. padding: 10rpx 30rpx;
  267. }
  268. .form-item {
  269. padding: 30rpx 0;
  270. border-bottom: 1rpx solid #f0f0f0;
  271. &:last-child { border-bottom: none; }
  272. }
  273. .form-label {
  274. display: block;
  275. font-size: 30rpx;
  276. color: #333;
  277. font-weight: 500;
  278. margin-bottom: 20rpx;
  279. &.required::before {
  280. content: '*';
  281. color: #e54d42;
  282. margin-right: 8rpx;
  283. }
  284. }
  285. .location-wrapper {
  286. display: flex;
  287. align-items: center;
  288. justify-content: space-between;
  289. .location-text {
  290. font-size: 28rpx;
  291. color: #333;
  292. flex: 1;
  293. &.placeholder { color: #c0c4cc; }
  294. }
  295. }
  296. .remark-textarea {
  297. width: 100%;
  298. font-size: 28rpx;
  299. min-height: 150rpx;
  300. }
  301. .placeholder { color: #c0c4cc; }
  302. .footer-save-button {
  303. position: fixed;
  304. left: 0;
  305. bottom: 0;
  306. width: 100%;
  307. padding: 20rpx 30rpx;
  308. background-color: #f5f6fa;
  309. box-sizing: border-box;
  310. padding-bottom: calc(20rpx + constant(safe-area-inset-bottom));
  311. padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
  312. }
  313. .save-btn {
  314. background-color: #409eff;
  315. color: #ffffff;
  316. border-radius: 50rpx;
  317. font-size: 32rpx;
  318. height: 90rpx;
  319. line-height: 90rpx;
  320. &::after { border: none; }
  321. }
  322. .loading-spinner {
  323. width: 40rpx;
  324. height: 40rpx;
  325. border-radius: 50%;
  326. border: 4rpx solid #f3f3f3;
  327. border-top-color: #3498db;
  328. animation: spin 1s linear infinite;
  329. }
  330. @keyframes spin {
  331. to { transform: rotate(360deg); }
  332. }
  333. </style>