lrf 1 year ago
commit
718e32671b
20 changed files with 1112 additions and 0 deletions
  1. 2 0
      .gitignore
  2. 16 0
      .hbuilderx/launch.json
  3. 18 0
      App.vue
  4. 6 0
      config/local.js
  5. 6 0
      config/prod.js
  6. 20 0
      index.html
  7. 30 0
      main.js
  8. 73 0
      manifest.json
  9. 115 0
      package-lock.json
  10. 14 0
      package.json
  11. 42 0
      pages.json
  12. 99 0
      pages/index/index.vue
  13. 141 0
      pages/list/edit.vue
  14. 116 0
      pages/list/index.vue
  15. 124 0
      pages/list/item.vue
  16. 135 0
      pages/list/searchBar.vue
  17. BIN
      static/logo.png
  18. 10 0
      uni.promisify.adaptor.js
  19. 76 0
      uni.scss
  20. 69 0
      util/http.js

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+unpackage/
+node_modules/

+ 16 - 0
.hbuilderx/launch.json

@@ -0,0 +1,16 @@
+{ // launch.json 配置了启动调试时相关设置,configurations下节点名称可为 app-plus/h5/mp-weixin/mp-baidu/mp-alipay/mp-qq/mp-toutiao/mp-360/
+  // launchtype项可配置值为local或remote, local代表前端连本地云函数,remote代表前端连云端云函数
+    "version": "0.0",
+    "configurations": [{
+     	"default" : 
+     	{
+     		"launchtype" : "local"
+     	},
+     	"mp-weixin" : 
+     	{
+     		"launchtype" : "local"
+     	},
+     	"type" : "uniCloud"
+     }
+    ]
+}

+ 18 - 0
App.vue

@@ -0,0 +1,18 @@
+<script>
+	export default {
+		onLaunch: function() {
+			console.log('App Launch')
+		},
+		onShow: function() {
+			console.log('App Show')
+		},
+		onHide: function() {
+			console.log('App Hide')
+		}
+	}
+</script>
+
+<style lang="scss">
+	/*每个页面公共css */
+	@import "uview-plus/index.scss";
+</style>

+ 6 - 0
config/local.js

@@ -0,0 +1,6 @@
+const domain = 'http://127.0.0.1';
+const prefix = '/perfume/api'
+export {
+	domain,
+	prefix
+}

+ 6 - 0
config/prod.js

@@ -0,0 +1,6 @@
+const domain = 'https://broadcast.waityou24.cn';
+const prefix = '/perfume/api'
+export {
+	domain,
+	prefix
+}

+ 20 - 0
index.html

@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <script>
+      var coverSupport = 'CSS' in window && typeof CSS.supports === 'function' && (CSS.supports('top: env(a)') ||
+        CSS.supports('top: constant(a)'))
+      document.write(
+        '<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0' +
+        (coverSupport ? ', viewport-fit=cover' : '') + '" />')
+    </script>
+    <title></title>
+    <!--preload-links-->
+    <!--app-context-->
+  </head>
+  <body>
+    <div id="app"><!--app-html--></div>
+    <script type="module" src="/main.js"></script>
+  </body>
+</html>

+ 30 - 0
main.js

@@ -0,0 +1,30 @@
+import App from './App'
+
+// #ifndef VUE3
+import Vue from 'vue'
+import './uni.promisify.adaptor'
+Vue.config.productionTip = false
+App.mpType = 'app'
+const app = new Vue({
+  ...App
+})
+app.$mount()
+// #endif
+
+// #ifdef VUE3
+import { createSSRApp } from 'vue'
+import uviewPlus from 'uview-plus'
+import { api, upload } from './util/http.js';
+import { domain } from './config/prod'
+export function createApp() {
+  const app = createSSRApp(App)
+  app.config.globalProperties.$api = api;
+  app.config.globalProperties.$upload = upload;
+  app.config.globalProperties.$fileDomain = domain;
+  app.config.globalProperties.$configSign = 'lrfSelfUse';
+  app.use(uviewPlus)
+  return {
+    app
+  }
+}
+// #endif

+ 73 - 0
manifest.json

