fix(发票): 统一普票/专票口径,详情支持申请开票与查看发票,列表状态对齐中台

专用发票默认关闭并受后台开关控制;旧订单详情接口改走中台。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
吴昊天 2026-10-27 13:09:31 +08:00
parent ab7e327f7b
commit 2463150b5f
20 changed files with 908 additions and 317 deletions

View File

@ -497,7 +497,7 @@ GET /api/center/order/detail/{orderNo}
"invoice": { // 发票信息(order_center.invoice 为空时从 eb_store_order_invoice 查询)
"title": "公司名称", // 发票抬头名称
"header_type": 2, // 1=个人 2=企业
"type": 1, // 1=电子发票 2=纸质发票
"type": 1, // 1=普通发票 2=专用发票
"tax_number": "91110000MA00XXXXXX", // 税号(企业抬头才有)
"email": "test@example.com", // 接收邮箱
"phone": "13800138000", // 联系电话

View File

@ -140,7 +140,7 @@ class CenterOrderController
})
);
}
// 送礼订单按查看者裁剪:购买者隐藏收货信息/物流/相关按钮,领取人保留只读物流能力
// 送礼订单按查看者裁剪:送礼人隐藏收货/物流/评价/售后,领取人保留收货物流与评价
$result = $services->applyGiftViewerPolicy($result, $uid);
return app('json')->success($result);
} catch (\Throwable $e) {
@ -282,6 +282,9 @@ class CenterOrderController
'door_service_postage' => $doorServicePostage, // 上门服务费用(元)
'remark' => true, // 买家留言,与配送方式无关
'invoice' => (bool)sys_config('invoice_func_status', 0), // 发票,与商品类型无关
// 专用发票:须发票功能开启且专票开关开启
'special_invoice' => (bool)sys_config('invoice_func_status', 0)
&& (bool)sys_config('special_invoice_status', 0),
'integral' => !$isActivityOrder && $integralOpen, // 积分抵扣,与商品类型无关
'coupon' => !$isActivityOrder && !empty($couponList), // 优惠券,与商品类型无关
'custom_form' => !empty($formattedCustomForm), // 自定义表单,与商品类型无关
@ -557,6 +560,9 @@ class CenterOrderController
'door_service_postage' => $doorServicePostage, // 上门服务费用(元)
'remark' => true, // 买家留言
'invoice' => (bool)sys_config('invoice_func_status', 0),// 发票
// 专用发票:须发票功能开启且专票开关开启
'special_invoice' => (bool)sys_config('invoice_func_status', 0)
&& (bool)sys_config('special_invoice_status', 0),
'integral' => $integralOpen, // 积分抵扣
'coupon' => !empty($couponList), // 优惠券
'custom_form' => !empty($cartCustomForm), // 自定义表单(取主商品定义)

View File

@ -86,29 +86,99 @@ class StoreOrderInvoiceController
if ((int)($order['uid'] ?? 0) !== $uid) {
return app('json')->fail('订单不存在');
}
// 本人开票记录
// 本人开票记录(申请表为真源);无记录时回退下单快照
$orderInvoice = $this->services->getOne(['order_id' => (int)$order['id'], 'uid' => $uid]);
$order['invoice'] = $orderInvoice ? $orderInvoice->toArray() : [];
if ($orderInvoice) {
$invoice = $orderInvoice->toArray();
} else {
$invoice = is_array($order['invoice'] ?? null) ? $order['invoice'] : [];
}
// 兼容旧页字段:备注列名、时间展示
if ($invoice) {
if (!isset($invoice['remark']) && isset($invoice['invoice_reamrk'])) {
$invoice['remark'] = $invoice['invoice_reamrk'];
}
if (isset($invoice['add_time']) && is_numeric($invoice['add_time'])) {
$invoice['add_time'] = date('Y-m-d H:i:s', (int)$invoice['add_time']);
}
if (isset($invoice['invoice_time']) && is_numeric($invoice['invoice_time']) && (int)$invoice['invoice_time'] > 0) {
$invoice['invoice_time'] = date('Y-m-d H:i:s', (int)$invoice['invoice_time']);
}
}
$order['invoice'] = $invoice ?: [];
// 以下映射旧版发票详情页所需的订单字段(旧 eb_store_order 结构已由订单中台替代)
$order['order_id'] = $order['order_no'] ?? $uni;
$order['add_time_y'] = date('Y-m-d', (int)($order['add_time'] ?? time()));
$order['add_time_h'] = date('H:i:s', (int)($order['add_time'] ?? time()));
$order['paid'] = (int)($order['pay_status'] ?? 0) == 1;
$addTime = (int)($order['add_time'] ?? time());
$order['add_time_y'] = date('Y-m-d', $addTime);
$order['add_time_h'] = date('H:i:s', $addTime);
$payStatus = (int)($order['pay_status'] ?? 0);
$order['paid'] = in_array($payStatus, [1, 2], true) ? 1 : 0;
$legacyStatus = $this->services->statusToLegacy((string)($order['status_code'] ?? ''));
$order['status'] = $legacyStatus;
$order['_status'] = [
'_type' => $this->services->statusToLegacy($order['status_code'] ?? ''),
'_payType' => $order['pay_type_name'] ?? ($order['pay_type'] ?? ''),
'_type' => $legacyStatus,
'_payType' => (string)($order['pay_type_name'] ?? ($order['pay_type'] ?? '')),
];
// 收货人信息优先取订单配送快照
$contact = is_array($order['contact'] ?? null) ? $order['contact'] : [];
$order['real_name'] = $contact['name'] ?? '';
$order['user_phone'] = $contact['phone'] ?? '';
// 物流信息
$logistics = $order['extensions']['logistics'] ?? [];
$order['delivery_type'] = ($order['delivery_type'] ?? '') ?: 'express';
$order['delivery_name'] = $logistics['delivery_name'] ?? '';
$order['delivery_id'] = $logistics['delivery_id'] ?? '';
// 商品快照合成旧版 cartInfo,供订单商品组件展示
$order['cartInfo'] = $this->services->buildLegacyCartInfo(is_array($order['extra_info'] ?? null) ? $order['extra_info'] : []);
// 收货人:顶层 contact → extra_info.contact → delivery.contact
$extra = is_array($order['extra_info'] ?? null) ? $order['extra_info'] : [];
$contact = [];
if (is_array($order['contact'] ?? null) && $order['contact']) {
$contact = $order['contact'];
} elseif (is_array($extra['contact'] ?? null)) {
$contact = $extra['contact'];
} elseif (is_array($order['delivery']['contact'] ?? null)) {
$contact = $order['delivery']['contact'];
}
$order['real_name'] = trim((string)($contact['name'] ?? ($order['real_name'] ?? '')));
if ($order['real_name'] === '') {
$order['real_name'] = trim((string)($order['nickname'] ?? ''));
}
$order['user_phone'] = trim((string)($contact['phone'] ?? ($order['user_phone'] ?? '')));
// 收货地址
if (empty($order['user_address'])) {
$addr = $contact['address'] ?? ($order['delivery']['user_address'] ?? '');
if (is_array($addr)) {
$order['user_address'] = implode('', array_filter([
$addr['province'] ?? '',
$addr['city'] ?? '',
$addr['district'] ?? '',
$addr['detail'] ?? '',
]));
} else {
$order['user_address'] = trim((string)$addr);
}
}
// 物流:顶层 → extensions → extra_info.delivery
$logistics = is_array($order['extensions']['logistics'] ?? null) ? $order['extensions']['logistics'] : [];
$deliveryExtra = is_array($extra['delivery'] ?? null) ? $extra['delivery'] : [];
if (empty($order['delivery_name'])) {
$order['delivery_name'] = (string)($logistics['delivery_name'] ?? ($deliveryExtra['delivery_name'] ?? ''));
}
if (empty($order['delivery_id'])) {
$order['delivery_id'] = (string)($logistics['delivery_id'] ?? ($deliveryExtra['delivery_id'] ?? ''));
}
if (empty($order['delivery_type'])) {
$shippingType = (int)($order['shipping_type'] ?? 0);
$order['delivery_type'] = (string)($logistics['delivery_type']
?? ($deliveryExtra['delivery_type'] ?? ($extra['delivery_type'] ?? '')));
if ($order['delivery_type'] === '') {
$order['delivery_type'] = $shippingType === 2 ? 'pickup' : ($shippingType === 3 ? 'door' : 'express');
}
}
// 商品快照:extra_info.items 优先,缺省回退 order_items
if (empty($extra['items']) && !empty($order['order_items']) && is_array($order['order_items'])) {
$extra['items'] = $order['order_items'];
}
$order['cartInfo'] = $this->services->buildLegacyCartInfo($extra);
$order['mark'] = (string)($order['mark'] ?? ($extra['mark'] ?? ''));
$order['use_integral'] = (int)($order['use_integral'] ?? ($order['use_integral_num'] ?? 0));
$order['system_store'] = is_array($order['store'] ?? null)
? $order['store']
: (is_array($order['system_store'] ?? null) ? $order['system_store'] : []);
$order['mapKey'] = sys_config('tengxun_map_key');
/** @var PayChannelServices $payChannelServices */
$payChannelServices = app()->make(PayChannelServices::class);

View File

@ -22,6 +22,7 @@ use crmeb\services\invoice\Invoice;
use app\dao\order\OrderInvoiceDao;
use app\services\user\UserInvoiceServices;
use app\services\user\UserServices;
use crmeb\trade\order\type\OrderTypeRegistry;
use think\facade\Log;
@ -78,10 +79,12 @@ class OrderInvoiceServices extends BaseService
$field = 'id as invoice_id,order_id,header_type,type,name,duty_number,drawer_phone,email,tell,address,bank,card_number,is_invoice,invoice_number,remark as invoice_reamrk,invoice_time,add_time as invoice_add_time,unique_num,invoice_num,invoice_type,invoice_serial_number,red_invoice_num';
$where['is_pay'] = 1;
$where['is_del'] = 0;
// 订单中台关联查询:仅选择中台表实际存在的字段
// 订单中台关联查询:带 source_type/source_id,便于实时推导待发货/待收货
$list = $this->dao->getList($where, $field, ['order' => function ($query) {
$query->field('id,order_no,uid,pay_price,status_code,refund_status,extra_info,add_time');
$query->field('id,order_no,uid,pay_price,status_code,refund_status,extra_info,add_time,source_type,source_id');
}], 'add_time desc', $page, $list);
// 按源订单类型批量解析业务子状态(extra_info 快照会被 syncStatus 覆盖成 processing,不能直接用)
$businessStatusMap = $this->resolveBusinessStatusMap($list);
// 批量取下单用户,作为收货人姓名电话的兜底数据
$uids = array_filter(array_unique(array_column(array_column($list, 'order'), 'uid')));
$userList = $uids ? app()->make(UserServices::class)->getColumn([['uid', 'in', $uids]], 'uid,real_name,nickname,phone', 'uid') : [];
@ -100,29 +103,98 @@ class OrderInvoiceServices extends BaseService
$contactPhone = trim((string)($contact['phone'] ?? ''));
$item['real_name'] = $contactName !== '' ? $contactName : trim((string)($user['real_name'] ?? ($user['nickname'] ?? '')));
$item['user_phone'] = $contactPhone !== '' ? $contactPhone : trim((string)($user['phone'] ?? ''));
$item['status'] = $this->statusToLegacy($order['status_code'] ?? '');
$statusCode = (string)($order['status_code'] ?? '');
$item['status_code'] = $statusCode;
$centerId = (int)($order['id'] ?? 0);
$businessStatus = $businessStatusMap[$centerId] ?? null;
if (is_array($businessStatus)) {
if (empty($businessStatus['label']) && !empty($businessStatus['text'])) {
$businessStatus['label'] = $businessStatus['text'];
}
$item['business_status'] = $businessStatus;
// 列表唯一展示文案:优先业务子状态(待收货),否则中台统一状态
$item['status_text'] = (string)($businessStatus['label'] ?: ($businessStatus['text'] ?? ''));
} else {
$item['business_status'] = null;
$item['status_text'] = '';
}
if ($item['status_text'] === '') {
// 管理端与订单中台列表一致:processing 展示「待处理」
$item['status_text'] = $statusCode === OrderCenter::STATUS_PROCESSING
? '待处理'
: (OrderCenter::$statusMap[$statusCode] ?? ($statusCode !== '' ? $statusCode : '--'));
}
// 兼容旧前端/移动端数字状态
$item['status'] = $this->statusToLegacy($statusCode);
$item['refund_status'] = $order['refund_status'] ?? 0;
$item['add_time'] = date('Y-m-d H:i:s', $order['add_time'] ?? $item['invoice_add_time'] ?? time());
$item['invoice_add_time'] = date('Y-m-d H:i:s', $item['invoice_add_time']);
// 中台关联原始数据不再下发给前端
unset($item['order']);
}
unset($item);
$count = $this->dao->count($where);
return compact('list', 'count');
}
/**
* 订单中台状态码映射为旧版订单状态数值(兼容发票模块前端展示)
* 批量从源订单适配器推导业务子状态
* @param array $list 含 order 关联的发票列表
* @return array<int, array|null> order_center.id => business_status
*/
protected function resolveBusinessStatusMap(array $list): array
{
$grouped = [];
$centerIndex = [];
foreach ($list as $row) {
$order = $row['order'] ?? [];
$centerId = (int)($order['id'] ?? 0);
$sourceType = (string)($order['source_type'] ?? '');
$sourceId = (int)($order['source_id'] ?? 0);
if ($centerId <= 0 || $sourceType === '' || $sourceId <= 0) {
continue;
}
$grouped[$sourceType][$sourceId] = true;
$centerIndex[$sourceType][$sourceId][] = $centerId;
}
$result = [];
foreach ($grouped as $sourceType => $idMap) {
try {
$adapter = OrderTypeRegistry::getAdapter($sourceType);
$modelClass = $adapter::getModelClass();
/** @var \crmeb\basic\BaseModel $model */
$model = app()->make($modelClass);
$sourceRows = $model->whereIn('id', array_keys($idMap))->select()->toArray();
foreach ($sourceRows as $sourceOrder) {
$sourceId = (int)($sourceOrder['id'] ?? 0);
$businessStatus = $adapter->resolveBusinessStatus($sourceOrder);
foreach ($centerIndex[$sourceType][$sourceId] ?? [] as $centerId) {
$result[$centerId] = $businessStatus;
}
}
} catch (\Throwable $e) {
Log::warning('发票列表推导订单业务状态失败: ' . $e->getMessage(), ['source_type' => $sourceType]);
}
}
return $result;
}
/**
* 订单中台状态码映射为旧版订单状态数值(兼容移动端发票列表 _type)
* @param string $statusCode
* @return int
*/
public function statusToLegacy(string $statusCode): int
{
return match ($statusCode) {
OrderCenter::STATUS_PENDING_PAYMENT => 0,
OrderCenter::STATUS_PROCESSING => 0,
OrderCenter::STATUS_PENDING_REVIEW => 2,
OrderCenter::STATUS_COMPLETED => 3,
OrderCenter::STATUS_CANCELED => -1,
OrderCenter::STATUS_AFTER_SALE, OrderCenter::STATUS_REFUNDING => -3,
OrderCenter::STATUS_REFUNDED => -2,
OrderCenter::STATUS_DELETED => -4,
default => 0,
};
}
@ -145,11 +217,13 @@ class OrderInvoiceServices extends BaseService
$extra = $order['extra_info'] ?? '';
$extra = is_string($extra) ? (json_decode($extra, true) ?: []) : ($extra ?: []);
$order['cartInfo'] = $this->buildLegacyCartInfo($extra);
$order['pay_price'] = $order['pay_price'] ?? '0.00';
// 移动端跳转开票详情携带的订单号对应中台 order_no
$order['order_id'] = $order['order_no'] ?? '';
unset($order['extra_info']);
$item['order'] = $order;
}
unset($item);
return $list;
}

