feat: 增加后端API接口

This commit is contained in:
2026-09-09 19:58:24 +08:00
parent c939d6df3b
commit 5f270b05d6
21 changed files with 1220 additions and 134 deletions

View File

@@ -55,6 +55,7 @@
"@dcloudio/uni-quickapp-webview": "3.0.0-5020420260813003",
"@wot-ui/ui": "^2.3.2",
"miniprogram-api-typings": "^5.2.3",
"pinia": "^4.0.3",
"vue": "^3.4.21",
"vue-i18n": "^9.1.9"
},

21
pnpm-lock.yaml generated
View File

@@ -62,6 +62,9 @@ importers:
miniprogram-api-typings:
specifier: ^5.2.3
version: 5.2.3
pinia:
specifier: ^4.0.3
version: 4.0.3(@vue/devtools-api@6.6.4)(typescript@4.9.5)(vue@3.5.42(typescript@4.9.5))
vue:
specifier: ^3.4.21
version: 3.5.42(typescript@4.9.5)
@@ -3578,6 +3581,16 @@ packages:
resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==}
engines: {node: '>=12'}
pinia@4.0.3:
resolution: {integrity: sha512-XMQqpvjgG7LMqVhhFUzKLT4KEbsYbfZZ0CZU9PgdFm1O2VKBmIkykL8+SfNhOaT7sR0wT6BEgKT0YNDYWgmVRA==}
peerDependencies:
'@vue/devtools-api': ^8.1.5
typescript: '>=5.6.0'
vue: ^3.5.11
peerDependenciesMeta:
typescript:
optional: true
pirates@4.0.7:
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
engines: {node: '>= 6'}
@@ -9039,6 +9052,14 @@ snapshots:
picomatch@4.0.7: {}
pinia@4.0.3(@vue/devtools-api@6.6.4)(typescript@4.9.5)(vue@3.5.42(typescript@4.9.5)):
dependencies:
'@vue/devtools-api': 6.6.4
nostics: 1.2.0
vue: 3.5.42(typescript@4.9.5)
optionalDependencies:
typescript: 4.9.5
pirates@4.0.7: {}
pixelmatch@4.0.2:

24
project.config.json Normal file
View File

@@ -0,0 +1,24 @@
{
"setting": {
"es6": true,
"postcss": true,
"minified": true,
"uglifyFileName": false,
"enhance": true,
"packNpmRelationList": [],
"babelSetting": {
"ignore": [],
"disablePlugins": [],
"outputPath": ""
},
"useCompilerPlugins": false
},
"compileType": "miniprogram",
"simulatorPluginLibVersion": {},
"packOptions": {
"ignore": [],
"include": []
},
"appid": "wxf7a95cb4373170c1",
"editorSetting": {}
}

View File

@@ -0,0 +1,14 @@
{
"libVersion": "3.17.2",
"projectname": "patch-mp",
"setting": {
"urlCheck": true,
"coverView": true,
"lazyloadPlaceholderEnable": false,
"skylineRenderEnable": false,
"preloadBackgroundData": false,
"autoAudits": false,
"showShadowRootInWxmlPanel": true,
"compileHotReLoad": true
}
}

View File

@@ -0,0 +1,124 @@
<template>
<wd-popup
v-model="patchStore.showCropPopup"
position="bottom"
:safe-area-inset-bottom="true"
:close-on-click-modal="true"
custom-style="border-radius: 32rpx 32rpx 0 0; padding: 40rpx 32rpx"
>
<view class="mb-5 text-center text-base text-gray-700 font-semibold">
{{ patchStore.cropApiType === 'ADD' ? '新增作物' : '编辑作物' }}
</view>
<wd-form
ref="form"
:model="patchStore.cropForm"
layout="horizontal"
title-width="80"
size="large"
border
>
<wd-form-item title="作物名称">
<wd-input
v-model="patchStore.cropForm.name"
:maxlength="20"
show-word-limit
placeholder="请输入作物名称"
/>
</wd-form-item>
<wd-form-item title="作物种类">
<wd-input
v-model="patchStore.cropForm.variety"
:maxlength="20"
show-word-limit
placeholder="请输入作物种类"
/>
</wd-form-item>
<wd-form-item
title="种植日期"
prop="date"
is-link
:value="patchStore.cropForm.date"
placeholder="请选择种植日期"
@click="showTimePicker = true"
/>
</wd-form>
<view class="flex w-full items-center justify-evenly">
<wd-button type="info" @click="cancelClick">
取消
</wd-button>
<wd-button v-if="patchStore.cropApiType === 'UPDATE'" type="danger" @click="deleteClick">
删除
</wd-button>
<wd-button type="primary" @click="saveClick">
保存
</wd-button>
</view>
<wd-datetime-picker
:model-value="new Date(patchStore.recordForm.date).getTime()"
v-model:visible="showTimePicker"
type="date"
:max-date="Date.now()"
placeholder="请选择记录时间"
@confirm="onDateConfirm"
/>
</wd-popup>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { usePatchStore } from "@/stores/patch";
import { formatDate } from "@/utils";
const patchStore = usePatchStore()
const showTimePicker = ref(false)
function onDateConfirm({ value }){
patchStore.recordForm.date = formatDate(new Date(value))
showTimePicker.value = false
}
function cancelClick() {
patchStore.showRecordPopup = false
}
async function deleteClick() {
wx.showModal({
title: '提示',
content: `确定删除 ${patchStore.cropForm.name} 作物吗?`,
async success(res){
if (res.confirm) {
await patchStore.deleteCropApi(patchStore.cropForm.id)
await patchStore.queryPatch(patchStore.currentPatch.id)
patchStore.recordList = []
uni.showToast({ title: '删除作物成功', icon: 'none' })
patchStore.showCropPopup = false
}
},
})
}
async function saveClick() {
if (!patchStore.cropForm.name) {
uni.showToast({ title: '请输入作物名称', icon: 'none' })
return
}
if (patchStore.cropApiType === 'ADD') {
await patchStore.addCropApi()
await patchStore.queryPatch(patchStore.currentPatch.id)
uni.showToast({ title: '新增作物成功', icon: 'none' })
} else if (patchStore.cropApiType === 'UPDATE') {
await patchStore.updateCropApi(patchStore.cropForm.id)
await patchStore.queryPatch(patchStore.currentPatch.id)
uni.showToast({ title: '更新作物成功', icon: 'none' })
}
patchStore.showCropPopup = false
}
</script>

