add_retail.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  1. <template>
  2. <view class="page-container">
  3. <!-- 隐藏的Canvas用于添加水印 -->
  4. <canvas
  5. id="watermarkCanvas"
  6. canvas-id="watermarkCanvas"
  7. :style="{width: canvasWidth + 'px', height: canvasHeight + 'px', position: 'fixed', left: '-9999px', top: '-9999px'}">
  8. </canvas>
  9. <view class="form-card">
  10. <view class="form-item">
  11. <text class="form-label">店铺名称</text>
  12. <input
  13. class="form-input"
  14. v-model="formData.storeName"
  15. placeholder="请输入店铺名称"
  16. placeholder-class="placeholder"
  17. />
  18. </view>
  19. <view class="form-item">
  20. <text class="form-label">店主名称</text>
  21. <input
  22. class="form-input"
  23. v-model="formData.ownerName"
  24. placeholder="请输入店主名称"
  25. placeholder-class="placeholder"
  26. />
  27. </view>
  28. <view class="form-item">
  29. <text class="form-label">联系方式</text>
  30. <input
  31. class="form-input"
  32. type="number"
  33. maxlength="11"
  34. v-model="formData.contact"
  35. placeholder="请输入联系方式"
  36. placeholder-class="placeholder"
  37. />
  38. </view>
  39. <view class="form-item" style="position: relative">
  40. <text class="form-label">店铺地址</text>
  41. <input
  42. class="form-input"
  43. v-model="formData.address"
  44. placeholder="请输入店铺地址"
  45. placeholder-class="placeholder"
  46. />
  47. <uni-icons @click="getLocation" style="position: absolute;bottom:20px;right: 20px;z-index: 999" type="location-filled" size="30" color="#3c82f8"></uni-icons>
  48. </view>
  49. <view class="form-item">
  50. <text class="form-label">店铺照片</text>
  51. <uni-file-picker
  52. class="file-picker"
  53. ref="files"
  54. :del-icon="edit"
  55. file-mediatype="image"
  56. mode="grid"
  57. :limit="9"
  58. title="最多选择9张图片"
  59. @tap="checkPermissionBeforeSelect"
  60. @select="handleFileSelect"
  61. v-model="formData.photos"
  62. />
  63. </view>
  64. </view>
  65. <view class="footer-save-button" v-if="edit">
  66. <button class="save-btn" @click="submitForm" >保 存</button>
  67. </view>
  68. </view>
  69. </template>
  70. <script>
  71. import {addStore, getRetailDetail, updateStore, uploadImage,parseLocation} from "@/api/hexiao";
  72. export default {
  73. data() {
  74. return {
  75. edit:false,
  76. isLocating:false,
  77. canvasWidth: 800,
  78. canvasHeight: 600,
  79. processingImages: false,
  80. imageAddress: '',
  81. // 表单数据
  82. formData: {
  83. photos: [],
  84. storeId: 0,
  85. storeName: '',
  86. ownerName: '',
  87. contact: '',
  88. address: ''
  89. }
  90. };
  91. },
  92. onLoad(opt){
  93. this.edit = opt.edit == 1;
  94. if(opt.id){
  95. this.formData.storeId = opt.id;
  96. getRetailDetail(opt.id).then(res=>{
  97. let data = res.data;
  98. this.formData = {
  99. storeId: data.id,
  100. storeName: data.store_name,
  101. ownerName: data.contact_name,
  102. contact: data.contact_phone,
  103. address: data.address,
  104. }
  105. this.formData.photos = [];
  106. let photos = data.store_photo.split(",")
  107. for (let i = 0; i < photos.length; i++) {
  108. let photo = photos[i];
  109. this.formData.photos.push({url:photo})
  110. }
  111. })
  112. }
  113. },
  114. methods: {
  115. // 点击时检查权限
  116. async checkPermissionBeforeSelect() {
  117. console.log("检查权限");
  118. try {
  119. // 检查定位权限状态
  120. const settingRes = await uni.getSetting();
  121. const locationAuth = settingRes.authSetting['scope.userLocation'];
  122. if (locationAuth === false) {
  123. // 已拒绝授权,提示用户
  124. const modalRes = await uni.showModal({
  125. title: '提示',
  126. content: '为了给图片添加地理位置水印,需要获取您的地理位置权限。是否前往设置开启?',
  127. confirmText: '去设置',
  128. cancelText: '取消'
  129. });
  130. if (modalRes.confirm) {
  131. // 打开设置页面
  132. const settingResult = await uni.openSetting();
  133. if (settingResult.authSetting['scope.userLocation']) {
  134. // 权限开启成功,先获取位置再选择图片
  135. await this.getLocationForImageSelection();
  136. this.$refs.files.choose();
  137. } else {
  138. uni.showToast({ title: '未开启定位权限,图片将不带位置水印', icon: 'none' });
  139. // 用户仍然可以选择图片,但没有位置水印
  140. this.$refs.files.choose();
  141. }
  142. } else {
  143. // 用户取消开启权限,仍然允许选择图片
  144. uni.showToast({ title: '未开启定位权限,图片将不带位置水印', icon: 'none' });
  145. this.$refs.files.choose();
  146. }
  147. } else {
  148. console.log("已获取定位权限");
  149. // 已有权限或未申请过权限,先获取位置再选择图片
  150. await this.getLocationForImageSelection();
  151. }
  152. } catch (error) {
  153. console.error('权限检查失败:', error);
  154. // 出现异常时,仍然允许选择图片
  155. uni.showToast({ title: '权限检查失败,图片将不带位置水印', icon: 'none' });
  156. }
  157. },
  158. // 专门用于图片选择的位置获取方法
  159. async getLocationForImageSelection() {
  160. await this.doGetLocation("imageAddress");
  161. },
  162. handleFileSelect(event) {
  163. this.onFileSelect(event);
  164. },
  165. // 修改后的添加水印处理函数
  166. async addWatermarkToImage(imagePath, address) {
  167. return new Promise((resolve) => {
  168. uni.getImageInfo({
  169. src: imagePath,
  170. success: (imageInfo) => {
  171. // 设置合理的画布尺寸
  172. let maxWidth = 800;
  173. let maxHeight = 1200;
  174. let width = imageInfo.width;
  175. let height = imageInfo.height;
  176. // 按比例缩放
  177. if (width > maxWidth || height > maxHeight) {
  178. let ratio = Math.min(maxWidth / width, maxHeight / height);
  179. width = width * ratio;
  180. height = height * ratio;
  181. }
  182. // 更新画布尺寸
  183. this.canvasWidth = Math.round(width);
  184. this.canvasHeight = Math.round(height);
  185. // 等待画布更新
  186. this.$nextTick(() => {
  187. setTimeout(() => {
  188. const ctx = uni.createCanvasContext('watermarkCanvas', this);
  189. // 清除画布
  190. ctx.clearRect(0, 0, width, height);
  191. // 绘制原图
  192. ctx.drawImage(imagePath, 0, 0, width, height);
  193. // 添加时间水印
  194. const now = new Date();
  195. const timeText = `${now.getFullYear()}-${(now.getMonth()+1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')} ${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`;
  196. // 设置水印样式
  197. ctx.setFontSize(24);
  198. ctx.setFillStyle('rgba(255, 255, 255, 0.8)');
  199. ctx.setTextAlign('right');
  200. // 添加时间水印
  201. ctx.fillText(timeText, width - 20, height - 40);
  202. // 添加地址水印
  203. if (address) {
  204. let addressText = address;
  205. if (address.length > 20) {
  206. addressText = address.substring(0, 20) + '...';
  207. }
  208. ctx.fillText(addressText, width - 20, height - 15);
  209. }
  210. // 绘制并导出
  211. ctx.draw(false, () => {
  212. setTimeout(() => {
  213. uni.canvasToTempFilePath({
  214. canvasId: 'watermarkCanvas',
  215. x: 0,
  216. y: 0,
  217. width: width,
  218. height: height,
  219. destWidth: width,
  220. destHeight: height,
  221. quality: 0.8,
  222. fileType: 'jpg',
  223. success: (res) => {
  224. console.log('水印图片生成成功:', res.tempFilePath);
  225. resolve(res.tempFilePath);
  226. },
  227. fail: (err) => {
  228. console.error('导出水印图片失败:', err);
  229. // 如果导出失败,直接返回原图
  230. resolve(imagePath);
  231. }
  232. }, this);
  233. }, 300);
  234. });
  235. }, 100);
  236. });
  237. },
  238. fail: (err) => {
  239. console.error('获取图片信息失败:', err);
  240. resolve(imagePath); // 获取图片信息失败时返回原图
  241. }
  242. });
  243. });
  244. },
  245. // 批量处理水印
  246. async processImagesWithWatermark(files, address) {
  247. const processedFiles = [];
  248. for (let i = 0; i < files.length; i++) {
  249. const file = files[i];
  250. const watermarkedPath = await this.addWatermarkToImage(file.path || file.url, address);
  251. processedFiles.push({
  252. ...file,
  253. path: watermarkedPath,
  254. url: watermarkedPath
  255. });
  256. }
  257. return processedFiles;
  258. },
  259. getLocation() {
  260. this.isLocating = true;
  261. let self = this;
  262. // 先检查定位权限
  263. uni.getSetting({
  264. success: (res) => {
  265. // 如果没有授权定位权限
  266. if (res.authSetting['scope.userLocation'] === false) {
  267. // 提示用户需要授权
  268. uni.showModal({
  269. title: '提示',
  270. content: '需要获取您的地理位置,请在设置中开启定位权限',
  271. confirmText: '去设置',
  272. cancelText: '取消',
  273. success: (modalRes) => {
  274. if (modalRes.confirm) {
  275. // 打开授权设置页面
  276. uni.openSetting({
  277. success: (settingRes) => {
  278. if (settingRes.authSetting['scope.userLocation']) {
  279. // 用户打开了定位权限,重新获取位置
  280. this.doGetLocation();
  281. } else {
  282. uni.showToast({ title: '未开启定位权限', icon: 'none' });
  283. this.isLocating = false;
  284. }
  285. },
  286. fail: () => {
  287. uni.showToast({ title: '打开设置失败', icon: 'none' });
  288. this.isLocating = false;
  289. }
  290. });
  291. } else {
  292. this.isLocating = false;
  293. }
  294. }
  295. });
  296. } else {
  297. // 已有权限或未申请过权限,直接获取位置
  298. this.doGetLocation();
  299. }
  300. },
  301. fail: () => {
  302. uni.showToast({ title: '权限检查失败', icon: 'none' });
  303. this.isLocating = false;
  304. }
  305. });
  306. },
  307. // 实际执行获取位置的方法
  308. doGetLocation(value) {
  309. let self = this;
  310. uni.getLocation({
  311. type: 'gcj02',
  312. isHighAccuracy: true,
  313. geocode: true,
  314. success: (res) => {
  315. console.log('当前位置信息:', res);
  316. parseLocation(res.latitude, res.longitude).then(res=>{
  317. if("Success" === res.data.message){
  318. let formatted_addresses = res.data.result.formatted_addresses
  319. if ( value === 'imageAddress'){
  320. self.imageAddress = formatted_addresses.standard_address;
  321. console.log('图片地址:', self.imageAddress);
  322. return;
  323. }
  324. self.formData.address = formatted_addresses.standard_address
  325. }else{
  326. uni.showToast({ title: '解析地址失败', icon: 'none' });
  327. }
  328. }).catch(err => {
  329. console.error('地址解析错误:', err);
  330. uni.showToast({ title: '地址解析失败', icon: 'none' });
  331. });
  332. },
  333. fail: (err) => {
  334. console.error('获取位置失败:', err);
  335. if (err.errCode === 103 || err.errMsg.includes('auth deny')) {
  336. uni.showToast({ title: '定位权限被拒绝', icon: 'none' });
  337. } else if (err.errCode === 101 || err.errMsg.includes('timeout')) {
  338. uni.showToast({ title: '定位超时,请稍后重试', icon: 'none' });
  339. } else {
  340. uni.showToast({ title: '获取位置失败', icon: 'none' });
  341. }
  342. },
  343. complete: () => {
  344. this.isLocating = false;
  345. }
  346. });
  347. },
  348. // 修改后的图片选择处理方法
  349. async onFileSelect(event) {
  350. if (this.processingImages) {
  351. uni.showToast({ title: '正在处理图片,请稍候', icon: 'none' });
  352. return;
  353. }
  354. this.processingImages = true;
  355. uni.showLoading({ title: '正在添加水印...' });
  356. const newFiles = event.tempFiles;
  357. try {
  358. // 对新选择的文件添加水印
  359. const watermarkedFiles = await this.processImagesWithWatermark(newFiles, this.imageAddress);
  360. // 保留之前已选择的图片,添加新的带水印图片
  361. const existingPhotos = this.formData.photos.filter(photo => photo.url);
  362. const newPhotos = watermarkedFiles.map(file => ({
  363. ...file,
  364. url: file.path || file.url
  365. }));
  366. this.formData.photos = [...existingPhotos, ...newPhotos];
  367. } catch (error) {
  368. console.error('处理图片失败:', error);
  369. uni.showToast({ title: '图片处理失败', icon: 'none' });
  370. } finally {
  371. this.processingImages = false;
  372. uni.hideLoading();
  373. }
  374. },
  375. // 提交表单
  376. async submitForm() {
  377. if(this.formData.photos.length === 0){
  378. uni.showToast({ title: '请上传门店照片', icon: 'none' });
  379. return;
  380. }
  381. // 数据校验
  382. if (!this.formData.storeName) {
  383. uni.showToast({ title: '请输入店铺名称', icon: 'none' });
  384. return;
  385. }
  386. if (!this.formData.ownerName) {
  387. uni.showToast({ title: '请输入店主名称', icon: 'none' });
  388. return;
  389. }
  390. if (!this.formData.contact) {
  391. uni.showToast({ title: '请输入联系方式', icon: 'none' });
  392. return;
  393. }
  394. if (!/^1[3-9]\d{9}$/.test(this.formData.contact)) {
  395. uni.showToast({ title: '请输入正确的手机号码', icon: 'none' });
  396. return;
  397. }
  398. if (!this.formData.address) {
  399. uni.showToast({ title: '请输入店铺地址', icon: 'none' });
  400. return;
  401. }
  402. uni.showLoading({ title: '正在保存...' });
  403. try {
  404. // 直接上传已添加水印的图片
  405. const filesToUpload = this.formData.photos.map(photo => ({
  406. path: photo.url || photo.path
  407. }));
  408. const imageUrlarr = await uploadImage(filesToUpload);
  409. if(imageUrlarr.length === 0){
  410. uni.showToast({ title: '门店照片上传失败,请重试', icon: 'none' });
  411. return;
  412. }
  413. if(this.formData.storeId>0){
  414. updateStore(this.formData.storeId,this.formData.storeName,this.formData.ownerName,this.formData.contact,this.formData.address,imageUrlarr.join(","))
  415. .then(res=>{
  416. uni.hideLoading();
  417. if(res.code == 0){
  418. uni.showToast({
  419. title: '保存成功',
  420. icon: 'success'
  421. });
  422. uni.navigateBack();
  423. }else{
  424. uni.showToast({
  425. title: res.msg,
  426. icon: 'none'
  427. });
  428. }
  429. })
  430. }else{
  431. addStore(this.formData.storeName,this.formData.ownerName,this.formData.contact,this.formData.address,imageUrlarr.join(","))
  432. .then(res=>{
  433. uni.hideLoading();
  434. if(res.code == 0){
  435. uni.showToast({
  436. title: '保存成功',
  437. icon: 'success'
  438. });
  439. uni.navigateBack();
  440. }else{
  441. uni.showToast({
  442. title: res.msg,
  443. icon: 'none'
  444. });
  445. }
  446. })
  447. }
  448. } catch (error) {
  449. uni.hideLoading();
  450. console.error('提交失败:', error);
  451. uni.showToast({
  452. title: '保存失败,请重试',
  453. icon: 'none'
  454. });
  455. }
  456. }
  457. }
  458. }
  459. </script>
  460. <style lang="scss" scoped>
  461. .page-container {
  462. min-height: 100vh;
  463. background: linear-gradient(to bottom, #e4efff, #f5f6fa 40%);
  464. padding: 30rpx;
  465. box-sizing: border-box;
  466. }
  467. .form-card {
  468. background-color: #ffffff;
  469. border-radius: 20rpx;
  470. padding: 0 40rpx; // 左右内边距
  471. box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.05);
  472. }
  473. .form-item {
  474. padding: 30rpx 0;
  475. border-bottom: 1rpx solid #f0f0f0;
  476. // 最后一个item不需要下边框
  477. &:last-child {
  478. border-bottom: none;
  479. }
  480. }
  481. .form-label {
  482. display: block; // 确保独占一行
  483. font-size: 30rpx;
  484. color: #333;
  485. font-weight: 500;
  486. }
  487. .form-input {
  488. margin-top: 20rpx; // 与label的间距
  489. font-size: 28rpx;
  490. color: #333;
  491. }
  492. .placeholder {
  493. color: #c0c4cc;
  494. }
  495. .footer-save-button {
  496. position: fixed;
  497. left: 0;
  498. bottom: 0;
  499. width: 100%;
  500. padding: 20rpx 30rpx;
  501. background-color: #f5f6fa;
  502. box-sizing: border-box;
  503. padding-bottom: calc(20rpx + constant(safe-area-inset-bottom));
  504. padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
  505. }
  506. .save-btn {
  507. background-color: #409eff;
  508. color: #ffffff;
  509. border-radius: 50rpx;
  510. font-size: 32rpx;
  511. height: 90rpx;
  512. line-height: 90rpx;
  513. &::after {
  514. border: none;
  515. }
  516. }
  517. /* 为uni-file-picker添加样式 */
  518. .file-picker {
  519. margin-top: 20rpx;
  520. }
  521. /* 确保图片预览正常显示 */
  522. ::v-deep .uni-file-picker__lists {
  523. .uni-file-picker__lists-item {
  524. image {
  525. width: 100%;
  526. height: 100%;
  527. }
  528. }
  529. }
  530. </style>