@@ -0,0 +1,73 @@
+{
+    "name" : "record",
+    "appid" : "__UNI__F0670FC",
+    "description" : "",
+    "versionName" : "1.0.0",
+    "versionCode" : "100",
+    "transformPx" : false,
+    /* 5+App特有相关 */
+    "app-plus" : {
+        "usingComponents" : true,
+        "nvueStyleCompiler" : "uni-app",
+        "compilerVersion" : 3,
+        "splashscreen" : {
+            "alwaysShowBeforeRender" : true,
+            "waiting" : true,
+            "autoclose" : true,
+            "delay" : 0
+        },
+        /* 模块配置 */
+        "modules" : {},
+        /* 应用发布信息 */
+        "distribute" : {
+            /* android打包配置 */
+            "android" : {
+                "permissions" : [
+                    "<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
+                    "<uses-permission android:name=\"android.permission.VIBRATE\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
+                    "<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CAMERA\"/>",
+                    "<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
+                    "<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
+                    "<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
+                    "<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
+                    "<uses-feature android:name=\"android.hardware.camera\"/>",
+                    "<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
+                ]
+            },
+            /* ios打包配置 */
+            "ios" : {},
+            /* SDK配置 */
+            "sdkConfigs" : {}
+        }
+    },
+    /* 快应用特有相关 */
+    "quickapp" : {},
+    /* 小程序特有相关 */
+    "mp-weixin" : {
+        "appid" : "wx87cf70e457acf176",
+        "setting" : {
+            "urlCheck" : false,
+            "minified" : true
+        },
+        "usingComponents" : true
+    },
+    "mp-alipay" : {
+        "usingComponents" : true
+    },
+    "mp-baidu" : {
+        "usingComponents" : true
+    },
+    "mp-toutiao" : {
+        "usingComponents" : true
+    },
+    "uniStatistics" : {
+        "enable" : false
+    },
+    "vueVersion" : "3"
+}

+ 115 - 0
package-lock.json

@@ -0,0 +1,115 @@
+{
+  "name": "record",
+  "version": "1.0.0",
+  "lockfileVersion": 2,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "record",
+      "version": "1.0.0",
+      "license": "MIT",
+      "dependencies": {
+        "uview-plus": "^3.1.36"
+      }
+    },
+    "node_modules/clipboard": {
+      "version": "2.0.11",
+      "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz",
+      "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==",
+      "dependencies": {
+        "good-listener": "^1.2.2",
+        "select": "^1.1.2",
+        "tiny-emitter": "^2.0.0"
+      }
+    },
+    "node_modules/dayjs": {
+      "version": "1.11.9",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz",
+      "integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA=="
+    },
+    "node_modules/delegate": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
+      "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw=="
+    },
+    "node_modules/good-listener": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz",
+      "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==",
+      "dependencies": {
+        "delegate": "^3.1.2"
+      }
+    },
+    "node_modules/select": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
+      "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA=="
+    },
+    "node_modules/tiny-emitter": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
+      "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
+    },
+    "node_modules/uview-plus": {
+      "version": "3.1.36",
+      "resolved": "https://registry.npmjs.org/uview-plus/-/uview-plus-3.1.36.tgz",
+      "integrity": "sha512-kGE5kA+H7x3WBGsGGKMYcNNSzX8W2tZKrne2KfAPpnCaCtFg92YBMnljQ9cU5E4gZwunnbSJD58CwfSxZJ/o/g==",
+      "dependencies": {
+        "clipboard": "^2.0.11",
+        "dayjs": "^1.11.3"
+      },
+      "engines": {
+        "HBuilderX": "^3.1.0"
+      }
+    }
+  },
+  "dependencies": {
+    "clipboard": {
+      "version": "2.0.11",
+      "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz",
+      "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==",
+      "requires": {
+        "good-listener": "^1.2.2",
+        "select": "^1.1.2",
+        "tiny-emitter": "^2.0.0"
+      }
+    },
+    "dayjs": {
+      "version": "1.11.9",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.9.tgz",
+      "integrity": "sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA=="
+    },
+    "delegate": {
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz",
+      "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw=="
+    },
+    "good-listener": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz",
+      "integrity": "sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==",
+      "requires": {
+        "delegate": "^3.1.2"
+      }
+    },
+    "select": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
+      "integrity": "sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA=="
+    },
+    "tiny-emitter": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
+      "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
+    },
+    "uview-plus": {
+      "version": "3.1.36",
+      "resolved": "https://registry.npmjs.org/uview-plus/-/uview-plus-3.1.36.tgz",
+      "integrity": "sha512-kGE5kA+H7x3WBGsGGKMYcNNSzX8W2tZKrne2KfAPpnCaCtFg92YBMnljQ9cU5E4gZwunnbSJD58CwfSxZJ/o/g==",
+      "requires": {
+        "clipboard": "^2.0.11",
+        "dayjs": "^1.11.3"
+      }
+    }
+  }
+}