View File

@@ -0,0 +1,106 @@
<template>
<wd-popup
v-model="patchStore.showPatchPopup"
position="bottom"
:safe-area-inset-bottom="true"
:close-on-click-modal="true"
custom-style="border-radius: 32rpx 32rpx 0 0; padding: 40rpx 32rpx"
@close="handeClosePopup"
>
<view class="mb-5 text-center text-base text-gray-700 font-semibold">
{{ patchStore.patchApiType === 'ADD' ? '新增菜地' : '编辑菜地' }}
</view>
<wd-form
ref="form"
:model="patchStore.patchForm"
layout="horizontal"
title-width="80"
size="large"
border
>
<wd-form-item title="名称">
<wd-input
v-model="patchStore.patchForm.name"
:maxlength="20"
show-word-limit
placeholder="请输入菜地名称"
/>
</wd-form-item>
<wd-form-item title="地点">
<wd-input
v-model="patchStore.patchForm.location"
:maxlength="20"
show-word-limit
placeholder="请输入菜地地点"
/>
</wd-form-item>
</wd-form>
<view class="flex w-full items-center justify-evenly">
<wd-button type="info" @click="cancelClick">
取消
</wd-button>
<wd-button v-if="patchStore.patchApiType === 'UPDATE'" type="danger" @click="deleteClick">
删除
</wd-button>
<wd-button type="primary" @click="saveClick">
保存
</wd-button>
</view>
</wd-popup>
</template>
<script setup lang="ts">
import { usePatchStore } from "@/stores/patch";
const patchStore = usePatchStore()
function handeClosePopup() {
setTimeout(() => {
patchStore.showPatchFab = true
}, 300)
}
function cancelClick() {
patchStore.showPatchPopup = false
handeClosePopup()
}
async function deleteClick() {
wx.showModal({
title: '提示',
content: `确定删除 ${patchStore.patchForm.name} 吗?`,
async success(res){
if (res.confirm) {
await patchStore.deletePatchApi(patchStore.patchForm.id)
await patchStore.queryPatchList()
uni.showToast({ title: '删除菜地成功', icon: 'none' })
patchStore.showPatchPopup = false
handeClosePopup()
}
},
})
}
async function saveClick() {
if (!patchStore.patchForm.name) {
uni.showToast({ title: '请输入菜地名称', icon: 'none' })
return
}
if (patchStore.patchApiType === 'ADD') {
await patchStore.addPatchApi()
await patchStore.queryPatchList()
uni.showToast({ title: '新增菜地成功', icon: 'none' })
} else if (patchStore.patchApiType === 'UPDATE') {
await patchStore.updatePatchApi(patchStore.patchForm.id)
await patchStore.queryPatchList()
uni.showToast({ title: '更新菜地成功', icon: 'none' })
}
patchStore.showPatchPopup = false
handeClosePopup()
}
</script>

View File

