@@ -247,6 +257,7 @@ import CleaningStatisticsPanel from './components/CleaningStatisticsPanel.vue';
import CleanersPanel from './components/CleanersPanel.vue';
import CleaningFieldHandoffPanel from './components/CleaningFieldHandoffPanel.vue';
import OperationsOverviewPanel from './components/OperationsOverviewPanel.vue';
+import StoresRoomsPanel from './components/StoresRoomsPanel.vue';
import {
ApiError,
assignCleaningTask,
@@ -283,7 +294,7 @@ import { money } from './format';
const savedToken = ref(localStorage.getItem('qipai.admin.token') || '');
const tokenDraft = ref(savedToken.value);
-const activeModule = ref<'overview' | 'cleaning'>('overview');
+const activeModule = ref<'overview' | 'stores' | 'cleaning'>('overview');
const activeTab = ref('tasks');
const lastError = ref('');
const lastMessage = ref('');
@@ -292,6 +303,11 @@ const settlementStatus = ref
('CONFIRMED');
const cleanerStatus = ref('ACTIVE');
const cleanerSearch = ref('');
const session = computed(() => ({ token: savedToken.value }));
+const activeModuleMeta = computed(() => ({
+ overview: { stage: 'M08-D', title: '平台运营总览' },
+ stores: { stage: 'M08-D', title: '门店与房间管理' },
+ cleaning: { stage: 'M08-B', title: '保洁任务与结算' }
+})[activeModule.value]);
const loading = reactive({ tasks: false, settlements: false, statistics: false, cleaners: false });
const tasks = reactive>({ items: [], total: 0, page: 1, pageSize: 20 });
const settlements = reactive>({
diff --git a/admin/src/api.ts b/admin/src/api.ts
index 1d595a2..0897dfa 100644
--- a/admin/src/api.ts
+++ b/admin/src/api.ts
@@ -6,6 +6,7 @@ import type {
CleaningTaskEvent,
CleaningTaskMember,
BusinessStatistics,
+ ManagedRoom,
ManagedStore,
ManagedUser,
PageResult,
@@ -14,6 +15,10 @@ import type {
SettlementStatus,
TaskStatus,
TransferMode,
+ RoomConfigurationStatus,
+ RoomInput,
+ RoomOperationalStatus,
+ StoreInput,
UserStatus,
WechatTransferPreflight
} from './types';
@@ -38,6 +43,71 @@ export function listManagedStores(session: ApiSession) {
return request(session, '/stores');
}
+export function createManagedStore(session: ApiSession, input: StoreInput) {
+ return request<{ storeId: string }>(session, '/stores', {
+ method: 'POST', body: JSON.stringify(input)
+ });
+}
+
+export function updateManagedStore(session: ApiSession, storeId: string, input: StoreInput) {
+ return request<{ storeId: string }>(session, `/stores/${storeId}`, {
+ method: 'PUT', body: JSON.stringify(input)
+ });
+}
+
+export function archiveManagedStore(session: ApiSession, storeId: string) {
+ return request<{ storeId: string; archived: boolean }>(session, `/stores/${storeId}`, {
+ method: 'DELETE'
+ });
+}
+
+export function listManagedRooms(session: ApiSession, storeId: string) {
+ return request(session, `/stores/${storeId}/rooms`);
+}
+
+export function createManagedRoom(session: ApiSession, input: RoomInput) {
+ return request<{ roomId: string }>(session, '/rooms', {
+ method: 'POST', body: JSON.stringify(input)
+ });
+}
+
+export function updateManagedRoom(session: ApiSession, roomId: string, input: RoomInput) {
+ return request<{ roomId: string }>(session, `/rooms/${roomId}`, {
+ method: 'PUT', body: JSON.stringify(input)
+ });
+}
+
+export function updateManagedRoomStatus(
+ session: ApiSession,
+ roomId: string,
+ input: {
+ storeId: string;
+ configurationStatus?: RoomConfigurationStatus;
+ operationalStatus?: RoomOperationalStatus;
+ reason: string;
+ }
+) {
+ return request<{ roomId: string }>(session, `/rooms/${roomId}/status`, {
+ method: 'PATCH', body: JSON.stringify(input)
+ });
+}
+
+export function archiveManagedRoom(session: ApiSession, roomId: string, storeId: string) {
+ return request<{ roomId: string; archived: boolean }>(
+ session, `/rooms/${roomId}?${new URLSearchParams({ storeId })}`, { method: 'DELETE' }
+ );
+}
+
+export function addManagedRoomDisabledPeriod(
+ session: ApiSession,
+ roomId: string,
+ input: { storeId: string; startsAt: string; endsAt: string; reason: string }
+) {
+ return request<{ disabledPeriodId: string }>(session, `/rooms/${roomId}/disabled-periods`, {
+ method: 'POST', body: JSON.stringify(input)
+ });
+}
+
export function getBusinessStatistics(
session: ApiSession,
input: { storeId: string; from: string; to: string }
diff --git a/admin/src/components/StoresRoomsPanel.vue b/admin/src/components/StoresRoomsPanel.vue
new file mode 100644
index 0000000..1feef73
--- /dev/null
+++ b/admin/src/components/StoresRoomsPanel.vue
@@ -0,0 +1,545 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 联系电话{{ selectedStore.contactPhone || '-' }}
+ 时区{{ selectedStore.timezone }}
+ 访客 Wi-Fi{{ wifiSummary(selectedStore) }}
+ 通知地址{{ selectedStore.notificationUrl || '-' }}
+
+
+
+ 营业时段
+
+
+ {{ weekdayLabel(hour.weekday) }}
+ {{ hour.isClosed ? '休息' : `${minuteLabel(hour.openMinute)}–${minuteLabel(hour.closeMinute)}` }}
+
+
+
+
+
+
+
+
+
+
+ {{ row.roomNo }} · {{ row.name }}
+ {{ row.categoryName }} · {{ row.capacity }} 人
+
+
+
+
+
+
+ {{ money(row.basePriceCents) }}/时段
+ 节假日 {{ money(row.holidayPriceCents) }}
+
+
+
+
+
+
+
+ {{ row.configurationStatus === 'ENABLED' ? '已启用' : '已停用' }}
+
+
+ {{ roomStatusLabel(row.operationalStatus) }}
+
+
+
+
+
+
+
+ {{ tag }}
+ -
+
+
+
+
+
+
+ 编辑
+ 改状态
+ 停用时段
+ 归档
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 每周营业时间
+
+ {{ weekdayLabel(hour.weekday) }}
+
+
+
+
+
+
+ 取消
+ 保存门店
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 取消
+ 保存房间
+
+
+
+
+
+
+
+
+
+ 取消确认变更
+
+
+
+
+
+
+
+
+ 取消保存时段
+
+
+
+
+
diff --git a/admin/src/styles.css b/admin/src/styles.css
index aeff099..c5ef9a9 100644
--- a/admin/src/styles.css
+++ b/admin/src/styles.css
@@ -1157,6 +1157,174 @@ textarea {
white-space: nowrap;
}
+.stores-page {
+ display: grid;
+ gap: 14px;
+}
+
+.store-toolbar h3,
+.store-detail-header h3,
+.rooms-toolbar h3 {
+ margin: 0;
+}
+
+.store-layout {
+ display: grid;
+ grid-template-columns: minmax(230px, 300px) minmax(0, 1fr);
+ min-height: 640px;
+}
+
+.store-list {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding: 12px;
+ border-right: 1px solid #e6ebf2;
+ background: #f8fafc;
+}
+
+.store-card {
+ display: grid;
+ gap: 5px;
+ width: 100%;
+ padding: 12px;
+ color: #526177;
+ text-align: left;
+ background: #fff;
+ border: 1px solid #dfe6ee;
+ border-radius: 9px;
+ cursor: pointer;
+}
+
+.store-card:hover,
+.store-card.active {
+ border-color: #2878ff;
+ box-shadow: 0 4px 16px rgba(40, 120, 255, 0.1);
+}
+
+.store-card.active {
+ background: #f1f6ff;
+}
+
+.store-card-title {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ color: #18212f;
+}
+
+.store-card small {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.store-detail {
+ min-width: 0;
+ padding: 18px;
+}
+
+.store-detail-empty {
+ min-height: 420px;
+}
+
+.store-detail-header,
+.rooms-toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 14px;
+}
+
+.store-detail-header p:not(.section-kicker) {
+ margin: 5px 0 0;
+ color: #69788c;
+}
+
+.store-summary-grid {
+ margin-top: 16px;
+}
+
+.store-summary-grid strong {
+ overflow: hidden;
+ font-size: 13px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.business-hours-board {
+ display: grid;
+ gap: 9px;
+ margin: 14px 0 22px;
+ padding: 12px;
+ border: 1px solid #e1e8f0;
+ border-radius: 8px;
+ background: #f8fafc;
+}
+
+.hours-chip-list {
+ display: grid;
+ grid-template-columns: repeat(7, minmax(76px, 1fr));
+ gap: 6px;
+}
+
+.hours-chip-list span {
+ display: grid;
+ gap: 3px;
+ padding: 7px;
+ color: #60708a;
+ font-size: 11px;
+ text-align: center;
+ background: #fff;
+ border: 1px solid #e1e8f0;
+ border-radius: 6px;
+}
+
+.hours-chip-list b {
+ color: #243247;
+ font-size: 11px;
+}
+
+.rooms-toolbar {
+ margin-bottom: 12px;
+ padding-top: 16px;
+ border-top: 1px solid #e6ebf2;
+}
+
+.asset-form {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 0 14px;
+}
+
+.asset-form .el-form-item {
+ min-width: 0;
+}
+
+.asset-form .el-input-number,
+.asset-form .el-select {
+ width: 100%;
+}
+
+.form-span-2 {
+ grid-column: span 2;
+}
+
+.hours-editor {
+ display: grid;
+ gap: 8px;
+ padding-top: 12px;
+ border-top: 1px solid #e6ebf2;
+}
+
+.hours-editor-row {
+ display: grid;
+ grid-template-columns: 44px 126px minmax(120px, 1fr) minmax(120px, 1fr);
+ gap: 8px;
+ align-items: center;
+}
+
.el-button {
border-radius: 8px;
}
@@ -1241,6 +1409,21 @@ textarea {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
+ .store-layout {
+ grid-template-columns: 1fr;
+ }
+
+ .store-list {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ border-right: 0;
+ border-bottom: 1px solid #e6ebf2;
+ }
+
+ .hours-chip-list {
+ grid-template-columns: repeat(4, minmax(76px, 1fr));
+ }
+
.trend-row {
grid-template-columns: 92px minmax(80px, 1fr);
}
@@ -1306,6 +1489,41 @@ textarea {
flex-direction: column;
}
+ .store-list {
+ grid-template-columns: 1fr;
+ }
+
+ .store-detail {
+ padding: 12px;
+ }
+
+ .store-detail-header,
+ .rooms-toolbar {
+ align-items: stretch;
+ flex-direction: column;
+ }
+
+ .store-summary-grid,
+ .asset-form {
+ grid-template-columns: 1fr;
+ }
+
+ .form-span-2 {
+ grid-column: span 1;
+ }
+
+ .hours-chip-list {
+ grid-template-columns: repeat(2, minmax(76px, 1fr));
+ }
+
+ .hours-editor-row {
+ grid-template-columns: 40px 1fr;
+ }
+
+ .hours-editor-row .el-select {
+ grid-column: span 2;
+ }
+
.toolbar-actions {
justify-content: flex-end;
}
diff --git a/admin/src/types.ts b/admin/src/types.ts
index 350069b..a9f5bc4 100644
--- a/admin/src/types.ts
+++ b/admin/src/types.ts
@@ -28,10 +28,76 @@ export interface ManagedStore {
address: string;
city: string;
district: string;
+ longitude: number | null;
+ latitude: number | null;
+ contactPhone: string;
businessStatus: 'OPEN' | 'CLOSED' | 'SUSPENDED';
timezone: string;
+ wifiSsid: string;
+ wifiConfigured: boolean;
+ notificationUrl: string;
+ sortOrder: number;
+ businessHours: StoreBusinessHour[];
}
+export interface StoreBusinessHour {
+ weekday: number;
+ openMinute: number;
+ closeMinute: number;
+ isClosed: boolean;
+}
+
+export interface StoreInput {
+ name: string;
+ address: string;
+ city: string;
+ district: string;
+ longitude?: number | null;
+ latitude?: number | null;
+ contactPhone: string;
+ timezone: string;
+ businessStatus: ManagedStore['businessStatus'];
+ wifiSsid: string;
+ wifiPassword?: string;
+ notificationUrl: string;
+ sortOrder: number;
+ businessHours: StoreBusinessHour[];
+}
+
+export type RoomConfigurationStatus = 'ENABLED' | 'DISABLED';
+export type RoomOperationalStatus =
+ | 'AVAILABLE'
+ | 'MAINTENANCE'
+ | 'RESERVED'
+ | 'IN_USE'
+ | 'CLEANING_REQUIRED';
+
+export interface ManagedRoom {
+ id: string;
+ storeId: string;
+ categoryName: string;
+ name: string;
+ roomNo: string;
+ capacity: number;
+ basePriceCents: number;
+ weekdayPriceCents: number;
+ holidayPriceCents: number;
+ overnightPriceCents: number;
+ fullDayPriceCents: number;
+ minimumSpendCents: number;
+ depositCents: number;
+ minimumMinutes: number;
+ maxAdvanceStartMinutes: number;
+ maxAdvanceDays: number;
+ configurationStatus: RoomConfigurationStatus;
+ operationalStatus: RoomOperationalStatus;
+ tags: string[];
+ images: string[];
+ sortOrder: number;
+}
+
+export type RoomInput = Omit;
+
export interface BusinessStatistics {
storeId: string;
from: string;
diff --git a/backend/src/routes/store-room-management.ts b/backend/src/routes/store-room-management.ts
index e13f027..dc627a4 100644
--- a/backend/src/routes/store-room-management.ts
+++ b/backend/src/routes/store-room-management.ts
@@ -29,7 +29,7 @@ const storeSchema = z.object({
timezone: z.string().trim().min(1).max(64).default('Asia/Shanghai'),
businessStatus: z.enum(['OPEN', 'CLOSED', 'SUSPENDED']).default('OPEN'),
wifiSsid: z.string().trim().max(128).default(''),
- wifiPassword: z.string().max(255).default(''),
+ wifiPassword: z.string().max(255).optional(),
notificationUrl: z.union([z.string().url(), z.literal('')]).default(''),
sortOrder: z.number().int().min(-100000).max(100000).default(0),
businessHours: hoursSchema.default([])
diff --git a/backend/src/stores/store-room-repository.ts b/backend/src/stores/store-room-repository.ts
index c669ba5..6890fd3 100644
--- a/backend/src/stores/store-room-repository.ts
+++ b/backend/src/stores/store-room-repository.ts
@@ -13,7 +13,7 @@ export interface StoreInput {
timezone: string;
businessStatus: 'OPEN' | 'CLOSED' | 'SUSPENDED';
wifiSsid: string;
- wifiPassword: string;
+ wifiPassword?: string;
notificationUrl: string;
sortOrder: number;
businessHours: Array<{
@@ -60,7 +60,10 @@ interface StoreRow extends RowDataPacket {
id: string; name: string; address: string; city: string; district: string;
longitude: string | null; latitude: string | null;
contactPhone: string; timezone: string; businessStatus: string; wifiSsid: string;
- notificationUrl: string; sortOrder: number;
+ wifiConfigured: number; notificationUrl: string; sortOrder: number;
+}
+interface StoreBusinessHourRow extends RowDataPacket {
+ storeId: string; weekday: number; openMinute: number; closeMinute: number; isClosed: number;
}
interface RoomRow extends RowDataPacket {
id: string; storeId: string; categoryName: string; name: string; roomNo: string; capacity: number;
@@ -85,17 +88,42 @@ export class StoreRoomRepository {
`SELECT s.id, s.name, s.address, s.city, s.district, s.longitude, s.latitude,
s.contact_phone AS contactPhone, s.timezone,
s.business_status AS businessStatus, s.wifi_ssid AS wifiSsid,
+ CASE WHEN s.wifi_password <> '' THEN 1 ELSE 0 END AS wifiConfigured,
s.notification_url AS notificationUrl, s.sort_order AS sortOrder
FROM qipai_stores s
WHERE s.tenant_id = ? AND s.deleted_at IS NULL AND ${scope.sql}
ORDER BY s.sort_order, s.id`,
[actor.tenantId, ...scope.params]
);
+ if (rows.length === 0) return [];
+ const storeIds = rows.map((row) => String(row.id));
+ const [hourRows] = await this.pool.execute(
+ `SELECT store_id AS storeId, weekday, open_minute AS openMinute,
+ close_minute AS closeMinute, is_closed AS isClosed
+ FROM qipai_store_business_hours
+ WHERE tenant_id = ? AND store_id IN (${storeIds.map(() => '?').join(',')})
+ ORDER BY store_id, weekday`,
+ [actor.tenantId, ...storeIds]
+ );
+ const hoursByStore = new Map();
+ for (const hour of hourRows) {
+ const storeId = String(hour.storeId);
+ const items = hoursByStore.get(storeId) ?? [];
+ items.push({
+ weekday: Number(hour.weekday),
+ openMinute: Number(hour.openMinute),
+ closeMinute: Number(hour.closeMinute),
+ isClosed: Boolean(hour.isClosed)
+ });
+ hoursByStore.set(storeId, items);
+ }
return rows.map((row) => ({
...row,
id: String(row.id),
longitude: row.longitude === null ? null : Number(row.longitude),
- latitude: row.latitude === null ? null : Number(row.latitude)
+ latitude: row.latitude === null ? null : Number(row.latitude),
+ wifiConfigured: Boolean(row.wifiConfigured),
+ businessHours: hoursByStore.get(String(row.id)) ?? []
}));
}
@@ -109,7 +137,7 @@ export class StoreRoomRepository {
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[actor.tenantId, input.name, input.address, input.city, input.district, input.longitude ?? null,
input.latitude ?? null, input.contactPhone, input.timezone, input.businessStatus,
- input.wifiSsid, input.wifiPassword, input.notificationUrl, input.sortOrder]
+ input.wifiSsid, input.wifiPassword ?? '', input.notificationUrl, input.sortOrder]
);
const storeId = String(result.insertId);
await this.replaceHours(connection, actor.tenantId, storeId, input.businessHours);
@@ -125,12 +153,12 @@ export class StoreRoomRepository {
`UPDATE qipai_stores SET name = ?, address = ?, city = ?, district = ?,
longitude = ?, latitude = ?,
contact_phone = ?, timezone = ?, business_status = ?, wifi_ssid = ?,
- wifi_password = ?, notification_url = ?, sort_order = ?
+ wifi_password = COALESCE(?, wifi_password), notification_url = ?, sort_order = ?
WHERE tenant_id = ? AND id = ?`,
[input.name, input.address, input.city, input.district,
input.longitude ?? null, input.latitude ?? null,
input.contactPhone, input.timezone, input.businessStatus, input.wifiSsid,
- input.wifiPassword, input.notificationUrl, input.sortOrder, actor.tenantId, storeId]
+ input.wifiPassword ?? null, input.notificationUrl, input.sortOrder, actor.tenantId, storeId]
);
await this.replaceHours(connection, actor.tenantId, storeId, input.businessHours);
await this.audit(connection, actor, 'STORE_UPDATED', 'STORE', storeId);
diff --git a/backend/tests/store-room.test.mjs b/backend/tests/store-room.test.mjs
index c8055f7..5124298 100644
--- a/backend/tests/store-room.test.mjs
+++ b/backend/tests/store-room.test.mjs
@@ -18,11 +18,18 @@ const storeActor = {
};
const repository = new StoreRoomRepository({
async execute(sql) {
- if (sql.includes('FROM qipai_stores')) return [[{ id: 11, name: 'A', longitude: null, latitude: null }], []];
+ if (sql.includes('FROM qipai_stores')) return [[{
+ id: 11, name: 'A', longitude: null, latitude: null, wifiConfigured: 1
+ }], []];
+ if (sql.includes('FROM qipai_store_business_hours')) return [[{
+ storeId: 11, weekday: 1, openMinute: 540, closeMinute: 1320, isClosed: 0
+ }], []];
return [[], []];
}
});
assert.equal((await repository.listStores(storeActor))[0].id, '11');
+assert.equal((await repository.listStores(storeActor))[0].wifiConfigured, true);
+assert.equal((await repository.listStores(storeActor))[0].businessHours[0].openMinute, 540);
assert.deepEqual(await repository.listRooms({
...storeActor,
access: { roles: ['STAFF'], capabilities: ['store.operation.read'], storeIds: ['11'] }
@@ -39,6 +46,7 @@ const token = signAccessToken({
}, secret, 900);
let roomInput;
let roomStatusInput;
+let storeInput;
let currentAccess = tenantActor.access;
const app = await buildApp({
storeRoom: {
@@ -59,7 +67,7 @@ const app = await buildApp({
repository: {
async listStores() { return []; },
async createStore() { return { storeId: '11' }; },
- async updateStore() { return { storeId: '11' }; },
+ async updateStore(_actor, _storeId, input) { storeInput = input; return { storeId: '11' }; },
async archiveStore() { return { storeId: '11', archived: true }; },
async listRooms() { return []; },
async createRoom(_actor, input) { roomInput = input; return { roomId: '31' }; },
@@ -97,6 +105,18 @@ const managementStores = await app.inject({
headers: { authorization: `Bearer ${token}` }
});
assert.equal(managementStores.statusCode, 200);
+const updatedStore = await app.inject({
+ method: 'PUT', url: '/admin-api/stores/11',
+ headers: { authorization: `Bearer ${token}` },
+ payload: {
+ name: 'A 店', address: '测试路 1 号', city: '上海', district: '浦东新区',
+ contactPhone: '13800000000', timezone: 'Asia/Shanghai', businessStatus: 'OPEN',
+ wifiSsid: 'qipai-guest', notificationUrl: '', sortOrder: 1,
+ businessHours: [{ weekday: 1, openMinute: 540, closeMinute: 1320, isClosed: false }]
+ }
+});
+assert.equal(updatedStore.statusCode, 200);
+assert.equal(storeInput.wifiPassword, undefined);
const statusUpdated = await app.inject({
method: 'PATCH', url: '/app-api/management/rooms/31/status',
headers: { authorization: `Bearer ${token}` },
diff --git a/scripts/check-admin-m08-d.mjs b/scripts/check-admin-m08-d.mjs
index 2d97c05..b6ce1d8 100644
--- a/scripts/check-admin-m08-d.mjs
+++ b/scripts/check-admin-m08-d.mjs
@@ -10,7 +10,9 @@ for (const pattern of [
'OperationsOverviewPanel',
"activeModule = 'overview'",
"activeModule = 'cleaning'",
+ "activeModule = 'stores'",
'平台运营总览',
+ 'StoresRoomsPanel',
'运营总览',
'savedToken',
'loadCleaningWorkspace'
@@ -47,6 +49,36 @@ for (const pattern of [
const api = read('admin/src/api.ts');
assert.match(api, /request\(session, '\/stores'\)/);
assert.match(api, /request\(session, `\/statistics\?\$\{params\}`\)/);
+for (const pattern of [
+ 'createManagedStore',
+ 'updateManagedStore',
+ 'archiveManagedStore',
+ 'listManagedRooms',
+ 'createManagedRoom',
+ 'updateManagedRoom',
+ 'updateManagedRoomStatus',
+ 'addManagedRoomDisabledPeriod'
+]) {
+ assert.match(api, new RegExp(pattern));
+}
+
+const stores = read('admin/src/components/StoresRoomsPanel.vue');
+for (const pattern of [
+ '门店与房间管理',
+ '搜索门店、城市或地址',
+ '每周营业时间',
+ 'wifiPassword',
+ 'listManagedStores',
+ 'listManagedRooms',
+ 'updateManagedRoomStatus',
+ '新增房间',
+ '停用时段',
+ 'configurationStatus',
+ 'operationalStatus',
+ 'archiveManagedRoom'
+]) {
+ assert.match(stores, new RegExp(pattern));
+}
const routes = read('backend/src/routes/business-statistics.ts');
assert.match(routes, /'\/app-api\/management\/statistics', '\/admin-api\/statistics'/);
@@ -54,6 +86,11 @@ assert.match(routes, /'\/app-api\/management\/statistics', '\/admin-api\/statist
const routeTest = read('backend/tests/business-statistics.test.mjs');
assert.match(routeTest, /url: '\/admin-api\/statistics\?/);
+const storeRepository = read('backend/src/stores/store-room-repository.ts');
+assert.match(storeRepository, /wifi_password = COALESCE\(\?, wifi_password\)/);
+assert.match(storeRepository, /FROM qipai_store_business_hours/);
+assert.match(storeRepository, /wifiConfigured/);
+
const styles = read('admin/src/styles.css');
for (const pattern of [
'.overview-page',
@@ -62,6 +99,9 @@ for (const pattern of [
'.overview-grid',
'.revenue-bars',
'.module-grid',
+ '.store-layout',
+ '.store-card',
+ '.hours-editor',
'@media (max-width: 980px)',
'@media (max-width: 560px)'
]) {