+ 14 - 0
package.json

@@ -0,0 +1,14 @@
+{
+  "name": "record",
+  "version": "1.0.0",
+  "description": "",
+  "main": "main.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "author": "lrf",
+  "license": "MIT",
+  "dependencies": {
+    "uview-plus": "^3.1.36"
+  }
+}

+ 42 - 0
pages.json

@@ -0,0 +1,42 @@
+{
+	"easycom": {
+		"custom": {
+			"^u--(.*)": "uview-plus/components/u-$1/u-$1.vue",
+			"^up-(.*)": "uview-plus/components/u-$1/u-$1.vue",
+			"^u-([^-].*)": "uview-plus/components/u-$1/u-$1.vue"
+		}
+	},
+	"pages": [ //pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
+		{
+			"path": "pages/index/index",
+			"style": {
+				"navigationBarTitleText": "登录加载中..."
+			}
+		}
+	    ,{
+            "path" : "pages/list/index",
+            "style" :                                                                                    
+            {
+                "navigationBarTitleText": "记录首页",
+                "enablePullDownRefresh": false
+            }
+            
+        }
+        ,{
+            "path" : "pages/list/edit",
+            "style" :                                                                                    
+            {
+                "navigationBarTitleText": "记录编辑",
+                "enablePullDownRefresh": false
+            }
+            
+        }
+    ],
+	"globalStyle": {
+		"navigationBarTextStyle": "black",
+		"navigationBarTitleText": "uni-app",
+		"navigationBarBackgroundColor": "#F8F8F8",
+		"backgroundColor": "#F8F8F8"
+	},
+	"uniIdRouter": {}
+}

+ 99 - 0
pages/index/index.vue

@@ -0,0 +1,99 @@
+<template>
+	<view class="content" style="height:100vh">
+		<u-loading-icon mode="circle" size="64" :vertical="true" color="#2b85e4" textColor="#2b85e4"
+			text="正在登录"></u-loading-icon>
+	</view>
+</template>
+
+<script setup>
+	import {
+		getCurrentInstance,
+		reactive,
+		computed
+	} from 'vue'
+	const api = getCurrentInstance()?.appContext.config.globalProperties.$api;
+	const configSign = getCurrentInstance()?.appContext.config.globalProperties.$configSign;
+	const openid = computed(() => {
+		return uni.getStorageSync('openid');
+	})
+	const login = async (js_code) => {
+		const result = await api('https://broadcast.waityou24.cn/wechat/api/login/app', 'GET', {
+			js_code,
+			config: configSign
+		});
+		if (result.errcode === 0) return result.data.openid;
+		else {
+			uni.showToast({
+				title: '登录失败请重进',
+				icon: 'fail',
+			});
+			return false;
+		}
+	};
+	const initUser = async () => {
+		uni.login({
+			success: async function(result) {
+				if (!result.code) {
+					uni.showToast({
+						title: '登录失败请重进',
+						icon: 'fail',
+					});
+					return false;
+				}
+				let openid = uni.getStorageSync('openid');
+				if (!openid) {
+					const res = await login(result.code)
+					if (res) {
+						uni.setStorageSync('openid', res);
+						openid = res;
+					}
+				}
+				if (openid) {
+					const result = await api('/user', 'POST', {
+						openid
+					});
+					if(result.errcode!=0) {
+						uni.showToast({
+							title: result.errmsg,
+							icon: 'error',
+						});
+						return false;
+					}
+					uni.navigateTo({
+						url: '/pages/list/index'
+					})
+				}
+
+			}
+		})
+	};
+	initUser();
+</script>
+
+<style>
+	.content {
+		display: flex;
+		flex-direction: column;
+		align-items: center;
+		justify-content: center;
+	}
+
+	.logo {
+		height: 200rpx;
+		width: 200rpx;
+		margin-top: 200rpx;
+		margin-left: auto;
+		margin-right: auto;
+		margin-bottom: 50rpx;
+	}
+
+	.text-area {
+		display: flex;
+		justify-content: center;
+	}
+
+	.title {
+		font-size: 36rpx;
+		color: #8f8f94;
+	}
+</style>