@@ -0,0 +1,256 @@
<template>
<wd-popup
v-model="patchStore.showRecordPopup"
position="bottom"
:safe-area-inset-bottom="true"
:close-on-click-modal="true"
custom-style="border-radius: 32rpx 32rpx 0 0; padding: 40rpx 32rpx"
@close="handeClosePopup"
>
<view class="flex flex-col max-h-[70vh] overflow-y-auto">
<view class="mb-5 text-center text-base text-gray-700 font-semibold">
{{ patchStore.recordApiType === 'ADD' ? '新增记录' : '编辑记录' }}
</view>
<wd-form
ref="form"
:model="patchStore.recordForm"
layout="horizontal"
title-width="80"
size="large"
border
>
<wd-form-item title="类型">
<wd-radio-group v-model="patchStore.recordForm.type" type="button">
<wd-radio
v-for="item in typeColumns"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</wd-radio>
</wd-radio-group>
</wd-form-item>
<wd-form-item title="内容">
<wd-input
v-model="patchStore.recordForm.content"
:maxlength="20"
show-word-limit
placeholder="请输入菜地地点"
/>
</wd-form-item>
<wd-form-item title="时期">
<wd-radio-group v-model="patchStore.recordForm.stage" type="button">
<wd-radio
v-for="item in stageColumns"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</wd-radio>
</wd-radio-group>
</wd-form-item>
<wd-form-item
title="日期"
prop="date"
is-link
:value="patchStore.recordForm.date"
placeholder="请选择记录日期"
@click="showTimePicker = true"
/>
</wd-form>
<wd-form-item title="图片">
<wd-upload v-model:file-list="patchStore.imageFileList" accept="image" multiple
:limit="9" image-mode="aspectFill"
:action="action" :success-status="[200, 201]"
:before-upload="beforeImageUpload"
:before-remove="beforeImageRemove"
@success="handleImageSuccess"
></wd-upload>
</wd-form-item>
<wd-form-item title="视频">
<wd-upload v-model:file-list="patchStore.videoFileList" accept="video" multiple
:limit="3"
:action="action" :success-status="[200, 201]"
:before-upload="beforeVideoUpload"
:before-remove="beforeVideoRemove"
@success="handleVideoSuccess"></wd-upload>
</wd-form-item>
<view class="flex w-full items-center justify-evenly">
<wd-button type="info" @click="cancelClick">
取消
</wd-button>
<wd-button v-if="patchStore.recordApiType === 'UPDATE'" type="danger" @click="deleteClick">
删除
</wd-button>
<wd-button type="primary" @click="saveClick">
保存
</wd-button>
</view>
<wd-datetime-picker
:model-value="new Date(patchStore.recordForm.date).getTime()"
v-model:visible="showTimePicker"
type="date"
:max-date="Date.now()"
placeholder="请选择记录时间"
@confirm="onDateConfirm"
/>
</view>
</wd-popup>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { usePatchStore } from "@/stores/patch";
import { formatDate } from "@/utils";
import type { UploadBeforeUploadOption, UploadSuccessEvent, UploadRemoveEvent } from '@wot-ui/ui/components/wd-upload/types'
const patchStore = usePatchStore()
const showTimePicker = ref(false)
const typeColumns = [
{ label: '🌱 播种', value: '🌱 播种' },
{ label: '💧 浇水', value: '💧 浇水' },
{ label: '🌿 除草', value: '🌿 除草' },
{ label: '🐛 除虫', value: '🐛 除虫' },
{ label: '✂️ 修剪', value: '✂️ 修剪' },
{ label: '🧪 施肥', value: '🧪 施肥' },
{ label: '🔄 移栽', value: '🔄 移栽' },
{ label: '🧺 收获', value: '🧺 收获' },
]
const stageColumns = [
{ label: '发芽期', value: '发芽期' },
{ label: '幼苗期', value: '幼苗期' },
{ label: '生长期', value: '生长期' },
{ label: '开花期', value: '开花期' },
{ label: '结果期', value: '结果期' },
{ label: '采收期', value: '采收期' },
]
const action = ref(`${patchStore.baseUrl}/patch-api/file/upload`)
function onDateConfirm({ value }){
patchStore.recordForm.date = formatDate(new Date(value))
showTimePicker.value = false
}
function handeClosePopup() {
setTimeout(() => {
patchStore.showRecordFab = true
}, 300)
}
function cancelClick() {
patchStore.showRecordPopup = false
handeClosePopup()
}
function beforeImageUpload(options: UploadBeforeUploadOption) {
const file = options.files[0]
const maxSize = 10 * 1024 * 1024
if (file.size > maxSize) {
uni.showToast({
title: '文件大小不能超过 10MB',
icon: 'none',
duration: 2500
})
return false
}
return true
}
function handleImageSuccess(event: UploadSuccessEvent) {
const url = JSON.parse(event.file.response)
patchStore.recordForm.images.push(url)
}
function beforeImageRemove(event: UploadRemoveEvent) {
let url = ''
if (patchStore.recordApiType === 'ADD') {
url = JSON.parse(event.file.response)
} else if (patchStore.recordApiType === 'UPDATE') {
url = event.file.url.replace(`${patchStore.baseUrl}/`, '')
}
if (url) {
const index = patchStore.recordForm.images.indexOf(url)
if (index > -1) {
patchStore.recordForm.images.splice(index, 1)
}
}
return true
}
function beforeVideoUpload(options: UploadBeforeUploadOption) {
const file = options.files[0]
const maxSize = 500 * 1024 * 1024
if (file.size > maxSize) {
uni.showToast({
title: '文件大小不能超过 500MB',
icon: 'none',
duration: 2500
})
return false
}
return true
}
function handleVideoSuccess(event: UploadSuccessEvent) {
const url = JSON.parse(event.file.response)
patchStore.recordForm.videos.push(url)
}
function beforeVideoRemove(event: UploadRemoveEvent) {
const url = JSON.parse(event.file.response)
if (url) {
const index = patchStore.recordForm.videos.indexOf(url)
if (index > -1) {
patchStore.recordForm.videos.splice(index, 1)
}
}
return true
}
async function deleteClick() {
wx.showModal({
title: '提示',
content: `确定删除 ${patchStore.recordForm.type} 记录吗?`,
async success(res){
if (res.confirm) {
await patchStore.deleteRecordApi(patchStore.recordForm.id)
await patchStore.queryRecordList(patchStore.currentPatch.crop.id)
uni.showToast({ title: '删除记录成功', icon: 'none' })
patchStore.showRecordPopup = false
handeClosePopup()
}
},
})
}
async function saveClick() {
if (patchStore.recordApiType === 'ADD') {
await patchStore.addRecordApi()
await patchStore.queryRecordList(patchStore.currentPatch.crop.id)
uni.showToast({ title: '新增记录成功', icon: 'none' })
} else if (patchStore.recordApiType === 'UPDATE') {
await patchStore.updateRecordApi(patchStore.recordForm.id)
await patchStore.queryRecordList(patchStore.currentPatch.crop.id)
uni.showToast({ title: '更新记录成功', icon: 'none' })
}
patchStore.showRecordPopup = false
handeClosePopup()
}
</script>

