提交 69e1168c 作者: 廖在望

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

上级 b5983f10
......@@ -93,7 +93,7 @@
border-radius: 32rpx 32rpx 0 0;
display: flex;
flex-direction: column;
height: 80vh;
height: 60vh;
font-family: 'DingTalk Sans', sans-serif;
}
......
<script setup lang="ts">
import { reactive } from 'vue'
import { reactive, onMounted } from 'vue'
import { onPullDownRefresh, onReachBottom, onShow } from '@dcloudio/uni-app'
import PriceDialog from './components/price-dialog.vue'
import Navigate from '@/utils/page/navigate'
import * as ChanxiaoAPI from '@/api/model/chanxiao'
import { getText } from '@/utils/dict/area'
import { getDictData, getText } from '@/utils/dict/area'
onMounted(() => {
getDictData()
})
// 下拉刷新
onPullDownRefresh(() => {
......@@ -26,13 +30,13 @@
}, 1000)
})
onShow(() => {
pageData.search.pageNo = 1
if (pageData.currentTransactionTab === 1) {
pageData.supplyInfos = []
// 只有当列表为空时才重新获取数据,避免每次返回页面都全量刷新
if (pageData.currentTransactionTab === 1 && pageData.supplyInfos.length === 0) {
pageData.search.pageNo = 1
fetchSupplyInfos()
}
if (pageData.currentTransactionTab === 2) {
pageData.purchaseDemands = []
if (pageData.currentTransactionTab === 2 && pageData.purchaseDemands.length === 0) {
pageData.search.pageNo = 1
fetchPurchaseDemands()
}
})
......@@ -151,11 +155,16 @@
ChanxiaoAPI.purchaseList(pageData.search)
.then((res) => {
const { records, total } = res
pageData.purchaseDemands = [...pageData.purchaseDemands, ...records]
pageData.purchaseDemands = pageData.purchaseDemands.map((item) => ({
...item,
location: getText(`${item.province},${item.city},${item.country}`, ' / '),
}))
const mappedRecords = records.map((item) => {
const nameParts = [item.cityName, item.countryName].filter(Boolean)
return {
...item,
location: nameParts.length > 0
? nameParts.join('')
: getText(`${item.city},${item.country}`, ''),
}
})
pageData.purchaseDemands = [...pageData.purchaseDemands, ...mappedRecords]
pageData.total = total
})
.finally(() => {
......@@ -171,11 +180,16 @@
ChanxiaoAPI.supplyList(params)
.then((res) => {
const { records, total } = res
pageData.supplyInfos = [...pageData.supplyInfos, ...records]
pageData.supplyInfos = pageData.supplyInfos.map((item) => ({
...item,
location: getText(`${item.province},${item.city},${item.country}`, ' / '),
}))
const mappedRecords = records.map((item) => {
const nameParts = [item.cityName, item.districtName].filter(Boolean)
return {
...item,
location: nameParts.length > 0
? nameParts.join('')
: getText(`${item.city},${item.district}`, ''),
}
})
pageData.supplyInfos = [...pageData.supplyInfos, ...mappedRecords]
pageData.total = total
})
.finally(() => {
......@@ -388,6 +402,12 @@
<!-- 采购需求列表 -->
<view v-if="pageData.currentTransactionTab === 2">
<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"
:key="demand.id"
class="product-card purchase"
......@@ -424,22 +444,86 @@
</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-icon" />
<view style="font-size: 24rpx">发布</view>
</view>
<view v-show="pageData.currentTransactionTab === 2" class="text-white text-center">
<view class="fab-icon" />
<view style="font-size: 24rpx">发布</view>
<!-- 发布悬浮按钮 -->
<view class="fab-container" @click="handlePublish">
<view class="ripple" :class="{ 'ripple-purchase': pageData.currentTransactionTab === 2 }"></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>
</fui-fab>
</view>
<PriceDialog ref="priceDialogRef" />
<fui-loading isFixed v-if="pageData.loading" backgroundColor="rgba(0, 0, 0, 0.4)" />
</template>
<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 {
margin-left: 14rpx;
}
......
......@@ -5,7 +5,6 @@
import { useGlobSetting } from '/@/hooks/setting'
import * as ChanxiaoAPI from '@/api/model/chanxiao'
import * as UserInfoAPI from '@/api/model/userInfo'
import { getCodeByText } from '@/utils/areaData'
import { useDictStore } from '@/store/modules/dict'
import { getText } from '@/utils/dict/area'
import AreaPicker from '@/components/AreaPicker/index.vue'
......@@ -60,10 +59,9 @@
classify: '',
classifyText: '',
inputTextArea: '',
image: null,
imageObj: null,
image: '',
imageObj: null as any,
},
position: [],
rules: [
{ name: 'classify', rule: ['required'], msg: ['请选择采购类别'] },
{ name: 'title', rule: ['required'], msg: ['请输入采购标题'] },
......@@ -75,14 +73,25 @@
{ name: 'address', rule: ['required'], msg: ['请选择收货地区'] },
{ name: 'image', rule: ['required'], msg: ['请上传参考图片'] },
],
agree: true
})
const { show, options, form } = toRefs(pageData)
async function initDict() {
pageData.options.classify = dictStore.getDictList.classify.map((item) => {
return { value: item.value, text: item.text }
})
pageData.options.classify = [
{ value: 1, text: '蔬菜' },
{ value: 2, text: '水果' },
{ value: 3, text: '粮食' },
{ value: 4, text: '畜牧' },
{ value: 0, text: '其他' },
]
// 如果有已选分类,需要回显文字
if (pageData.form.classify && pageData.options.classify.length) {
const item = pageData.options.classify.find(i => i.value == pageData.form.classify)
if (item) pageData.form.classifyText = item.text
}
}
function handleAreaConfirm(e) {
......@@ -90,28 +99,44 @@
pageData.form.province = e.text[0]
pageData.form.city = e.text[1]
pageData.form.country = e.text[2]
// @ts-ignore
pageData.form.areaText = e.fullText
}
function getCurrentAddressInfo() {
const loc = uni.getStorageSync('location')
if (!loc) return
pageData.position = [loc.lon, loc.lat]
UserInfoAPI.location({ lon: loc.lon, lat: loc.lat }).then((res) => {
pageData.form.province = res.province
pageData.form.city = res.city
pageData.form.country = res.country
pageData.form.address = `${res.province},${res.city},${res.country}`
pageData.form.areaText = `${res.province}/${res.city}/${res.country}`
// @ts-ignore
const areaCodes = [res.province, res.city, res.country].filter(Boolean).join(',')
pageData.form.areaText = getText(areaCodes, ' / ')
})
}
function getDetails(id) {
pageData.loading = true
ChanxiaoAPI.purchaseSellDetails({ id }).then((res) => {
ChanxiaoAPI.purchaseSellDetails({ id }).then(async (res) => {
pageData.form = res
// 格式化地区显示
pageData.form.areaText = `${res.province || ''}/${res.city || ''}/${res.country || ''}`.replace(/\/+$/, '')
// 确保字典已加载
if (!pageData.options.classify.length) {
await initDict()
}
// 优先用接口返回的中文名,getText 作为兜底
const nameParts = [res.cityName, res.countryName].filter(Boolean)
if (nameParts.length > 0) {
// @ts-ignore
pageData.form.areaText = nameParts.join('')
} else {
const areaCodes = [res.city, res.country].filter(Boolean).join(',')
// @ts-ignore
pageData.form.areaText = getText(areaCodes, '')
}
if (res.classify && pageData.options.classify.length) {
const item = pageData.options.classify.find(i => i.value == res.classify)
......@@ -149,6 +174,10 @@
}
const formRef = ref()
function submit() {
if (!pageData.agree) {
toastRef.value.show({ text: '请先同意发布协议' })
return
}
formRef.value.validator(pageData.form, pageData.rules, true).then((res) => {
if (res.isPassed) {
ChanxiaoAPI.purchaseSellAdd(pageData.form).then(() => {
......@@ -166,34 +195,96 @@
<template>
<view class="page">
<!-- 发布/编辑模式 -->
<view v-if="isSave" class="formBox">
<fui-form ref="formRef" label-weight="auto" top="60">
<view class="mt20">
<fui-input disabled required label="采购类别" v-model="form.classifyText" @click="show.classify = true" labelSize="28" label-width="180" />
<fui-input required label="招聘标题" placeholder="请输入标题" v-model="form.title" labelSize="28" label-width="180" />
<view v-if="isSave" class="form-container">
<view class="header-card">
<image class="header-bg" src="/static/images/login/farm_base_bg.png" mode="aspectFill" />
<view class="header-overlay"></view>
<view class="header-content">
<view class="title">发布采购需求</view>
<view class="subtitle">精准对接供货商,高效满足您的采购需求</view>
<view class="stepper-box">
<view class="step-item active">
<view class="step-num">1</view>
<view class="step-text">提交信息</view>
</view>
<view class="step-line"></view>
<view class="step-item">
<view class="step-num">2</view>
<view class="step-text">后台审核</view>
</view>
<view class="step-line"></view>
<view class="step-item">
<view class="step-num">3</view>
<view class="step-text">完成</view>
</view>
</view>
</view>
<view class="mt20">
<!-- 装饰图标 -->
<image class="deco-icon" src="/static/images/nongchang/device2.png" mode="aspectFit" />
</view>
<fui-form ref="formRef" label-weight="auto">
<view class="form-group">
<view class="group-title">基本信息</view>
<fui-input disabled required label="采购分类" v-model="form.classifyText" @click="show.classify = true" placeholder="请选择类别" labelSize="28" label-width="160" size="28" :padding="['16rpx', '0']">
<template #right>
<fui-icon name="arrowright" :size="32" color="#ccc"></fui-icon>
</template>
</fui-input>
<fui-input required label="采购标题" placeholder="例如:长期大量采购红富士苹果" v-model="form.title" labelSize="28" label-width="160" size="28" :padding="['16rpx', '0']" />
</view>
<view class="form-group">
<view class="group-title">预算与数量</view>
<view class="form-item required">
<text class="label">预算区间</text>
<view class="price-range">
<input type="number" class="price-input" v-model="form.priceStart" placeholder="最低价" />
<text class="sep">-</text>
<input type="number" class="price-input" v-model="form.priceEnd" placeholder="最高价" />
<view class="price-range-box">
<input type="number" class="price-input" v-model="form.priceStart" placeholder="最低价" placeholder-class="ph-style" />
<text class="sep"></text>
<input type="number" class="price-input" v-model="form.priceEnd" placeholder="最高价" placeholder-class="ph-style" />
<text class="unit-text"></text>
</view>
</view>
<fui-input required type="number" label="采购数量" v-model="form.count" labelSize="28" label-width="180" />
<fui-input required label="单位" v-model="form.unit" labelSize="28" label-width="180" />
<fui-input required type="number" label="采购数量" placeholder="请输入计划采购数量" v-model="form.count" labelSize="28" label-width="160" size="28" :padding="['16rpx', '0']" />
<fui-input required label="计价单位" placeholder="如:斤、箱、吨" v-model="form.unit" labelSize="28" label-width="160" size="28" :padding="['16rpx', '0']" />
</view>
<view class="mt20">
<fui-input disabled required label="收货地区" v-model="form.areaText" @click="show.address = true" labelSize="28" label-width="180" />
<fui-input required label="截止日期" v-model="form.deadLine" @click="show.time = true" disabled labelSize="28" label-width="180" />
<view class="form-group">
<view class="group-title">时效与地区</view>
<fui-input disabled required label="收货地区" v-model="form.areaText" @click="show.address = true" placeholder="请选择收货地" labelSize="28" label-width="160" size="28" :padding="['16rpx', '0']">
<template #right>
<fui-icon name="arrowright" :size="32" color="#ccc"></fui-icon>
</template>
</fui-input>
<fui-input required label="截止日期" v-model="form.deadLine" @click="show.time = true" disabled placeholder="请选择截止日期" labelSize="28" label-width="160" size="28" :padding="['16rpx', '0']">
<template #right>
<fui-icon name="arrowright" :size="32" color="#ccc"></fui-icon>
</template>
</fui-input>
</view>
<view class="form-group">
<view class="group-title">图片展示</view>
<view class="upload-section">
<view class="upload-tip"><span class="required-star">*</span> 上传参考图片,帮助供货商更准确报价</view>
<uni-file-picker :value="form.imageObj" limit="1" @select="handleUpload" @delete="form.image = ''" />
</view>
</view>
<view class="bg-white mt20" style="padding: 30rpx">
<view class="mb-1" style="font-size: 28rpx"><span style="color: red">*&nbsp;</span>上传参考图片</view>
<uni-file-picker :value="form.imageObj" limit="1" @select="handleUpload" @delete="form.image = null" />
<view class="agreement-row">
<checkbox-group @change="pageData.agree = !pageData.agree">
<label class="checkbox-label">
<checkbox :checked="pageData.agree" color="#fa8c16" style="transform:scale(0.7)" />
<text class="agreement-text">我已阅读并同意 <text class="link">《湘农数智农服发布协议》</text></text>
</label>
</checkbox-group>
</view>
<view class="fui-btn__box">
<fui-button text="立即发布" bold radius="100rpx" @click="submit" />
<view class="submit-btn-box">
<fui-button text="立即发布" bold radius="100rpx" background="#fa8c16" @click="submit" />
</view>
</fui-form>
</view>
......@@ -257,11 +348,187 @@
</template>
<style lang="scss" scoped>
.page { background-color: #f7f8fa; min-height: 100vh; font-family: 'DingTalk Sans', sans-serif; }
.formBox { padding: 24rpx; .mt20 { background: #fff; border-radius: 20rpx; padding: 10rpx 24rpx; margin-bottom: 24rpx; } }
.price-range { display: flex; align-items: center; flex: 1; margin-left: 20rpx; .price-input { flex: 1; text-align: center; font-size: 28rpx; } .sep { margin: 0 10rpx; color: #ccc; } }
.form-item { padding: 30rpx 0; display: flex; align-items: center; border-bottom: 1rpx solid #f8f8f8; .label { font-size: 28rpx; color: #333; width: 180rpx; .red { color: #ff4d4f; } } }
.page { background-color: #f7f9fb; min-height: 100vh; font-family: 'DingTalk Sans', sans-serif; }
.form-container {
padding-bottom: 40rpx;
.header-card {
position: relative;
height: 320rpx;
overflow: hidden;
display: flex;
align-items: center;
.header-bg {
position: absolute;
width: 100%;
height: 100%;
z-index: 1;
}
.header-overlay {
position: absolute;
width: 100%;
height: 100%;
background: linear-gradient(135deg, rgba(250, 140, 22, 0.9) 0%, rgba(255, 187, 150, 0.8) 100%);
z-index: 2;
}
.header-content {
position: relative;
z-index: 3;
width: 100%;
padding: 0 40rpx;
margin-top: -20rpx;
}
.title { font-size: 38rpx; font-weight: bold; color: #fff; margin-bottom: 6rpx; text-shadow: 0 2rpx 4rpx rgba(0,0,0,0.1); }
.subtitle { font-size: 24rpx; color: rgba(255, 255, 255, 0.9); }
.deco-icon {
position: absolute;
right: -20rpx;
bottom: 20rpx;
width: 200rpx;
height: 200rpx;
opacity: 0.15;
z-index: 2;
transform: rotate(-15deg);
}
}
.stepper-box {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 30rpx;
padding: 0 20rpx;
.step-item {
display: flex;
flex-direction: column;
align-items: center;
opacity: 0.7;
position: relative;
z-index: 2;
&.active {
opacity: 1;
.step-num {
background: #fff;
color: #fa8c16;
border-color: #fff;
box-shadow: 0 4rpx 10rpx rgba(0,0,0,0.1);
}
.step-text { font-weight: bold; }
}
.step-num {
width: 36rpx;
height: 36rpx;
border: 2rpx solid rgba(255, 255, 255, 0.8);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 22rpx;
font-weight: bold;
margin-bottom: 8rpx;
color: #fff;
transition: all 0.3s;
}
.step-text {
font-size: 20rpx;
color: #fff;
}
}
.step-line {
flex: 1;
height: 2rpx;
background: rgba(255, 255, 255, 0.4);
margin: 0 15rpx;
margin-top: -30rpx;
}
}
.form-group {
background: #fff;
margin: 20rpx 24rpx 0;
border-radius: 20rpx;
padding: 24rpx 28rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.02);
.group-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
display: flex;
align-items: center;
&::before {
content: '';
width: 6rpx;
height: 28rpx;
background: #fa8c16;
border-radius: 4rpx;
margin-right: 12rpx;
}
}
}
}
.form-item {
padding: 20rpx 0;
display: flex;
align-items: center;
border-bottom: 1rpx solid #f8f8f8;
.label {
font-size: 28rpx;
color: #333;
width: 160rpx;
}
}
.required-star { color: #ff4d4f; margin-right: 4rpx; }
.price-range-box {
display: flex;
align-items: center;
flex: 1;
.price-input {
flex: 1;
height: 72rpx;
background: #f5f7f9;
border-radius: 12rpx;
padding: 0 20rpx;
font-size: 28rpx;
text-align: center;
}
.sep { margin: 0 16rpx; color: #999; font-size: 24rpx; }
.unit-text { margin-left: 12rpx; color: #666; font-size: 28rpx; }
}
.upload-section {
.upload-tip { font-size: 24rpx; color: #666; margin-bottom: 24rpx; }
}
.agreement-row {
padding: 20rpx 40rpx;
.agreement-text { font-size: 24rpx; color: #999; }
.link { color: #fa8c16; }
}
.submit-btn-box {
padding: 40rpx;
}
/* 采购详情样式 */
.product-detail {
.detail-banner { width: 750rpx; height: 500rpx; background-color: #fff; position: relative; .banner-img { width: 100%; height: 100%; } .banner-placeholder { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background-color: #f1f1f1; } .status-tag { position: absolute; right: 30rpx; top: 30rpx; background-color: rgba(93, 182, 111, 0.9); color: #fff; padding: 8rpx 20rpx; border-radius: 30rpx; font-size: 24rpx; } }
......@@ -270,5 +537,8 @@
.bottom-bar { position: fixed; bottom: 0; left: 0; right: 0; height: 110rpx; background-color: #fff; padding: 0 30rpx; display: flex; align-items: center; box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.05); z-index: 100; .action-btns { flex: 1; .contact-btn { height: 80rpx; background-color: #fa8c16; color: #fff; display: flex; align-items: center; justify-content: center; border-radius: 40rpx; font-size: 28rpx; font-weight: bold; } } }
}
:deep(.fui-input__label) { font-family: 'DingTalk Sans' !important; }
:deep(.fui-input__label) { font-family: 'DingTalk Sans' !important; font-size: 28rpx !important; }
:deep(.fui-input__content) { font-size: 28rpx !important; }
:deep(.fui-input__placeholder) { font-size: 26rpx !important; }
.ph-style { font-size: 26rpx !important; color: #ccc; }
</style>
......@@ -62,8 +62,8 @@
province: '',
city: '',
country: '',
image: null,
imageObj: null,
image: '',
imageObj: null as any,
},
rules: [
{ name: 'classify', rule: ['required'], msg: ['请选择分类'] },
......@@ -77,14 +77,25 @@
{ name: 'detailedAddress', rule: ['required'], msg: ['请输入详细地址'] },
{ name: 'image', rule: ['required'], msg: ['请上传图片'] },
],
agree: true
})
const { show, options, form } = toRefs(pageData)
async function initDict() {
pageData.options.classify = dictStore.getDictList.classify.map((item) => {
return { value: item.value, text: item.text }
})
pageData.options.classify = [
{ value: 1, text: '蔬菜' },
{ value: 2, text: '水果' },
{ value: 3, text: '粮食' },
{ value: 4, text: '畜牧' },
{ value: 0, text: '其他' },
]
// 如果有已选分类,需要回显文字
if (pageData.form.classify && pageData.options.classify.length) {
const item = pageData.options.classify.find(i => i.value == pageData.form.classify)
if (item) pageData.form.classifyText = item.text
}
}
function handleAreaConfirm(e) {
......@@ -92,6 +103,8 @@
pageData.form.province = e.text[0]
pageData.form.city = e.text[1]
pageData.form.country = e.text[2]
// @ts-ignore
pageData.form.areaText = e.fullText
}
function getCurrentAddressInfo() {
......@@ -102,15 +115,26 @@
pageData.form.city = res.city
pageData.form.country = res.country
pageData.form.address = `${res.province},${res.city},${res.country}`
// @ts-ignore
const areaCodes = [res.province, res.city, res.country].filter(Boolean).join(',')
pageData.form.areaText = getText(areaCodes, ' / ')
})
}
function getDetails(id) {
pageData.loading = true
ChanxiaoAPI.supplyDetails({ id }).then((res) => {
ChanxiaoAPI.supplyDetails({ id }).then(async (res) => {
pageData.form = res
// 确保字典已加载
if (!pageData.options.classify.length) {
await initDict()
}
// 格式化地区显示
pageData.form.areaText = `${res.province || ''}/${res.city || ''}/${res.country || ''}`.replace(/\/+$/, '')
const areaCodes = [res.province, res.city, res.country].filter(Boolean).join(',')
// @ts-ignore
pageData.form.areaText = getText(areaCodes, ' / ')
if (res.classify) {
const item = pageData.options.classify.find(i => i.value == res.classify)
......@@ -145,7 +169,7 @@
success: (res) => {
const data = JSON.parse(res.data)
if (data.code === 200 || data.code === 0) {
toastRef.show({ type: 'success', text: '上传成功' })
toastRef.value.show({ type: 'success', text: '上传成功' })
pageData.form.image = data.message
}
}
......@@ -153,10 +177,14 @@
}
const formRef = ref()
function submit() {
if (!pageData.agree) {
toastRef.value.show({ text: '请先同意发布协议' })
return
}
formRef.value.validator(pageData.form, pageData.rules, true).then((res) => {
if (res.isPassed) {
ChanxiaoAPI.supplyAdd(pageData.form).then(() => {
toastRef.show({ type: 'success', text: '发布成功' })
toastRef.value.show({ type: 'success', text: '发布成功' })
setTimeout(() => uni.navigateBack(), 1500)
})
}
......@@ -170,36 +198,94 @@
<template>
<view class="page">
<!-- 发布/编辑模式 -->
<view v-if="isSave" class="formBox">
<fui-form ref="formRef" label-weight="auto" top="60">
<view class="mt20">
<fui-input required label="标题" placeholder="请输入标题" v-model="form.title" labelSize="28" label-width="180" />
<fui-input label="规格说明" placeholder="请输入规格说明" v-model="form.productSpecs" labelSize="28" label-width="180" />
<view v-if="isSave" class="form-container">
<view class="header-card">
<image class="header-bg" src="/static/images/login/farm_base_bg.png" mode="aspectFill" />
<view class="header-overlay"></view>
<view class="header-content">
<view class="title">发布供应需求</view>
<view class="subtitle">让精准买家更快速地找到您的优质货源</view>
<view class="stepper-box">
<view class="step-item active">
<view class="step-num">1</view>
<view class="step-text">提交信息</view>
</view>
<view class="step-line"></view>
<view class="step-item">
<view class="step-num">2</view>
<view class="step-text">后台审核</view>
</view>
<view class="step-line"></view>
<view class="step-item">
<view class="step-num">3</view>
<view class="step-text">完成</view>
</view>
</view>
</view>
<view class="mt20">
<!-- 装饰图标 -->
<image class="deco-icon" src="/static/images/nongchang/device1.png" mode="aspectFit" />
</view>
<fui-form ref="formRef" label-weight="auto">
<view class="form-group">
<view class="group-title">基本信息</view>
<fui-input required label="供应标题" placeholder="例如:优质红富士苹果" v-model="form.title" labelSize="28" label-width="160" size="28" :padding="['16rpx', '0']" />
<fui-input label="规格说明" placeholder="例如:一级果,直径80mm以上" v-model="form.productSpecs" labelSize="28" label-width="160" size="28" :padding="['16rpx', '0']" />
<fui-input disabled required label="所属分类" v-model="form.classifyText" @click="show.classify = true" placeholder="请选择分类" labelSize="28" label-width="160" size="28" :padding="['16rpx', '0']">
<template #right>
<fui-icon name="arrowright" :size="32" color="#ccc"></fui-icon>
</template>
</fui-input>
</view>
<view class="form-group">
<view class="group-title">价格与库存</view>
<view class="form-item required">
<text class="label">价格区间</text>
<view class="price-range">
<input type="number" class="price-input" v-model="form.minPrice" placeholder="最低价" />
<text class="sep">-</text>
<input type="number" class="price-input" v-model="form.maxPrice" placeholder="最高价" />
<view class="price-range-box">
<input type="number" class="price-input" v-model="form.minPrice" placeholder="最低价" placeholder-class="ph-style" />
<text class="sep"></text>
<input type="number" class="price-input" v-model="form.maxPrice" placeholder="最高价" placeholder-class="ph-style" />
<text class="unit-text"></text>
</view>
</view>
<fui-input required label="单位" placeholder="请输入" v-model="form.unit" labelSize="28" label-width="180" />
<fui-input required type="number" label="供应数量" v-model="form.supplyQuantity" labelSize="28" label-width="180" />
<fui-input required type="number" label="起订量" v-model="form.minOrderQuantity" labelSize="28" label-width="180" />
<fui-input required label="计价单位" placeholder="如:斤、箱、吨" v-model="form.unit" labelSize="28" label-width="160" size="28" :padding="['16rpx', '0']" />
<fui-input required type="number" label="供应总量" placeholder="输入可供总量" v-model="form.supplyQuantity" labelSize="28" label-width="160" size="28" :padding="['16rpx', '0']" />
<fui-input required type="number" label="起订量" placeholder="输入最低起订数量" v-model="form.minOrderQuantity" labelSize="28" label-width="160" size="28" :padding="['16rpx', '0']" />
</view>
<view class="mt20">
<fui-input disabled required label="所在地区" v-model="form.address" @click="show.address = true" labelSize="28" label-width="180" />
<fui-input required label="详细地址" v-model="form.detailedAddress" labelSize="28" label-width="180" />
<fui-input disabled required label="分类" v-model="form.classifyText" @click="show.classify = true" labelSize="28" label-width="180" />
<view class="form-group">
<view class="group-title">发货信息</view>
<fui-input disabled required label="所在地区" v-model="form.areaText" @click="show.address = true" placeholder="请选择发货地" labelSize="28" label-width="160" size="28" :padding="['16rpx', '0']">
<template #right>
<fui-icon name="arrowright" :size="32" color="#ccc"></fui-icon>
</template>
</fui-input>
<fui-input required label="详细地址" placeholder="街道、门牌号等" v-model="form.detailedAddress" labelSize="28" label-width="160" size="28" :padding="['16rpx', '0']" />
</view>
<view class="form-group">
<view class="group-title">图片展示</view>
<view class="upload-section">
<view class="upload-tip"><span class="required-star">*</span> 上传实拍图片,成交率提升3倍</view>
<uni-file-picker :value="form.imageObj" limit="1" @select="handleUpload" @delete="form.image = ''" />
</view>
</view>
<view class="bg-white mt20" style="padding: 30rpx">
<view class="mb-1" style="font-size: 28rpx"><span style="color: red">*&nbsp;</span>上传图片</view>
<uni-file-picker :value="form.imageObj" limit="1" @select="handleUpload" @delete="form.image = null" />
<view class="agreement-row">
<checkbox-group @change="pageData.agree = !pageData.agree">
<label class="checkbox-label">
<checkbox :checked="pageData.agree" color="#5db66f" style="transform:scale(0.7)" />
<text class="agreement-text">我已阅读并同意 <text class="link">《湘农数智农服发布协议》</text></text>
</label>
</checkbox-group>
</view>
<view class="fui-btn__box">
<fui-button text="立即发布" bold radius="100rpx" @click="submit" />
<view class="submit-btn-box">
<fui-button text="立即发布" bold radius="100rpx" background="#5db66f" @click="submit" />
</view>
</fui-form>
</view>
......@@ -266,18 +352,192 @@
<AreaPicker v-model:show="show.address" :layer="3" @confirm="handleAreaConfirm" />
<fui-picker :show="show.classify" :options="options.classify" @change="handleChangeClassify" @cancel="show.classify = false" />
<fui-date-picker :show="show.time1" type="3" @change="handleChangeTime1" @cancel="show.time1 = false" />
<fui-date-picker :show="show.time2" type="3" @change="handleChangeTime2" @cancel="show.time2 = false" />
<fui-toast ref="toastRef" />
</view>
</template>
<style lang="scss" scoped>
.page { background-color: #f7f8fa; min-height: 100vh; font-family: 'DingTalk Sans', sans-serif; }
.formBox { padding: 24rpx; .mt20 { background: #fff; border-radius: 20rpx; padding: 10rpx 24rpx; margin-bottom: 24rpx; } }
.price-range { display: flex; align-items: center; flex: 1; margin-left: 20rpx; .price-input { flex: 1; text-align: center; font-size: 28rpx; } .sep { margin: 0 10rpx; color: #ccc; } }
.form-item { padding: 30rpx 0; display: flex; align-items: center; border-bottom: 1rpx solid #f8f8f8; .label { font-size: 28rpx; color: #333; width: 180rpx; .red { color: #ff4d4f; } } }
.page { background-color: #f7f9fb; min-height: 100vh; font-family: 'DingTalk Sans', sans-serif; }
.form-container {
padding-bottom: 40rpx;
.header-card {
position: relative;
height: 320rpx;
overflow: hidden;
display: flex;
align-items: center;
.header-bg {
position: absolute;
width: 100%;
height: 100%;
z-index: 1;
}
.header-overlay {
position: absolute;
width: 100%;
height: 100%;
background: linear-gradient(135deg, rgba(93, 182, 111, 0.9) 0%, rgba(76, 175, 80, 0.8) 100%);
z-index: 2;
}
.header-content {
position: relative;
z-index: 3;
width: 100%;
padding: 0 40rpx;
margin-top: -20rpx;
}
.title { font-size: 38rpx; font-weight: bold; color: #fff; margin-bottom: 6rpx; text-shadow: 0 2rpx 4rpx rgba(0,0,0,0.1); }
.subtitle { font-size: 24rpx; color: rgba(255, 255, 255, 0.9); }
.deco-icon {
position: absolute;
right: -20rpx;
bottom: 20rpx;
width: 200rpx;
height: 200rpx;
opacity: 0.15;
z-index: 2;
transform: rotate(-15deg);
}
}
.stepper-box {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 30rpx;
padding: 0 20rpx;
.step-item {
display: flex;
flex-direction: column;
align-items: center;
opacity: 0.7;
position: relative;
z-index: 2;
&.active {
opacity: 1;
.step-num {
background: #fff;
color: #5db66f;
border-color: #fff;
box-shadow: 0 4rpx 10rpx rgba(0,0,0,0.1);
}
.step-text { font-weight: bold; }
}
.step-num {
width: 36rpx;
height: 36rpx;
border: 2rpx solid rgba(255, 255, 255, 0.8);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 22rpx;
font-weight: bold;
margin-bottom: 8rpx;
color: #fff;
transition: all 0.3s;
}
.step-text {
font-size: 20rpx;
color: #fff;
}
}
.step-line {
flex: 1;
height: 2rpx;
background: rgba(255, 255, 255, 0.4);
margin: 0 15rpx;
margin-top: -30rpx;
}
}
.form-group {
background: #fff;
margin: 20rpx 24rpx 0;
border-radius: 20rpx;
padding: 24rpx 28rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.02);
.group-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
display: flex;
align-items: center;
&::before {
content: '';
width: 6rpx;
height: 28rpx;
background: #5db66f;
border-radius: 4rpx;
margin-right: 12rpx;
}
}
}
}
.form-item {
padding: 20rpx 0;
display: flex;
align-items: center;
border-bottom: 1rpx solid #f8f8f8;
.label {
font-size: 28rpx;
color: #333;
width: 160rpx;
}
}
.required-star { color: #ff4d4f; margin-right: 4rpx; }
.price-range-box {
display: flex;
align-items: center;
flex: 1;
.price-input {
flex: 1;
height: 72rpx;
background: #f5f7f9;
border-radius: 12rpx;
padding: 0 20rpx;
font-size: 28rpx;
text-align: center;
}
.sep { margin: 0 16rpx; color: #999; font-size: 24rpx; }
.unit-text { margin-left: 12rpx; color: #666; font-size: 28rpx; }
}
.upload-section {
.upload-tip { font-size: 24rpx; color: #666; margin-bottom: 24rpx; }
}
.agreement-row {
padding: 20rpx 40rpx;
.agreement-text { font-size: 24rpx; color: #999; }
.link { color: #5db66f; }
}
.submit-btn-box {
padding: 40rpx;
}
/* 商品详情模式样式 */
.product-detail {
.detail-banner { width: 750rpx; height: 600rpx; background-color: #fff; .banner-img { width: 100%; height: 100%; } .banner-placeholder { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background-color: #f1f1f1; } }
......@@ -286,5 +546,8 @@
.bottom-bar { position: fixed; bottom: 0; left: 0; right: 0; height: 110rpx; background-color: #fff; padding: 0 30rpx; display: flex; align-items: center; box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.05); z-index: 100; .action-btns { flex: 1; .contact-btn { height: 80rpx; background-color: #5db66f; color: #fff; display: flex; align-items: center; justify-content: center; border-radius: 40rpx; font-size: 28rpx; font-weight: bold; } } }
}
:deep(.fui-input__label) { font-family: 'DingTalk Sans' !important; }
:deep(.fui-input__label) { font-family: 'DingTalk Sans' !important; font-size: 28rpx !important; }
:deep(.fui-input__content) { font-size: 28rpx !important; }
:deep(.fui-input__placeholder) { font-size: 26rpx !important; }
.ph-style { font-size: 26rpx !important; color: #ccc; }
</style>
......@@ -141,6 +141,9 @@
function onSlideSuccess() {
if (slideType.value === 'sms') {
// 立即开启倒计时(乐观更新),接口报错也不停止,防止恶意连点
startCountdown()
// 发送验证码
const params = {
mobile: model.form.data.username,
......@@ -149,10 +152,10 @@
API.sysSms(params)
.then(async () => {
Message.toast('验证码已发送')
startCountdown()
})
.catch(() => {
Message.toast('验证码发送失败')
.catch((err) => {
// 即使接口返回 code: 130 等错误,也不干扰已启动的倒计时
console.error('短信发送业务异常:', err)
})
} else {
// 执行登录
......
......@@ -93,6 +93,9 @@
function onSlideSuccess() {
if (slideType.value === 'sms') {
// 开启乐观倒计时,不论接口成败,60s内不准重发
startCountdown()
const params = {
mobile: model.form.data.phone,
smsmode: 2, // 2-注册
......@@ -100,10 +103,9 @@
API.sysSms(params)
.then(async () => {
Message.toast('验证码已发送')
startCountdown()
})
.catch(() => {
Message.toast('验证码发送失败')
.catch((err) => {
console.error('注册短信发送失败:', err)
})
} else {
doRegister()
......
......@@ -747,22 +747,74 @@
</view>
</view>
<fui-fab
v-if="!userStore.isAuditMode"
position="right"
distance="10"
bottom="240"
width="96"
@click="handlePublish"
>
<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 class="fab-container" v-if="!userStore.isAuditMode" @click="handlePublish">
<view class="ripple"></view>
<view class="ripple ripple-2"></view>
<view class="fab-entry">
<image style="width: 48rpx; height: 48rpx" src="/static/images/nongchang/work_icon.png" />
<text>找人干活</text>
</view>
</fui-fab>
</view>
</template>
<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 {
margin-top: 38rpx;
}
......
......@@ -7,13 +7,14 @@
import * as nongjifuwu from '@/api/model/nongjifuwu'
import { getText } from '@/utils/dict/area'
import AreaPicker from '@/components/AreaPicker/index.vue'
import ApplyDialog from '@/pages/nongjifuwu/components/apply-dialog.vue'
const userStore = useUserStore()
const globSetting = useGlobSetting()
onLoad((option) => {
pageData.form.type = option.type
if (option.id) {
pageData.title = '查看农活详情'
pageData.title = '农活详情'
getDetails(option.id)
} else {
pageData.title = '新增农活作业'
......@@ -66,7 +67,13 @@
nongjifuwu.farmMachineDetails({ id }).then((res) => {
pageData.form = res
pageData.form.pictureObj = res.picture && parseUrlInfo(res.picture)
if (res.scope) pageData.optionsValText = getText(res.scope, ' / ')
// 优先用接口返回的中文名拼接,getText 作为兜底
const nameParts = [res.cityName, res.districtName].filter(Boolean)
if (nameParts.length > 0) {
pageData.optionsValText = nameParts.join('')
} else if (res.scope) {
pageData.optionsValText = getText(res.scope, '')
}
}).finally(() => pageData.loading = false)
}
......@@ -112,6 +119,14 @@
const d = new Date()
return `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`
}
const applyDialogRef = ref()
function handleApply() {
applyDialogRef.value.open({
id: pageData.form.id,
serviceType: pageData.form.type,
})
}
</script>
<template>
......@@ -155,15 +170,121 @@
</fui-form>
</view>
<!-- 详情模式... (已在之前步骤重构) -->
<view v-else class="product-detail">
<!-- 详情 UI 内容 -->
<view class="detail-banner">
<image v-if="form.picture" :src="form.picture" mode="aspectFill" class="banner-img" />
<view v-else class="banner-placeholder"><fui-icon name="picture" :size="100" color="#ddd"></fui-icon></view>
<!-- 详情模式 -->
<view v-else class="detail-page">
<!-- 顶部封面图区域 -->
<view class="detail-hero">
<image
v-if="form.picture"
:src="form.picture"
mode="aspectFill"
class="hero-img"
/>
<view v-else class="hero-placeholder">
<fui-icon name="picture" :size="120" color="rgba(255,255,255,0.4)"></fui-icon>
<text class="hero-placeholder-text">暂无图片</text>
</view>
<!-- 顶部渐变遮罩 -->
<view class="hero-overlay"></view>
<!-- 状态标签 -->
<view class="hero-badge">
<view class="badge-dot"></view>
<text class="badge-text">招募中</text>
</view>
</view>
<view class="info-card">
<text class="product-title">{{ form.name }}</text>
<!-- 主信息卡片 -->
<view class="main-card">
<text class="service-title">{{ form.name }}</text>
<!-- 价格/关键信息行 -->
<view class="key-info-row">
<view class="key-tag">
<fui-icon name="time" :size="26" color="#5db66f"></fui-icon>
<text class="key-tag-text">{{ form.startTime || '--' }}{{ form.endTime || '--' }}</text>
</view>
</view>
</view>
<!-- 详细信息 section -->
<view class="info-section">
<view class="section-header">
<view class="section-bar"></view>
<text class="section-title">作业信息</text>
</view>
<view class="info-grid">
<view class="info-row">
<view class="info-icon-wrap">
<fui-icon name="location" :size="32" color="#5db66f"></fui-icon>
</view>
<view class="info-content">
<text class="info-label">服务范围</text>
<text class="info-value">{{ pageData.optionsValText || form.scope || '暂未填写' }}</text>
</view>
</view>
<view class="info-divider"></view>
<view class="info-row">
<view class="info-icon-wrap">
<fui-icon name="map" :size="32" color="#5db66f"></fui-icon>
</view>
<view class="info-content">
<text class="info-label">详细地址</text>
<text class="info-value">{{ form.address || '暂未填写' }}</text>
</view>
</view>
<view class="info-divider"></view>
<view class="info-row">
<view class="info-icon-wrap">
<fui-icon name="mobile" :size="32" color="#5db66f"></fui-icon>
</view>
<view class="info-content">
<text class="info-label">联系方式</text>
<text class="info-value">{{ form.phone || '暂未填写' }}</text>
</view>
</view>
<view v-if="form.startTime || form.endTime" class="info-divider"></view>
<view v-if="form.startTime || form.endTime" class="info-row">
<view class="info-icon-wrap">
<fui-icon name="time" :size="32" color="#5db66f"></fui-icon>
</view>
<view class="info-content">
<text class="info-label">作业时间</text>
<text class="info-value">{{ form.startTime }}{{ form.endTime }}</text>
</view>
</view>
</view>
</view>
<!-- 作业需求 section -->
<view class="info-section" v-if="form.demand">
<view class="section-header">
<view class="section-bar"></view>
<text class="section-title">作业需求</text>
</view>
<view class="demand-box">
<text class="demand-text">{{ form.demand }}</text>
</view>
</view>
<!-- 底部安全高度 -->
<view style="height: 180rpx;"></view>
<!-- 固定底部栏 -->
<view class="detail-footer">
<view class="footer-hint">
<text class="hint-title">有意接单?立即报名</text>
<text class="hint-sub">平台将为您推送给发布方</text>
</view>
<view class="footer-apply-btn" @tap="handleApply">
<text class="apply-btn-text">立即报名</text>
</view>
</view>
</view>
......@@ -172,14 +293,292 @@
<fui-date-picker :show="show.time2" type="3" @change="handleChangeTime2" @cancel="show.time2 = false" :min-date="getCurrentDate()" />
<fui-toast ref="toastRef" />
<fui-loading isFixed v-if="pageData.loading" />
<ApplyDialog ref="applyDialogRef" />
</view>
</template>
<style lang="scss" scoped>
.page { background-color: #f7f8fa; min-height: 100vh; font-family: 'DingTalk Sans', sans-serif; }
.formBox { padding: 24rpx; .mt20 { background: #fff; border-radius: 20rpx; padding: 10rpx 24rpx; margin-bottom: 24rpx; } }
.form-item { padding: 30rpx 0; display: flex; align-items: center; border-bottom: 1rpx solid #f8f8f8; .label { font-size: 28rpx; color: #333; width: 180rpx; .red { color: #ff4d4f; } } }
.time-range { display: flex; align-items: center; flex: 1; margin-left: 20rpx; .time-input { flex: 1; text-align: center; } .sep { margin: 0 10rpx; color: #ccc; } }
.page {
background-color: #f8fafc;
min-height: 100vh;
font-family: 'DingTalk Sans', sans-serif;
}
/* ===== 发布模式 ===== */
.formBox {
padding: 24rpx;
.mt20 {
background: #fff;
border-radius: 20rpx;
padding: 10rpx 24rpx;
margin-bottom: 24rpx;
}
}
.form-item {
padding: 30rpx 0;
display: flex;
align-items: center;
border-bottom: 1rpx solid #f8f8f8;
.label { font-size: 28rpx; color: #333; width: 180rpx; }
}
.time-range {
display: flex;
align-items: center;
flex: 1;
margin-left: 20rpx;
.time-input { flex: 1; text-align: center; }
.sep { margin: 0 10rpx; color: #ccc; }
}
.select-text { font-size: 28rpx; &.placeholder { color: #ccc; } }
:deep(.fui-input__label) { font-family: 'DingTalk Sans' !important; }
/* ===== 详情模式 ===== */
.detail-page {
background-color: #f8fafc;
}
.detail-hero {
position: relative;
width: 100%;
height: 520rpx;
background: linear-gradient(135deg, #3a944c 0%, #5db66f 100%);
overflow: hidden;
.hero-img {
width: 100%;
height: 100%;
display: block;
}
.hero-placeholder {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.hero-placeholder-text {
font-size: 26rpx;
color: rgba(255,255,255,0.5);
margin-top: 20rpx;
}
}
.hero-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 240rpx;
background: linear-gradient(to top, rgba(0,0,0,0.45) 0%, transparent 100%);
}
.hero-badge {
position: absolute;
top: 30rpx;
right: 30rpx;
display: flex;
align-items: center;
background-color: rgba(255,255,255,0.18);
backdrop-filter: blur(8px);
border: 1rpx solid rgba(255,255,255,0.3);
padding: 10rpx 22rpx;
border-radius: 40rpx;
.badge-dot {
width: 14rpx;
height: 14rpx;
border-radius: 50%;
background-color: #52c41a;
margin-right: 10rpx;
box-shadow: 0 0 0 4rpx rgba(82,196,26,0.3);
}
.badge-text {
font-size: 24rpx;
color: #fff;
font-weight: 500;
}
}
}
.main-card {
background-color: #fff;
margin: -40rpx 24rpx 20rpx;
border-radius: 28rpx;
padding: 36rpx 32rpx 28rpx;
box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.08);
position: relative;
z-index: 10;
.service-title {
font-size: 40rpx;
font-weight: bold;
color: #1a1a1a;
line-height: 1.35;
display: block;
margin-bottom: 24rpx;
letter-spacing: 1rpx;
}
.key-info-row {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
.key-tag {
display: flex;
align-items: center;
background-color: #f0faf2;
border-radius: 10rpx;
padding: 10rpx 20rpx;
.key-tag-text {
font-size: 26rpx;
color: #3a944c;
margin-left: 8rpx;
font-weight: 500;
}
}
}
}
.info-section {
background-color: #fff;
margin: 0 24rpx 20rpx;
border-radius: 24rpx;
padding: 32rpx;
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
.section-header {
display: flex;
align-items: center;
margin-bottom: 28rpx;
.section-bar {
width: 8rpx;
height: 36rpx;
background: linear-gradient(to bottom, #5db66f, #3a944c);
border-radius: 4rpx;
margin-right: 16rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #1a1a1a;
}
}
.info-grid {
.info-row {
display: flex;
align-items: flex-start;
padding: 4rpx 0;
.info-icon-wrap {
width: 56rpx;
height: 56rpx;
background-color: #f0faf2;
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.info-content {
flex: 1;
margin-left: 20rpx;
padding: 4rpx 0;
.info-label {
font-size: 24rpx;
color: #94a3b8;
display: block;
margin-bottom: 6rpx;
}
.info-value {
font-size: 30rpx;
color: #1e293b;
line-height: 1.4;
font-weight: 500;
}
}
}
.info-divider {
height: 1rpx;
background-color: #f1f5f9;
margin: 24rpx 0;
}
}
.demand-box {
background-color: #f8fafc;
border-radius: 16rpx;
padding: 28rpx;
border-left: 6rpx solid #5db66f;
.demand-text {
font-size: 28rpx;
color: #475569;
line-height: 1.7;
}
}
}
/* ===== 底部操作栏 ===== */
.detail-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(255,255,255,0.97);
backdrop-filter: blur(10px);
padding: 20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom));
box-shadow: 0 -8rpx 32rpx rgba(0,0,0,0.06);
z-index: 100;
display: flex;
align-items: center;
justify-content: space-between;
.footer-hint {
display: flex;
flex-direction: column;
.hint-title {
font-size: 30rpx;
font-weight: bold;
color: #1e293b;
}
.hint-sub {
font-size: 22rpx;
color: #94a3b8;
margin-top: 4rpx;
}
}
.footer-apply-btn {
background: linear-gradient(135deg, #5db66f 0%, #3a944c 100%);
height: 88rpx;
padding: 0 52rpx;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(93,182,111,0.35);
&:active {
opacity: 0.9;
transform: scale(0.97);
}
.apply-btn-text {
font-size: 30rpx;
color: #fff;
font-weight: bold;
letter-spacing: 2rpx;
}
}
}
</style>
......@@ -4,14 +4,16 @@
import { useUserStore } from '@/store/modules/user'
import { useGlobSetting } from '/@/hooks/setting'
import * as nongjifuwu from '@/api/model/nongjifuwu'
import { getText } from '@/utils/dict/area'
import AreaPicker from '@/components/AreaPicker/index.vue'
import ApplyDialog from '@/pages/nongjifuwu/components/apply-dialog.vue'
const userStore = useUserStore()
const globSetting = useGlobSetting()
onLoad((option) => {
pageData.form.type = option.type
if (option.id) {
pageData.title = '查看农机详情'
pageData.title = '农机服务详情'
getDetails(option.id)
} else {
pageData.title = '新增农机作业'
......@@ -57,6 +59,13 @@
nongjifuwu.farmMachineDetails({ id }).then((res) => {
pageData.form = res
pageData.form.pictureObj = res.picture && parseUrlInfo(res.picture)
// 优先用接口返回的中文名拼接,getText 作为兜底
const nameParts = [res.cityName, res.districtName].filter(Boolean)
if (nameParts.length > 0) {
pageData.optionsValText = nameParts.join('')
} else if (res.scope) {
pageData.optionsValText = getText(res.scope, '')
}
}).finally(() => pageData.loading = false)
}
......@@ -66,7 +75,6 @@
}
const toastRef = ref()
const uploadRef = ref()
function handleUpload(file) {
uni.uploadFile({
url: `${globSetting.apiUrl + globSetting.urlPrefix}/sys/common/upload`,
......@@ -97,12 +105,21 @@
}
})
}
const applyDialogRef = ref()
function handleApply() {
applyDialogRef.value.open({
id: pageData.form.id,
serviceType: pageData.form.type,
})
}
</script>
<template>
<view class="page">
<view class="formBox">
<fui-form ref="formRef" label-weight="auto" top="60" :disabled="form.id ? true : false">
<!-- 发布模式 -->
<view v-if="!form.id" class="formBox">
<fui-form ref="formRef" label-weight="auto" top="60">
<view class="mt20">
<fui-input required label="作业标题" placeholder="请输入" v-model="form.name" labelSize="28" label-width="180" />
<fui-input required label="联系方式" type="number" placeholder="请输入手机号" v-model="form.phone" labelSize="28" label-width="180" />
......@@ -124,23 +141,517 @@
<uni-file-picker :value="form.pictureObj" limit="1" @select="handleUpload" @delete="handleDelete" />
</view>
<view class="fui-btn__box" v-if="!form.id">
<view class="fui-btn__box">
<fui-button text="提交发布" bold radius="100rpx" @click="submit" />
</view>
</fui-form>
</view>
<!-- 详情模式 -->
<view v-else class="detail-page">
<!-- 顶部封面图 -->
<view class="detail-hero">
<image
v-if="form.picture"
:src="form.picture"
mode="aspectFill"
class="hero-img"
/>
<view v-else class="hero-placeholder">
<fui-icon name="picture" :size="120" color="rgba(255,255,255,0.4)"></fui-icon>
<text class="hero-placeholder-text">暂无图片</text>
</view>
<view class="hero-overlay"></view>
<!-- 价格浮层 -->
<view class="hero-price-float">
<text class="price-symbol">¥</text>
<text class="price-num">{{ form.price }}</text>
<text class="price-unit">/亩</text>
</view>
<!-- 服务状态 -->
<view class="hero-badge">
<view class="badge-dot"></view>
<text class="badge-text">服务中</text>
</view>
</view>
<!-- 主信息卡 -->
<view class="main-card">
<text class="service-title">{{ form.name }}</text>
<view class="price-highlight-row">
<view class="price-wrap">
<text class="price-label">作业收费</text>
<view class="price-main">
<text class="ph-symbol">¥</text>
<text class="ph-value">{{ form.price }}</text>
<text class="ph-unit">/亩</text>
</view>
</view>
<view class="divider-v"></view>
<view class="scope-wrap">
<text class="scope-label">服务范围</text>
<text class="scope-value">{{ pageData.optionsValText || '全国' }}</text>
</view>
</view>
</view>
<!-- 联系信息 section -->
<view class="info-section">
<view class="section-header">
<view class="section-bar"></view>
<text class="section-title">服务信息</text>
</view>
<AreaPicker v-model:show="show.address" :layer="3" title="选择服务范围" @confirm="handleAreaConfirm" />
<fui-toast ref="toastRef" />
<fui-loading isFixed v-if="pageData.loading" />
<view class="info-grid">
<view class="info-row">
<view class="info-icon-wrap orange">
<fui-icon name="mobile" :size="30" color="#fa8c16"></fui-icon>
</view>
<view class="info-content">
<text class="info-label">联系方式</text>
<text class="info-value">{{ form.phone || '暂未填写' }}</text>
</view>
</view>
<view class="info-divider"></view>
<view class="info-row">
<view class="info-icon-wrap orange">
<fui-icon name="location" :size="30" color="#fa8c16"></fui-icon>
</view>
<view class="info-content">
<text class="info-label">服务区域</text>
<text class="info-value">{{ pageData.optionsValText || form.scope || '全国范围' }}</text>
</view>
</view>
</view>
</view>
<!-- 服务说明区域(如有更多字段可展开) -->
<view class="tips-section">
<view class="tips-row">
<view class="tip-item">
<view class="tip-icon">
<fui-icon name="shield" :size="36" color="#5db66f"></fui-icon>
</view>
<text class="tip-text">平台认证</text>
</view>
<view class="tip-divider"></view>
<view class="tip-item">
<view class="tip-icon">
<fui-icon name="notice" :size="36" color="#5db66f"></fui-icon>
</view>
<text class="tip-text">专业团队</text>
</view>
<view class="tip-divider"></view>
<view class="tip-item">
<view class="tip-icon">
<fui-icon name="tag" :size="36" color="#5db66f"></fui-icon>
</view>
<text class="tip-text">按亩计价</text>
</view>
</view>
</view>
<!-- 底部安全高度 -->
<view style="height: 180rpx;"></view>
<!-- 固定底部栏 -->
<view class="detail-footer">
<view class="footer-price-area">
<text class="footer-price-label">作业价格</text>
<view class="footer-price-main">
<text class="fp-symbol">¥</text>
<text class="fp-value">{{ form.price }}</text>
<text class="fp-unit">/亩起</text>
</view>
</view>
<view class="footer-apply-btn" @tap="handleApply">
<text class="apply-btn-text">立即预约</text>
</view>
</view>
</view>
<AreaPicker v-model:show="show.address" :layer="3" title="选择服务范围" @confirm="handleAreaConfirm" />
<fui-toast ref="toastRef" />
<fui-loading isFixed v-if="pageData.loading" />
<ApplyDialog ref="applyDialogRef" />
</view>
</template>
<style lang="scss" scoped>
.page { background-color: #f7f8fa; min-height: 100vh; font-family: 'DingTalk Sans', sans-serif; }
.formBox { padding: 24rpx; .mt20 { background: #fff; border-radius: 20rpx; padding: 10rpx 24rpx; margin-bottom: 24rpx; } }
.form-item { padding: 30rpx 0; display: flex; align-items: center; border-bottom: 1rpx solid #f8f8f8; .label { font-size: 28rpx; color: #333; width: 180rpx; .red { color: #ff4d4f; } } }
.page {
background-color: #f8fafc;
min-height: 100vh;
font-family: 'DingTalk Sans', sans-serif;
}
/* ===== 发布模式 ===== */
.formBox {
padding: 24rpx;
.mt20 {
background: #fff;
border-radius: 20rpx;
padding: 10rpx 24rpx;
margin-bottom: 24rpx;
}
}
.form-item {
padding: 30rpx 0;
display: flex;
align-items: center;
border-bottom: 1rpx solid #f8f8f8;
.label { font-size: 28rpx; color: #333; width: 180rpx; }
}
.select-text { font-size: 28rpx; &.placeholder { color: #ccc; } }
.unit-slot { font-size: 28rpx; color: #999; margin-left: 12rpx; }
:deep(.fui-input__label) { font-family: 'DingTalk Sans' !important; }
/* ===== 详情模式 ===== */
.detail-page {
background-color: #f8fafc;
}
.detail-hero {
position: relative;
width: 100%;
height: 520rpx;
background: linear-gradient(135deg, #e65c00 0%, #fa8c16 50%, #f9af31 100%);
overflow: hidden;
.hero-img {
width: 100%;
height: 100%;
display: block;
}
.hero-placeholder {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.hero-placeholder-text {
font-size: 26rpx;
color: rgba(255,255,255,0.5);
margin-top: 20rpx;
}
}
.hero-overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 280rpx;
background: linear-gradient(to top, rgba(0,0,0,0.5) 0%, transparent 100%);
}
/* 左下角价格浮层 */
.hero-price-float {
position: absolute;
bottom: 60rpx;
left: 32rpx;
display: flex;
align-items: baseline;
.price-symbol {
font-size: 30rpx;
color: #fff;
font-weight: bold;
opacity: 0.9;
}
.price-num {
font-size: 72rpx;
color: #fff;
font-weight: bold;
line-height: 1;
margin-left: 4rpx;
text-shadow: 0 2rpx 10rpx rgba(0,0,0,0.3);
}
.price-unit {
font-size: 28rpx;
color: rgba(255,255,255,0.8);
margin-left: 8rpx;
}
}
.hero-badge {
position: absolute;
top: 30rpx;
right: 30rpx;
display: flex;
align-items: center;
background-color: rgba(255,255,255,0.18);
backdrop-filter: blur(8px);
border: 1rpx solid rgba(255,255,255,0.3);
padding: 10rpx 22rpx;
border-radius: 40rpx;
.badge-dot {
width: 14rpx;
height: 14rpx;
border-radius: 50%;
background-color: #52c41a;
margin-right: 10rpx;
box-shadow: 0 0 0 4rpx rgba(82,196,26,0.3);
}
.badge-text {
font-size: 24rpx;
color: #fff;
font-weight: 500;
}
}
}
.main-card {
background-color: #fff;
margin: -40rpx 24rpx 20rpx;
border-radius: 28rpx;
padding: 36rpx 32rpx 28rpx;
box-shadow: 0 8rpx 32rpx rgba(0,0,0,0.08);
position: relative;
z-index: 10;
.service-title {
font-size: 40rpx;
font-weight: bold;
color: #1a1a1a;
line-height: 1.35;
display: block;
margin-bottom: 28rpx;
letter-spacing: 1rpx;
}
.price-highlight-row {
display: flex;
align-items: center;
background-color: #fff8f0;
border-radius: 16rpx;
padding: 24rpx;
.price-wrap {
flex: 1;
display: flex;
flex-direction: column;
.price-label {
font-size: 22rpx;
color: #94a3b8;
margin-bottom: 8rpx;
}
.price-main {
display: flex;
align-items: baseline;
.ph-symbol { font-size: 24rpx; color: #fa541c; font-weight: bold; }
.ph-value { font-size: 52rpx; color: #fa541c; font-weight: bold; margin-left: 2rpx; line-height: 1; }
.ph-unit { font-size: 22rpx; color: #fa8c16; margin-left: 4rpx; }
}
}
.divider-v {
width: 1rpx;
height: 60rpx;
background-color: #ffd591;
margin: 0 28rpx;
}
.scope-wrap {
flex: 1.2;
display: flex;
flex-direction: column;
.scope-label {
font-size: 22rpx;
color: #94a3b8;
margin-bottom: 8rpx;
}
.scope-value {
font-size: 28rpx;
color: #1e293b;
font-weight: 600;
line-height: 1.3;
}
}
}
}
.info-section {
background-color: #fff;
margin: 0 24rpx 20rpx;
border-radius: 24rpx;
padding: 32rpx;
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
.section-header {
display: flex;
align-items: center;
margin-bottom: 28rpx;
.section-bar {
width: 8rpx;
height: 36rpx;
background: linear-gradient(to bottom, #fa8c16, #e65c00);
border-radius: 4rpx;
margin-right: 16rpx;
}
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #1a1a1a;
}
}
.info-grid {
.info-row {
display: flex;
align-items: flex-start;
padding: 4rpx 0;
.info-icon-wrap {
width: 56rpx;
height: 56rpx;
background-color: #f0faf2;
border-radius: 14rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
&.orange {
background-color: #fff8f0;
}
}
.info-content {
flex: 1;
margin-left: 20rpx;
padding: 4rpx 0;
.info-label {
font-size: 24rpx;
color: #94a3b8;
display: block;
margin-bottom: 6rpx;
}
.info-value {
font-size: 30rpx;
color: #1e293b;
line-height: 1.4;
font-weight: 500;
}
}
}
.info-divider {
height: 1rpx;
background-color: #f1f5f9;
margin: 24rpx 0;
}
}
}
/* 服务保障小图标行 */
.tips-section {
background-color: #fff;
margin: 0 24rpx 20rpx;
border-radius: 24rpx;
padding: 32rpx;
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
.tips-row {
display: flex;
align-items: center;
.tip-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
.tip-icon {
width: 72rpx;
height: 72rpx;
background-color: #f0faf2;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12rpx;
}
.tip-text {
font-size: 24rpx;
color: #475569;
font-weight: 500;
}
}
.tip-divider {
width: 1rpx;
height: 56rpx;
background-color: #f1f5f9;
}
}
}
/* ===== 底部操作栏 ===== */
.detail-footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(255,255,255,0.97);
backdrop-filter: blur(10px);
padding: 20rpx 32rpx calc(20rpx + env(safe-area-inset-bottom));
box-shadow: 0 -8rpx 32rpx rgba(0,0,0,0.06);
z-index: 100;
display: flex;
align-items: center;
justify-content: space-between;
.footer-price-area {
display: flex;
flex-direction: column;
.footer-price-label {
font-size: 22rpx;
color: #94a3b8;
margin-bottom: 4rpx;
}
.footer-price-main {
display: flex;
align-items: baseline;
.fp-symbol { font-size: 22rpx; color: #fa541c; font-weight: bold; }
.fp-value { font-size: 44rpx; color: #fa541c; font-weight: bold; margin-left: 2rpx; line-height: 1; }
.fp-unit { font-size: 22rpx; color: #fa8c16; margin-left: 4rpx; }
}
}
.footer-apply-btn {
background: linear-gradient(135deg, #fa8c16 0%, #fa541c 100%);
height: 88rpx;
padding: 0 52rpx;
border-radius: 44rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(250,140,22,0.35);
&:active {
opacity: 0.9;
transform: scale(0.97);
}
.apply-btn-text {
font-size: 30rpx;
color: #fff;
font-weight: bold;
letter-spacing: 2rpx;
}
}
}
</style>
......@@ -3,6 +3,21 @@ import { cascaderHn } from '/@/api/model/dict'
const storageKey = 'app_dict_data_area_cascaderHn'
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() {
// 先从本地加载数据
let dictData = getLocalDict()
......@@ -15,6 +30,9 @@ export async function getDictData() {
}
areaOptions = dictData
// 构建查询Map
areaMap.clear()
buildAreaMap(areaOptions)
return dictData
}
......@@ -24,7 +42,11 @@ export async function getDictData() {
export function getLocalDict() {
const data = uni.getStorageSync(storageKey)
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
}
......@@ -40,37 +62,16 @@ export function refreshDictData() {
}
export function getText(scope: string, spliced: string) {
if (!scope || !areaOptions || areaOptions.length === 0) {
return ''
if (!scope) return ''
// 如果Map尚未初始化,则回退到原始递归方法或返回空
if (areaMap.size === 0) {
const dict = getLocalDict()
if (!dict) return ''
}
const values = scope.split(',')
const labels = []
// 递归查找label
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
}
const labels = values.map(val => areaMap.get(val.trim()) || val.trim())
for (const value of values) {
const text = findLabel(areaOptions, value.trim())
labels.push(text)
}
if (spliced) {
return labels ? labels.join(spliced) : ''
} else {
return labels ? labels.join('') : ''
}
return labels.join(spliced || '')
}
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论