1
This commit is contained in:
Generated
+8
@@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
Generated
+9
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="JAVA_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
|
<exclude-output />
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="17" project-jdk-type="JavaSDK">
|
||||||
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+8
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/client-web.iml" filepath="$PROJECT_DIR$/.idea/client-web.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -4,7 +4,7 @@ services:
|
|||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
args:
|
args:
|
||||||
VITE_ADMIN_WEB_ORIGIN: "${VITE_ADMIN_WEB_ORIGIN:-}"
|
VITE_ADMIN_WEB_ORIGIN: "${VITE_ADMIN_WEB_ORIGIN:-https://test-admin.buggoglobal.com}"
|
||||||
NODE_IMAGE: "${NODE_BASE_IMAGE:-node:22-alpine}"
|
NODE_IMAGE: "${NODE_BASE_IMAGE:-node:22-alpine}"
|
||||||
NGINX_IMAGE: "${NGINX_BASE_IMAGE:-nginx:1.27-alpine}"
|
NGINX_IMAGE: "${NGINX_BASE_IMAGE:-nginx:1.27-alpine}"
|
||||||
image: client-web-test:local
|
image: client-web-test:local
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ test('allows qualification uploads with multipart overhead through the API proxy
|
|||||||
test('routes domestic and overseas browser APIs to their test gateways without duplicate api prefixes', () => {
|
test('routes domestic and overseas browser APIs to their test gateways without duplicate api prefixes', () => {
|
||||||
assert.match(testCompose, /API_UPSTREAM: "http:\/\/test-api-cn\.buggoglobal\.com"/)
|
assert.match(testCompose, /API_UPSTREAM: "http:\/\/test-api-cn\.buggoglobal\.com"/)
|
||||||
assert.match(testCompose, /OVERSEAS_API_UPSTREAM: "http:\/\/test-api-global\.buggoglobal\.com"/)
|
assert.match(testCompose, /OVERSEAS_API_UPSTREAM: "http:\/\/test-api-global\.buggoglobal\.com"/)
|
||||||
|
assert.match(testCompose, /VITE_ADMIN_WEB_ORIGIN: "\$\{VITE_ADMIN_WEB_ORIGIN:-https:\/\/test-admin\.buggoglobal\.com\}"/)
|
||||||
assert.match(config, /location \/overseas-api\/ \{[\s\S]*?rewrite \^\/overseas-api\/\(\.\*\)\$ \/api\/\$1 break;[\s\S]*?proxy_pass \$\{OVERSEAS_API_UPSTREAM\};/)
|
assert.match(config, /location \/overseas-api\/ \{[\s\S]*?rewrite \^\/overseas-api\/\(\.\*\)\$ \/api\/\$1 break;[\s\S]*?proxy_pass \$\{OVERSEAS_API_UPSTREAM\};/)
|
||||||
assert.match(config, /proxy_set_header Host \$proxy_host;/)
|
assert.match(config, /proxy_set_header Host \$proxy_host;/)
|
||||||
assert.doesNotMatch(config, /\/api\/api/)
|
assert.doesNotMatch(config, /\/api\/api/)
|
||||||
|
|||||||
@@ -238,6 +238,7 @@ export interface MerchantFirstLegOrderRow {
|
|||||||
inboundCode: string | null
|
inboundCode: string | null
|
||||||
channelName: string | null
|
channelName: string | null
|
||||||
destinationType: ApiDestinationType | null
|
destinationType: ApiDestinationType | null
|
||||||
|
pickupMethod: ApiPickupMethod | null
|
||||||
customerName: string | null
|
customerName: string | null
|
||||||
receivingCompanyName: string | null
|
receivingCompanyName: string | null
|
||||||
specificationModel: string | null
|
specificationModel: string | null
|
||||||
|
|||||||
@@ -12,13 +12,14 @@ test('maps the first-leg aggregate list fields without loading detail rows', ()
|
|||||||
assert.match(apiSource, /cargoes\?: Array<\{ skuNo\?: string \| null \}>/)
|
assert.match(apiSource, /cargoes\?: Array<\{ skuNo\?: string \| null \}>/)
|
||||||
assert.match(apiSource, /apiRequest<PageResult<MerchantFirstLegOrderRow>>/)
|
assert.match(apiSource, /apiRequest<PageResult<MerchantFirstLegOrderRow>>/)
|
||||||
assert.match(storeSource, /function mapFirstLegOrderRow/)
|
assert.match(storeSource, /function mapFirstLegOrderRow/)
|
||||||
for (const field of ['inboundId', 'inboundCode', 'channelName', 'destinationWarehouseName', 'packageCount', 'expressCompany', 'trackingNo', 'totalGrossWeightKg', 'totalNetWeightKg', 'totalVolumeCbm', 'totalQuantity', 'remark', 'shipmentId', 'referenceId', 'createdAt']) {
|
for (const field of ['inboundId', 'inboundCode', 'channelName', 'destinationWarehouseName', 'pickupMethod', 'packageCount', 'expressCompany', 'trackingNo', 'totalGrossWeightKg', 'totalNetWeightKg', 'totalVolumeCbm', 'totalQuantity', 'remark', 'shipmentId', 'referenceId', 'createdAt']) {
|
||||||
assert.match(storeSource, new RegExp(`row\\.${field}`))
|
assert.match(storeSource, new RegExp(`row\\.${field}`))
|
||||||
}
|
}
|
||||||
assert.match(storeSource, /row\.skuNos/)
|
assert.match(storeSource, /row\.skuNos/)
|
||||||
assert.match(storeSource, /row\.cargoes/)
|
assert.match(storeSource, /row\.cargoes/)
|
||||||
assert.match(storeSource, /chargeableWeightKg: null/)
|
assert.match(storeSource, /chargeableWeightKg: null/)
|
||||||
assert.match(storeSource, /chargeableVolumeCbm: null/)
|
assert.match(storeSource, /chargeableVolumeCbm: null/)
|
||||||
|
assert.match(apiSource, /pickupMethod: ApiPickupMethod \\| null/)
|
||||||
assert.match(storeSource, /orderResult\.value\.records\.map\(mapFirstLegOrderRow\)/)
|
assert.match(storeSource, /orderResult\.value\.records\.map\(mapFirstLegOrderRow\)/)
|
||||||
assert.match(storeSource, /orderResult\.value\.records\.map\(\(item\) => mapUnifiedOrderRow\(item, warehouses\.value\)\)/)
|
assert.match(storeSource, /orderResult\.value\.records\.map\(\(item\) => mapUnifiedOrderRow\(item, warehouses\.value\)\)/)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -398,7 +398,7 @@ function mapFirstLegOrderRow(row: MerchantFirstLegOrderRow): FirstLegOrder {
|
|||||||
inboundId: row.inboundId == null ? '' : String(row.inboundId), inboundCode: row.inboundCode ?? '',
|
inboundId: row.inboundId == null ? '' : String(row.inboundId), inboundCode: row.inboundCode ?? '',
|
||||||
},
|
},
|
||||||
createdAt: row.createdAt, updatedAt: row.createdAt, tradeTerm: '', destinationCountryCode: '', destinationWarehouseId: '',
|
createdAt: row.createdAt, updatedAt: row.createdAt, tradeTerm: '', destinationCountryCode: '', destinationWarehouseId: '',
|
||||||
destinationWarehouseName: row.destinationWarehouseName ?? '', pickupMethod: 'SELF_EXPRESS', channelCode: '',
|
destinationWarehouseName: row.destinationWarehouseName ?? '', pickupMethod: row.pickupMethod ?? 'SELF_EXPRESS', channelCode: '',
|
||||||
channelName: row.channelName ?? '', pricingTaxMode: 'TAX_INCLUDED', carrierId: '', carrierName: '', taxMode: 'BUY_DECLARATION',
|
channelName: row.channelName ?? '', pricingTaxMode: 'TAX_INCLUDED', carrierId: '', carrierName: '', taxMode: 'BUY_DECLARATION',
|
||||||
customsInspection: false, destinationType: row.destinationType ?? 'PRIVATE', amazonWarehouseCode: '',
|
customsInspection: false, destinationType: row.destinationType ?? 'PRIVATE', amazonWarehouseCode: '',
|
||||||
shipmentId: row.shipmentId ?? row.internationalOrderNo ?? '', requestedPickupAt: '', receivingTime: '', submittedAt: '', completedAt: '',
|
shipmentId: row.shipmentId ?? row.internationalOrderNo ?? '', requestedPickupAt: '', receivingTime: '', submittedAt: '', completedAt: '',
|
||||||
|
|||||||
@@ -44,4 +44,15 @@ onMounted(() => { void Promise.all([load(), notifications.refreshUnreadCount()])
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.notification-page{display:flex;flex-direction:column;gap:16px}.page-heading{display:flex;justify-content:space-between;color:var(--navy)}.page-heading h1{margin:2px 0 5px;font-size:24px}.page-heading p:not(.eyebrow){margin:0;color:var(--muted);font-size:13px}.eyebrow{margin:0;color:var(--primary);font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase}.content-card{padding:16px;border:1px solid var(--border);border-radius:8px;background:var(--surface,#fff)}.filter-bar{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px}.mark-all{margin-left:auto}.state-panel{display:flex;min-height:180px;align-items:center;justify-content:center;flex-direction:column;gap:12px;text-align:center}.state-panel--error{color:#b42318;background:#fff8f7;border:1px solid #f5c2bd;border-radius:6px}.state-panel p{margin:0}.table-empty{display:flex;align-items:center;flex-direction:column;gap:8px;padding:42px 12px;color:var(--muted)}.table-empty strong{color:var(--navy)}.table-empty span{font-size:12px}.pagination-row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding-top:14px;color:var(--muted);font-size:12px}@media(max-width:720px){.content-card{padding:12px}.mark-all{margin-left:0;width:100%}.filter-bar .el-select,.filter-bar>.el-button{flex:1}.pagination-row{align-items:flex-start;flex-direction:column}.page-heading h1{font-size:21px}}
|
.notification-page{display:flex;flex-direction:column;gap:16px}.page-heading{display:flex;justify-content:space-between;color:var(--navy)}.page-heading h1{margin:2px 0 5px;font-size:24px}.page-heading p:not(.eyebrow){margin:0;color:var(--muted);font-size:13px}.eyebrow{margin:0;color:var(--primary);font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase}.content-card{padding:16px;border:1px solid var(--border);border-radius:8px;background:var(--surface,#fff)}.filter-bar{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:14px}.mark-all{margin-left:auto}.state-panel{display:flex;min-height:180px;align-items:center;justify-content:center;flex-direction:column;gap:12px;text-align:center}.state-panel--error{color:#b42318;background:#fff8f7;border:1px solid #f5c2bd;border-radius:6px}.state-panel p{margin:0}.table-empty{display:flex;align-items:center;flex-direction:column;gap:8px;padding:42px 12px;color:var(--muted)}.table-empty strong{color:var(--navy)}.table-empty span{font-size:12px}.pagination-row{display:flex;align-items:center;justify-content:space-between;gap:12px;padding-top:14px;color:var(--muted);font-size:12px}@media(max-width:720px){.content-card{padding:12px}.mark-all{margin-left:0;width:100%}.filter-bar .el-select,.filter-bar>.el-button{flex:1}.pagination-row{align-items:flex-start;flex-direction:column}.page-heading h1{font-size:21px}}
|
||||||
|
/* Keep the notification filters compact and aligned with the other client lists. */
|
||||||
|
.filter-bar { align-items: flex-end; }
|
||||||
|
.filter-bar > .el-select { width: 190px; }
|
||||||
|
.filter-bar > .el-input { width: 260px; }
|
||||||
|
.filter-bar > .el-button { flex: 0 0 auto; min-height: 32px; }
|
||||||
|
.filter-bar > .el-button + .el-button { margin-left: 0; }
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.filter-bar { align-items: stretch; flex-direction: column; }
|
||||||
|
.filter-bar > .el-select, .filter-bar > .el-input { width: 100%; }
|
||||||
|
.filter-bar > .el-button { width: 100%; }
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -107,3 +107,12 @@ test('keeps client baseline permissions selected when server resources do not de
|
|||||||
assert.match(permissionsViewSource, /:disabled="isRequiredPermission\(item\.code\)"/)
|
assert.match(permissionsViewSource, /:disabled="isRequiredPermission\(item\.code\)"/)
|
||||||
assert.doesNotMatch(permissionsViewSource, /selectedCodes\.value\.unshift\('account:profile:view'\)/)
|
assert.doesNotMatch(permissionsViewSource, /selectedCodes\.value\.unshift\('account:profile:view'\)/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('exposes all message-center permissions in the enterprise role editor', () => {
|
||||||
|
for (const permission of ['merchant:notification:view', 'merchant:notification:read', 'merchant:notification:read-all']) {
|
||||||
|
assert.match(contractsSource, new RegExp(`code: '${permission}'`))
|
||||||
|
assert.match(contractsSource, new RegExp(`'${permission}': \\['${permission}'\\]`))
|
||||||
|
}
|
||||||
|
assert.match(contractsSource, /key: 'notifications'/)
|
||||||
|
assert.match(storeSource, /const mappedMerchantPermissionCodes = new Set\(Object\.values\(FRONTEND_TO_MERCHANT_PERMISSION_CODES\)\.flat\(\)\)/)
|
||||||
|
})
|
||||||
|
|||||||
@@ -25,6 +25,15 @@ export const TENANT_PERMISSION_GROUPS: readonly TenantPermissionGroup[] = [
|
|||||||
label: '业务总览',
|
label: '业务总览',
|
||||||
items: [{ code: 'dashboard:view', label: '查看总览', description: '所有企业成员默认可用,可查看企业统计和最近订单' }],
|
items: [{ code: 'dashboard:view', label: '查看总览', description: '所有企业成员默认可用,可查看企业统计和最近订单' }],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'notifications',
|
||||||
|
label: '消息中心',
|
||||||
|
items: [
|
||||||
|
{ code: 'merchant:notification:view', label: '查看我的消息', description: '查看本企业和当前成员可见的站内消息' },
|
||||||
|
{ code: 'merchant:notification:read', label: '标记消息已读', description: '将已查看的站内消息标记为已读' },
|
||||||
|
{ code: 'merchant:notification:read-all', label: '全部标记已读', description: '将当前成员的全部站内消息标记为已读' },
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'finance',
|
key: 'finance',
|
||||||
label: '财务账单',
|
label: '财务账单',
|
||||||
|
|||||||
@@ -200,7 +200,7 @@ async function resetPassword(member: TenantMember) {
|
|||||||
<el-form-item label="登录账号" prop="account" required><el-input v-model="form.account" :disabled="Boolean(editingId)" maxlength="64" placeholder="4–64 位字母、数字或 _ . @ -" /></el-form-item>
|
<el-form-item label="登录账号" prop="account" required><el-input v-model="form.account" :disabled="Boolean(editingId)" maxlength="64" placeholder="4–64 位字母、数字或 _ . @ -" /></el-form-item>
|
||||||
<el-form-item v-if="!editingId" label="初始密码" prop="password" required><el-input v-model="form.password" type="password" show-password autocomplete="new-password" maxlength="64" placeholder="8–64 位,同时包含字母和数字" /></el-form-item>
|
<el-form-item v-if="!editingId" label="初始密码" prop="password" required><el-input v-model="form.password" type="password" show-password autocomplete="new-password" maxlength="64" placeholder="8–64 位,同时包含字母和数字" /></el-form-item>
|
||||||
<div class="member-form-grid"><el-form-item label="联系电话" prop="phone"><el-input v-model="form.phone" maxlength="32" placeholder="例如 13800138000(选填)" /></el-form-item><el-form-item label="邮箱" prop="email"><el-input v-model="form.email" maxlength="128" placeholder="例如 name@example.com(选填)" /></el-form-item></div>
|
<div class="member-form-grid"><el-form-item label="联系电话" prop="phone"><el-input v-model="form.phone" maxlength="32" placeholder="例如 13800138000(选填)" /></el-form-item><el-form-item label="邮箱" prop="email"><el-input v-model="form.email" maxlength="128" placeholder="例如 name@example.com(选填)" /></el-form-item></div>
|
||||||
<el-form-item label="成员角色" prop="roleIds" required><el-select v-model="form.roleIds" multiple :disabled="Boolean(editingId) && !canGrantMemberRole" placeholder="请选择角色"><el-option v-for="role in store.roles.filter((item) => item.status === 'ACTIVE')" :key="role.id" :label="role.name" :value="role.id" /></el-select></el-form-item>
|
<el-form-item label="成员角色" prop="roleIds" required><el-select v-model="form.roleIds" multiple :disabled="Boolean(editingId) && !canGrantMemberRole" placeholder="请选择角色"><el-option v-for="role in store.roles.filter((item) => item.status === 'ACTIVE' && (!item.builtin || form.roleIds.includes(item.id)))" :key="role.id" :label="role.name" :value="role.id" :disabled="role.builtin" /></el-select></el-form-item>
|
||||||
<div v-if="!editingId" class="member-form-tip"><KeyRound :size="17" /><span>请通过安全渠道将初始密码交给成员;前端不会保存或再次展示该密码。</span></div>
|
<div v-if="!editingId" class="member-form-tip"><KeyRound :size="17" /><span>请通过安全渠道将初始密码交给成员;前端不会保存或再次展示该密码。</span></div>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer><div class="drawer-actions"><el-button :disabled="saving" @click="editorOpen = false">取消</el-button><el-button type="primary" :loading="saving" @click="saveMember">保存</el-button></div></template>
|
<template #footer><div class="drawer-actions"><el-button :disabled="saving" @click="editorOpen = false">取消</el-button><el-button type="primary" :loading="saving" @click="saveMember">保存</el-button></div></template>
|
||||||
|
|||||||
Reference in New Issue
Block a user