diff --git a/src/pages-service/tickets/components/CredentialsView.vue b/src/pages-service/tickets/components/CredentialsView.vue
index f1d563e..943b809 100644
--- a/src/pages-service/tickets/components/CredentialsView.vue
+++ b/src/pages-service/tickets/components/CredentialsView.vue
@@ -1,54 +1,170 @@
-
+
-
-
- {{ order.productName || '景区产品' }}
- 订单号{{ order.orderNo || order.id || '-' }}
- {{ order.visitDate || '-' }} {{ order.timeSlot || '全天' }}
- 出行人 {{ firstTraveler }}
-
-
-
- 第三方票务凭证
- 请向检票人员出示此二维码
-
-
- 核验状态{{ order.gateCredential.status === 'issued' ? '可使用' : '处理中' }}
+
+
+
+
+ {{ order.productName || '景区产品' }}
+ {{ order.visitDate || '—' }} · {{ order.timeSlot || '全天' }}
+
+ {{ orderStatus.label }}
- {{ right.name }} × {{ right.quantity || 0 }}请在预约日期和时段内完成核验,截图可能无法正常使用。
-
-
-
- 自营园内权益
- 请向服务人员出示此二维码
-
-
- 核验状态{{ order.selfCredential.status === 'issued' ? '可使用' : '处理中' }}
+
+ 游客
+ {{ firstTraveler }} · {{ maskPhone(order.phone) }}
- {{ right.name }} × {{ right.quantity || 0 }}请在服务点出示二维码,权益核销后不可再次使用。
- 暂无可用凭证
-
+
+
+
+
+
+
+
+ 第三方票务系统出票
+ 第三方票务凭证
+
+
+ {{ gateStatusText }}
+
+
+
+
+
+
+
+
+ {{ right.name }} × {{ right.quantity || 0 }}
+ {{ entitlementMeta(right.status).label }}
+
+
+ 门票可凭此码或身份证入园;观光车按第三方票务规则使用
+
+
+
+
+
+
+
+
+
+ 园内工作人员扫码
+ 园内权益凭证
+
+
+ 自营凭证
+
+
+
+
+
+
+
+
+ {{ right.name }} × {{ right.quantity || 0 }}{{ right.unit || '份' }}
+ {{ entitlementMeta(right.status).label }}
+
+
+ 多个自营二销项目共用此码,员工扫码后选择项目核销
+
+
+
+
+
+
+ 暂无可用凭证
+ 出票完成后,二维码会展示在这里
+
-
+
+
+
+
+
+
diff --git a/src/pages-service/tickets/components/OrderDetail.vue b/src/pages-service/tickets/components/OrderDetail.vue
index bdd1e34..b946350 100644
--- a/src/pages-service/tickets/components/OrderDetail.vue
+++ b/src/pages-service/tickets/components/OrderDetail.vue
@@ -1,77 +1,159 @@
-
+
-
-
-
- {{ order.productName || '景区产品' }}
- {{ statusText }}
+
+
+
+ {{ order.productName }}
+ {{ statusMeta.label }}
-
- 订单号
- {{ order.orderNo || order.id || '-' }}
+
+ 订单号
+ {{ order.orderNo || order.id || '—' }}
-
- 使用信息
- 使用日期{{ order.visitDate || '-' }}
- 使用时段{{ order.timeSlot || '全天' }}
-
-
-
- 出行人
-
- {{ traveler.name || '-' }}
- {{ traveler.idType || '游客' }}
-
- 暂无出行人信息
-
- 联系人手机
- {{ order.phone || '-' }}
+
+ 使用信息
+
+
+ 使用日期
+ {{ order.visitDate }}
+
+
+ 使用时段
+ {{ order.timeSlot }}
+
-
- 权益与使用状态
-
- {{ right.name }}{{ fulfillmentText(right.fulfillment) }}
- {{ right.quantity || 0 }}份 · {{ rightStatus(right.status) }}
+
+ 出行人
+
+ {{ order.customerName || order.contactName || order.travelers?.[0]?.name || '—' }}
+ {{ travelerIdType }}
+
+
+ 联系手机
+ {{ order.phone || '—' }}
-
- 费用信息
- 产品金额¥{{ money(order.productAmount) }}
- 保险金额¥{{ money(order.insuranceAmount) }}
- 实付金额¥{{ money(order.paidAmount) }}
- 支付时间{{ order.paidAt || '-' }}
+
+ 权益与使用状态
+
+
+
+ {{ right.name }} × {{ right.quantity }}
+ {{ right.fulfillment === 'third_party' ? '第三方出票' : '自营权益码' }} · 退款金额 {{ money(right.refundAllocation) }}
+
+ {{ entitlementMeta(right.status).label }}
+
+ 支付完成后生成履约权益
+
-
- 退款规则
- 未使用的权益可按产品退款规则申请退款;已核销权益不支持退款,实际退款以审核结果为准。
+
+ 费用信息
+
+
+ 商品金额
+ {{ money(productAmount) }}
+
+
+ 出行保险
+ {{ insuranceText }}
+
+
+ 订单实付
+ {{ money(order.amount ?? order.paidAmount) }}
+
+
+
+ 支付时间
+ {{ order.paidAt }}
+
+
+
+
+
+
+
+ 退款规则
+ {{ refundRuleText }}
+
+
-
-
-
-
+
+
+
+
+
diff --git a/src/pages-service/tickets/components/RefundFlow.vue b/src/pages-service/tickets/components/RefundFlow.vue
index 77592ee..ff1496f 100644
--- a/src/pages-service/tickets/components/RefundFlow.vue
+++ b/src/pages-service/tickets/components/RefundFlow.vue
@@ -7,17 +7,22 @@
订单号{{ order.orderNo || order.id || '-' }}
退款状态待审核
-
+
+
+ {{ isPackage ? '套餐整单退款' : '按未使用权益退款' }}
+ {{ isPackage ? '套餐不支持拆项退款,本次将退回全部未使用权益对应的整单实付金额。' : '请选择需要退款的未使用权益,退款金额按购买时实际分摊金额计算。' }}
+
+
选择退款权益
-
-
+
+
{{ right.name }}{{ right.quantity || 0 }}份,可退 ¥{{ money((right.refundAllocation || 0) * (right.quantity || 0)) }}
当前没有可申请退款的权益
@@ -26,19 +31,19 @@
退款原因
-
+
{{ reason }}
- 预计退款金额¥{{ money(refundAmount) }}
+ 预计退款金额¥{{ money(refundAmount) }}
实际到账金额以审核结果为准
-
+
@@ -54,6 +59,7 @@ const props = defineProps({
const emit = defineEmits(['submit', 'detail', 'home'])
const selectedIds = ref([])
const selectedReason = ref('')
+const isPackage = computed(() => props.order?.refundMode === 'all_or_nothing')
const pendingRights = computed(() => (props.order?.entitlements || []).filter((right) => right.status === 'pending'))
const refundAmount = computed(() => calculateRefundAmount(props.order, selectedIds.value))
const money = (value) => Number(value || 0).toFixed(2)
diff --git a/src/pages-service/tickets/credentials.vue b/src/pages-service/tickets/credentials.vue
index 726f00e..631f9bc 100644
--- a/src/pages-service/tickets/credentials.vue
+++ b/src/pages-service/tickets/credentials.vue
@@ -2,7 +2,7 @@
diff --git a/src/pages-service/tickets/data.js b/src/pages-service/tickets/data.js
index 527e337..57d613a 100644
--- a/src/pages-service/tickets/data.js
+++ b/src/pages-service/tickets/data.js
@@ -13,6 +13,13 @@ export const ORDER_STATUS_META = {
exception: { label: '异常', className: 'border-rose-200 bg-rose-50 text-rose-700' },
}
+export const ENTITLEMENT_STATUS_META = {
+ pending: { label: '待使用', className: 'bg-blue-50 text-blue-700' },
+ used: { label: '已使用', className: 'bg-emerald-50 text-emerald-700' },
+ refunding: { label: '退款中', className: 'bg-orange-50 text-orange-700' },
+ refunded: { label: '已退款', className: 'bg-slate-100 text-slate-600' },
+}
+
export const SCENIC_SPOT = {
id: 'libo-xiaoqikong',
name: '荔波小七孔景区',
diff --git a/src/pages-service/tickets/model.test.mjs b/src/pages-service/tickets/model.test.mjs
index d477ad6..8afcbbd 100644
--- a/src/pages-service/tickets/model.test.mjs
+++ b/src/pages-service/tickets/model.test.mjs
@@ -16,7 +16,7 @@ import {
test('provides coherent default visitor ticket data', async () => {
const data = await import('./data.js')
- const { AVAILABLE_DATES, GROUPS, INITIAL_TRAVELERS, INSURANCE, ORDER_STATUS_META, PLACEHOLDER_IMAGE, PRODUCTS, REFUND_REASONS, SCENIC_SPOT } = data
+ const { AVAILABLE_DATES, ENTITLEMENT_STATUS_META, GROUPS, INITIAL_TRAVELERS, INSURANCE, ORDER_STATUS_META, PLACEHOLDER_IMAGE, PRODUCTS, REFUND_REASONS, SCENIC_SPOT } = data
const { INITIAL_ORDERS } = data
assert.equal(PLACEHOLDER_IMAGE, 'https://one-feel-bucket.oss-cn-guangzhou.aliyuncs.com/oneFeel/2082673896938569729.jpg')
@@ -86,6 +86,10 @@ test('provides coherent default visitor ticket data', async () => {
exception: '异常',
},
)
+ assert.deepEqual(
+ Object.fromEntries(Object.entries(ENTITLEMENT_STATUS_META).map(([status, meta]) => [status, meta.label])),
+ { pending: '待使用', used: '已使用', refunding: '退款中', refunded: '已退款' },
+ )
assert.equal(INITIAL_ORDERS.length, 11)
assert.deepEqual(
INITIAL_ORDERS.map(({ orderNo, productName, status, amount }) => [orderNo, productName, status, amount]),
diff --git a/src/pages-service/tickets/view-contract.test.mjs b/src/pages-service/tickets/view-contract.test.mjs
index a7d6180..3a68ace 100644
--- a/src/pages-service/tickets/view-contract.test.mjs
+++ b/src/pages-service/tickets/view-contract.test.mjs
@@ -94,6 +94,82 @@ test("order list matches the visitor prototype card layout and fields", async ()
assert.match(component, /type="right"/);
});
+test("order detail matches the visitor prototype sections and actions", async () => {
+ const component = await readTicketFile("components/OrderDetail.vue");
+ const page = await readTicketFile("order-detail.vue");
+
+ assert.match(component, /box-border flex w-full min-w-0 flex-col gap-3 p-4 pb-28/);
+ assert.match(component, /rounded-2xl bg-white p-4/);
+ assert.match(component, /ORDER_STATUS_META/);
+ assert.match(component, /rounded-full border px-2\.5 py-1 text-xs font-semibold/);
+ assert.match(component, /:class="statusMeta\.className"/);
+ assert.match(component, /order\.orderNo/);
+ assert.match(component, /使用信息/);
+ assert.match(component, /使用日期/);
+ assert.match(component, /使用时段/);
+ assert.match(component, /出行人/);
+ assert.match(component, /travelerIdType/);
+ assert.match(component, /联系手机/);
+ assert.match(component, /order\.customerName/);
+ assert.match(component, /order\.phone/);
+ assert.doesNotMatch(component, /当前状态|statusDescription| {
+ const component = await readTicketFile("components/CredentialsView.vue");
+ const page = await readTicketFile("credentials.vue");
+
+ assert.match(component, /import Qrcode from "@\/components\/Qrcode\/index\.vue"/);
+ assert.equal((component.match(/ {
+ const component = await readTicketFile("components/RefundFlow.vue");
+
+ assert.match(component, /套餐整单退款/);
+ assert.match(component, /套餐不支持拆项退款,本次将退回全部未使用权益对应的整单实付金额。/);
+ assert.match(component, /refundMode === 'all_or_nothing'/);
+ assert.match(component, /bg-orange-50/);
+ assert.match(component, /border-orange-400/);
+ assert.match(component, /border-orange-500 bg-orange-500/);
+ assert.match(component, /text-orange-600/);
+ assert.match(component, /bg-orange-500/);
+ assert.doesNotMatch(component, /#7c3aed|#faf5ff/);
+});
+
test("traveler manager matches the approved prototype details", async () => {
const component = await readTicketFile("components/TravelerManager.vue");
const page = await readTicketFile("travelers.vue");