Compare commits
3 Commits
5178b6379f
...
4f96f7459f
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f96f7459f | |||
| 63c7e4ab6f | |||
| 0d5ebdb591 |
@ -752,6 +752,30 @@
|
|||||||
return omsUrl(preferOmsH5() ? '/h5/login' : '/login');
|
return omsUrl(preferOmsH5() ? '/h5/login' : '/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let omsLoginWarmed = false;
|
||||||
|
function warmupOmsLogin() {
|
||||||
|
if (omsLoginWarmed) return;
|
||||||
|
omsLoginWarmed = true;
|
||||||
|
try {
|
||||||
|
const url = loginPortalUrl();
|
||||||
|
const origin = resolveOmsOrigin();
|
||||||
|
const preconnect = document.createElement('link');
|
||||||
|
preconnect.rel = 'preconnect';
|
||||||
|
preconnect.href = origin;
|
||||||
|
document.head.appendChild(preconnect);
|
||||||
|
const prefetch = document.createElement('link');
|
||||||
|
prefetch.rel = 'prefetch';
|
||||||
|
prefetch.href = url;
|
||||||
|
document.head.appendChild(prefetch);
|
||||||
|
if (/localhost|127\.0\.0\.1/i.test(new URL(origin).hostname)) {
|
||||||
|
fetch(new URL('/src/views/login/index.vue', origin), {
|
||||||
|
credentials: 'omit',
|
||||||
|
mode: 'cors',
|
||||||
|
}).catch(function () {});
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
|
||||||
function registerUrl(dest) {
|
function registerUrl(dest) {
|
||||||
// 官网「注册询价」走客户登录页微信扫码(注册/登录同一入口)
|
// 官网「注册询价」走客户登录页微信扫码(注册/登录同一入口)
|
||||||
const url = new URL(loginPortalUrl());
|
const url = new URL(loginPortalUrl());
|
||||||
@ -2654,7 +2678,13 @@
|
|||||||
if (heroCta) {
|
if (heroCta) {
|
||||||
heroCta.textContent = t('heroCta');
|
heroCta.textContent = t('heroCta');
|
||||||
heroCta.href = loginPortalUrl();
|
heroCta.href = loginPortalUrl();
|
||||||
|
if (!heroCta.dataset.omsWarm) {
|
||||||
|
heroCta.dataset.omsWarm = '1';
|
||||||
|
heroCta.addEventListener('pointerenter', warmupOmsLogin, { once: true });
|
||||||
|
heroCta.addEventListener('focus', warmupOmsLogin, { once: true });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
warmupOmsLogin();
|
||||||
const welcomeHuman = shell.querySelector('.ai-welcome-human');
|
const welcomeHuman = shell.querySelector('.ai-welcome-human');
|
||||||
if (welcomeHuman) welcomeHuman.textContent = t('human') + ' →';
|
if (welcomeHuman) welcomeHuman.textContent = t('human') + ' →';
|
||||||
renderWelcomeStats();
|
renderWelcomeStats();
|
||||||
|
|||||||
@ -66,6 +66,14 @@
|
|||||||
: 'IDE客户中心'
|
: 'IDE客户中心'
|
||||||
: 'IDE I Faster & Simpler';
|
: 'IDE I Faster & Simpler';
|
||||||
} catch (e2) {}
|
} catch (e2) {}
|
||||||
|
if (path === '/login') {
|
||||||
|
var plane = document.createElement('link');
|
||||||
|
plane.rel = 'preload';
|
||||||
|
plane.as = 'image';
|
||||||
|
plane.href = '/login-assets/hero-plane.webp';
|
||||||
|
plane.type = 'image/webp';
|
||||||
|
document.head.appendChild(plane);
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
|
|||||||
BIN
public/login-assets/hero-truck.png
Normal file
BIN
public/login-assets/hero-truck.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 158 KiB |
@ -117,6 +117,7 @@ export async function postForewarningServiceKpiAreaGetKpiAreaInfos(
|
|||||||
export type KpiAreaInfoImportResult = {
|
export type KpiAreaInfoImportResult = {
|
||||||
successCount: number
|
successCount: number
|
||||||
failCount: number
|
failCount: number
|
||||||
|
skipCount: number
|
||||||
failures: Array<{ rowNumber: number; reason: string }>
|
failures: Array<{ rowNumber: number; reason: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -261,6 +262,7 @@ export async function importKpiAreaInfo(
|
|||||||
return {
|
return {
|
||||||
successCount: Number(d.successCount ?? d.SuccessCount ?? 0),
|
successCount: Number(d.successCount ?? d.SuccessCount ?? 0),
|
||||||
failCount: Number(d.failCount ?? d.FailCount ?? 0),
|
failCount: Number(d.failCount ?? d.FailCount ?? 0),
|
||||||
|
skipCount: Number(d.skipCount ?? d.SkipCount ?? 0),
|
||||||
failures,
|
failures,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -62,6 +62,11 @@ export async function updateSalesProduct(body: Record<string, any> & { id: numbe
|
|||||||
assertBusinessOk(res.data)
|
assertBusinessOk(res.data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function updateSalesProductCode(body: { id: number | string; productCode: string }) {
|
||||||
|
const res = await axiosInstance.post(omsUrl('/channel/sales-product/product-code/update'), body)
|
||||||
|
assertBusinessOk(res.data)
|
||||||
|
}
|
||||||
|
|
||||||
export async function deleteSalesProduct(id: number | string) {
|
export async function deleteSalesProduct(id: number | string) {
|
||||||
const res = await axiosInstance.post(omsUrl('/channel/sales-product/delete'), { id })
|
const res = await axiosInstance.post(omsUrl('/channel/sales-product/delete'), { id })
|
||||||
assertBusinessOk(res.data)
|
assertBusinessOk(res.data)
|
||||||
|
|||||||
@ -656,6 +656,7 @@ const CORE_GROUPS: PageButtonGroup[] = [
|
|||||||
{ code: 'AddProduct', name: '新增产品' },
|
{ code: 'AddProduct', name: '新增产品' },
|
||||||
{ code: 'Create', name: '新增' },
|
{ code: 'Create', name: '新增' },
|
||||||
{ code: 'Update', name: '修改' },
|
{ code: 'Update', name: '修改' },
|
||||||
|
{ code: 'UpdateCode', name: '修改产品代码' },
|
||||||
{ code: 'Delete', name: '删除' },
|
{ code: 'Delete', name: '删除' },
|
||||||
{ code: 'EditPrice', name: '编辑' },
|
{ code: 'EditPrice', name: '编辑' },
|
||||||
{ code: 'Save', name: '保存' },
|
{ code: 'Save', name: '保存' },
|
||||||
|
|||||||
@ -66,10 +66,10 @@ export default {
|
|||||||
'menu.marketing': 'Marketing',
|
'menu.marketing': 'Marketing',
|
||||||
'menu.marketing.memberLevel': 'Member Levels',
|
'menu.marketing.memberLevel': 'Member Levels',
|
||||||
'marketing.memberLevel.pageTip':
|
'marketing.memberLevel.pageTip':
|
||||||
'Member levels control points upgrades and optional extra discounts. Price tier keys are maintained on the customer and are independent of level upgrades.',
|
'Levels are based on rolling 12-month paid base-freight points. Discount applies to base freight only; coupons do not stack. V0–V3 never downgrade; V4/V5 step down one level. Floor is 5% off.',
|
||||||
'marketing.memberLevel.searchPlaceholder': 'Search code / name',
|
'marketing.memberLevel.searchPlaceholder': 'Search code / name',
|
||||||
'marketing.memberLevel.modalTip':
|
'marketing.memberLevel.modalTip':
|
||||||
'Reach the points threshold to upgrade; extra discount stacks after sell-price tier. Configure price tiers on the customer.',
|
'Threshold is rolling 12-month points. 0.01 = 1% off base freight (99 fold), cap 0.05. Enable downgrade only for V4/V5 (one step).',
|
||||||
'marketing.memberLevel.modal.add': 'Add level',
|
'marketing.memberLevel.modal.add': 'Add level',
|
||||||
'marketing.memberLevel.modal.edit': 'Edit level',
|
'marketing.memberLevel.modal.edit': 'Edit level',
|
||||||
'marketing.memberLevel.deleteConfirm': 'Delete level "{name}"? {count} customers use it.',
|
'marketing.memberLevel.deleteConfirm': 'Delete level "{name}"? {count} customers use it.',
|
||||||
@ -78,10 +78,14 @@ export default {
|
|||||||
'marketing.memberLevel.form.levelCodeExtra': 'Immutable after create; prefer uppercase codes',
|
'marketing.memberLevel.form.levelCodeExtra': 'Immutable after create; prefer uppercase codes',
|
||||||
'marketing.memberLevel.form.levelName': 'Level name',
|
'marketing.memberLevel.form.levelName': 'Level name',
|
||||||
'marketing.memberLevel.form.levelNamePlaceholder': 'e.g. Standard, Silver',
|
'marketing.memberLevel.form.levelNamePlaceholder': 'e.g. Standard, Silver',
|
||||||
'marketing.memberLevel.form.minPoints': 'Min points',
|
'marketing.memberLevel.form.minPoints': 'Rolling points min',
|
||||||
'marketing.memberLevel.form.minPointsExtra': 'Total points required to reach this level',
|
'marketing.memberLevel.form.minPointsExtra': 'Rolling 12-month paid base-freight points to reach this level',
|
||||||
'marketing.memberLevel.form.extraDiscountRate': 'Extra discount rate',
|
'marketing.memberLevel.form.extraDiscountRate': 'Base-freight off rate',
|
||||||
'marketing.memberLevel.form.extraDiscountRateExtra': 'e.g. 0.01 = extra 1% off after tier price',
|
'marketing.memberLevel.form.extraDiscountRateExtra': '0.01 = 1% off base freight; cap 0.05',
|
||||||
|
'marketing.memberLevel.form.canDowngrade': 'Allow downgrade',
|
||||||
|
'marketing.memberLevel.form.canDowngradeExtra': 'V4/V5 only; daily job steps down one level. V0–V3 stay.',
|
||||||
|
'marketing.memberLevel.discount.original': 'List price',
|
||||||
|
'marketing.memberLevel.discount.fold': '{fold}% of freight',
|
||||||
'marketing.memberLevel.form.sort': 'Sort',
|
'marketing.memberLevel.form.sort': 'Sort',
|
||||||
'marketing.memberLevel.form.sortExtra': 'Smaller number ranks first',
|
'marketing.memberLevel.form.sortExtra': 'Smaller number ranks first',
|
||||||
'marketing.memberLevel.form.status': 'Status',
|
'marketing.memberLevel.form.status': 'Status',
|
||||||
@ -94,8 +98,8 @@ export default {
|
|||||||
'marketing.memberLevel.form.requiredHint': 'Level code and name are required',
|
'marketing.memberLevel.form.requiredHint': 'Level code and name are required',
|
||||||
'marketing.memberLevel.section.basic': 'Basic info',
|
'marketing.memberLevel.section.basic': 'Basic info',
|
||||||
'marketing.memberLevel.section.basicHint': 'Code cannot change after create; name and status can',
|
'marketing.memberLevel.section.basicHint': 'Code cannot change after create; name and status can',
|
||||||
'marketing.memberLevel.section.pricing': 'Points & discount',
|
'marketing.memberLevel.section.pricing': 'Threshold & freight discount',
|
||||||
'marketing.memberLevel.section.pricingHint': 'Upgrade threshold and extra discount',
|
'marketing.memberLevel.section.pricingHint': 'Rolling 12-month points; discount on base freight only',
|
||||||
'marketing.memberLevel.section.display': 'Display & benefits',
|
'marketing.memberLevel.section.display': 'Display & benefits',
|
||||||
'marketing.memberLevel.section.displayHint': 'Portal display only; does not affect pricing',
|
'marketing.memberLevel.section.displayHint': 'Portal display only; does not affect pricing',
|
||||||
'marketing.memberLevel.preview.nameFallback': 'Untitled level',
|
'marketing.memberLevel.preview.nameFallback': 'Untitled level',
|
||||||
@ -2368,7 +2372,7 @@ export default {
|
|||||||
'order.priceCheck.form.destinationCountry.placeholder': 'Please select',
|
'order.priceCheck.form.destinationCountry.placeholder': 'Please select',
|
||||||
'order.priceCheck.form.shippingMethod': 'Shipping method',
|
'order.priceCheck.form.shippingMethod': 'Shipping method',
|
||||||
'order.priceCheck.form.shippingMethod.placeholder': 'Please select',
|
'order.priceCheck.form.shippingMethod.placeholder': 'Please select',
|
||||||
'order.priceCheck.form.channel': 'Channel',
|
'order.priceCheck.form.channel': 'Route',
|
||||||
'order.priceCheck.form.channel.placeholder': 'All',
|
'order.priceCheck.form.channel.placeholder': 'All',
|
||||||
'order.priceCheck.form.cargoWeight': 'Cargo weight',
|
'order.priceCheck.form.cargoWeight': 'Cargo weight',
|
||||||
'order.priceCheck.form.cargoWeight.placeholder': 'Please enter weight (KG)',
|
'order.priceCheck.form.cargoWeight.placeholder': 'Please enter weight (KG)',
|
||||||
@ -2389,7 +2393,7 @@ export default {
|
|||||||
'order.priceCheck.form.postalCode': 'Postal code',
|
'order.priceCheck.form.postalCode': 'Postal code',
|
||||||
'order.priceCheck.form.postalCode.tip': 'more accurate',
|
'order.priceCheck.form.postalCode.tip': 'more accurate',
|
||||||
'order.priceCheck.form.postalCode.placeholder': 'Optional — improves zoning',
|
'order.priceCheck.form.postalCode.placeholder': 'Optional — improves zoning',
|
||||||
'order.priceCheck.form.moreFilters': 'More filters',
|
'order.priceCheck.form.moreFilters': 'More',
|
||||||
'order.priceCheck.form.activeFilters': '{count} selected',
|
'order.priceCheck.form.activeFilters': '{count} selected',
|
||||||
'order.priceCheck.form.clearFilters': 'Clear',
|
'order.priceCheck.form.clearFilters': 'Clear',
|
||||||
'order.priceCheck.form.removeFilter': 'Remove filter',
|
'order.priceCheck.form.removeFilter': 'Remove filter',
|
||||||
@ -2465,9 +2469,9 @@ export default {
|
|||||||
'order.priceCheck.table.viewDetail': 'Details',
|
'order.priceCheck.table.viewDetail': 'Details',
|
||||||
'order.priceCheck.table.viewGuide': 'Channel guide',
|
'order.priceCheck.table.viewGuide': 'Channel guide',
|
||||||
'order.priceCheck.table.viewGuideHint': 'Click to view route guide',
|
'order.priceCheck.table.viewGuideHint': 'Click to view route guide',
|
||||||
'order.priceCheck.table.customsOrderNow': 'Place order',
|
'order.priceCheck.table.customsOrderNow': 'Go to order',
|
||||||
'order.priceCheck.table.orderNow': 'Place order',
|
'order.priceCheck.table.orderNow': 'Go to order',
|
||||||
'order.priceCheck.table.askAi': 'Ask AI',
|
'order.priceCheck.table.askAi': 'Ask advisor',
|
||||||
'order.priceCheck.table.askAiCanShip': 'Can this ship?',
|
'order.priceCheck.table.askAiCanShip': 'Can this ship?',
|
||||||
'order.priceCheck.table.askAiCheaper': 'Anything cheaper?',
|
'order.priceCheck.table.askAiCheaper': 'Anything cheaper?',
|
||||||
'order.priceCheck.table.empty': 'No matching quotes',
|
'order.priceCheck.table.empty': 'No matching quotes',
|
||||||
@ -4670,7 +4674,7 @@ export default {
|
|||||||
'user.customer.tip.accountBalance': 'Available balance — adjust carefully',
|
'user.customer.tip.accountBalance': 'Available balance — adjust carefully',
|
||||||
'user.customer.tip.totalQuota': 'Credit limit',
|
'user.customer.tip.totalQuota': 'Credit limit',
|
||||||
'user.customer.tip.usedQuota': 'Used quota, usually accumulated by the system',
|
'user.customer.tip.usedQuota': 'Used quota, usually accumulated by the system',
|
||||||
'user.customer.tip.memberLevel': 'Used for points upgrade and optional extra discount',
|
'user.customer.tip.memberLevel': 'Auto-assigned from rolling 12-month paid base-freight points',
|
||||||
'user.customer.tip.priceTierKey': 'Controls quote price tier; independent of member level upgrade',
|
'user.customer.tip.priceTierKey': 'Controls quote price tier; independent of member level upgrade',
|
||||||
'user.customer.modal.addTitle': 'Add Customer',
|
'user.customer.modal.addTitle': 'Add Customer',
|
||||||
'user.customer.modal.editTitle': 'Edit Customer',
|
'user.customer.modal.editTitle': 'Edit Customer',
|
||||||
@ -5171,7 +5175,13 @@ export default {
|
|||||||
'channel.salesProducts.placeholder.productDescription': 'Customer-facing description (optional)',
|
'channel.salesProducts.placeholder.productDescription': 'Customer-facing description (optional)',
|
||||||
'channel.salesProducts.placeholder.preStorageScript': 'Script before order persistence (optional)',
|
'channel.salesProducts.placeholder.preStorageScript': 'Script before order persistence (optional)',
|
||||||
'channel.salesProducts.tip.productCode':
|
'channel.salesProducts.tip.productCode':
|
||||||
'Rule: region/country code + 01 line/02 express/03 postal/04 packet + other.\nExample: US04-5DAY\nNot editable after create; uppercase letters, digits, and - only',
|
'Rule: region/country code + 01 line/02 express/03 postal/04 packet + other.\nExample: US04-5DAY\nUppercase letters, digits, and - only; can be changed later via the edit action',
|
||||||
|
'channel.salesProducts.form.editProductCode': 'Edit',
|
||||||
|
'channel.salesProducts.modal.editProductCodeTitle': 'Edit product code',
|
||||||
|
'channel.salesProducts.modal.editProductCodeHint':
|
||||||
|
'Updates the sales-product master code only. Historical orders keep the old code; quoting and place-order use the new one.',
|
||||||
|
'channel.salesProducts.messages.productCodeRequired': 'Enter a product code',
|
||||||
|
'channel.salesProducts.messages.productCodeUpdated': 'Product code updated',
|
||||||
'channel.salesProducts.tip.productName':
|
'channel.salesProducts.tip.productName':
|
||||||
'Rule: region/country + product type description + other.\nExample: US air packet-5 days',
|
'Rule: region/country + product type description + other.\nExample: US air packet-5 days',
|
||||||
'channel.salesProducts.tip.group': 'Used for website/admin grouping, e.g. express parcel',
|
'channel.salesProducts.tip.group': 'Used for website/admin grouping, e.g. express parcel',
|
||||||
@ -5558,7 +5568,7 @@ export default {
|
|||||||
'channel.area.message.deleteFailed': 'Delete failed. Please try again later.',
|
'channel.area.message.deleteFailed': 'Delete failed. Please try again later.',
|
||||||
'channel.area.import.modalTitle': 'Import area details',
|
'channel.area.import.modalTitle': 'Import area details',
|
||||||
'channel.area.import.tip':
|
'channel.area.import.tip':
|
||||||
'Download the template first. Country code is required (country name is filled from the code). Put match dimension/mode codes from the Enum sheet. One row, one dimension: postcode rules if filled; state only = state match; state+city = city match; empty = country fallback.',
|
'Download the template first. Import is incremental: exact duplicate rules in this area are skipped, not overwritten. Country code is required (country name is filled from the code). Put match dimension/mode codes from the Enum sheet. One row, one dimension: postcode rules if filled; state only = state match; state+city = city match; empty = country fallback.',
|
||||||
'channel.area.import.downloadTemplate': 'Download template',
|
'channel.area.import.downloadTemplate': 'Download template',
|
||||||
'channel.area.import.submit': 'Import',
|
'channel.area.import.submit': 'Import',
|
||||||
'channel.area.import.downloadSuccess': 'Template downloaded',
|
'channel.area.import.downloadSuccess': 'Template downloaded',
|
||||||
@ -5569,7 +5579,7 @@ export default {
|
|||||||
'channel.area.import.failed': 'Import failed',
|
'channel.area.import.failed': 'Import failed',
|
||||||
'channel.area.import.partialSuccess': 'Some rows failed. See details below.',
|
'channel.area.import.partialSuccess': 'Some rows failed. See details below.',
|
||||||
'channel.area.import.allFailed': 'Import failed. Please check the file.',
|
'channel.area.import.allFailed': 'Import failed. Please check the file.',
|
||||||
'channel.area.import.resultSummary': '{success} succeeded, {fail} failed',
|
'channel.area.import.resultSummary': '{success} succeeded, {skip} duplicate skipped, {fail} failed',
|
||||||
'channel.area.import.failRow': 'Row {row}: {reason}',
|
'channel.area.import.failRow': 'Row {row}: {reason}',
|
||||||
'channel.area.export.success': 'Exported {count} row(s)',
|
'channel.area.export.success': 'Exported {count} row(s)',
|
||||||
'channel.area.export.failed': 'Export failed',
|
'channel.area.export.failed': 'Export failed',
|
||||||
|
|||||||
@ -26,20 +26,19 @@ export default {
|
|||||||
'Grant "{templateName}" to {count} customers ({total} coupons total). Est. max discount {amount}. Cannot be undone.',
|
'Grant "{templateName}" to {count} customers ({total} coupons total). Est. max discount {amount}. Cannot be undone.',
|
||||||
|
|
||||||
'marketing.pointsRule.pageTip':
|
'marketing.pointsRule.pageTip':
|
||||||
'Points are earned after payment. Rule changes do not affect past orders. Use the Ledger tab for list/search/manual adjustments (reason ≥10 chars).',
|
'1 paid yuan of base freight = 1 point. Surcharges and coupon amounts do not earn points. Points are for rolling 12-month leveling only and cannot offset freight.',
|
||||||
'marketing.pointsRule.tab.rule': 'Rules',
|
'marketing.pointsRule.sectionTitle': 'Points rules',
|
||||||
'marketing.pointsRule.tab.flow': 'Ledger',
|
'marketing.pointsRule.tab.flow': 'Ledger',
|
||||||
'marketing.pointsRule.form.ruleName': 'Rule name',
|
'marketing.pointsRule.form.ruleName': 'Rule name',
|
||||||
'marketing.pointsRule.form.pointsPerYuan': 'Points per yuan',
|
'marketing.pointsRule.form.pointsPerYuan': 'Points per base-freight yuan',
|
||||||
'marketing.pointsRule.form.pointsPerYuanExtra': 'Based on payable amount',
|
'marketing.pointsRule.form.pointsPerYuanExtra': 'Paid base freight only; surcharges and coupons excluded',
|
||||||
'marketing.pointsRule.form.firstOrderMultiplier': 'First-order multiplier',
|
|
||||||
'marketing.pointsRule.form.maxPointsPerOrder': 'Max points per order',
|
|
||||||
'marketing.pointsRule.form.status': 'Status',
|
'marketing.pointsRule.form.status': 'Status',
|
||||||
'marketing.pointsRule.saveConfirmTitle': 'Save points rule',
|
'marketing.pointsRule.saveConfirmTitle': 'Save points rule',
|
||||||
'marketing.pointsRule.saveConfirmContent': 'Changes apply to future orders only.',
|
'marketing.pointsRule.saveConfirmContent':
|
||||||
|
'Applies to future paid orders only. Surcharges do not earn points; points cannot offset freight.',
|
||||||
|
|
||||||
'marketing.pointsFlow.pageTip':
|
'marketing.pointsFlow.pageTip':
|
||||||
'Ledger includes earn, clawback, and manual adjustments. Filter by customer code; reason ≥10 chars. Use Rules button for grant ratio settings.',
|
'Ledger of base-freight grants, refund/delete clawbacks, and adjustments. Leveling uses rolling 12 months. Points cannot offset freight.',
|
||||||
'marketing.pointsFlow.searchPlaceholder': 'Search remark / biz no.',
|
'marketing.pointsFlow.searchPlaceholder': 'Search remark / biz no.',
|
||||||
'marketing.pointsFlow.customerCodePlaceholder': 'Customer code',
|
'marketing.pointsFlow.customerCodePlaceholder': 'Customer code',
|
||||||
'marketing.pointsFlow.changeTypeFilter': 'Change type',
|
'marketing.pointsFlow.changeTypeFilter': 'Change type',
|
||||||
@ -69,7 +68,7 @@ export default {
|
|||||||
'marketing.pointsFlow.adjustModal.remarkMin': 'Reason must be at least 10 characters',
|
'marketing.pointsFlow.adjustModal.remarkMin': 'Reason must be at least 10 characters',
|
||||||
|
|
||||||
'marketing.couponTemplate.pageTip':
|
'marketing.couponTemplate.pageTip':
|
||||||
'Published templates can be claimed; ending issuance does not invalidate issued coupons. Total quantity can only increase.',
|
'Coupons deduct base freight only and are mutex with member discount. Newcomer pack auto-grants on register; second-order coupon unlocks after first delivery success.',
|
||||||
'marketing.couponTemplate.searchPlaceholder': 'Search code / name',
|
'marketing.couponTemplate.searchPlaceholder': 'Search code / name',
|
||||||
'marketing.couponTemplate.statusFilter': 'Status',
|
'marketing.couponTemplate.statusFilter': 'Status',
|
||||||
'marketing.couponTemplate.status.draft': 'Draft',
|
'marketing.couponTemplate.status.draft': 'Draft',
|
||||||
@ -77,13 +76,13 @@ export default {
|
|||||||
'marketing.couponTemplate.status.ended': 'Ended',
|
'marketing.couponTemplate.status.ended': 'Ended',
|
||||||
'marketing.couponTemplate.modal.add': 'Add template',
|
'marketing.couponTemplate.modal.add': 'Add template',
|
||||||
'marketing.couponTemplate.modal.edit': 'Edit template',
|
'marketing.couponTemplate.modal.edit': 'Edit template',
|
||||||
'marketing.couponTemplate.modalTip': 'Min order threshold uses AmountAfterLevel. One coupon per order.',
|
'marketing.couponTemplate.modalTip': 'Threshold uses paid base freight. One coupon per order; not stacked with member discount.',
|
||||||
'marketing.couponTemplate.form.templateCode': 'Template code',
|
'marketing.couponTemplate.form.templateCode': 'Template code',
|
||||||
'marketing.couponTemplate.form.templateName': 'Name',
|
'marketing.couponTemplate.form.templateName': 'Name',
|
||||||
'marketing.couponTemplate.form.couponType': 'Type',
|
'marketing.couponTemplate.form.couponType': 'Type',
|
||||||
'marketing.couponTemplate.form.grantChannel': 'Issue type',
|
'marketing.couponTemplate.form.grantChannel': 'Issue type',
|
||||||
'marketing.couponTemplate.form.grantChannelTip':
|
'marketing.couponTemplate.form.grantChannelTip':
|
||||||
'Choose Newcomer to auto-grant every published newcomer template on successful registration',
|
'Newcomer: grant on register, second coupon unlocks after first delivery. Referral: grant both sides after invitee first delivery. Route: country codes. Festival: bulk grant.',
|
||||||
'marketing.couponTemplate.form.faceValue': 'Face value',
|
'marketing.couponTemplate.form.faceValue': 'Face value',
|
||||||
'marketing.couponTemplate.form.discountRate': 'Discount rate',
|
'marketing.couponTemplate.form.discountRate': 'Discount rate',
|
||||||
'marketing.couponTemplate.form.discountRateExtra': 'e.g. 0.95 = 5% off',
|
'marketing.couponTemplate.form.discountRateExtra': 'e.g. 0.95 = 5% off',
|
||||||
@ -91,14 +90,16 @@ export default {
|
|||||||
'marketing.couponTemplate.form.maxDiscountAmountExtra': 'Optional cap for percent-off; empty = no cap',
|
'marketing.couponTemplate.form.maxDiscountAmountExtra': 'Optional cap for percent-off; empty = no cap',
|
||||||
'marketing.couponTemplate.form.maxDiscountAmountPlaceholder': 'Optional',
|
'marketing.couponTemplate.form.maxDiscountAmountPlaceholder': 'Optional',
|
||||||
'marketing.couponTemplate.form.minOrderAmount': 'Min order amount',
|
'marketing.couponTemplate.form.minOrderAmount': 'Min order amount',
|
||||||
'marketing.couponTemplate.form.minOrderAmountExtra': 'Threshold after level discount',
|
'marketing.couponTemplate.form.minOrderAmountExtra': 'Threshold is paid base freight; 0 = no min',
|
||||||
'marketing.couponTemplate.form.totalQuantity': 'Total quantity',
|
'marketing.couponTemplate.form.totalQuantity': 'Total quantity',
|
||||||
'marketing.couponTemplate.form.totalQuantityExtra': '0 = unlimited',
|
'marketing.couponTemplate.form.totalQuantityExtra': '0 = unlimited',
|
||||||
'marketing.couponTemplate.form.perCustomerLimit': 'Per customer limit',
|
'marketing.couponTemplate.form.perCustomerLimit': 'Per customer limit',
|
||||||
'marketing.couponTemplate.form.validType': 'Validity type',
|
'marketing.couponTemplate.form.validType': 'Validity type',
|
||||||
'marketing.couponTemplate.form.validStartTime': 'Start time',
|
'marketing.couponTemplate.form.validStartTime': 'Start time',
|
||||||
'marketing.couponTemplate.form.validEndTime': 'End time',
|
'marketing.couponTemplate.form.validEndTime': 'End time',
|
||||||
'marketing.couponTemplate.form.validDaysAfterReceive': 'Valid days after claim',
|
'marketing.couponTemplate.form.validDaysAfterReceive': 'Valid days after claim/unlock',
|
||||||
|
'marketing.couponTemplate.form.applyCountries': 'Country codes',
|
||||||
|
'marketing.couponTemplate.form.applyCountriesExtra': 'For route coupons, e.g. MY,SG; empty = all countries',
|
||||||
'marketing.couponTemplate.form.remark': 'Remark',
|
'marketing.couponTemplate.form.remark': 'Remark',
|
||||||
'marketing.couponTemplate.form.requiredHint': 'Template code, name and type are required',
|
'marketing.couponTemplate.form.requiredHint': 'Template code, name and type are required',
|
||||||
'marketing.couponTemplate.columns.issued': 'Issued',
|
'marketing.couponTemplate.columns.issued': 'Issued',
|
||||||
@ -111,8 +112,12 @@ export default {
|
|||||||
'marketing.couponTemplate.grantChannel.manual': 'Manual grant',
|
'marketing.couponTemplate.grantChannel.manual': 'Manual grant',
|
||||||
'marketing.couponTemplate.grantChannel.newcomer': 'Newcomer',
|
'marketing.couponTemplate.grantChannel.newcomer': 'Newcomer',
|
||||||
'marketing.couponTemplate.grantChannel.selfReceive': 'Self claim',
|
'marketing.couponTemplate.grantChannel.selfReceive': 'Self claim',
|
||||||
|
'marketing.couponTemplate.grantChannel.referral': 'Referral',
|
||||||
|
'marketing.couponTemplate.grantChannel.route': 'Route',
|
||||||
|
'marketing.couponTemplate.grantChannel.festival': 'Festival',
|
||||||
'marketing.couponTemplate.validType.fixed': 'Fixed dates',
|
'marketing.couponTemplate.validType.fixed': 'Fixed dates',
|
||||||
'marketing.couponTemplate.validType.afterReceive': 'N days after claim',
|
'marketing.couponTemplate.validType.afterReceive': 'N days after claim',
|
||||||
|
'marketing.couponTemplate.validType.afterUnlock': 'N days after unlock',
|
||||||
'marketing.couponTemplate.actions.publish': 'Publish',
|
'marketing.couponTemplate.actions.publish': 'Publish',
|
||||||
'marketing.couponTemplate.actions.end': 'End',
|
'marketing.couponTemplate.actions.end': 'End',
|
||||||
'marketing.couponTemplate.publishConfirmTitle': 'Publish template',
|
'marketing.couponTemplate.publishConfirmTitle': 'Publish template',
|
||||||
@ -121,7 +126,7 @@ export default {
|
|||||||
'marketing.couponTemplate.endConfirmContent': 'No new claims; existing coupons remain valid.',
|
'marketing.couponTemplate.endConfirmContent': 'No new claims; existing coupons remain valid.',
|
||||||
|
|
||||||
'marketing.couponGrant.pageTip':
|
'marketing.couponGrant.pageTip':
|
||||||
'List shows issued coupon instances. Use Bulk grant to issue coupons; sub-accounts map to main customer. >100 customers or >5000 est. discount requires confirm phrase.',
|
'Issued coupon instances. Festival and route coupons can be bulk-granted here. Coupons deduct base freight only and are mutex with member discount.',
|
||||||
'marketing.couponGrant.tab.grant': 'Bulk grant',
|
'marketing.couponGrant.tab.grant': 'Bulk grant',
|
||||||
'marketing.couponGrant.tab.records': 'Records',
|
'marketing.couponGrant.tab.records': 'Records',
|
||||||
'marketing.couponGrant.modalTip': 'Grants cannot be undone. Preview first, then confirm.',
|
'marketing.couponGrant.modalTip': 'Grants cannot be undone. Preview first, then confirm.',
|
||||||
@ -159,28 +164,37 @@ export default {
|
|||||||
'marketing.couponGrant.records.columns.validEndTime': 'Valid until',
|
'marketing.couponGrant.records.columns.validEndTime': 'Valid until',
|
||||||
'marketing.couponGrant.records.columns.useTime': 'Used at',
|
'marketing.couponGrant.records.columns.useTime': 'Used at',
|
||||||
'marketing.couponGrant.instanceStatus.available': 'Available',
|
'marketing.couponGrant.instanceStatus.available': 'Available',
|
||||||
|
'marketing.couponGrant.instanceStatus.pendingUnlock': 'Locked (unlock)',
|
||||||
'marketing.couponGrant.instanceStatus.locked': 'Locked',
|
'marketing.couponGrant.instanceStatus.locked': 'Locked',
|
||||||
'marketing.couponGrant.instanceStatus.used': 'Used',
|
'marketing.couponGrant.instanceStatus.used': 'Used',
|
||||||
'marketing.couponGrant.instanceStatus.expired': 'Expired',
|
'marketing.couponGrant.instanceStatus.expired': 'Expired',
|
||||||
|
|
||||||
'marketing.commissionRule.pageTip':
|
'marketing.commissionRule.pageTip':
|
||||||
'Commission is calculated only after deliverySuccess. Example: ¥1000→¥2; ¥999 payable = ¥0. Changes do not retroactively affect settled rows.',
|
'Invite rewards use paid base freight after deliverySuccess: 5% on the 1st order (cap 50), 3% on orders 2–6 (cap 30, within 90 days of register). Below the paid-base-freight floor earns nothing. Credits freight balance after T+3 business days; refunds claw back. Invitees get a ¥5 coupon on register. Rule changes are not retroactive.',
|
||||||
'marketing.commissionRule.form.ruleName': 'Rule name',
|
'marketing.commissionRule.form.ruleName': 'Rule name',
|
||||||
'marketing.commissionRule.form.baseAmount': 'Base amount',
|
'marketing.commissionRule.form.minInviteLevelCode': 'Min invite level',
|
||||||
'marketing.commissionRule.form.baseAmountExtra': 'Floor segment size',
|
'marketing.commissionRule.form.minInviteLevelCodeExtra': 'Default V1; V0 cannot invite',
|
||||||
'marketing.commissionRule.form.commissionAmount': 'Commission per segment',
|
'marketing.commissionRule.form.minPaidBaseFreight': 'Min paid base freight',
|
||||||
'marketing.commissionRule.form.commissionAmountExtra': 'e.g. 1000→2, 999 payable = 0',
|
'marketing.commissionRule.form.minPaidBaseFreightExtra': 'Paid base freight only, excluding surcharges',
|
||||||
'marketing.commissionRule.form.freezeDays': 'Freeze days',
|
'marketing.commissionRule.form.firstOrderRate': '1st-order rate (%)',
|
||||||
'marketing.commissionRule.form.freezeDaysExtra': 'Days after deliverySuccess before settle',
|
'marketing.commissionRule.form.firstOrderRateExtra': 'First delivered order',
|
||||||
|
'marketing.commissionRule.form.firstOrderCap': '1st-order cap',
|
||||||
|
'marketing.commissionRule.form.followOrderRate': 'Follow-on rate (%)',
|
||||||
|
'marketing.commissionRule.form.followOrderRateExtra': 'Orders 2–N',
|
||||||
|
'marketing.commissionRule.form.followOrderCap': 'Follow-on cap',
|
||||||
|
'marketing.commissionRule.form.followWindowDays': 'Follow-on window (days)',
|
||||||
|
'marketing.commissionRule.form.maxRewardOrders': 'Max rewarded orders',
|
||||||
|
'marketing.commissionRule.form.settleBusinessDays': 'Settle business days',
|
||||||
|
'marketing.commissionRule.form.settleBusinessDaysExtra': 'T+N business days after deliverySuccess into freight balance',
|
||||||
'marketing.commissionRule.form.monthlyCap': 'Monthly cap',
|
'marketing.commissionRule.form.monthlyCap': 'Monthly cap',
|
||||||
'marketing.commissionRule.form.monthlyCapExtra': 'Empty = unlimited',
|
'marketing.commissionRule.form.monthlyCapExtra': 'Empty = unlimited',
|
||||||
'marketing.commissionRule.form.status': 'Status',
|
'marketing.commissionRule.form.status': 'Status',
|
||||||
'marketing.commissionRule.saveConfirmTitle': 'Save commission rule',
|
'marketing.commissionRule.saveConfirmTitle': 'Save commission rule',
|
||||||
'marketing.commissionRule.saveCompare':
|
'marketing.commissionRule.saveCompare':
|
||||||
'Base {oldBase}→{newBase}, commission {oldCommission}→{newCommission}/segment, freeze {oldFreeze}→{newFreeze} days. Settled rows unchanged.',
|
'1st {oldRate}%→{newRate}%, follow {oldFollow}%→{newFollow}%, floor {oldMin}→{newMin}, settle {oldSettle}→{newSettle} business days. Settled rows unchanged.',
|
||||||
|
|
||||||
'marketing.commissionDetail.pageTip':
|
'marketing.commissionDetail.pageTip':
|
||||||
'Commission details. Use toolbar to adjust balance or edit commission rules. Do not set Pending to Settled.',
|
'Commission details. Use toolbar to adjust balance. Edit commission rules on the Points rules page. Do not set Pending to Settled.',
|
||||||
'marketing.commissionDetail.searchPlaceholder': 'Waybill no.',
|
'marketing.commissionDetail.searchPlaceholder': 'Waybill no.',
|
||||||
'marketing.commissionDetail.statusFilter': 'Status',
|
'marketing.commissionDetail.statusFilter': 'Status',
|
||||||
'marketing.commissionDetail.status.pending': 'Pending',
|
'marketing.commissionDetail.status.pending': 'Pending',
|
||||||
@ -190,8 +204,10 @@ export default {
|
|||||||
'marketing.commissionDetail.columns.referrer': 'Referrer',
|
'marketing.commissionDetail.columns.referrer': 'Referrer',
|
||||||
'marketing.commissionDetail.columns.referee': 'Referee',
|
'marketing.commissionDetail.columns.referee': 'Referee',
|
||||||
'marketing.commissionDetail.columns.orderNo': 'Waybill',
|
'marketing.commissionDetail.columns.orderNo': 'Waybill',
|
||||||
'marketing.commissionDetail.columns.baseAmount': 'Base amount',
|
'marketing.commissionDetail.columns.baseAmount': 'Paid base freight',
|
||||||
'marketing.commissionDetail.columns.commission': 'Commission',
|
'marketing.commissionDetail.columns.commission': 'Reward',
|
||||||
|
'marketing.commissionDetail.columns.orderIndex': 'Order #',
|
||||||
|
'marketing.commissionDetail.columns.settleDue': 'Settle due',
|
||||||
'marketing.commissionDetail.columns.status': 'Status',
|
'marketing.commissionDetail.columns.status': 'Status',
|
||||||
'marketing.commissionDetail.columns.eligibleTime': 'Eligible time',
|
'marketing.commissionDetail.columns.eligibleTime': 'Eligible time',
|
||||||
'marketing.commissionDetail.columns.settleTime': 'Settle time',
|
'marketing.commissionDetail.columns.settleTime': 'Settle time',
|
||||||
@ -276,7 +292,8 @@ export default {
|
|||||||
'marketing.portal.common.eyebrow': 'Marketing',
|
'marketing.portal.common.eyebrow': 'Marketing',
|
||||||
|
|
||||||
'marketing.portal.invite.title': 'Invite Friends',
|
'marketing.portal.invite.title': 'Invite Friends',
|
||||||
'marketing.portal.invite.subtitle': 'Share your link. After friends register and get delivered, you earn referral commission.',
|
'marketing.portal.invite.subtitle':
|
||||||
|
'V1+ can invite. Friends get ¥5 on register. After their orders are delivered and paid base freight meets the floor, you earn a percent into freight balance.',
|
||||||
'marketing.portal.invite.howTitle': '3 quick steps',
|
'marketing.portal.invite.howTitle': '3 quick steps',
|
||||||
'marketing.portal.invite.howHint': 'Share in about a minute',
|
'marketing.portal.invite.howHint': 'Share in about a minute',
|
||||||
'marketing.portal.invite.how1Title': 'Copy your invite',
|
'marketing.portal.invite.how1Title': 'Copy your invite',
|
||||||
@ -284,8 +301,14 @@ export default {
|
|||||||
'marketing.portal.invite.how2Title': 'Friends register',
|
'marketing.portal.invite.how2Title': 'Friends register',
|
||||||
'marketing.portal.invite.how2Desc':
|
'marketing.portal.invite.how2Desc':
|
||||||
'Friends open the link in WeChat and finish official-account registration to bind the referral',
|
'Friends open the link in WeChat and finish official-account registration to bind the referral',
|
||||||
'marketing.portal.invite.how3Title': 'Earn after delivery',
|
'marketing.portal.invite.how3Title': 'Wait for delivered orders',
|
||||||
'marketing.portal.invite.how3Desc': 'When their order is delivered, commission goes to Referral Earnings',
|
'marketing.portal.invite.how3Desc':
|
||||||
|
'Friends get ¥5 on register. You earn 5% of paid base freight on their 1st delivered order and 3% on orders 2–6, credited to freight balance after T+3 business days.',
|
||||||
|
'marketing.portal.invite.lockedTitle': 'Upgrade to {level} to invite friends',
|
||||||
|
'marketing.portal.invite.lockedDesc': 'Your current level cannot invite. Place orders to earn points and upgrade.',
|
||||||
|
'marketing.portal.invite.pendingReward': 'Pending',
|
||||||
|
'marketing.portal.invite.settledReward': 'Credited',
|
||||||
|
'marketing.portal.invite.tag.reward': '{count} rewarded orders',
|
||||||
'marketing.portal.invite.yourCode': 'Your invite code',
|
'marketing.portal.invite.yourCode': 'Your invite code',
|
||||||
'marketing.portal.invite.code': 'Invite code',
|
'marketing.portal.invite.code': 'Invite code',
|
||||||
'marketing.portal.invite.link': 'Invite link',
|
'marketing.portal.invite.link': 'Invite link',
|
||||||
@ -302,7 +325,7 @@ export default {
|
|||||||
'marketing.portal.invite.step3': 'After their order is delivered, you earn commission',
|
'marketing.portal.invite.step3': 'After their order is delivered, you earn commission',
|
||||||
'marketing.portal.invite.refereeCount': 'Invited friends',
|
'marketing.portal.invite.refereeCount': 'Invited friends',
|
||||||
'marketing.portal.invite.earnTitle': 'View earnings',
|
'marketing.portal.invite.earnTitle': 'View earnings',
|
||||||
'marketing.portal.invite.earnDesc': 'Commission settles after delivery success',
|
'marketing.portal.invite.earnDesc': 'Credits freight balance T+3 business days after delivery',
|
||||||
'marketing.portal.invite.levelTitle': 'My membership',
|
'marketing.portal.invite.levelTitle': 'My membership',
|
||||||
'marketing.portal.invite.levelDesc': 'Earn points from paid orders to upgrade membership',
|
'marketing.portal.invite.levelDesc': 'Earn points from paid orders to upgrade membership',
|
||||||
'marketing.portal.invite.refereeTitle': 'My referees',
|
'marketing.portal.invite.refereeTitle': 'My referees',
|
||||||
@ -314,28 +337,29 @@ export default {
|
|||||||
'marketing.portal.invite.tag.delivered': 'Delivered',
|
'marketing.portal.invite.tag.delivered': 'Delivered',
|
||||||
|
|
||||||
'marketing.portal.member.title': 'My Level',
|
'marketing.portal.member.title': 'My Level',
|
||||||
'marketing.portal.member.subtitle': 'Points accumulate after successful payment. Higher levels unlock better extra discounts.',
|
'marketing.portal.member.subtitle':
|
||||||
'marketing.portal.member.howTitle': 'How to level up',
|
'Level is based on paid base freight in the last 12 months. V4 Diamond and V5 Crown are not protected; volume drops can downgrade. Surcharges are not discounted.',
|
||||||
'marketing.portal.member.howHint': 'More orders, more points',
|
'marketing.portal.member.howTitle': 'How levels work',
|
||||||
'marketing.portal.member.how1Title': 'Place and pay',
|
'marketing.portal.member.howHint': 'Rolling 12-month base-freight points',
|
||||||
'marketing.portal.member.how1Desc': 'Points are granted automatically after successful payment',
|
'marketing.portal.member.how1Title': 'Pay base freight',
|
||||||
'marketing.portal.member.how2Title': 'Accumulate to upgrade',
|
'marketing.portal.member.how1Desc': '1 yuan of paid base freight = 1 point; surcharges and coupons excluded',
|
||||||
'marketing.portal.member.how2Desc': 'Reach the next threshold to upgrade your membership level',
|
'marketing.portal.member.how2Title': 'Rolling 12-month level',
|
||||||
'marketing.portal.member.how3Title': 'Enjoy better rates',
|
'marketing.portal.member.how2Desc': 'Daily rebuild; upgrades apply immediately',
|
||||||
'marketing.portal.member.how3Desc': 'Higher levels unlock better extra discounts',
|
'marketing.portal.member.how3Title': 'Base-freight discount',
|
||||||
|
'marketing.portal.member.how3Desc': 'Up to 5% off base freight only; coupons do not stack — best of the two',
|
||||||
'marketing.portal.member.currentLevel': 'Current level',
|
'marketing.portal.member.currentLevel': 'Current level',
|
||||||
'marketing.portal.member.defaultLevel': 'Standard',
|
'marketing.portal.member.defaultLevel': 'Standard',
|
||||||
'marketing.portal.member.priceTier': 'Price tier',
|
'marketing.portal.member.priceTier': 'Price tier',
|
||||||
'marketing.portal.member.totalPoints': 'Total points',
|
'marketing.portal.member.totalPoints': '12-month points',
|
||||||
'marketing.portal.member.availablePoints': 'Available points',
|
'marketing.portal.member.availablePoints': 'Available points',
|
||||||
'marketing.portal.member.extraDiscount': 'Extra discount',
|
'marketing.portal.member.extraDiscount': 'Base-freight discount',
|
||||||
'marketing.portal.member.nextLevel': 'Next: {name}',
|
'marketing.portal.member.nextLevel': 'Next: {name}',
|
||||||
'marketing.portal.member.needPoints': '{points} more points to upgrade',
|
'marketing.portal.member.needPoints': '{points} more points to the next level',
|
||||||
'marketing.portal.member.topLevelTip': 'You are at the top level. Keep ordering to earn points.',
|
'marketing.portal.member.topLevelTip': 'Top level. V4/V5 can step down if volume drops.',
|
||||||
'marketing.portal.member.pointsFlowTitle': 'Points history',
|
'marketing.portal.member.pointsFlowTitle': 'Points history',
|
||||||
'marketing.portal.member.flowHint': 'Every grant and deduction shows here',
|
'marketing.portal.member.flowHint': 'For leveling only — cannot offset freight',
|
||||||
'marketing.portal.member.emptyFlowTitle': 'No points history yet',
|
'marketing.portal.member.emptyFlowTitle': 'No points history yet',
|
||||||
'marketing.portal.member.emptyFlowDesc': 'Points are granted after successful payment',
|
'marketing.portal.member.emptyFlowDesc': 'Points are granted from paid base freight',
|
||||||
'marketing.portal.member.goOrder': 'Place an order',
|
'marketing.portal.member.goOrder': 'Place an order',
|
||||||
'marketing.portal.member.flow.changeType': 'Type',
|
'marketing.portal.member.flow.changeType': 'Type',
|
||||||
'marketing.portal.member.flow.pointsDelta': 'Delta',
|
'marketing.portal.member.flow.pointsDelta': 'Delta',
|
||||||
@ -346,23 +370,26 @@ export default {
|
|||||||
'marketing.portal.coupons.title': 'My Coupons',
|
'marketing.portal.coupons.title': 'My Coupons',
|
||||||
'marketing.portal.coupons.subtitle': 'One coupon per order. Non-transferable. Sub-accounts use the main account pack.',
|
'marketing.portal.coupons.subtitle': 'One coupon per order. Non-transferable. Sub-accounts use the main account pack.',
|
||||||
'marketing.portal.coupons.howTitle': 'How to use coupons',
|
'marketing.portal.coupons.howTitle': 'How to use coupons',
|
||||||
'marketing.portal.coupons.howHint': 'One per order at checkout',
|
'marketing.portal.coupons.howHint': 'One per order; base freight only; mutex with member discount',
|
||||||
'marketing.portal.coupons.how1Title': 'Check available',
|
'marketing.portal.coupons.how1Title': 'Check coupons',
|
||||||
'marketing.portal.coupons.how1Desc': 'Confirm face value, min order, and validity in Available',
|
'marketing.portal.coupons.how1Desc': 'Locked coupons unlock after first delivery; check face value and min order in Available',
|
||||||
'marketing.portal.coupons.how2Title': 'Select at checkout',
|
'marketing.portal.coupons.how2Title': 'Select at checkout',
|
||||||
'marketing.portal.coupons.how2Desc': 'Apply one coupon on the place-order settlement page',
|
'marketing.portal.coupons.how2Desc': 'Apply one coupon; it only deducts base freight, not surcharges',
|
||||||
'marketing.portal.coupons.how3Title': 'Watch expiry',
|
'marketing.portal.coupons.how3Title': 'Watch expiry',
|
||||||
'marketing.portal.coupons.how3Desc': 'Expiring-soon coupons are highlighted—use them soon',
|
'marketing.portal.coupons.how3Desc': 'You get an in-app notice 3 days before expiry',
|
||||||
'marketing.portal.coupons.tab.available': 'Available',
|
'marketing.portal.coupons.tab.available': 'Available',
|
||||||
|
'marketing.portal.coupons.tab.locked': 'Locked',
|
||||||
'marketing.portal.coupons.tab.used': 'Used',
|
'marketing.portal.coupons.tab.used': 'Used',
|
||||||
'marketing.portal.coupons.tab.expired': 'Expired',
|
'marketing.portal.coupons.tab.expired': 'Expired',
|
||||||
'marketing.portal.coupons.empty': 'No coupons',
|
'marketing.portal.coupons.empty': 'No coupons',
|
||||||
'marketing.portal.coupons.emptyTitle': 'No available coupons',
|
'marketing.portal.coupons.emptyTitle': 'No available coupons',
|
||||||
'marketing.portal.coupons.emptyDesc': 'Watch for campaigns, or place an order to check rates',
|
'marketing.portal.coupons.emptyDesc': 'Watch for campaigns, or place an order to check rates',
|
||||||
|
'marketing.portal.coupons.emptyLockedTitle': 'No locked coupons',
|
||||||
'marketing.portal.coupons.emptyUsedTitle': 'No usage history',
|
'marketing.portal.coupons.emptyUsedTitle': 'No usage history',
|
||||||
'marketing.portal.coupons.emptyExpiredTitle': 'No expired coupons',
|
'marketing.portal.coupons.emptyExpiredTitle': 'No expired coupons',
|
||||||
'marketing.portal.coupons.emptyOtherDesc': 'Switch to Available to see your current pack',
|
'marketing.portal.coupons.emptyOtherDesc': 'Switch to Available to see your current pack',
|
||||||
'marketing.portal.coupons.minOrder': 'Min order ¥{amount}',
|
'marketing.portal.coupons.minOrder': 'Min order ¥{amount}',
|
||||||
|
'marketing.portal.coupons.unlockHint': 'Complete first delivery to unlock the ¥15 coupon',
|
||||||
'marketing.portal.coupons.expiringSoon': 'Expiring soon',
|
'marketing.portal.coupons.expiringSoon': 'Expiring soon',
|
||||||
'marketing.portal.coupons.discountUnit': ' off',
|
'marketing.portal.coupons.discountUnit': ' off',
|
||||||
'marketing.portal.coupons.unnamed': 'Coupon',
|
'marketing.portal.coupons.unnamed': 'Coupon',
|
||||||
@ -425,10 +452,10 @@ export default {
|
|||||||
'marketing.priceCheck.couponUnavailable': 'Unavailable',
|
'marketing.priceCheck.couponUnavailable': 'Unavailable',
|
||||||
'marketing.priceCheck.couponChanged': 'Price recalculated — confirm payable amount',
|
'marketing.priceCheck.couponChanged': 'Price recalculated — confirm payable amount',
|
||||||
'marketing.priceCheck.breakdownTitle': 'Price breakdown',
|
'marketing.priceCheck.breakdownTitle': 'Price breakdown',
|
||||||
'marketing.priceCheck.breakdown.receivableSubtotal': 'Subtotal after tier',
|
'marketing.priceCheck.breakdown.receivableSubtotal': 'Fee subtotal',
|
||||||
'marketing.priceCheck.breakdown.levelDiscount': 'Level extra discount',
|
'marketing.priceCheck.breakdown.levelDiscount': 'Member base-freight discount',
|
||||||
'marketing.priceCheck.breakdown.amountAfterLevel': 'Before coupon',
|
'marketing.priceCheck.breakdown.amountAfterLevel': 'After member discount',
|
||||||
'marketing.priceCheck.breakdown.couponDiscount': 'Coupon discount',
|
'marketing.priceCheck.breakdown.couponDiscount': 'Coupon (best of member vs coupon)',
|
||||||
'marketing.priceCheck.breakdown.payable': 'Payable',
|
'marketing.priceCheck.breakdown.payable': 'Payable',
|
||||||
|
|
||||||
'marketing.register.title': 'Register',
|
'marketing.register.title': 'Register',
|
||||||
@ -463,7 +490,7 @@ export default {
|
|||||||
'profile.marketing.coupons': 'My coupons',
|
'profile.marketing.coupons': 'My coupons',
|
||||||
'profile.marketing.commission': 'Referral earnings',
|
'profile.marketing.commission': 'Referral earnings',
|
||||||
'profile.marketing.inviteDesc': 'Share link and grow referees',
|
'profile.marketing.inviteDesc': 'Share link and grow referees',
|
||||||
'profile.marketing.memberDesc': 'Points and membership level',
|
'profile.marketing.memberDesc': 'Rolling 12-month points and membership level',
|
||||||
'profile.marketing.couponsDesc': 'View available coupons',
|
'profile.marketing.couponsDesc': 'View available coupons',
|
||||||
'profile.marketing.commissionDesc': 'Commission, transfer, withdraw',
|
'profile.marketing.commissionDesc': 'Commission, transfer, withdraw',
|
||||||
'profile.marketing.navigateFailed': 'Failed to open page, please refresh and try again',
|
'profile.marketing.navigateFailed': 'Failed to open page, please refresh and try again',
|
||||||
|
|||||||
@ -26,20 +26,19 @@ export default {
|
|||||||
'将向 {count} 个客户发放「{templateName}」共 {total} 张,预估最大抵扣 {amount} 元,发放后不可撤回。',
|
'将向 {count} 个客户发放「{templateName}」共 {total} 张,预估最大抵扣 {amount} 元,发放后不可撤回。',
|
||||||
|
|
||||||
'marketing.pointsRule.pageTip':
|
'marketing.pointsRule.pageTip':
|
||||||
'积分在支付成功后按实付金额发放;修改比例不追溯历史。列表查询与人工调账见「积分流水」页签,调账原因不少于 10 字。',
|
'实付基础运费 1 元 = 1 积分;燃油、偏远、清关、退件、关税、赔偿及优惠券抵扣不计积分。积分只用于滚动 12 个月定级,不可抵运费、兑礼或提现。',
|
||||||
'marketing.pointsRule.tab.rule': '规则配置',
|
'marketing.pointsRule.sectionTitle': '积分规则',
|
||||||
'marketing.pointsRule.tab.flow': '积分流水',
|
'marketing.pointsRule.tab.flow': '积分流水',
|
||||||
'marketing.pointsRule.form.ruleName': '规则名称',
|
'marketing.pointsRule.form.ruleName': '规则名称',
|
||||||
'marketing.pointsRule.form.pointsPerYuan': '每元得积分',
|
'marketing.pointsRule.form.pointsPerYuan': '每元基础运费积分',
|
||||||
'marketing.pointsRule.form.pointsPerYuanExtra': '基数为应付实付金额',
|
'marketing.pointsRule.form.pointsPerYuanExtra': '基数为实付基础运费,不含附加费与券抵扣',
|
||||||
'marketing.pointsRule.form.firstOrderMultiplier': '首单倍数',
|
|
||||||
'marketing.pointsRule.form.maxPointsPerOrder': '单笔积分上限',
|
|
||||||
'marketing.pointsRule.form.status': '状态',
|
'marketing.pointsRule.form.status': '状态',
|
||||||
'marketing.pointsRule.saveConfirmTitle': '确认保存积分规则',
|
'marketing.pointsRule.saveConfirmTitle': '确认保存积分规则',
|
||||||
'marketing.pointsRule.saveConfirmContent': '保存后仅影响之后的订单积分发放,不会追溯已支付订单。',
|
'marketing.pointsRule.saveConfirmContent':
|
||||||
|
'保存后仅影响之后支付的订单积分;改比例不追溯。附加费不计积分,积分不可抵扣运费。',
|
||||||
|
|
||||||
'marketing.pointsFlow.pageTip':
|
'marketing.pointsFlow.pageTip':
|
||||||
'积分流水记录支付发放、退款扣回与人工调账。可按客户编号筛选;调账原因不少于 10 字。规则配置请点工具栏「规则配置」。',
|
'流水记录基础运费发放、退款/删单扣回与人工调账。定级看近 12 个月合计,不是终身累计。积分不可抵运费。',
|
||||||
'marketing.pointsFlow.searchPlaceholder': '搜索备注/业务号',
|
'marketing.pointsFlow.searchPlaceholder': '搜索备注/业务号',
|
||||||
'marketing.pointsFlow.customerCodePlaceholder': '客户编号',
|
'marketing.pointsFlow.customerCodePlaceholder': '客户编号',
|
||||||
'marketing.pointsFlow.changeTypeFilter': '变动类型',
|
'marketing.pointsFlow.changeTypeFilter': '变动类型',
|
||||||
@ -69,7 +68,7 @@ export default {
|
|||||||
'marketing.pointsFlow.adjustModal.remarkMin': '调账原因不少于 10 字',
|
'marketing.pointsFlow.adjustModal.remarkMin': '调账原因不少于 10 字',
|
||||||
|
|
||||||
'marketing.couponTemplate.pageTip':
|
'marketing.couponTemplate.pageTip':
|
||||||
'券模板发布后客户可领取或使用;停发不影响已领券。面额/门槛修改不追溯已发券。总量只允许增大。',
|
'券只抵基础运费,与会员折扣互斥取更优。新人券注册自动发;第二单券等首单派送成功后解锁。线路定向券填写国家代码。',
|
||||||
'marketing.couponTemplate.searchPlaceholder': '搜索编码/名称',
|
'marketing.couponTemplate.searchPlaceholder': '搜索编码/名称',
|
||||||
'marketing.couponTemplate.statusFilter': '状态',
|
'marketing.couponTemplate.statusFilter': '状态',
|
||||||
'marketing.couponTemplate.status.draft': '草稿',
|
'marketing.couponTemplate.status.draft': '草稿',
|
||||||
@ -77,13 +76,13 @@ export default {
|
|||||||
'marketing.couponTemplate.status.ended': '已停发',
|
'marketing.couponTemplate.status.ended': '已停发',
|
||||||
'marketing.couponTemplate.modal.add': '新增券模板',
|
'marketing.couponTemplate.modal.add': '新增券模板',
|
||||||
'marketing.couponTemplate.modal.edit': '编辑券模板',
|
'marketing.couponTemplate.modal.edit': '编辑券模板',
|
||||||
'marketing.couponTemplate.modalTip': '满减门槛按等级折后金额(AmountAfterLevel)判断。每单限用 1 张券。',
|
'marketing.couponTemplate.modalTip': '门槛按实付基础运费判断,附加费不可用券。每单限用 1 张,不与会员折叠加。',
|
||||||
'marketing.couponTemplate.form.templateCode': '模板编码',
|
'marketing.couponTemplate.form.templateCode': '模板编码',
|
||||||
'marketing.couponTemplate.form.templateName': '券名称',
|
'marketing.couponTemplate.form.templateName': '券名称',
|
||||||
'marketing.couponTemplate.form.couponType': '券类型',
|
'marketing.couponTemplate.form.couponType': '券类型',
|
||||||
'marketing.couponTemplate.form.grantChannel': '发放类型',
|
'marketing.couponTemplate.form.grantChannel': '发放类型',
|
||||||
'marketing.couponTemplate.form.grantChannelTip':
|
'marketing.couponTemplate.form.grantChannelTip':
|
||||||
'选「新人券」后,客户注册成功会自动发放全部已发布的新人券模板(有多少发多少)',
|
'新人券:注册发首单券/备货券,第二单券待派送成功解锁。裂变券:新客首单派送成功后给双方发券。线路定向:填适用国家。节日券:批量发放。',
|
||||||
'marketing.couponTemplate.form.faceValue': '面额',
|
'marketing.couponTemplate.form.faceValue': '面额',
|
||||||
'marketing.couponTemplate.form.discountRate': '折扣率',
|
'marketing.couponTemplate.form.discountRate': '折扣率',
|
||||||
'marketing.couponTemplate.form.discountRateExtra': '如 0.95 表示 95 折',
|
'marketing.couponTemplate.form.discountRateExtra': '如 0.95 表示 95 折',
|
||||||
@ -91,14 +90,16 @@ export default {
|
|||||||
'marketing.couponTemplate.form.maxDiscountAmountExtra': '折扣券可选封顶金额,空表示不封顶',
|
'marketing.couponTemplate.form.maxDiscountAmountExtra': '折扣券可选封顶金额,空表示不封顶',
|
||||||
'marketing.couponTemplate.form.maxDiscountAmountPlaceholder': '可选',
|
'marketing.couponTemplate.form.maxDiscountAmountPlaceholder': '可选',
|
||||||
'marketing.couponTemplate.form.minOrderAmount': '使用门槛',
|
'marketing.couponTemplate.form.minOrderAmount': '使用门槛',
|
||||||
'marketing.couponTemplate.form.minOrderAmountExtra': '门槛基数为等级折后金额',
|
'marketing.couponTemplate.form.minOrderAmountExtra': '门槛基数为实付基础运费,0 表示无门槛',
|
||||||
'marketing.couponTemplate.form.totalQuantity': '发放总量',
|
'marketing.couponTemplate.form.totalQuantity': '发放总量',
|
||||||
'marketing.couponTemplate.form.totalQuantityExtra': '0 表示不限',
|
'marketing.couponTemplate.form.totalQuantityExtra': '0 表示不限',
|
||||||
'marketing.couponTemplate.form.perCustomerLimit': '每人上限',
|
'marketing.couponTemplate.form.perCustomerLimit': '每人上限',
|
||||||
'marketing.couponTemplate.form.validType': '有效期类型',
|
'marketing.couponTemplate.form.validType': '有效期类型',
|
||||||
'marketing.couponTemplate.form.validStartTime': '开始时间',
|
'marketing.couponTemplate.form.validStartTime': '开始时间',
|
||||||
'marketing.couponTemplate.form.validEndTime': '结束时间',
|
'marketing.couponTemplate.form.validEndTime': '结束时间',
|
||||||
'marketing.couponTemplate.form.validDaysAfterReceive': '领取后有效天数',
|
'marketing.couponTemplate.form.validDaysAfterReceive': '领取/解锁后有效天数',
|
||||||
|
'marketing.couponTemplate.form.applyCountries': '适用国家',
|
||||||
|
'marketing.couponTemplate.form.applyCountriesExtra': '线路定向券填写,如 MY,SG;空表示不限国家',
|
||||||
'marketing.couponTemplate.form.remark': '备注',
|
'marketing.couponTemplate.form.remark': '备注',
|
||||||
'marketing.couponTemplate.form.requiredHint': '请填写模板编码、名称与券类型',
|
'marketing.couponTemplate.form.requiredHint': '请填写模板编码、名称与券类型',
|
||||||
'marketing.couponTemplate.columns.issued': '已发',
|
'marketing.couponTemplate.columns.issued': '已发',
|
||||||
@ -111,8 +112,12 @@ export default {
|
|||||||
'marketing.couponTemplate.grantChannel.manual': '手动发放',
|
'marketing.couponTemplate.grantChannel.manual': '手动发放',
|
||||||
'marketing.couponTemplate.grantChannel.newcomer': '新人券',
|
'marketing.couponTemplate.grantChannel.newcomer': '新人券',
|
||||||
'marketing.couponTemplate.grantChannel.selfReceive': '客户自领',
|
'marketing.couponTemplate.grantChannel.selfReceive': '客户自领',
|
||||||
|
'marketing.couponTemplate.grantChannel.referral': '裂变券',
|
||||||
|
'marketing.couponTemplate.grantChannel.route': '线路定向券',
|
||||||
|
'marketing.couponTemplate.grantChannel.festival': '节日券',
|
||||||
'marketing.couponTemplate.validType.fixed': '固定日期',
|
'marketing.couponTemplate.validType.fixed': '固定日期',
|
||||||
'marketing.couponTemplate.validType.afterReceive': '领取后 N 天',
|
'marketing.couponTemplate.validType.afterReceive': '领取后 N 天',
|
||||||
|
'marketing.couponTemplate.validType.afterUnlock': '解锁后 N 天',
|
||||||
'marketing.couponTemplate.actions.publish': '发布',
|
'marketing.couponTemplate.actions.publish': '发布',
|
||||||
'marketing.couponTemplate.actions.end': '停发',
|
'marketing.couponTemplate.actions.end': '停发',
|
||||||
'marketing.couponTemplate.publishConfirmTitle': '确认发布券模板',
|
'marketing.couponTemplate.publishConfirmTitle': '确认发布券模板',
|
||||||
@ -121,7 +126,7 @@ export default {
|
|||||||
'marketing.couponTemplate.endConfirmContent': '停发后不可新领,已领券仍可使用。',
|
'marketing.couponTemplate.endConfirmContent': '停发后不可新领,已领券仍可使用。',
|
||||||
|
|
||||||
'marketing.couponGrant.pageTip':
|
'marketing.couponGrant.pageTip':
|
||||||
'列表为已发放的券实例。点「批量发放」可向客户发券;子账号编号自动归主客户。超过 100 人或预估抵扣超过 5000 元须输入「确认发放」。',
|
'列表为已发放的券实例。节日券、线路定向券可在此批量发放。券只抵基础运费,与会员折扣互斥。',
|
||||||
'marketing.couponGrant.tab.grant': '批量发放',
|
'marketing.couponGrant.tab.grant': '批量发放',
|
||||||
'marketing.couponGrant.tab.records': '发放记录',
|
'marketing.couponGrant.tab.records': '发放记录',
|
||||||
'marketing.couponGrant.modalTip': '发券后不可撤回。请先预览人数与预估抵扣,再确认发放。',
|
'marketing.couponGrant.modalTip': '发券后不可撤回。请先预览人数与预估抵扣,再确认发放。',
|
||||||
@ -159,28 +164,37 @@ export default {
|
|||||||
'marketing.couponGrant.records.columns.validEndTime': '有效期至',
|
'marketing.couponGrant.records.columns.validEndTime': '有效期至',
|
||||||
'marketing.couponGrant.records.columns.useTime': '使用时间',
|
'marketing.couponGrant.records.columns.useTime': '使用时间',
|
||||||
'marketing.couponGrant.instanceStatus.available': '可用',
|
'marketing.couponGrant.instanceStatus.available': '可用',
|
||||||
|
'marketing.couponGrant.instanceStatus.pendingUnlock': '未解锁',
|
||||||
'marketing.couponGrant.instanceStatus.locked': '预占中',
|
'marketing.couponGrant.instanceStatus.locked': '预占中',
|
||||||
'marketing.couponGrant.instanceStatus.used': '已使用',
|
'marketing.couponGrant.instanceStatus.used': '已使用',
|
||||||
'marketing.couponGrant.instanceStatus.expired': '已过期',
|
'marketing.couponGrant.instanceStatus.expired': '已过期',
|
||||||
|
|
||||||
'marketing.commissionRule.pageTip':
|
'marketing.commissionRule.pageTip':
|
||||||
'分成仅在订单状态为「派送成功」(deliverySuccess) 后计算并进入冻结期。例:每满 1000 元得 2 元,实付 999 元 = 0 分成。改规则不追溯历史。',
|
'邀请奖励按被邀请人派送成功订单的实付基础运费计:第 1 单 5%(封顶 50 元),第 2–6 单 3%(封顶 30 元,注册 90 天内)。实付基础运费不足门槛不计奖。T+3 个工作日入运费余额,退款扣回。被邀请人注册即发 5 元券。改规则不追溯。',
|
||||||
'marketing.commissionRule.form.ruleName': '规则名称',
|
'marketing.commissionRule.form.ruleName': '规则名称',
|
||||||
'marketing.commissionRule.form.baseAmount': '基数金额(元)',
|
'marketing.commissionRule.form.minInviteLevelCode': '邀请资格最低等级',
|
||||||
'marketing.commissionRule.form.baseAmountExtra': 'floor 整段:实付每满此金额计一段',
|
'marketing.commissionRule.form.minInviteLevelCodeExtra': '默认 V1,V0 不可邀请',
|
||||||
'marketing.commissionRule.form.commissionAmount': '每段分成(元)',
|
'marketing.commissionRule.form.minPaidBaseFreight': '计奖门槛(实付基础运费)',
|
||||||
'marketing.commissionRule.form.commissionAmountExtra': '例:1000→2,999 元实付 = 0 分成',
|
'marketing.commissionRule.form.minPaidBaseFreightExtra': '按实付基础运费,不含附加费',
|
||||||
'marketing.commissionRule.form.freezeDays': '冻结天数',
|
'marketing.commissionRule.form.firstOrderRate': '首单比例(%)',
|
||||||
'marketing.commissionRule.form.freezeDaysExtra': '派送成功后可结算的等待天数',
|
'marketing.commissionRule.form.firstOrderRateExtra': '第 1 笔派送成功订单',
|
||||||
|
'marketing.commissionRule.form.firstOrderCap': '首单封顶(元)',
|
||||||
|
'marketing.commissionRule.form.followOrderRate': '后续单比例(%)',
|
||||||
|
'marketing.commissionRule.form.followOrderRateExtra': '第 2–N 单',
|
||||||
|
'marketing.commissionRule.form.followOrderCap': '后续单封顶(元)',
|
||||||
|
'marketing.commissionRule.form.followWindowDays': '后续单窗口(天)',
|
||||||
|
'marketing.commissionRule.form.maxRewardOrders': '最多计奖单数',
|
||||||
|
'marketing.commissionRule.form.settleBusinessDays': '结算工作日',
|
||||||
|
'marketing.commissionRule.form.settleBusinessDaysExtra': '派送成功后 T+N 个工作日入运费余额',
|
||||||
'marketing.commissionRule.form.monthlyCap': '单月封顶(元)',
|
'marketing.commissionRule.form.monthlyCap': '单月封顶(元)',
|
||||||
'marketing.commissionRule.form.monthlyCapExtra': '留空表示不限',
|
'marketing.commissionRule.form.monthlyCapExtra': '留空表示不限',
|
||||||
'marketing.commissionRule.form.status': '状态',
|
'marketing.commissionRule.form.status': '状态',
|
||||||
'marketing.commissionRule.saveConfirmTitle': '确认保存分成规则',
|
'marketing.commissionRule.saveConfirmTitle': '确认保存分成规则',
|
||||||
'marketing.commissionRule.saveCompare':
|
'marketing.commissionRule.saveCompare':
|
||||||
'基数 {oldBase}→{newBase} 元,分成 {oldCommission}→{newCommission} 元/段,冻结 {oldFreeze}→{newFreeze} 天。不影响已结算明细。',
|
'首单 {oldRate}%→{newRate}%,后续 {oldFollow}%→{newFollow}%,门槛 {oldMin}→{newMin} 元,结算 {oldSettle}→{newSettle} 个工作日。不影响已结算明细。',
|
||||||
|
|
||||||
'marketing.commissionDetail.pageTip':
|
'marketing.commissionDetail.pageTip':
|
||||||
'查询分成明细。工具栏可人工调整余额,或打开分成规则配置。禁止将待结算直接改为已结算。',
|
'查询分成明细。工具栏可人工调整余额。分成规则请到「积分规则」页配置。禁止将待结算直接改为已结算。',
|
||||||
'marketing.commissionDetail.searchPlaceholder': '运单号',
|
'marketing.commissionDetail.searchPlaceholder': '运单号',
|
||||||
'marketing.commissionDetail.statusFilter': '状态',
|
'marketing.commissionDetail.statusFilter': '状态',
|
||||||
'marketing.commissionDetail.status.pending': '待结算',
|
'marketing.commissionDetail.status.pending': '待结算',
|
||||||
@ -190,8 +204,10 @@ export default {
|
|||||||
'marketing.commissionDetail.columns.referrer': '引荐人',
|
'marketing.commissionDetail.columns.referrer': '引荐人',
|
||||||
'marketing.commissionDetail.columns.referee': '下级',
|
'marketing.commissionDetail.columns.referee': '下级',
|
||||||
'marketing.commissionDetail.columns.orderNo': '运单号',
|
'marketing.commissionDetail.columns.orderNo': '运单号',
|
||||||
'marketing.commissionDetail.columns.baseAmount': '分成基数',
|
'marketing.commissionDetail.columns.baseAmount': '实付基础运费',
|
||||||
'marketing.commissionDetail.columns.commission': '分成金额',
|
'marketing.commissionDetail.columns.commission': '奖励金额',
|
||||||
|
'marketing.commissionDetail.columns.orderIndex': '第几单',
|
||||||
|
'marketing.commissionDetail.columns.settleDue': '计划结算',
|
||||||
'marketing.commissionDetail.columns.status': '状态',
|
'marketing.commissionDetail.columns.status': '状态',
|
||||||
'marketing.commissionDetail.columns.eligibleTime': '派送成功时间',
|
'marketing.commissionDetail.columns.eligibleTime': '派送成功时间',
|
||||||
'marketing.commissionDetail.columns.settleTime': '结算时间',
|
'marketing.commissionDetail.columns.settleTime': '结算时间',
|
||||||
@ -276,15 +292,20 @@ export default {
|
|||||||
'marketing.portal.common.eyebrow': '营销中心',
|
'marketing.portal.common.eyebrow': '营销中心',
|
||||||
|
|
||||||
'marketing.portal.invite.title': '邀请好友',
|
'marketing.portal.invite.title': '邀请好友',
|
||||||
'marketing.portal.invite.subtitle': '分享专属链接,好友注册并下单后,您可获得推荐收益。',
|
'marketing.portal.invite.subtitle': 'V1 及以上可邀请。好友注册即得 5 元券;其订单派送成功且实付基础运费达标后,您按比例获得运费余额奖励。',
|
||||||
'marketing.portal.invite.howTitle': '三步上手',
|
'marketing.portal.invite.howTitle': '三步上手',
|
||||||
'marketing.portal.invite.howHint': '大约 1 分钟完成分享',
|
'marketing.portal.invite.howHint': '大约 1 分钟完成分享',
|
||||||
'marketing.portal.invite.how1Title': '复制邀请信息',
|
'marketing.portal.invite.how1Title': '复制邀请信息',
|
||||||
'marketing.portal.invite.how1Desc': '复制邀请码、链接,或保存右侧海报发给好友',
|
'marketing.portal.invite.how1Desc': '复制邀请码、链接,或保存右侧海报发给好友',
|
||||||
'marketing.portal.invite.how2Title': '好友完成注册',
|
'marketing.portal.invite.how2Title': '好友完成注册',
|
||||||
'marketing.portal.invite.how2Desc': '好友在微信内打开链接,完成公众号授权注册后自动绑定引荐关系',
|
'marketing.portal.invite.how2Desc': '好友在微信内打开链接,完成公众号授权注册后自动绑定引荐关系',
|
||||||
'marketing.portal.invite.how3Title': '坐等收益到账',
|
'marketing.portal.invite.how3Title': '等好友订单派送成功',
|
||||||
'marketing.portal.invite.how3Desc': '好友下单并派送成功后,分成进入推荐收益',
|
'marketing.portal.invite.how3Desc': '好友注册即得 5 元券。其第 1 单按实付基础运费 5%、第 2–6 单 3% 计入您的待结算奖励,T+3 工作日入运费余额。',
|
||||||
|
'marketing.portal.invite.lockedTitle': '升级到 {level} 后可邀请好友',
|
||||||
|
'marketing.portal.invite.lockedDesc': '当前等级暂无邀请资格,先下单累计积分升级会员。',
|
||||||
|
'marketing.portal.invite.pendingReward': '待入账',
|
||||||
|
'marketing.portal.invite.settledReward': '已入账',
|
||||||
|
'marketing.portal.invite.tag.reward': '奖励 {count} 单',
|
||||||
'marketing.portal.invite.yourCode': '您的专属邀请码',
|
'marketing.portal.invite.yourCode': '您的专属邀请码',
|
||||||
'marketing.portal.invite.code': '邀请码',
|
'marketing.portal.invite.code': '邀请码',
|
||||||
'marketing.portal.invite.link': '邀请链接',
|
'marketing.portal.invite.link': '邀请链接',
|
||||||
@ -301,7 +322,7 @@ export default {
|
|||||||
'marketing.portal.invite.step3': '好友下单并派送成功后,您可获得分成',
|
'marketing.portal.invite.step3': '好友下单并派送成功后,您可获得分成',
|
||||||
'marketing.portal.invite.refereeCount': '已邀请好友',
|
'marketing.portal.invite.refereeCount': '已邀请好友',
|
||||||
'marketing.portal.invite.earnTitle': '查看推荐收益',
|
'marketing.portal.invite.earnTitle': '查看推荐收益',
|
||||||
'marketing.portal.invite.earnDesc': '派送成功后可结算到佣金账户',
|
'marketing.portal.invite.earnDesc': '派送成功后 T+3 工作日入运费余额',
|
||||||
'marketing.portal.invite.levelTitle': '我的会员等级',
|
'marketing.portal.invite.levelTitle': '我的会员等级',
|
||||||
'marketing.portal.invite.levelDesc': '下单累计积分,升级会员等级',
|
'marketing.portal.invite.levelDesc': '下单累计积分,升级会员等级',
|
||||||
'marketing.portal.invite.refereeTitle': '我的下级',
|
'marketing.portal.invite.refereeTitle': '我的下级',
|
||||||
@ -313,28 +334,29 @@ export default {
|
|||||||
'marketing.portal.invite.tag.delivered': '已派送',
|
'marketing.portal.invite.tag.delivered': '已派送',
|
||||||
|
|
||||||
'marketing.portal.member.title': '我的等级',
|
'marketing.portal.member.title': '我的等级',
|
||||||
'marketing.portal.member.subtitle': '支付成功后累计积分,积分越高额外折扣越优惠。',
|
'marketing.portal.member.subtitle':
|
||||||
'marketing.portal.member.howTitle': '等级怎么升级',
|
'会员等级根据近 12 个月实付基础运费自动计算;V4 钻石、V5 皇冠不享受保级,发货量下滑会触发降级;附加费不参与折扣。',
|
||||||
'marketing.portal.member.howHint': '下单越多,积分越高',
|
'marketing.portal.member.howTitle': '等级怎么计算',
|
||||||
'marketing.portal.member.how1Title': '下单并支付',
|
'marketing.portal.member.howHint': '滚动 12 个月基础运费积分',
|
||||||
'marketing.portal.member.how1Desc': '运单支付成功后,系统自动发放积分',
|
'marketing.portal.member.how1Title': '支付基础运费',
|
||||||
'marketing.portal.member.how2Title': '积分累计升级',
|
'marketing.portal.member.how1Desc': '实付基础运费 1 元 = 1 分;附加费和券抵扣不计',
|
||||||
'marketing.portal.member.how2Desc': '达到下一等级门槛后,自动升级会员等级',
|
'marketing.portal.member.how2Title': '滚动 12 个月定级',
|
||||||
'marketing.portal.member.how3Title': '享受更优运价',
|
'marketing.portal.member.how2Desc': '每天凌晨按近 12 个月积分更新等级;升级即时生效',
|
||||||
'marketing.portal.member.how3Desc': '等级越高,可叠加的额外折扣越优惠',
|
'marketing.portal.member.how3Title': '基础运费折扣',
|
||||||
|
'marketing.portal.member.how3Desc': '最高 95 折且只打基础运费;与优惠券取更优,不叠加',
|
||||||
'marketing.portal.member.currentLevel': '当前等级',
|
'marketing.portal.member.currentLevel': '当前等级',
|
||||||
'marketing.portal.member.defaultLevel': '普通会员',
|
'marketing.portal.member.defaultLevel': '普通会员',
|
||||||
'marketing.portal.member.priceTier': '价区',
|
'marketing.portal.member.priceTier': '价区',
|
||||||
'marketing.portal.member.totalPoints': '累计积分',
|
'marketing.portal.member.totalPoints': '近 12 个月积分',
|
||||||
'marketing.portal.member.availablePoints': '可用积分',
|
'marketing.portal.member.availablePoints': '可用积分',
|
||||||
'marketing.portal.member.extraDiscount': '额外折扣',
|
'marketing.portal.member.extraDiscount': '基础运费折扣',
|
||||||
'marketing.portal.member.nextLevel': '下一等级:{name}',
|
'marketing.portal.member.nextLevel': '下一等级:{name}',
|
||||||
'marketing.portal.member.needPoints': '再获得 {points} 积分即可升级',
|
'marketing.portal.member.needPoints': '再积 {points} 分可升到下一等级',
|
||||||
'marketing.portal.member.topLevelTip': '已达当前最高等级,继续下单可持续累积积分',
|
'marketing.portal.member.topLevelTip': '已达当前最高等级;V4/V5 发货量下滑可能降级',
|
||||||
'marketing.portal.member.pointsFlowTitle': '积分明细',
|
'marketing.portal.member.pointsFlowTitle': '积分明细',
|
||||||
'marketing.portal.member.flowHint': '每笔发放与扣减都会在此展示',
|
'marketing.portal.member.flowHint': '只用于定级,不可抵运费、兑礼或提现',
|
||||||
'marketing.portal.member.emptyFlowTitle': '暂无积分记录',
|
'marketing.portal.member.emptyFlowTitle': '暂无积分记录',
|
||||||
'marketing.portal.member.emptyFlowDesc': '下单支付成功后会自动发放积分',
|
'marketing.portal.member.emptyFlowDesc': '支付成功后按实付基础运费发放积分',
|
||||||
'marketing.portal.member.goOrder': '去下单',
|
'marketing.portal.member.goOrder': '去下单',
|
||||||
'marketing.portal.member.flow.changeType': '类型',
|
'marketing.portal.member.flow.changeType': '类型',
|
||||||
'marketing.portal.member.flow.pointsDelta': '变动',
|
'marketing.portal.member.flow.pointsDelta': '变动',
|
||||||
@ -345,23 +367,26 @@ export default {
|
|||||||
'marketing.portal.coupons.title': '我的优惠券',
|
'marketing.portal.coupons.title': '我的优惠券',
|
||||||
'marketing.portal.coupons.subtitle': '下单时可选用一张;券不可转让,子账号下单使用主账号券包。',
|
'marketing.portal.coupons.subtitle': '下单时可选用一张;券不可转让,子账号下单使用主账号券包。',
|
||||||
'marketing.portal.coupons.howTitle': '优惠券怎么用',
|
'marketing.portal.coupons.howTitle': '优惠券怎么用',
|
||||||
'marketing.portal.coupons.howHint': '一单一张,结算时选用',
|
'marketing.portal.coupons.howHint': '一单一张,只抵基础运费,与会员折扣取更优',
|
||||||
'marketing.portal.coupons.how1Title': '查看可用券',
|
'marketing.portal.coupons.how1Title': '查看可用券',
|
||||||
'marketing.portal.coupons.how1Desc': '在「可用」列表确认面额、门槛与有效期',
|
'marketing.portal.coupons.how1Desc': '未解锁券需等首单派送成功;可用券确认面额、门槛与有效期',
|
||||||
'marketing.portal.coupons.how2Title': '下单时选择',
|
'marketing.portal.coupons.how2Title': '下单时选择',
|
||||||
'marketing.portal.coupons.how2Desc': '去下单结算页勾选一张优惠券抵扣运费',
|
'marketing.portal.coupons.how2Desc': '结算页勾选一张优惠券,仅抵基础运费,附加费不可用',
|
||||||
'marketing.portal.coupons.how3Title': '注意有效期',
|
'marketing.portal.coupons.how3Title': '注意有效期',
|
||||||
'marketing.portal.coupons.how3Desc': '即将过期的券会标红提醒,建议尽快使用',
|
'marketing.portal.coupons.how3Desc': '到期前 3 天会站内信提醒,即将过期的券会标红',
|
||||||
'marketing.portal.coupons.tab.available': '可用',
|
'marketing.portal.coupons.tab.available': '可用',
|
||||||
|
'marketing.portal.coupons.tab.locked': '未解锁',
|
||||||
'marketing.portal.coupons.tab.used': '已使用',
|
'marketing.portal.coupons.tab.used': '已使用',
|
||||||
'marketing.portal.coupons.tab.expired': '已过期',
|
'marketing.portal.coupons.tab.expired': '已过期',
|
||||||
'marketing.portal.coupons.empty': '暂无优惠券',
|
'marketing.portal.coupons.empty': '暂无优惠券',
|
||||||
'marketing.portal.coupons.emptyTitle': '暂无可用优惠券',
|
'marketing.portal.coupons.emptyTitle': '暂无可用优惠券',
|
||||||
'marketing.portal.coupons.emptyDesc': '关注活动发放,或直接去下单看看当前运费',
|
'marketing.portal.coupons.emptyDesc': '关注活动发放,或直接去下单看看当前运费',
|
||||||
|
'marketing.portal.coupons.emptyLockedTitle': '暂无未解锁优惠券',
|
||||||
'marketing.portal.coupons.emptyUsedTitle': '还没有使用记录',
|
'marketing.portal.coupons.emptyUsedTitle': '还没有使用记录',
|
||||||
'marketing.portal.coupons.emptyExpiredTitle': '暂无过期优惠券',
|
'marketing.portal.coupons.emptyExpiredTitle': '暂无过期优惠券',
|
||||||
'marketing.portal.coupons.emptyOtherDesc': '切换到「可用」查看当前券包',
|
'marketing.portal.coupons.emptyOtherDesc': '切换到「可用」查看当前券包',
|
||||||
'marketing.portal.coupons.minOrder': '满 {amount} 元可用',
|
'marketing.portal.coupons.minOrder': '满 {amount} 元可用',
|
||||||
|
'marketing.portal.coupons.unlockHint': '完成首单派送即可解锁 15 元券',
|
||||||
'marketing.portal.coupons.expiringSoon': '即将过期',
|
'marketing.portal.coupons.expiringSoon': '即将过期',
|
||||||
'marketing.portal.coupons.discountUnit': '折',
|
'marketing.portal.coupons.discountUnit': '折',
|
||||||
'marketing.portal.coupons.unnamed': '优惠券',
|
'marketing.portal.coupons.unnamed': '优惠券',
|
||||||
@ -424,10 +449,10 @@ export default {
|
|||||||
'marketing.priceCheck.couponUnavailable': '不可用',
|
'marketing.priceCheck.couponUnavailable': '不可用',
|
||||||
'marketing.priceCheck.couponChanged': '已重新查价,请确认应付金额',
|
'marketing.priceCheck.couponChanged': '已重新查价,请确认应付金额',
|
||||||
'marketing.priceCheck.breakdownTitle': '价格明细',
|
'marketing.priceCheck.breakdownTitle': '价格明细',
|
||||||
'marketing.priceCheck.breakdown.receivableSubtotal': '等级价小计',
|
'marketing.priceCheck.breakdown.receivableSubtotal': '费用小计',
|
||||||
'marketing.priceCheck.breakdown.levelDiscount': '等级额外折扣',
|
'marketing.priceCheck.breakdown.levelDiscount': '会员基础运费折扣',
|
||||||
'marketing.priceCheck.breakdown.amountAfterLevel': '券前金额',
|
'marketing.priceCheck.breakdown.amountAfterLevel': '折后金额',
|
||||||
'marketing.priceCheck.breakdown.couponDiscount': '券抵扣',
|
'marketing.priceCheck.breakdown.couponDiscount': '券抵扣(与会员折取更优)',
|
||||||
'marketing.priceCheck.breakdown.payable': '应付实付',
|
'marketing.priceCheck.breakdown.payable': '应付实付',
|
||||||
|
|
||||||
'marketing.register.title': '客户注册',
|
'marketing.register.title': '客户注册',
|
||||||
@ -461,7 +486,7 @@ export default {
|
|||||||
'profile.marketing.coupons': '我的优惠券',
|
'profile.marketing.coupons': '我的优惠券',
|
||||||
'profile.marketing.commission': '推荐收益',
|
'profile.marketing.commission': '推荐收益',
|
||||||
'profile.marketing.inviteDesc': '分享链接,积累下级',
|
'profile.marketing.inviteDesc': '分享链接,积累下级',
|
||||||
'profile.marketing.memberDesc': '积分与会员价区',
|
'profile.marketing.memberDesc': '近 12 个月基础运费积分与会员等级',
|
||||||
'profile.marketing.couponsDesc': '查看可用券包',
|
'profile.marketing.couponsDesc': '查看可用券包',
|
||||||
'profile.marketing.commissionDesc': '分成、转余额、提现',
|
'profile.marketing.commissionDesc': '分成、转余额、提现',
|
||||||
'profile.marketing.navigateFailed': '页面跳转失败,请刷新后重试',
|
'profile.marketing.navigateFailed': '页面跳转失败,请刷新后重试',
|
||||||
|
|||||||
@ -66,10 +66,10 @@ export default {
|
|||||||
'menu.marketing': '营销',
|
'menu.marketing': '营销',
|
||||||
'menu.marketing.memberLevel': '会员等级',
|
'menu.marketing.memberLevel': '会员等级',
|
||||||
'marketing.memberLevel.pageTip':
|
'marketing.memberLevel.pageTip':
|
||||||
'会员等级用于积分升级与可选额外折扣。价区键在客户资料中单独维护,与等级升级无关。修改额外折扣率只影响之后的查价,不追溯历史订单。已被客户使用的等级不可删除,请改为禁用。',
|
'会员等级按近 12 个月实付基础运费积分滚动定级。折扣只打基础运费,附加费原价;与优惠券取更优不叠加。V0–V3 只升不降,V4/V5 可逐级降。最高 95 折。',
|
||||||
'marketing.memberLevel.searchPlaceholder': '搜索编码/名称',
|
'marketing.memberLevel.searchPlaceholder': '搜索编码/名称',
|
||||||
'marketing.memberLevel.modalTip':
|
'marketing.memberLevel.modalTip':
|
||||||
'升级积分达到门槛后可升到本等级;额外折扣率在卖价款区价之后叠加。价区请到客户管理中配置。',
|
'积分门槛为滚动 12 个月累计积分下限。折扣率 0.01 = 基础运费 99 折,封顶 0.05(95 折)。勾选「允许降级」后滚动积分不足只降一级。',
|
||||||
'marketing.memberLevel.modal.add': '新增会员等级',
|
'marketing.memberLevel.modal.add': '新增会员等级',
|
||||||
'marketing.memberLevel.modal.edit': '编辑会员等级',
|
'marketing.memberLevel.modal.edit': '编辑会员等级',
|
||||||
'marketing.memberLevel.deleteConfirm':
|
'marketing.memberLevel.deleteConfirm':
|
||||||
@ -79,10 +79,14 @@ export default {
|
|||||||
'marketing.memberLevel.form.levelCodeExtra': '创建后不可修改,建议用英文大写',
|
'marketing.memberLevel.form.levelCodeExtra': '创建后不可修改,建议用英文大写',
|
||||||
'marketing.memberLevel.form.levelName': '等级名称',
|
'marketing.memberLevel.form.levelName': '等级名称',
|
||||||
'marketing.memberLevel.form.levelNamePlaceholder': '如 普通会员、银卡',
|
'marketing.memberLevel.form.levelNamePlaceholder': '如 普通会员、银卡',
|
||||||
'marketing.memberLevel.form.minPoints': '升级积分',
|
'marketing.memberLevel.form.minPoints': '滚动积分下限',
|
||||||
'marketing.memberLevel.form.minPointsExtra': '达到该累计积分后可升到本等级',
|
'marketing.memberLevel.form.minPointsExtra': '近 12 个月实付基础运费积分达到该值可升到本级',
|
||||||
'marketing.memberLevel.form.extraDiscountRate': '额外折扣率',
|
'marketing.memberLevel.form.extraDiscountRate': '基础运费折扣率',
|
||||||
'marketing.memberLevel.form.extraDiscountRateExtra': '如 0.01 表示在价区价基础上再减 1%',
|
'marketing.memberLevel.form.extraDiscountRateExtra': '0.01 = 99 折,仅作用于基础运费,最高 0.05(95 折)',
|
||||||
|
'marketing.memberLevel.form.canDowngrade': '允许降级',
|
||||||
|
'marketing.memberLevel.form.canDowngradeExtra': '仅 V4 钻石、V5 皇冠勾选;每天凌晨按滚动积分逐级降,V0–V3 保级',
|
||||||
|
'marketing.memberLevel.discount.original': '原价',
|
||||||
|
'marketing.memberLevel.discount.fold': '{fold} 折',
|
||||||
'marketing.memberLevel.form.sort': '排序',
|
'marketing.memberLevel.form.sort': '排序',
|
||||||
'marketing.memberLevel.form.sortExtra': '数字越小越靠前',
|
'marketing.memberLevel.form.sortExtra': '数字越小越靠前',
|
||||||
'marketing.memberLevel.form.status': '状态',
|
'marketing.memberLevel.form.status': '状态',
|
||||||
@ -95,8 +99,8 @@ export default {
|
|||||||
'marketing.memberLevel.form.requiredHint': '请填写等级编码与名称',
|
'marketing.memberLevel.form.requiredHint': '请填写等级编码与名称',
|
||||||
'marketing.memberLevel.section.basic': '基础信息',
|
'marketing.memberLevel.section.basic': '基础信息',
|
||||||
'marketing.memberLevel.section.basicHint': '编码创建后不可改,名称与状态可随时调整',
|
'marketing.memberLevel.section.basicHint': '编码创建后不可改,名称与状态可随时调整',
|
||||||
'marketing.memberLevel.section.pricing': '积分与折扣',
|
'marketing.memberLevel.section.pricing': '积分门槛与运费折扣',
|
||||||
'marketing.memberLevel.section.pricingHint': '决定升级门槛与额外折扣',
|
'marketing.memberLevel.section.pricingHint': '滚动 12 个月积分定级;折扣只打基础运费',
|
||||||
'marketing.memberLevel.section.display': '展示与权益',
|
'marketing.memberLevel.section.display': '展示与权益',
|
||||||
'marketing.memberLevel.section.displayHint': '门户展示用,不影响计价逻辑',
|
'marketing.memberLevel.section.displayHint': '门户展示用,不影响计价逻辑',
|
||||||
'marketing.memberLevel.preview.nameFallback': '未命名等级',
|
'marketing.memberLevel.preview.nameFallback': '未命名等级',
|
||||||
@ -2254,7 +2258,7 @@ export default {
|
|||||||
'order.priceCheck.form.destinationCountry.placeholder': '请选择',
|
'order.priceCheck.form.destinationCountry.placeholder': '请选择',
|
||||||
'order.priceCheck.form.shippingMethod': '运输方式',
|
'order.priceCheck.form.shippingMethod': '运输方式',
|
||||||
'order.priceCheck.form.shippingMethod.placeholder': '请选择',
|
'order.priceCheck.form.shippingMethod.placeholder': '请选择',
|
||||||
'order.priceCheck.form.channel': '渠道',
|
'order.priceCheck.form.channel': '线路',
|
||||||
'order.priceCheck.form.channel.placeholder': '全部',
|
'order.priceCheck.form.channel.placeholder': '全部',
|
||||||
'order.priceCheck.form.cargoWeight': '货物重量',
|
'order.priceCheck.form.cargoWeight': '货物重量',
|
||||||
'order.priceCheck.form.cargoWeight.placeholder': '请输入重量(KG)',
|
'order.priceCheck.form.cargoWeight.placeholder': '请输入重量(KG)',
|
||||||
@ -2275,7 +2279,7 @@ export default {
|
|||||||
'order.priceCheck.form.postalCode': '邮编',
|
'order.priceCheck.form.postalCode': '邮编',
|
||||||
'order.priceCheck.form.postalCode.tip': '更准',
|
'order.priceCheck.form.postalCode.tip': '更准',
|
||||||
'order.priceCheck.form.postalCode.placeholder': '选填,补全更准确',
|
'order.priceCheck.form.postalCode.placeholder': '选填,补全更准确',
|
||||||
'order.priceCheck.form.moreFilters': '更多条件',
|
'order.priceCheck.form.moreFilters': '更多',
|
||||||
'order.priceCheck.form.activeFilters': '已选 {count} 项',
|
'order.priceCheck.form.activeFilters': '已选 {count} 项',
|
||||||
'order.priceCheck.form.clearFilters': '清空',
|
'order.priceCheck.form.clearFilters': '清空',
|
||||||
'order.priceCheck.form.removeFilter': '移除条件',
|
'order.priceCheck.form.removeFilter': '移除条件',
|
||||||
@ -2351,9 +2355,9 @@ export default {
|
|||||||
'order.priceCheck.table.viewDetail': '详情',
|
'order.priceCheck.table.viewDetail': '详情',
|
||||||
'order.priceCheck.table.viewGuide': '渠道说明',
|
'order.priceCheck.table.viewGuide': '渠道说明',
|
||||||
'order.priceCheck.table.viewGuideHint': '点击查看线路说明',
|
'order.priceCheck.table.viewGuideHint': '点击查看线路说明',
|
||||||
'order.priceCheck.table.customsOrderNow': '立刻下单',
|
'order.priceCheck.table.customsOrderNow': '去下单',
|
||||||
'order.priceCheck.table.orderNow': '立刻下单',
|
'order.priceCheck.table.orderNow': '去下单',
|
||||||
'order.priceCheck.table.askAi': '问 AI',
|
'order.priceCheck.table.askAi': '问顾问',
|
||||||
'order.priceCheck.table.askAiCanShip': '这个能不能发?',
|
'order.priceCheck.table.askAiCanShip': '这个能不能发?',
|
||||||
'order.priceCheck.table.askAiCheaper': '有没有更便宜?',
|
'order.priceCheck.table.askAiCheaper': '有没有更便宜?',
|
||||||
'order.priceCheck.table.empty': '暂无匹配报价',
|
'order.priceCheck.table.empty': '暂无匹配报价',
|
||||||
@ -4704,7 +4708,7 @@ export default {
|
|||||||
'user.customer.tip.accountBalance': '客户可用余额,谨慎手工调整',
|
'user.customer.tip.accountBalance': '客户可用余额,谨慎手工调整',
|
||||||
'user.customer.tip.totalQuota': '信用总额度上限',
|
'user.customer.tip.totalQuota': '信用总额度上限',
|
||||||
'user.customer.tip.usedQuota': '已占用额度,通常由系统累计',
|
'user.customer.tip.usedQuota': '已占用额度,通常由系统累计',
|
||||||
'user.customer.tip.memberLevel': '用于积分升级与可选额外折扣',
|
'user.customer.tip.memberLevel': '由近 12 个月实付基础运费积分自动定级,可手工调整',
|
||||||
'user.customer.tip.priceTierKey': '决定报价价区,与会员等级升级无关',
|
'user.customer.tip.priceTierKey': '决定报价价区,与会员等级升级无关',
|
||||||
'user.customer.modal.addTitle': '新增客户',
|
'user.customer.modal.addTitle': '新增客户',
|
||||||
'user.customer.modal.editTitle': '编辑客户',
|
'user.customer.modal.editTitle': '编辑客户',
|
||||||
@ -5206,7 +5210,13 @@ export default {
|
|||||||
'channel.salesProducts.placeholder.productDescription': '面向客户的产品说明(选填)',
|
'channel.salesProducts.placeholder.productDescription': '面向客户的产品说明(选填)',
|
||||||
'channel.salesProducts.placeholder.preStorageScript': '订单落库前执行的脚本(选填)',
|
'channel.salesProducts.placeholder.preStorageScript': '订单落库前执行的脚本(选填)',
|
||||||
'channel.salesProducts.tip.productCode':
|
'channel.salesProducts.tip.productCode':
|
||||||
'命名规则:地区/国家代码 + 01专线/02快递/03邮政/04小包 + 其他。\n示例:US04-5DAY\n创建后不可修改;仅支持大写字母、数字和 -',
|
'命名规则:地区/国家代码 + 01专线/02快递/03邮政/04小包 + 其他。\n示例:US04-5DAY\n仅支持大写字母、数字和 -;创建后可通过修改入口单独改代码',
|
||||||
|
'channel.salesProducts.form.editProductCode': '修改',
|
||||||
|
'channel.salesProducts.modal.editProductCodeTitle': '修改产品代码',
|
||||||
|
'channel.salesProducts.modal.editProductCodeHint':
|
||||||
|
'仅改销售产品主数据代码。历史订单仍保留原代码;下单、查价将使用新代码。',
|
||||||
|
'channel.salesProducts.messages.productCodeRequired': '请输入产品代码',
|
||||||
|
'channel.salesProducts.messages.productCodeUpdated': '产品代码已更新',
|
||||||
'channel.salesProducts.tip.productName':
|
'channel.salesProducts.tip.productName':
|
||||||
'命名规则:地区/国家 + 产品类型描述 + 其他。\n示例:美国空运小包-5日达',
|
'命名规则:地区/国家 + 产品类型描述 + 其他。\n示例:美国空运小包-5日达',
|
||||||
'channel.salesProducts.tip.group': '用于官网/后台分组展示,如专线小包',
|
'channel.salesProducts.tip.group': '用于官网/后台分组展示,如专线小包',
|
||||||
@ -5569,7 +5579,7 @@ export default {
|
|||||||
'channel.area.search.button.query': '查询',
|
'channel.area.search.button.query': '查询',
|
||||||
'channel.area.search.button.reset': '重置',
|
'channel.area.search.button.reset': '重置',
|
||||||
'channel.area.import.modalTitle': '导入分区明细',
|
'channel.area.import.modalTitle': '导入分区明细',
|
||||||
'channel.area.import.tip': '请先下载模板。国家代码必填(无需填国家名称,导入时按代码自动带出)。匹配维度、匹配方式请填 Sheet「枚举值」中的编码。一行一个维度:填邮编则为邮编规则(只填起始=前缀;起止相同=精确;起止不同=闭区间);不填邮编只填州=州匹配;州+城市=城市匹配;都空=仅国家兜底。',
|
'channel.area.import.tip': '请先下载模板。导入为增量写入:与本分区已有规则完全相同的行会跳过,不会覆盖。国家代码必填(无需填国家名称,导入时按代码自动带出)。匹配维度、匹配方式请填 Sheet「枚举值」中的编码。一行一个维度:填邮编则为邮编规则(只填起始=前缀;起止相同=精确;起止不同=闭区间);不填邮编只填州=州匹配;州+城市=城市匹配;都空=仅国家兜底。',
|
||||||
'channel.area.import.downloadTemplate': '下载模板',
|
'channel.area.import.downloadTemplate': '下载模板',
|
||||||
'channel.area.import.submit': '开始导入',
|
'channel.area.import.submit': '开始导入',
|
||||||
'channel.area.import.downloadSuccess': '模板下载成功',
|
'channel.area.import.downloadSuccess': '模板下载成功',
|
||||||
@ -5580,7 +5590,7 @@ export default {
|
|||||||
'channel.area.import.failed': '导入失败',
|
'channel.area.import.failed': '导入失败',
|
||||||
'channel.area.import.partialSuccess': '部分行导入失败,请查看失败明细',
|
'channel.area.import.partialSuccess': '部分行导入失败,请查看失败明细',
|
||||||
'channel.area.import.allFailed': '导入失败,请检查文件内容',
|
'channel.area.import.allFailed': '导入失败,请检查文件内容',
|
||||||
'channel.area.import.resultSummary': '成功 {success} 条,失败 {fail} 条',
|
'channel.area.import.resultSummary': '成功 {success} 条,跳过重复 {skip} 条,失败 {fail} 条',
|
||||||
'channel.area.import.failRow': '第 {row} 行:{reason}',
|
'channel.area.import.failRow': '第 {row} 行:{reason}',
|
||||||
'channel.area.export.success': '已导出 {count} 条',
|
'channel.area.export.success': '已导出 {count} 条',
|
||||||
'channel.area.export.failed': '导出失败',
|
'channel.area.export.failed': '导出失败',
|
||||||
|
|||||||
@ -135,8 +135,8 @@ async function bootstrap() {
|
|||||||
installWeChatH5HistoryGuard(router);
|
installWeChatH5HistoryGuard(router);
|
||||||
initAuthKeepAlive();
|
initAuthKeepAlive();
|
||||||
|
|
||||||
// PC 登录/首页可后台预热;H5 全程跳过,避免全局 CSS 打乱首屏
|
// 登录页勿预热 VXE/Element:体积大且会注入全局 CSS,和首屏抢带宽
|
||||||
if ((light || isHomePath(entryPath)) && !isH5LikePath(entryPath)) {
|
if (isHomePath(entryPath) && !isH5LikePath(entryPath) && !light) {
|
||||||
void ensureHeavyPlugins();
|
void ensureHeavyPlugins();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -93,6 +93,7 @@ body[arco-theme='dark'] {
|
|||||||
|
|
||||||
.table-section,
|
.table-section,
|
||||||
.warehouse-list-page-card .status-tabs,
|
.warehouse-list-page-card .status-tabs,
|
||||||
|
.mkt-status-tabs,
|
||||||
.tabs-actions-section {
|
.tabs-actions-section {
|
||||||
border-color: var(--app-card-border) !important;
|
border-color: var(--app-card-border) !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -34,6 +34,7 @@
|
|||||||
{{
|
{{
|
||||||
t('channel.area.import.resultSummary', {
|
t('channel.area.import.resultSummary', {
|
||||||
success: importResult.successCount,
|
success: importResult.successCount,
|
||||||
|
skip: importResult.skipCount,
|
||||||
fail: importResult.failCount,
|
fail: importResult.failCount,
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
@ -117,7 +118,7 @@
|
|||||||
try {
|
try {
|
||||||
const result = await importKpiAreaInfo(areaId.value, selectedFile.value);
|
const result = await importKpiAreaInfo(areaId.value, selectedFile.value);
|
||||||
importResult.value = result;
|
importResult.value = result;
|
||||||
if (result.successCount > 0) {
|
if (result.successCount > 0 || result.skipCount > 0) {
|
||||||
emit('success');
|
emit('success');
|
||||||
}
|
}
|
||||||
if (result.failCount === 0) {
|
if (result.failCount === 0) {
|
||||||
@ -125,7 +126,7 @@
|
|||||||
visible.value = false;
|
visible.value = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (result.successCount > 0) {
|
if (result.successCount > 0 || result.skipCount > 0) {
|
||||||
Message.warning(t('channel.area.import.partialSuccess'));
|
Message.warning(t('channel.area.import.partialSuccess'));
|
||||||
} else {
|
} else {
|
||||||
Message.error(t('channel.area.import.allFailed'));
|
Message.error(t('channel.area.import.allFailed'));
|
||||||
|
|||||||
@ -18,6 +18,7 @@
|
|||||||
saveSalesProductCustomerGuide,
|
saveSalesProductCustomerGuide,
|
||||||
toggleSalesProductPriceStatus,
|
toggleSalesProductPriceStatus,
|
||||||
updateSalesProduct,
|
updateSalesProduct,
|
||||||
|
updateSalesProductCode,
|
||||||
updateSalesProductPrice,
|
updateSalesProductPrice,
|
||||||
} from '@/api/channel/salesProduct';
|
} from '@/api/channel/salesProduct';
|
||||||
import { getLabelTemplateList } from '@/api/channel/labelTemplate';
|
import { getLabelTemplateList } from '@/api/channel/labelTemplate';
|
||||||
@ -154,6 +155,9 @@
|
|||||||
const customerGuideSaving = ref(false);
|
const customerGuideSaving = ref(false);
|
||||||
const customerGuidePanelRef = ref<InstanceType<typeof MarkdownDocPanel> | null>(null);
|
const customerGuidePanelRef = ref<InstanceType<typeof MarkdownDocPanel> | null>(null);
|
||||||
const savingConfig = ref(false);
|
const savingConfig = ref(false);
|
||||||
|
const productCodeModalVisible = ref(false);
|
||||||
|
const productCodeDraft = ref('');
|
||||||
|
const savingProductCode = ref(false);
|
||||||
|
|
||||||
function parseAdditionalServiceCodes(raw: unknown): string[] {
|
function parseAdditionalServiceCodes(raw: unknown): string[] {
|
||||||
if (Array.isArray(raw)) {
|
if (Array.isArray(raw)) {
|
||||||
@ -252,6 +256,42 @@
|
|||||||
.replace(/[^A-Z0-9-]/g, '');
|
.replace(/[^A-Z0-9-]/g, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openProductCodeModal() {
|
||||||
|
if (selectedProduct.value == null) return;
|
||||||
|
productCodeDraft.value = normalizeBizCode(formData.productCode);
|
||||||
|
productCodeModalVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSaveProductCode() {
|
||||||
|
const pid = selectedProduct.value;
|
||||||
|
if (pid == null) return;
|
||||||
|
const next = normalizeBizCode(productCodeDraft.value);
|
||||||
|
if (!next) {
|
||||||
|
Message.warning(t('channel.salesProducts.messages.productCodeRequired'));
|
||||||
|
return Promise.reject();
|
||||||
|
}
|
||||||
|
savingProductCode.value = true;
|
||||||
|
try {
|
||||||
|
await updateSalesProductCode({ id: pid, productCode: next });
|
||||||
|
formData.productCode = next;
|
||||||
|
const hit = productList.value.find((p: any) => p.id === pid);
|
||||||
|
if (hit?.raw) {
|
||||||
|
hit.raw.productCode = next;
|
||||||
|
hit.raw.ProductCode = next;
|
||||||
|
}
|
||||||
|
if (configFormBaseline.value) {
|
||||||
|
configFormBaseline.value = { ...configFormBaseline.value, productCode: next };
|
||||||
|
}
|
||||||
|
productCodeModalVisible.value = false;
|
||||||
|
Message.success(t('channel.salesProducts.messages.productCodeUpdated'));
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || '修改产品代码失败');
|
||||||
|
return Promise.reject(e);
|
||||||
|
} finally {
|
||||||
|
savingProductCode.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const selectedProductContext = computed(() => {
|
const selectedProductContext = computed(() => {
|
||||||
const pid = selectedProduct.value;
|
const pid = selectedProduct.value;
|
||||||
if (pid == null) return null;
|
if (pid == null) return null;
|
||||||
@ -4014,10 +4054,18 @@
|
|||||||
</template>
|
</template>
|
||||||
<icon-question-circle class="tip-icon" />
|
<icon-question-circle class="tip-icon" />
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="product-code-edit-icon"
|
||||||
|
:aria-label="t('channel.salesProducts.form.editProductCode')"
|
||||||
|
@click.stop="openProductCodeModal"
|
||||||
|
>
|
||||||
|
<icon-edit />
|
||||||
|
</button>
|
||||||
</label>
|
</label>
|
||||||
<a-input
|
<a-input
|
||||||
v-model="formData.productCode"
|
v-model="formData.productCode"
|
||||||
:placeholder="t('channel.salesProducts.form.placeholder.input')"
|
:placeholder="t('channel.salesProducts.placeholder.productCode')"
|
||||||
class="form-input"
|
class="form-input"
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
@ -5487,6 +5535,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="productCodeModalVisible"
|
||||||
|
:title="t('channel.salesProducts.modal.editProductCodeTitle')"
|
||||||
|
width="420px"
|
||||||
|
:mask-closable="false"
|
||||||
|
:ok-loading="savingProductCode"
|
||||||
|
@ok="handleSaveProductCode"
|
||||||
|
@cancel="productCodeModalVisible = false"
|
||||||
|
>
|
||||||
|
<p class="product-code-edit-hint">{{ t('channel.salesProducts.modal.editProductCodeHint') }}</p>
|
||||||
|
<a-input
|
||||||
|
:model-value="productCodeDraft"
|
||||||
|
:placeholder="t('channel.salesProducts.placeholder.productCode')"
|
||||||
|
allow-clear
|
||||||
|
@update:model-value="(v) => (productCodeDraft = normalizeBizCode(v))"
|
||||||
|
@press-enter="handleSaveProductCode"
|
||||||
|
/>
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
<ChannelReferencePreviewModal ref="referencePreviewRef" :dict-label-fn="dictLabelByType" />
|
<ChannelReferencePreviewModal ref="referencePreviewRef" :dict-label-fn="dictLabelByType" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -5691,6 +5758,31 @@
|
|||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.product-code-edit-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--app-primary-color, #007979);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-code-edit-hint {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
.form-input,
|
.form-input,
|
||||||
.form-select,
|
.form-select,
|
||||||
.form-textarea {
|
.form-textarea {
|
||||||
|
|||||||
@ -55,13 +55,14 @@
|
|||||||
'is-expiring': item.isExpiringSoon,
|
'is-expiring': item.isExpiringSoon,
|
||||||
'is-used': item.status === 3,
|
'is-used': item.status === 3,
|
||||||
'is-expired': item.status === 4,
|
'is-expired': item.status === 4,
|
||||||
|
'is-locked': item.status === 5,
|
||||||
}"
|
}"
|
||||||
@click="item.status === 1 && goOrder()"
|
@click="item.status === 1 && goOrder()"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-if="item.status === 3 || item.status === 4"
|
v-if="item.status === 3 || item.status === 4 || item.status === 5"
|
||||||
class="coupon-stamp"
|
class="coupon-stamp"
|
||||||
:class="item.status === 3 ? 'is-used' : 'is-expired'"
|
:class="item.status === 3 ? 'is-used' : item.status === 5 ? 'is-locked' : 'is-expired'"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
{{ statusLabel(item.status) }}
|
{{ statusLabel(item.status) }}
|
||||||
@ -99,7 +100,10 @@
|
|||||||
<div class="meta-row meta-row--validity">
|
<div class="meta-row meta-row--validity">
|
||||||
<span class="meta-label">{{ t('h5.coupons.validLabel') }}</span>
|
<span class="meta-label">{{ t('h5.coupons.validLabel') }}</span>
|
||||||
<strong class="meta-value">
|
<strong class="meta-value">
|
||||||
{{ formatDate(item.validStartTime) }} ~ {{ formatDate(item.validEndTime) }}
|
<template v-if="item.status === 5">{{ t('h5.coupons.how.unlock') }}</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ formatDate(item.validStartTime) }} ~ {{ formatDate(item.validEndTime) }}
|
||||||
|
</template>
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -140,12 +144,12 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const tabs = ['available', 'used', 'expired'] as const;
|
const tabs = ['available', 'locked', 'used', 'expired'] as const;
|
||||||
const activeTab = ref<(typeof tabs)[number]>('available');
|
const activeTab = ref<(typeof tabs)[number]>('available');
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const rows = ref<any[]>([]);
|
const rows = ref<any[]>([]);
|
||||||
|
|
||||||
const statusMap: Record<string, number> = { available: 1, used: 3, expired: 4 };
|
const statusMap: Record<string, number> = { available: 1, locked: 5, used: 3, expired: 4 };
|
||||||
|
|
||||||
const drawerStyle = {
|
const drawerStyle = {
|
||||||
borderRadius: '20px 20px 0 0',
|
borderRadius: '20px 20px 0 0',
|
||||||
@ -173,6 +177,7 @@ function formatDate(value: unknown) {
|
|||||||
function statusLabel(status: number) {
|
function statusLabel(status: number) {
|
||||||
if (status === 3) return t('h5.coupons.tab.used');
|
if (status === 3) return t('h5.coupons.tab.used');
|
||||||
if (status === 4) return t('h5.coupons.tab.expired');
|
if (status === 4) return t('h5.coupons.tab.expired');
|
||||||
|
if (status === 5) return t('h5.coupons.tab.locked');
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,7 +185,12 @@ function usageTip(item: {
|
|||||||
couponType?: string;
|
couponType?: string;
|
||||||
minOrderAmount?: number;
|
minOrderAmount?: number;
|
||||||
faceValue?: number | null;
|
faceValue?: number | null;
|
||||||
|
status?: number;
|
||||||
|
unlockHint?: string;
|
||||||
}) {
|
}) {
|
||||||
|
if (item.status === 5) {
|
||||||
|
return item.unlockHint || t('h5.coupons.how.unlock');
|
||||||
|
}
|
||||||
const type = String(item.couponType || '');
|
const type = String(item.couponType || '');
|
||||||
const min = formatAmount(item.minOrderAmount ?? 0);
|
const min = formatAmount(item.minOrderAmount ?? 0);
|
||||||
if (type === 'percent_off') {
|
if (type === 'percent_off') {
|
||||||
@ -249,6 +259,7 @@ async function load() {
|
|||||||
grantChannel,
|
grantChannel,
|
||||||
isNewcomer: grantChannel === 'newcomer',
|
isNewcomer: grantChannel === 'newcomer',
|
||||||
isExpiringSoon: isExpiringSoon(validEndTime, status),
|
isExpiringSoon: isExpiringSoon(validEndTime, status),
|
||||||
|
unlockHint: raw.unlockHint ?? raw.UnlockHint ?? '',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
@ -475,6 +486,10 @@ watch(
|
|||||||
&.is-expired {
|
&.is-expired {
|
||||||
color: #c9cdd4;
|
color: #c9cdd4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.is-locked {
|
||||||
|
color: var(--app-primary-color, #1d7281);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.coupon-ticket__face {
|
.coupon-ticket__face {
|
||||||
|
|||||||
@ -53,7 +53,8 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<section v-else-if="invite && activeTab === 'share'" class="invite-hero">
|
<section v-else-if="invite && activeTab === 'share'" class="invite-hero">
|
||||||
<p class="invite-hero__lead">{{ t('h5.invite.lead') }}</p>
|
<template v-if="canInvite">
|
||||||
|
<p class="invite-hero__lead">{{ t('h5.invite.lead') }}</p>
|
||||||
|
|
||||||
<p class="field-label">{{ t('h5.invite.code') }}</p>
|
<p class="field-label">{{ t('h5.invite.code') }}</p>
|
||||||
<div class="focus-block">
|
<div class="focus-block">
|
||||||
@ -94,6 +95,8 @@
|
|||||||
@ready="onPosterReady"
|
@ready="onPosterReady"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
<p v-else class="invite-hero__lead">{{ t('h5.invite.locked') }}</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section v-else-if="invite && activeTab === 'referees'" class="card">
|
<section v-else-if="invite && activeTab === 'referees'" class="card">
|
||||||
@ -159,6 +162,11 @@ const drawerStyle = {
|
|||||||
const inviteCode = computed(() =>
|
const inviteCode = computed(() =>
|
||||||
String(invite.value?.inviteCode ?? invite.value?.InviteCode ?? ''),
|
String(invite.value?.inviteCode ?? invite.value?.InviteCode ?? ''),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const canInvite = computed(() => {
|
||||||
|
const raw = invite.value?.canInvite ?? invite.value?.CanInvite;
|
||||||
|
return raw === true || raw === 1 || raw === 'true';
|
||||||
|
});
|
||||||
/** 落地公众号微信注册页,保证 ?ref= 经 OAuth 后仍能带到注册提交 */
|
/** 落地公众号微信注册页,保证 ?ref= 经 OAuth 后仍能带到注册提交 */
|
||||||
const inviteUrl = computed(() => buildInviteRegisterUrl(inviteCode.value));
|
const inviteUrl = computed(() => buildInviteRegisterUrl(inviteCode.value));
|
||||||
const shareText = computed(() => buildInviteShareText(inviteCode.value, inviteUrl.value));
|
const shareText = computed(() => buildInviteShareText(inviteCode.value, inviteUrl.value));
|
||||||
|
|||||||
@ -833,6 +833,7 @@ export default {
|
|||||||
'h5.coupons.empty': 'No coupons',
|
'h5.coupons.empty': 'No coupons',
|
||||||
'h5.coupons.unnamed': 'Coupon',
|
'h5.coupons.unnamed': 'Coupon',
|
||||||
'h5.coupons.tab.available': 'Available',
|
'h5.coupons.tab.available': 'Available',
|
||||||
|
'h5.coupons.tab.locked': 'Locked',
|
||||||
'h5.coupons.tab.used': 'Used',
|
'h5.coupons.tab.used': 'Used',
|
||||||
'h5.coupons.tab.expired': 'Expired',
|
'h5.coupons.tab.expired': 'Expired',
|
||||||
'h5.coupons.minOrder': 'Min order {amount}',
|
'h5.coupons.minOrder': 'Min order {amount}',
|
||||||
@ -845,13 +846,15 @@ export default {
|
|||||||
'h5.coupons.how.fixed': 'Select at checkout. Min order {amount} to redeem',
|
'h5.coupons.how.fixed': 'Select at checkout. Min order {amount} to redeem',
|
||||||
'h5.coupons.how.direct': 'Select at checkout for an instant face-value off',
|
'h5.coupons.how.direct': 'Select at checkout for an instant face-value off',
|
||||||
'h5.coupons.how.percent': 'Select at checkout. Min order {amount} for percent off',
|
'h5.coupons.how.percent': 'Select at checkout. Min order {amount} for percent off',
|
||||||
'h5.coupons.how.shipping': 'Select at checkout. Min order {amount} for shipping off',
|
'h5.coupons.how.shipping': 'Select at checkout. Min order {amount} off base freight',
|
||||||
|
'h5.coupons.how.unlock': 'Complete first delivery to unlock the ¥15 coupon',
|
||||||
|
|
||||||
'h5.invite.title': 'Invite friends',
|
'h5.invite.title': 'Invite friends',
|
||||||
'h5.invite.tab.share': 'Share invite',
|
'h5.invite.tab.share': 'Share invite',
|
||||||
'h5.invite.tab.referees': 'Invited',
|
'h5.invite.tab.referees': 'Invited',
|
||||||
'h5.invite.lead':
|
'h5.invite.lead':
|
||||||
'Share your code or QR. Friends open it in WeChat, finish official-account registration, and both unlock rewards.',
|
'Friends get ¥5 on register. After their orders are delivered, you earn a percent of paid base freight.',
|
||||||
|
'h5.invite.locked': 'Upgrade to V1 to invite friends',
|
||||||
'h5.invite.code': 'Invite code',
|
'h5.invite.code': 'Invite code',
|
||||||
'h5.invite.link': 'Invite link (open in WeChat)',
|
'h5.invite.link': 'Invite link (open in WeChat)',
|
||||||
'h5.invite.copy': 'Copy',
|
'h5.invite.copy': 'Copy',
|
||||||
@ -903,7 +906,7 @@ export default {
|
|||||||
'h5.pay.later': 'Later',
|
'h5.pay.later': 'Later',
|
||||||
'h5.me.balance': 'Balance',
|
'h5.me.balance': 'Balance',
|
||||||
'h5.me.recharge': 'Recharge',
|
'h5.me.recharge': 'Recharge',
|
||||||
'h5.me.points': 'Points',
|
'h5.me.points': '12-month points',
|
||||||
'h5.me.pointsToNext': '{points} to {name}',
|
'h5.me.pointsToNext': '{points} to {name}',
|
||||||
'h5.me.pointsTopLevel': 'Top level reached',
|
'h5.me.pointsTopLevel': 'Top level reached',
|
||||||
'h5.me.lowBalance': 'Low balance — recharge soon',
|
'h5.me.lowBalance': 'Low balance — recharge soon',
|
||||||
|
|||||||
@ -828,15 +828,18 @@ export default {
|
|||||||
'h5.coupons.how.fixed': '下单结算勾选本券,满{amount}元可抵扣',
|
'h5.coupons.how.fixed': '下单结算勾选本券,满{amount}元可抵扣',
|
||||||
'h5.coupons.how.direct': '下单结算勾选本券,直接减免面额',
|
'h5.coupons.how.direct': '下单结算勾选本券,直接减免面额',
|
||||||
'h5.coupons.how.percent': '下单结算勾选本券,满{amount}元按折扣抵扣',
|
'h5.coupons.how.percent': '下单结算勾选本券,满{amount}元按折扣抵扣',
|
||||||
'h5.coupons.how.shipping': '下单结算勾选本券,满{amount}元可抵运费',
|
'h5.coupons.how.shipping': '下单结算勾选本券,满{amount}元减免基础运费',
|
||||||
|
'h5.coupons.how.unlock': '完成首单派送即可解锁 15 元券',
|
||||||
'h5.coupons.tab.available': '可用',
|
'h5.coupons.tab.available': '可用',
|
||||||
|
'h5.coupons.tab.locked': '未解锁',
|
||||||
'h5.coupons.tab.used': '已使用',
|
'h5.coupons.tab.used': '已使用',
|
||||||
'h5.coupons.tab.expired': '已过期',
|
'h5.coupons.tab.expired': '已过期',
|
||||||
|
|
||||||
'h5.invite.title': '邀请好友',
|
'h5.invite.title': '邀请好友',
|
||||||
'h5.invite.tab.share': '分享邀请',
|
'h5.invite.tab.share': '分享邀请',
|
||||||
'h5.invite.tab.referees': '已邀请好友',
|
'h5.invite.tab.referees': '已邀请好友',
|
||||||
'h5.invite.lead': '分享邀请码或二维码,好友在微信内打开并完成公众号注册后,双方可享营销奖励',
|
'h5.invite.lead': '好友注册即得 5 元券;其订单派送成功后,您按实付基础运费获得奖励',
|
||||||
|
'h5.invite.locked': '升级到 V1 后可邀请好友',
|
||||||
'h5.invite.code': '邀请码',
|
'h5.invite.code': '邀请码',
|
||||||
'h5.invite.link': '邀请链接(微信内打开)',
|
'h5.invite.link': '邀请链接(微信内打开)',
|
||||||
'h5.invite.copy': '复制',
|
'h5.invite.copy': '复制',
|
||||||
@ -888,7 +891,7 @@ export default {
|
|||||||
'h5.pay.later': '稍后再说',
|
'h5.pay.later': '稍后再说',
|
||||||
'h5.me.balance': '账户余额',
|
'h5.me.balance': '账户余额',
|
||||||
'h5.me.recharge': '去充值',
|
'h5.me.recharge': '去充值',
|
||||||
'h5.me.points': '积分',
|
'h5.me.points': '近12个月积分',
|
||||||
'h5.me.pointsToNext': '距{name}还差 {points}',
|
'h5.me.pointsToNext': '距{name}还差 {points}',
|
||||||
'h5.me.pointsTopLevel': '已达最高等级',
|
'h5.me.pointsTopLevel': '已达最高等级',
|
||||||
'h5.me.lowBalance': '余额偏低,建议尽快充值',
|
'h5.me.lowBalance': '余额偏低,建议尽快充值',
|
||||||
|
|||||||
@ -418,7 +418,13 @@ const memberLevelName = computed(() =>
|
|||||||
const levelBadge = computed(() => memberLevelName.value || customerGroupLabel.value);
|
const levelBadge = computed(() => memberLevelName.value || customerGroupLabel.value);
|
||||||
|
|
||||||
const totalPoints = computed(() =>
|
const totalPoints = computed(() =>
|
||||||
Number(memberOverview.value?.totalPoints ?? memberOverview.value?.TotalPoints ?? 0),
|
Number(
|
||||||
|
memberOverview.value?.rollingPoints ??
|
||||||
|
memberOverview.value?.RollingPoints ??
|
||||||
|
memberOverview.value?.totalPoints ??
|
||||||
|
memberOverview.value?.TotalPoints ??
|
||||||
|
0,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
const nextLevelName = computed(() =>
|
const nextLevelName = computed(() =>
|
||||||
String(memberOverview.value?.nextLevelName ?? memberOverview.value?.NextLevelName ?? ''),
|
String(memberOverview.value?.nextLevelName ?? memberOverview.value?.NextLevelName ?? ''),
|
||||||
|
|||||||
@ -26,8 +26,6 @@
|
|||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="login-hero__admin-stack">
|
<div class="login-hero__admin-stack">
|
||||||
<p class="login-hero__brief">{{ t('login.hero.admin.brief') }}</p>
|
|
||||||
|
|
||||||
<ol
|
<ol
|
||||||
class="login-hero__principles"
|
class="login-hero__principles"
|
||||||
@mouseenter="hovered = true"
|
@mouseenter="hovered = true"
|
||||||
@ -38,12 +36,12 @@
|
|||||||
<li
|
<li
|
||||||
v-for="(pillar, index) in adminPillars"
|
v-for="(pillar, index) in adminPillars"
|
||||||
:key="pillar.key"
|
:key="pillar.key"
|
||||||
class="login-hero__principle"
|
class="login-hero__principle login-hero__spine-item login-hero__spine-item--left"
|
||||||
:class="{
|
:class="{
|
||||||
'is-active': activePrinciple === index,
|
'is-active': activePrinciple === index,
|
||||||
'is-dim': activePrinciple >= 0 && activePrinciple !== index,
|
'is-dim': activePrinciple >= 0 && activePrinciple !== index,
|
||||||
}"
|
}"
|
||||||
:style="{ '--i': index }"
|
:style="{ '--i': index, '--t': pillar.t }"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@mouseenter="activePrinciple = index"
|
@mouseenter="activePrinciple = index"
|
||||||
@focus="activePrinciple = index"
|
@focus="activePrinciple = index"
|
||||||
@ -61,27 +59,23 @@
|
|||||||
</li>
|
</li>
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<div class="login-hero__ai" role="note">
|
<div
|
||||||
|
class="login-hero__ai login-hero__spine-item login-hero__spine-item--left"
|
||||||
|
role="note"
|
||||||
|
style="--t: 0.72"
|
||||||
|
>
|
||||||
<span class="login-hero__ai-badge">
|
<span class="login-hero__ai-badge">
|
||||||
<icon-robot :size="14" />
|
<icon-robot :size="14" />
|
||||||
AI
|
AI
|
||||||
</span>
|
</span>
|
||||||
<p class="login-hero__ai-text">{{ t('login.hero.admin.ai') }}</p>
|
<p class="login-hero__ai-text">{{ t('login.hero.admin.ai') }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="login-hero__flow" aria-hidden="true">
|
|
||||||
<span>{{ t('login.hero.admin.flow.pickup') }}</span>
|
|
||||||
<i />
|
|
||||||
<span>{{ t('login.hero.admin.flow.transit') }}</span>
|
|
||||||
<i />
|
|
||||||
<span>{{ t('login.hero.admin.flow.delivery') }}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="login-hero__visual">
|
<div class="login-hero__visual">
|
||||||
<picture>
|
<picture v-if="isCustomer">
|
||||||
<source srcset="/login-assets/hero-plane.webp" type="image/webp" />
|
<source srcset="/login-assets/hero-plane.webp" type="image/webp" />
|
||||||
<img
|
<img
|
||||||
class="login-hero__plane"
|
class="login-hero__plane"
|
||||||
@ -90,6 +84,13 @@
|
|||||||
draggable="false"
|
draggable="false"
|
||||||
/>
|
/>
|
||||||
</picture>
|
</picture>
|
||||||
|
<img
|
||||||
|
v-else
|
||||||
|
class="login-hero__truck"
|
||||||
|
src="/login-assets/hero-truck.png"
|
||||||
|
alt=""
|
||||||
|
draggable="false"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -116,8 +117,37 @@ interface AdminPillar {
|
|||||||
icon: Component;
|
icon: Component;
|
||||||
titleKey: string;
|
titleKey: string;
|
||||||
descKey: string;
|
descKey: string;
|
||||||
|
side: 'left' | 'right';
|
||||||
|
t: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const adminPillars: AdminPillar[] = [
|
||||||
|
{
|
||||||
|
key: 'care',
|
||||||
|
icon: IconCheckCircle,
|
||||||
|
titleKey: 'login.hero.admin.pillar.care.title',
|
||||||
|
descKey: 'login.hero.admin.pillar.care.desc',
|
||||||
|
side: 'left',
|
||||||
|
t: 0.24,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'speed',
|
||||||
|
icon: IconThunderbolt,
|
||||||
|
titleKey: 'login.hero.admin.pillar.speed.title',
|
||||||
|
descKey: 'login.hero.admin.pillar.speed.desc',
|
||||||
|
side: 'left',
|
||||||
|
t: 0.40,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'reach',
|
||||||
|
icon: IconMindMapping,
|
||||||
|
titleKey: 'login.hero.admin.pillar.reach.title',
|
||||||
|
descKey: 'login.hero.admin.pillar.reach.desc',
|
||||||
|
side: 'left',
|
||||||
|
t: 0.56,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
light?: boolean;
|
light?: boolean;
|
||||||
@ -138,27 +168,6 @@ let cycleTimer: ReturnType<typeof window.setInterval> | null = null;
|
|||||||
|
|
||||||
const isCustomer = computed(() => props.portal === 'customer');
|
const isCustomer = computed(() => props.portal === 'customer');
|
||||||
|
|
||||||
const adminPillars: AdminPillar[] = [
|
|
||||||
{
|
|
||||||
key: 'care',
|
|
||||||
icon: IconCheckCircle,
|
|
||||||
titleKey: 'login.hero.admin.pillar.care.title',
|
|
||||||
descKey: 'login.hero.admin.pillar.care.desc',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'speed',
|
|
||||||
icon: IconThunderbolt,
|
|
||||||
titleKey: 'login.hero.admin.pillar.speed.title',
|
|
||||||
descKey: 'login.hero.admin.pillar.speed.desc',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'reach',
|
|
||||||
icon: IconMindMapping,
|
|
||||||
titleKey: 'login.hero.admin.pillar.reach.title',
|
|
||||||
descKey: 'login.hero.admin.pillar.reach.desc',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
function startCycle() {
|
function startCycle() {
|
||||||
stopCycle();
|
stopCycle();
|
||||||
if (isCustomer.value) return;
|
if (isCustomer.value) return;
|
||||||
@ -258,25 +267,75 @@ onBeforeUnmount(() => {
|
|||||||
max-width: 520px;
|
max-width: 520px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-hero--admin .login-hero__inner {
|
.login-hero--admin {
|
||||||
justify-content: flex-start;
|
width: 100%;
|
||||||
align-items: center;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-hero--admin .login-hero__content {
|
.login-hero--admin .login-hero__inner {
|
||||||
flex: 1 1 auto;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 520px;
|
|
||||||
display: block;
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-hero--admin .login-hero__content,
|
||||||
|
.login-hero--admin .login-hero__admin-stack,
|
||||||
|
.login-hero--admin .login-hero__principles {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-hero--admin .login-hero__spine-item {
|
||||||
|
--split-x: calc(
|
||||||
|
var(--login-split-top) + (var(--login-split-bottom) - var(--login-split-top)) * var(--t, 0.5)
|
||||||
|
);
|
||||||
|
position: absolute;
|
||||||
|
top: calc(var(--t, 0.5) * 100%);
|
||||||
|
left: auto;
|
||||||
|
right: calc(100% - var(--split-x) + 20px);
|
||||||
|
width: min(360px, 30vw);
|
||||||
|
margin: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-hero--admin .login-hero__spine-item--left {
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-hero--admin .login-hero__principle {
|
||||||
|
animation-name: login-hero-principle-in-admin;
|
||||||
|
transition:
|
||||||
|
background 0.25s ease,
|
||||||
|
opacity 0.25s ease;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes login-hero-principle-in-admin {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-hero__admin-stack {
|
.login-hero__admin-stack {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 460px;
|
max-width: 440px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-hero__brief {
|
.login-hero__brief {
|
||||||
margin: 0 0 22px;
|
margin: 0 0 18px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
@ -299,8 +358,8 @@ onBeforeUnmount(() => {
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 44px minmax(0, 1fr) 36px;
|
grid-template-columns: 44px minmax(0, 1fr) 36px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 14px;
|
gap: 12px;
|
||||||
padding: 16px 12px 16px 4px;
|
padding: 12px 8px 12px 2px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
cursor: default;
|
cursor: default;
|
||||||
outline: none;
|
outline: none;
|
||||||
@ -371,8 +430,8 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.login-hero__principle-title {
|
.login-hero__principle-title {
|
||||||
margin: 0 0 6px;
|
margin: 0 0 4px;
|
||||||
font-size: 22px;
|
font-size: 20px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
letter-spacing: 0.04em;
|
letter-spacing: 0.04em;
|
||||||
@ -409,7 +468,7 @@ onBeforeUnmount(() => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
margin: 22px 0 0;
|
margin: 16px 0 0;
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.14);
|
border: 1px solid rgba(255, 255, 255, 0.14);
|
||||||
@ -443,29 +502,20 @@ onBeforeUnmount(() => {
|
|||||||
color: rgba(255, 255, 255, 0.72);
|
color: rgba(255, 255, 255, 0.72);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-hero__flow {
|
.login-hero--admin .login-hero__visual {
|
||||||
|
position: absolute;
|
||||||
|
left: calc((var(--login-split-top) + var(--login-split-bottom)) / 4 - 150px);
|
||||||
|
top: 50%;
|
||||||
|
bottom: auto;
|
||||||
|
z-index: 1;
|
||||||
|
width: min(380px, 32vw);
|
||||||
|
margin: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
transform: translate(-50%, -50%) scale(1.3);
|
||||||
margin: 20px 0 0;
|
justify-content: center;
|
||||||
padding-top: 16px;
|
align-self: auto;
|
||||||
border-top: 1px solid rgba(255, 255, 255, 0.12);
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0.12em;
|
|
||||||
color: rgba(255, 255, 255, 0.45);
|
|
||||||
|
|
||||||
i {
|
|
||||||
flex: 1;
|
|
||||||
height: 1px;
|
|
||||||
min-width: 16px;
|
|
||||||
background: linear-gradient(
|
|
||||||
90deg,
|
|
||||||
rgba(255, 255, 255, 0.08),
|
|
||||||
rgba(255, 255, 255, 0.35),
|
|
||||||
rgba(255, 255, 255, 0.08)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes login-hero-principle-in {
|
@keyframes login-hero-principle-in {
|
||||||
@ -542,6 +592,17 @@ onBeforeUnmount(() => {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login-hero--admin .login-hero__truck {
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
height: auto;
|
||||||
|
display: block;
|
||||||
|
margin: 0;
|
||||||
|
transform: none;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
.login-hero--light {
|
.login-hero--light {
|
||||||
color: rgba(26, 45, 74, 0.88);
|
color: rgba(26, 45, 74, 0.88);
|
||||||
}
|
}
|
||||||
@ -607,30 +668,26 @@ onBeforeUnmount(() => {
|
|||||||
color: rgba(26, 45, 74, 0.68);
|
color: rgba(26, 45, 74, 0.68);
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-hero--light .login-hero__flow {
|
|
||||||
border-top-color: rgba(26, 45, 74, 0.1);
|
|
||||||
color: rgba(26, 45, 74, 0.42);
|
|
||||||
|
|
||||||
i {
|
|
||||||
background: linear-gradient(
|
|
||||||
90deg,
|
|
||||||
rgba(26, 45, 74, 0.04),
|
|
||||||
rgba(26, 45, 74, 0.22),
|
|
||||||
rgba(26, 45, 74, 0.04)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 1250px) {
|
@media screen and (max-width: 1250px) {
|
||||||
.login-hero__visual {
|
.login-hero__visual {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login-hero--admin .login-hero__visual {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
.login-hero__content {
|
.login-hero__content {
|
||||||
flex: 1 1 100%;
|
flex: 1 1 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 900px) {
|
||||||
|
.login-hero--admin .login-hero__visual {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
.login-hero__title {
|
.login-hero__title {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
@ -653,10 +710,6 @@ onBeforeUnmount(() => {
|
|||||||
.login-hero__principle-title {
|
.login-hero__principle-title {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-hero__flow {
|
|
||||||
letter-spacing: 0.06em;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
|||||||
@ -24,19 +24,36 @@
|
|||||||
:class="{ 'login-brand-corner--light': isLightMode }"
|
:class="{ 'login-brand-corner--light': isLightMode }"
|
||||||
>
|
>
|
||||||
<span class="login-brand-corner__name">{{ t('brand.name') }}</span>
|
<span class="login-brand-corner__name">{{ t('brand.name') }}</span>
|
||||||
<span class="login-brand-corner__suffix">I {{ t('brand.fullName') }}</span>
|
<span class="login-brand-corner__suffix">· {{ t('brand.fullName') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<aside :class="['login-hero-panel', { 'login-hero-panel--admin': !isCustomerLogin }]">
|
<aside v-if="isCustomerLogin" class="login-hero-panel">
|
||||||
<LoginHero
|
<LoginHero
|
||||||
:light="isLightMode"
|
:light="isLightMode"
|
||||||
:portal="isCustomerLogin ? 'customer' : 'admin'"
|
portal="customer"
|
||||||
/>
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
<div class="login-characters-panel">
|
<div v-if="isCustomerLogin" class="login-characters-panel">
|
||||||
<LoginCharacters v-if="showCharacters" @register="registerLoginCharacters" />
|
<LoginCharacters v-if="showCharacters" @register="registerLoginCharacters" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<aside
|
||||||
|
v-if="shellReady && !isCustomerLogin"
|
||||||
|
class="login-hero-panel login-hero-panel--admin"
|
||||||
|
>
|
||||||
|
<LoginHero
|
||||||
|
:light="isLightMode"
|
||||||
|
portal="admin"
|
||||||
|
/>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="shellReady && !isCustomerLogin"
|
||||||
|
class="login-characters-panel login-characters-panel--admin"
|
||||||
|
>
|
||||||
|
<LoginCharacters v-if="showCharacters" @register="registerLoginCharacters" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="shellReady" ref="rightPaneRef" class="login-pane login-pane--right">
|
<div v-if="shellReady" ref="rightPaneRef" class="login-pane login-pane--right">
|
||||||
<div ref="loginFormRef" :class="['login-form', { 'login-form--ready': shellReady }]">
|
<div ref="loginFormRef" :class="['login-form', { 'login-form--ready': shellReady }]">
|
||||||
<div class="login-form-toolbar">
|
<div class="login-form-toolbar">
|
||||||
@ -72,10 +89,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<footer v-if="shellReady" class="login-footer">
|
|
||||||
{{ t('login.footer.copyright') }}
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -96,7 +109,6 @@ import { getLoginTypeFromRouteName } from '@/utils/login-portal';
|
|||||||
import { IconSunFill, IconMoonFill } from '@arco-design/web-vue/es/icon';
|
import { IconSunFill, IconMoonFill } from '@arco-design/web-vue/es/icon';
|
||||||
import LoginForm from './components/pwd-login/index.vue';
|
import LoginForm from './components/pwd-login/index.vue';
|
||||||
import LoginHero from './components/LoginHero.vue';
|
import LoginHero from './components/LoginHero.vue';
|
||||||
import LoginCareBanner from './components/LoginCareBanner.vue';
|
|
||||||
import LoginLangToggle from './components/LoginLangToggle.vue';
|
import LoginLangToggle from './components/LoginLangToggle.vue';
|
||||||
import {
|
import {
|
||||||
LOGIN_CHARACTERS_KEY,
|
LOGIN_CHARACTERS_KEY,
|
||||||
@ -112,6 +124,9 @@ const WechatLogin = defineAsyncComponent(
|
|||||||
const LoginCharacters = defineAsyncComponent(
|
const LoginCharacters = defineAsyncComponent(
|
||||||
() => import('./components/LoginCharacters.vue'),
|
() => import('./components/LoginCharacters.vue'),
|
||||||
);
|
);
|
||||||
|
const LoginCareBanner = defineAsyncComponent(
|
||||||
|
() => import('./components/LoginCareBanner.vue'),
|
||||||
|
);
|
||||||
|
|
||||||
const LOGIN_APPEARANCE_KEY = 'login-appearance';
|
const LOGIN_APPEARANCE_KEY = 'login-appearance';
|
||||||
const LOGIN_PAGE_SCROLL_LOCK_CLASS = 'login-page-no-scroll';
|
const LOGIN_PAGE_SCROLL_LOCK_CLASS = 'login-page-no-scroll';
|
||||||
@ -271,14 +286,18 @@ function prefetchWechatQr() {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
document.documentElement.classList.add(LOGIN_PAGE_SCROLL_LOCK_CLASS);
|
document.documentElement.classList.add(LOGIN_PAGE_SCROLL_LOCK_CLASS);
|
||||||
shellReady.value = true;
|
shellReady.value = true;
|
||||||
// 首屏先出表单,下一帧再挂角色动画分包
|
const idle = window.requestIdleCallback
|
||||||
requestAnimationFrame(() => {
|
? (cb: () => void, timeout = 1200) => window.requestIdleCallback(cb, { timeout })
|
||||||
|
: (cb: () => void, timeout = 1200) => window.setTimeout(cb, Math.min(timeout, 400));
|
||||||
|
// 表单先出,空闲后再挂角色,避免和首屏 JS/图抢带宽
|
||||||
|
idle(() => {
|
||||||
showCharacters.value = true;
|
showCharacters.value = true;
|
||||||
});
|
}, 1600);
|
||||||
// 后台预拉完整语言包,进系统时几乎不等
|
idle(() => {
|
||||||
void import('@/locale').then(({ ensureFullLocaleMessages }) => {
|
void import('@/locale').then(({ ensureFullLocaleMessages }) => {
|
||||||
void ensureFullLocaleMessages();
|
void ensureFullLocaleMessages();
|
||||||
});
|
});
|
||||||
|
}, 2500);
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@ -318,24 +337,6 @@ onBeforeUnmount(() => {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-footer {
|
|
||||||
position: fixed;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 16px;
|
|
||||||
z-index: 12;
|
|
||||||
text-align: center;
|
|
||||||
padding: 0 16px;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: rgba(148, 163, 184, 0.92);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-container--light .login-footer {
|
|
||||||
color: rgba(100, 116, 139, 0.88);
|
|
||||||
}
|
|
||||||
|
|
||||||
.login-split {
|
.login-split {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
@ -435,10 +436,22 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.login-hero-panel--admin {
|
.login-hero-panel--admin {
|
||||||
display: flex;
|
inset: 0;
|
||||||
align-items: center;
|
top: 0;
|
||||||
justify-content: center;
|
left: 0;
|
||||||
width: min(800px, calc((var(--login-split-top) + var(--login-split-bottom)) / 2 - 16px));
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
transform: none;
|
||||||
|
z-index: 3;
|
||||||
|
display: block;
|
||||||
|
width: auto;
|
||||||
|
max-width: none;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-hero-panel--admin .login-hero {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.login-characters-panel {
|
.login-characters-panel {
|
||||||
@ -451,6 +464,22 @@ onBeforeUnmount(() => {
|
|||||||
padding: 0 0 16px 8px;
|
padding: 0 0 16px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.login-characters-panel--admin {
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 6;
|
||||||
|
padding: 0;
|
||||||
|
transform: scale(0.82);
|
||||||
|
transform-origin: bottom right;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-characters-panel--admin :deep(.login-characters__stage) {
|
||||||
|
width: 450px;
|
||||||
|
transform-origin: bottom right;
|
||||||
|
}
|
||||||
|
|
||||||
.login-form-toolbar {
|
.login-form-toolbar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 14px;
|
top: 14px;
|
||||||
|
|||||||
@ -46,9 +46,6 @@
|
|||||||
'login.hero.admin.pillar.reach.desc': 'Pickup to delivery · Clear flow · Fewer detours',
|
'login.hero.admin.pillar.reach.desc': 'Pickup to delivery · Clear flow · Fewer detours',
|
||||||
'login.hero.admin.ai':
|
'login.hero.admin.ai':
|
||||||
'Embrace AI: spot issues, protect SLAs, simplify flows — people decide, AI accelerates.',
|
'Embrace AI: spot issues, protect SLAs, simplify flows — people decide, AI accelerates.',
|
||||||
'login.hero.admin.flow.pickup': 'Pickup',
|
|
||||||
'login.hero.admin.flow.transit': 'In transit',
|
|
||||||
'login.hero.admin.flow.delivery': 'Delivered',
|
|
||||||
'login.care.period.morning': 'Good morning — a fresh start',
|
'login.care.period.morning': 'Good morning — a fresh start',
|
||||||
'login.care.period.afternoon': 'Good afternoon — steady wins',
|
'login.care.period.afternoon': 'Good afternoon — steady wins',
|
||||||
'login.care.period.evening': 'Good evening — thanks for today',
|
'login.care.period.evening': 'Good evening — thanks for today',
|
||||||
|
|||||||
@ -45,9 +45,6 @@
|
|||||||
'login.hero.admin.pillar.reach.title': '更简单',
|
'login.hero.admin.pillar.reach.title': '更简单',
|
||||||
'login.hero.admin.pillar.reach.desc': '揽收到签收 · 链路清晰 · 少绕少错',
|
'login.hero.admin.pillar.reach.desc': '揽收到签收 · 链路清晰 · 少绕少错',
|
||||||
'login.hero.admin.ai': '积极拥抱 AI:帮你盯异常、催时效、简流程——人做决策,AI 做加速。',
|
'login.hero.admin.ai': '积极拥抱 AI:帮你盯异常、催时效、简流程——人做决策,AI 做加速。',
|
||||||
'login.hero.admin.flow.pickup': '揽收',
|
|
||||||
'login.hero.admin.flow.transit': '在途',
|
|
||||||
'login.hero.admin.flow.delivery': '签收',
|
|
||||||
'login.care.period.morning': '早上好,新的一天开始了',
|
'login.care.period.morning': '早上好,新的一天开始了',
|
||||||
'login.care.period.afternoon': '下午好,稳稳推进就好',
|
'login.care.period.afternoon': '下午好,稳稳推进就好',
|
||||||
'login.care.period.evening': '傍晚好,今天也辛苦了',
|
'login.care.period.evening': '傍晚好,今天也辛苦了',
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<MarketingPageShell :tip="t('marketing.commissionDetail.pageTip')">
|
<MarketingPageShell :tip="t('marketing.commissionDetail.pageTip')">
|
||||||
|
<div class="mkt-status-tabs">
|
||||||
|
<SlidingTabs v-model="statusTab" :items="statusTabs" @change="handleSearch" />
|
||||||
|
</div>
|
||||||
<div class="table-toolbar">
|
<div class="table-toolbar">
|
||||||
<div class="toolbar-left">
|
<div class="toolbar-left">
|
||||||
<div class="toolbar-filters">
|
<div class="toolbar-filters">
|
||||||
@ -11,18 +14,6 @@
|
|||||||
@press-enter="handleSearch"
|
@press-enter="handleSearch"
|
||||||
@clear="handleSearch"
|
@clear="handleSearch"
|
||||||
/>
|
/>
|
||||||
<a-select
|
|
||||||
v-model="statusFilter"
|
|
||||||
allow-clear
|
|
||||||
class="mkt-filter-status"
|
|
||||||
:style="{ width: '140px' }"
|
|
||||||
:placeholder="t('marketing.commissionDetail.statusFilter')"
|
|
||||||
@change="handleSearch"
|
|
||||||
>
|
|
||||||
<a-option :value="1">{{ t('marketing.commissionDetail.status.pending') }}</a-option>
|
|
||||||
<a-option :value="2">{{ t('marketing.commissionDetail.status.settled') }}</a-option>
|
|
||||||
<a-option :value="3">{{ t('marketing.commissionDetail.status.cancelled') }}</a-option>
|
|
||||||
</a-select>
|
|
||||||
<MarketingPageTipIcon />
|
<MarketingPageTipIcon />
|
||||||
<a-button class="app-btn-search" @click="handleSearch">
|
<a-button class="app-btn-search" @click="handleSearch">
|
||||||
{{ t('common.search') }}
|
{{ t('common.search') }}
|
||||||
@ -31,9 +22,6 @@
|
|||||||
<a-button class="app-btn-toolbar" @click="openAdjustModal">
|
<a-button class="app-btn-toolbar" @click="openAdjustModal">
|
||||||
{{ t('marketing.commissionDetail.adjust') }}
|
{{ t('marketing.commissionDetail.adjust') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button @click="openRuleModal">
|
|
||||||
{{ t('menu.marketing.commissionRule') }}
|
|
||||||
</a-button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-right">
|
<div class="toolbar-right">
|
||||||
<a-space :size="8">
|
<a-space :size="8">
|
||||||
@ -92,52 +80,21 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ModalPro>
|
</ModalPro>
|
||||||
|
|
||||||
<ModalPro
|
|
||||||
v-model:visible="ruleVisible"
|
|
||||||
:title="t('menu.marketing.commissionRule')"
|
|
||||||
:confirm-loading="ruleSaving"
|
|
||||||
width="640px"
|
|
||||||
unmount-on-close
|
|
||||||
@ok="handleRuleOk"
|
|
||||||
@cancel="handleRuleCancel"
|
|
||||||
>
|
|
||||||
<div class="mkt-modal">
|
|
||||||
<div class="mkt-modal__tip">
|
|
||||||
<icon-info-circle class="mkt-modal__tip-icon" />
|
|
||||||
<p>{{ t('marketing.commissionRule.pageTip') }}</p>
|
|
||||||
</div>
|
|
||||||
<a-spin :loading="ruleLoading">
|
|
||||||
<FormPro
|
|
||||||
ref="ruleFormRef"
|
|
||||||
class="mkt-modal__form"
|
|
||||||
form-layout="vertical"
|
|
||||||
:hide-colon="true"
|
|
||||||
:item-list="ruleItemList"
|
|
||||||
:row-gutter="16"
|
|
||||||
:default-span="12"
|
|
||||||
:label-col-props="{ span: 24 }"
|
|
||||||
:wrapper-col-props="{ span: 24 }"
|
|
||||||
/>
|
|
||||||
</a-spin>
|
|
||||||
</div>
|
|
||||||
</ModalPro>
|
|
||||||
</MarketingPageShell>
|
</MarketingPageShell>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { Message, Modal } from '@arco-design/web-vue'
|
import { Message } from '@arco-design/web-vue'
|
||||||
import HandsontablePro from '@/components/HandsontablePro/index.vue'
|
import HandsontablePro from '@/components/HandsontablePro/index.vue'
|
||||||
import PaginationPro from '@/components/PaginationPro/index.vue'
|
import PaginationPro from '@/components/PaginationPro/index.vue'
|
||||||
import ModalPro from '@/components/ModalPro/index.vue'
|
import ModalPro from '@/components/ModalPro/index.vue'
|
||||||
import FormPro from '@/components/FormPro/index.vue'
|
import FormPro from '@/components/FormPro/index.vue'
|
||||||
|
import SlidingTabs from '@/components/SlidingTabs/index.vue'
|
||||||
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
|
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
|
||||||
import { isEnableStatusEnabled, toEnableStatus } from '@/utils/enableStatus'
|
|
||||||
import { buildStatusTagHtml, type StatusTagTone } from '@/utils/statusTag'
|
import { buildStatusTagHtml, type StatusTagTone } from '@/utils/statusTag'
|
||||||
import { adjustCommissionDetail, getCommissionDetailList } from '@/api/marketing/commissionDetail'
|
import { adjustCommissionDetail, getCommissionDetailList } from '@/api/marketing/commissionDetail'
|
||||||
import { getCommissionRule, updateCommissionRule } from '@/api/marketing/commissionRule'
|
|
||||||
import MarketingPageShell from '../components/MarketingPageShell.vue'
|
import MarketingPageShell from '../components/MarketingPageShell.vue'
|
||||||
import MarketingPageTipIcon from '../components/MarketingPageTipIcon.vue'
|
import MarketingPageTipIcon from '../components/MarketingPageTipIcon.vue'
|
||||||
|
|
||||||
@ -160,9 +117,11 @@ interface DetailRow {
|
|||||||
orderNo: string
|
orderNo: string
|
||||||
basePayableAmount: number
|
basePayableAmount: number
|
||||||
commissionAmount: number
|
commissionAmount: number
|
||||||
|
rewardOrderIndex: number
|
||||||
status: number
|
status: number
|
||||||
statusName: string
|
statusName: string
|
||||||
eligibleTime: string
|
eligibleTime: string
|
||||||
|
settleDueTime: string
|
||||||
settleTime: string
|
settleTime: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,20 +129,21 @@ const tableRows = ref<DetailRow[]>([])
|
|||||||
const totalCount = ref(0)
|
const totalCount = ref(0)
|
||||||
const listLoading = ref(false)
|
const listLoading = ref(false)
|
||||||
const orderNo = ref('')
|
const orderNo = ref('')
|
||||||
const statusFilter = ref<number | undefined>(undefined)
|
const statusTab = ref('all')
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
|
|
||||||
|
const statusTabs = computed(() => [
|
||||||
|
{ key: 'all', label: t('common.all') },
|
||||||
|
{ key: '1', label: t('marketing.commissionDetail.status.pending') },
|
||||||
|
{ key: '2', label: t('marketing.commissionDetail.status.settled') },
|
||||||
|
{ key: '3', label: t('marketing.commissionDetail.status.cancelled') },
|
||||||
|
])
|
||||||
const pageSize = ref(50)
|
const pageSize = ref(50)
|
||||||
|
|
||||||
const adjustVisible = ref(false)
|
const adjustVisible = ref(false)
|
||||||
const adjustLoading = ref(false)
|
const adjustLoading = ref(false)
|
||||||
const adjustFormRef = ref<FormExpose>()
|
const adjustFormRef = ref<FormExpose>()
|
||||||
|
|
||||||
const ruleVisible = ref(false)
|
|
||||||
const ruleLoading = ref(false)
|
|
||||||
const ruleSaving = ref(false)
|
|
||||||
const ruleFormRef = ref<FormExpose>()
|
|
||||||
const originalRule = ref<Record<string, unknown> | null>(null)
|
|
||||||
|
|
||||||
const adjustItemList = computed(() => [
|
const adjustItemList = computed(() => [
|
||||||
{
|
{
|
||||||
field: 'customerCode',
|
field: 'customerCode',
|
||||||
@ -241,82 +201,16 @@ const adjustItemList = computed(() => [
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
const ruleItemList = computed(() => [
|
|
||||||
{
|
|
||||||
field: 'ruleName',
|
|
||||||
label: t('marketing.commissionRule.form.ruleName'),
|
|
||||||
comp: 'a-input',
|
|
||||||
required: true,
|
|
||||||
rules: [{ required: true, message: t('marketing.commissionRule.form.ruleName') }],
|
|
||||||
compProps: {
|
|
||||||
allowClear: true,
|
|
||||||
maxLength: 64,
|
|
||||||
placeholder: t('marketing.commissionRule.form.ruleName'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'status',
|
|
||||||
label: t('marketing.commissionRule.form.status'),
|
|
||||||
comp: 'a-radio-group',
|
|
||||||
required: true,
|
|
||||||
rules: [{ required: true, message: t('marketing.commissionRule.form.status') }],
|
|
||||||
compProps: {
|
|
||||||
type: 'button',
|
|
||||||
options: [
|
|
||||||
{ label: t('common.enabled'), value: 2 },
|
|
||||||
{ label: t('common.disabled'), value: 1 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'baseAmount',
|
|
||||||
label: t('marketing.commissionRule.form.baseAmount'),
|
|
||||||
comp: 'a-input-number',
|
|
||||||
required: true,
|
|
||||||
tooltip: t('marketing.commissionRule.form.baseAmountExtra'),
|
|
||||||
rules: [{ required: true, message: t('marketing.commissionRule.form.baseAmount') }],
|
|
||||||
compProps: { min: 1, precision: 2, style: 'width: 100%', placeholder: '1000' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'commissionAmount',
|
|
||||||
label: t('marketing.commissionRule.form.commissionAmount'),
|
|
||||||
comp: 'a-input-number',
|
|
||||||
required: true,
|
|
||||||
tooltip: t('marketing.commissionRule.form.commissionAmountExtra'),
|
|
||||||
rules: [{ required: true, message: t('marketing.commissionRule.form.commissionAmount') }],
|
|
||||||
compProps: { min: 0, precision: 2, style: 'width: 100%', placeholder: '2' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'freezeDays',
|
|
||||||
label: t('marketing.commissionRule.form.freezeDays'),
|
|
||||||
comp: 'a-input-number',
|
|
||||||
required: true,
|
|
||||||
tooltip: t('marketing.commissionRule.form.freezeDaysExtra'),
|
|
||||||
rules: [{ required: true, message: t('marketing.commissionRule.form.freezeDays') }],
|
|
||||||
compProps: { min: 0, precision: 0, style: 'width: 100%', placeholder: '7' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'monthlyCap',
|
|
||||||
label: t('marketing.commissionRule.form.monthlyCap'),
|
|
||||||
comp: 'a-input-number',
|
|
||||||
tooltip: t('marketing.commissionRule.form.monthlyCapExtra'),
|
|
||||||
compProps: {
|
|
||||||
min: 0,
|
|
||||||
precision: 2,
|
|
||||||
style: 'width: 100%',
|
|
||||||
placeholder: t('marketing.commissionRule.form.monthlyCapExtra'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
const hotColumns = computed(() => [
|
const hotColumns = computed(() => [
|
||||||
{ key: 'referrerCustomerCode', label: t('marketing.commissionDetail.columns.referrer'), width: 110 },
|
{ key: 'referrerCustomerCode', label: t('marketing.commissionDetail.columns.referrer'), width: 110 },
|
||||||
{ key: 'refereeCustomerCode', label: t('marketing.commissionDetail.columns.referee'), width: 110 },
|
{ key: 'refereeCustomerCode', label: t('marketing.commissionDetail.columns.referee'), width: 110 },
|
||||||
{ key: 'orderNo', label: t('marketing.commissionDetail.columns.orderNo'), width: 130 },
|
{ key: 'orderNo', label: t('marketing.commissionDetail.columns.orderNo'), width: 130 },
|
||||||
{ key: 'basePayableAmount', label: t('marketing.commissionDetail.columns.baseAmount'), width: 100 },
|
{ key: 'basePayableAmount', label: t('marketing.commissionDetail.columns.baseAmount'), width: 120 },
|
||||||
{ key: 'commissionAmount', label: t('marketing.commissionDetail.columns.commission'), width: 90 },
|
{ key: 'commissionAmount', label: t('marketing.commissionDetail.columns.commission'), width: 90 },
|
||||||
|
{ key: 'rewardOrderIndex', label: t('marketing.commissionDetail.columns.orderIndex'), width: 80 },
|
||||||
{ key: 'statusName', label: t('marketing.commissionDetail.columns.status'), width: 90 },
|
{ key: 'statusName', label: t('marketing.commissionDetail.columns.status'), width: 90 },
|
||||||
{ key: 'eligibleTime', label: t('marketing.commissionDetail.columns.eligibleTime'), width: 160 },
|
{ key: 'eligibleTime', label: t('marketing.commissionDetail.columns.eligibleTime'), width: 160 },
|
||||||
|
{ key: 'settleDueTime', label: t('marketing.commissionDetail.columns.settleDue'), width: 160 },
|
||||||
{ key: 'settleTime', label: t('marketing.commissionDetail.columns.settleTime'), width: 160 },
|
{ key: 'settleTime', label: t('marketing.commissionDetail.columns.settleTime'), width: 160 },
|
||||||
])
|
])
|
||||||
|
|
||||||
@ -345,33 +239,21 @@ function mapRow(item: Record<string, unknown>): DetailRow {
|
|||||||
orderNo: String(item.orderNo ?? item.OrderNo ?? ''),
|
orderNo: String(item.orderNo ?? item.OrderNo ?? ''),
|
||||||
basePayableAmount: Number(item.basePayableAmount ?? item.BasePayableAmount ?? 0),
|
basePayableAmount: Number(item.basePayableAmount ?? item.BasePayableAmount ?? 0),
|
||||||
commissionAmount: Number(item.commissionAmount ?? item.CommissionAmount ?? 0),
|
commissionAmount: Number(item.commissionAmount ?? item.CommissionAmount ?? 0),
|
||||||
|
rewardOrderIndex: Number(item.rewardOrderIndex ?? item.RewardOrderIndex ?? 0),
|
||||||
status: Number(item.status ?? item.Status ?? 0),
|
status: Number(item.status ?? item.Status ?? 0),
|
||||||
statusName: String(item.statusName ?? item.StatusName ?? ''),
|
statusName: String(item.statusName ?? item.StatusName ?? ''),
|
||||||
eligibleTime: String(item.eligibleTime ?? item.EligibleTime ?? '').replace('T', ' ').slice(0, 19) || '--',
|
eligibleTime: String(item.eligibleTime ?? item.EligibleTime ?? '').replace('T', ' ').slice(0, 19) || '--',
|
||||||
|
settleDueTime: String(item.settleDueTime ?? item.SettleDueTime ?? '').replace('T', ' ').slice(0, 19) || '--',
|
||||||
settleTime: String(item.settleTime ?? item.SettleTime ?? '').replace('T', ' ').slice(0, 19) || '--',
|
settleTime: String(item.settleTime ?? item.SettleTime ?? '').replace('T', ' ').slice(0, 19) || '--',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapRule(item: Record<string, unknown>) {
|
|
||||||
const status = item.status ?? item.Status ?? 2
|
|
||||||
const cap = item.monthlyCap ?? item.MonthlyCap
|
|
||||||
return {
|
|
||||||
id: (item.id ?? item.Id) as number | undefined,
|
|
||||||
ruleName: String(item.ruleName ?? item.RuleName ?? ''),
|
|
||||||
baseAmount: Number(item.baseAmount ?? item.BaseAmount ?? 1000),
|
|
||||||
commissionAmount: Number(item.commissionAmount ?? item.CommissionAmount ?? 2),
|
|
||||||
freezeDays: Number(item.freezeDays ?? item.FreezeDays ?? 7),
|
|
||||||
monthlyCap: cap != null && cap !== '' ? Number(cap) : undefined,
|
|
||||||
status: toEnableStatus(isEnableStatusEnabled(status)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadList() {
|
async function loadList() {
|
||||||
listLoading.value = true
|
listLoading.value = true
|
||||||
try {
|
try {
|
||||||
const { items, total } = await getCommissionDetailList({
|
const { items, total } = await getCommissionDetailList({
|
||||||
orderNo: orderNo.value.trim() || undefined,
|
orderNo: orderNo.value.trim() || undefined,
|
||||||
status: statusFilter.value,
|
status: statusTab.value === 'all' ? undefined : Number(statusTab.value),
|
||||||
page: currentPage.value,
|
page: currentPage.value,
|
||||||
pageSize: pageSize.value,
|
pageSize: pageSize.value,
|
||||||
})
|
})
|
||||||
@ -447,91 +329,6 @@ async function handleAdjustOk() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openRuleModal() {
|
|
||||||
ruleVisible.value = true
|
|
||||||
ruleLoading.value = true
|
|
||||||
await nextTick()
|
|
||||||
try {
|
|
||||||
const data = await getCommissionRule()
|
|
||||||
await nextTick()
|
|
||||||
if (data && typeof data === 'object') {
|
|
||||||
const mapped = mapRule(data as Record<string, unknown>)
|
|
||||||
originalRule.value = { ...mapped }
|
|
||||||
ruleFormRef.value?.setValues(mapped)
|
|
||||||
} else {
|
|
||||||
const defaults = {
|
|
||||||
id: undefined,
|
|
||||||
ruleName: '',
|
|
||||||
baseAmount: 1000,
|
|
||||||
commissionAmount: 2,
|
|
||||||
freezeDays: 7,
|
|
||||||
monthlyCap: undefined,
|
|
||||||
status: 2,
|
|
||||||
}
|
|
||||||
originalRule.value = { ...defaults }
|
|
||||||
ruleFormRef.value?.setValues(defaults)
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
Message.error(e?.message || t('common.loadFailed'))
|
|
||||||
} finally {
|
|
||||||
ruleLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleRuleCancel() {
|
|
||||||
ruleVisible.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleRuleOk() {
|
|
||||||
try {
|
|
||||||
await ruleFormRef.value?.validate()
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const values = ruleFormRef.value?.getValues() || {}
|
|
||||||
const ruleName = String(values.ruleName || '').trim()
|
|
||||||
if (!ruleName) {
|
|
||||||
Message.warning(t('marketing.commissionRule.form.ruleName'))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const next = {
|
|
||||||
id: values.id,
|
|
||||||
ruleName,
|
|
||||||
baseAmount: Number(values.baseAmount ?? 1000),
|
|
||||||
commissionAmount: Number(values.commissionAmount ?? 2),
|
|
||||||
freezeDays: Number(values.freezeDays ?? 7),
|
|
||||||
monthlyCap:
|
|
||||||
values.monthlyCap != null && values.monthlyCap !== '' ? Number(values.monthlyCap) : null,
|
|
||||||
status: Number(values.status ?? 2),
|
|
||||||
}
|
|
||||||
const o = originalRule.value || {}
|
|
||||||
Modal.confirm({
|
|
||||||
title: t('marketing.commissionRule.saveConfirmTitle'),
|
|
||||||
content: t('marketing.commissionRule.saveCompare', {
|
|
||||||
oldBase: o.baseAmount ?? next.baseAmount,
|
|
||||||
newBase: next.baseAmount,
|
|
||||||
oldCommission: o.commissionAmount ?? next.commissionAmount,
|
|
||||||
newCommission: next.commissionAmount,
|
|
||||||
oldFreeze: o.freezeDays ?? next.freezeDays,
|
|
||||||
newFreeze: next.freezeDays,
|
|
||||||
}),
|
|
||||||
onOk: async () => {
|
|
||||||
ruleSaving.value = true
|
|
||||||
try {
|
|
||||||
await updateCommissionRule(next)
|
|
||||||
Message.success(t('common.saveSuccess'))
|
|
||||||
ruleVisible.value = false
|
|
||||||
} catch (e: any) {
|
|
||||||
Message.error(e?.message || t('common.saveFailed'))
|
|
||||||
throw e
|
|
||||||
} finally {
|
|
||||||
ruleSaving.value = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => loadList())
|
onMounted(() => loadList())
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -6,12 +6,12 @@
|
|||||||
import { onMounted } from 'vue'
|
import { onMounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
/** 兼容旧菜单「分成规则」:跳转到分成明细(规则配置为弹窗) */
|
/** 兼容旧菜单「分成规则」:跳转到积分规则页(分成配置在该页下方) */
|
||||||
defineOptions({ name: 'MarketingCommissionRule' })
|
defineOptions({ name: 'MarketingCommissionRule' })
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
router.replace({ name: 'MarketingCommissionDetail' })
|
router.replace({ name: 'MarketingPointsRule' })
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<MarketingPageShell :tip="t('marketing.commissionWithdraw.pageTip')">
|
<MarketingPageShell :tip="t('marketing.commissionWithdraw.pageTip')">
|
||||||
|
<div class="mkt-status-tabs">
|
||||||
|
<SlidingTabs v-model="statusTab" :items="statusTabs" @change="handleSearch" />
|
||||||
|
</div>
|
||||||
<div class="table-toolbar">
|
<div class="table-toolbar">
|
||||||
<div class="toolbar-left">
|
<div class="toolbar-left">
|
||||||
<div class="toolbar-filters">
|
<div class="toolbar-filters">
|
||||||
@ -11,19 +14,6 @@
|
|||||||
@press-enter="handleSearch"
|
@press-enter="handleSearch"
|
||||||
@clear="handleSearch"
|
@clear="handleSearch"
|
||||||
/>
|
/>
|
||||||
<a-select
|
|
||||||
v-model="statusFilter"
|
|
||||||
allow-clear
|
|
||||||
class="mkt-filter-status"
|
|
||||||
:style="{ width: '140px' }"
|
|
||||||
:placeholder="t('marketing.commissionWithdraw.statusFilter')"
|
|
||||||
@change="handleSearch"
|
|
||||||
>
|
|
||||||
<a-option :value="1">{{ t('marketing.commissionWithdraw.status.pending') }}</a-option>
|
|
||||||
<a-option :value="2">{{ t('marketing.commissionWithdraw.status.approved') }}</a-option>
|
|
||||||
<a-option :value="3">{{ t('marketing.commissionWithdraw.status.rejected') }}</a-option>
|
|
||||||
<a-option :value="4">{{ t('marketing.commissionWithdraw.status.paid') }}</a-option>
|
|
||||||
</a-select>
|
|
||||||
<a-select
|
<a-select
|
||||||
v-model="withdrawType"
|
v-model="withdrawType"
|
||||||
allow-clear
|
allow-clear
|
||||||
@ -123,6 +113,7 @@ import HandsontablePro from '@/components/HandsontablePro/index.vue'
|
|||||||
import PaginationPro from '@/components/PaginationPro/index.vue'
|
import PaginationPro from '@/components/PaginationPro/index.vue'
|
||||||
import ModalPro from '@/components/ModalPro/index.vue'
|
import ModalPro from '@/components/ModalPro/index.vue'
|
||||||
import FormPro from '@/components/FormPro/index.vue'
|
import FormPro from '@/components/FormPro/index.vue'
|
||||||
|
import SlidingTabs from '@/components/SlidingTabs/index.vue'
|
||||||
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
|
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
|
||||||
import {
|
import {
|
||||||
approveCommissionWithdraw,
|
approveCommissionWithdraw,
|
||||||
@ -163,11 +154,19 @@ const tableRows = ref<WithdrawRow[]>([])
|
|||||||
const totalCount = ref(0)
|
const totalCount = ref(0)
|
||||||
const listLoading = ref(false)
|
const listLoading = ref(false)
|
||||||
const customerCode = ref('')
|
const customerCode = ref('')
|
||||||
const statusFilter = ref<number | undefined>(1)
|
const statusTab = ref('all')
|
||||||
const withdrawType = ref<number | undefined>(undefined)
|
const withdrawType = ref<number | undefined>(undefined)
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const pageSize = ref(50)
|
const pageSize = ref(50)
|
||||||
|
|
||||||
|
const statusTabs = computed(() => [
|
||||||
|
{ key: 'all', label: t('common.all') },
|
||||||
|
{ key: '1', label: t('marketing.commissionWithdraw.status.pending') },
|
||||||
|
{ key: '2', label: t('marketing.commissionWithdraw.status.approved') },
|
||||||
|
{ key: '3', label: t('marketing.commissionWithdraw.status.rejected') },
|
||||||
|
{ key: '4', label: t('marketing.commissionWithdraw.status.paid') },
|
||||||
|
])
|
||||||
|
|
||||||
const rejectVisible = ref(false)
|
const rejectVisible = ref(false)
|
||||||
const rejectLoading = ref(false)
|
const rejectLoading = ref(false)
|
||||||
const rejectTarget = ref<WithdrawRow | null>(null)
|
const rejectTarget = ref<WithdrawRow | null>(null)
|
||||||
@ -262,7 +261,7 @@ async function loadList() {
|
|||||||
try {
|
try {
|
||||||
const { items, total } = await getCommissionWithdrawList({
|
const { items, total } = await getCommissionWithdrawList({
|
||||||
customerCode: customerCode.value.trim() || undefined,
|
customerCode: customerCode.value.trim() || undefined,
|
||||||
status: statusFilter.value,
|
status: statusTab.value === 'all' ? undefined : Number(statusTab.value),
|
||||||
withdrawType: withdrawType.value,
|
withdrawType: withdrawType.value,
|
||||||
page: currentPage.value,
|
page: currentPage.value,
|
||||||
pageSize: pageSize.value,
|
pageSize: pageSize.value,
|
||||||
|
|||||||
@ -1,6 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="marketing-admin-container" :class="containerClass">
|
<div
|
||||||
<div class="marketing-admin-card" :class="{ 'is-form': variant === 'form' }">
|
class="marketing-admin-container"
|
||||||
|
:class="[
|
||||||
|
containerClass,
|
||||||
|
variant === 'form' || variant === 'cards' ? 'is-form-scroll' : '',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="marketing-admin-card"
|
||||||
|
:class="{
|
||||||
|
'is-form': variant === 'form',
|
||||||
|
'is-cards': variant === 'cards',
|
||||||
|
}"
|
||||||
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -15,7 +27,7 @@ const props = withDefaults(
|
|||||||
tip?: string
|
tip?: string
|
||||||
tipType?: 'info' | 'success' | 'warning' | 'error'
|
tipType?: 'info' | 'success' | 'warning' | 'error'
|
||||||
/** list = Handsontable 列表页;form = 配置/发券表单页 */
|
/** list = Handsontable 列表页;form = 配置/发券表单页 */
|
||||||
variant?: 'list' | 'form'
|
variant?: 'list' | 'form' | 'cards'
|
||||||
containerClass?: string
|
containerClass?: string
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
@ -37,6 +49,10 @@ provide(MARKETING_PAGE_TIP_KEY, toRef(props, 'tip'))
|
|||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: var(--app-page-bg);
|
background: var(--app-page-bg);
|
||||||
|
|
||||||
|
&.is-form-scroll {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.marketing-admin-card {
|
.marketing-admin-card {
|
||||||
@ -50,12 +66,26 @@ provide(MARKETING_PAGE_TIP_KEY, toRef(props, 'tip'))
|
|||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
|
||||||
&.is-form {
|
&.is-form {
|
||||||
overflow: auto;
|
flex: 0 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 760px;
|
||||||
|
overflow: visible;
|
||||||
padding: 20px 24px 24px;
|
padding: 20px 24px 24px;
|
||||||
|
|
||||||
> :deep(*) {
|
> :deep(*) {
|
||||||
max-width: 960px;
|
max-width: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.is-cards {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
|
overflow: visible;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="points-flow-panel">
|
<div class="points-flow-panel">
|
||||||
|
<div class="mkt-status-tabs">
|
||||||
|
<SlidingTabs v-model="changeTypeTab" :items="changeTypeTabs" @change="handleSearch" />
|
||||||
|
</div>
|
||||||
<div class="table-toolbar">
|
<div class="table-toolbar">
|
||||||
<div class="toolbar-left">
|
<div class="toolbar-left">
|
||||||
<div class="toolbar-filters">
|
<div class="toolbar-filters">
|
||||||
@ -11,19 +14,6 @@
|
|||||||
@press-enter="handleSearch"
|
@press-enter="handleSearch"
|
||||||
@clear="handleSearch"
|
@clear="handleSearch"
|
||||||
/>
|
/>
|
||||||
<a-select
|
|
||||||
v-model="changeType"
|
|
||||||
allow-clear
|
|
||||||
class="mkt-filter-status"
|
|
||||||
:style="{ width: '140px' }"
|
|
||||||
:placeholder="t('marketing.pointsFlow.changeTypeFilter')"
|
|
||||||
@change="handleSearch"
|
|
||||||
>
|
|
||||||
<a-option :value="1">{{ t('marketing.pointsFlow.changeType.earn') }}</a-option>
|
|
||||||
<a-option :value="2">{{ t('marketing.pointsFlow.changeType.consume') }}</a-option>
|
|
||||||
<a-option :value="3">{{ t('marketing.pointsFlow.changeType.adjust') }}</a-option>
|
|
||||||
<a-option :value="4">{{ t('marketing.pointsFlow.changeType.refund') }}</a-option>
|
|
||||||
</a-select>
|
|
||||||
<MarketingPageTipIcon />
|
<MarketingPageTipIcon />
|
||||||
<a-button class="app-btn-search" @click="handleSearch">
|
<a-button class="app-btn-search" @click="handleSearch">
|
||||||
{{ t('common.search') }}
|
{{ t('common.search') }}
|
||||||
@ -32,9 +22,6 @@
|
|||||||
<a-button type="primary" @click="openAdjustModal">
|
<a-button type="primary" @click="openAdjustModal">
|
||||||
{{ t('marketing.pointsFlow.adjust') }}
|
{{ t('marketing.pointsFlow.adjust') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button @click="openRuleModal">
|
|
||||||
{{ t('marketing.pointsRule.tab.rule') }}
|
|
||||||
</a-button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="toolbar-right">
|
<div class="toolbar-right">
|
||||||
<a-space :size="8">
|
<a-space :size="8">
|
||||||
@ -92,51 +79,20 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</ModalPro>
|
</ModalPro>
|
||||||
|
|
||||||
<ModalPro
|
|
||||||
v-model:visible="ruleVisible"
|
|
||||||
:title="t('marketing.pointsRule.tab.rule')"
|
|
||||||
:confirm-loading="ruleSaving"
|
|
||||||
width="640px"
|
|
||||||
unmount-on-close
|
|
||||||
@ok="handleRuleOk"
|
|
||||||
@cancel="handleRuleCancel"
|
|
||||||
>
|
|
||||||
<div class="flow-modal">
|
|
||||||
<div class="flow-modal__tip">
|
|
||||||
<icon-info-circle class="flow-modal__tip-icon" />
|
|
||||||
<p>{{ t('marketing.pointsRule.saveConfirmContent') }}</p>
|
|
||||||
</div>
|
|
||||||
<a-spin :loading="ruleLoading">
|
|
||||||
<FormPro
|
|
||||||
ref="ruleFormRef"
|
|
||||||
class="flow-modal__form"
|
|
||||||
form-layout="vertical"
|
|
||||||
:hide-colon="true"
|
|
||||||
:item-list="ruleItemList"
|
|
||||||
:row-gutter="16"
|
|
||||||
:default-span="12"
|
|
||||||
:label-col-props="{ span: 24 }"
|
|
||||||
:wrapper-col-props="{ span: 24 }"
|
|
||||||
/>
|
|
||||||
</a-spin>
|
|
||||||
</div>
|
|
||||||
</ModalPro>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { Message, Modal } from '@arco-design/web-vue'
|
import { Message } from '@arco-design/web-vue'
|
||||||
import HandsontablePro from '@/components/HandsontablePro/index.vue'
|
import HandsontablePro from '@/components/HandsontablePro/index.vue'
|
||||||
import PaginationPro from '@/components/PaginationPro/index.vue'
|
import PaginationPro from '@/components/PaginationPro/index.vue'
|
||||||
import ModalPro from '@/components/ModalPro/index.vue'
|
import ModalPro from '@/components/ModalPro/index.vue'
|
||||||
import FormPro from '@/components/FormPro/index.vue'
|
import FormPro from '@/components/FormPro/index.vue'
|
||||||
|
import SlidingTabs from '@/components/SlidingTabs/index.vue'
|
||||||
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
|
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
|
||||||
import { isEnableStatusEnabled, toEnableStatus } from '@/utils/enableStatus'
|
|
||||||
import { adjustPoints, getPointsFlowList } from '@/api/marketing/pointsFlow'
|
import { adjustPoints, getPointsFlowList } from '@/api/marketing/pointsFlow'
|
||||||
import { getPointsRule, updatePointsRule } from '@/api/marketing/pointsRule'
|
|
||||||
import MarketingPageTipIcon from './MarketingPageTipIcon.vue'
|
import MarketingPageTipIcon from './MarketingPageTipIcon.vue'
|
||||||
|
|
||||||
defineOptions({ name: 'PointsFlowPanel' })
|
defineOptions({ name: 'PointsFlowPanel' })
|
||||||
@ -167,19 +123,22 @@ const tableRows = ref<PointsFlowRow[]>([])
|
|||||||
const totalCount = ref(0)
|
const totalCount = ref(0)
|
||||||
const listLoading = ref(false)
|
const listLoading = ref(false)
|
||||||
const customerCode = ref('')
|
const customerCode = ref('')
|
||||||
const changeType = ref<number | undefined>(undefined)
|
const changeTypeTab = ref('all')
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
|
|
||||||
|
const changeTypeTabs = computed(() => [
|
||||||
|
{ key: 'all', label: t('common.all') },
|
||||||
|
{ key: '1', label: t('marketing.pointsFlow.changeType.earn') },
|
||||||
|
{ key: '2', label: t('marketing.pointsFlow.changeType.consume') },
|
||||||
|
{ key: '3', label: t('marketing.pointsFlow.changeType.adjust') },
|
||||||
|
{ key: '4', label: t('marketing.pointsFlow.changeType.refund') },
|
||||||
|
])
|
||||||
const pageSize = ref(50)
|
const pageSize = ref(50)
|
||||||
|
|
||||||
const adjustVisible = ref(false)
|
const adjustVisible = ref(false)
|
||||||
const adjustLoading = ref(false)
|
const adjustLoading = ref(false)
|
||||||
const adjustFormRef = ref<FormExpose>()
|
const adjustFormRef = ref<FormExpose>()
|
||||||
|
|
||||||
const ruleVisible = ref(false)
|
|
||||||
const ruleLoading = ref(false)
|
|
||||||
const ruleSaving = ref(false)
|
|
||||||
const ruleFormRef = ref<FormExpose>()
|
|
||||||
|
|
||||||
const adjustItemList = computed(() => [
|
const adjustItemList = computed(() => [
|
||||||
{
|
{
|
||||||
field: 'customerCode',
|
field: 'customerCode',
|
||||||
@ -229,60 +188,6 @@ const adjustItemList = computed(() => [
|
|||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
||||||
const ruleItemList = computed(() => [
|
|
||||||
{
|
|
||||||
field: 'ruleName',
|
|
||||||
label: t('marketing.pointsRule.form.ruleName'),
|
|
||||||
comp: 'a-input',
|
|
||||||
required: true,
|
|
||||||
rules: [{ required: true, message: t('marketing.pointsRule.form.ruleName') }],
|
|
||||||
compProps: {
|
|
||||||
placeholder: t('marketing.pointsRule.form.ruleName'),
|
|
||||||
maxLength: 64,
|
|
||||||
allowClear: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'status',
|
|
||||||
label: t('marketing.pointsRule.form.status'),
|
|
||||||
comp: 'a-radio-group',
|
|
||||||
required: true,
|
|
||||||
rules: [{ required: true, message: t('marketing.pointsRule.form.status') }],
|
|
||||||
compProps: {
|
|
||||||
type: 'button',
|
|
||||||
options: [
|
|
||||||
{ label: t('common.enabled'), value: 2 },
|
|
||||||
{ label: t('common.disabled'), value: 1 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'pointsPerYuan',
|
|
||||||
label: t('marketing.pointsRule.form.pointsPerYuan'),
|
|
||||||
comp: 'a-input-number',
|
|
||||||
required: true,
|
|
||||||
tooltip: t('marketing.pointsRule.form.pointsPerYuanExtra'),
|
|
||||||
rules: [{ required: true, message: t('marketing.pointsRule.form.pointsPerYuan') }],
|
|
||||||
compProps: { min: 0, precision: 4, style: 'width: 100%', placeholder: '1' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'firstOrderMultiplier',
|
|
||||||
label: t('marketing.pointsRule.form.firstOrderMultiplier'),
|
|
||||||
comp: 'a-input-number',
|
|
||||||
required: true,
|
|
||||||
rules: [{ required: true, message: t('marketing.pointsRule.form.firstOrderMultiplier') }],
|
|
||||||
compProps: { min: 1, max: 10, step: 0.1, precision: 2, style: 'width: 100%', placeholder: '1' },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
field: 'maxPointsPerOrder',
|
|
||||||
label: t('marketing.pointsRule.form.maxPointsPerOrder'),
|
|
||||||
comp: 'a-input-number',
|
|
||||||
required: true,
|
|
||||||
rules: [{ required: true, message: t('marketing.pointsRule.form.maxPointsPerOrder') }],
|
|
||||||
compProps: { min: 0, precision: 0, style: 'width: 100%', placeholder: '10000' },
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
const hotColumns = computed(() => [
|
const hotColumns = computed(() => [
|
||||||
{ key: 'customerCode', label: t('marketing.pointsFlow.columns.customerCode'), width: 110 },
|
{ key: 'customerCode', label: t('marketing.pointsFlow.columns.customerCode'), width: 110 },
|
||||||
{ key: 'customerName', label: t('marketing.pointsFlow.columns.customerName'), width: 120 },
|
{ key: 'customerName', label: t('marketing.pointsFlow.columns.customerName'), width: 120 },
|
||||||
@ -308,24 +213,12 @@ function mapRow(item: Record<string, unknown>): PointsFlowRow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapRule(item: Record<string, unknown>) {
|
|
||||||
const status = item.status ?? item.Status ?? 2
|
|
||||||
return {
|
|
||||||
id: (item.id ?? item.Id) as number | undefined,
|
|
||||||
ruleName: String(item.ruleName ?? item.RuleName ?? ''),
|
|
||||||
pointsPerYuan: Number(item.pointsPerYuan ?? item.PointsPerYuan ?? 1),
|
|
||||||
firstOrderMultiplier: Number(item.firstOrderMultiplier ?? item.FirstOrderMultiplier ?? 1),
|
|
||||||
maxPointsPerOrder: Number(item.maxPointsPerOrder ?? item.MaxPointsPerOrder ?? 10000),
|
|
||||||
status: toEnableStatus(isEnableStatusEnabled(status)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadList() {
|
async function loadList() {
|
||||||
listLoading.value = true
|
listLoading.value = true
|
||||||
try {
|
try {
|
||||||
const { items, total } = await getPointsFlowList({
|
const { items, total } = await getPointsFlowList({
|
||||||
customerCode: customerCode.value.trim() || undefined,
|
customerCode: customerCode.value.trim() || undefined,
|
||||||
changeType: changeType.value,
|
changeType: changeTypeTab.value === 'all' ? undefined : Number(changeTypeTab.value),
|
||||||
page: currentPage.value,
|
page: currentPage.value,
|
||||||
pageSize: pageSize.value,
|
pageSize: pageSize.value,
|
||||||
})
|
})
|
||||||
@ -395,79 +288,9 @@ async function handleAdjustOk() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function openRuleModal() {
|
|
||||||
ruleVisible.value = true
|
|
||||||
ruleLoading.value = true
|
|
||||||
await nextTick()
|
|
||||||
try {
|
|
||||||
const data = await getPointsRule()
|
|
||||||
await nextTick()
|
|
||||||
if (data && typeof data === 'object') {
|
|
||||||
ruleFormRef.value?.setValues(mapRule(data as Record<string, unknown>))
|
|
||||||
} else {
|
|
||||||
ruleFormRef.value?.setValues({
|
|
||||||
id: undefined,
|
|
||||||
ruleName: '',
|
|
||||||
pointsPerYuan: 1,
|
|
||||||
firstOrderMultiplier: 1,
|
|
||||||
maxPointsPerOrder: 10000,
|
|
||||||
status: 2,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
Message.error(e?.message || t('common.loadFailed'))
|
|
||||||
} finally {
|
|
||||||
ruleLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleRuleCancel() {
|
|
||||||
ruleVisible.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleRuleOk() {
|
|
||||||
try {
|
|
||||||
await ruleFormRef.value?.validate()
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
const values = ruleFormRef.value?.getValues() || {}
|
|
||||||
const ruleName = String(values.ruleName || '').trim()
|
|
||||||
if (!ruleName) {
|
|
||||||
Message.warning(t('marketing.pointsRule.form.ruleName'))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
Modal.confirm({
|
|
||||||
title: t('marketing.pointsRule.saveConfirmTitle'),
|
|
||||||
content: t('marketing.pointsRule.saveConfirmContent'),
|
|
||||||
onOk: async () => {
|
|
||||||
ruleSaving.value = true
|
|
||||||
try {
|
|
||||||
await updatePointsRule({
|
|
||||||
id: values.id,
|
|
||||||
ruleName,
|
|
||||||
pointsPerYuan: Number(values.pointsPerYuan ?? 1),
|
|
||||||
firstOrderMultiplier: Number(values.firstOrderMultiplier ?? 1),
|
|
||||||
maxPointsPerOrder: Number(values.maxPointsPerOrder ?? 10000),
|
|
||||||
status: Number(values.status ?? 2),
|
|
||||||
})
|
|
||||||
Message.success(t('common.saveSuccess'))
|
|
||||||
ruleVisible.value = false
|
|
||||||
} catch (e: any) {
|
|
||||||
Message.error(e?.message || t('common.saveFailed'))
|
|
||||||
throw e
|
|
||||||
} finally {
|
|
||||||
ruleSaving.value = false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => loadList())
|
onMounted(() => loadList())
|
||||||
|
|
||||||
defineExpose({ loadList, openAdjustModal, openRuleModal })
|
defineExpose({ loadList, openAdjustModal })
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="less">
|
<style scoped lang="less">
|
||||||
@ -534,15 +357,5 @@ defineExpose({ loadList, openAdjustModal, openRuleModal })
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.arco-radio-group-button) {
|
|
||||||
display: flex;
|
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
.arco-radio-button {
|
|
||||||
flex: 1;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<MarketingPageShell tip-type="warning" :tip="t('marketing.couponGrant.pageTip')">
|
<MarketingPageShell tip-type="warning" :tip="t('marketing.couponGrant.pageTip')">
|
||||||
|
<div class="mkt-status-tabs">
|
||||||
|
<SlidingTabs v-model="recordStatusTab" :items="recordStatusTabs" @change="handleRecordSearch" />
|
||||||
|
</div>
|
||||||
<div class="table-toolbar">
|
<div class="table-toolbar">
|
||||||
<div class="toolbar-left">
|
<div class="toolbar-left">
|
||||||
<div class="toolbar-filters">
|
<div class="toolbar-filters">
|
||||||
@ -21,19 +24,6 @@
|
|||||||
@press-enter="handleRecordSearch"
|
@press-enter="handleRecordSearch"
|
||||||
@clear="handleRecordSearch"
|
@clear="handleRecordSearch"
|
||||||
/>
|
/>
|
||||||
<a-select
|
|
||||||
v-model="recordStatus"
|
|
||||||
allow-clear
|
|
||||||
class="mkt-filter-status"
|
|
||||||
:style="{ width: '140px' }"
|
|
||||||
:placeholder="t('marketing.couponGrant.records.statusFilter')"
|
|
||||||
@change="handleRecordSearch"
|
|
||||||
>
|
|
||||||
<a-option :value="1">{{ t('marketing.couponGrant.instanceStatus.available') }}</a-option>
|
|
||||||
<a-option :value="2">{{ t('marketing.couponGrant.instanceStatus.locked') }}</a-option>
|
|
||||||
<a-option :value="3">{{ t('marketing.couponGrant.instanceStatus.used') }}</a-option>
|
|
||||||
<a-option :value="4">{{ t('marketing.couponGrant.instanceStatus.expired') }}</a-option>
|
|
||||||
</a-select>
|
|
||||||
<a-input
|
<a-input
|
||||||
v-model="recordKeyword"
|
v-model="recordKeyword"
|
||||||
allow-clear
|
allow-clear
|
||||||
@ -146,6 +136,7 @@ import FormPro from '@/components/FormPro/index.vue'
|
|||||||
import HandsontablePro from '@/components/HandsontablePro/index.vue'
|
import HandsontablePro from '@/components/HandsontablePro/index.vue'
|
||||||
import PaginationPro from '@/components/PaginationPro/index.vue'
|
import PaginationPro from '@/components/PaginationPro/index.vue'
|
||||||
import ModalPro from '@/components/ModalPro/index.vue'
|
import ModalPro from '@/components/ModalPro/index.vue'
|
||||||
|
import SlidingTabs from '@/components/SlidingTabs/index.vue'
|
||||||
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
|
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
|
||||||
import { getCouponTemplateList } from '@/api/marketing/couponTemplate'
|
import { getCouponTemplateList } from '@/api/marketing/couponTemplate'
|
||||||
import { getCouponInstanceList, grantCoupons, previewCouponGrant } from '@/api/marketing/couponGrant'
|
import { getCouponInstanceList, grantCoupons, previewCouponGrant } from '@/api/marketing/couponGrant'
|
||||||
@ -190,11 +181,20 @@ const recordTotal = ref(0)
|
|||||||
const recordLoading = ref(false)
|
const recordLoading = ref(false)
|
||||||
const recordTemplateId = ref<number | undefined>(undefined)
|
const recordTemplateId = ref<number | undefined>(undefined)
|
||||||
const recordCustomerCode = ref('')
|
const recordCustomerCode = ref('')
|
||||||
const recordStatus = ref<number | undefined>(undefined)
|
const recordStatusTab = ref('all')
|
||||||
const recordKeyword = ref('')
|
const recordKeyword = ref('')
|
||||||
const recordPage = ref(1)
|
const recordPage = ref(1)
|
||||||
const recordPageSize = ref(30)
|
const recordPageSize = ref(30)
|
||||||
|
|
||||||
|
const recordStatusTabs = computed(() => [
|
||||||
|
{ key: 'all', label: t('common.all') },
|
||||||
|
{ key: '5', label: t('marketing.couponGrant.instanceStatus.pendingUnlock') },
|
||||||
|
{ key: '1', label: t('marketing.couponGrant.instanceStatus.available') },
|
||||||
|
{ key: '2', label: t('marketing.couponGrant.instanceStatus.locked') },
|
||||||
|
{ key: '3', label: t('marketing.couponGrant.instanceStatus.used') },
|
||||||
|
{ key: '4', label: t('marketing.couponGrant.instanceStatus.expired') },
|
||||||
|
])
|
||||||
|
|
||||||
const itemList = computed(() => [
|
const itemList = computed(() => [
|
||||||
{
|
{
|
||||||
field: 'templateId',
|
field: 'templateId',
|
||||||
@ -336,7 +336,7 @@ async function loadRecords() {
|
|||||||
const { items, total } = await getCouponInstanceList({
|
const { items, total } = await getCouponInstanceList({
|
||||||
templateId: recordTemplateId.value,
|
templateId: recordTemplateId.value,
|
||||||
customerCode: recordCustomerCode.value.trim() || undefined,
|
customerCode: recordCustomerCode.value.trim() || undefined,
|
||||||
status: recordStatus.value,
|
status: recordStatusTab.value === 'all' ? undefined : Number(recordStatusTab.value),
|
||||||
keyword: recordKeyword.value.trim() || undefined,
|
keyword: recordKeyword.value.trim() || undefined,
|
||||||
page: recordPage.value,
|
page: recordPage.value,
|
||||||
pageSize: recordPageSize.value,
|
pageSize: recordPageSize.value,
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<MarketingPageShell :tip="t('marketing.couponTemplate.pageTip')">
|
<MarketingPageShell :tip="t('marketing.couponTemplate.pageTip')">
|
||||||
|
<div class="mkt-status-tabs">
|
||||||
|
<SlidingTabs v-model="statusTab" :items="statusTabs" @change="handleSearch" />
|
||||||
|
</div>
|
||||||
<div class="table-toolbar">
|
<div class="table-toolbar">
|
||||||
<div class="toolbar-left">
|
<div class="toolbar-left">
|
||||||
<div class="toolbar-filters">
|
<div class="toolbar-filters">
|
||||||
@ -11,18 +14,6 @@
|
|||||||
@press-enter="handleSearch"
|
@press-enter="handleSearch"
|
||||||
@clear="handleSearch"
|
@clear="handleSearch"
|
||||||
/>
|
/>
|
||||||
<a-select
|
|
||||||
v-model="statusFilter"
|
|
||||||
allow-clear
|
|
||||||
class="mkt-filter-status"
|
|
||||||
:style="{ width: '140px' }"
|
|
||||||
:placeholder="t('marketing.couponTemplate.statusFilter')"
|
|
||||||
@change="handleSearch"
|
|
||||||
>
|
|
||||||
<a-option :value="0">{{ t('marketing.couponTemplate.status.draft') }}</a-option>
|
|
||||||
<a-option :value="1">{{ t('marketing.couponTemplate.status.active') }}</a-option>
|
|
||||||
<a-option :value="2">{{ t('marketing.couponTemplate.status.ended') }}</a-option>
|
|
||||||
</a-select>
|
|
||||||
<MarketingPageTipIcon />
|
<MarketingPageTipIcon />
|
||||||
<a-button class="app-btn-search" @click="handleSearch">
|
<a-button class="app-btn-search" @click="handleSearch">
|
||||||
{{ t('common.search') }}
|
{{ t('common.search') }}
|
||||||
@ -103,6 +94,7 @@ import HandsontablePro from '@/components/HandsontablePro/index.vue'
|
|||||||
import PaginationPro from '@/components/PaginationPro/index.vue'
|
import PaginationPro from '@/components/PaginationPro/index.vue'
|
||||||
import ModalPro from '@/components/ModalPro/index.vue'
|
import ModalPro from '@/components/ModalPro/index.vue'
|
||||||
import FormPro from '@/components/FormPro/index.vue'
|
import FormPro from '@/components/FormPro/index.vue'
|
||||||
|
import SlidingTabs from '@/components/SlidingTabs/index.vue'
|
||||||
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
|
import { useTableContainerHeight } from '@/composables/useTableContainerHeight'
|
||||||
import {
|
import {
|
||||||
createCouponTemplate,
|
createCouponTemplate,
|
||||||
@ -141,6 +133,8 @@ interface TemplateRow {
|
|||||||
validStartTime: string | null
|
validStartTime: string | null
|
||||||
validEndTime: string | null
|
validEndTime: string | null
|
||||||
validDaysAfterReceive: number | null
|
validDaysAfterReceive: number | null
|
||||||
|
applyScopeJson: string
|
||||||
|
applyCountryCodes: string
|
||||||
remark: string
|
remark: string
|
||||||
status: number
|
status: number
|
||||||
statusName: string
|
statusName: string
|
||||||
@ -156,10 +150,17 @@ const tableRows = ref<TemplateRow[]>([])
|
|||||||
const totalCount = ref(0)
|
const totalCount = ref(0)
|
||||||
const listLoading = ref(false)
|
const listLoading = ref(false)
|
||||||
const keyword = ref('')
|
const keyword = ref('')
|
||||||
const statusFilter = ref<number | undefined>(undefined)
|
const statusTab = ref('all')
|
||||||
const currentPage = ref(1)
|
const currentPage = ref(1)
|
||||||
const pageSize = ref(50)
|
const pageSize = ref(50)
|
||||||
|
|
||||||
|
const statusTabs = computed(() => [
|
||||||
|
{ key: 'all', label: t('common.all') },
|
||||||
|
{ key: '0', label: t('marketing.couponTemplate.status.draft') },
|
||||||
|
{ key: '1', label: t('marketing.couponTemplate.status.active') },
|
||||||
|
{ key: '2', label: t('marketing.couponTemplate.status.ended') },
|
||||||
|
])
|
||||||
|
|
||||||
const modalVisible = ref(false)
|
const modalVisible = ref(false)
|
||||||
const modalLoading = ref(false)
|
const modalLoading = ref(false)
|
||||||
const editingId = ref<number | undefined>(undefined)
|
const editingId = ref<number | undefined>(undefined)
|
||||||
@ -176,6 +177,9 @@ const grantChannelOptions = computed(() => [
|
|||||||
{ label: t('marketing.couponTemplate.grantChannel.manual'), value: 'manual' },
|
{ label: t('marketing.couponTemplate.grantChannel.manual'), value: 'manual' },
|
||||||
{ label: t('marketing.couponTemplate.grantChannel.newcomer'), value: 'newcomer' },
|
{ label: t('marketing.couponTemplate.grantChannel.newcomer'), value: 'newcomer' },
|
||||||
{ label: t('marketing.couponTemplate.grantChannel.selfReceive'), value: 'selfReceive' },
|
{ label: t('marketing.couponTemplate.grantChannel.selfReceive'), value: 'selfReceive' },
|
||||||
|
{ label: t('marketing.couponTemplate.grantChannel.referral'), value: 'referral' },
|
||||||
|
{ label: t('marketing.couponTemplate.grantChannel.route'), value: 'route' },
|
||||||
|
{ label: t('marketing.couponTemplate.grantChannel.festival'), value: 'festival' },
|
||||||
])
|
])
|
||||||
|
|
||||||
const itemList = computed(() => [
|
const itemList = computed(() => [
|
||||||
@ -287,6 +291,7 @@ const itemList = computed(() => [
|
|||||||
options: [
|
options: [
|
||||||
{ label: t('marketing.couponTemplate.validType.fixed'), value: 1 },
|
{ label: t('marketing.couponTemplate.validType.fixed'), value: 1 },
|
||||||
{ label: t('marketing.couponTemplate.validType.afterReceive'), value: 2 },
|
{ label: t('marketing.couponTemplate.validType.afterReceive'), value: 2 },
|
||||||
|
{ label: t('marketing.couponTemplate.validType.afterUnlock'), value: 3 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -319,10 +324,21 @@ const itemList = computed(() => [
|
|||||||
label: t('marketing.couponTemplate.form.validDaysAfterReceive'),
|
label: t('marketing.couponTemplate.form.validDaysAfterReceive'),
|
||||||
comp: 'a-input-number',
|
comp: 'a-input-number',
|
||||||
required: true,
|
required: true,
|
||||||
ifShow: (model: Record<string, unknown>) => Number(model.validType) === 2,
|
ifShow: (model: Record<string, unknown>) => Number(model.validType) === 2 || Number(model.validType) === 3,
|
||||||
rules: [{ required: true, message: t('marketing.couponTemplate.form.validDaysAfterReceive') }],
|
rules: [{ required: true, message: t('marketing.couponTemplate.form.validDaysAfterReceive') }],
|
||||||
compProps: { min: 1, precision: 0, style: 'width: 100%', placeholder: '30' },
|
compProps: { min: 1, precision: 0, style: 'width: 100%', placeholder: '30' },
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
field: 'applyCountryCodes',
|
||||||
|
label: t('marketing.couponTemplate.form.applyCountries'),
|
||||||
|
comp: 'a-input',
|
||||||
|
tooltip: t('marketing.couponTemplate.form.applyCountriesExtra'),
|
||||||
|
span: 24,
|
||||||
|
compProps: {
|
||||||
|
placeholder: 'MY,SG',
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
field: 'remark',
|
field: 'remark',
|
||||||
label: t('marketing.couponTemplate.form.remark'),
|
label: t('marketing.couponTemplate.form.remark'),
|
||||||
@ -349,6 +365,26 @@ const hotColumns = computed(() => [
|
|||||||
{ key: 'statusName', label: t('marketing.couponTemplate.columns.status'), width: 80 },
|
{ key: 'statusName', label: t('marketing.couponTemplate.columns.status'), width: 80 },
|
||||||
])
|
])
|
||||||
|
|
||||||
|
function parseCountryCodes(json: unknown): string {
|
||||||
|
if (!json) return ''
|
||||||
|
try {
|
||||||
|
const parsed = typeof json === 'string' ? JSON.parse(json) : json
|
||||||
|
const codes = Array.isArray(parsed?.countryCodes) ? parsed.countryCodes : []
|
||||||
|
return codes.filter(Boolean).join(',')
|
||||||
|
} catch {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toApplyScopeJson(raw: unknown): string | undefined {
|
||||||
|
const codes = String(raw || '')
|
||||||
|
.split(/[,,\s]+/)
|
||||||
|
.map((x) => x.trim().toUpperCase())
|
||||||
|
.filter(Boolean)
|
||||||
|
if (!codes.length) return undefined
|
||||||
|
return JSON.stringify({ countryCodes: codes })
|
||||||
|
}
|
||||||
|
|
||||||
function formatDateTime(value: unknown): string | null {
|
function formatDateTime(value: unknown): string | null {
|
||||||
if (value == null || value === '') return null
|
if (value == null || value === '') return null
|
||||||
return String(value).replace('T', ' ').slice(0, 19)
|
return String(value).replace('T', ' ').slice(0, 19)
|
||||||
@ -404,6 +440,8 @@ function mapRow(item: Record<string, unknown>): TemplateRow {
|
|||||||
item.validDaysAfterReceive != null || item.ValidDaysAfterReceive != null
|
item.validDaysAfterReceive != null || item.ValidDaysAfterReceive != null
|
||||||
? Number(item.validDaysAfterReceive ?? item.ValidDaysAfterReceive)
|
? Number(item.validDaysAfterReceive ?? item.ValidDaysAfterReceive)
|
||||||
: null,
|
: null,
|
||||||
|
applyScopeJson: String(item.applyScopeJson ?? item.ApplyScopeJson ?? ''),
|
||||||
|
applyCountryCodes: parseCountryCodes(item.applyScopeJson ?? item.ApplyScopeJson),
|
||||||
remark: String(item.remark ?? item.Remark ?? ''),
|
remark: String(item.remark ?? item.Remark ?? ''),
|
||||||
status: Number(item.status ?? item.Status ?? 0),
|
status: Number(item.status ?? item.Status ?? 0),
|
||||||
statusName: String(item.statusName ?? item.StatusName ?? ''),
|
statusName: String(item.statusName ?? item.StatusName ?? ''),
|
||||||
@ -422,7 +460,7 @@ async function loadList() {
|
|||||||
try {
|
try {
|
||||||
const { items, total } = await getCouponTemplateList({
|
const { items, total } = await getCouponTemplateList({
|
||||||
keyword: keyword.value.trim() || undefined,
|
keyword: keyword.value.trim() || undefined,
|
||||||
status: statusFilter.value,
|
status: statusTab.value === 'all' ? undefined : Number(statusTab.value),
|
||||||
page: currentPage.value,
|
page: currentPage.value,
|
||||||
pageSize: pageSize.value,
|
pageSize: pageSize.value,
|
||||||
})
|
})
|
||||||
@ -462,6 +500,7 @@ async function openModal(row?: TemplateRow) {
|
|||||||
validStartTime: row.validStartTime || undefined,
|
validStartTime: row.validStartTime || undefined,
|
||||||
validEndTime: row.validEndTime || undefined,
|
validEndTime: row.validEndTime || undefined,
|
||||||
validDaysAfterReceive: row.validDaysAfterReceive ?? 30,
|
validDaysAfterReceive: row.validDaysAfterReceive ?? 30,
|
||||||
|
applyCountryCodes: row.applyCountryCodes || '',
|
||||||
remark: row.remark || '',
|
remark: row.remark || '',
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
@ -479,6 +518,7 @@ async function openModal(row?: TemplateRow) {
|
|||||||
validStartTime: undefined,
|
validStartTime: undefined,
|
||||||
validEndTime: undefined,
|
validEndTime: undefined,
|
||||||
validDaysAfterReceive: 30,
|
validDaysAfterReceive: 30,
|
||||||
|
applyCountryCodes: '',
|
||||||
remark: '',
|
remark: '',
|
||||||
}
|
}
|
||||||
formRef.value?.setValues(values)
|
formRef.value?.setValues(values)
|
||||||
@ -552,7 +592,8 @@ async function handleModalOk() {
|
|||||||
validType,
|
validType,
|
||||||
validStartTime: validType === 1 ? values.validStartTime || undefined : undefined,
|
validStartTime: validType === 1 ? values.validStartTime || undefined : undefined,
|
||||||
validEndTime: validType === 1 ? values.validEndTime || undefined : undefined,
|
validEndTime: validType === 1 ? values.validEndTime || undefined : undefined,
|
||||||
validDaysAfterReceive: validType === 2 ? Number(values.validDaysAfterReceive ?? 30) : undefined,
|
validDaysAfterReceive: validType === 2 || validType === 3 ? Number(values.validDaysAfterReceive ?? 30) : undefined,
|
||||||
|
applyScopeJson: toApplyScopeJson(values.applyCountryCodes),
|
||||||
remark: String(values.remark || '').trim() || undefined,
|
remark: String(values.remark || '').trim() || undefined,
|
||||||
grantChannel: String(values.grantChannel || 'manual').trim() || 'manual',
|
grantChannel: String(values.grantChannel || 'manual').trim() || 'manual',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -218,7 +218,7 @@
|
|||||||
<a-input-number
|
<a-input-number
|
||||||
v-model="formState.extraDiscountRate"
|
v-model="formState.extraDiscountRate"
|
||||||
:min="0"
|
:min="0"
|
||||||
:max="0.99"
|
:max="0.05"
|
||||||
:step="0.01"
|
:step="0.01"
|
||||||
:precision="4"
|
:precision="4"
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
@ -226,6 +226,15 @@
|
|||||||
/>
|
/>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-col>
|
</a-col>
|
||||||
|
<a-col :span="12">
|
||||||
|
<a-form-item
|
||||||
|
:label="t('marketing.memberLevel.form.canDowngrade')"
|
||||||
|
field="canDowngrade"
|
||||||
|
:tooltip="t('marketing.memberLevel.form.canDowngradeExtra')"
|
||||||
|
>
|
||||||
|
<a-switch v-model="formState.canDowngrade" />
|
||||||
|
</a-form-item>
|
||||||
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@ -305,6 +314,7 @@ interface MemberLevelRow {
|
|||||||
levelName: string
|
levelName: string
|
||||||
minPoints: number
|
minPoints: number
|
||||||
extraDiscountRate: number
|
extraDiscountRate: number
|
||||||
|
canDowngrade: boolean
|
||||||
sort: number
|
sort: number
|
||||||
iconUrl: string
|
iconUrl: string
|
||||||
status: boolean
|
status: boolean
|
||||||
@ -318,6 +328,7 @@ interface MemberLevelFormState {
|
|||||||
levelName: string
|
levelName: string
|
||||||
minPoints: number
|
minPoints: number
|
||||||
extraDiscountRate: number
|
extraDiscountRate: number
|
||||||
|
canDowngrade: boolean
|
||||||
sort: number
|
sort: number
|
||||||
iconUrl: string
|
iconUrl: string
|
||||||
status: number
|
status: number
|
||||||
@ -352,10 +363,13 @@ const previewInitial = computed(() => {
|
|||||||
return code ? code.slice(0, 1).toUpperCase() : 'M'
|
return code ? code.slice(0, 1).toUpperCase() : 'M'
|
||||||
})
|
})
|
||||||
|
|
||||||
const previewDiscountText = computed(() => {
|
const previewDiscountText = computed(() => formatFreightOff(formState.extraDiscountRate))
|
||||||
const rate = Number(formState.extraDiscountRate || 0)
|
|
||||||
return `${(rate * 100).toFixed(2)}%`
|
function formatFreightOff(rate: unknown) {
|
||||||
})
|
const n = Number(rate || 0)
|
||||||
|
if (!n) return t('marketing.memberLevel.discount.original')
|
||||||
|
return t('marketing.memberLevel.discount.fold', { fold: Math.round((1 - n) * 100) })
|
||||||
|
}
|
||||||
|
|
||||||
function createEmptyForm(): MemberLevelFormState {
|
function createEmptyForm(): MemberLevelFormState {
|
||||||
return {
|
return {
|
||||||
@ -363,6 +377,7 @@ function createEmptyForm(): MemberLevelFormState {
|
|||||||
levelName: '',
|
levelName: '',
|
||||||
minPoints: 0,
|
minPoints: 0,
|
||||||
extraDiscountRate: 0,
|
extraDiscountRate: 0,
|
||||||
|
canDowngrade: false,
|
||||||
sort: 0,
|
sort: 0,
|
||||||
iconUrl: '',
|
iconUrl: '',
|
||||||
status: 2,
|
status: 2,
|
||||||
@ -387,6 +402,7 @@ function mapRow(item: any): MemberLevelRow {
|
|||||||
levelName: item.levelName ?? item.LevelName ?? '',
|
levelName: item.levelName ?? item.LevelName ?? '',
|
||||||
minPoints: Number(item.minPoints ?? item.MinPoints ?? 0),
|
minPoints: Number(item.minPoints ?? item.MinPoints ?? 0),
|
||||||
extraDiscountRate: Number(item.extraDiscountRate ?? item.ExtraDiscountRate ?? 0),
|
extraDiscountRate: Number(item.extraDiscountRate ?? item.ExtraDiscountRate ?? 0),
|
||||||
|
canDowngrade: Boolean(item.canDowngrade ?? item.CanDowngrade),
|
||||||
sort: Number(item.sort ?? item.Sort ?? 0),
|
sort: Number(item.sort ?? item.Sort ?? 0),
|
||||||
iconUrl: String(item.iconUrl ?? item.IconUrl ?? ''),
|
iconUrl: String(item.iconUrl ?? item.IconUrl ?? ''),
|
||||||
status: isEnableStatusEnabled(status),
|
status: isEnableStatusEnabled(status),
|
||||||
@ -404,7 +420,13 @@ const hotColumns = computed(() => [
|
|||||||
key: 'extraDiscountRate',
|
key: 'extraDiscountRate',
|
||||||
label: t('marketing.memberLevel.form.extraDiscountRate'),
|
label: t('marketing.memberLevel.form.extraDiscountRate'),
|
||||||
width: 100,
|
width: 100,
|
||||||
formatter: (v: number) => `${(Number(v) * 100).toFixed(2)}%`,
|
formatter: (v: number) => formatFreightOff(v),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'canDowngrade',
|
||||||
|
label: t('marketing.memberLevel.form.canDowngrade'),
|
||||||
|
width: 90,
|
||||||
|
formatter: (v: boolean) => (v ? t('common.enabled') : t('common.disabled')),
|
||||||
},
|
},
|
||||||
{ key: 'sort', label: t('marketing.memberLevel.form.sort'), width: 70 },
|
{ key: 'sort', label: t('marketing.memberLevel.form.sort'), width: 70 },
|
||||||
{ key: 'statusName', label: t('marketing.memberLevel.form.status'), width: 80 },
|
{ key: 'statusName', label: t('marketing.memberLevel.form.status'), width: 80 },
|
||||||
@ -450,6 +472,7 @@ function openDrawer(row?: MemberLevelRow) {
|
|||||||
levelName: row.levelName,
|
levelName: row.levelName,
|
||||||
minPoints: row.minPoints,
|
minPoints: row.minPoints,
|
||||||
extraDiscountRate: row.extraDiscountRate,
|
extraDiscountRate: row.extraDiscountRate,
|
||||||
|
canDowngrade: row.canDowngrade,
|
||||||
sort: row.sort,
|
sort: row.sort,
|
||||||
iconUrl: row.iconUrl || '',
|
iconUrl: row.iconUrl || '',
|
||||||
status: toEnableStatus(row.status),
|
status: toEnableStatus(row.status),
|
||||||
@ -507,6 +530,7 @@ async function handleDrawerOk() {
|
|||||||
levelName,
|
levelName,
|
||||||
minPoints: Number(formState.minPoints ?? 0),
|
minPoints: Number(formState.minPoints ?? 0),
|
||||||
extraDiscountRate: Number(formState.extraDiscountRate ?? 0),
|
extraDiscountRate: Number(formState.extraDiscountRate ?? 0),
|
||||||
|
canDowngrade: Boolean(formState.canDowngrade),
|
||||||
sort: Number(formState.sort ?? 0),
|
sort: Number(formState.sort ?? 0),
|
||||||
iconUrl: String(formState.iconUrl || '').trim() || undefined,
|
iconUrl: String(formState.iconUrl || '').trim() || undefined,
|
||||||
status: Number(formState.status ?? 2),
|
status: Number(formState.status ?? 2),
|
||||||
|
|||||||
@ -1,16 +1,604 @@
|
|||||||
<template>
|
<template>
|
||||||
<MarketingPageShell :tip="t('marketing.pointsFlow.pageTip')">
|
<MarketingPageShell variant="cards" :tip="t('marketing.pointsRule.pageTip')">
|
||||||
<PointsFlowPanel />
|
<div class="points-rule-page">
|
||||||
|
<section class="rule-section">
|
||||||
|
<h2 class="rule-section__title">{{ t('marketing.pointsRule.sectionTitle') }}</h2>
|
||||||
|
<div class="rule-section__tip">
|
||||||
|
<icon-info-circle class="rule-section__tip-icon" />
|
||||||
|
<p>{{ t('marketing.pointsRule.saveConfirmContent') }}</p>
|
||||||
|
<MarketingPageTipIcon />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-spin :loading="pointsLoading">
|
||||||
|
<FormPro
|
||||||
|
ref="pointsFormRef"
|
||||||
|
class="rule-form"
|
||||||
|
form-layout="vertical"
|
||||||
|
:hide-colon="true"
|
||||||
|
:item-list="pointsItemList"
|
||||||
|
:row-gutter="20"
|
||||||
|
:default-span="12"
|
||||||
|
:label-col-props="{ span: 24 }"
|
||||||
|
:wrapper-col-props="{ span: 24 }"
|
||||||
|
/>
|
||||||
|
</a-spin>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<a-space :size="8">
|
||||||
|
<a-button :disabled="pointsLoading || pointsSaving" @click="handlePointsReset">
|
||||||
|
{{ t('common.reset') }}
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
:loading="pointsSaving"
|
||||||
|
:disabled="pointsLoading"
|
||||||
|
@click="handlePointsSave"
|
||||||
|
>
|
||||||
|
{{ t('common.save') }}
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-if="canEditCommission" class="rule-section">
|
||||||
|
<h2 class="rule-section__title">{{ t('menu.marketing.commissionRule') }}</h2>
|
||||||
|
<div class="rule-section__tip">
|
||||||
|
<icon-info-circle class="rule-section__tip-icon" />
|
||||||
|
<p>{{ t('marketing.commissionRule.pageTip') }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a-spin :loading="commissionLoading">
|
||||||
|
<FormPro
|
||||||
|
ref="commissionFormRef"
|
||||||
|
class="rule-form"
|
||||||
|
form-layout="vertical"
|
||||||
|
:hide-colon="true"
|
||||||
|
:item-list="commissionItemList"
|
||||||
|
:row-gutter="20"
|
||||||
|
:default-span="12"
|
||||||
|
:label-col-props="{ span: 24 }"
|
||||||
|
:wrapper-col-props="{ span: 24 }"
|
||||||
|
/>
|
||||||
|
</a-spin>
|
||||||
|
|
||||||
|
<div class="form-actions">
|
||||||
|
<a-space :size="8">
|
||||||
|
<a-button :disabled="commissionLoading || commissionSaving" @click="handleCommissionReset">
|
||||||
|
{{ t('common.reset') }}
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
:loading="commissionSaving"
|
||||||
|
:disabled="commissionLoading"
|
||||||
|
@click="handleCommissionSave"
|
||||||
|
>
|
||||||
|
{{ t('common.save') }}
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
</MarketingPageShell>
|
</MarketingPageShell>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
|
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import { Message, Modal } from '@arco-design/web-vue'
|
||||||
|
import FormPro from '@/components/FormPro/index.vue'
|
||||||
|
import { isEnableStatusEnabled, toEnableStatus } from '@/utils/enableStatus'
|
||||||
|
import { hasPermission } from '@/utils/rbac-permission'
|
||||||
|
import { getPointsRule, updatePointsRule } from '@/api/marketing/pointsRule'
|
||||||
|
import { getCommissionRule, updateCommissionRule } from '@/api/marketing/commissionRule'
|
||||||
import MarketingPageShell from '../components/MarketingPageShell.vue'
|
import MarketingPageShell from '../components/MarketingPageShell.vue'
|
||||||
import PointsFlowPanel from '../components/PointsFlowPanel.vue'
|
import MarketingPageTipIcon from '../components/MarketingPageTipIcon.vue'
|
||||||
|
|
||||||
/** 兼容旧菜单「积分规则」:与积分流水同一页,规则配置走弹窗 */
|
|
||||||
defineOptions({ name: 'MarketingPointsRule' })
|
defineOptions({ name: 'MarketingPointsRule' })
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
type FormExpose = {
|
||||||
|
validate: () => Promise<unknown>
|
||||||
|
getValues: () => Record<string, unknown>
|
||||||
|
setValues: (values: Record<string, unknown>) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const canEditCommission = computed(() => hasPermission('Marketing.Commission.Rule.Edit'))
|
||||||
|
|
||||||
|
const pointsLoading = ref(false)
|
||||||
|
const pointsSaving = ref(false)
|
||||||
|
const pointsFormRef = ref<FormExpose>()
|
||||||
|
const loadedPoints = ref<Record<string, unknown>>({
|
||||||
|
id: undefined,
|
||||||
|
ruleName: '',
|
||||||
|
pointsPerYuan: 1,
|
||||||
|
status: 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
const commissionLoading = ref(false)
|
||||||
|
const commissionSaving = ref(false)
|
||||||
|
const commissionFormRef = ref<FormExpose>()
|
||||||
|
function emptyCommission(): Record<string, unknown> {
|
||||||
|
return {
|
||||||
|
id: undefined,
|
||||||
|
ruleName: '',
|
||||||
|
minInviteLevelCode: 'V1',
|
||||||
|
minPaidBaseFreight: 50,
|
||||||
|
firstOrderRatePercent: 5,
|
||||||
|
firstOrderCap: 50,
|
||||||
|
followOrderRatePercent: 3,
|
||||||
|
followOrderCap: 30,
|
||||||
|
followWindowDays: 90,
|
||||||
|
maxRewardOrders: 6,
|
||||||
|
settleBusinessDays: 3,
|
||||||
|
monthlyCap: undefined,
|
||||||
|
status: 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toRatePercent(value: unknown, fallback: number) {
|
||||||
|
const n = Number(value)
|
||||||
|
if (!Number.isFinite(n)) return fallback
|
||||||
|
return n <= 1 ? Math.round(n * 10000) / 100 : n
|
||||||
|
}
|
||||||
|
|
||||||
|
function toRateDecimal(value: unknown, fallbackPercent: number) {
|
||||||
|
const n = Number(value)
|
||||||
|
if (!Number.isFinite(n)) return fallbackPercent / 100
|
||||||
|
return n > 1 ? n / 100 : n
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadedCommission = ref<Record<string, unknown>>(emptyCommission())
|
||||||
|
|
||||||
|
const statusSwitchProps = computed(() => ({
|
||||||
|
checkedValue: 2,
|
||||||
|
uncheckedValue: 1,
|
||||||
|
checkedText: t('common.enabled'),
|
||||||
|
uncheckedText: t('common.disabled'),
|
||||||
|
}))
|
||||||
|
|
||||||
|
const pointsItemList = computed(() => [
|
||||||
|
{
|
||||||
|
field: 'ruleName',
|
||||||
|
label: t('marketing.pointsRule.form.ruleName'),
|
||||||
|
span: 24,
|
||||||
|
comp: 'a-input',
|
||||||
|
required: true,
|
||||||
|
rules: [{ required: true, message: t('marketing.pointsRule.form.ruleName') }],
|
||||||
|
compProps: {
|
||||||
|
placeholder: t('marketing.pointsRule.form.ruleName'),
|
||||||
|
maxLength: 64,
|
||||||
|
allowClear: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
label: t('marketing.pointsRule.form.status'),
|
||||||
|
span: 24,
|
||||||
|
comp: 'a-switch',
|
||||||
|
required: true,
|
||||||
|
rules: [{ required: true, message: t('marketing.pointsRule.form.status') }],
|
||||||
|
compProps: statusSwitchProps.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'pointsPerYuan',
|
||||||
|
label: t('marketing.pointsRule.form.pointsPerYuan'),
|
||||||
|
span: 24,
|
||||||
|
comp: 'a-input-number',
|
||||||
|
required: true,
|
||||||
|
tooltip: t('marketing.pointsRule.form.pointsPerYuanExtra'),
|
||||||
|
rules: [{ required: true, message: t('marketing.pointsRule.form.pointsPerYuan') }],
|
||||||
|
compProps: { min: 0, precision: 4, style: 'width: 100%', placeholder: '1' },
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const commissionItemList = computed(() => [
|
||||||
|
{
|
||||||
|
field: 'ruleName',
|
||||||
|
label: t('marketing.commissionRule.form.ruleName'),
|
||||||
|
span: 24,
|
||||||
|
comp: 'a-input',
|
||||||
|
required: true,
|
||||||
|
rules: [{ required: true, message: t('marketing.commissionRule.form.ruleName') }],
|
||||||
|
compProps: {
|
||||||
|
allowClear: true,
|
||||||
|
maxLength: 64,
|
||||||
|
placeholder: t('marketing.commissionRule.form.ruleName'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'status',
|
||||||
|
label: t('marketing.commissionRule.form.status'),
|
||||||
|
span: 24,
|
||||||
|
comp: 'a-switch',
|
||||||
|
required: true,
|
||||||
|
rules: [{ required: true, message: t('marketing.commissionRule.form.status') }],
|
||||||
|
compProps: statusSwitchProps.value,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'minInviteLevelCode',
|
||||||
|
label: t('marketing.commissionRule.form.minInviteLevelCode'),
|
||||||
|
span: 12,
|
||||||
|
comp: 'a-input',
|
||||||
|
required: true,
|
||||||
|
tooltip: t('marketing.commissionRule.form.minInviteLevelCodeExtra'),
|
||||||
|
rules: [{ required: true, message: t('marketing.commissionRule.form.minInviteLevelCode') }],
|
||||||
|
compProps: { allowClear: true, maxLength: 8, placeholder: 'V1' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'minPaidBaseFreight',
|
||||||
|
label: t('marketing.commissionRule.form.minPaidBaseFreight'),
|
||||||
|
span: 12,
|
||||||
|
comp: 'a-input-number',
|
||||||
|
required: true,
|
||||||
|
tooltip: t('marketing.commissionRule.form.minPaidBaseFreightExtra'),
|
||||||
|
rules: [{ required: true, message: t('marketing.commissionRule.form.minPaidBaseFreight') }],
|
||||||
|
compProps: { min: 0, precision: 2, style: 'width: 100%', placeholder: '50' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'firstOrderRatePercent',
|
||||||
|
label: t('marketing.commissionRule.form.firstOrderRate'),
|
||||||
|
span: 12,
|
||||||
|
comp: 'a-input-number',
|
||||||
|
required: true,
|
||||||
|
tooltip: t('marketing.commissionRule.form.firstOrderRateExtra'),
|
||||||
|
rules: [{ required: true, message: t('marketing.commissionRule.form.firstOrderRate') }],
|
||||||
|
compProps: { min: 0, max: 100, precision: 2, style: 'width: 100%', placeholder: '5' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'firstOrderCap',
|
||||||
|
label: t('marketing.commissionRule.form.firstOrderCap'),
|
||||||
|
span: 12,
|
||||||
|
comp: 'a-input-number',
|
||||||
|
required: true,
|
||||||
|
rules: [{ required: true, message: t('marketing.commissionRule.form.firstOrderCap') }],
|
||||||
|
compProps: { min: 0, precision: 2, style: 'width: 100%', placeholder: '50' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'followOrderRatePercent',
|
||||||
|
label: t('marketing.commissionRule.form.followOrderRate'),
|
||||||
|
span: 12,
|
||||||
|
comp: 'a-input-number',
|
||||||
|
required: true,
|
||||||
|
tooltip: t('marketing.commissionRule.form.followOrderRateExtra'),
|
||||||
|
rules: [{ required: true, message: t('marketing.commissionRule.form.followOrderRate') }],
|
||||||
|
compProps: { min: 0, max: 100, precision: 2, style: 'width: 100%', placeholder: '3' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'followOrderCap',
|
||||||
|
label: t('marketing.commissionRule.form.followOrderCap'),
|
||||||
|
span: 12,
|
||||||
|
comp: 'a-input-number',
|
||||||
|
required: true,
|
||||||
|
rules: [{ required: true, message: t('marketing.commissionRule.form.followOrderCap') }],
|
||||||
|
compProps: { min: 0, precision: 2, style: 'width: 100%', placeholder: '30' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'followWindowDays',
|
||||||
|
label: t('marketing.commissionRule.form.followWindowDays'),
|
||||||
|
span: 12,
|
||||||
|
comp: 'a-input-number',
|
||||||
|
required: true,
|
||||||
|
rules: [{ required: true, message: t('marketing.commissionRule.form.followWindowDays') }],
|
||||||
|
compProps: { min: 1, precision: 0, style: 'width: 100%', placeholder: '90' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'maxRewardOrders',
|
||||||
|
label: t('marketing.commissionRule.form.maxRewardOrders'),
|
||||||
|
span: 12,
|
||||||
|
comp: 'a-input-number',
|
||||||
|
required: true,
|
||||||
|
rules: [{ required: true, message: t('marketing.commissionRule.form.maxRewardOrders') }],
|
||||||
|
compProps: { min: 1, precision: 0, style: 'width: 100%', placeholder: '6' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'settleBusinessDays',
|
||||||
|
label: t('marketing.commissionRule.form.settleBusinessDays'),
|
||||||
|
span: 12,
|
||||||
|
comp: 'a-input-number',
|
||||||
|
required: true,
|
||||||
|
tooltip: t('marketing.commissionRule.form.settleBusinessDaysExtra'),
|
||||||
|
rules: [{ required: true, message: t('marketing.commissionRule.form.settleBusinessDays') }],
|
||||||
|
compProps: { min: 0, precision: 0, style: 'width: 100%', placeholder: '3' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'monthlyCap',
|
||||||
|
label: t('marketing.commissionRule.form.monthlyCap'),
|
||||||
|
span: 12,
|
||||||
|
comp: 'a-input-number',
|
||||||
|
tooltip: t('marketing.commissionRule.form.monthlyCapExtra'),
|
||||||
|
compProps: {
|
||||||
|
min: 0,
|
||||||
|
precision: 2,
|
||||||
|
style: 'width: 100%',
|
||||||
|
placeholder: t('marketing.commissionRule.form.monthlyCapExtra'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
function mapPoints(item: Record<string, unknown>) {
|
||||||
|
const status = item.status ?? item.Status ?? 2
|
||||||
|
return {
|
||||||
|
id: (item.id ?? item.Id) as number | undefined,
|
||||||
|
ruleName: String(item.ruleName ?? item.RuleName ?? ''),
|
||||||
|
pointsPerYuan: Number(item.pointsPerYuan ?? item.PointsPerYuan ?? 1),
|
||||||
|
status: toEnableStatus(isEnableStatusEnabled(status)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapCommission(item: Record<string, unknown>) {
|
||||||
|
const status = item.status ?? item.Status ?? 2
|
||||||
|
const cap = item.monthlyCap ?? item.MonthlyCap
|
||||||
|
const settle =
|
||||||
|
item.settleBusinessDays ?? item.SettleBusinessDays ?? item.freezeDays ?? item.FreezeDays ?? 3
|
||||||
|
return {
|
||||||
|
id: (item.id ?? item.Id) as number | undefined,
|
||||||
|
ruleName: String(item.ruleName ?? item.RuleName ?? ''),
|
||||||
|
minInviteLevelCode: String(item.minInviteLevelCode ?? item.MinInviteLevelCode ?? 'V1'),
|
||||||
|
minPaidBaseFreight: Number(item.minPaidBaseFreight ?? item.MinPaidBaseFreight ?? 50),
|
||||||
|
firstOrderRatePercent: toRatePercent(item.firstOrderRate ?? item.FirstOrderRate, 5),
|
||||||
|
firstOrderCap: Number(item.firstOrderCap ?? item.FirstOrderCap ?? 50),
|
||||||
|
followOrderRatePercent: toRatePercent(item.followOrderRate ?? item.FollowOrderRate, 3),
|
||||||
|
followOrderCap: Number(item.followOrderCap ?? item.FollowOrderCap ?? 30),
|
||||||
|
followWindowDays: Number(item.followWindowDays ?? item.FollowWindowDays ?? 90),
|
||||||
|
maxRewardOrders: Number(item.maxRewardOrders ?? item.MaxRewardOrders ?? 6),
|
||||||
|
settleBusinessDays: Number(settle),
|
||||||
|
monthlyCap: cap != null && cap !== '' ? Number(cap) : undefined,
|
||||||
|
status: toEnableStatus(isEnableStatusEnabled(status)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPoints(values: Record<string, unknown>) {
|
||||||
|
loadedPoints.value = values
|
||||||
|
pointsFormRef.value?.setValues({ ...values })
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyCommission(values: Record<string, unknown>) {
|
||||||
|
loadedCommission.value = values
|
||||||
|
commissionFormRef.value?.setValues({ ...values })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPoints() {
|
||||||
|
pointsLoading.value = true
|
||||||
|
try {
|
||||||
|
const data = await getPointsRule()
|
||||||
|
await nextTick()
|
||||||
|
if (data && typeof data === 'object') {
|
||||||
|
applyPoints(mapPoints(data as Record<string, unknown>))
|
||||||
|
} else {
|
||||||
|
applyPoints({
|
||||||
|
id: undefined,
|
||||||
|
ruleName: '',
|
||||||
|
pointsPerYuan: 1,
|
||||||
|
status: 2,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || t('common.loadFailed'))
|
||||||
|
} finally {
|
||||||
|
pointsLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCommission() {
|
||||||
|
if (!canEditCommission.value) return
|
||||||
|
commissionLoading.value = true
|
||||||
|
try {
|
||||||
|
const data = await getCommissionRule()
|
||||||
|
await nextTick()
|
||||||
|
if (data && typeof data === 'object') {
|
||||||
|
applyCommission(mapCommission(data as Record<string, unknown>))
|
||||||
|
} else {
|
||||||
|
applyCommission(emptyCommission())
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || t('common.loadFailed'))
|
||||||
|
} finally {
|
||||||
|
commissionLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointsReset() {
|
||||||
|
applyPoints({ ...loadedPoints.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCommissionReset() {
|
||||||
|
applyCommission({ ...loadedCommission.value })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handlePointsSave() {
|
||||||
|
try {
|
||||||
|
await pointsFormRef.value?.validate()
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const values = pointsFormRef.value?.getValues() || {}
|
||||||
|
const ruleName = String(values.ruleName || '').trim()
|
||||||
|
if (!ruleName) {
|
||||||
|
Message.warning(t('marketing.pointsRule.form.ruleName'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Modal.confirm({
|
||||||
|
title: t('marketing.pointsRule.saveConfirmTitle'),
|
||||||
|
content: t('marketing.pointsRule.saveConfirmContent'),
|
||||||
|
onOk: async () => {
|
||||||
|
pointsSaving.value = true
|
||||||
|
try {
|
||||||
|
await updatePointsRule({
|
||||||
|
id: values.id,
|
||||||
|
ruleName,
|
||||||
|
pointsPerYuan: Number(values.pointsPerYuan ?? 1),
|
||||||
|
firstOrderMultiplier: 1,
|
||||||
|
maxPointsPerOrder: 0,
|
||||||
|
status: Number(values.status ?? 2),
|
||||||
|
})
|
||||||
|
Message.success(t('common.saveSuccess'))
|
||||||
|
await loadPoints()
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || t('common.saveFailed'))
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
pointsSaving.value = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleCommissionSave() {
|
||||||
|
try {
|
||||||
|
await commissionFormRef.value?.validate()
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const values = commissionFormRef.value?.getValues() || {}
|
||||||
|
const ruleName = String(values.ruleName || '').trim()
|
||||||
|
if (!ruleName) {
|
||||||
|
Message.warning(t('marketing.commissionRule.form.ruleName'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const next = {
|
||||||
|
id: values.id,
|
||||||
|
ruleName,
|
||||||
|
commissionMode: 'percent_of_paid_base_freight',
|
||||||
|
minInviteLevelCode: String(values.minInviteLevelCode || 'V1').trim(),
|
||||||
|
minPaidBaseFreight: Number(values.minPaidBaseFreight ?? 50),
|
||||||
|
firstOrderRate: toRateDecimal(values.firstOrderRatePercent, 5),
|
||||||
|
firstOrderCap: Number(values.firstOrderCap ?? 50),
|
||||||
|
followOrderRate: toRateDecimal(values.followOrderRatePercent, 3),
|
||||||
|
followOrderCap: Number(values.followOrderCap ?? 30),
|
||||||
|
followWindowDays: Number(values.followWindowDays ?? 90),
|
||||||
|
maxRewardOrders: Number(values.maxRewardOrders ?? 6),
|
||||||
|
settleBusinessDays: Number(values.settleBusinessDays ?? 3),
|
||||||
|
freezeDays: Number(values.settleBusinessDays ?? 3),
|
||||||
|
monthlyCap:
|
||||||
|
values.monthlyCap != null && values.monthlyCap !== '' ? Number(values.monthlyCap) : null,
|
||||||
|
status: Number(values.status ?? 2),
|
||||||
|
}
|
||||||
|
const o = loadedCommission.value
|
||||||
|
Modal.confirm({
|
||||||
|
title: t('marketing.commissionRule.saveConfirmTitle'),
|
||||||
|
content: t('marketing.commissionRule.saveCompare', {
|
||||||
|
oldRate: o.firstOrderRatePercent,
|
||||||
|
newRate: values.firstOrderRatePercent,
|
||||||
|
oldFollow: o.followOrderRatePercent,
|
||||||
|
newFollow: values.followOrderRatePercent,
|
||||||
|
oldMin: o.minPaidBaseFreight,
|
||||||
|
newMin: next.minPaidBaseFreight,
|
||||||
|
oldSettle: o.settleBusinessDays,
|
||||||
|
newSettle: next.settleBusinessDays,
|
||||||
|
}),
|
||||||
|
onOk: async () => {
|
||||||
|
commissionSaving.value = true
|
||||||
|
try {
|
||||||
|
await updateCommissionRule(next)
|
||||||
|
Message.success(t('common.saveSuccess'))
|
||||||
|
await loadCommission()
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || t('common.saveFailed'))
|
||||||
|
throw e
|
||||||
|
} finally {
|
||||||
|
commissionSaving.value = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadPoints()
|
||||||
|
await loadCommission()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
@import '../styles/admin-shared.less';
|
||||||
|
|
||||||
|
.points-rule-page {
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
align-items: stretch;
|
||||||
|
|
||||||
|
@media (max-width: 991px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 20px 24px 24px;
|
||||||
|
background: var(--app-card-bg);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
:deep(.form-actions) {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-section__title {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 24px;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-section__tip {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: flex-start;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: color-mix(in srgb, var(--app-primary-color) 8%, var(--app-card-bg));
|
||||||
|
border: 1px solid color-mix(in srgb, var(--app-primary-color) 18%, var(--color-border-2));
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: var(--color-text-2, #4e5969);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-section__tip-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-top: 2px;
|
||||||
|
color: var(--app-primary-color);
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rule-form {
|
||||||
|
:deep(.arco-form-item) {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.arco-form-item-label-col) {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.arco-input-wrapper),
|
||||||
|
:deep(.arco-input-number) {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.arco-radio-group-button) {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
.arco-radio-button {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -116,6 +116,12 @@
|
|||||||
flex: 0 0 140px;
|
flex: 0 0 140px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mkt-status-tabs {
|
||||||
|
padding: 10px 16px 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-bottom: 1px solid var(--color-border-2);
|
||||||
|
}
|
||||||
|
|
||||||
/* 查询按钮左侧说明问号:与筛选区垂直对齐 */
|
/* 查询按钮左侧说明问号:与筛选区垂直对齐 */
|
||||||
.toolbar-filters :deep(.marketing-page-tip-icon),
|
.toolbar-filters :deep(.marketing-page-tip-icon),
|
||||||
.toolbar-left :deep(.marketing-page-tip-icon) {
|
.toolbar-left :deep(.marketing-page-tip-icon) {
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { IconDown } from '@arco-design/web-vue/es/icon'
|
import IconTabService from '@/assets/icons/h5/tab-service.svg'
|
||||||
|
|
||||||
defineOptions({ name: 'PriceCheckQuoteResultCard' })
|
defineOptions({ name: 'PriceCheckQuoteResultCard' })
|
||||||
|
|
||||||
@ -328,13 +328,13 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="quote-card__actions" @click.stop>
|
<div class="quote-card__actions" @click.stop>
|
||||||
<button type="button" class="ops-btn ops-btn--primary" @click="emit('order')">
|
<a-dropdown trigger="click" position="bl">
|
||||||
{{ t('order.priceCheck.table.orderNow') }}
|
<button
|
||||||
</button>
|
type="button"
|
||||||
<a-dropdown trigger="click" position="br">
|
class="advisor-icon-btn"
|
||||||
<button type="button" class="ops-btn ops-btn--ghost">
|
:aria-label="t('order.priceCheck.table.askAi')"
|
||||||
{{ t('order.priceCheck.table.askAi') }}
|
>
|
||||||
<icon-down style="margin-left: 2px" />
|
<IconTabService class="advisor-icon-btn__icon" />
|
||||||
</button>
|
</button>
|
||||||
<template #content>
|
<template #content>
|
||||||
<a-doption @click="emit('ask', 'canShip')">
|
<a-doption @click="emit('ask', 'canShip')">
|
||||||
@ -345,6 +345,9 @@
|
|||||||
</a-doption>
|
</a-doption>
|
||||||
</template>
|
</template>
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
|
<button type="button" class="ops-btn ops-btn--primary" @click="emit('order')">
|
||||||
|
{{ t('order.priceCheck.table.orderNow') }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a-spin>
|
</a-spin>
|
||||||
@ -666,25 +669,18 @@
|
|||||||
|
|
||||||
.quote-card__actions {
|
.quote-card__actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
align-items: stretch;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
min-width: 88px;
|
|
||||||
|
|
||||||
:deep(.arco-dropdown-open),
|
:deep(.arco-dropdown-open),
|
||||||
:deep(.arco-trigger),
|
:deep(.arco-trigger),
|
||||||
> :deep(.arco-dropdown-trigger),
|
> :deep(.arco-dropdown-trigger),
|
||||||
> .arco-dropdown-open {
|
> .arco-dropdown-open {
|
||||||
width: 100%;
|
display: inline-flex;
|
||||||
display: block;
|
flex-shrink: 0;
|
||||||
}
|
|
||||||
|
|
||||||
> :deep(span),
|
|
||||||
> :deep(div) {
|
|
||||||
width: 100%;
|
|
||||||
display: block;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -692,36 +688,54 @@
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 100%;
|
|
||||||
height: 28px;
|
|
||||||
padding: 0 10px;
|
|
||||||
border-radius: 6px;
|
|
||||||
font-size: 12px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
|
|
||||||
&--ghost {
|
|
||||||
background: transparent;
|
|
||||||
border-color: var(--color-border-2);
|
|
||||||
color: var(--color-text-2);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--app-primary-color, #007979);
|
|
||||||
border-color: color-mix(in srgb, var(--app-primary-color, #007979) 35%, var(--color-border-2));
|
|
||||||
background: color-mix(in srgb, var(--app-primary-color, #007979) 4%, transparent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&--primary {
|
&--primary {
|
||||||
|
min-width: 88px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 16px;
|
||||||
|
border-radius: 6px;
|
||||||
background: var(--app-primary-color, #007979);
|
background: var(--app-primary-color, #007979);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
text-align: center;
|
||||||
|
box-shadow: 0 1px 2px color-mix(in srgb, var(--app-primary-color, #007979) 28%, transparent);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
filter: brightness(1.04);
|
filter: brightness(1.06);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.advisor-icon-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--app-primary-color, #007979);
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: color-mix(in srgb, var(--app-primary-color, #007979) 8%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.advisor-icon-btn__icon {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.quote-card__fee-pop {
|
.quote-card__fee-pop {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -799,7 +813,7 @@
|
|||||||
|
|
||||||
.quote-card__actions {
|
.quote-card__actions {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: flex-end;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -810,13 +824,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.quote-card__actions {
|
.quote-card__actions {
|
||||||
flex-direction: row;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ops-btn--primary {
|
.ops-btn--primary {
|
||||||
flex: 1;
|
|
||||||
min-width: 120px;
|
min-width: 120px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { IconDown, IconSearch } from '@arco-design/web-vue/es/icon'
|
import { IconClose, IconDown, IconSearch } from '@arco-design/web-vue/es/icon'
|
||||||
import SlidingTabs from '@/components/SlidingTabs/index.vue'
|
import SlidingTabs from '@/components/SlidingTabs/index.vue'
|
||||||
import type { PriceCheckShippingGroup } from '@/api/order'
|
import type { PriceCheckShippingGroup } from '@/api/order'
|
||||||
|
|
||||||
@ -132,7 +132,7 @@ function clearSelection(e: Event) {
|
|||||||
aria-label="clear"
|
aria-label="clear"
|
||||||
@click="clearSelection"
|
@click="clearSelection"
|
||||||
>
|
>
|
||||||
×
|
<icon-close />
|
||||||
</button>
|
</button>
|
||||||
<icon-down class="trigger-icon" />
|
<icon-down class="trigger-icon" />
|
||||||
</div>
|
</div>
|
||||||
@ -233,20 +233,23 @@ function clearSelection(e: Event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.trigger-clear {
|
.trigger-clear {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 18px;
|
width: 16px;
|
||||||
height: 18px;
|
height: 16px;
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
|
padding: 0;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 50%;
|
background: transparent;
|
||||||
background: var(--color-fill-4);
|
color: var(--color-text-3);
|
||||||
color: #fff;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 16px;
|
line-height: 1;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--color-text-3);
|
color: var(--color-text-1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -40,7 +40,7 @@
|
|||||||
const formData = reactive({
|
const formData = reactive({
|
||||||
destinationCountry: '',
|
destinationCountry: '',
|
||||||
shippingMethod: '',
|
shippingMethod: '',
|
||||||
cargoWeight: '',
|
cargoWeight: '1',
|
||||||
length: '',
|
length: '',
|
||||||
width: '',
|
width: '',
|
||||||
height: '',
|
height: '',
|
||||||
@ -92,18 +92,6 @@
|
|||||||
const couponInstanceId = ref<number | undefined>(undefined);
|
const couponInstanceId = ref<number | undefined>(undefined);
|
||||||
const availableCoupons = ref<CouponAvailableOutput[]>([]);
|
const availableCoupons = ref<CouponAvailableOutput[]>([]);
|
||||||
|
|
||||||
const advancedActiveCount = computed(() => {
|
|
||||||
let n = 0;
|
|
||||||
if (formData.shippingMethod) n += 1;
|
|
||||||
if (formData.cargoType) n += 1;
|
|
||||||
if (formData.warehouseId != null) n += 1;
|
|
||||||
if (hasFullDimensions() || hasPartialDimensions()) n += 1;
|
|
||||||
if (String(formData.destinationProvince || '').trim()) n += 1;
|
|
||||||
if (String(formData.destinationCity || '').trim()) n += 1;
|
|
||||||
if (couponInstanceId.value) n += 1;
|
|
||||||
return n;
|
|
||||||
});
|
|
||||||
|
|
||||||
const needPostalHint = computed(() => {
|
const needPostalHint = computed(() => {
|
||||||
if (!hasQueried.value) return false;
|
if (!hasQueried.value) return false;
|
||||||
if (String(formData.postalCode || '').trim()) return false;
|
if (String(formData.postalCode || '').trim()) return false;
|
||||||
@ -125,7 +113,6 @@
|
|||||||
| 'destinationCity'
|
| 'destinationCity'
|
||||||
| 'cargoType'
|
| 'cargoType'
|
||||||
| 'warehouse'
|
| 'warehouse'
|
||||||
| 'shipping'
|
|
||||||
| 'coupon';
|
| 'coupon';
|
||||||
value?: string | number;
|
value?: string | number;
|
||||||
};
|
};
|
||||||
@ -181,17 +168,6 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (formData.shippingMethod) {
|
|
||||||
const flat = (shippingMethodGroups.value || []).flatMap((g) => g.products || []);
|
|
||||||
const hit = flat.find((it) => it.productCode === formData.shippingMethod);
|
|
||||||
chips.push({
|
|
||||||
key: 'shipping',
|
|
||||||
kind: 'shipping',
|
|
||||||
value: formData.shippingMethod,
|
|
||||||
label: hit?.label || formData.shippingMethod,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (couponInstanceId.value) {
|
if (couponInstanceId.value) {
|
||||||
const c = availableCoupons.value.find((x) => (x.id ?? x.Id) === couponInstanceId.value);
|
const c = availableCoupons.value.find((x) => (x.id ?? x.Id) === couponInstanceId.value);
|
||||||
const name = String(c?.templateName ?? c?.TemplateName ?? '').trim();
|
const name = String(c?.templateName ?? c?.TemplateName ?? '').trim();
|
||||||
@ -229,10 +205,6 @@
|
|||||||
formData.warehouseId = undefined;
|
formData.warehouseId = undefined;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (chip.kind === 'shipping') {
|
|
||||||
formData.shippingMethod = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (chip.kind === 'coupon') {
|
if (chip.kind === 'coupon') {
|
||||||
couponInstanceId.value = undefined;
|
couponInstanceId.value = undefined;
|
||||||
}
|
}
|
||||||
@ -246,7 +218,6 @@
|
|||||||
formData.destinationCity = '';
|
formData.destinationCity = '';
|
||||||
formData.cargoType = '';
|
formData.cargoType = '';
|
||||||
formData.warehouseId = undefined;
|
formData.warehouseId = undefined;
|
||||||
formData.shippingMethod = '';
|
|
||||||
couponInstanceId.value = undefined;
|
couponInstanceId.value = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -621,14 +592,15 @@
|
|||||||
availableCoupons.value = coupons ?? [];
|
availableCoupons.value = coupons ?? [];
|
||||||
hintBannerDismissed.value = false;
|
hintBannerDismissed.value = false;
|
||||||
queryResults.value = processQueryResults(items || []);
|
queryResults.value = processQueryResults(items || []);
|
||||||
selectedGuideRecord.value = null;
|
|
||||||
resetRecommendState();
|
resetRecommendState();
|
||||||
sortMode.value = 'recommend';
|
sortMode.value = 'recommend';
|
||||||
currentPage.value = 1;
|
currentPage.value = 1;
|
||||||
recalcLoadingKeys.value = {};
|
recalcLoadingKeys.value = {};
|
||||||
if (!queryResults.value.length) {
|
if (!queryResults.value.length) {
|
||||||
|
selectedGuideRecord.value = null;
|
||||||
applyPriceCheckFailures(failures ?? []);
|
applyPriceCheckFailures(failures ?? []);
|
||||||
} else {
|
} else {
|
||||||
|
selectedGuideRecord.value = sortedResults.value[0] ?? queryResults.value[0];
|
||||||
clearPriceCheckFailures();
|
clearPriceCheckFailures();
|
||||||
Message.success(t('order.priceCheck.message.querySuccess', { count: queryResults.value.length }));
|
Message.success(t('order.priceCheck.message.querySuccess', { count: queryResults.value.length }));
|
||||||
}
|
}
|
||||||
@ -648,7 +620,7 @@
|
|||||||
Object.assign(formData, {
|
Object.assign(formData, {
|
||||||
destinationCountry: '',
|
destinationCountry: '',
|
||||||
shippingMethod: '',
|
shippingMethod: '',
|
||||||
cargoWeight: '',
|
cargoWeight: '1',
|
||||||
length: '',
|
length: '',
|
||||||
width: '',
|
width: '',
|
||||||
height: '',
|
height: '',
|
||||||
@ -1001,6 +973,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-row form-row-primary">
|
<div class="form-row form-row-primary">
|
||||||
|
<div class="form-item form-item-channel">
|
||||||
|
<label class="form-label">{{ $t('order.priceCheck.form.channel') }}</label>
|
||||||
|
<ShippingMethodTabSelect
|
||||||
|
v-model="formData.shippingMethod"
|
||||||
|
:groups="shippingMethodGroups"
|
||||||
|
:loading="optionsLoading"
|
||||||
|
:placeholder="$t('order.priceCheck.form.channel.placeholder')"
|
||||||
|
class="form-select"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-item form-item-country">
|
<div class="form-item form-item-country">
|
||||||
<label class="form-label required">{{ $t('order.priceCheck.form.destinationCountry') }}</label>
|
<label class="form-label required">{{ $t('order.priceCheck.form.destinationCountry') }}</label>
|
||||||
<a-select
|
<a-select
|
||||||
@ -1035,21 +1018,21 @@
|
|||||||
</a-select>
|
</a-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-item">
|
<div ref="postalInputRef" class="form-item form-item-compact">
|
||||||
<label class="form-label required">{{ $t('order.priceCheck.form.cargoWeight') }}</label>
|
<label class="form-label">{{ $t('order.priceCheck.form.postalCode') }}</label>
|
||||||
<a-input
|
<a-input
|
||||||
v-model="formData.cargoWeight"
|
v-model="formData.postalCode"
|
||||||
:placeholder="$t('order.priceCheck.form.cargoWeight.placeholder')"
|
:placeholder="$t('order.priceCheck.form.postalCode.placeholder')"
|
||||||
class="form-input"
|
class="form-input"
|
||||||
@press-enter="handleQuery"
|
@press-enter="handleQuery"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ref="postalInputRef" class="form-item">
|
<div class="form-item form-item-compact">
|
||||||
<label class="form-label">{{ $t('order.priceCheck.form.postalCode') }}</label>
|
<label class="form-label required">{{ $t('order.priceCheck.form.cargoWeight') }}</label>
|
||||||
<a-input
|
<a-input
|
||||||
v-model="formData.postalCode"
|
v-model="formData.cargoWeight"
|
||||||
:placeholder="$t('order.priceCheck.form.postalCode.placeholder')"
|
:placeholder="$t('order.priceCheck.form.cargoWeight.placeholder')"
|
||||||
class="form-input"
|
class="form-input"
|
||||||
@press-enter="handleQuery"
|
@press-enter="handleQuery"
|
||||||
/>
|
/>
|
||||||
@ -1071,10 +1054,7 @@
|
|||||||
{{ $t('order.priceCheck.actions.reset') }}
|
{{ $t('order.priceCheck.actions.reset') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
<button type="button" class="advanced-toggle-btn" @click="showAdvanced = !showAdvanced">
|
<button type="button" class="advanced-toggle-btn" @click="showAdvanced = !showAdvanced">
|
||||||
<span>
|
<span>{{ $t('order.priceCheck.form.moreFilters') }}</span>
|
||||||
{{ $t('order.priceCheck.form.moreFilters') }}
|
|
||||||
<span v-if="advancedActiveCount" class="advanced-count">{{ advancedActiveCount }}</span>
|
|
||||||
</span>
|
|
||||||
<icon-up v-if="showAdvanced" />
|
<icon-up v-if="showAdvanced" />
|
||||||
<icon-down v-else />
|
<icon-down v-else />
|
||||||
</button>
|
</button>
|
||||||
@ -1110,10 +1090,10 @@
|
|||||||
<icon-close />
|
<icon-close />
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="filter-summary-bar__clear" @click="clearAdvancedConditions">
|
||||||
|
{{ $t('order.priceCheck.form.clearFilters') }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="filter-summary-bar__clear" @click="clearAdvancedConditions">
|
|
||||||
{{ $t('order.priceCheck.form.clearFilters') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-show="showAdvanced" class="form-row form-row-advanced">
|
<div v-show="showAdvanced" class="form-row form-row-advanced">
|
||||||
@ -1183,17 +1163,6 @@
|
|||||||
</a-select>
|
</a-select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-item">
|
|
||||||
<label class="form-label">{{ $t('order.priceCheck.form.channel') }}</label>
|
|
||||||
<ShippingMethodTabSelect
|
|
||||||
v-model="formData.shippingMethod"
|
|
||||||
:groups="shippingMethodGroups"
|
|
||||||
:loading="optionsLoading"
|
|
||||||
:placeholder="$t('order.priceCheck.form.channel.placeholder')"
|
|
||||||
class="form-select"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-item">
|
<div class="form-item">
|
||||||
<label class="form-label">{{ $t('order.priceCheck.form.cargoType') }}</label>
|
<label class="form-label">{{ $t('order.priceCheck.form.cargoType') }}</label>
|
||||||
<a-select
|
<a-select
|
||||||
@ -1365,8 +1334,8 @@
|
|||||||
background-color: var(--app-page-bg);
|
background-color: var(--app-page-bg);
|
||||||
}
|
}
|
||||||
.price-check-layout { display: flex; gap: 12px; flex: 1; min-height: 0; min-width: 0; }
|
.price-check-layout { display: flex; gap: 12px; flex: 1; min-height: 0; min-width: 0; }
|
||||||
.price-check-main { flex: 7 1 0; min-width: 0; min-height: 0; display: flex; flex-direction: column; gap: 12px; overflow: hidden; }
|
.price-check-main { flex: 5 1 0; min-width: 0; min-height: 0; display: flex; flex-direction: column; gap: 12px; overflow: hidden; }
|
||||||
.price-check-aside { flex: 3 1 0; min-width: 300px; min-height: 0; display: flex; flex-direction: column; > * { flex: 1; min-height: 0; } }
|
.price-check-aside { flex: 3 1 0; min-width: 340px; min-height: 0; display: flex; flex-direction: column; > * { flex: 1; min-height: 0; } }
|
||||||
.liquid-panel {
|
.liquid-panel {
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 0;
|
z-index: 0;
|
||||||
@ -1439,8 +1408,9 @@
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
.form-row { display: flex; gap: 20px; align-items: flex-start; flex-wrap: wrap; }
|
.form-row { display: flex; gap: 16px; align-items: flex-start; flex-wrap: wrap; }
|
||||||
.form-row-primary, .form-row-advanced { overflow-x: auto; padding-bottom: 2px; -webkit-overflow-scrolling: touch; }
|
.form-row-primary, .form-row-advanced { overflow-x: auto; padding-bottom: 2px; -webkit-overflow-scrolling: touch; }
|
||||||
|
.form-row-primary { flex-wrap: nowrap; }
|
||||||
.form-row-advanced {
|
.form-row-advanced {
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
@ -1543,23 +1513,9 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
&:hover { color: var(--app-primary-color, #007979); }
|
&:hover { color: var(--app-primary-color, #007979); }
|
||||||
}
|
}
|
||||||
.advanced-count {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
min-width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
margin-left: 4px;
|
|
||||||
padding: 0 4px;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: rgba(0, 121, 121, 0.1);
|
|
||||||
color: var(--app-primary-color, #007979);
|
|
||||||
font-size: 11px;
|
|
||||||
line-height: 16px;
|
|
||||||
}
|
|
||||||
.form-hint { margin: 0; color: var(--color-text-3); font-size: 12px; line-height: 1.3; }
|
.form-hint { margin: 0; color: var(--color-text-3); font-size: 12px; line-height: 1.3; }
|
||||||
.form-item {
|
.form-item {
|
||||||
display: flex; flex-direction: column; gap: 8px; flex: 0 0 auto; width: 168px; min-width: 140px;
|
display: flex; flex-direction: column; gap: 8px; flex: 0 0 auto; width: 148px; min-width: 132px;
|
||||||
&.dimensions-group { width: 260px; min-width: 220px; }
|
&.dimensions-group { width: 260px; min-width: 220px; }
|
||||||
&.button-group { width: auto; min-width: 0; flex-shrink: 0; }
|
&.button-group { width: auto; min-width: 0; flex-shrink: 0; }
|
||||||
.button-group-row {
|
.button-group-row {
|
||||||
@ -1570,7 +1526,9 @@
|
|||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
}
|
}
|
||||||
&.form-item-country { width: 280px; min-width: 240px; flex-shrink: 0; }
|
&.form-item-country { width: 240px; min-width: 200px; flex-shrink: 0; }
|
||||||
|
&.form-item-channel { width: 220px; min-width: 180px; flex-shrink: 0; }
|
||||||
|
&.form-item-compact { width: 112px; min-width: 100px; }
|
||||||
}
|
}
|
||||||
.country-shortcuts {
|
.country-shortcuts {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -24,6 +24,7 @@
|
|||||||
<section class="portal-detail-section">
|
<section class="portal-detail-section">
|
||||||
<a-tabs v-model:active-key="activeTab" type="rounded" @change="handleTabChange">
|
<a-tabs v-model:active-key="activeTab" type="rounded" @change="handleTabChange">
|
||||||
<a-tab-pane key="available" :title="t('marketing.portal.coupons.tab.available')" />
|
<a-tab-pane key="available" :title="t('marketing.portal.coupons.tab.available')" />
|
||||||
|
<a-tab-pane key="locked" :title="t('marketing.portal.coupons.tab.locked')" />
|
||||||
<a-tab-pane key="used" :title="t('marketing.portal.coupons.tab.used')" />
|
<a-tab-pane key="used" :title="t('marketing.portal.coupons.tab.used')" />
|
||||||
<a-tab-pane key="expired" :title="t('marketing.portal.coupons.tab.expired')" />
|
<a-tab-pane key="expired" :title="t('marketing.portal.coupons.tab.expired')" />
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
@ -75,10 +76,18 @@
|
|||||||
</a-tag>
|
</a-tag>
|
||||||
</div>
|
</div>
|
||||||
<p class="coupon-validity">
|
<p class="coupon-validity">
|
||||||
{{ formatDate(item.validStartTime) }} ~ {{ formatDate(item.validEndTime) }}
|
<template v-if="item.status === 5">
|
||||||
|
{{ item.unlockHint || t('marketing.portal.coupons.unlockHint') }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ formatDate(item.validStartTime) }} ~ {{ formatDate(item.validEndTime) }}
|
||||||
|
</template>
|
||||||
</p>
|
</p>
|
||||||
<div class="coupon-body-foot">
|
<div class="coupon-body-foot">
|
||||||
<a-tag v-if="item.isExpiringSoon" size="small">
|
<a-tag v-if="item.status === 5" size="small">
|
||||||
|
{{ t('marketing.portal.coupons.tab.locked') }}
|
||||||
|
</a-tag>
|
||||||
|
<a-tag v-else-if="item.isExpiringSoon" size="small">
|
||||||
{{ t('marketing.portal.coupons.expiringSoon') }}
|
{{ t('marketing.portal.coupons.expiringSoon') }}
|
||||||
</a-tag>
|
</a-tag>
|
||||||
<span v-else-if="item.status === 1" class="hint hint--action">
|
<span v-else-if="item.status === 1" class="hint hint--action">
|
||||||
@ -132,6 +141,7 @@ interface CouponRow {
|
|||||||
status: number
|
status: number
|
||||||
statusName: string
|
statusName: string
|
||||||
isExpiringSoon: boolean
|
isExpiringSoon: boolean
|
||||||
|
unlockHint: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
@ -160,6 +170,7 @@ const howSteps = computed(() => [
|
|||||||
|
|
||||||
const statusMap: Record<string, number> = {
|
const statusMap: Record<string, number> = {
|
||||||
available: 1,
|
available: 1,
|
||||||
|
locked: 5,
|
||||||
used: 3,
|
used: 3,
|
||||||
expired: 4,
|
expired: 4,
|
||||||
}
|
}
|
||||||
@ -167,6 +178,7 @@ const statusMap: Record<string, number> = {
|
|||||||
const emptyTitle = computed(() => {
|
const emptyTitle = computed(() => {
|
||||||
if (activeTab.value === 'used') return t('marketing.portal.coupons.emptyUsedTitle')
|
if (activeTab.value === 'used') return t('marketing.portal.coupons.emptyUsedTitle')
|
||||||
if (activeTab.value === 'expired') return t('marketing.portal.coupons.emptyExpiredTitle')
|
if (activeTab.value === 'expired') return t('marketing.portal.coupons.emptyExpiredTitle')
|
||||||
|
if (activeTab.value === 'locked') return t('marketing.portal.coupons.emptyLockedTitle')
|
||||||
return t('marketing.portal.coupons.emptyTitle')
|
return t('marketing.portal.coupons.emptyTitle')
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -181,6 +193,7 @@ const availableSummary = computed(() => String(availableCount.value))
|
|||||||
|
|
||||||
function statusFallback(status: number) {
|
function statusFallback(status: number) {
|
||||||
if (status === 1) return t('marketing.portal.coupons.tab.available')
|
if (status === 1) return t('marketing.portal.coupons.tab.available')
|
||||||
|
if (status === 5) return t('marketing.portal.coupons.tab.locked')
|
||||||
if (status === 3) return t('marketing.portal.coupons.tab.used')
|
if (status === 3) return t('marketing.portal.coupons.tab.used')
|
||||||
if (status === 4) return t('marketing.portal.coupons.tab.expired')
|
if (status === 4) return t('marketing.portal.coupons.tab.expired')
|
||||||
return '--'
|
return '--'
|
||||||
@ -228,6 +241,7 @@ function mapRow(item: Record<string, unknown>): CouponRow {
|
|||||||
status: Number(item.status ?? item.Status ?? 0),
|
status: Number(item.status ?? item.Status ?? 0),
|
||||||
statusName: String(item.statusName ?? item.StatusName ?? ''),
|
statusName: String(item.statusName ?? item.StatusName ?? ''),
|
||||||
isExpiringSoon: isExpiringSoon(validEndTime),
|
isExpiringSoon: isExpiringSoon(validEndTime),
|
||||||
|
unlockHint: String(item.unlockHint ?? item.UnlockHint ?? ''),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,19 +6,28 @@
|
|||||||
<span class="portal-glass__blob" aria-hidden="true" />
|
<span class="portal-glass__blob" aria-hidden="true" />
|
||||||
<span class="portal-glass__shine" aria-hidden="true" />
|
<span class="portal-glass__shine" aria-hidden="true" />
|
||||||
<div class="portal-glass__body share-hero__focus">
|
<div class="portal-glass__body share-hero__focus">
|
||||||
<p class="share-hero__eyebrow">{{ t('marketing.portal.invite.yourCode') }}</p>
|
<template v-if="canInvite">
|
||||||
<div class="invite-code-row">
|
<p class="share-hero__eyebrow">{{ t('marketing.portal.invite.yourCode') }}</p>
|
||||||
<span class="invite-code">{{ inviteCode }}</span>
|
<div class="invite-code-row">
|
||||||
<span class="referee-pill">
|
<span class="invite-code">{{ inviteCode }}</span>
|
||||||
{{ t('marketing.portal.invite.refereeOnly', { count: refereeCount }) }}
|
<span class="referee-pill">
|
||||||
</span>
|
{{ t('marketing.portal.invite.refereeOnly', { count: refereeCount }) }}
|
||||||
</div>
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="reward-row">
|
||||||
|
<span>{{ t('marketing.portal.invite.pendingReward') }} ¥{{ formatMoney(pendingReward) }}</span>
|
||||||
|
<span>{{ t('marketing.portal.invite.settledReward') }} ¥{{ formatMoney(settledReward) }}</span>
|
||||||
|
</div>
|
||||||
|
<a-button type="primary" long size="large" class="share-primary" @click="copyShareText">
|
||||||
|
{{ t('marketing.portal.invite.copySharePrimary') }}
|
||||||
|
</a-button>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<p class="share-hero__eyebrow">{{ t('marketing.portal.invite.lockedTitle', { level: minInviteLevel }) }}</p>
|
||||||
|
<p class="portal-empty__desc">{{ t('marketing.portal.invite.lockedDesc') }}</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
<a-button type="primary" long size="large" class="share-primary" @click="copyShareText">
|
<details v-if="canInvite" class="share-more">
|
||||||
{{ t('marketing.portal.invite.copySharePrimary') }}
|
|
||||||
</a-button>
|
|
||||||
|
|
||||||
<details class="share-more">
|
|
||||||
<summary>{{ t('marketing.portal.invite.secondaryActions') }}</summary>
|
<summary>{{ t('marketing.portal.invite.secondaryActions') }}</summary>
|
||||||
<div class="share-more__body">
|
<div class="share-more__body">
|
||||||
<div class="link-row">
|
<div class="link-row">
|
||||||
@ -45,6 +54,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<PortalHowTo
|
<PortalHowTo
|
||||||
|
v-if="canInvite"
|
||||||
:title="t('marketing.portal.invite.howTitle')"
|
:title="t('marketing.portal.invite.howTitle')"
|
||||||
:hint="t('marketing.portal.invite.howHint')"
|
:hint="t('marketing.portal.invite.howHint')"
|
||||||
:steps="howSteps"
|
:steps="howSteps"
|
||||||
@ -60,7 +70,7 @@
|
|||||||
<div v-if="!refereeRows.length && !refereeLoading" class="portal-empty">
|
<div v-if="!refereeRows.length && !refereeLoading" class="portal-empty">
|
||||||
<p class="portal-empty__title">{{ t('marketing.portal.invite.emptyTitle') }}</p>
|
<p class="portal-empty__title">{{ t('marketing.portal.invite.emptyTitle') }}</p>
|
||||||
<p class="portal-empty__desc">{{ t('marketing.portal.invite.emptyDesc') }}</p>
|
<p class="portal-empty__desc">{{ t('marketing.portal.invite.emptyDesc') }}</p>
|
||||||
<a-button type="primary" @click="copyShareText">
|
<a-button v-if="canInvite" type="primary" @click="copyShareText">
|
||||||
{{ t('marketing.portal.invite.copySharePrimary') }}
|
{{ t('marketing.portal.invite.copySharePrimary') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
@ -80,6 +90,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="portal-list-item__side referee-tags">
|
<div class="portal-list-item__side referee-tags">
|
||||||
|
<a-tag v-if="row.rewardOrderCount" size="small">
|
||||||
|
{{ t('marketing.portal.invite.tag.reward', { count: row.rewardOrderCount }) }}
|
||||||
|
</a-tag>
|
||||||
<a-tag v-if="row.hasDeliverySuccess" size="small">
|
<a-tag v-if="row.hasDeliverySuccess" size="small">
|
||||||
{{ t('marketing.portal.invite.tag.delivered') }}
|
{{ t('marketing.portal.invite.tag.delivered') }}
|
||||||
</a-tag>
|
</a-tag>
|
||||||
@ -136,6 +149,7 @@ interface RefereeRow {
|
|||||||
createTime: string
|
createTime: string
|
||||||
hasPaidOrder: boolean
|
hasPaidOrder: boolean
|
||||||
hasDeliverySuccess: boolean
|
hasDeliverySuccess: boolean
|
||||||
|
rewardOrderCount: number
|
||||||
avatarText: string
|
avatarText: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,6 +188,27 @@ const refereeCount = computed(() =>
|
|||||||
Number(invite.value?.refereeCount ?? invite.value?.RefereeCount ?? refereeTotal.value ?? 0),
|
Number(invite.value?.refereeCount ?? invite.value?.RefereeCount ?? refereeTotal.value ?? 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const canInvite = computed(() => {
|
||||||
|
const raw = invite.value?.canInvite ?? invite.value?.CanInvite
|
||||||
|
return raw === true || raw === 1 || raw === 'true'
|
||||||
|
})
|
||||||
|
|
||||||
|
const minInviteLevel = computed(() =>
|
||||||
|
String(invite.value?.minInviteLevelCode ?? invite.value?.MinInviteLevelCode ?? 'V1'),
|
||||||
|
)
|
||||||
|
|
||||||
|
const pendingReward = computed(() =>
|
||||||
|
Number(invite.value?.pendingReward ?? invite.value?.PendingReward ?? 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
const settledReward = computed(() =>
|
||||||
|
Number(invite.value?.settledReward ?? invite.value?.SettledReward ?? 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
function formatMoney(value: number) {
|
||||||
|
return value.toFixed(2)
|
||||||
|
}
|
||||||
|
|
||||||
async function copyShareText() {
|
async function copyShareText() {
|
||||||
await copyText(shareText.value)
|
await copyText(shareText.value)
|
||||||
}
|
}
|
||||||
@ -200,6 +235,7 @@ function mapReferee(item: Record<string, unknown>): RefereeRow {
|
|||||||
createTime: String(item.createTime ?? item.CreateTime ?? '').replace('T', ' ').slice(0, 19),
|
createTime: String(item.createTime ?? item.CreateTime ?? '').replace('T', ' ').slice(0, 19),
|
||||||
hasPaidOrder: Boolean(item.hasPaidOrder ?? item.HasPaidOrder),
|
hasPaidOrder: Boolean(item.hasPaidOrder ?? item.HasPaidOrder),
|
||||||
hasDeliverySuccess: Boolean(item.hasDeliverySuccessOrder ?? item.HasDeliverySuccessOrder),
|
hasDeliverySuccess: Boolean(item.hasDeliverySuccessOrder ?? item.HasDeliverySuccessOrder),
|
||||||
|
rewardOrderCount: Number(item.rewardOrderCount ?? item.RewardOrderCount ?? 0),
|
||||||
avatarText: avatarSource.slice(0, 1).toUpperCase(),
|
avatarText: avatarSource.slice(0, 1).toUpperCase(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -313,6 +349,14 @@ onMounted(() => {
|
|||||||
border: 1px solid color-mix(in srgb, var(--app-primary-color) 18%, var(--color-border-2));
|
border: 1px solid color-mix(in srgb, var(--app-primary-color) 18%, var(--color-border-2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.reward-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 12px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--color-text-2);
|
||||||
|
}
|
||||||
|
|
||||||
.share-primary {
|
.share-primary {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,9 +10,6 @@
|
|||||||
<p class="level-eyebrow">{{ t('marketing.portal.member.currentLevel') }}</p>
|
<p class="level-eyebrow">{{ t('marketing.portal.member.currentLevel') }}</p>
|
||||||
<div class="level-name-row">
|
<div class="level-name-row">
|
||||||
<h2 class="level-name">{{ levelName }}</h2>
|
<h2 class="level-name">{{ levelName }}</h2>
|
||||||
<span v-if="priceTierKey" class="tier-badge">
|
|
||||||
{{ t('marketing.portal.member.priceTier') }} · {{ priceTierKey }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<a-button type="primary" size="small" class="member-cta" @click="goPlaceOrder">
|
<a-button type="primary" size="small" class="member-cta" @click="goPlaceOrder">
|
||||||
@ -20,14 +17,12 @@
|
|||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p class="benefit-desc">{{ levelNotice }}</p>
|
||||||
|
|
||||||
<div class="points-grid" :class="{ 'has-discount': !!extraDiscountText }">
|
<div class="points-grid" :class="{ 'has-discount': !!extraDiscountText }">
|
||||||
<div class="points-item">
|
<div class="points-item">
|
||||||
<span class="points-label">{{ t('marketing.portal.member.totalPoints') }}</span>
|
<span class="points-label">{{ t('marketing.portal.member.totalPoints') }}</span>
|
||||||
<span class="points-value">{{ formatNumber(totalPoints) }}</span>
|
<span class="points-value">{{ formatNumber(rollingPoints) }}</span>
|
||||||
</div>
|
|
||||||
<div class="points-item">
|
|
||||||
<span class="points-label">{{ t('marketing.portal.member.availablePoints') }}</span>
|
|
||||||
<span class="points-value">{{ formatNumber(availablePoints) }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="extraDiscountText" class="points-item">
|
<div v-if="extraDiscountText" class="points-item">
|
||||||
<span class="points-label">{{ t('marketing.portal.member.extraDiscount') }}</span>
|
<span class="points-label">{{ t('marketing.portal.member.extraDiscount') }}</span>
|
||||||
@ -170,14 +165,14 @@ const levelName = computed(
|
|||||||
String(overview.value?.memberLevelName ?? overview.value?.MemberLevelName ?? '') ||
|
String(overview.value?.memberLevelName ?? overview.value?.MemberLevelName ?? '') ||
|
||||||
t('marketing.portal.member.defaultLevel'),
|
t('marketing.portal.member.defaultLevel'),
|
||||||
)
|
)
|
||||||
const priceTierKey = computed(() =>
|
const rollingPoints = computed(() =>
|
||||||
String(overview.value?.priceTierKey ?? overview.value?.PriceTierKey ?? ''),
|
Number(
|
||||||
)
|
overview.value?.rollingPoints ??
|
||||||
const totalPoints = computed(() =>
|
overview.value?.RollingPoints ??
|
||||||
Number(overview.value?.totalPoints ?? overview.value?.TotalPoints ?? 0),
|
overview.value?.totalPoints ??
|
||||||
)
|
overview.value?.TotalPoints ??
|
||||||
const availablePoints = computed(() =>
|
0,
|
||||||
Number(overview.value?.availablePoints ?? overview.value?.AvailablePoints ?? 0),
|
),
|
||||||
)
|
)
|
||||||
const nextLevelName = computed(() =>
|
const nextLevelName = computed(() =>
|
||||||
String(overview.value?.nextLevelName ?? overview.value?.NextLevelName ?? ''),
|
String(overview.value?.nextLevelName ?? overview.value?.NextLevelName ?? ''),
|
||||||
@ -192,14 +187,17 @@ const extraDiscountRate = computed(() =>
|
|||||||
Number(overview.value?.extraDiscountRate ?? overview.value?.ExtraDiscountRate ?? 0),
|
Number(overview.value?.extraDiscountRate ?? overview.value?.ExtraDiscountRate ?? 0),
|
||||||
)
|
)
|
||||||
const extraDiscountText = computed(() => {
|
const extraDiscountText = computed(() => {
|
||||||
if (!extraDiscountRate.value) return ''
|
if (!extraDiscountRate.value) return t('marketing.memberLevel.discount.original')
|
||||||
return `${(extraDiscountRate.value * 100).toFixed(1)}%`
|
return t('marketing.memberLevel.discount.fold', {
|
||||||
|
fold: Math.round((1 - extraDiscountRate.value) * 100),
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
const levelNotice = computed(() => t('marketing.portal.member.subtitle'))
|
||||||
|
|
||||||
const progressPercent = computed(() => {
|
const progressPercent = computed(() => {
|
||||||
if (!nextLevelName.value) return 100
|
if (!nextLevelName.value) return 100
|
||||||
const need = pointsToNext.value
|
const need = pointsToNext.value
|
||||||
const total = totalPoints.value
|
const total = rollingPoints.value
|
||||||
const target = total + need
|
const target = total + need
|
||||||
if (!target) return 100
|
if (!target) return 100
|
||||||
return Math.min(Math.round((total / target) * 100), 100)
|
return Math.min(Math.round((total / target) * 100), 100)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user