View File

@ -26,7 +26,7 @@ INSERT INTO `eb_page_link` (`id`, `cate_id`, `type`, `name`, `url`, `param`, `ex
(37, 20, 1, '待评价订单', '/pages/goods/order_list/index?status=3', '', '', 1, 0, 1772520919),
(38, 20, 1, '联系客服', '/pages/extension/customer_list/chat', '', '', 1, 0, 1772520919),
(39, 20, 1, '我的收藏', '/pages/users/user_goods_collection/index', '', '', 1, 0, 1772520919),
(40, 20, 1, '发票管理', '/pages/users/user_invoice_list/index', '', '', 1, 0, 1772520919),
(40, 20, 1, '发票管理', '/pages/users/user_invoice_list/index', '', '', 1, 2, 1772520919),
(41, 20, 1, '每日签到', '/pages/users/user_sgin/index', '', '', 1, 0, 1772520919),
(42, 20, 1, '客服接待', '/kefu/mobile_list', '', '', 1, 0, 1772520919),
(54, 20, 1, '订单核销', '/pages/admin/order_cancellation/index', '', '', 1, 0, 1772520919);
(54, 20, 1, '订单核销', '/pages/admin/order_cancellation/index', '', '', 1, 1, 1772520919);

View File

@ -83,11 +83,7 @@
</el-table-column>
<el-table-column label="订单状态" min-width="130">
<template #default="scope">
<div v-if="scope.row.status === 0">未发货</div>
<div v-else-if="scope.row.status === 1">待收货</div>
<div v-else-if="scope.row.status === 2">待评价</div>
<div v-else-if="scope.row.status === 3">已完成</div>
<div v-else-if="scope.row.status === -2">已退款</div>
<el-tag :type="getOrderStatusClass(scope.row)">{{ getOrderStatusLabel(scope.row) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" fixed="right" width="300">
@ -131,8 +127,8 @@
>
<el-divider v-if="scope.row.is_invoice !== 1 && scope.row.refund_status == 0" direction="vertical" />
</template>
<a v-if="scope.row.status != -2" v-db-click @click="edit(scope.row)">操作</a>
<el-divider v-if="scope.row.status != -2" direction="vertical" />
<a v-if="!isRefundedOrder(scope.row)" v-db-click @click="edit(scope.row)">操作</a>
<el-divider v-if="!isRefundedOrder(scope.row)" direction="vertical" />
<a v-db-click @click="orderInfo(scope.row.id)">订单信息</a>
</template>
</el-table-column>
@ -332,6 +328,77 @@ const orderData = ref({
const orderId = ref(0);
const invoiceId = ref(0);
/** 中台统一状态码(有业务子状态时不叠展示「待处理」等) */
const CENTER_STATUS_CODES = [
'pending_payment',
'processing',
'pending_review',
'completed',
'canceled',
'after_sale',
'refunding',
'refunded',
'deleted',
];
/** 与订单中台列表 statusCodeOptions 同口径 */
const ORDER_STATUS_OPTIONS = [
{ key: 'pending_payment', label: '待支付' },
{ key: 'processing', label: '待处理' },
{ key: 'pending_review', label: '待评价' },
{ key: 'completed', label: '已完成' },
{ key: 'canceled', label: '已取消' },
{ key: 'after_sale', label: '售后中' },
{ key: 'refunding', label: '退款中' },
{ key: 'refunded', label: '已退款' },
{ key: 'deleted', label: '已删除' },
];
function getBusinessStatusText(row) {
const bs = row?.business_status;
if (!bs || typeof bs !== 'object') return '';
const text = String(bs.label || bs.text || '').trim();
if (!text) return '';
const code = String(bs.code || '');
const genericTexts = ORDER_STATUS_OPTIONS.map((o) => o.label).concat(['进行中']);
// 待发货/待收货等业务文案优先;中台统一码快照(待处理/进行中)忽略
if (!genericTexts.includes(text)) return text;
if (code && !CENTER_STATUS_CODES.includes(code)) return text;
return '';
}
function getOrderStatusLabel(row) {
if (row && Number(row.is_del) === 1) return '已删除';
// 后端已按源订单实时推导 status_text(待收货/待发货优先)
if (row?.status_text) return row.status_text;
const businessText = getBusinessStatusText(row);
if (businessText) return businessText;
const code = row?.status_code;
const item = ORDER_STATUS_OPTIONS.find((opt) => opt.key === code);
return item ? item.label : code || '--';
}
function getOrderStatusClass(row) {
if (row && Number(row.is_del) === 1) return 'danger';
if (getBusinessStatusText(row)) return 'primary';
const map = {
pending_payment: 'warning',
processing: 'primary',
pending_review: 'success',
completed: 'success',
canceled: 'danger',
after_sale: 'warning',
refunding: 'warning',
refunded: 'info',
deleted: 'danger',
};
return map[row?.status_code] || 'info';
}
function isRefundedOrder(row) {
return row?.status_code === 'refunded' || Number(row?.status) === -2;
}
// created
getTabs();
getList();
@ -541,4 +608,8 @@ function onClickTab() {
:deep(.order-detail-drawer .el-drawer__body) {
padding: 20px 0 !important;
}
.business-status {
font-size: 12px;
color: #999;
}
</style>

View File

@ -138,11 +138,13 @@ export function getGiftOrderDetail(id) {
return request.get('order/gift_detail/' + id);
}
/**
* 订单详情
* @param string uni
* 订单详情(兼容旧调用:已迁移至中台 center/order/detail)
* @param string uni 订单号
*/
export function getOrderDetail(uni, cart_id) {
return request.get('order/detail/' + uni + `${cart_id ? `/${cart_id}`:''}`);
// 旧路由 order/detail 已下线;统一走中台详情。cart_id 参数已废弃。
void cart_id;
return request.get(`center/order/detail/${uni}`);
}
/**
* 退款订单详情

View File

@ -128,14 +128,18 @@ export default {
buyAgain(order, params) {
repurchaseToOrderCreate(order, params);
},
/** 删除订单 */
deleteOrder(order, params) {
/** 删除订单(详情页通过 context.onDelete 自行调接口并返回列表;列表页走事件总线) */
deleteOrder(order, params, context) {
const orderNo = params.order_no || order.order_no || '';
uni.showModal({
title: i18n.t(`提示`),
content: i18n.t(`确定删除该订单吗?删除后不可恢复`),
success: (res) => {
if (res.confirm) {
if (context && typeof context.onDelete === 'function') {
context.onDelete({ order_no: orderNo });
return;
}
uni.$emit('order:delete', { order_no: orderNo });
}
},
@ -208,4 +212,24 @@ export default {
refunded(order, params) {
uni.navigateTo({ url: routes.orderDetail + '?order_no=' + order.order_no });
},
/**
* 申请开票(详情页通过 context.onApplyInvoice 打开抬头选择弹窗)
*/
applyInvoice(order, params, context) {
if (context && typeof context.onApplyInvoice === 'function') {
context.onApplyInvoice(params || {});
return;
}
uni.showToast({ title: i18n.t(`请在订单详情中申请开票`), icon: 'none' });
},
/**
* 查看发票(详情页通过 context.onViewInvoice 打开发票信息弹窗)
*/
viewInvoice(order, params, context) {
if (context && typeof context.onViewInvoice === 'function') {
context.onViewInvoice(params || {});
return;
}
uni.showToast({ title: i18n.t(`请在订单详情中查看发票`), icon: 'none' });
},
};

View File

@ -107,8 +107,8 @@ const props = defineProps({
},
/** 当前选中的抬头 ID(字符串) */
checked: { type: String, default: '' },
/** 是否启用专用发票(受 features.special_invoice 控制) */
specialInvoice: { type: Boolean, default: true },
/** 是否启用专用发票(受 features.special_invoice 控制,缺省关闭) */
specialInvoice: { type: Boolean, default: false },
/** 跳转 user_invoice_form 时携带的回跳参数(不含 ?) */
urlQuery: { type: String, default: '' },
/** 弹窗显隐(支持 v-model:popupShow) */

View File

@ -1,20 +1,43 @@
<!--
底部操作按钮组组件
展示订单操作按钮(如去支付、取消订单、确认收货、申请退款等),点击触发对应 action
样式对齐旧版订单详情 footer:固定于屏幕底部 + 底部安全区适配 + 右对齐胶囊按钮
数据来源:section.props.buttons(每项含 {text, action, primary, params})
样式对齐旧版订单详情 footer:固定于屏幕底部 + 底部安全区适配
- 右侧:主操作胶囊按钮
- 左侧:placement=more 的次要操作收入「更多」菜单(如申请开票/查看发票)
数据来源:section.props.buttons(每项含 {text, action, primary, params, placement?})
-->
<template>
<view class="btn-group" v-if="buttons && buttons.length > 0">
<view
v-for="(btn, idx) in buttons"
:key="idx"
class="btn-item"
:class="btn.primary ? 'btn-primary' : 'btn-default'"
@tap="onTap(btn)"
>
{{ btn.text }}
<view class="btn-group" v-if="hasAnyButton">
<!-- 左下角「更多」(开票等次要操作) -->
<view class="more-wrap" v-if="moreButtons.length">
<view class="more" @tap.stop="toggleMore">
{{ $t('更多') }}
<text class="iconfont icon-xiangshang"></text>
</view>
<view class="more-box" v-if="moreOpen">
<view
class="more-btn"
v-for="(btn, idx) in moreButtons"
:key="'more-' + idx"
@tap.stop="onTap(btn)"
>{{ btn.text }}</view>
</view>
</view>
<view v-else class="more-placeholder"></view>
<view class="right-btn">
<view
v-for="(btn, idx) in mainButtons"
:key="'main-' + idx"
class="btn-item"
:class="btn.primary ? 'btn-primary' : 'btn-default'"
@tap="onTap(btn)"
>
{{ btn.text }}
</view>
</view>
<view class="more-mask" v-if="moreOpen" @tap="moreOpen = false"></view>
</view>
</template>
@ -32,7 +55,7 @@ export default {
</script>
<script setup>
import { computed } from 'vue';
import { computed, ref, watch } from 'vue';
const props = defineProps({
/** section 对象,含 props.buttons 和 _ctx 全局状态 */
@ -43,16 +66,34 @@ const emit = defineEmits(['action']);
/** 后端直填 props */
const p = computed(() => (props.section && props.section.props) || {});
/** 引擎注入的全局状态(预留) */
const ctx = computed(() => (props.section && props.section._ctx) || {});
/** 按钮数组 */
const buttons = computed(() => p.value.buttons || []);
/** placement=more 收入左下角「更多」 */
const moreButtons = computed(() =>
(buttons.value || []).filter((btn) => (btn.placement || '') === 'more')
);
/** 右侧主操作按钮 */
const mainButtons = computed(() =>
(buttons.value || []).filter((btn) => (btn.placement || '') !== 'more')
);
const hasAnyButton = computed(() => moreButtons.value.length > 0 || mainButtons.value.length > 0);
const moreOpen = ref(false);
watch(buttons, () => {
moreOpen.value = false;
});
function toggleMore() {
moreOpen.value = !moreOpen.value;
}
/**
* 按钮点击事件
* @param {object} btn 按钮对象
*/
function onTap(btn) {
moreOpen.value = false;
emit('action', {
action: btn.action,
params: btn.params || {},
@ -70,7 +111,7 @@ function onTap(btn) {
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: flex-end;
justify-content: space-between;
padding: 20rpx 30rpx;
padding-bottom: calc(20rpx + constant(safe-area-inset-bottom)); /*兼容 IOS<11.2*/
padding-bottom: calc(20rpx + env(safe-area-inset-bottom)); /*兼容 IOS>11.2*/
@ -78,6 +119,49 @@ function onTap(btn) {
border-top: 1rpx solid #f5f5f5;
z-index: 20;
}
.more-placeholder { width: 1rpx; }
.more-wrap { position: relative; z-index: 22; }
.more {
font-size: 26rpx;
color: #666;
display: flex;
align-items: center;
}
.more .iconfont { font-size: 22rpx; margin-left: 4rpx; }
.more-box {
position: absolute;
left: 0;
bottom: 70rpx;
min-width: 180rpx;
background: #fff;
border-radius: 12rpx;
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.12);
overflow: hidden;
z-index: 23;
}
.more-btn {
padding: 24rpx 30rpx;
font-size: 26rpx;
color: #333;
white-space: nowrap;
text-align: center;
}
.more-btn + .more-btn { border-top: 1rpx solid #f5f5f5; }
.more-mask {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: 21;
background: transparent;
}
.right-btn {
display: flex;
align-items: center;
justify-content: flex-end;
flex: 1;
}
/* 按钮胶囊样式(对齐旧版 .footer .bnt) */
.btn-item {
height: 60rpx;

View File

@ -16,10 +16,10 @@
<text class="invoice-tag">{{ invoice.header_type === 2 ? $t('企业') : $t('个人') }}</text>
</view>
</view>
<!-- 发票类型:电子/纸质 -->
<!-- 发票类型:1=普通 2=专用(与 eb_user_invoice / eb_order_invoice.type 一致) -->
<view class="info-row acea-row row-between">
<text class="info-label">{{ $t('发票类型') }}</text>
<text class="info-value">{{ invoice.type === 2 ? $t('纸质发票') : $t('电子发票') }}</text>
<text class="info-value">{{ invoice.type === 2 ? $t('增值税电子专用发票') : $t('增值税电子普通发票') }}</text>
</view>
<!-- 税号(企业抬头才有) -->
<view v-if="invoice.tax_number" class="info-row acea-row row-between">

View File

@ -242,7 +242,7 @@ const features = ref({
door_service: false, // 是否支持上门服务
remark: true,
invoice: true,
special_invoice: true,
special_invoice: false,
integral: false,
coupon: false,
custom_form: false,
@ -301,7 +301,7 @@ const remark = ref('');
const invoiceList = ref([]); // 已保存的抬头列表
const invoiceId = ref(''); // 选中的抬头 ID(字符串)
const invoicePopupShow = ref(false); // 抬头选择弹窗显隐
const specialInvoice = ref(true); // 是否启用专用发票(由 features.special_invoice 控制)
const specialInvoice = ref(false); // 是否启用专用发票(由 features.special_invoice 控制)
// 提交
const submitting = ref(false);
@ -363,7 +363,7 @@ const invoiceUrlQuery = computed(() => {
'&new=' + news.value +
'&address_id=' + addressId.value +
'&submit_api=' + encodeURIComponent(submitApi.value) +
'&specialInvoice=' + (specialInvoice.value ? 'true' : 'false');
'&specialInvoice=' + (specialInvoice.value ? '1' : '0');
} else {
query =
'product_id=' + encodeURIComponent(productId.value) +
@ -373,7 +373,7 @@ const invoiceUrlQuery = computed(() => {
'&activity_id=' + activityId.value +
'&num=' + num.value +
'&submit_api=' + encodeURIComponent(submitApi.value) +
'&specialInvoice=' + (specialInvoice.value ? 'true' : 'false');
'&specialInvoice=' + (specialInvoice.value ? '1' : '0');
}
return query;
});
@ -482,8 +482,8 @@ async function loadConfirmData() {
}
// 保存活动订单信息(含免费支付方式标识与跳转地址,由交易模式 Handler 下发)
activityInfo.value = data.activity_info || {};
// 同步发票相关配置:后端未返回时默认开启专用发票
specialInvoice.value = features.value.special_invoice !== false;
// 同步发票相关配置(专用发票须后台显式开启,缺省为关)
specialInvoice.value = !!features.value.special_invoice;
usableIntegral.value = parseInt(data.usable_integral || 0);
integralOpen.value = !!data.integral_open;
integralRatio.value = data.integral_ratio || 0;

View File

@ -1,5 +1,7 @@
<template>
<view :style="colorStyle">
<!-- 弹层打开时锁页面滚动,避免滑动穿透到订单详情 -->
<page-meta :page-style="pageLocked ? 'overflow:hidden;' : ''" />
<view :style="colorStyle" :class="{ 'page-locked': pageLocked }">
<page-loading v-if="loading" />
<view v-else class="order-details">
<!-- =============================================== -->
@ -34,7 +36,26 @@
@shareH5="shareH5"
@close="giftModalShow = false"
/>
<view class="mask" v-if="giftModalShow" @click="giftModalShow = false"></view>
<view class="mask" v-if="giftModalShow || invoiceModalShow" @click="closeInvoiceOverlays"></view>
<!-- 查看发票 -->
<invoiceModal
:aleartStatus="invoiceModalShow"
:invoiceData="invoiceModalData"
@close="invoiceModalShow = false"
/>
<!-- 申请开票:选择已有抬头并提交 make_up_invoice -->
<invoice-picker
:inv-show="invoicePickerShow"
:is-special="specialInvoice"
:url-query="invoiceUrlQuery"
:inv-checked="invoiceCheckedId"
:order-id="orderNo"
:inv-list="invoiceList"
:is-order="1"
@inv-close="invoicePickerShow = false"
@inv-change="submitInvoiceApply"
@inv-cancel="invoicePickerShow = false"
/>
</view>
</view>
</template>
@ -61,19 +82,25 @@ import { resolveSectionComponent, InfoListComponent } from '../components/order/
import SectionDispatcher from '../components/order/detail/SectionDispatcher.vue';
// #endif
import actionMap from '../api/action-map.js';
import { getCenterOrderDetail, takeOrder, cancelOrder } from '@/api/order-center.js';
import { express } from '@/api/order.js';
import { getCenterOrderDetail, takeOrder, cancelOrder, delOrder } from '@/api/order-center.js';
import routes from '../api/routes.js';
import { useColorTheme } from '@/composables/useColorTheme';
import i18n from '@/utils/lang.js';
import giftModal from '@/pages/goods/order_pay_status/components/giftModal.vue';
import invoiceModal from '@/pages/goods/components/invoiceModal/index.vue';
import invoicePicker from '@/pages/goods/components/invoicePicker/index.vue';
import { invoiceList as fetchInvoiceList, makeUpinvoice } from '@/api/user.js';
defineOptions({ name: 'OrderDetailEngine' });
const props = defineProps({
/** 订单号 */
orderNo: { type: String, default: '' },
/** 从发票抬头页带回的待选抬头 ID;有值且订单尚未开票时自动打开开票弹层 */
pendingInvoiceId: { type: String, default: '' },
});
/** 向包装页同步开票弹层显隐(用于隐藏悬浮 home) */
const emit = defineEmits(['invoice-overlay', 'pending-invoice-consumed']);
const { colorStyle } = useColorTheme();
@ -83,9 +110,15 @@ const pageConfig = ref({ sections: [], extensions: {} });
const loading = ref(true);
const confirmingReceive = ref(false);
const cancellingOrder = ref(false);
const logisticsLoading = ref(false);
const deletingOrder = ref(false);
// 送礼分享
const giftModalShow = ref(false);
// 发票:申请开票 / 查看发票
const invoicePickerShow = ref(false);
const invoiceModalShow = ref(false);
const invoiceList = ref([]);
const invoiceCheckedId = ref('');
const invoiceApplying = ref(false);
// ---------- computed ----------
/**
@ -129,6 +162,27 @@ const viewerRole = computed(() => {
const orderInfoFields = computed(() => {
return (sectionPropsMap.value['order_info'] && sectionPropsMap.value['order_info'].fields) || {};
});
/** 是否启用专用发票(extensions.special_invoice) */
const specialInvoice = computed(() => !!(pageConfig.value.extensions && pageConfig.value.extensions.special_invoice));
/** 跳转抬头表单时携带的回跳参数 */
const invoiceUrlQuery = computed(() => `specialInvoice=${specialInvoice.value ? 1 : 0}`);
/**
* 查看发票弹窗数据(兼容 invoiceModal 使用的 name/duty_number 字段)
*/
const invoiceModalData = computed(() => {
const inv = (pageConfig.value.extensions && pageConfig.value.extensions.invoice) || {};
const phone = inv.drawer_phone || inv.phone || inv.tell || '';
return {
...inv,
name: inv.name || inv.title || '',
duty_number: inv.duty_number || inv.tax_number || '',
drawer_phone: phone,
phone,
remark: inv.remark || '',
is_invoice: Number(inv.is_invoice || 0),
pay_price: payPrice.value,
};
});
/** giftModal 分享数据 */
const giftShareData = computed(() => {
// section.props.product = { product: {title,image,items}, product_id }
@ -170,9 +224,9 @@ async function loadDetail() {
uni[r.mode || 'redirectTo']({ url: r.url });
return;
}
pageConfig.value = (res.data && res.data.page_config) || { sections: [], extensions: {} };
// 详情接口首屏只返回已缓存的物流轨迹,未命中时异步补齐,避免被第三方物流慢请求阻塞
loadLogisticsTracks();
pageConfig.value = sanitizePageConfig((res.data && res.data.page_config) || { sections: [], extensions: {} });
// 详情页配送信息只展示公司/单号,轨迹由「查看物流」单独查询,此处不再异步拉轨迹
maybeOpenPendingInvoice();
} catch (e) {
uni.showToast({ title: i18n.t('加载失败'), icon: 'none' });
} finally {
@ -181,44 +235,65 @@ async function loadDetail() {
}
/**
* 异步加载物流轨迹
* 详情接口不再同步请求第三方物流,仅返回已缓存轨迹;未命中时由前端按需查询并回填 section
* 失败时静默降级,仍展示快递公司 + 单号
* @return {Promise<void>}
* 从发票抬头编辑页返回时:若带回 invoice_id 且订单尚未开票,自动打开抬头选择并预选该抬头
*/
async function loadLogisticsTracks() {
if (logisticsLoading.value) return;
const sections = pageConfig.value.sections || [];
const section = sections.find(function (s) { return s.key === 'logistics'; });
if (!section) return;
// 已命中后端轨迹缓存,无需再次查询
const cached = (section.props && section.props.tracks) || [];
if (cached.length > 0) return;
const orderNo = (sectionPropsMap.value['order_info'] && sectionPropsMap.value['order_info'].fields
&& sectionPropsMap.value['order_info'].fields.order_no) || props.orderNo || '';
if (!orderNo) return;
logisticsLoading.value = true;
try {
const res = await express(orderNo);
const list = res && res.data && res.data.express && res.data.express.result
? res.data.express.result.list : null;
if (!Array.isArray(list) || list.length === 0) return;
section.props.tracks = list.map(function (track) {
return {
title: track.title || track.status || track.context || track.name || '',
time: track.time || track.ftime || '',
desc: track.desc || '',
};
});
} catch (e) {
// 物流查询失败不影响详情展示
} finally {
logisticsLoading.value = false;
function maybeOpenPendingInvoice() {
const invoiceId = String(props.pendingInvoiceId || '');
if (!invoiceId) return;
emit('pending-invoice-consumed');
const inv = (pageConfig.value.extensions && pageConfig.value.extensions.invoice) || {};
// 已有开票申请则不再弹层
if (inv && (inv.id || inv.invoice_id || inv.name || inv.title)) {
return;
}
openInvoiceApply(invoiceId);
}
/**
* 裁剪查看物流按钮:虚拟发货 / 商家送货 / 无配送信息区时不展示「查看物流」
* (配送信息区本身由后端按类型下发;虚拟发货有备注时仍保留区块)
* @param {object} config page_config
* @return {object}
*/
function sanitizePageConfig(config) {
const next = config || { sections: [], extensions: {} };
const sections = Array.isArray(next.sections) ? next.sections : [];
const logistics = sections.find(function (s) { return s && s.key === 'logistics'; });
const deliveryType = String((logistics && logistics.props && logistics.props.delivery_type) || '');
const stripLogisticsBtn = deliveryType === 'fictitious'
|| deliveryType === 'send'
|| !logistics
|| logistics.visible === false;
if (!stripLogisticsBtn) return next;
const stripViewLogistics = function (list) {
if (!Array.isArray(list)) return list;
return list.filter(function (btn) { return (btn && btn.action) !== 'viewLogistics'; });
};
const ex = next.extensions || {};
if (ex.order_buttons) ex.order_buttons = stripViewLogistics(ex.order_buttons);
if (ex.buttons) ex.buttons = stripViewLogistics(ex.buttons);
next.extensions = ex;
next.sections = sections.map(function (s) {
if (!s || !s.props || !Array.isArray(s.props.buttons)) return s;
return Object.assign({}, s, {
props: Object.assign({}, s.props, { buttons: stripViewLogistics(s.props.buttons) }),
});
});
return next;
}
// 包装页 onShow 返回时调用(评价完成/售后进度变化等场景,返回详情需看到最新状态)
defineExpose({ refresh: loadDetail });
/** 开票/查看发票弹层是否打开(供包装页隐藏悬浮 home) */
const invoiceOverlayOpen = computed(() => invoicePickerShow.value || invoiceModalShow.value);
/** 任意遮罩弹层打开时锁定页面滚动 */
const pageLocked = computed(() => invoiceOverlayOpen.value || giftModalShow.value);
watch(invoiceOverlayOpen, (open) => {
emit('invoice-overlay', open);
}, { immediate: true });
defineExpose({ refresh: loadDetail, invoiceOverlayOpen });
// ========== 全局状态注入 ==========
@ -291,8 +366,14 @@ function onButtonAction(payload) {
context = { onConfirm: confirmReceive };
} else if (payload.action === 'cancelOrder') {
context = { onCancel: cancelUnpaidOrder };
} else if (payload.action === 'deleteOrder') {
context = { onDelete: deleteOrderAndBack };
} else if (payload.action === 'shareGift') {
context = { onShareGift: openGiftShare };
} else if (payload.action === 'applyInvoice') {
context = { onApplyInvoice: openInvoiceApply };
} else if (payload.action === 'viewInvoice') {
context = { onViewInvoice: openInvoiceView };
}
handler(Object.assign({}, sectionPropsMap.value, { order_no: orderNo }), payload.params || {}, context);
}
@ -305,6 +386,72 @@ function openGiftShare() {
giftModalShow.value = true;
}
/** 关闭发票相关浮层 */
function closeInvoiceOverlays() {
invoiceModalShow.value = false;
giftModalShow.value = false;
}
/** 查看发票 */
function openInvoiceView() {
if (!invoiceModalData.value || !(invoiceModalData.value.name || invoiceModalData.value.title)) {
uni.showToast({ title: i18n.t('暂无发票信息'), icon: 'none' });
return;
}
invoiceModalShow.value = true;
}
/**
* 申请开票:拉取抬头列表并打开选择弹窗
* @param {string} [preselectId] 预选抬头 ID(从抬头编辑页返回时传入)
*/
function openInvoiceApply(preselectId = '') {
uni.showLoading({ title: i18n.t('正在加载中'), mask: true });
fetchInvoiceList()
.then((res) => {
uni.hideLoading();
invoiceList.value = (res.data || []).map((item) => {
item.id = String(item.id);
return item;
});
invoiceCheckedId.value = preselectId ? String(preselectId) : '';
invoicePickerShow.value = true;
})
.catch((err) => {
uni.hideLoading();
uni.showToast({ title: (typeof err === 'string' ? err : err?.msg) || i18n.t('加载失败'), icon: 'none' });
});
}
/**
* 确认提交开票申请
* @param {string} invoiceId 选中的抬头 ID
*/
function submitInvoiceApply(invoiceId) {
if (!invoiceId) {
uni.showToast({ title: i18n.t('请选择发票抬头'), icon: 'none' });
return;
}
if (invoiceApplying.value) return;
invoiceApplying.value = true;
makeUpinvoice({
order_id: props.orderNo,
invoice_id: invoiceId,
})
.then(async () => {
uni.showToast({ title: i18n.t('申请成功'), icon: 'success' });
invoicePickerShow.value = false;
await loadDetail();
invoiceModalShow.value = true;
})
.catch((err) => {
uni.showToast({ title: (typeof err === 'string' ? err : err?.msg) || i18n.t('申请失败'), icon: 'none' });
})
.finally(() => {
invoiceApplying.value = false;
});
}
/**
* 详情页确认收货
* @param {object} payload {order_no}
@ -343,6 +490,32 @@ async function cancelUnpaidOrder(payload) {
cancellingOrder.value = false;
}
}
/**
* 详情页删除订单:成功后通知列表刷新并返回列表(订单已不存在,不可留在详情)
* @param {object} payload {order_no}
*/
async function deleteOrderAndBack(payload) {
if (deletingOrder.value) return;
deletingOrder.value = true;
try {
await delOrder(payload.order_no);
uni.$emit('order:list-refresh');
uni.showToast({ title: i18n.t('删除成功'), icon: 'success' });
setTimeout(() => {
const pages = getCurrentPages();
if (pages.length > 1) {
uni.navigateBack({ delta: 1 });
} else {
uni.redirectTo({ url: '/pages/goods/order_list/index' });
}
}, 500);
} catch (e) {
uni.showToast({ title: (typeof e === 'string' ? e : e?.msg) || i18n.t('删除失败'), icon: 'none' });
} finally {
deletingOrder.value = false;
}
}
/**
* 行动入口卡片点击(ActionCard emit)
* @param {object} payload {action, link}
@ -456,6 +629,12 @@ function shareH5() {
padding-bottom: calc(150rpx + env(safe-area-inset-bottom));
}
/* H5:弹层打开时锁住页面滚动(小程序侧靠 page-meta) */
.page-locked {
height: 100vh;
overflow: hidden;
}
.mask {
position: fixed;
left: 0;
@ -463,7 +642,7 @@ function shareH5() {
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
/* 低于 giftModal 的 999,避免遮罩盖住弹窗按钮 */
/* 低于 giftModal 的 999,避免遮罩盖住弹窗按钮;高于开票弹层由 invoicePicker 自管 */
z-index: 998;
}
</style>

View File

@ -1,9 +1,15 @@
<template>
<view>
<OrderDetailEngine ref="engine" :order-no="orderNo" />
<!-- 系统悬浮菜单(小程序端不显示) -->
<OrderDetailEngine
ref="engine"
:order-no="orderNo"
:pending-invoice-id="pendingInvoiceId"
@invoice-overlay="onInvoiceOverlay"
@pending-invoice-consumed="pendingInvoiceId = ''"
/>
<!-- 系统悬浮菜单(小程序端不显示);开票弹层打开时隐藏,避免压在弹层上 -->
<!-- #ifndef MP -->
<home></home>
<home v-show="!invoiceOverlayOpen"></home>
<!-- #endif -->
</view>
</template>
@ -15,10 +21,18 @@ import OrderDetailEngine from '@/dynamic-pages/engines/OrderDetailEngine.vue';
import home from '@/components/home';
const orderNo = ref('');
/** 从发票抬头页带回的待选抬头 ID,用于自动打开开票弹层 */
const pendingInvoiceId = ref('');
const engine = ref(null);
const invoiceOverlayOpen = ref(false);
function onInvoiceOverlay(open) {
invoiceOverlayOpen.value = !!open;
}
onLoad((options) => {
orderNo.value = options.order_no || '';
pendingInvoiceId.value = options.invoice_id ? String(options.invoice_id) : '';
});
// 首次 onShow 与 onLoad 引擎的首次加载重复,跳过;此后每次返回本页(评价完成 navigateBack 等)刷新详情数据

View File

@ -1,212 +1,207 @@
<template>
<view class="aleart" v-if="aleartStatus" :style="colorStyle">
<!-- 顶部状态:图标半悬于弹窗外,文案居中 -->
<view class="icon-top">
<text class="iconfont icon-fapiao2"
:style="invoiceData.is_invoice?'background-color: var(--view-theme)':'background-color: #999'"></text>
<view
class="icon-wrap"
:class="invoiceData.is_invoice ? 'on' : 'off'"
>
<text class="iconfont icon-fapiao2"></text>
</view>
<view class="bill">
{{invoiceData.is_invoice?$t(`已开票`): $t(`未开票`)}}
{{ invoiceData.is_invoice ? $t(`已开票`) : $t(`未开票`) }}
</view>
</view>
<view class="aleart-body">
<view class="body-head">{{$t(`发票信息`)}}</view>
<view class="body-head">{{ $t(`发票信息`) }}</view>
<view class="label">
<view class="">
{{$t(`发票抬头`)}}
</view>
<view class="label-value">
{{invoiceData.name}}
</view>
<view>{{ $t(`发票抬头`) }}</view>
<view class="label-value">{{ invoiceData.name || invoiceData.title || '--' }}</view>
</view>
<view class="label">
<view class="">
{{$t(`发票抬头类型`)}}
</view>
<view class="label-value">
{{invoiceData.header_type == 1?$t(`个人`):$t(`企业`)}}
</view>
<view>{{ $t(`发票抬头类型`) }}</view>
<view class="label-value">{{ invoiceData.header_type == 1 ? $t(`个人`) : $t(`企业`) }}</view>
</view>
<view class="label">
<view class="">
{{$t(`发票类型`)}}
</view>
<view class="label-value">
{{invoiceData.type==1?$t(`电子普通发票`):$t(`电子专用发票`)}}
</view>
<view>{{ $t(`发票类型`) }}</view>
<view class="label-value">{{ invoiceData.type == 1 ? $t(`电子普通发票`) : $t(`电子专用发票`) }}</view>
</view>
<view class="label" v-if="invoiceData.duty_number">
<view class="">
{{$t(`企业税号`)}}
</view>
<view class="label-value">
{{invoiceData.duty_number}}
</view>
<view class="label" v-if="invoiceData.duty_number || invoiceData.tax_number">
<view>{{ $t(`企业税号`) }}</view>
<view class="label-value">{{ invoiceData.duty_number || invoiceData.tax_number }}</view>
</view>
<view class="body-head">{{$t(`联系信息`)}}</view>
<view class="body-head">{{ $t(`联系信息`) }}</view>
<view class="label">
<view class="">
{{$t(`真实姓名`)}}
</view>
<view class="label-value">
{{invoiceData.name}}
</view>
<view>{{ $t(`真实姓名`) }}</view>
<view class="label-value">{{ invoiceData.name || invoiceData.title || '--' }}</view>
</view>
<view class="label">
<view class="">
{{$t(`联系电话`)}}
</view>
<view class="label-value">
{{invoiceData.drawer_phone}}
</view>
<view>{{ $t(`联系电话`) }}</view>
<view class="label-value">{{ contactPhone || '--' }}</view>
</view>
<view class="label">
<view class="">
{{$t(`联系邮箱`)}}
</view>
<view class="label-value">
{{invoiceData.email}}
</view>
<view>{{ $t(`联系邮箱`) }}</view>
<view class="label-value">{{ invoiceData.email || '--' }}</view>
</view>
<view class="label">
<view class="">
{{$t(`发票备注`)}}
</view>
<view class="label-value">
{{invoiceData.remark}}
</view>
<view>{{ $t(`发票备注`) }}</view>
<view class="label-value">{{ invoiceData.remark || '--' }}</view>
</view>
</view>
<view class="btn" @click="close">
{{$t(`确认`)}}
</view>
<view class="btn" @click="close">{{ $t(`确认`) }}</view>
</view>
</template>
<script setup>
import { useColorTheme } from '@/composables/useColorTheme';
import { computed } from 'vue';
import { useColorTheme } from '@/composables/useColorTheme';
const { colorStyle } = useColorTheme();
const { colorStyle } = useColorTheme();
defineProps({
aleartStatus: {
type: Boolean,
default: false
},
invoiceData: {
type: Object,
default: () => ({})
}
});
const props = defineProps({
aleartStatus: {
type: Boolean,
default: false,
},
invoiceData: {
type: Object,
default: () => ({}),
},
});
const emit = defineEmits(['close']);
const emit = defineEmits(['close']);
function close() {
emit('close')
}
/** 联系电话:个人 drawer_phone / 契约 phone / 企业 tell */
const contactPhone = computed(() => {
const d = props.invoiceData || {};
return d.drawer_phone || d.phone || d.tell || '';
});
function close() {
emit('close');
}
</script>
<style lang="scss" scoped>
.aleart {
width: 80%;
// height: 714rpx;
position: fixed;
left: 50%;
transform: translateX(-50%);
z-index: 9999;
top: 45%;
margin-top: -357rpx;
background-color: #fff;
padding: 30rpx;
border-radius: 12rpx;
background-image: -webkit-gradient(linear, //表示渐变的为直线 另外一个值是radial
50% 0, //直线型渐变的起点位置 后边有一个属性background-size规定背景的大小,30 X 15px 50% 0 都是乘以父元素的宽高。
0 100%, //结束点的位置 和上类似
from(transparent), //起点的颜色
color-stop(.5, transparent), //中间某一个点必须达到这个颜色,表示变化过程 .5b表示这个渐变范围长度的总长的50%
color-stop(.5, #999999), //同上
to(#999999)), //结束段的颜色
//一个背景块的分为两个15X 15 组成。
.aleart {
width: 80%;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
z-index: 9999;
background-color: #fff;
padding: 0 30rpx 30rpx;
border-radius: 12rpx;
overflow: visible;
/* 底部锯齿票根效果 */
background-image: -webkit-gradient(
linear,
50% 0,
0 100%,
from(transparent),
color-stop(0.5, transparent),
color-stop(0.5, #999999),
to(#999999)
),
-webkit-gradient(
linear,
50% 0,
100% 100%,
from(transparent),
color-stop(0.5, transparent),
color-stop(0.5, #999999),
to(#999999)
);
background-size: 20rpx 10rpx;
background-repeat: repeat-x;
background-position: 0 100%;
-webkit-gradient(linear, 50% 0, 100% 100%, from(transparent),
color-stop(.5, transparent),
color-stop(.5, #999999),
to(#999999));
background-size: 20rpx 10rpx;
background-repeat: repeat-x;
background-position: 0 100%;
.icon-top {
display: flex;
flex-direction: column;
align-items: center;
margin-top: -48rpx;
margin-bottom: 12rpx;
.icon-top {
margin-left: calc(50% - 40rpx);
margin-top: -40rpx;
display: flex;
flex-direction: column;
align-items: center;
.icon-wrap {
width: 96rpx;
height: 96rpx;
border-radius: 50%;
width: 100rpx;
height: 100rpx;
.icon-fapiao2 {
text-align: center;
border-radius: 50%;
font-size: 80rpx;
color: #fff;
background-color: var(--view-theme);
padding: 20rpx;
border: 4rpx solid #fff;
margin-top: -40rpx;
}
.bill {
width: 172rpx;
text-align: center;
}
}
.title {
font-size: 34rpx;
color: var(--view-theme);
font-weight: bold;
text-align: center;
padding-bottom: 10rpx;
border-bottom: 1px solid var(--view-op-ten);
}
.aleart-body {
border: 4rpx solid #fff;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
padding: 60rpx 0;
box-sizing: border-box;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
.body-head {
font-size: 30rpx;
font-weight: bold;
padding-bottom: 10rpx;
border-bottom: 1px solid #EEEEEE;
margin: 10rpx 0;
.iconfont {
font-size: 48rpx;
color: #fff;
line-height: 1;
}
.label {
width: 100%;
display: flex;
justify-content: space-between;
margin-bottom: 15rpx;
color: #333333;
font-size: 28rpx;
&.on {
background-color: var(--view-theme, #e93323);
}
.label-value {
color: #666666;
}
&.off {
background-color: #999;
}
}
.btn {
width: 100%;
padding: 15rpx 0;
color: #fff;
background: var(--view-theme);
border-radius: 20px;
.bill {
margin-top: 12rpx;
font-size: 28rpx;
color: #333;
text-align: center;
margin-bottom: 30rpx;
line-height: 1.2;
}
}
.aleart-body {
display: flex;
flex-direction: column;
padding: 20rpx 0 10rpx;
.body-head {
font-size: 30rpx;
font-weight: bold;
padding-bottom: 10rpx;
border-bottom: 1px solid #eeeeee;
margin: 16rpx 0 12rpx;
}
.label {
width: 100%;
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 15rpx;
color: #333333;
font-size: 28rpx;
gap: 24rpx;
.label-value {
color: #666666;
text-align: right;
flex: 1;
word-break: break-all;
}
}
}
.btn {
width: 100%;
padding: 15rpx 0;
color: #fff;
background: var(--view-theme, #e93323);
border-radius: 20px;
text-align: center;
margin-top: 10rpx;
margin-bottom: 20rpx;
}
}
</style>

View File

@ -1,9 +1,12 @@
<template>
<view>
<view :class="{ mask: invShow }" @touchmove.stop.prevent @click="invClose"></view>
<view class="popup" :class="{ on: invShow }">
<view class="popup-hd">{{$t(`抬头选择`)}}<text class="iconfont icon-guanbi" @click="invClose"></text></view>
<scroll-view class="popup-bd" scroll-y="true">
<!-- 独立层级容器:盖住订单底栏(z=20)与悬浮 home(z=999);背景滚动由 mask + 页面锁处理 -->
<view class="invoice-picker-root" v-if="invShow">
<view class="mask" @touchmove.stop.prevent @click="invClose"></view>
<!-- stop 阻断冒泡到页面;勿 prevent,否则内部 scroll-view 无法滑动 -->
<view class="popup on" @touchmove.stop>
<view class="popup-hd" @touchmove.stop.prevent>{{$t(`抬头选择`)}}<text class="iconfont icon-guanbi" @click="invClose"></text></view>
<!-- scroll-view 需可纵向滚动,勿在其上 prevent -->
<scroll-view class="popup-bd" scroll-y="true" :show-scrollbar="false">
<radio-group v-if="invList.length" name="inv" @change="invChange">
<template v-for="(item, index) in invList" :key="item.id">
<label v-if="item.type === 1 || item.type === 2 && isSpecial"
@ -35,7 +38,7 @@
hover-class="none"><text class="iconfont icon-bianji"></text>{{$t(`编辑`)}}
</navigator>
<navigator :render-link="false" v-else class="navigator"
:url="`/pages/users/user_invoice_form/index?from=order_details&id=${item.id}&order_id=${orderId}`"
:url="`/pages/users/user_invoice_form/index?from=order_details&id=${item.id}&order_id=${orderId}&${urlQuery}`"
hover-class="none"><text class="iconfont icon-bianji"></text>{{$t(`编辑`)}}
</navigator>
</view>
@ -48,7 +51,7 @@
<view>{{$t(`您还没有添加发票信息哟`)}}~</view>
</view>
</scroll-view>
<view class="popup-ft">
<view class="popup-ft" @touchmove.stop.prevent>
<navigator :render-link="false" v-if="!isOrder" class="navigator"
:url="`/pages/users/user_invoice_form/index?from=order_confirm&${urlQuery}`" hover-class="none">
<text class="iconfont icon-fapiao"></text>{{$t(`添加新的抬头`)}}
@ -132,21 +135,46 @@
margin-right: 0;
}
.popup {
/* 高于订单底栏(20)与悬浮 home(999),避免底栏/悬浮钮压在弹层上 */
.invoice-picker-root {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: 10050;
}
.mask {
position: absolute;
left: 0;
top: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
opacity: 1;
z-index: 1;
}
.popup {
position: absolute;
bottom: 0;
left: 0;
z-index: 9;
z-index: 2;
width: 100%;
max-height: 85vh;
display: flex;
flex-direction: column;
border-top-left-radius: 16rpx;
border-top-right-radius: 16rpx;
background-color: #F5F5F5;
transform: translateY(100%);
transition: 0.3s;
box-sizing: border-box;
padding-bottom: constant(safe-area-inset-bottom);
padding-bottom: env(safe-area-inset-bottom);
}
.popup.on {
transform: translateY(0);
transform: none;
}
.popup-hd {
@ -168,7 +196,9 @@
}
.popup-bd {
flex: 1;
height: 600rpx;
max-height: 50vh;
padding-right: 30rpx;
padding-left: 30rpx;
box-sizing: border-box;
@ -274,7 +304,9 @@
}
.popup-ft {
flex-shrink: 0;
padding: 14rpx 30rpx 44rpx;
background-color: #F5F5F5;
.navigator {
height: 86rpx;

View File

@ -432,8 +432,8 @@
<view class="conter">-{{ $t(`¥`) }}{{ parseFloat(orderInfo.memberPrice).toFixed(2) }}</view>
</view>
<view v-if="orderInfo.gift_price > 0" class="item acea-row row-between">
<view>{{ $t(`礼品附加费用`) }}:</view>
<view class="conter">-{{ $t(`¥`) }}{{ parseFloat(orderInfo.gift_price).toFixed(2) }}</view>
<view>{{ $t(`礼品附加费`) }}:</view>
<view class="conter">+{{ $t(`¥`) }}{{ parseFloat(orderInfo.gift_price).toFixed(2) }}</view>
</view>
<view class="item acea-row row-between" v-if="orderInfo.coupon_price > 0">
<view>{{ $t(`优惠券抵扣`) }}:</view>
@ -777,11 +777,18 @@ const refundedPriceText = computed(() => {
});
// ---------- 生命周期 ----------
// 已重定向到中台详情时,禁止 onShow 再调废弃的 order/detail
const redirectedToCenter = ref(false);
onLoad((options) => {
// 订单数据结构已迁移至中台 OrderCenter,旧版扁平接口(order/detail)已废弃
// 延用原页面路径作为入口,重定向到新版引擎页面,避免修改 20+ 个跳转入口
if (options.order_id) {
uni.redirectTo({ url: '/pages/center/order/detail/index?order_no=' + options.order_id });
redirectedToCenter.value = true;
const q = [`order_no=${options.order_id}`];
if (options.invoice_id) q.push(`invoice_id=${options.invoice_id}`);
if (options.header_type) q.push(`header_type=${options.header_type}`);
uni.redirectTo({ url: '/pages/center/order/detail/index?' + q.join('&') });
return;
}
if (options.order_id) {
@ -794,6 +801,7 @@ onLoad((options) => {
});
onShow(() => {
if (redirectedToCenter.value) return;
if (isLogin.value) {
getOrderInfo();
getUserInfo();

View File

@ -13,7 +13,8 @@
</label>
</radio-group>
</view>
<view class="acea-row row-middle" v-if="basicConfigData.special_invoice_status === '1' && header_type === '2'">
<!-- 仅企业 + 后台开启专用发票时可选专票(个人只能普票) -->
<view class="acea-row row-middle" v-if="specialInvoice && header_type === '2'">
<view>{{$t(`发票类型`)}}</view>
<text class="w-fill" @click="callType">
<text>{{ type === '2' ? $t(`增值税电子专用发票`) : $t(`增值税电子普通发票`) }} </text>
@ -131,9 +132,19 @@ const popupType = ref(false);
const typeName = ref('');
const urlQuery = ref('');
const from = ref('');
const specialInvoice = ref(true);
/** 是否允许企业选择专用发票(由 BASIC_CONFIG / 路由参数共同决定) */
const specialInvoice = ref(false);
const order_id = ref('');
const basicConfigData = uni.getStorageSync('BASIC_CONFIG') || '';
const basicConfigData = uni.getStorageSync('BASIC_CONFIG') || {};
/**
* 配置开关真值判断(兼容 1/'1'/true/'true')
* @param {*} value
* @return {boolean}
*/
function isConfigEnabled(value) {
return value === true || value === 1 || value === '1' || value === 'true';
}
// ---------- computed ----------
const backUrl = computed(() => {
@ -152,6 +163,8 @@ onHide(() => {
from.value = ''
});
onLoad((options) => {
// 先读基础配置;路由参数可覆盖(下单/详情页会显式传入)
specialInvoice.value = isConfigEnabled(basicConfigData.special_invoice_status);
if (options.id) uni.setNavigationBarTitle({
title: i18n.t(`编辑发票`)
@ -177,9 +190,7 @@ onLoad((options) => {
getInvoiceDetail();
break;
case 'specialInvoice':
if (options[key] === 'false') {
specialInvoice.value = false;
}
specialInvoice.value = isConfigEnabled(options[key]);
break;
}
}
@ -397,17 +408,14 @@ function formSubmit(e) {
})
}
break;
case 'order_details':
if (id.value) {
uni.navigateTo({
url: `/pages/goods/order_details/index?order_id=${order_id.value}&invoice_id=${id.value}&header_type=${header_type.value}`
})
} else {
uni.navigateTo({
url: `/pages/goods/order_details/index?order_id=${order_id.value}&invoice_id=${res.data.id}&header_type=${header_type.value}`
})
}
case 'order_details': {
// 订单详情已迁中台引擎,勿再跳旧 order_details(会请求已废弃的 order/detail)
const invoiceId = id.value || (res.data && res.data.id) || '';
uni.redirectTo({
url: `/pages/center/order/detail/index?order_no=${order_id.value}&invoice_id=${invoiceId}&header_type=${header_type.value}`
});
break;
}
default:
uni.navigateTo({
url: '/pages/users/user_invoice_list/index?from=invoice_form'

View File

@ -7,8 +7,8 @@
<view v-show="nav === 1" class="record-wrapper">
<view v-for="item in orderList" :key="item.id" class="item">
<view class="item-hd acea-row">
<image class="image" :src="item.order.cartInfo[0].productInfo.image"></image>
<view class="text">{{ item.order.cartInfo[0].productInfo.store_name + item.order.cartInfo[0].productInfo.attrInfo.suk || '' }}</view>
<image class="image" :src="invoiceGoodsImage(item)"></image>
<view class="text">{{ invoiceGoodsName(item) }}</view>
</view>
<view class="item-bd acea-row row-between-wrapper">
<view>
@ -17,14 +17,18 @@
</view>
<view class="money">
{{ $t(`¥`) }}
<text class="num">{{ item.order.pay_price }}</text>
<text class="num">{{ (item.order && item.order.pay_price) || '0.00' }}</text>
</view>
</view>
<view class="item-ft acea-row row-between-wrapper">
<view>{{ item.is_invoice ? $t(`已开票`) : $t(`未开票`) }}</view>
<view class="acea-row row-center-wrapper">
<view class="link mr20" @click="getInvoiceLink(item.id)" v-if="item.is_invoice == 1 && item.unique_num != '' && item.red_invoice_num == ''">{{ $t(`复制`) }}</view>
<navigator :render-link="false" class="link" :url="`/pages/users/user_invoice_order/index?order_id=${item.order.order_id}`">{{ $t(`查看详情`) }}</navigator>
<navigator
:render-link="false"
class="link"
:url="`/pages/users/user_invoice_order/index?order_id=${(item.order && (item.order.order_id || item.order.order_no)) || ''}`"
>{{ $t(`查看详情`) }}</navigator>
</view>
</view>
</view>
@ -68,7 +72,7 @@
<image :src="imgHost + '/statics/images/noInvoice.png'"></image>
<view>{{ $t(`没有发票信息哟~`) }}</view>
</view>
<navigator :render-link="false" class="add-link" :url="`/pages/users/user_invoice_form/index?specialInvoice=${specialInvoice}`">
<navigator :render-link="false" class="add-link" :url="`/pages/users/user_invoice_form/index?specialInvoice=${specialInvoice ? 1 : 0}`">
<text class="iconfont icon-fapiao"></text>
{{ $t(`添加新发票`) }}
</navigator>
@ -105,7 +109,7 @@ const page = ref(1);
const limit = ref(30);
const loading = ref(false);
const finished = ref(false);
const specialInvoice = ref(true);
const specialInvoice = ref(false);
// ---------- watch ----------
watch(nav, (value) => {
@ -131,6 +135,15 @@ onLoad((option) => {
});
// ---------- methods ----------
function invoiceGoodsImage(item) {
const product = item?.order?.cartInfo?.[0]?.productInfo || {};
return product?.attrInfo?.image || product?.image || '';
}
function invoiceGoodsName(item) {
const product = item?.order?.cartInfo?.[0]?.productInfo || {};
const suk = product?.attrInfo?.suk || '';
return `${product?.store_name || ''}${suk}`;
}
function getInvoiceLink(id) {
uni.showLoading({
title: i18n.t(`复制中`)

View File

@ -56,22 +56,32 @@
<view class="conter">{{orderInfo.invoice.remark}}</view>
</view>
</view>
<orderGoods :evaluate='evaluate' :orderId="order_id" :cartInfo="cartInfo" :jump="true" :paid="orderInfo.paid" :oid="orderInfo.id" :isShow="false" :statusType="status.type"></orderGoods>
<orderGoods
v-if="cartInfo && cartInfo.length"
:evaluate="evaluate"
:orderId="order_id"
:cartInfo="cartInfo"
:jump="true"
:paid="orderInfo.paid"
:oid="orderInfo.id"
:isShow="false"
:statusType="status.type"
></orderGoods>
<view class='wrapper'>
<view class='item acea-row row-between'>
<view>{{$t(`订单编号`)}}:</view>
<view class='conter acea-row row-middle row-right'>{{orderInfo.order_id}}
<view class='conter acea-row row-middle row-right'>{{orderInfo.order_id || orderInfo.order_no}}
<!-- #ifndef H5 -->
<text class='copy' @tap='copy'>{{$t(`复制`)}}</text>
<!-- #endif -->
<!-- #ifdef H5 -->
<text class='copy copy-data' :data-clipboard-text="orderInfo.order_id">{{$t(`复制`)}}</text>
<text class='copy copy-data' :data-clipboard-text="orderInfo.order_id || orderInfo.order_no">{{$t(`复制`)}}</text>
<!-- #endif -->
</view>
</view>
<view class='item acea-row row-between'>
<view>{{$t(`下单时间`)}}:</view>
<view class='conter'>{{(orderInfo.add_time_y || '') +' '+(orderInfo.add_time_h || 0)}}</view>
<view class='conter'>{{(orderInfo.add_time_y || '') +' '+(orderInfo.add_time_h || '')}}</view>
</view>
<view class='item acea-row row-between'>
<view>{{$t(`支付状态`)}}:</view>
@ -80,7 +90,7 @@
</view>
<view class='item acea-row row-between'>
<view>{{$t(`支付方式`)}}:</view>
<view class='conter'>{{orderInfo._status._payType}}</view>
<view class='conter'>{{ (orderInfo._status && orderInfo._status._payType) || orderInfo.pay_type_name || orderInfo.pay_type || '' }}</view>
</view>
<view class='item acea-row row-between' v-if="orderInfo.mark">
<view>{{$t(`买家留言`)}}:</view>
@ -688,13 +698,14 @@ function getOrderInfo() {
title: i18n.t(`正在加载中`)
});
orderInvoiceDetail(order_id.value).then(res => {
let _type = res.data._status._type;
const data = res.data || {};
const statusInfo = data._status || {};
uni.hideLoading();
orderInfo.value = res.data;
cartInfo.value = res.data.cartInfo;
evaluate.value = _type == 3 ? 3 : 0;
system_store.value = res.data.system_store;
if (orderInfo.value.refund_status != 0) {
orderInfo.value = Object.assign({ system_store: {}, _status: {} }, data);
cartInfo.value = Array.isArray(data.cartInfo) ? data.cartInfo : [];
evaluate.value = statusInfo._type == 3 ? 3 : 0;
system_store.value = data.system_store || {};
if (Number(orderInfo.value.refund_status) !== 0) {
isGoodsReturn.value = true;
}
getOrderStatus();
@ -712,7 +723,7 @@ function getOrderInfo() {
// #ifndef H5
function copy() {
uni.setClipboardData({
data: orderInfo.value.order_id
data: orderInfo.value.order_id || orderInfo.value.order_no || ''
});
}
// #endif