View File

@@ -1,9 +1,13 @@
import 'uno.css'
import { createSSRApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
export function createApp() {
const app = createSSRApp(App)
const pinia = createPinia()
return { app }
app.use(pinia)
return { app, pinia }
}

View File

@@ -1,6 +1,6 @@
{
"name" : "",
"appid" : "",
"appid" : "wxf7a95cb4373170c1",
"description" : "",
"versionName" : "1.0.0",
"versionCode" : "100",

View File

@@ -4,6 +4,10 @@
"path": "pages/index/index",
"style": { "navigationBarTitleText": "首页" }
},
{
"path": "pages/record/index",
"style": { "navigationBarTitleText": "记录" }
},
{
"path": "pages/user/index",
"style": { "navigationBarTitleText": "我的" }

View File

@@ -13,52 +13,51 @@
</view>
</view>
<wd-card title="1号菜地">
<image src="https://wot-ui.cn/assets/panda.jpg" mode="aspectFill" />
<wd-card v-for="(item, index) in patchStore.patchList" :key="item.id">
<template #title>
<view class="wd-card__title-text flex items-center justify-between">
<text>{{ item.name }}</text>
<text v-if="item.location" class="text-sm text-[#999]">{{ item.location }}</text>
</view>
</template>
<wd-img width="100%" height="40vh" :src="`${patchStore.baseUrl}/${item.crop?.images[0]}`"
lazy-load mode="aspectFill" @click="handlePreview(item.crop)">
<template #error>
<view class="error-wrap">暂无记录</view>
</template>
</wd-img>
<template #footer>
<view class="flex-1 flex justify-between items-center">
<view class="flex flex-col gap-1">
<view v-if="item.crop" class="flex flex-col gap-1">
<view class="text-base font-medium">
西红柿
<text class="text-sm text-gray-400 font-normal ml-1">粉果番茄</text>
{{ item.crop.name }}
<text class="text-sm text-gray-400 font-normal ml-1">{{ item.crop.variety }}</text>
</view>
<view class="flex gap-1 align-center">
<wd-text text="🌱 生长期"/>
<wd-text text="📅 2025-04-10"/>
<wd-text v-if="item.crop.stage" :text="item.crop.stage"/>
<wd-text :text="`📅 ${item.crop.date}`"/>
</view>
</view>
<wd-text v-else text="暂无作物 请先添加"/>
<view class="flex gap-1">
<wd-button icon="edit" round></wd-button>
<wd-button icon="list" round></wd-button>
<wd-button icon="edit" round @click="editPatchClick(item)"></wd-button>
<wd-button icon="list" round @click="viewDetailClick(item)"></wd-button>
</view>
</view>
</template>
</wd-card>
<wd-card title="2号菜地">
<image src="https://wot-ui.cn/assets/redpanda.jpg" mode="aspectFill" />
<wd-empty v-if="patchStore.patchList.length === 0" tip="暂无数据"/>
<template #footer>
<view class="flex-1 flex justify-between items-center">
<view class="flex flex-col gap-1">
<view class="text-base font-medium">
黄瓜
<text class="text-sm text-gray-400 font-normal ml-1">旱黄瓜</text>
</view>
<view class="flex gap-1 align-center">
<wd-text text="🌱 开花期"/>
<wd-text text="📅 2025-04-10"/>
</view>
</view>
<view class="flex gap-1">
<wd-button icon="edit" round></wd-button>
<wd-button icon="list" round></wd-button>
</view>
</view>
</template>
</wd-card>
<liu-drag-button v-if="patchStore.showPatchFab" background="#22c55e" @clickBtn="addPatchClick">
<wd-icon name="plus" size="24px"></wd-icon>
</liu-drag-button>
<patch-popup />
<view class="h-[10px]"></view>
</view>
</template>
@@ -67,9 +66,13 @@
import { computed } from 'vue'
import { onShow } from '@dcloudio/uni-app'
import appLogo from '@/static/logo.png'
import { usePatchStore } from "@/stores/patch";
import type {ICrop, IPatch} from "@/types";
import PatchPopup from "@/components/PatchPopup.vue";
defineOptions({ name: 'Home' })
const patchStore = usePatchStore()
const imgURL = appLogo
// ===== 时间 =====
@@ -90,10 +93,48 @@ const today = computed(() => {
return `${m}${d}日 周${w}`
})
onShow(async () => {
await loadData()
})
onShow(() => loadData())
async function loadData() {
await patchStore.queryPatchList()
}
function loadData() {
function addPatchClick() {
patchStore.patchForm = {
id: 0,
name: '',
location: '',
status: ''
}
patchStore.patchApiType = 'ADD'
patchStore.showPatchPopup = true
patchStore.showPatchFab = false
}
function editPatchClick(patch: IPatch) {
patchStore.patchForm = JSON.parse(JSON.stringify(patch))
patchStore.patchApiType = 'UPDATE'
patchStore.showPatchPopup = true
patchStore.showPatchFab = false
}
async function viewDetailClick(patch: IPatch) {
patchStore.currentPatch = patch
if (patch.crop) {
await patchStore.queryRecordList(patch.crop.id)
} else {
patchStore.recordList = []
}
wx.navigateTo({
url: '/pages/record/index'
})
}
function handlePreview(crop: ICrop) {
const fullUrls = crop.images?.map(url => `${patchStore.baseUrl}/${url}`) as string[]
uni.previewImage({ urls: fullUrls, current: fullUrls[0] })
}
</script>

File diff suppressed because one or more lines are too long

View File

@@ -1,105 +1,8 @@
<template>
<view class="min-h-screen flex flex-col" style="background-image: linear-gradient(180deg, #4CAF50 0%, #8BC34A 42%, #E8F5E9 100%)">
<wd-segmented :options="list" v-model:value="current"
custom-style="align-self: center; width: 60%; margin-top: 10px;"></wd-segmented>
<view class="self-center py-3 flex items-center gap-2 text-gray-800">
<view class="flex min-w-0 flex-col">
<text class="truncate text-lg text-white font-semibold">
西红柿
</text>
</view>
</view>
<wd-card>
<template #title>
<view class="w-full flex flex-col gap-1.5">
<view class="flex justify-between items-center">
<view class="text-base font-medium">💧 浇水</view>
<wd-tag type="primary" round>🌱 开花期</wd-tag>
<view class="text-xs text-gray-400">06-20 08:30</view>
</view>
</view>
</template>
<view class="flex flex-col gap-2">
<text class="text-sm text-gray-500">浇透水土壤湿度约70%</text>
</view>
</wd-card>
<wd-card>
<template #title>
<view class="w-full flex flex-col gap-1.5">
<view class="flex justify-between items-center">
<view class="text-base font-medium">🌿 施肥</view>
<wd-tag type="primary" round>🌱 开花期</wd-tag>
<view class="text-xs text-gray-400">06-18 17:00</view>
</view>
</view>
</template>
<view class="flex flex-col gap-2">
<image
src="https://wot-ui.cn/assets/panda.jpg"
mode="aspectFill"
class="rounded-lg"
/>
<text class="text-sm text-gray-500">追施磷酸二氢钾800倍叶面喷施</text>
</view>
</wd-card>
<wd-card>
<template #title>
<view class="w-full flex flex-col gap-1.5">
<view class="flex justify-between items-center">
<view class="text-base font-medium">🐛 病虫害防治</view>
<wd-tag type="warning" round> 发现蚜虫</wd-tag>
<view class="text-xs text-gray-400">06-15 09:00</view>
</view>
</view>
</template>
<view class="flex flex-col gap-2">
<text class="text-sm text-gray-500">叶片背面发现少量蚜虫喷施吡虫啉1000倍液</text>
</view>
</wd-card>
<wd-card>
<template #title>
<view class="w-full flex flex-col gap-1.5">
<view class="flex justify-between items-center">
<view class="text-base font-medium"> 整枝打杈</view>
<wd-tag type="primary" round>🌱 开花期</wd-tag>
<view class="text-xs text-gray-400">06-12 16:30</view>
</view>
</view>
</template>
<view class="flex flex-col gap-2">
<text class="text-sm text-gray-500">摘除第一穗花以下所有侧枝保留主蔓</text>
</view>
</wd-card>
<!-- 04-10 播种最早 -->
<wd-card>
<template #title>
<view class="w-full flex flex-col gap-1.5">
<view class="flex justify-between items-center">
<view class="text-base font-medium">🌱 播种</view>
<wd-tag type="success" round>📅 苗期</wd-tag>
<view class="text-xs text-gray-400">04-10 10:00</view>
</view>
</view>
</template>
<view class="flex flex-col gap-2">
<text class="text-sm text-gray-500">穴盘育苗覆土1cm浇透水放置于温暖通风处</text>
</view>
</wd-card>
<view class="h-[10px]"></view>
<view class="min-h-screen flex flex-col">
<wd-text text="我的"/>
</view>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const list = ref<string[]>(['当前种植', '历史种植'])
const current = ref('点赞')
</script>

259
src/stores/patch.ts Normal file
View File

@@ -0,0 +1,259 @@
import { defineStore } from 'pinia'
import type { ICrop, IPatch, IRecord } from "@/types";
import type { UploadFile } from "@wot-ui/ui/components/wd-upload/types";
export const usePatchStore = defineStore('patch', {
state: () => ({
baseUrl: 'https://cxx0822.s.3q.hair',
showPatchFab: true,
patchList: [] as IPatch[],
patchApiType: 'ADD' as 'ADD' | 'UPDATE' | 'DELETE',
patchForm: {
} as IPatch,
showPatchPopup: false,
currentPatch: {} as IPatch,
recordList: [] as IRecord[],
cropApiType: 'ADD' as 'ADD' | 'UPDATE' | 'DELETE',
cropForm: {
} as ICrop,
showCropPopup: false,
showRecordFab: true,
showRecordPopup: false,
recordApiType: 'ADD' as 'ADD' | 'UPDATE' | 'DELETE',
recordForm: {
id: 0,
crop_id: 0,
type: '',
date: '',
content: '',
images: [],
videos: [],
stage: '',
} as IRecord,
imageFileList: [] as UploadFile[],
videoFileList: [] as UploadFile[],
}),
actions: {
async queryPatchList() {
this.patchList = await this.queryPatchListApi()
},
async queryPatch(patchId: number) {
this.currentPatch = await this.queryPatchApi(patchId)
},
async queryRecordList(cropId: number) {
this.recordList = await this.queryRecordListApi(cropId)
},
async queryPatchListApi() {
return new Promise<IPatch[]>((resolve, reject) => {
wx.request({
url: `${this.baseUrl}/patch-api/patches`,
method: 'GET',
header: { 'Content-Type': 'application/json' },
success: (res) => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(res.data as IPatch[])
} else {
reject({ status: res.statusCode, data: res.data })
}
},
fail: (err) => reject(err),
})
})
},
async queryPatchApi(patchId: number) {
return new Promise<IPatch>((resolve, reject) => {
wx.request({
url: `${this.baseUrl}/patch-api/patches/${patchId}`,
method: 'GET',
header: { 'Content-Type': 'application/json' },
success: (res) => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(res.data as IPatch)
} else {
reject({ status: res.statusCode, data: res.data })
}
},
fail: (err) => reject(err),
})
})
},
async addPatchApi() {
return new Promise<boolean>((resolve, reject) => {
wx.request({
url: `${this.baseUrl}/patch-api/patches`,
method: 'POST',
header: { 'Content-Type': 'application/json' },
data: this.patchForm,
success: (res) => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(true)
} else {
reject({ status: res.statusCode, data: false })
}
},
fail: (err) => reject(err),
})
})
},
async updatePatchApi(patchId: number) {
return new Promise<boolean>((resolve, reject) => {
wx.request({
url: `${this.baseUrl}/patch-api/patches/${patchId}`,
method: 'PUT',
header: { 'Content-Type': 'application/json' },
data: this.patchForm,
success: (res) => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(true)
} else {
reject({ status: res.statusCode, data: false })
}
},
fail: (err) => reject(err),
})
})
},
async deletePatchApi(patchId: number) {
return new Promise<boolean>((resolve, reject) => {
wx.request({
url: `${this.baseUrl}/patch-api/patches/${patchId}`,
method: 'DELETE',
header: { 'Content-Type': 'application/json' },
success: (res) => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(true)
} else {
reject({ status: res.statusCode, data: false })
}
},
fail: (err) => reject(err),
})
})
},
async addCropApi() {
return new Promise<boolean>((resolve, reject) => {
wx.request({
url: `${this.baseUrl}/patch-api/crops`,
method: 'POST',
header: { 'Content-Type': 'application/json' },
data: this.cropForm,
success: (res) => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(true)
} else {
reject({ status: res.statusCode, data: false })
}
},
fail: (err) => reject(err),
})
})
},
async updateCropApi(cropId: number) {
return new Promise<boolean>((resolve, reject) => {
wx.request({
url: `${this.baseUrl}/patch-api/crops/${cropId}`,
method: 'PUT',
header: { 'Content-Type': 'application/json' },
data: this.cropForm,
success: (res) => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(true)
} else {
reject({ status: res.statusCode, data: false })
}
},
fail: (err) => reject(err),
})
})
},
async deleteCropApi(cropId: number) {
return new Promise<boolean>((resolve, reject) => {
wx.request({
url: `${this.baseUrl}/patch-api/crops/${cropId}`,
method: 'DELETE',
header: { 'Content-Type': 'application/json' },
success: (res) => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(true)
} else {
reject({ status: res.statusCode, data: false })
}
},
fail: (err) => reject(err),
})
})
},
async queryRecordListApi(cropId: number) {
return new Promise<IRecord[]>((resolve, reject) => {
wx.request({
url: `${this.baseUrl}/patch-api/records/crop/${cropId}`,
method: 'GET',
header: { 'Content-Type': 'application/json' },
success: (res) => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(res.data as IRecord[])
} else {
reject({ status: res.statusCode, data: res.data })
}
},
fail: (err) => reject(err),
})
})
},
async addRecordApi() {
return new Promise<boolean>((resolve, reject) => {
wx.request({
url: `${this.baseUrl}/patch-api/records`,
method: 'POST',
header: { 'Content-Type': 'application/json' },
data: this.recordForm,
success: (res) => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(true)
} else {
reject({ status: res.statusCode, data: false })
}
},
fail: (err) => reject(err),
})
})
},
async updateRecordApi(recordId: number) {
return new Promise<boolean>((resolve, reject) => {
wx.request({
url: `${this.baseUrl}/patch-api/records/${recordId}`,
method: 'PUT',
header: { 'Content-Type': 'application/json' },
data: this.recordForm,
success: (res) => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(true)
} else {
reject({ status: res.statusCode, data: false })
}
},
fail: (err) => reject(err),
})
})
},
async deleteRecordApi(recordId: number) {
return new Promise<boolean>((resolve, reject) => {
wx.request({
url: `${this.baseUrl}/patch-api/records/${recordId}`,
method: 'DELETE',
header: { 'Content-Type': 'application/json' },
success: (res) => {
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(true)
} else {
reject({ status: res.statusCode, data: false })
}
},
fail: (err) => reject(err),
})
})
},
},
})