+ 141 - 0
pages/list/edit.vue

@@ -0,0 +1,141 @@
+<template>
+  <view style="height:100vh;padding:20px">
+    <u--form labelPosition="left" :model="model" :rules="rules" ref="form" errorType="toast">
+      <u-form-item label="名称" prop="formData.name" borderBottom required>
+        <u--input v-model="model.formData.name"></u--input>
+      </u-form-item>
+      <u-form-item label="品牌" prop="formData.brand" borderBottom required>
+        <u--input v-model="model.formData.brand"></u--input>
+      </u-form-item>
+
+      <u-form-item label="规格" prop="formData.spec" borderBottom required>
+        <u--input v-model="model.formData.spec"></u--input>
+      </u-form-item>
+      <u-form-item label="是否购买" prop="formData.is_buy" borderBottom>
+        <u-radio-group v-model="model.formData.is_buy" placement="column">
+          <u-radio label="已购买" :name="true"></u-radio>
+          <u-radio label="未购买" :name="false"></u-radio>
+        </u-radio-group>
+      </u-form-item>
+      <u-form-item label="图片">
+        <u-upload :fileList="model.formData.files" @afterRead="afterRead" @delete="deletePic" multiple></u-upload>
+      </u-form-item>
+
+      <u-form-item borderBottom>
+        <up-button type="primary" shape="circle" @click="onSave()" text="保存"></up-button>
+      </u-form-item>
+    </u--form>
+  </view>
+</template>
+
+<script setup>
+import {
+  ref,
+  onMounted,
+  computed,
+  getCurrentInstance,
+} from 'vue'
+const $api = getCurrentInstance()?.appContext.config.globalProperties.$api;
+const $upload = getCurrentInstance()?.appContext.config.globalProperties.$upload;
+const $fileDomain = getCurrentInstance()?.appContext.config.globalProperties.$fileDomain;
+let id
+const search = async () => {
+  const res = await $api(`/perfume/${id}`)
+  if (res.errcode == 0) {
+    if (res.data) model.value.formData = res.data
+  }
+}
+onMounted(() => {
+  form.value.setRules(rules);
+  const crs = getCurrentPages()
+  const params = crs[crs.length - 1].options
+  if (params.id) {
+    id = params.id
+    search()
+  }
+})
+const form = ref()
+const model = ref({
+  formData: {
+    is_buy: true,
+    files: []
+  }
+})
+const rules = {
+  'formData.name': {
+    type: 'string',
+    required: true,
+    message: "请填写产品名称",
+    trigger: ['blur']
+  },
+  'formData.brand': {
+    type: 'string',
+    required: true,
+    message: "请填写品牌",
+    trigger: ['blur']
+  },
+  'formData.spec': {
+    type: 'string',
+    required: true,
+    message: "请填写规格",
+    trigger: ['blur']
+  }
+}
+
+// #region 图片上传
+const afterRead = async (e) => {
+  const uri = '/files/perfume/upload'
+  const list = []
+  for (const i of e.file) {
+    const res = await $upload(uri, i.url)
+    const obj = { url: `${$fileDomain}${res.uri}`, upload: true }
+    list.push(obj)
+  }
+  model.value.formData.files = list;
+}
+const deletePic = async (e) => {
+  const res = await $api('/perfume/deleteImg', 'DELETE', { url: e.file.url });
+  if (res.errcode === 0) {
+    // 前端
+    model.value.formData.files.splice(e.index)
+  }
+
+}
+// #endregion
+
+
+// #region 验证与保存
+const onSave = () => {
+  form.value.validate().then(res => {
+    save()
+  }).catch(errors => {
+    console.log(errors)
+  })
+}
+
+const save = async () => {
+  const data = model.value.formData;
+  let openid = uni.getStorageSync('openid');
+  let res;
+  if (!id) {
+    res = await $api('/perfume', 'POST', { ...data, openid })
+  } else {
+    res = await $api(`/perfume/${id}`, 'POST', { ...data, openid })
+  }
+  if (res.errcode == 0) {
+    uni.showToast({
+      title: '保存成功',
+      icon: 'succcess',
+    });
+    uni.navigateBack()
+  } else {
+    uni.showToast({
+      title: res.errmsg || '保存失败',
+      icon: 'fail',
+    });
+  }
+}
+// #endregion
+</script>
+
+<style></style>

