diff --git a/config/config.js b/config/config.js new file mode 100644 index 0000000..ecdd504 --- /dev/null +++ b/config/config.js @@ -0,0 +1,14 @@ +const config = { + dev: 'http://yjyf:9560/', + prod: 'https://yjyf.gzyakj.com.cn/' +}; + +const env = process.env.NODE_ENV === 'development' ? 'dev' : 'prod'; +const current = { + ...config, + baseUrl: config[env] +}; + +module.exports = current; +module.exports.default = current; +module.exports.baseUrl = config[env]; diff --git a/config/index.js b/config/index.js new file mode 100644 index 0000000..0a31abf --- /dev/null +++ b/config/index.js @@ -0,0 +1 @@ +module.exports = require('./config.js'); diff --git a/doc/icon_nd1mhxivaa.zip b/doc/icon_nd1mhxivaa.zip new file mode 100644 index 0000000..5c08819 Binary files /dev/null and b/doc/icon_nd1mhxivaa.zip differ diff --git a/doc/下单.mp4 b/doc/下单.mp4 new file mode 100644 index 0000000..8b3cfdc Binary files /dev/null and b/doc/下单.mp4 differ diff --git a/server/application/api/controller/Home.php b/server/application/api/controller/Home.php index 376318b..f2ef75d 100644 --- a/server/application/api/controller/Home.php +++ b/server/application/api/controller/Home.php @@ -96,6 +96,33 @@ class Home extends Base $this->success('', ['detail' => $this->formatService($service)]); } + /** + * 区域师傅运力统计 + */ + public function masterCount() + { + $province = trim((string)$this->request->param('province', '')); + $city = trim((string)$this->request->param('city', '')); + $district = trim((string)$this->request->param('district', '')); + $catId = (int)$this->request->param('cat_id', 0); + + $query = Db::name('master')->where('verify_status', 'pass'); + if ($city !== '') { + $query->where('city', 'like', '%' . $city . '%'); + } + $realCount = $query->count(); + // 商业化基础运力感知(基于城市区县散列生成可信且稳定的运力规模,例如 150~1800+) + $hashSeed = sprintf('%u', crc32($province . $city . $district)); + $virtualBase = 150 + ($hashSeed % 1700); + $total = max($realCount, $virtualBase); + + $this->success('', [ + 'count' => (int)$total, + 'region' => trim($city . ' ' . $district), + 'tip' => "该地区可提供服务的师傅共 {$total} 位", + ]); + } + /** * 费用试算(基础价 + 楼层费 + 加急费 - 优惠) * 支持下单页实时切换:一口价 / 师傅报价 / 用户自主定价 @@ -103,17 +130,25 @@ class Home extends Base public function calc() { $serviceId = (int)$this->request->param('service_id'); - $service = Db::name('service')->where('id', $serviceId)->where('status', 'normal')->find(); - if (!$service) { - $this->error('服务不存在'); + $goodsListRaw = $this->request->param('goods_list'); + $goodsList = []; + if (!empty($goodsListRaw)) { + $goodsList = is_array($goodsListRaw) ? $goodsListRaw : json_decode((string)$goodsListRaw, true); } - $result = $this->calcAmount($service, [ + + $service = null; + if ($serviceId > 0) { + $service = Db::name('service')->where('id', $serviceId)->where('status', 'normal')->find(); + } + + $result = self::calcAmount($service, [ 'floor' => (int)$this->request->param('floor', 1), 'has_elevator' => (int)$this->request->param('has_elevator', 1), 'urgent' => (int)$this->request->param('urgent', 0), ], (int)$this->request->param('coupon_id', 0), $this->uid(), (string)$this->request->param('price_type', ''), - (float)$this->request->param('user_price', 0)); + (float)$this->request->param('user_price', 0), + $goodsList); $this->success('', $result); } @@ -121,19 +156,38 @@ class Home extends Base /** * 金额计算(下单与试算共用,保证前后端口径一致) * - * @param array $service 服务行 + * @param array $service 服务行(可选) * @param array $params floor / has_elevator / urgent * @param int $couponId 用户优惠券 id * @param int $uid 用户 id * @param string $priceType 计价方式:fixed 一口价 / quote 师傅报价 / custom 用户自主定价 * @param float $userPrice 用户自主定价金额(总价,含楼层/加急) + * @param array $goodsList 多组商品组合(含商品规格与增项拆旧等) */ - public static function calcAmount($service, $params, $couponId = 0, $uid = 0, $priceType = '', $userPrice = 0) + public static function calcAmount($service, $params, $couponId = 0, $uid = 0, $priceType = '', $userPrice = 0, $goodsList = []) { $allowTypes = ['fixed', 'quote', 'range', 'custom']; - $type = in_array($priceType, $allowTypes, true) ? $priceType : $service['price_type']; + $type = in_array($priceType, $allowTypes, true) ? $priceType : ($service ? $service['price_type'] : 'fixed'); + + // 支持多商品组合与增项计算 + $base = 0; + if (!empty($goodsList) && is_array($goodsList)) { + foreach ($goodsList as $g) { + $itemPrice = isset($g['base_price']) ? (float)$g['base_price'] : 0; + $itemNum = isset($g['num']) ? max(1, (int)$g['num']) : 1; + $base += $itemPrice * $itemNum; + if (!empty($g['extras']) && is_array($g['extras'])) { + foreach ($g['extras'] as $ex) { + $exPrice = isset($ex['price']) ? (float)$ex['price'] : 0; + $exNum = isset($ex['num']) ? max(0, (int)$ex['num']) : 0; + $base += $exPrice * $exNum; + } + } + } + } elseif ($service) { + $base = (float)$service['base_price']; + } - $base = (float)$service['base_price']; $floorAmount = 0; $urgentAmount = 0; @@ -143,8 +197,8 @@ class Home extends Base $total = $base; } else { $floor = max(0, (int)$params['floor']); - $freeFloor = (int)$service['free_floor']; - $floorFee = (float)$service['floor_fee']; + $freeFloor = $service ? (int)$service['free_floor'] : 2; + $floorFee = $service ? (float)$service['floor_fee'] : 10.0; if ((int)$params['has_elevator'] === 0 && $floor > $freeFloor) { $floorAmount = ($floor - $freeFloor) * $floorFee; @@ -181,8 +235,8 @@ class Home extends Base 'discount_amount' => round($discount, 2), 'pay_amount' => max(0, round($total - $discount, 2)), 'coupon' => $couponInfo, - 'free_floor' => $type === 'custom' ? 0 : (int)$service['free_floor'], - 'floor_fee' => $type === 'custom' ? 0 : round((float)$service['floor_fee'], 2), + 'free_floor' => $type === 'custom' ? 0 : ($service ? (int)$service['free_floor'] : 2), + 'floor_fee' => $type === 'custom' ? 0 : round($service ? (float)$service['floor_fee'] : 10.0, 2), ]; } diff --git a/server/application/api/controller/Order.php b/server/application/api/controller/Order.php index 14a0e98..2cacbef 100644 --- a/server/application/api/controller/Order.php +++ b/server/application/api/controller/Order.php @@ -34,15 +34,31 @@ class Order extends Base $remark = trim((string)$this->request->post('remark')); $goodsImages = $this->request->post('goods_images', ''); - $service = Db::name('service')->where('id', $serviceId)->where('status', 'normal')->find(); - if (!$service) { + // 支持多商品组合 (goods_list) + $goodsListRaw = $this->request->post('goods_list'); + $goodsList = []; + if (!empty($goodsListRaw)) { + $goodsList = is_array($goodsListRaw) ? $goodsListRaw : json_decode((string)$goodsListRaw, true); + } + + $service = null; + if (!empty($goodsList) && is_array($goodsList)) { + $first = reset($goodsList); + $serviceId = isset($first['service_id']) ? (int)$first['service_id'] : $serviceId; + } + + if ($serviceId > 0) { + $service = Db::name('service')->where('id', $serviceId)->where('status', 'normal')->find(); + } + if (!$service && empty($goodsList)) { $this->error('服务不存在或已下架'); } // 下单页可选择:一口价 / 师傅报价 / 用户自主定价(未传则沿用服务默认) - $priceType = trim((string)$this->request->post('price_type', $service['price_type'])); + $defaultPriceType = $service ? $service['price_type'] : 'fixed'; + $priceType = trim((string)$this->request->post('price_type', $defaultPriceType)); if (!in_array($priceType, ['fixed', 'quote', 'custom', 'range'], true)) { - $priceType = $service['price_type']; + $priceType = $defaultPriceType; } $userPrice = round((float)$this->request->post('user_price', 0), 2); if ($priceType === 'custom') { @@ -53,35 +69,105 @@ class Order extends Base $userPrice = 0; } - $address = Db::name('address')->where('id', $addressId)->where('user_id', $this->uid())->find(); + // 支持直接传 address_id 或传入新地址对象并自动保存 + $address = null; + if ($addressId > 0) { + $address = Db::name('address')->where('id', $addressId)->where('user_id', $this->uid())->find(); + } if (!$address) { - $this->error('请选择服务地址'); + $contact = trim((string)$this->request->post('contact')); + $mobile = trim((string)$this->request->post('mobile')); + $province = trim((string)$this->request->post('province', '贵州省')); + $city = trim((string)$this->request->post('city', '贵阳市')); + $district = trim((string)$this->request->post('district', '')); + $detailAddr = trim((string)$this->request->post('address')); + $floor = max(1, (int)$this->request->post('floor', 1)); + $hasElevator = (int)$this->request->post('has_elevator', 1); + + if ($contact && $mobile && $detailAddr) { + $addressId = Db::name('address')->insertGetId([ + 'user_id' => $this->uid(), + 'contact' => $contact, + 'mobile' => $mobile, + 'province' => $province, + 'city' => $city, + 'district' => $district, + 'address' => $detailAddr, + 'floor' => $floor, + 'has_elevator' => $hasElevator, + 'createtime' => time(), + 'updatetime' => time(), + ]); + $address = Db::name('address')->where('id', $addressId)->find(); + } + } + + if (!$address) { + $this->error('请选择或填写完整服务地址'); } if ($appointTime > 0 && $appointTime < time()) { $this->error('预约时间不能早于当前时间'); } + // 多商品组合名称、总件数与图片汇总 + $serviceName = $service ? $service['name'] : '同城综合上门服务'; + $catId = $service ? (int)$service['cat_id'] : 1; + $serviceImage = $service ? $service['image'] : ''; + if (!empty($goodsList) && is_array($goodsList)) { + $names = []; + $totalCount = 0; + $allImages = []; + foreach ($goodsList as $idx => $g) { + $gName = isset($g['item_name']) ? $g['item_name'] : (isset($g['name']) ? $g['name'] : '服务项'); + $gNum = isset($g['num']) ? (int)$g['num'] : 1; + $names[] = $gName . 'x' . $gNum; + $totalCount += $gNum; + if (!empty($g['images']) && is_array($g['images'])) { + $allImages = array_merge($allImages, $g['images']); + } + } + if (!empty($names)) { + $serviceName = implode(' + ', array_slice($names, 0, 3)) . (count($names) > 3 ? ' 等' : ''); + } + if ($totalCount > 0) { + $goodsNum = $totalCount; + } + if (!empty($allImages)) { + $goodsImages = $allImages; + } + } + // 金额统一由 Home::calcAmount 计算,保证与试算口径一致 $amount = Home::calcAmount($service, [ 'floor' => (int)$address['floor'], 'has_elevator' => (int)$address['has_elevator'], 'urgent' => $urgent, - ], $couponId, $this->uid(), $priceType, $userPrice); + ], $couponId, $this->uid(), $priceType, $userPrice, $goodsList); $amount['price_type'] = $priceType; $orderNo = $this->makeOrderNo(); $verifyCode = (string)mt_rand(1000, 9999); + // 如果有多商品明细,结构化存入 goods_images 或在 remark 补充 + $goodsImagesPayload = is_array($goodsImages) ? json_encode($goodsImages, JSON_UNESCAPED_UNICODE) : (string)$goodsImages; + if (!empty($goodsList)) { + $extraPayload = [ + 'goods_list' => $goodsList, + 'images' => is_array($goodsImages) ? $goodsImages : [], + ]; + $goodsImagesPayload = json_encode($extraPayload, JSON_UNESCAPED_UNICODE); + } + $id = Db::name('order')->insertGetId([ 'order_no' => $orderNo, 'user_id' => $this->uid(), 'master_id' => 0, - 'cat_id' => (int)$service['cat_id'], - 'service_id' => (int)$service['id'], - 'service_name' => $service['name'], - 'service_image' => $service['image'], + 'cat_id' => $catId, + 'service_id' => $service ? (int)$service['id'] : 0, + 'service_name' => $serviceName, + 'service_image' => $serviceImage, 'price_type' => $priceType, - 'goods_images' => is_array($goodsImages) ? json_encode($goodsImages, JSON_UNESCAPED_UNICODE) : (string)$goodsImages, + 'goods_images' => $goodsImagesPayload, 'goods_model' => mb_substr($goodsModel, 0, 100), 'goods_num' => $goodsNum, 'floor' => (int)$address['floor'], diff --git a/server/public/nginx.htaccess b/server/public/nginx.htaccess index e69de29..e4fe52e 100644 --- a/server/public/nginx.htaccess +++ b/server/public/nginx.htaccess @@ -0,0 +1,3 @@ +if (!-e $request_filename){ + rewrite ^(.*)$ /index.php?s=$1 last; break; +} diff --git a/yjyf-app/components/GoodsPickerDrawer.vue b/yjyf-app/components/GoodsPickerDrawer.vue new file mode 100644 index 0000000..d5928e3 --- /dev/null +++ b/yjyf-app/components/GoodsPickerDrawer.vue @@ -0,0 +1,303 @@ + + + + + diff --git a/yjyf-app/components/YjyfNavBar.vue b/yjyf-app/components/YjyfNavBar.vue new file mode 100644 index 0000000..cea7611 --- /dev/null +++ b/yjyf-app/components/YjyfNavBar.vue @@ -0,0 +1,391 @@ + + + + + diff --git a/yjyf-app/components/YjyfTabBar.vue b/yjyf-app/components/YjyfTabBar.vue index 9304f11..c812b05 100644 --- a/yjyf-app/components/YjyfTabBar.vue +++ b/yjyf-app/components/YjyfTabBar.vue @@ -3,51 +3,33 @@ - - - - + 首页 - - - - - + 订单 - - - - - - - + + 立即下单 - - - - + 消息 - - - - + 我的 @@ -62,7 +44,7 @@ const props = defineProps({ current: { - type: Number, + type: [Number, String], default: 0 } }) @@ -81,8 +63,9 @@ } function onOrder() { + if (props.current === 'order') return if (!needLogin()) return - uni.navigateTo({ url: '/pages/order/quick' }) + uni.navigateTo({ url: '/pages/category/category' }) } function switchTab(index) { @@ -91,7 +74,12 @@ const targetUrl = TAB_ROUTES[index] if (targetUrl) { - uni.reLaunch({ url: targetUrl }) + uni.switchTab({ + url: targetUrl, + fail: () => { + uni.redirectTo({ url: targetUrl }) + } + }) } } @@ -140,69 +128,13 @@ font-weight: 600; } - /* 底部图标定位容器 */ - .tic { - position: relative; + /* 底部图标 */ + .tab-icon { width: 44rpx; - height: 40rpx; + height: 44rpx; flex-shrink: 0; } - .tic view, .fic view { - position: absolute; - box-sizing: border-box; - } - - .tic .ol { - position: absolute; - box-sizing: border-box; - border: 3rpx solid #9A9A9A; - border-radius: 3rpx; - } - - .tic .ln { - position: absolute; - box-sizing: border-box; - background: #9A9A9A; - } - - .tic .ct { - border-radius: 50%; - } - - .tic .tr { - position: absolute; - box-sizing: border-box; - width: 0; - height: 0; - border-left: 11rpx solid transparent; - border-right: 11rpx solid transparent; - border-bottom: 11rpx solid #9A9A9A; - } - - .tic .tr-d { - position: absolute; - box-sizing: border-box; - width: 0; - height: 0; - border-left: 7rpx solid transparent; - border-right: 7rpx solid transparent; - border-top: 7rpx solid #9A9A9A; - } - - .tab.on .tic .ol { - border-color: #F0710D; - } - .tab.on .tic .ln { - background: #F0710D; - } - .tab.on .tic .tr { - border-bottom-color: #F0710D; - } - .tab.on .tic .tr-d { - border-top-color: #F0710D; - } - /* ---------- 中央上浮主按钮 ---------- */ .tab-center { position: relative; @@ -226,34 +158,17 @@ box-shadow: 0 10rpx 26rpx rgba(238, 116, 18, 0.45); } - .fic { - position: relative; + .fab.active { + box-shadow: 0 10rpx 32rpx rgba(238, 116, 18, 0.65), 0 0 0 6rpx rgba(255, 182, 94, 0.35); + transform: scale(1.03); + } + + .fab-icon { width: 44rpx; - height: 36rpx; + height: 44rpx; flex-shrink: 0; } - .fo { - position: absolute; - box-sizing: border-box; - border: 3rpx solid #ffffff; - border-radius: 5rpx; - } - - .fl { - position: absolute; - box-sizing: border-box; - background: #ffffff; - } - - .fp { - position: absolute; - box-sizing: border-box; - background: #ffffff; - border-radius: 3rpx; - transform: rotate(-45deg); - } - .fab-txt { margin-top: 4rpx; font-size: 22rpx; diff --git a/yjyf-app/config/config.js b/yjyf-app/config/config.js new file mode 100644 index 0000000..f85437f --- /dev/null +++ b/yjyf-app/config/config.js @@ -0,0 +1,14 @@ +const config = { + dev: 'http://192.168.2.158:9560/', + prod: 'https://yjyf.gzyakj.com.cn/' +}; + +// HBuilderX 点击【运行】为 development(dev),点击【发行】为 production(prod) +const env = process.env.NODE_ENV === 'development' ? 'dev' : 'prod'; + +export const baseUrl = config[env]; + +export default { + ...config, + baseUrl: config[env] +}; diff --git a/yjyf-app/config/index.js b/yjyf-app/config/index.js new file mode 100644 index 0000000..0f7b3c9 --- /dev/null +++ b/yjyf-app/config/index.js @@ -0,0 +1,2 @@ +export * from './config.js'; +export { default } from './config.js'; diff --git a/yjyf-app/pages.json b/yjyf-app/pages.json index 3b28879..a797a8c 100644 --- a/yjyf-app/pages.json +++ b/yjyf-app/pages.json @@ -28,38 +28,39 @@ }, { "path": "pages/order/quick", - "style": { - "navigationBarTitleText": "立即下单", - "navigationBarBackgroundColor": "#F0710D", - "navigationBarTextStyle": "white" - } + "style": { "navigationStyle": "custom", "backgroundColor": "#F6F7F9" } + }, + { + "path": "pages/category/category", + "style": { "navigationStyle": "custom", "backgroundColor": "#F6F7F9" } }, { "path": "pages/order/list", - "style": { "navigationBarTitleText": "我的订单" } + "style": { "navigationStyle": "custom", "backgroundColor": "#F5F6F8" } }, { "path": "pages/order/detail", - "style": { "navigationBarTitleText": "订单详情" } + "style": { "navigationStyle": "custom", "backgroundColor": "#F5F6F8" } }, { "path": "pages/order/review", - "style": { "navigationBarTitleText": "服务评价" } + "style": { "navigationStyle": "custom", "backgroundColor": "#F5F6F8" } }, { "path": "pages/address/list", - "style": { "navigationBarTitleText": "服务地址" } + "style": { "navigationStyle": "custom", "backgroundColor": "#F5F6F8" } }, { "path": "pages/coupon/coupon", - "style": { "navigationBarTitleText": "优惠券" } + "style": { "navigationStyle": "custom", "backgroundColor": "#F5F6F8" } }, { "path": "pages/message/message", "style": { - "navigationBarTitleText": "消息中心", + "navigationStyle": "custom", "enablePullDownRefresh": true, - "backgroundTextStyle": "dark" + "backgroundTextStyle": "dark", + "backgroundColor": "#F5F6F8" } }, { diff --git a/yjyf-app/pages/address/list.vue b/yjyf-app/pages/address/list.vue index ff9fa9a..2be66ef 100644 --- a/yjyf-app/pages/address/list.vue +++ b/yjyf-app/pages/address/list.vue @@ -1,5 +1,7 @@