27
src/types/index.ts Normal file
View File

@@ -0,0 +1,27 @@
export interface ICrop {
id: number
patch_id: number
name: string
variety: string
date: string
images?: string[]
}
export interface IPatch {
id: number
name: string
location: string
status: string
crop?: ICrop
}
export interface IRecord {
id: number
crop_id: number
type: string
date: string
content: string
images: string[]
videos: string[]
stage: string
}

View File

@@ -0,0 +1,12 @@
## 1.0.52023-07-13
优化
## 1.0.42023-06-08
增加预览二维码
## 1.0.32023-05-31
增加license
## 1.0.22023-04-28
优化
## 1.0.12023-04-26
增加示例
## 1.0.02023-04-26
初始化发布

View File

@@ -0,0 +1,143 @@
<template>
<view>
<movable-area class="movable-area" :scale-area="false">
<movable-view class="movable-view" :class="!isRemove?'animation-info':''" style="pointer-events: auto;"
:style="{ background: background }"
@click="clickBtn" @touchstart="touchstart" @touchend="touchend" @change="onChange" direction="all"
inertia="true" :x="x" :y="y" :disabled="disabled" :out-of-bounds="true" :damping="200" :friction="100">
<slot></slot>
</movable-view>
</movable-area>
</view>
</template>
<script>
export default {
props: {
background: {
type: String,
default: "#8b5cf6"
},
//是否禁用拖动
disabled: {
type: Boolean,
default: false
},
//是否自动停靠
canDocking: {
type: Boolean,
default: true
},
//按钮默认位置离底部距离px
bottomPx: {
type: Number,
default: 30
},
//按钮默认位置离右边距离px
rightPx: {
type: Number,
default: 0
},
},
data() {
return {
left: 0,
top: 0,
isRemove: true,
windowWidth: 0,
windowHeight: 0,
btnWidth: 0,
btnHeight: 0,
x: 10000,
y: 10000,
old: {
x: 0,
y: 0
}
};
},
mounted() {
this.getSysInfo()
},
methods: {
getSysInfo() {
let sysInfo = uni.getSystemInfoSync()
this.windowWidth = sysInfo.windowWidth
this.windowHeight = sysInfo.windowHeight
let view = uni.createSelectorQuery().in(this).select(".movable-view")
view.boundingClientRect(rect => {
this.btnWidth = rect.width
this.btnHeight = rect.height
this.x = this.old.x
this.y = this.old.y
this.$nextTick(res => {
this.x = this.windowWidth - this.btnWidth - this.rightPx
this.y = this.windowHeight - this.btnHeight - this.bottomPx
})
}).exec()
},
//移动按钮
onChange(e) {
this.old.x = e.detail.x
this.old.y = e.detail.y
},
//开始移动
touchstart(e) {
this.isRemove = true
},
//结束移动
touchend(e) {
if (this.canDocking && this.old.x) {
this.x = this.old.x
this.y = this.old.y
let bWidth = (this.windowWidth - this.btnWidth) / 2
if (this.x < 0 || (this.x > 0 && this.x <= bWidth)) {
this.$nextTick(res => {
this.x = 0
})
} else {
this.$nextTick(res => {
this.x = this.windowWidth - this.btnWidth
})
}
this.isRemove = false
}
},
//点击按钮
clickBtn() {
this.$emit('clickBtn')
}
}
};
</script>
<style scoped>
.movable-view {
width: 100rpx;
height: 100rpx;
box-shadow: 0px 4rpx 12rpx 0px rgba(15, 23, 42, 0.08);
border-radius: 50rpx;
color: #FFFFFF;
font-size: 26rpx;
touch-action: none;
display: flex;
align-items: center;
justify-content: center;
}
.animation-info {
transition: left .25s ease;
}
.movable-area {
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 999999 !important;
pointer-events: none;
}
</style>