+ 116 - 0
pages/list/index.vue

@@ -0,0 +1,116 @@
+<template>
+  <view class="u-page">
+    <view class="u-demo-block" style="padding:10px;height:100vh">
+      <view>
+        <search-bar @toFilterSearch="filterSearch"></search-bar>
+      </view>
+      <view v-if="list.length > 0">
+        <u-list @scrolltolower="scrollSearch">
+          <u-list-item v-for="i in list" :key="i._id">
+            <item :data="i" @toRefresh="refresh"></item>
+          </u-list-item>
+          <view v-if="noData" style="margin-bottom: 20px;">
+            <u-empty iconSize="0" text="没数据啦!点击按钮再次加载!">
+              <up-row>
+                <up-col>
+                  <up-button type="primary" icon="reload" shape="circle" @click="trySearch()"></up-button>
+                </up-col>
+              </up-row>
+            </u-empty>
+          </view>
+        </u-list>
+      </view>
+      <view v-else>
+        <u-empty mode="list">
+          <up-row>
+            <up-col>
+              <up-button type="primary" icon="reload" shape="circle" @click="trySearch()"></up-button>
+            </up-col>
+          </up-row>
+        </u-empty>
+      </view>
+    </view>
+    <view style="position: fixed;right:5vw;bottom:10vh;z-index:9999">
+      <up-row>
+        <up-col>
+          <up-button type="primary" icon="plus" shape="circle" @click="toAdd()"></up-button>
+        </up-col>
+      </up-row>
+    </view>
+
+  </view>
+</template>
+
+<script setup>
+import { getCurrentInstance, ref, onMounted } from 'vue';
+import item from './item.vue'
+import searchBar from './searchBar.vue'
+let skip = ref(0);
+let limit = ref(5);
+let list = ref([]);
+let total = ref(0);
+let noData = ref(false);
+let searchCondition = ref({});
+const search = async (skip = 0, limit = 5, add = true) => {
+  uni.showLoading({
+    title: '加载中...',
+    mask: true,
+    success: async () => {
+      try {
+        const query = { skip, limit, ...searchCondition.value };
+        const result = await $api('/perfume', 'GET', query);
+        if (result.errcode == 0) {
+          if (add) list.value.push(...result.data);
+          else list.value = result.data;
+          total.value = result.total
+          if (list.value.length >= result.total) noData.value = true
+        }
+        uni.hideLoading()
+      } catch (error) {
+        console.log(error)
+        uni.hideLoading()
+      }
+
+    },
+    fail: (e) => console.log(e)
+
+  })
+
+}
+const refresh = () => {
+  skip.value = 0
+  search()
+}
+onMounted(async () => {
+  await search()
+})
+const $api = getCurrentInstance()?.appContext.config.globalProperties.$api;
+
+const toAdd = () => {
+  uni.navigateTo({
+    url: `/pages/list/edit?type=add`,
+  })
+}
+const scrollSearch = () => {
+  if (noData.value) return
+  skip.value = skip.value + limit.value;
+  search(skip.value, limit.value)
+}
+const trySearch = async () => {
+  const res = await $api('/perfume/count', 'GET')
+  if (res.data > total.value) {
+    skip.value = total.value;
+    noData.value = false;
+    search(skip.value, limit.value)
+  }
+}
+const filterSearch = (data) => {
+  skip.value = 0;
+  total.value = 0;
+  noData.value = false;
+  searchCondition.value = data;
+  search(skip.value, limit.value, false);
+}
+</script>
+
+<style></style>

+ 124 - 0
pages/list/item.vue

