feat(merchant): 企业订单聚合列表与关联账号只读详情

- 新增 /merchant/orders,按主账号+关联账号聚合订单并支持状态筛选
- 新增企业订单页,展示订单归属账号(主账号/关联账号)
- 工作台企业订单入口改到聚合列表,近期订单可点击查看
- 订单详情允许同商家账号查看,非下单账号隐藏操作按钮
- 报价选择仅下单账号可操作,防止关联账号/主账号误操作
这个提交包含在:
linda
2026-10-28 19:27:51 +08:00
父节点 c6c2583dfc
当前提交 fbc6feee87
共有 7 个文件被更改,包括 213 次插入 和 7 次删除

查看文件

@@ -98,6 +98,10 @@
"path": "pages/merchant/workbench",
"style": { "navigationBarTitleText": "商家工作台" }
},
{
"path": "pages/merchant/orders",
"style": { "navigationBarTitleText": "企业订单" }
},
{
"path": "pages/merchant/batch",
"style": { "navigationBarTitleText": "批量下单" }

106
pages/merchant/orders.vue 普通文件
查看文件

@@ -0,0 +1,106 @@
<template>
<view class="page">
<view class="tabs">
<view v-for="t in tabs" :key="t.key" class="tab" :class="{ on: cur === t.key }" @tap="switchTab(t.key)">
<text class="tab-txt">{{ t.name }}</text>
</view>
</view>
<view class="list">
<view class="item" v-for="o in list" :key="o.id" @tap="goDetail(o.id)">
<view class="hd">
<text class="no">{{ o.order_no }}</text>
<text class="status" :class="'s' + o.status">{{ o.status_text }}</text>
</view>
<view class="bd">
<view class="thumb"><text class="thumb-txt">{{ (o.service_name || '').slice(0, 2) }}</text></view>
<view class="mi">
<text class="sname">{{ o.service_name }}</text>
<text class="saddr">{{ o.address_text || o.address }}</text>
<text class="stime">预约 {{ o.appoint_text }}{{ Number(o.urgent) ? ' · 急单' : '' }}</text>
</view>
<text class="amt">¥{{ o.pay_amount }}</text>
</view>
<view class="ft">
<text class="acc" :class="{ owner: o.account_role === 'owner' }">{{ o.account_role_text }} · {{ o.account_name }}</text>
<text class="time">{{ o.create_text }}</text>
</view>
</view>
</view>
<view class="empty" v-if="!loading && !list.length"><text>暂无企业订单</text></view>
<view class="tip" v-if="list.length"><text>共 {{ total }} 单,仅展示前 {{ list.length }} 条</text></view>
</view>
</template>
<script setup>
import { ref } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import api from '@/utils/api.js';
import { isLogin, toLogin } from '@/utils/request.js';
const tabs = [
{ key: 'all', name: '全部' },
{ key: 'active', name: '进行中' },
{ key: 'completed', name: '已完成' },
{ key: 'cancelled', name: '已取消' }
];
const cur = ref('all');
const list = ref([]);
const total = ref(0);
const loading = ref(false);
async function load() {
if (!isLogin()) { toLogin(); return; }
loading.value = true;
try {
const res = await api.merchantOrders({ status: cur.value, limit: 50 });
list.value = res.list || [];
total.value = res.total || list.value.length;
} catch (e) {
} finally {
loading.value = false;
}
}
function switchTab(k) {
cur.value = k;
load();
}
function goDetail(id) {
uni.navigateTo({ url: '/pages/order/detail?id=' + id });
}
onShow(() => load());
</script>
<style scoped>
.page { min-height: 100vh; background: #F5F6F8; padding-bottom: 40rpx; }
.tabs { display: flex; background: #fff; padding: 0 12rpx; box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.03); }
.tab { flex: 1; text-align: center; padding: 26rpx 0; }
.tab-txt { font-size: 28rpx; color: #666; }
.tab.on .tab-txt { color: #F0710D; font-weight: 700; }
.list { padding: 20rpx 28rpx; }
.item { background: #fff; border-radius: 20rpx; margin-bottom: 20rpx; padding: 22rpx 24rpx; box-shadow: 0 6rpx 20rpx rgba(240,129,31,0.06); }
.hd { display: flex; justify-content: space-between; align-items: center; padding-bottom: 16rpx; border-bottom: 1rpx solid #F4F4F4; }
.no { font-size: 22rpx; color: #9A9A9A; }
.status { font-size: 24rpx; font-weight: 600; color: #F0710D; }
.status.s8 { color: #2FA84F; }
.status.s9 { color: #9A9A9A; }
.status.s7 { color: #E8331B; }
.bd { display: flex; align-items: center; padding: 20rpx 0; }
.thumb { width: 92rpx; height: 92rpx; border-radius: 16rpx; flex-shrink: 0; background: linear-gradient(160deg,#FFF7F0,#FCE9D6); display: flex; align-items: center; justify-content: center; }
.thumb-txt { font-size: 26rpx; font-weight: 700; color: #B85E2A; }
.mi { flex: 1; min-width: 0; margin-left: 18rpx; display: flex; flex-direction: column; }
.sname { font-size: 28rpx; font-weight: 600; color: #1F1F1F; }
.saddr { margin-top: 6rpx; font-size: 22rpx; color: #9A9A9A; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.stime { margin-top: 6rpx; font-size: 21rpx; color: #B5B5B5; }
.amt { flex-shrink: 0; margin-left: 14rpx; font-size: 32rpx; font-weight: 700; color: #F0710D; }
.ft { display: flex; align-items: center; justify-content: space-between; padding-top: 14rpx; border-top: 1rpx solid #F4F4F4; }
.acc { padding: 0 14rpx; line-height: 36rpx; border-radius: 8rpx; background: #F0EDFF; color: #5B3FD8; font-size: 20rpx; }
.acc.owner { background: #FFF1E0; color: #E07A2E; }
.time { font-size: 20rpx; color: #B5B5B5; }
.empty { padding: 100rpx 0 30rpx; text-align: center; font-size: 24rpx; color: #9A9A9A; }
.tip { padding: 10rpx 0 30rpx; text-align: center; font-size: 20rpx; color: #B5B5B5; }
</style>

查看文件

@@ -55,7 +55,7 @@
<view class="card">
<view class="card-head"><text class="card-title">近期订单</text><text class="card-more" @tap="go('/pages/order/list')">全部 ›</text></view>
<view class="o-row" v-for="o in orders" :key="o.id">
<view class="o-row" v-for="o in orders" :key="o.id" @tap="goOrder(o.id)">
<view class="o-l">
<text class="o-name">{{ o.service_name }}</text>
<text class="o-no">{{ o.order_no }} · {{ o.time_text }}</text>
@@ -106,7 +106,7 @@
const entries = [
{ key: 'batch', name: '批量下单', icon: '批', bg: '#FFF1E0', color: '#E07A2E', action: () => go('/pages/merchant/batch') },
{ key: 'orders', name: '企业订单', icon: '单', bg: '#EAF2FF', color: '#4A7CFF', action: () => go('/pages/order/list') },
{ key: 'orders', name: '企业订单', icon: '单', bg: '#EAF2FF', color: '#4A7CFF', action: () => go('/pages/merchant/orders') },
{ key: 'invoice', name: '开具发票', icon: '票', bg: '#E8F7EE', color: '#2FA84F', action: () => go('/pages/merchant/invoice') },
{ key: 'profile', name: '企业资料', icon: '档', bg: '#F0EBFF', color: '#7A5AF8', action: () => go('/pages/merchant/apply') },
{ key: 'address', name: '服务地址', icon: '址', bg: '#FFF0F0', color: '#E8331B', action: () => go('/pages/address/list') },
@@ -114,6 +114,7 @@
];
function go(url) { uni.navigateTo({ url: url }); }
function goOrder(id) { uni.navigateTo({ url: '/pages/order/detail?id=' + id }); }
async function load() {
if (!isLogin()) { toLogin(); return; }

查看文件

@@ -3,6 +3,7 @@
<view class="hero">
<text class="hero-status">{{ order.status_text }}</text>
<text class="hero-tip">{{ statusTip }}</text>
<text class="hero-tip" v-if="order.is_order_owner === false">商家账号查看 · 仅查看,操作请由下单账号完成</text>
</view>
<view class="card" v-if="master">
@@ -86,10 +87,10 @@
</view>
<view class="bar">
<view class="op" v-if="order.can_cancel" @tap="cancel">取消订单</view>
<view class="op primary" v-if="order.can_pay" @tap="pay">去支付</view>
<view class="op primary" v-if="order.can_verify" @tap="verify">确认验收</view>
<view class="op primary" v-if="order.can_review" @tap="review">去评价</view>
<view class="op" v-if="order.is_order_owner !== false && order.can_cancel" @tap="cancel">取消订单</view>
<view class="op primary" v-if="order.is_order_owner !== false && order.can_pay" @tap="pay">去支付</view>
<view class="op primary" v-if="order.is_order_owner !== false && order.can_verify" @tap="verify">确认验收</view>
<view class="op primary" v-if="order.is_order_owner !== false && order.can_review" @tap="review">去评价</view>
<view class="op" @tap="backHome">返回首页</view>
</view>
</view>
@@ -115,6 +116,7 @@
const canChoose = computed(() => {
return !!(order.value && order.value.price_type === 'quote'
&& Number(order.value.status) === 2 && Number(order.value.master_id) === 0
&& order.value.is_order_owner !== false
&& quotes.value.some(q => q.status === 'wait'));
});

查看文件

@@ -171,6 +171,58 @@ class Merchant extends Base
$this->success('', ['months' => $months, 'orders' => $orders]);
}
/**
* 企业订单:主账号 + 关联账号的全部订单
*/
public function orders()
{
$merchant = $this->requireMerchant();
$uids = $this->merchantUserIds($merchant);
$status = $this->request->param('status', 'all');
$page = max(1, (int)$this->request->param('page', 1));
$limit = min(50, max(1, (int)$this->request->param('limit', 20)));
$build = function () use ($uids, $status) {
$q = Db::name('order')->where('user_id', 'in', $uids);
if ($status === 'active') {
$q->where('status', 'in', \app\common\library\OrderStatus::activeValues());
} elseif ($status === 'completed') {
$q->where('status', \app\common\library\OrderStatus::COMPLETED);
} elseif ($status === 'cancelled') {
$q->where('status', \app\common\library\OrderStatus::CANCELLED);
} elseif ($status !== 'all' && $status !== '') {
$q->where('status', (int)$status);
}
return $q;
};
$total = $build()->count();
$rows = $build()->order('id desc')->page($page, $limit)->select();
$userMap = Db::name('user')->where('id', 'in', $uids)->column('id,nickname,mobile', 'id');
foreach ($rows as &$row) {
$ownerUser = (int)$row['user_id'] === (int)$merchant['user_id'];
$u = isset($userMap[(int)$row['user_id']]) ? $userMap[(int)$row['user_id']] : null;
$mobile = $u ? (string)$u['mobile'] : '';
$row['pay_amount'] = (float)$row['pay_amount'];
$row['urgent'] = (int)$row['urgent'];
$row['status_text'] = \app\common\library\OrderStatus::label((int)$row['status']);
$row['appoint_text'] = $row['appoint_time'] ? date('m-d H:i', (int)$row['appoint_time']) : '尽快上门';
$row['create_text'] = date('Y-m-d H:i', (int)$row['createtime']);
$row['address_text'] = $row['province'] . $row['city'] . $row['district'] . $row['address'];
$row['account_name'] = $u && $u['nickname'] ? $u['nickname'] : ('用户' . substr($mobile, -4));
$row['account_role'] = $ownerUser ? 'owner' : 'member';
$row['account_role_text'] = $ownerUser ? '主账号' : '关联账号';
}
unset($row);
$this->success('', [
'list' => $rows,
'total' => (int)$total,
'page' => $page,
]);
}
/**
* 开票记录
*/

查看文件

@@ -216,7 +216,20 @@ class Order extends Base
public function detail()
{
$orderId = (int)$this->request->param('id');
$order = $this->ownOrder($orderId, 'user');
if ($orderId <= 0) {
$this->error('订单参数错误');
}
$order = Db::name('order')->where('id', $orderId)->find();
if (!$order) {
$this->error('订单不存在');
}
// 本人订单,或同一商家(主账号 + 关联账号)的订单
if ((int)$order['user_id'] !== $this->uid()) {
$uids = $this->currentMerchantUserIds();
if (!in_array((int)$order['user_id'], $uids, true)) {
$this->error('无权操作该订单', null, 403);
}
}
$logs = Db::name('order_log')->where('order_id', $orderId)->order('id asc')->select();
foreach ($logs as &$log) {
@@ -531,9 +544,36 @@ class Order extends Base
'create_text' => date('Y-m-d H:i', (int)$row['createtime']),
'urgent' => (int)$row['urgent'],
'price_type_text' => $this->priceTypeText($row['price_type']),
'is_order_owner' => (int)$row['user_id'] === $this->uid(),
]);
}
/**
* 当前用户所属商家的全部账号(主账号 + 关联账号)
*/
protected function currentMerchantUserIds()
{
$merchant = Db::name('merchant')->where('user_id', $this->uid())->find();
if (!$merchant) {
$bind = Db::name('merchant_member')->where('user_id', $this->uid())->find();
if ($bind) {
$merchant = Db::name('merchant')->where('id', (int)$bind['merchant_id'])->find();
}
}
if (!$merchant) {
return [];
}
$ids = [(int)$merchant['user_id']];
$rows = Db::name('merchant_member')->where('merchant_id', (int)$merchant['id'])->column('user_id');
foreach ($rows as $id) {
$id = (int)$id;
if ($id > 0 && !in_array($id, $ids, true)) {
$ids[] = $id;
}
}
return $ids;
}
/**
* 计价方式文案(前后端与后台统一口径)
*/

查看文件

@@ -74,6 +74,7 @@ export default {
merchantInfo: () => get('/merchant/info'),
merchantUpdate: (data) => post('/merchant/update', data),
merchantBills: () => get('/merchant/bills'),
merchantOrders: (data) => get('/merchant/orders', data),
merchantInvoices: () => get('/merchant/invoices'),
merchantInvoice: (data) => post('/merchant/invoice', data),
merchantMembers: () => get('/merchant/members'),