View File

@@ -0,0 +1,6 @@
### 1、本插件可免费下载使用
### 2、未经许可严禁复制本插件派生同类插件上传插件市场
### 3、未经许可严禁在插件市场恶意复制抄袭本插件进行违规获利;
### 4、对本软件的任何使用都必须遵守这些条款违反这些条款的个人或组织将面临法律追究。

View File

@@ -0,0 +1,85 @@
{
"id": "liu-drag-button",
"displayName": "可拖动悬浮按钮",
"version": "1.0.5",
"description": "可拖动的悬浮按钮兼容小程序、H5支持自动停靠支持自定义样式使用相当简单源码简单易修改。",
"keywords": [
"悬浮按钮",
"拖拽按钮",
"拖动",
"按钮",
"拖拽"
],
"repository": "",
"engines": {
"HBuilderX": "^3.1.0"
},
"dcloudext": {
"type": "component-vue",
"sale": {
"regular": {
"price": "0.00"
},
"sourcecode": {
"price": "0.00"
}
},
"contact": {
"qq": ""
},
"declaration": {
"ads": "无",
"data": "无",
"permissions": "无"
},
"npmurl": ""
},
"uni_modules": {
"dependencies": [],
"encrypt": [],
"platforms": {
"cloud": {
"tcb": "y",
"aliyun": "y"
},
"client": {
"Vue": {
"vue2": "y",
"vue3": "u"
},
"App": {
"app-vue": "u",
"app-nvue": "u"
},
"H5-mobile": {
"Safari": "y",
"Android Browser": "y",
"微信浏览器(Android)": "y",
"QQ浏览器(Android)": "y"
},
"H5-pc": {
"Chrome": "u",
"IE": "u",
"Edge": "u",
"Firefox": "u",
"Safari": "u"
},
"小程序": {
"微信": "y",
"阿里": "u",
"百度": "u",
"字节跳动": "u",
"QQ": "u",
"钉钉": "u",
"快手": "u",
"飞书": "u",
"京东": "u"
},
"快应用": {
"华为": "u",
"联盟": "u"
}
}
}
}
}