@@ -0,0 +1,124 @@
+<template>
+  <view style="padding:10px;border-radius:10px" :class="`${data.is_buy ? 'is_buy' : 'not_buy'}`">
+    <up-row @click="toDetail(data)">
+      <up-col span="12">
+        <view class="top__blank">{{ data.name }}</view>
+      </up-col>
+    </up-row>
+    <up-row @click="toDetail(data)">
+      <up-col span="12">
+        <view class="top__blank">{{ data.brand }}</view>
+      </up-col>
+    </up-row>
+    <up-row @click="toDetail(data)">
+      <up-col span="12">
+        <view class="top__blank">{{ data.spec }}</view>
+      </up-col>
+    </up-row>
+
+    <up-row justify="center" gutter="10">
+      <up-col span="8">
+        <view class="top__blank" v-if="data.files?.length > 0" style="z-index:9998">
+          <u-swiper :list="data.files" keyName="url" height="80" circular @click="preview"></u-swiper>
+        </view>
+      </up-col>
+      <up-col span="3">
+        <up-button :customStyle="{ width: '50px' }" type="error" icon="trash" shape="circle"
+          @click="toDeleteConfirm()"></up-button>
+      </up-col>
+    </up-row>
+  </view>
+  <u-popup :show="show" mode="center" :customStyle="{ width: '80vw', height: '15vh' }">
+    <view style="margin:10px 30px">
+      <up-row :customStyle="{ padding: '10px' }">
+        <up-col span="12" :customStyle="{ 'text-align': 'center' }">
+          <text style="padding:10px">确认删除嘛?</text>
+        </up-col>
+      </up-row>
+      <up-row gutter="10" justify="between">
+        <up-col span="5">
+          <up-button :customStyle="{ width: '100px' }" @click="closeConfirm()">取消</up-button>
+        </up-col>
+        <up-col span="5">
+          <up-button :customStyle="{ width: '100px' }" type="success" @click="toDelete()">确认删除</up-button>
+        </up-col>
+      </up-row>
+    </view>
+  </u-popup>
+  <u-divider></u-divider>
+</template>
+
+<script setup>
+import {
+  defineProps,
+  ref,
+  getCurrentInstance,
+  defineEmits,
+} from 'vue'
+const $emit = defineEmits(['toRefresh'])
+const $api = getCurrentInstance()?.appContext.config.globalProperties.$api;
+const props = defineProps({
+  data: Object
+})
+const show = ref(false)
+const toDetail = (data) => {
+  uni.navigateTo({
+    url: `/pages/list/edit?id=${data._id}`,
+  })
+}
+const preview = (index) => {
+  const file = props?.data?.files
+  if (file && file.length > 0) {
+    const urls = file.map(i => i.url)
+    uni.previewImage({
+      current: index,
+      urls,
+    })
+  }
+}
+const toDeleteConfirm = () => {
+  show.value = true;
+}
+const closeConfirm = () => {
+  show.value = false;
+}
+const toDelete = async () => {
+  const data = props.data;
+  // 删除数据
+  const res = await $api(`/perfume/${data._id}`, 'DELETE')
+  if (res.errcode === 0) {
+    // 删除图片
+    const files = data.files;
+    for (const f of files) {
+      await $api('/perfume/deleteImg', 'DELETE', { url: f.url });
+    }
+    uni.showToast({
+      title: '删除成功',
+      icon: 'success',
+    });
+    $emit('toRefresh')
+  } else {
+    uni.showToast({
+      title: '删除失败',
+      icon: 'fail',
+    });
+    console.log(res.errmsg)
+  }
+  closeConfirm()
+}
+</script>
+
+<style scoped>
+.top__blank {
+  padding-top: 2px;
+  padding-bottom: 3px;
+}
+
+.is_buy {
+  background: #71d5a1;
+}
+
+.not_buy {
+  background: #fab6b6;
+}
+</style>

+ 135 - 0
pages/list/searchBar.vue

