提交 69e1168c 作者: 廖在望

feat: 登录页面、供应需求、采购需求、地区组件、相关详情页面重构&问题修复。

上级 b5983f10
...@@ -93,7 +93,7 @@ ...@@ -93,7 +93,7 @@
border-radius: 32rpx 32rpx 0 0; border-radius: 32rpx 32rpx 0 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: 80vh; height: 60vh;
font-family: 'DingTalk Sans', sans-serif; font-family: 'DingTalk Sans', sans-serif;
} }
......
<script setup lang="ts"> <script setup lang="ts">
import { reactive } from 'vue' import { reactive, onMounted } from 'vue'
import { onPullDownRefresh, onReachBottom, onShow } from '@dcloudio/uni-app' import { onPullDownRefresh, onReachBottom, onShow } from '@dcloudio/uni-app'
import PriceDialog from './components/price-dialog.vue' import PriceDialog from './components/price-dialog.vue'
import Navigate from '@/utils/page/navigate' import Navigate from '@/utils/page/navigate'
import * as ChanxiaoAPI from '@/api/model/chanxiao' import * as ChanxiaoAPI from '@/api/model/chanxiao'
import { getText } from '@/utils/dict/area' import { getDictData, getText } from '@/utils/dict/area'
onMounted(() => {
getDictData()
})
// 下拉刷新 // 下拉刷新
onPullDownRefresh(() => { onPullDownRefresh(() => {
...@@ -26,13 +30,13 @@ ...@@ -26,13 +30,13 @@
}, 1000) }, 1000)
}) })
onShow(() => { onShow(() => {
// 只有当列表为空时才重新获取数据,避免每次返回页面都全量刷新
if (pageData.currentTransactionTab === 1 && pageData.supplyInfos.length === 0) {
pageData.search.pageNo = 1 pageData.search.pageNo = 1
if (pageData.currentTransactionTab === 1) {
pageData.supplyInfos = []
fetchSupplyInfos() fetchSupplyInfos()
} }
if (pageData.currentTransactionTab === 2) { if (pageData.currentTransactionTab === 2 && pageData.purchaseDemands.length === 0) {
pageData.purchaseDemands = [] pageData.search.pageNo = 1
fetchPurchaseDemands() fetchPurchaseDemands()
} }
}) })
...@@ -151,11 +155,16 @@ ...@@ -151,11 +155,16 @@
ChanxiaoAPI.purchaseList(pageData.search) ChanxiaoAPI.purchaseList(pageData.search)
.then((res) => { .then((res) => {
const { records, total } = res const { records, total } = res
pageData.purchaseDemands = [...pageData.purchaseDemands, ...records] const mappedRecords = records.map((item) => {
pageData.purchaseDemands = pageData.purchaseDemands.map((item) => ({ const nameParts = [item.cityName, item.countryName].filter(Boolean)
return {
...item, ...item,
location: getText(`${item.province},${item.city},${item.country}`, ' / '), location: nameParts.length > 0
})) ? nameParts.join('')
: getText(`${item.city},${item.country}`, ''),
}
})
pageData.purchaseDemands = [...pageData.purchaseDemands, ...mappedRecords]
pageData.total = total pageData.total = total
}) })
.finally(() => { .finally(() => {
...@@ -171,11 +180,16 @@ ...@@ -171,11 +180,16 @@
ChanxiaoAPI.supplyList(params) ChanxiaoAPI.supplyList(params)
.then((res) => { .then((res) => {
const { records, total } = res const { records, total } = res
pageData.supplyInfos = [...pageData.supplyInfos, ...records] const mappedRecords = records.map((item) => {
pageData.supplyInfos = pageData.supplyInfos.map((item) => ({ const nameParts = [item.cityName, item.districtName].filter(Boolean)
return {
...item, ...item,
location: getText(`${item.province},${item.city},${item.country}`, ' / '), location: nameParts.length > 0
})) ? nameParts.join('')
: getText(`${item.city},${item.district}`, ''),
}
})
pageData.supplyInfos = [...pageData.supplyInfos, ...mappedRecords]
pageData.total = total pageData.total = total
}) })
.finally(() => { .finally(() => {
...@@ -388,6 +402,12 @@ ...@@ -388,6 +402,12 @@
<!-- 采购需求列表 --> <!-- 采购需求列表 -->
<view v-if="pageData.currentTransactionTab === 2"> <view v-if="pageData.currentTransactionTab === 2">
<view <view
v-if="pageData.purchaseDemands.length === 0"
style="height: 528rpx"
>
<fui-empty src="/static/images/no-data.png" title="暂无数据" />
</view>
<view
v-for="(demand, index) in pageData.purchaseDemands" v-for="(demand, index) in pageData.purchaseDemands"
:key="demand.id" :key="demand.id"
class="product-card purchase" class="product-card purchase"
...@@ -424,22 +444,86 @@ ...@@ -424,22 +444,86 @@
</view> </view>
</view> </view>
</view> </view>
<fui-fab position="right" distance="30" bottom="150" width="96" @click="handlePublish"> <!-- 发布悬浮按钮 -->
<view v-show="pageData.currentTransactionTab === 1" class="text-white text-center"> <view class="fab-container" @click="handlePublish">
<view class="fab-icon" /> <view class="ripple" :class="{ 'ripple-purchase': pageData.currentTransactionTab === 2 }"></view>
<view style="font-size: 24rpx">发布</view> <view class="ripple ripple-2" :class="{ 'ripple-purchase': pageData.currentTransactionTab === 2 }"></view>
<view class="fab-entry" :class="{ 'fab-purchase': pageData.currentTransactionTab === 2 }">
<fui-icon name="plus" :size="48" color="#fff" bold></fui-icon>
<text>发布</text>
</view> </view>
<view v-show="pageData.currentTransactionTab === 2" class="text-white text-center">
<view class="fab-icon" />
<view style="font-size: 24rpx">发布</view>
</view> </view>
</fui-fab>
<PriceDialog ref="priceDialogRef" /> <PriceDialog ref="priceDialogRef" />
<fui-loading isFixed v-if="pageData.loading" backgroundColor="rgba(0, 0, 0, 0.4)" /> <fui-loading isFixed v-if="pageData.loading" backgroundColor="rgba(0, 0, 0, 0.4)" />
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.fab-container {
position: fixed;
right: 30rpx;
bottom: 150rpx;
width: 110rpx;
height: 110rpx;
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
.ripple {
position: absolute;
width: 100%;
height: 100%;
background-color: #5db66f;
border-radius: 50%;
opacity: 0.4;
animation: ripple 2s infinite ease-out;
&.ripple-purchase {
background-color: #fa8c16;
}
}
.ripple-2 {
animation-delay: 1s;
}
.fab-entry {
position: relative;
width: 110rpx;
height: 110rpx;
background: linear-gradient(135deg, #5db66f 0%, #4caf50 100%);
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 16rpx rgba(93, 182, 111, 0.3);
&.fab-purchase {
background: linear-gradient(135deg, #ffbb96 0%, #fa8c16 100%);
box-shadow: 0 4rpx 16rpx rgba(250, 140, 22, 0.3);
}
text {
color: #fff;
font-size: 20rpx;
font-weight: bold;
margin-top: -4rpx;
}
}
}
@keyframes ripple {
0% {
transform: scale(1);
opacity: 0.4;
}
100% {
transform: scale(1.6);
opacity: 0;
}
}
.ml-7 { .ml-7 {
margin-left: 14rpx; margin-left: 14rpx;
} }
......
...@@ -141,6 +141,9 @@ ...@@ -141,6 +141,9 @@
function onSlideSuccess() { function onSlideSuccess() {
if (slideType.value === 'sms') { if (slideType.value === 'sms') {
// 立即开启倒计时(乐观更新),接口报错也不停止,防止恶意连点
startCountdown()
// 发送验证码 // 发送验证码
const params = { const params = {
mobile: model.form.data.username, mobile: model.form.data.username,
...@@ -149,10 +152,10 @@ ...@@ -149,10 +152,10 @@
API.sysSms(params) API.sysSms(params)
.then(async () => { .then(async () => {
Message.toast('验证码已发送') Message.toast('验证码已发送')
startCountdown()
}) })
.catch(() => { .catch((err) => {
Message.toast('验证码发送失败') // 即使接口返回 code: 130 等错误,也不干扰已启动的倒计时
console.error('短信发送业务异常:', err)
}) })
} else { } else {
// 执行登录 // 执行登录
......
...@@ -93,6 +93,9 @@ ...@@ -93,6 +93,9 @@
function onSlideSuccess() { function onSlideSuccess() {
if (slideType.value === 'sms') { if (slideType.value === 'sms') {
// 开启乐观倒计时,不论接口成败,60s内不准重发
startCountdown()
const params = { const params = {
mobile: model.form.data.phone, mobile: model.form.data.phone,
smsmode: 2, // 2-注册 smsmode: 2, // 2-注册
...@@ -100,10 +103,9 @@ ...@@ -100,10 +103,9 @@
API.sysSms(params) API.sysSms(params)
.then(async () => { .then(async () => {
Message.toast('验证码已发送') Message.toast('验证码已发送')
startCountdown()
}) })
.catch(() => { .catch((err) => {
Message.toast('验证码发送失败') console.error('注册短信发送失败:', err)
}) })
} else { } else {
doRegister() doRegister()
......
...@@ -747,22 +747,74 @@ ...@@ -747,22 +747,74 @@
</view> </view>
</view> </view>
<fui-fab <!-- 找人干活悬浮按钮 -->
v-if="!userStore.isAuditMode" <view class="fab-container" v-if="!userStore.isAuditMode" @click="handlePublish">
position="right" <view class="ripple"></view>
distance="10" <view class="ripple ripple-2"></view>
bottom="240" <view class="fab-entry">
width="96" <image style="width: 48rpx; height: 48rpx" src="/static/images/nongchang/work_icon.png" />
@click="handlePublish" <text>找人干活</text>
> </view>
<view class="text-white text-center">
<image style="width: 52rpx; height: 52rpx" src="/static/images/nongchang/work_icon.png" />
<view style="font-size: 18rpx; margin-top: -16rpx">找人干活</view>
</view> </view>
</fui-fab>
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.fab-container {
position: fixed;
right: 30rpx;
bottom: 240rpx;
width: 110rpx;
height: 110rpx;
z-index: 100;
display: flex;
align-items: center;
justify-content: center;
.ripple {
position: absolute;
width: 100%;
height: 100%;
background-color: #5db66f;
border-radius: 50%;
opacity: 0.4;
animation: ripple 2s infinite ease-out;
}
.ripple-2 {
animation-delay: 1s;
}
.fab-entry {
position: relative;
width: 110rpx;
height: 110rpx;
background: linear-gradient(135deg, #a5d63f 0%, #2e8b57 100%);
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 16rpx rgba(46, 139, 87, 0.3);
text {
color: #fff;
font-size: 18rpx;
font-weight: bold;
margin-top: -4rpx;
}
}
}
@keyframes ripple {
0% {
transform: scale(1);
opacity: 0.4;
}
100% {
transform: scale(1.6);
opacity: 0;
}
}
.mt-19 { .mt-19 {
margin-top: 38rpx; margin-top: 38rpx;
} }
......
...@@ -3,6 +3,21 @@ import { cascaderHn } from '/@/api/model/dict' ...@@ -3,6 +3,21 @@ import { cascaderHn } from '/@/api/model/dict'
const storageKey = 'app_dict_data_area_cascaderHn' const storageKey = 'app_dict_data_area_cascaderHn'
let areaOptions = [] let areaOptions = []
let areaMap = new Map()
/**
* 构建扁平化字典Map,用于高效查询
*/
function buildAreaMap(nodes) {
if (!nodes) return
for (const node of nodes) {
areaMap.set(node.value, node.text)
if (node.children && node.children.length > 0) {
buildAreaMap(node.children)
}
}
}
export async function getDictData() { export async function getDictData() {
// 先从本地加载数据 // 先从本地加载数据
let dictData = getLocalDict() let dictData = getLocalDict()
...@@ -15,6 +30,9 @@ export async function getDictData() { ...@@ -15,6 +30,9 @@ export async function getDictData() {
} }
areaOptions = dictData areaOptions = dictData
// 构建查询Map
areaMap.clear()
buildAreaMap(areaOptions)
return dictData return dictData
} }
...@@ -24,7 +42,11 @@ export async function getDictData() { ...@@ -24,7 +42,11 @@ export async function getDictData() {
export function getLocalDict() { export function getLocalDict() {
const data = uni.getStorageSync(storageKey) const data = uni.getStorageSync(storageKey)
if (data) { if (data) {
return data ? JSON.parse(data) : null const dict = data ? JSON.parse(data) : null
if (dict && areaMap.size === 0) {
buildAreaMap(dict)
}
return dict
} }
return null return null
} }
...@@ -40,37 +62,16 @@ export function refreshDictData() { ...@@ -40,37 +62,16 @@ export function refreshDictData() {
} }
export function getText(scope: string, spliced: string) { export function getText(scope: string, spliced: string) {
if (!scope || !areaOptions || areaOptions.length === 0) { if (!scope) return ''
return ''
}
const values = scope.split(',') // 如果Map尚未初始化,则回退到原始递归方法或返回空
const labels = [] if (areaMap.size === 0) {
const dict = getLocalDict()
// 递归查找label if (!dict) return ''
const findLabel = (nodes, value) => {
for (const node of nodes) {
if (node.value === value) {
return node.text
}
if (node.children && node.children.length > 0) {
const found = findLabel(node.children, value)
if (found) {
return found
}
}
}
return null // 如果没找到,返回原始value
} }
for (const value of values) { const values = scope.split(',')
const text = findLabel(areaOptions, value.trim()) const labels = values.map(val => areaMap.get(val.trim()) || val.trim())
labels.push(text)
}
if (spliced) { return labels.join(spliced || '')
return labels ? labels.join(spliced) : ''
} else {
return labels ? labels.join('') : ''
}
} }
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论