View File

@@ -0,0 +1,36 @@
# liu-drag-button适用于uni-app项目的可拖动悬浮按钮组件
### 本组件目前兼容微信小程序、H5
### 本组件是可拖动的悬浮按钮兼容小程序、H5支持自动停靠支持自定义样式源码简单易修改
# --- 扫码预览、关注我们 ---
## 扫码关注公众号,查看更多插件信息,预览插件效果!
![](https://uni.ckapi.pro/uniapp/publicize.png)
### 使用方式
``` html
<liu-drag-button @clickBtn="clickBtn">按钮</liu-drag-button>
```
``` javascript
export default {
data() {
return {
};
},
methods: {
//点击按钮
clickBtn(){
console.log('按钮被点击了')
},
}
}
```
### 属性说明
| 名称 | 类型 | 默认值 | 描述 |
| ----------------------------|--------------- | ------------------ | ---------------|
| disabled | Boolean | false | 是否禁用拖动
| canDocking | Boolean | true | 是否自动停靠
| bottomPx | Number | 30 | 按钮默认位置离底部距离px
| rightPx | Number | 0 | 按钮默认位置离右边距离px

19
src/utils/index.ts Normal file
View File

@@ -0,0 +1,19 @@
export function formatDate(ts: string | number) {
if (!ts)
return ''
const d = new Date(ts)
const pad = (n: number) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
}
export function formatDateTime(ts: string | number) {
if (!ts)
return ''
const d = new Date(ts)
const pad = (n: number) => String(n).padStart(2, '0')
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`
}
export function generateUniqueId() {
return Date.now() + '-' + Math.random().toString(36).slice(2, 6);
}