@@ -0,0 +1,135 @@
+<template>
+  <div id="searchBar" style="margin:10px 0px">
+    <up-row>
+      <up-col :span="12">
+        <u-search v-model="obj.name" :show-action="true" actionText="更多" :animation="true" :clearable="true"
+          placeholder="请输入商品名称" @search="toSearch" @custom="showMoreFilter"></u-search>
+      </up-col>
+    </up-row>
+    <u-popup :show="show" mode="right">
+      <view class="search__input">
+        <up-row>
+          <up-col :span="12">
+            <up-input placeholder="请输入商品名称" v-model="obj.name"></up-input>
+          </up-col>
+        </up-row>
+      </view>
+      <view class="search__input">
+        <up-row>
+          <up-col :span="12">
+            <u-cell :title="obj.brand" :label="getLabel('brand')" :isLink="true" arrowDirection="right"
+              @click="toSelect('brand')"></u-cell>
+          </up-col>
+        </up-row>
+      </view>
+      <view class="search__input">
+        <up-row>
+          <up-col :span="12">
+            <u-cell :title="obj.spec" :label="getLabel('spec')" :isLink="true" arrowDirection="right"
+              @click="toSelect('spec')"></u-cell>
+          </up-col>
+        </up-row>
+      </view>
+      <view class="search__input">
+        <up-row gutter="10" justify="around">
+          <up-col span="4">
+            <up-button @click="searchCancel">取消</up-button>
+          </up-col>
+          <up-col span="4">
+            <up-button @click="toMultSearch" type="primary">查询</up-button>
+          </up-col>
+        </up-row>
+      </view>
+    </u-popup>
+    <u-action-sheet :actions="selectionList" :title="selectionTitle" :show="sheetShow"
+      @select="sheetClick"></u-action-sheet>
+  </div>
+</template>
+
+<script setup>
+import { ref, defineEmits, getCurrentInstance, onMounted } from 'vue';
+const $emit = defineEmits(['toFilterSearch'])
+let obj = ref({});
+let show = ref(false);
+let brandSelection = ref([]);
+let specSelection = ref([]);
+let selectionList = ref([]);
+let selection = ref(''); // brand,spec
+let selectionTitle = ref('请选择');
+let sheetShow = ref(false);
+const $api = getCurrentInstance()?.appContext.config.globalProperties.$api;
+// #region 更多内容查询
+const showMoreFilter = () => {
+  show.value = true;
+}
+const searchCancel = () => {
+  delete obj.value.brand;
+  delete obj.value.spec;
+  show.value = false;
+}
+const toMultSearch = () => {
+  $emit('toFilterSearch', obj.value)
+  show.value = false;
+}
+// #endregion
+
+/**商品名称搜索框向上提交查询 */
+const toSearch = () => {
+  $emit('toFilterSearch', { name: obj.value.name })
+}
+
+// #region 品牌与规格选项请求及渲染
+onMounted(() => {
+  searchCondition();
+})
+const searchCondition = async () => {
+  const res = await $api('/perfume/getSelection', 'GET')
+  if (res.errcode !== 0) return;
+  const noChoose = { name: '不选择', value: undefined }
+  const { brandList = [], specList = [] } = res.data;
+  brandSelection.value = brandList.map(i => ({ name: i, value: i }));
+  brandSelection.value.unshift(noChoose)
+  specSelection.value = specList.map(i => ({ name: i, value: i }));
+  specSelection.value.unshift(noChoose)
+}
+const getLabel = (type) => {
+  const val = obj.value[type]
+  if (!val && type === 'brand') return '请选择商品品牌'
+  if (!val && type === 'spec') return '请选择商品规格'
+}
+// #endregion
+// #region sheet选择框操作
+const toSelect = (type) => {
+  selection.value = type;
+  let list = [];
+  let tit = '请选择'
+  if (type === 'brand') {
+    list = brandSelection.value
+    tit = `${tit}商品品牌`
+  }
+  else if (type === 'spec') {
+    list = specSelection.value
+    tit = `${tit}商品规格`
+  }
+  selectionList.value = list;
+  selectionTitle.value = tit;
+  sheetShow.value = true;
+}
+const sheetClick = (data) => {
+  obj.value[selection.value] = data.value
+  closeSheet();
+}
+const closeSheet = () => {
+  selection.value = undefined;
+  selectionList.value = [];
+  selectionTitle.value = '请选择'
+  sheetShow.value = false;
+}
+// #endregion
+</script>
+
+<style scoped>
+.search__input {
+  margin: 10px 5px;
+}
+</style>

BIN
static/logo.png


+ 10 - 0
uni.promisify.adaptor.js

@@ -0,0 +1,10 @@
+uni.addInterceptor({
+  returnValue (res) {
+    if (!(!!res && (typeof res === "object" || typeof res === "function") && typeof res.then === "function")) {
+      return res;
+    }
+    return new Promise((resolve, reject) => {
+      res.then((res) => res[0] ? reject(res[0]) : resolve(res[1]));
+    });
+  },
+});

+ 76 - 0
uni.scss

