feat(M08-A): 接入顾客端续费换房分享
This commit is contained in:
@@ -83,6 +83,14 @@ export class OrderManagementRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async customerRenew(actor: OrderActor, orderId: string, input: {
|
||||||
|
endAt: Date; pricingPolicy: PricingPolicy; reason: string;
|
||||||
|
}) {
|
||||||
|
return this.renewForActor(actor, orderId, input, async (connection, order) => {
|
||||||
|
await this.assertOwner(connection, actor.tenantId, order.id, actor.userId);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async changeRoom(actor: OrderActor, orderId: string, input: {
|
async changeRoom(actor: OrderActor, orderId: string, input: {
|
||||||
roomId: string; reason: string;
|
roomId: string; reason: string;
|
||||||
}) {
|
}) {
|
||||||
@@ -124,6 +132,17 @@ export class OrderManagementRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async customerChangeRoom(actor: OrderActor, orderId: string, input: {
|
||||||
|
roomId: string; reason: string;
|
||||||
|
}) {
|
||||||
|
return this.changeRoomForActor(actor, orderId, input, async (connection, order, target) => {
|
||||||
|
await this.assertOwner(connection, actor.tenantId, order.id, actor.userId);
|
||||||
|
if (target.storeId !== order.storeId) {
|
||||||
|
throw new OrderManagementError('ORDER_ROOM_CHANGE_STORE_INVALID');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async adjustTime(actor: OrderActor, orderId: string, input: {
|
async adjustTime(actor: OrderActor, orderId: string, input: {
|
||||||
startAt?: Date; endAt?: Date; reason: string;
|
startAt?: Date; endAt?: Date; reason: string;
|
||||||
}) {
|
}) {
|
||||||
@@ -220,6 +239,93 @@ export class OrderManagementRepository {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async renewForActor(
|
||||||
|
actor: OrderActor, orderId: string, input: {
|
||||||
|
endAt: Date; pricingPolicy: PricingPolicy; reason: string;
|
||||||
|
},
|
||||||
|
authorize: (connection: PoolConnection, order: OrderRow) => Promise<void>
|
||||||
|
) {
|
||||||
|
return this.transaction(async (connection) => {
|
||||||
|
const order = await this.loadOrder(connection, actor.tenantId, orderId);
|
||||||
|
await authorize(connection, order);
|
||||||
|
const duplicate = await this.duplicate(connection, actor, orderId);
|
||||||
|
if (duplicate) return this.duplicateResult(orderId, duplicate);
|
||||||
|
if (!['PAID', 'RESERVED', 'IN_PROGRESS'].includes(order.status)) {
|
||||||
|
throw new OrderManagementError('ORDER_RENEW_STATUS_INVALID');
|
||||||
|
}
|
||||||
|
if (input.endAt <= order.endAt) throw new OrderManagementError('ORDER_RENEW_END_INVALID');
|
||||||
|
await this.lockRooms(connection, actor.tenantId, [order.roomId]);
|
||||||
|
await this.assertAvailable(
|
||||||
|
connection, actor.tenantId, order.roomId, order.endAt, input.endAt, orderId
|
||||||
|
);
|
||||||
|
const unitPrice = await this.resolveUnitPrice(
|
||||||
|
connection, actor.tenantId, order, input.pricingPolicy
|
||||||
|
);
|
||||||
|
const amountDeltaCents = Math.ceil(
|
||||||
|
(input.endAt.getTime() - order.endAt.getTime()) / 3600000
|
||||||
|
) * unitPrice;
|
||||||
|
await connection.execute(
|
||||||
|
`UPDATE qipai_orders
|
||||||
|
SET end_at = ?, total_amount_cents = total_amount_cents + ?,
|
||||||
|
adjustment_amount_cents = adjustment_amount_cents + ?
|
||||||
|
WHERE tenant_id = ? AND id = ?`,
|
||||||
|
[input.endAt, amountDeltaCents, amountDeltaCents, actor.tenantId, orderId]
|
||||||
|
);
|
||||||
|
await connection.execute(
|
||||||
|
`UPDATE qipai_room_reservations
|
||||||
|
SET ends_at = ?, expires_at = GREATEST(expires_at, ?)
|
||||||
|
WHERE tenant_id = ? AND order_id = ?`,
|
||||||
|
[input.endAt, input.endAt, actor.tenantId, orderId]
|
||||||
|
);
|
||||||
|
return this.record(connection, actor, order, 'RENEW', input.reason, {
|
||||||
|
endAt: order.endAt
|
||||||
|
}, { endAt: input.endAt, pricingPolicy: input.pricingPolicy }, amountDeltaCents);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async changeRoomForActor(
|
||||||
|
actor: OrderActor, orderId: string, input: { roomId: string; reason: string },
|
||||||
|
authorize: (
|
||||||
|
connection: PoolConnection, order: OrderRow, target: RoomRow
|
||||||
|
) => Promise<void>
|
||||||
|
) {
|
||||||
|
return this.transaction(async (connection) => {
|
||||||
|
const order = await this.loadOrder(connection, actor.tenantId, orderId);
|
||||||
|
const duplicate = await this.duplicate(connection, actor, orderId);
|
||||||
|
if (duplicate) return this.duplicateResult(orderId, duplicate);
|
||||||
|
if (!['PENDING_PAYMENT', 'PAID', 'RESERVED', 'IN_PROGRESS'].includes(order.status)) {
|
||||||
|
throw new OrderManagementError('ORDER_ROOM_CHANGE_STATUS_INVALID');
|
||||||
|
}
|
||||||
|
if (input.roomId === order.roomId) throw new OrderManagementError('ORDER_ROOM_UNCHANGED');
|
||||||
|
await this.lockRooms(connection, actor.tenantId, [order.roomId, input.roomId]);
|
||||||
|
const target = await this.loadRoom(connection, actor.tenantId, input.roomId);
|
||||||
|
await authorize(connection, order, target);
|
||||||
|
await this.assertAvailable(
|
||||||
|
connection, actor.tenantId, target.id, order.startAt, order.endAt, orderId
|
||||||
|
);
|
||||||
|
const oldRoom = await this.loadRoom(connection, actor.tenantId, order.roomId);
|
||||||
|
const hours = Math.ceil((order.endAt.getTime() - order.startAt.getTime()) / 3600000);
|
||||||
|
const amountDeltaCents = hours * (target.basePriceCents - oldRoom.basePriceCents);
|
||||||
|
await connection.execute(
|
||||||
|
`UPDATE qipai_orders
|
||||||
|
SET store_id = ?, room_id = ?,
|
||||||
|
total_amount_cents = GREATEST(0, total_amount_cents + ?),
|
||||||
|
adjustment_amount_cents = adjustment_amount_cents + ?
|
||||||
|
WHERE tenant_id = ? AND id = ?`,
|
||||||
|
[target.storeId, target.id, amountDeltaCents, amountDeltaCents,
|
||||||
|
actor.tenantId, orderId]
|
||||||
|
);
|
||||||
|
await connection.execute(
|
||||||
|
`UPDATE qipai_room_reservations SET room_id = ?
|
||||||
|
WHERE tenant_id = ? AND order_id = ?`,
|
||||||
|
[target.id, actor.tenantId, orderId]
|
||||||
|
);
|
||||||
|
return this.record(connection, actor, order, 'CHANGE_ROOM', input.reason, {
|
||||||
|
storeId: order.storeId, roomId: order.roomId
|
||||||
|
}, { storeId: target.storeId, roomId: target.id }, amountDeltaCents);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private async loadOrder(connection: PoolConnection, tenantId: string, orderId: string) {
|
private async loadOrder(connection: PoolConnection, tenantId: string, orderId: string) {
|
||||||
const [rows] = await connection.execute<OrderRow[]>(
|
const [rows] = await connection.execute<OrderRow[]>(
|
||||||
`SELECT o.id, o.store_id AS storeId, o.room_id AS roomId, o.status,
|
`SELECT o.id, o.store_id AS storeId, o.room_id AS roomId, o.status,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { FastifyInstance, FastifyReply } from 'fastify';
|
import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import type { AuthRepository } from '../auth/auth-repository.js';
|
import type { AuthRepository } from '../auth/auth-repository.js';
|
||||||
import { authenticateAccessToken } from '../auth/authenticate.js';
|
import { authenticateAccessToken } from '../auth/authenticate.js';
|
||||||
@@ -27,7 +27,8 @@ const noteSchema = z.object({ note: z.string().min(1).max(512) }).strict();
|
|||||||
|
|
||||||
export interface OrderManagementRouteOptions {
|
export interface OrderManagementRouteOptions {
|
||||||
repository: Pick<OrderManagementRepository,
|
repository: Pick<OrderManagementRepository,
|
||||||
'renew' | 'changeRoom' | 'adjustTime' | 'note' | 'cancellationQuote'>;
|
'renew' | 'changeRoom' | 'adjustTime' | 'note' | 'cancellationQuote'
|
||||||
|
| 'customerRenew' | 'customerChangeRoom'>;
|
||||||
authRepository: Pick<AuthRepository, 'validateSession'>;
|
authRepository: Pick<AuthRepository, 'validateSession'>;
|
||||||
accessControl: { getAccessProfile(tenantId: string, userId: string): Promise<AccessProfile> };
|
accessControl: { getAccessProfile(tenantId: string, userId: string): Promise<AccessProfile> };
|
||||||
jwtSecret: string;
|
jwtSecret: string;
|
||||||
@@ -36,6 +37,34 @@ export interface OrderManagementRouteOptions {
|
|||||||
export async function registerOrderManagementRoutes(
|
export async function registerOrderManagementRoutes(
|
||||||
app: FastifyInstance, options: OrderManagementRouteOptions
|
app: FastifyInstance, options: OrderManagementRouteOptions
|
||||||
) {
|
) {
|
||||||
|
app.post('/app-api/orders/:orderId/renew', async (request, reply) => {
|
||||||
|
const auth = await authenticate(request.headers.authorization, options);
|
||||||
|
const params = paramsSchema.safeParse(request.params);
|
||||||
|
const body = renewSchema.safeParse(request.body);
|
||||||
|
if (!auth) return unauthorized(reply, request.traceId);
|
||||||
|
if (!params.success || !body.success) return invalid(reply, request.traceId);
|
||||||
|
const actor = customerActor(auth, request);
|
||||||
|
return handle(reply, request.traceId, async () => ({
|
||||||
|
code: 0,
|
||||||
|
data: await options.repository.customerRenew(actor, params.data.orderId, body.data),
|
||||||
|
traceId: request.traceId
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post('/app-api/orders/:orderId/change-room', async (request, reply) => {
|
||||||
|
const auth = await authenticate(request.headers.authorization, options);
|
||||||
|
const params = paramsSchema.safeParse(request.params);
|
||||||
|
const body = roomSchema.safeParse(request.body);
|
||||||
|
if (!auth) return unauthorized(reply, request.traceId);
|
||||||
|
if (!params.success || !body.success) return invalid(reply, request.traceId);
|
||||||
|
const actor = customerActor(auth, request);
|
||||||
|
return handle(reply, request.traceId, async () => ({
|
||||||
|
code: 0,
|
||||||
|
data: await options.repository.customerChangeRoom(actor, params.data.orderId, body.data),
|
||||||
|
traceId: request.traceId
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
app.get('/app-api/orders/:orderId/cancellation-quote', async (request, reply) => {
|
app.get('/app-api/orders/:orderId/cancellation-quote', async (request, reply) => {
|
||||||
const auth = await authenticate(request.headers.authorization, options);
|
const auth = await authenticate(request.headers.authorization, options);
|
||||||
const params = paramsSchema.safeParse(request.params);
|
const params = paramsSchema.safeParse(request.params);
|
||||||
@@ -82,6 +111,17 @@ export async function registerOrderManagementRoutes(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function customerActor(
|
||||||
|
auth: { tenantId: string; userId: string; access: AccessProfile },
|
||||||
|
request: FastifyRequest
|
||||||
|
): OrderActor {
|
||||||
|
return {
|
||||||
|
tenantId: auth.tenantId, userId: auth.userId, actorType: 'USER',
|
||||||
|
source: 'APP', traceId: request.traceId, ip: request.ip,
|
||||||
|
userAgent: String(request.headers['user-agent'] ?? ''), access: auth.access
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function authenticate(
|
async function authenticate(
|
||||||
authorization: string | undefined, options: OrderManagementRouteOptions
|
authorization: string | undefined, options: OrderManagementRouteOptions
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const token = signAccessToken({
|
|||||||
tid: '7', aid: '9', rv: 1
|
tid: '7', aid: '9', rv: 1
|
||||||
}, secret, 900);
|
}, secret, 900);
|
||||||
let called;
|
let called;
|
||||||
|
let customerCalled;
|
||||||
const authRepository = {
|
const authRepository = {
|
||||||
async validateSession() {
|
async validateSession() {
|
||||||
return {
|
return {
|
||||||
@@ -33,7 +34,15 @@ const app = await buildApp({
|
|||||||
called = { actor, orderId, body };
|
called = { actor, orderId, body };
|
||||||
return { orderId, adjustmentType: 'RENEW', amountDeltaCents: 1200 };
|
return { orderId, adjustmentType: 'RENEW', amountDeltaCents: 1200 };
|
||||||
},
|
},
|
||||||
|
async customerRenew(actor, orderId, body) {
|
||||||
|
customerCalled = { action: 'renew', actor, orderId, body };
|
||||||
|
return { orderId, adjustmentType: 'RENEW', amountDeltaCents: 1800 };
|
||||||
|
},
|
||||||
async changeRoom() { throw new Error('not called'); },
|
async changeRoom() { throw new Error('not called'); },
|
||||||
|
async customerChangeRoom(actor, orderId, body) {
|
||||||
|
customerCalled = { action: 'changeRoom', actor, orderId, body };
|
||||||
|
return { orderId, adjustmentType: 'CHANGE_ROOM', amountDeltaCents: -500 };
|
||||||
|
},
|
||||||
async adjustTime() { throw new Error('not called'); },
|
async adjustTime() { throw new Error('not called'); },
|
||||||
async note() { throw new Error('not called'); },
|
async note() { throw new Error('not called'); },
|
||||||
async cancellationQuote() {
|
async cancellationQuote() {
|
||||||
@@ -71,6 +80,37 @@ assert.equal(called.orderId, '31');
|
|||||||
assert.equal(called.actor.traceId, 'm04c-renew-route');
|
assert.equal(called.actor.traceId, 'm04c-renew-route');
|
||||||
assert.equal(called.body.pricingPolicy, 'LOCKED');
|
assert.equal(called.body.pricingPolicy, 'LOCKED');
|
||||||
|
|
||||||
|
const customerRenewed = await app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/app-api/orders/31/renew',
|
||||||
|
headers: { authorization: `Bearer ${token}`, 'x-trace-id': 'm08a-customer-renew' },
|
||||||
|
payload: {
|
||||||
|
endAt: new Date(Date.now() + 10800000).toISOString(),
|
||||||
|
pricingPolicy: 'CURRENT',
|
||||||
|
reason: 'miniapp renew'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assert.equal(customerRenewed.statusCode, 200);
|
||||||
|
assert.equal(customerCalled.action, 'renew');
|
||||||
|
assert.equal(customerCalled.actor.source, 'APP');
|
||||||
|
assert.equal(customerCalled.actor.traceId, 'm08a-customer-renew');
|
||||||
|
assert.equal(customerCalled.body.pricingPolicy, 'CURRENT');
|
||||||
|
|
||||||
|
const customerChanged = await app.inject({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/app-api/orders/31/change-room',
|
||||||
|
headers: { authorization: `Bearer ${token}`, 'x-trace-id': 'm08a-customer-change-room' },
|
||||||
|
payload: {
|
||||||
|
roomId: '32',
|
||||||
|
reason: 'miniapp change room'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assert.equal(customerChanged.statusCode, 200);
|
||||||
|
assert.equal(customerCalled.action, 'changeRoom');
|
||||||
|
assert.equal(customerCalled.actor.source, 'APP');
|
||||||
|
assert.equal(customerCalled.orderId, '31');
|
||||||
|
assert.equal(customerCalled.body.roomId, '32');
|
||||||
|
|
||||||
const quote = await app.inject({
|
const quote = await app.inject({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: '/app-api/orders/31/cancellation-quote',
|
url: '/app-api/orders/31/cancellation-quote',
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ Page({
|
|||||||
order: null,
|
order: null,
|
||||||
history: [],
|
history: [],
|
||||||
cancellation: null,
|
cancellation: null,
|
||||||
|
renewMinutes: 60,
|
||||||
|
changeRoomId: '',
|
||||||
|
shareTtlMinutes: 30,
|
||||||
|
shareToken: '',
|
||||||
|
sharePermissions: ['VIEW_ROOM', 'OPEN_DOOR'],
|
||||||
},
|
},
|
||||||
|
|
||||||
onLoad(options) {
|
onLoad(options) {
|
||||||
@@ -79,6 +84,81 @@ Page({
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onRenewMinutesInput(event) {
|
||||||
|
this.setData({ renewMinutes: Number(event.detail.value || 0) })
|
||||||
|
},
|
||||||
|
|
||||||
|
onChangeRoomInput(event) {
|
||||||
|
this.setData({ changeRoomId: String(event.detail.value || '').trim() })
|
||||||
|
},
|
||||||
|
|
||||||
|
onShareTtlInput(event) {
|
||||||
|
this.setData({ shareTtlMinutes: Number(event.detail.value || 0) })
|
||||||
|
},
|
||||||
|
|
||||||
|
async renewOrder() {
|
||||||
|
if (!this.data.orderId || !this.data.order) return
|
||||||
|
const minutes = Number(this.data.renewMinutes || 0)
|
||||||
|
if (!Number.isFinite(minutes) || minutes < 15) {
|
||||||
|
this.setData({ errorMessage: '续费时长至少 15 分钟' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const currentEnd = new Date(this.data.order.endAt)
|
||||||
|
const nextEnd = new Date(currentEnd.getTime() + minutes * 60000)
|
||||||
|
await this.withOrderRequest(async () => {
|
||||||
|
const response = await request(`/orders/${encodeURIComponent(this.data.orderId)}/renew`, {
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
endAt: nextEnd.toISOString(),
|
||||||
|
pricingPolicy: 'CURRENT',
|
||||||
|
reason: `顾客小程序续费 ${minutes} 分钟`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await this.loadOrder(this.data.orderId)
|
||||||
|
this.setData({
|
||||||
|
successMessage: `续费已提交,补差 ${cents(response.data.amountDeltaCents)}`,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
async changeRoom() {
|
||||||
|
if (!this.data.orderId || !this.data.changeRoomId) {
|
||||||
|
this.setData({ errorMessage: '请输入目标房间 ID' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await this.withOrderRequest(async () => {
|
||||||
|
const response = await request(`/orders/${encodeURIComponent(this.data.orderId)}/change-room`, {
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
roomId: this.data.changeRoomId,
|
||||||
|
reason: '顾客小程序换房',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await this.loadOrder(this.data.orderId)
|
||||||
|
this.setData({
|
||||||
|
successMessage: `换房已提交,差价 ${cents(response.data.amountDeltaCents)}`,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
async createShare() {
|
||||||
|
if (!this.data.orderId) return
|
||||||
|
const ttlMinutes = Math.min(Math.max(Number(this.data.shareTtlMinutes || 30), 5), 1440)
|
||||||
|
await this.withOrderRequest(async () => {
|
||||||
|
const response = await request(`/orders/${encodeURIComponent(this.data.orderId)}/shares`, {
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
permissions: this.data.sharePermissions,
|
||||||
|
ttlMinutes,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
this.setData({
|
||||||
|
shareToken: response.data.token,
|
||||||
|
successMessage: `分享口令已生成,${response.data.expiresInMinutes} 分钟内有效`,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
async withOrderRequest(work) {
|
async withOrderRequest(work) {
|
||||||
this.setData({ loading: true, errorMessage: '', successMessage: '' })
|
this.setData({ loading: true, errorMessage: '', successMessage: '' })
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<view wx:if="{{successMessage}}" class="success">{{successMessage}}</view>
|
<view wx:if="{{successMessage}}" class="success">{{successMessage}}</view>
|
||||||
|
|
||||||
<view wx:if="{{order}}" class="card">
|
<view wx:if="{{order}}" class="card">
|
||||||
<view class="order-title">{{order.storeName}} · {{order.roomName}}</view>
|
<view class="order-title">{{order.storeName}} / {{order.roomName}}</view>
|
||||||
<view class="muted">订单号:{{order.orderNo}}</view>
|
<view class="muted">订单号:{{order.orderNo}}</view>
|
||||||
<view class="muted">房间:{{order.roomNo}}</view>
|
<view class="muted">房间:{{order.roomNo}}</view>
|
||||||
<view class="muted">开始:{{order.startText}}</view>
|
<view class="muted">开始:{{order.startText}}</view>
|
||||||
@@ -26,6 +26,34 @@
|
|||||||
<button loading="{{loading}}" bindtap="cancelOrder">取消订单</button>
|
<button loading="{{loading}}" bindtap="cancelOrder">取消订单</button>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
|
<view class="card">
|
||||||
|
<view class="section-title">续费</view>
|
||||||
|
<view class="field">
|
||||||
|
<text>分钟</text>
|
||||||
|
<input type="number" value="{{renewMinutes}}" bindinput="onRenewMinutesInput" />
|
||||||
|
</view>
|
||||||
|
<button loading="{{loading}}" bindtap="renewOrder">提交续费</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card">
|
||||||
|
<view class="section-title">换房</view>
|
||||||
|
<view class="field">
|
||||||
|
<text>目标房间 ID</text>
|
||||||
|
<input type="number" value="{{changeRoomId}}" bindinput="onChangeRoomInput" />
|
||||||
|
</view>
|
||||||
|
<button loading="{{loading}}" bindtap="changeRoom">提交换房</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view class="card">
|
||||||
|
<view class="section-title">分享</view>
|
||||||
|
<view class="field">
|
||||||
|
<text>有效分钟</text>
|
||||||
|
<input type="number" value="{{shareTtlMinutes}}" bindinput="onShareTtlInput" />
|
||||||
|
</view>
|
||||||
|
<button loading="{{loading}}" bindtap="createShare">生成分享口令</button>
|
||||||
|
<view wx:if="{{shareToken}}" class="token">{{shareToken}}</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<view wx:if="{{cancellation}}" class="card">
|
<view wx:if="{{cancellation}}" class="card">
|
||||||
<view class="section-title">取消测算</view>
|
<view class="section-title">取消测算</view>
|
||||||
<view class="muted">是否允许:{{cancellation.allowed ? '允许' : '不允许'}}</view>
|
<view class="muted">是否允许:{{cancellation.allowed ? '允许' : '不允许'}}</view>
|
||||||
@@ -34,7 +62,7 @@
|
|||||||
<view class="muted">免费取消分钟线:{{cancellation.cutoffMinutes}}</view>
|
<view class="muted">免费取消分钟线:{{cancellation.cutoffMinutes}}</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view class="section-title">状态历史</view>
|
<view class="section-title history-title">状态历史</view>
|
||||||
<view wx:for="{{history}}" wx:key="id" class="history-item">
|
<view wx:for="{{history}}" wx:key="id" class="history-item">
|
||||||
<view>{{item.action}}:{{item.fromStatus || '-'}} → {{item.toStatus}}</view>
|
<view>{{item.action}}:{{item.fromStatus || '-'}} → {{item.toStatus}}</view>
|
||||||
<view class="muted">{{item.createdText}}</view>
|
<view class="muted">{{item.createdText}}</view>
|
||||||
|
|||||||
@@ -59,6 +59,41 @@
|
|||||||
flex: 1 1 220rpx;
|
flex: 1 1 220rpx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16rpx;
|
||||||
|
margin: 20rpx 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field text {
|
||||||
|
width: 180rpx;
|
||||||
|
color: #475467;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field input {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 72rpx;
|
||||||
|
padding: 0 20rpx;
|
||||||
|
border: 1rpx solid #d0d5dd;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token {
|
||||||
|
margin-top: 16rpx;
|
||||||
|
padding: 16rpx;
|
||||||
|
word-break: break-all;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
background: #f2f4f7;
|
||||||
|
color: #344054;
|
||||||
|
font-size: 24rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history-title {
|
||||||
|
margin-top: 28rpx;
|
||||||
|
}
|
||||||
|
|
||||||
.success {
|
.success {
|
||||||
margin: 16rpx 0;
|
margin: 16rpx 0;
|
||||||
color: #17823b;
|
color: #17823b;
|
||||||
|
|||||||
@@ -51,7 +51,10 @@ for (const route of [
|
|||||||
'/history',
|
'/history',
|
||||||
'/cancellation-quote',
|
'/cancellation-quote',
|
||||||
'/cancel',
|
'/cancel',
|
||||||
'/open-door'
|
'/open-door',
|
||||||
|
'/renew',
|
||||||
|
'/change-room',
|
||||||
|
'/shares'
|
||||||
]) {
|
]) {
|
||||||
assert.match(orders, new RegExp(route.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
assert.match(orders, new RegExp(route.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user