@@ -0,0 +1,76 @@
+/**
+ * 这里是uni-app内置的常用样式变量
+ *
+ * uni-app 官方扩展插件及插件市场(https://ext.dcloud.net.cn)上很多三方插件均使用了这些样式变量
+ * 如果你是插件开发者,建议你使用scss预处理,并在插件代码中直接使用这些变量(无需 import 这个文件),方便用户通过搭积木的方式开发整体风格一致的App
+ *
+ */
+
+/**
+ * 如果你是App开发者(插件使用者),你可以通过修改这些变量来定制自己的插件主题,实现自定义主题功能
+ *
+ * 如果你的项目同样使用了scss预处理,你也可以直接在你的 scss 代码中使用如下变量,同时无需 import 这个文件
+ */
+
+/* 颜色变量 */
+@import 'uview-plus/theme.scss';
+/* 行为相关颜色 */
+$uni-color-primary: #007aff;
+$uni-color-success: #4cd964;
+$uni-color-warning: #f0ad4e;
+$uni-color-error: #dd524d;
+
+/* 文字基本颜色 */
+$uni-text-color:#333;//基本色
+$uni-text-color-inverse:#fff;//反色
+$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
+$uni-text-color-placeholder: #808080;
+$uni-text-color-disable:#c0c0c0;
+
+/* 背景颜色 */
+$uni-bg-color:#ffffff;
+$uni-bg-color-grey:#f8f8f8;
+$uni-bg-color-hover:#f1f1f1;//点击状态颜色
+$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色
+
+/* 边框颜色 */
+$uni-border-color:#c8c7cc;
+
+/* 尺寸变量 */
+
+/* 文字尺寸 */
+$uni-font-size-sm:12px;
+$uni-font-size-base:14px;
+$uni-font-size-lg:16;
+
+/* 图片尺寸 */
+$uni-img-size-sm:20px;
+$uni-img-size-base:26px;
+$uni-img-size-lg:40px;
+
+/* Border Radius */
+$uni-border-radius-sm: 2px;
+$uni-border-radius-base: 3px;
+$uni-border-radius-lg: 6px;
+$uni-border-radius-circle: 50%;
+
+/* 水平间距 */
+$uni-spacing-row-sm: 5px;
+$uni-spacing-row-base: 10px;
+$uni-spacing-row-lg: 15px;
+
+/* 垂直间距 */
+$uni-spacing-col-sm: 4px;
+$uni-spacing-col-base: 8px;
+$uni-spacing-col-lg: 12px;
+
+/* 透明度 */
+$uni-opacity-disabled: 0.3; // 组件禁用态的透明度
+
+/* 文章场景相关 */
+$uni-color-title: #2C405A; // 文章标题颜色
+$uni-font-size-title:20px;
+$uni-color-subtitle: #555555; // 二级标题颜色
+$uni-font-size-subtitle:26px;
+$uni-color-paragraph: #3F536E; // 文章段落颜色
+$uni-font-size-paragraph:15px;

+ 69 - 0
util/http.js

@@ -0,0 +1,69 @@
+import * as local from '../config/local.js';
+import * as prod from '../config/prod.js';
+let domain, prefix;
+if (process.env.NODE_ENV === 'development') {
+  domain = local.domain;
+  prefix = local.prefix;
+} else {
+  domain = prod.domain;
+  prefix = prod.prefix;
+}
+export const api = async (uri, method = 'GET', data) => {
+  if (!Array.isArray(data)) {
+    for (const key in data) {
+      if (data[key] === undefined) {
+        delete data[key];
+      }
+    }
+  }
+  let url;
+  if (uri.includes('http') || uri.includes('https')) url = uri;
+  else url = `${domain}${prefix}${uri}`;
+  let openid = uni.getStorageSync('openid');
+  return new Promise((resolve, reject) => {
+    uni.request({
+      url,
+      method: method || 'GET',
+      data: data || {},
+      header: {
+        auth: openid,
+      },
+      success: (res) => {
+        resolve(res.data);
+      },
+      fail: (err) => {
+        uni.showToast({
+          title: '请求接口失败',
+          icon: 'fail',
+        });
+        reject(err.data);
+      },
+    });
+  });
+};
+
+export const upload = async (uri, data) => {
+  let url;
+  if (uri.includes('http') || uri.includes('https')) url = uri;
+  else url = `${prod.domain}${uri}`;
+  return new Promise((resolve, reject) => {
+    uni.uploadFile({
+      url,
+      name: 'file',
+      formData: {},
+      filePath: data,
+      success: (res) => {
+        if (res?.data) resolve(JSON.parse(res.data));
+        else resolve();
+      },
+      fail: (err) => {
+        console.log(err);
+        uni.showToast({
+          title: '文件上传失败',
+          icon: 'fail',
+        });
+        reject(err.data);
+      },
+    });
+  });
+};