feat: H5 底栏与顾问体验、查价失败分组,以及下单/渠道与凭证加固
All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m51s
All checks were successful
Build and Deploy Front / deploy (push) Successful in 5m51s
- H5:底栏 Liquid Dock 改版(图标/FAB 提示/badge)、订单详情操作图标化、空态与顾问侧栏样式 - Nana/官网:欢迎区与运单上下文、MCP 卡片示例;顾问 API 按环境直连 admin 网关 - 下单:电商平台改为字典下拉;9610 指南免税侧文案对齐汇总申报 - 查价:失败原因抽取并按步骤分组展示 - 渠道:重量因素数值排序、费用项防重复、取整区间重叠校验;分区匹配维度/模式显示中文 - 用户/仓储:OpenAPI 凭证仅客户门户主客户可见;打印控件下载链接抽公共常量并提示 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
1026d909e9
commit
2a4251f9db
@ -620,7 +620,7 @@
|
|||||||
<span class="sol-badge">合规推荐</span>
|
<span class="sol-badge">合规推荐</span>
|
||||||
</div>
|
</div>
|
||||||
<ul class="sol-list">
|
<ul class="sol-list">
|
||||||
<li>报关方式:简易 / 汇总均可</li>
|
<li>报关方式:汇总申报(正式报关单)</li>
|
||||||
<li>HS编码:前4位即可</li>
|
<li>HS编码:前4位即可</li>
|
||||||
<li>核心凭证:综服平台免税登记记录</li>
|
<li>核心凭证:综服平台免税登记记录</li>
|
||||||
<li>操作平台:单一窗口 + 综服平台</li>
|
<li>操作平台:单一窗口 + 综服平台</li>
|
||||||
@ -677,7 +677,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>报关方式</td>
|
<td>报关方式</td>
|
||||||
<td class="col-refund">汇总申报</td>
|
<td class="col-refund">汇总申报</td>
|
||||||
<td class="col-exempt">简易 / 汇总均可</td>
|
<td class="col-exempt">汇总申报(正式报关单)</td>
|
||||||
<td style="color:#5a6472;">统一简易申报</td>
|
<td style="color:#5a6472;">统一简易申报</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@ -707,10 +707,38 @@
|
|||||||
return window.location.origin;
|
return window.location.origin;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Admin API 根(方案 A:浏览器直连 api 网关,勿打前端 nginx SPA)。
|
||||||
|
* - prod:https://api.ideship.com/admin
|
||||||
|
* - test:https://api.test.ideship.com/admin
|
||||||
|
* - 本地 Vite 同源代理:与 resolveOmsOrigin 相同(无 /admin 前缀)
|
||||||
|
*/
|
||||||
|
function resolveOmsApiOrigin() {
|
||||||
|
const configured =
|
||||||
|
window.IDE_OMS_API_ORIGIN || localStorage.getItem('ide-oms-api-origin');
|
||||||
|
if (configured) return String(configured).replace(/\/$/, '');
|
||||||
|
|
||||||
|
const host = (window.location.hostname || '').toLowerCase();
|
||||||
|
if (host === 'test.ideship.com' || host.endsWith('.test.ideship.com')) {
|
||||||
|
return 'https://api.test.ideship.com/admin';
|
||||||
|
}
|
||||||
|
if (host === 'ideship.com' || host === 'www.ideship.com' || host.endsWith('.ideship.com')) {
|
||||||
|
return 'https://api.ideship.com/admin';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 本地 / 文件打开:走 OMS 开发源(Vite 代理 /channel/*)
|
||||||
|
return resolveOmsOrigin();
|
||||||
|
}
|
||||||
|
|
||||||
function omsUrl(pathname) {
|
function omsUrl(pathname) {
|
||||||
return `${resolveOmsOrigin()}${pathname}`;
|
return `${resolveOmsOrigin()}${pathname}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function omsApiUrl(pathname) {
|
||||||
|
const path = pathname.startsWith('/') ? pathname : `/${pathname}`;
|
||||||
|
return `${resolveOmsApiOrigin()}${path}`;
|
||||||
|
}
|
||||||
|
|
||||||
function loginTrackUrl(number) {
|
function loginTrackUrl(number) {
|
||||||
const target = preferOmsH5() ? '/h5/login' : '/login';
|
const target = preferOmsH5() ? '/h5/login' : '/login';
|
||||||
const redirect = preferOmsH5() ? '/h5/orders' : '/home/workplace';
|
const redirect = preferOmsH5() ? '/h5/orders' : '/home/workplace';
|
||||||
@ -2234,7 +2262,7 @@
|
|||||||
routesLoadState = 'loading';
|
routesLoadState = 'loading';
|
||||||
renderRoutesList();
|
renderRoutesList();
|
||||||
try {
|
try {
|
||||||
const res = await fetch(omsUrl('/channel/sales-product/portal/catalog'), {
|
const res = await fetch(omsApiUrl('/channel/sales-product/portal/catalog'), {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: { Accept: 'application/json' },
|
headers: { Accept: 'application/json' },
|
||||||
credentials: 'omit',
|
credentials: 'omit',
|
||||||
|
|||||||
9
src/assets/icons/h5/tab-me.svg
Normal file
9
src/assets/icons/h5/tab-me.svg
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<circle cx="12" cy="8" r="3.2" stroke="currentColor" stroke-width="1.8" />
|
||||||
|
<path
|
||||||
|
d="M5.5 20.2c.9-3.6 3.4-5.4 6.5-5.4s5.6 1.8 6.5 5.4"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.8"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 303 B |
@ -1,12 +1,17 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
<path
|
<rect
|
||||||
d="M4.5 8.2L12 4l7.5 4.2v8.6L12 21l-7.5-4.2V8.2z"
|
x="5"
|
||||||
|
y="3"
|
||||||
|
width="14"
|
||||||
|
height="18"
|
||||||
|
rx="2.2"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
stroke-width="1.8"
|
stroke-width="1.8"
|
||||||
stroke-linejoin="round"
|
|
||||||
/>
|
/>
|
||||||
<path d="M4.5 8.2L12 12.4l7.5-4.2" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round" />
|
<path
|
||||||
<path d="M12 12.4V21" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" />
|
d="M8.5 9h7M8.5 13h7M8.5 17h4.5"
|
||||||
<path d="M9.2 6.5l2.8 1.6 2.8-1.6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" />
|
stroke="currentColor"
|
||||||
<path d="M12 8.1v4" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" />
|
stroke-width="1.8"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 637 B After Width: | Height: | Size: 332 B |
20
src/assets/icons/h5/tab-service.svg
Normal file
20
src/assets/icons/h5/tab-service.svg
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<path
|
||||||
|
d="M4 14.5V12a8 8 0 0 1 16 0v2.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.8"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M4 14h2.8v5.2H4A1.6 1.6 0 0 1 2.4 17.6v-2A1.6 1.6 0 0 1 4 14z"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.8"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M20 14h-2.8v5.2H20a1.6 1.6 0 0 0 1.6-1.6v-2A1.6 1.6 0 0 0 20 14z"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.8"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 530 B |
@ -121,14 +121,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ref="mainScrollRef" class="ai-app-main">
|
<div ref="mainScrollRef" class="ai-app-main">
|
||||||
<section v-show="showWelcome" class="ai-welcome">
|
<section v-show="showWelcome && !waybillContext" class="ai-welcome">
|
||||||
<div class="ai-welcome__hero">
|
<div class="ai-welcome__hero">
|
||||||
<NanaAvatar class="ai-welcome__avatar" size="md" />
|
<NanaAvatar class="ai-welcome__avatar" size="sm" />
|
||||||
<h2>{{ t('nanaAssistant.welcomeTitle') }}</h2>
|
<div class="ai-welcome__copy">
|
||||||
<p>{{ t('nanaAssistant.welcomeSub') }}</p>
|
<h2>{{ welcomeHeadline }}</h2>
|
||||||
|
<p>{{ t('nanaAssistant.welcomeSub') }}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="ai-quick-section-label">{{ t('nanaAssistant.mcp.sectionLabel') }}</p>
|
|
||||||
<div class="ai-mcp-grid">
|
<div class="ai-mcp-grid">
|
||||||
<button
|
<button
|
||||||
v-for="cap in mcpCapabilities"
|
v-for="cap in mcpCapabilities"
|
||||||
@ -138,8 +139,53 @@
|
|||||||
:disabled="chatLoading"
|
:disabled="chatLoading"
|
||||||
@click="startMcpCapability(cap.id)"
|
@click="startMcpCapability(cap.id)"
|
||||||
>
|
>
|
||||||
<span class="ai-mcp-card__title">{{ t(cap.titleKey) }}</span>
|
<span class="ai-mcp-card__icon" aria-hidden="true">
|
||||||
<span class="ai-mcp-card__desc">{{ t(cap.descKey) }}</span>
|
<component :is="mcpCardIcon(cap.id)" />
|
||||||
|
</span>
|
||||||
|
<span class="ai-mcp-card__body">
|
||||||
|
<span class="ai-mcp-card__title">{{ t(cap.titleKey) }}</span>
|
||||||
|
<span class="ai-mcp-card__desc">{{ t(cap.exampleKey) }}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-show="showWelcome && waybillContext" class="ai-welcome ai-welcome--waybill">
|
||||||
|
<div class="ai-welcome__hero">
|
||||||
|
<NanaAvatar class="ai-welcome__avatar" size="sm" />
|
||||||
|
<div class="ai-welcome__copy">
|
||||||
|
<h2>{{ welcomeHeadline }}</h2>
|
||||||
|
<p>{{ t('nanaAssistant.waybillWelcomeSub', { no: waybillDisplayNo }) }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ai-mcp-grid ai-mcp-grid--waybill">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ai-mcp-card"
|
||||||
|
:disabled="chatLoading"
|
||||||
|
@click="askWaybillTrack"
|
||||||
|
>
|
||||||
|
<span class="ai-mcp-card__icon" aria-hidden="true">
|
||||||
|
<component :is="mcpCardIcon('track')" />
|
||||||
|
</span>
|
||||||
|
<span class="ai-mcp-card__body">
|
||||||
|
<span class="ai-mcp-card__title">{{ t('nanaAssistant.waybillAskTrack') }}</span>
|
||||||
|
<span class="ai-mcp-card__desc">{{ waybillDisplayNo }}</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ai-mcp-card"
|
||||||
|
:disabled="chatLoading"
|
||||||
|
@click="startMcpCapability('price')"
|
||||||
|
>
|
||||||
|
<span class="ai-mcp-card__icon" aria-hidden="true">
|
||||||
|
<component :is="mcpCardIcon('price')" />
|
||||||
|
</span>
|
||||||
|
<span class="ai-mcp-card__body">
|
||||||
|
<span class="ai-mcp-card__title">{{ t('nanaAssistant.waybillAskFee') }}</span>
|
||||||
|
<span class="ai-mcp-card__desc">{{ t('nanaAssistant.mcp.priceExample') }}</span>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -196,7 +242,7 @@
|
|||||||
:aria-label="t('nanaAssistant.send')"
|
:aria-label="t('nanaAssistant.send')"
|
||||||
:disabled="!canSend"
|
:disabled="!canSend"
|
||||||
>
|
>
|
||||||
<icon-arrow-up />
|
<icon-send />
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="ai-composer-footer">
|
<div class="ai-composer-footer">
|
||||||
@ -268,12 +314,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
import { computed, nextTick, onMounted, onUnmounted, ref, watch, type Component } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
import {
|
import {
|
||||||
IconArrowUp,
|
IconSend,
|
||||||
IconClockCircle,
|
IconClockCircle,
|
||||||
IconCustomerService,
|
IconCustomerService,
|
||||||
IconDelete,
|
IconDelete,
|
||||||
@ -284,6 +330,7 @@ import {
|
|||||||
IconMessage,
|
IconMessage,
|
||||||
IconPlus,
|
IconPlus,
|
||||||
IconPublic,
|
IconPublic,
|
||||||
|
IconSafe,
|
||||||
IconStorage,
|
IconStorage,
|
||||||
IconUser,
|
IconUser,
|
||||||
IconWifi,
|
IconWifi,
|
||||||
@ -303,6 +350,8 @@ import {
|
|||||||
parseWaybillContextFromQuery,
|
parseWaybillContextFromQuery,
|
||||||
type WaybillContext,
|
type WaybillContext,
|
||||||
} from '@/composables/useAskAdvisor';
|
} from '@/composables/useAskAdvisor';
|
||||||
|
import IconTabCalculator from '@/assets/icons/h5/tab-calculator.svg';
|
||||||
|
import IconTabPackage from '@/assets/icons/h5/tab-package.svg';
|
||||||
import {
|
import {
|
||||||
MCP_CAPABILITY_GUIDES,
|
MCP_CAPABILITY_GUIDES,
|
||||||
detectMissingFields,
|
detectMissingFields,
|
||||||
@ -377,6 +426,16 @@ const guidedSession = ref(false);
|
|||||||
const threads = ref<Thread[]>([]);
|
const threads = ref<Thread[]>([]);
|
||||||
|
|
||||||
const mcpCapabilities = MCP_CAPABILITY_GUIDES;
|
const mcpCapabilities = MCP_CAPABILITY_GUIDES;
|
||||||
|
|
||||||
|
const mcpCardIcons: Record<McpCapabilityId, Component> = {
|
||||||
|
price: IconTabCalculator,
|
||||||
|
guide: IconSafe,
|
||||||
|
track: IconTabPackage,
|
||||||
|
};
|
||||||
|
|
||||||
|
function mcpCardIcon(id: McpCapabilityId) {
|
||||||
|
return mcpCardIcons[id];
|
||||||
|
}
|
||||||
const activeThreadId = ref<string | null>(null);
|
const activeThreadId = ref<string | null>(null);
|
||||||
const messages = ref<ChatMessage[]>([]);
|
const messages = ref<ChatMessage[]>([]);
|
||||||
const mainScrollRef = ref<HTMLElement>();
|
const mainScrollRef = ref<HTMLElement>();
|
||||||
@ -445,7 +504,20 @@ const isChatting = computed(
|
|||||||
() => messages.value.some((m) => m.role === 'user') || guidedSession.value,
|
() => messages.value.some((m) => m.role === 'user') || guidedSession.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
const showWelcome = computed(() => !isChatting.value && !waybillContext.value);
|
const showWelcome = computed(() => !isChatting.value);
|
||||||
|
|
||||||
|
const welcomeGreet = computed(() => {
|
||||||
|
const hour = new Date().getHours();
|
||||||
|
if (hour < 6) return t('nanaAssistant.greet.night');
|
||||||
|
if (hour < 12) return t('nanaAssistant.greet.morning');
|
||||||
|
if (hour < 14) return t('nanaAssistant.greet.noon');
|
||||||
|
if (hour < 18) return t('nanaAssistant.greet.afternoon');
|
||||||
|
return t('nanaAssistant.greet.evening');
|
||||||
|
});
|
||||||
|
|
||||||
|
const welcomeHeadline = computed(() =>
|
||||||
|
t('nanaAssistant.welcomeHello', { greet: welcomeGreet.value }),
|
||||||
|
);
|
||||||
|
|
||||||
const canSend = computed(() => Boolean(inputText.value.trim() && !chatLoading.value));
|
const canSend = computed(() => Boolean(inputText.value.trim() && !chatLoading.value));
|
||||||
|
|
||||||
@ -876,7 +948,7 @@ async function startMcpCapability(id: McpCapabilityId) {
|
|||||||
messages.value.push({ role: 'assistant', content: coach });
|
messages.value.push({ role: 'assistant', content: coach });
|
||||||
guidedSession.value = true;
|
guidedSession.value = true;
|
||||||
saveThreads();
|
saveThreads();
|
||||||
inputText.value = '';
|
inputText.value = t(cap.exampleKey);
|
||||||
await nextTick();
|
await nextTick();
|
||||||
resizeInput();
|
resizeInput();
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
|
|||||||
@ -691,12 +691,40 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ai-welcome__hero {
|
.ai-welcome__hero {
|
||||||
text-align: center;
|
display: flex;
|
||||||
padding: 24px 12px 28px;
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
text-align: left;
|
||||||
|
padding: 12px 4px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-welcome__avatar {
|
.ai-welcome__avatar.nana-fab-wrap,
|
||||||
margin: 0 auto 16px;
|
.ai-welcome__avatar.nana-fab-wrap--sm {
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin: 0;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-welcome__copy {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-welcome__hero h2 {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
font-size: clamp(1.15rem, 3.6vw, 1.4rem);
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
color: var(--kc-primary, #1b2a6b);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-welcome__hero p {
|
||||||
|
margin: 0;
|
||||||
|
max-width: none;
|
||||||
|
font-size: 0.88rem;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--color-text-3, #86909c);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-avatar {
|
.ai-avatar {
|
||||||
@ -713,22 +741,6 @@
|
|||||||
box-shadow: 0 8px 24px rgba(27, 42, 107, 0.2);
|
box-shadow: 0 8px 24px rgba(27, 42, 107, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-welcome__hero h2 {
|
|
||||||
margin: 0 0 8px;
|
|
||||||
font-size: clamp(1.35rem, 4vw, 1.65rem);
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: -0.02em;
|
|
||||||
color: var(--kc-primary, #1b2a6b);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-welcome__hero p {
|
|
||||||
margin: 0 auto;
|
|
||||||
max-width: 420px;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
line-height: 1.55;
|
|
||||||
color: var(--kc-secondary, #6a75b8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-quick-section-label {
|
.ai-quick-section-label {
|
||||||
margin: 0 0 12px;
|
margin: 0 0 12px;
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
@ -748,35 +760,39 @@
|
|||||||
|
|
||||||
.ai-mcp-grid {
|
.ai-mcp-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
.ai-mcp-grid--waybill {
|
||||||
.ai-mcp-grid {
|
grid-template-columns: 1fr 1fr;
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-mcp-card {
|
.ai-mcp-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
gap: 4px;
|
justify-content: flex-start;
|
||||||
padding: 14px 12px;
|
gap: 10px;
|
||||||
|
min-height: 92px;
|
||||||
|
padding: 16px 12px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
border-radius: 14px;
|
border-radius: 12px;
|
||||||
border: none;
|
border: 1px solid color-mix(in srgb, var(--color-text-1, #1d2129) 10%, transparent);
|
||||||
background: color-mix(in srgb, var(--app-primary-color, #007979) 8%, var(--app-card-bg));
|
background: transparent;
|
||||||
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.04);
|
box-shadow: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 0.15s ease, box-shadow 0.15s ease;
|
transition: opacity 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-mcp-card:hover {
|
.ai-mcp-card:hover {
|
||||||
transform: translateY(-1px);
|
transform: none;
|
||||||
box-shadow: 0 6px 16px color-mix(in srgb, var(--app-primary-color, #007979) 16%, transparent);
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-mcp-card:active:not(:disabled) {
|
||||||
|
opacity: 0.72;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-mcp-card:disabled {
|
.ai-mcp-card:disabled {
|
||||||
@ -785,6 +801,31 @@
|
|||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ai-mcp-card__icon {
|
||||||
|
flex: 0 0 28px;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--app-primary-color, #007979);
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-mcp-card__icon svg,
|
||||||
|
.ai-mcp-card__icon .arco-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ai-mcp-card__body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.ai-mcp-card__title {
|
.ai-mcp-card__title {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@ -799,7 +840,7 @@
|
|||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.ai-mcp-grid {
|
.ai-mcp-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1199,7 +1240,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ai-welcome__hero {
|
.ai-welcome__hero {
|
||||||
padding: 16px 0 20px;
|
padding: 8px 0 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ai-avatar {
|
.ai-avatar {
|
||||||
|
|||||||
@ -284,7 +284,7 @@ import { canAccessCustomerPortalRoute, isAdminPortalOnlyPath, isCustomerPortalOn
|
|||||||
import { resolveCurrentCustomer } from '@/api/customer/resolve';
|
import { resolveCurrentCustomer } from '@/api/customer/resolve';
|
||||||
import { CUSTOMER_BALANCE_CHANGED_EVENT } from '@/api/customer/accountFlow';
|
import { CUSTOMER_BALANCE_CHANGED_EVENT } from '@/api/customer/accountFlow';
|
||||||
import { resolvePortalDrawerPanel } from '@/views/user/portal-drawer/types';
|
import { resolvePortalDrawerPanel } from '@/views/user/portal-drawer/types';
|
||||||
import { openPortalSideDrawer } from '@/views/user/portal-drawer/usePortalSideDrawer';
|
import { PRINT_CONTROL_DOWNLOAD_URL } from '@/constants/printControl';
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
@ -297,9 +297,6 @@ const topMenuCon = ref<HTMLElement | null>(null);
|
|||||||
const topNavEnable = computed(() => true); // 启用顶部导航
|
const topNavEnable = computed(() => true); // 启用顶部导航
|
||||||
const tabNavEnable = computed(() => appStore.tabBar);
|
const tabNavEnable = computed(() => appStore.tabBar);
|
||||||
const printConnectionState = useHiprintClientState();
|
const printConnectionState = useHiprintClientState();
|
||||||
/** 打印控件安装包(百度网盘,提取码 idex) */
|
|
||||||
const PRINT_CONTROL_DOWNLOAD_URL =
|
|
||||||
'https://pan.baidu.com/s/1FrpI1prvtRKG1f9jXYSL1A?pwd=idex';
|
|
||||||
|
|
||||||
const menuItemEls = new Map<string, HTMLElement>();
|
const menuItemEls = new Map<string, HTMLElement>();
|
||||||
const indicatorReady = ref(false);
|
const indicatorReady = ref(false);
|
||||||
|
|||||||
3
src/constants/printControl.ts
Normal file
3
src/constants/printControl.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/** 打印控件 / 一键添加包裹插件安装包(百度网盘,提取码 idex) */
|
||||||
|
export const PRINT_CONTROL_DOWNLOAD_URL =
|
||||||
|
'https://pan.baidu.com/s/1FrpI1prvtRKG1f9jXYSL1A?pwd=idex'
|
||||||
@ -24,6 +24,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.warehouse-list-page-card .status-tabs {
|
.warehouse-list-page-card .status-tabs {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
padding: 10px 16px;
|
padding: 10px 16px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
border-bottom: 1px solid var(--color-border-2);
|
border-bottom: 1px solid var(--color-border-2);
|
||||||
|
|||||||
@ -73,15 +73,29 @@ export function weightValueToIntervalCell(
|
|||||||
return formatWeightIntervalLeftOpenRightClosed(start, upper);
|
return formatWeightIntervalLeftOpenRightClosed(start, upper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function factorValueSortNumber(v: string): number | null {
|
||||||
|
const parsed = parseWeightIntervalBounds(v);
|
||||||
|
if (parsed) return parsed.end;
|
||||||
|
const n = Number.parseFloat(String(v).trim());
|
||||||
|
return Number.isFinite(n) ? n : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 按数值大小升序排列因素值(如 2, 0.5, 1 → 0.5, 1, 2)。 */
|
||||||
|
export function sortFactorValuesBySize(values: string[] | undefined): string[] {
|
||||||
|
return [...(values ?? [])].sort((a, b) => {
|
||||||
|
const na = factorValueSortNumber(String(a));
|
||||||
|
const nb = factorValueSortNumber(String(b));
|
||||||
|
if (na != null && nb != null && na !== nb) return na - nb;
|
||||||
|
if (na != null && nb == null) return -1;
|
||||||
|
if (na == null && nb != null) return 1;
|
||||||
|
return String(a).localeCompare(String(b), undefined, { numeric: true, sensitivity: 'base' });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** 提取因素值列表中的重量上限并升序排列(用于区间左界)。 */
|
/** 提取因素值列表中的重量上限并升序排列(用于区间左界)。 */
|
||||||
export function weightFactorValuesToSortedUpperBounds(values: string[]): number[] {
|
export function weightFactorValuesToSortedUpperBounds(values: string[]): number[] {
|
||||||
const nums = (values ?? [])
|
const nums = (values ?? [])
|
||||||
.map((v) => {
|
.map((v) => factorValueSortNumber(v))
|
||||||
const parsed = parseWeightIntervalBounds(v);
|
|
||||||
if (parsed) return parsed.end;
|
|
||||||
const n = Number.parseFloat(String(v).trim());
|
|
||||||
return Number.isFinite(n) ? n : null;
|
|
||||||
})
|
|
||||||
.filter((n): n is number => n != null);
|
.filter((n): n is number => n != null);
|
||||||
return [...new Set(nums)].sort((a, b) => a - b);
|
return [...new Set(nums)].sort((a, b) => a - b);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -551,10 +551,24 @@ export function useKpiAreaPage(areaType: KpiAreaType) {
|
|||||||
|
|
||||||
const selectedAreaName = computed(() => currentId.value?.name ?? '');
|
const selectedAreaName = computed(() => currentId.value?.name ?? '');
|
||||||
|
|
||||||
|
function formatMatchDim(code?: string) {
|
||||||
|
const dim = String(code || '').trim().toLowerCase();
|
||||||
|
const key = `channel.area.matchDim.${dim}`;
|
||||||
|
const label = t(key);
|
||||||
|
return label === key ? dim : label;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMatchMode(code?: string) {
|
||||||
|
const mode = String(code || '').trim().toLowerCase();
|
||||||
|
const key = `channel.area.matchMode.${mode}`;
|
||||||
|
const label = t(key);
|
||||||
|
return label === key ? mode : label;
|
||||||
|
}
|
||||||
|
|
||||||
const hotColumns = computed<ChannelHotColumn[]>(() => [
|
const hotColumns = computed<ChannelHotColumn[]>(() => [
|
||||||
{ key: 'countryName', label: t('channel.area.form.country'), width: 120 },
|
{ key: 'countryName', label: t('channel.area.form.country'), width: 120 },
|
||||||
{ key: 'matchDim', label: t('channel.area.form.matchDim'), width: 100 },
|
{ key: 'matchDimName', label: t('channel.area.form.matchDim'), width: 100 },
|
||||||
{ key: 'matchMode', label: t('channel.area.form.matchMode'), width: 100 },
|
{ key: 'matchModeName', label: t('channel.area.form.matchMode'), width: 100 },
|
||||||
{ key: 'province', label: t('channel.area.form.province'), width: 120 },
|
{ key: 'province', label: t('channel.area.form.province'), width: 120 },
|
||||||
{ key: 'city', label: t('channel.area.form.city'), width: 120 },
|
{ key: 'city', label: t('channel.area.form.city'), width: 120 },
|
||||||
{ key: 'leftPostcode', label: t('channel.area.table.leftPostcode'), width: 120 },
|
{ key: 'leftPostcode', label: t('channel.area.table.leftPostcode'), width: 120 },
|
||||||
@ -732,7 +746,18 @@ export function useKpiAreaPage(areaType: KpiAreaType) {
|
|||||||
totalCount.value = Number(
|
totalCount.value = Number(
|
||||||
response?.totalCount ?? response?.total ?? items.length ?? 0
|
response?.totalCount ?? response?.total ?? items.length ?? 0
|
||||||
);
|
);
|
||||||
tableData.value = items;
|
tableData.value = items.map((item) => {
|
||||||
|
const inferred = inferGeoMatch(item);
|
||||||
|
const matchDim = item.matchDim || inferred.matchDim;
|
||||||
|
const matchMode = item.matchMode || inferred.matchMode;
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
matchDim,
|
||||||
|
matchMode,
|
||||||
|
matchDimName: formatMatchDim(matchDim),
|
||||||
|
matchModeName: formatMatchMode(matchMode),
|
||||||
|
};
|
||||||
|
});
|
||||||
} catch {
|
} catch {
|
||||||
tableData.value = [];
|
tableData.value = [];
|
||||||
totalCount.value = 0;
|
totalCount.value = 0;
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
createSalesProduct,
|
createSalesProduct,
|
||||||
createSalesProductPrice,
|
createSalesProductPrice,
|
||||||
deleteSalesProduct,
|
deleteSalesProduct,
|
||||||
|
deleteSalesProductPrice,
|
||||||
getSalesProductDetail,
|
getSalesProductDetail,
|
||||||
getSalesProductList,
|
getSalesProductList,
|
||||||
getSalesProductAllocation,
|
getSalesProductAllocation,
|
||||||
@ -29,6 +30,7 @@
|
|||||||
import {
|
import {
|
||||||
canonicalWeightFactorCellForApi,
|
canonicalWeightFactorCellForApi,
|
||||||
isWeightPricingFactor,
|
isWeightPricingFactor,
|
||||||
|
sortFactorValuesBySize,
|
||||||
weightValueToIntervalCell,
|
weightValueToIntervalCell,
|
||||||
} from '@/utils/priceWeightInterval';
|
} from '@/utils/priceWeightInterval';
|
||||||
import { IconPlus, IconDelete, IconSearch, IconEdit, IconDragArrow, IconQuestionCircleFill, IconQuestionCircle } from '@arco-design/web-vue/es/icon';
|
import { IconPlus, IconDelete, IconSearch, IconEdit, IconDragArrow, IconQuestionCircleFill, IconQuestionCircle } from '@arco-design/web-vue/es/icon';
|
||||||
@ -768,7 +770,11 @@
|
|||||||
});
|
});
|
||||||
return normalizeCustomerFactorSelection(mapped);
|
return normalizeCustomerFactorSelection(mapped);
|
||||||
}
|
}
|
||||||
return vs.map((v) => String(v));
|
const mapped = vs.map((v) => String(v));
|
||||||
|
if (isWeightValueFactor(factor)) {
|
||||||
|
return sortFactorValuesBySize(mapped);
|
||||||
|
}
|
||||||
|
return mapped;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 价格表客户组列表头:与写死全量字典列一致,用 customer_group 的 dictName;入参为勾选的因素编码 */
|
/** 价格表客户组列表头:与写死全量字典列一致,用 customer_group 的 dictName;入参为勾选的因素编码 */
|
||||||
@ -2216,6 +2222,17 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const costItemOptions = computed(() => mapDictToSelectOptions('fee_item'));
|
const costItemOptions = computed(() => mapDictToSelectOptions('fee_item'));
|
||||||
|
|
||||||
|
/** 同一产品下已占用的费用项(编辑当前条时排除自身) */
|
||||||
|
function isCostItemAlreadyUsed(costItem: string) {
|
||||||
|
const code = String(costItem ?? '').trim().toLowerCase();
|
||||||
|
if (!code) return false;
|
||||||
|
const editingId = isEditMode.value ? editingPriceRecord.value?.id : null;
|
||||||
|
return priceTableData.value.some((row: any) => {
|
||||||
|
if (editingId != null && row?.id === editingId) return false;
|
||||||
|
return String(row?.costItem ?? '').trim().toLowerCase() === code;
|
||||||
|
});
|
||||||
|
}
|
||||||
const costUnitOptions = computed(() => mapDictToSelectOptions('cost_unit'));
|
const costUnitOptions = computed(() => mapDictToSelectOptions('cost_unit'));
|
||||||
const currencyOptions = computed(() => mapDictToSelectOptions('currency'));
|
const currencyOptions = computed(() => mapDictToSelectOptions('currency'));
|
||||||
const factorOptions = computed(() => {
|
const factorOptions = computed(() => {
|
||||||
@ -2389,7 +2406,9 @@
|
|||||||
addFactorValueSelected.value = normalizeCustomerFactorSelection(vals || []);
|
addFactorValueSelected.value = normalizeCustomerFactorSelection(vals || []);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
addFactorValueSelected.value = vals || [];
|
const next = vals || [];
|
||||||
|
addFactorValueSelected.value =
|
||||||
|
factor && isWeightValueFactor(factor) ? sortFactorValuesBySize(next) : next;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加因素值
|
// 添加因素值
|
||||||
@ -2410,9 +2429,12 @@
|
|||||||
}
|
}
|
||||||
const allowed = new Set(opts.map((o) => String(o.value)));
|
const allowed = new Set(opts.map((o) => String(o.value)));
|
||||||
const canonical = toCanonicalFactorValues(factor, row.factorValues || []);
|
const canonical = toCanonicalFactorValues(factor, row.factorValues || []);
|
||||||
addFactorValueSelected.value = canonical
|
const selected = canonical
|
||||||
.map((v) => String(v).trim())
|
.map((v) => String(v).trim())
|
||||||
.filter((v) => v && allowed.has(v));
|
.filter((v) => v && allowed.has(v));
|
||||||
|
addFactorValueSelected.value = isWeightValueFactor(factor)
|
||||||
|
? sortFactorValuesBySize(selected)
|
||||||
|
: selected;
|
||||||
addFactorValueModalVisible.value = true;
|
addFactorValueModalVisible.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -2464,6 +2486,11 @@
|
|||||||
currentFactorRow.value.factorValues,
|
currentFactorRow.value.factorValues,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (factor && isWeightValueFactor(factor)) {
|
||||||
|
currentFactorRow.value.factorValues = sortFactorValuesBySize(
|
||||||
|
currentFactorRow.value.factorValues,
|
||||||
|
);
|
||||||
|
}
|
||||||
Message.success('添加成功');
|
Message.success('添加成功');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -2499,7 +2526,9 @@
|
|||||||
addFactorValueSelected.value =
|
addFactorValueSelected.value =
|
||||||
factor && isCustomerGroupValueFactor(factor)
|
factor && isCustomerGroupValueFactor(factor)
|
||||||
? normalizeCustomerFactorSelection(enabled.filter((v) => !isAllCustomerCode(v)))
|
? normalizeCustomerFactorSelection(enabled.filter((v) => !isAllCustomerCode(v)))
|
||||||
: enabled;
|
: factor && isWeightValueFactor(factor)
|
||||||
|
? sortFactorValuesBySize(enabled)
|
||||||
|
: enabled;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 拖拽排序相关
|
// 拖拽排序相关
|
||||||
@ -2929,6 +2958,10 @@
|
|||||||
Message.warning('请先填写费用项');
|
Message.warning('请先填写费用项');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (isCostItemAlreadyUsed(basicInfoForm.costItem)) {
|
||||||
|
Message.warning('该产品已存在此费用项,同一产品下每种费用项只能有一条');
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const msg = await submitPriceConfig(pid);
|
const msg = await submitPriceConfig(pid);
|
||||||
Message.success(msg);
|
Message.success(msg);
|
||||||
@ -3362,32 +3395,40 @@
|
|||||||
deletePriceModalVisible.value = true;
|
deletePriceModalVisible.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 确认删除价格配置
|
// 确认删除价格配置(落库 POST /channel/sales-product/price/delete)
|
||||||
const handleConfirmDeletePrice = () => {
|
const handleConfirmDeletePrice = async () => {
|
||||||
if (!deletingPriceRecord.value) return;
|
if (!deletingPriceRecord.value) return;
|
||||||
|
|
||||||
const correctName = deletingPriceRecord.value.priceName || deletingPriceRecord.value.costItem || '';
|
const record = deletingPriceRecord.value;
|
||||||
if (deletePriceInput.value !== correctName) {
|
const typed = String(deletePriceInput.value ?? '').trim();
|
||||||
|
const correctNames = [
|
||||||
|
record.priceName,
|
||||||
|
record.costItem,
|
||||||
|
dictLabelByType('fee_item', record.costItem),
|
||||||
|
]
|
||||||
|
.map((x) => String(x ?? '').trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
if (!correctNames.includes(typed)) {
|
||||||
Message.error('输入的费用项名称不正确,请重新输入');
|
Message.error('输入的费用项名称不正确,请重新输入');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从列表中删除
|
const productId = selectedProduct.value;
|
||||||
const index = priceTableData.value.findIndex((item) => item.id === deletingPriceRecord.value.id);
|
const priceId = record.id;
|
||||||
if (index > -1) {
|
if (priceId == null) {
|
||||||
priceTableData.value.splice(index, 1);
|
|
||||||
// 重新编号
|
|
||||||
priceTableData.value.forEach((item, idx) => {
|
|
||||||
item.serialNumber = idx + 1;
|
|
||||||
});
|
|
||||||
Message.success('删除成功');
|
|
||||||
} else {
|
|
||||||
Message.error('删除失败,未找到该记录');
|
Message.error('删除失败,未找到该记录');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await deleteSalesProductPrice(priceId);
|
||||||
|
Message.success('删除成功');
|
||||||
|
deletePriceModalVisible.value = false;
|
||||||
|
deletingPriceRecord.value = null;
|
||||||
|
deletePriceInput.value = '';
|
||||||
|
if (productId != null) await loadPriceTabData(productId);
|
||||||
|
} catch (e: any) {
|
||||||
|
Message.error(e?.message || '删除失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
deletePriceModalVisible.value = false;
|
|
||||||
deletingPriceRecord.value = null;
|
|
||||||
deletePriceInput.value = '';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 取消删除价格配置
|
// 取消删除价格配置
|
||||||
@ -4314,6 +4355,7 @@
|
|||||||
v-for="option in costItemOptions"
|
v-for="option in costItemOptions"
|
||||||
:key="option.value"
|
:key="option.value"
|
||||||
:value="option.value"
|
:value="option.value"
|
||||||
|
:disabled="isCostItemAlreadyUsed(option.value)"
|
||||||
>
|
>
|
||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
</a-option>
|
</a-option>
|
||||||
|
|||||||
@ -525,6 +525,7 @@
|
|||||||
v-for="option in costItemOptions"
|
v-for="option in costItemOptions"
|
||||||
:key="option.value"
|
:key="option.value"
|
||||||
:value="option.value"
|
:value="option.value"
|
||||||
|
:disabled="isCostItemAlreadyUsed(option.value)"
|
||||||
>
|
>
|
||||||
{{ option.label }}
|
{{ option.label }}
|
||||||
</a-option>
|
</a-option>
|
||||||
@ -1324,13 +1325,14 @@
|
|||||||
</a-button>
|
</a-button>
|
||||||
</div>
|
</div>
|
||||||
<a-select
|
<a-select
|
||||||
v-model="addFactorValueSelected"
|
:model-value="addFactorValueSelected"
|
||||||
:options="currentFactorValueOptions"
|
:options="currentFactorValueOptions"
|
||||||
:placeholder="currentFactorValueOptions.length ? '请选择因素值' : '该因素暂无可选值'"
|
:placeholder="currentFactorValueOptions.length ? '请选择因素值' : '该因素暂无可选值'"
|
||||||
multiple
|
multiple
|
||||||
allow-clear
|
allow-clear
|
||||||
allow-search
|
allow-search
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
|
@change="handleAddFactorValueSelectedChange"
|
||||||
@press-enter="handleConfirmAddFactorValue"
|
@press-enter="handleConfirmAddFactorValue"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -1441,6 +1443,7 @@ import Sortable from 'sortablejs'
|
|||||||
import {
|
import {
|
||||||
canonicalWeightFactorCellForApi,
|
canonicalWeightFactorCellForApi,
|
||||||
isWeightPricingFactor,
|
isWeightPricingFactor,
|
||||||
|
sortFactorValuesBySize,
|
||||||
weightValueToIntervalCell,
|
weightValueToIntervalCell,
|
||||||
} from '@/utils/priceWeightInterval'
|
} from '@/utils/priceWeightInterval'
|
||||||
import { getAppGetList } from '@/api/ApplicationServices/apps'
|
import { getAppGetList } from '@/api/ApplicationServices/apps'
|
||||||
@ -1712,7 +1715,11 @@ function toCanonicalFactorValues(factor: string, values: string[] | undefined):
|
|||||||
return hit?.dictCode ?? s
|
return hit?.dictCode ?? s
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return vs.map((v) => String(v))
|
const mapped = vs.map((v) => String(v))
|
||||||
|
if (isWeightValueFactor(factor)) {
|
||||||
|
return sortFactorValuesBySize(mapped)
|
||||||
|
}
|
||||||
|
return mapped
|
||||||
}
|
}
|
||||||
|
|
||||||
function dictFactorDisplayNameOnly(dictType: string, stored: string): string {
|
function dictFactorDisplayNameOnly(dictType: string, stored: string): string {
|
||||||
@ -3221,6 +3228,10 @@ const handleSavePriceList = async () => {
|
|||||||
Message.warning('请先填写费用项')
|
Message.warning('请先填写费用项')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (isCostItemAlreadyUsed(basicInfoForm.costItem)) {
|
||||||
|
Message.warning('该渠道已存在此费用项,同一渠道下每种费用项只能有一条')
|
||||||
|
return
|
||||||
|
}
|
||||||
const cid = (selectedChannel.value as any)?.id
|
const cid = (selectedChannel.value as any)?.id
|
||||||
if (cid == null) {
|
if (cid == null) {
|
||||||
Message.warning('请先选择渠道')
|
Message.warning('请先选择渠道')
|
||||||
@ -4066,6 +4077,17 @@ const factorOptions = computed(() =>
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
const costItemOptions = computed(() => mapDictToSelectOptions('fee_item'))
|
const costItemOptions = computed(() => mapDictToSelectOptions('fee_item'))
|
||||||
|
|
||||||
|
/** 同一渠道下已占用的费用项(编辑当前条时排除自身) */
|
||||||
|
function isCostItemAlreadyUsed(costItem: string) {
|
||||||
|
const code = String(costItem ?? '').trim().toLowerCase()
|
||||||
|
if (!code) return false
|
||||||
|
const editingId = isEditMode.value ? editingPriceRecord.value?.id : null
|
||||||
|
return priceTableData.value.some((row: any) => {
|
||||||
|
if (editingId != null && row?.id === editingId) return false
|
||||||
|
return String(row?.costItem ?? '').trim().toLowerCase() === code
|
||||||
|
})
|
||||||
|
}
|
||||||
const costUnitOptions = computed(() => mapDictToSelectOptions('cost_unit'))
|
const costUnitOptions = computed(() => mapDictToSelectOptions('cost_unit'))
|
||||||
const currencyOptions = computed(() => mapDictToSelectOptions('currency'))
|
const currencyOptions = computed(() => mapDictToSelectOptions('currency'))
|
||||||
const calculationFormulaOptions = computed(() => mapDictToSelectOptions('calculation_formula'))
|
const calculationFormulaOptions = computed(() => mapDictToSelectOptions('calculation_formula'))
|
||||||
@ -4117,6 +4139,13 @@ const isAllFactorValuesSelected = computed(() => {
|
|||||||
return options.every((option) => addFactorValueSelected.value.includes(option.value))
|
return options.every((option) => addFactorValueSelected.value.includes(option.value))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
function handleAddFactorValueSelectedChange(vals: string[]) {
|
||||||
|
const factor = currentFactorRow.value?.factor
|
||||||
|
const next = vals || []
|
||||||
|
addFactorValueSelected.value =
|
||||||
|
factor && isWeightValueFactor(factor) ? sortFactorValuesBySize(next) : next
|
||||||
|
}
|
||||||
|
|
||||||
const handleAddFactorValue = async (row: any) => {
|
const handleAddFactorValue = async (row: any) => {
|
||||||
if (!row?.factor) {
|
if (!row?.factor) {
|
||||||
Message.warning('请先选择因素')
|
Message.warning('请先选择因素')
|
||||||
@ -4129,9 +4158,12 @@ const handleAddFactorValue = async (row: any) => {
|
|||||||
const opts = factorValuesOptionsByFactor.value[factor] || []
|
const opts = factorValuesOptionsByFactor.value[factor] || []
|
||||||
const allowed = new Set(opts.map((o) => String(o.value)))
|
const allowed = new Set(opts.map((o) => String(o.value)))
|
||||||
const canonical = toCanonicalFactorValues(factor, row.factorValues || [])
|
const canonical = toCanonicalFactorValues(factor, row.factorValues || [])
|
||||||
addFactorValueSelected.value = canonical
|
const selected = canonical
|
||||||
.map((v) => String(v).trim())
|
.map((v) => String(v).trim())
|
||||||
.filter((v) => v && allowed.has(v))
|
.filter((v) => v && allowed.has(v))
|
||||||
|
addFactorValueSelected.value = isWeightValueFactor(factor)
|
||||||
|
? sortFactorValuesBySize(selected)
|
||||||
|
: selected
|
||||||
addFactorValueModalVisible.value = true
|
addFactorValueModalVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4160,6 +4192,11 @@ function commitAddFactorSelections(opts: { forbidEmpty: boolean }): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
currentFactorRow.value.factorValues.push(...toAdd)
|
currentFactorRow.value.factorValues.push(...toAdd)
|
||||||
|
if (isWeightValueFactor(currentFactorRow.value.factor)) {
|
||||||
|
currentFactorRow.value.factorValues = sortFactorValuesBySize(
|
||||||
|
currentFactorRow.value.factorValues,
|
||||||
|
)
|
||||||
|
}
|
||||||
Message.success('添加成功')
|
Message.success('添加成功')
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -4186,7 +4223,10 @@ const handleSelectAllFactorValues = () => {
|
|||||||
addFactorValueSelected.value = []
|
addFactorValueSelected.value = []
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
addFactorValueSelected.value = currentFactorValueOptions.value.map((option) => option.value)
|
const enabled = currentFactorValueOptions.value.map((option) => option.value)
|
||||||
|
const factor = currentFactorRow.value?.factor
|
||||||
|
addFactorValueSelected.value =
|
||||||
|
factor && isWeightValueFactor(factor) ? sortFactorValuesBySize(enabled) : enabled
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除因素值
|
// 删除因素值
|
||||||
|
|||||||
@ -180,6 +180,81 @@
|
|||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function rangesOverlap(startA: number, endA: number, startB: number, endB: number) {
|
||||||
|
return startA < endB && startB < endA;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findOverlappingRange(
|
||||||
|
startValue: number,
|
||||||
|
endValue: number,
|
||||||
|
rows: any[],
|
||||||
|
excludeId?: number
|
||||||
|
) {
|
||||||
|
return rows.find((row) => {
|
||||||
|
if (excludeId != null && Number(row?.id) === excludeId) return false;
|
||||||
|
const start = Number(row?.startValue);
|
||||||
|
const end = Number(row?.endValue);
|
||||||
|
if (!Number.isFinite(start) || !Number.isFinite(end)) return false;
|
||||||
|
return rangesOverlap(startValue, endValue, start, end);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadAllRoundingInfos(roundingId: number) {
|
||||||
|
const { items } = await postWeightRoundingInfoList({
|
||||||
|
roundingId,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 500,
|
||||||
|
});
|
||||||
|
return items || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function findRangeGap(
|
||||||
|
startValue: number,
|
||||||
|
endValue: number,
|
||||||
|
rows: any[],
|
||||||
|
excludeId?: number
|
||||||
|
) {
|
||||||
|
const merged = rows
|
||||||
|
.filter((row) => excludeId == null || Number(row?.id) !== excludeId)
|
||||||
|
.map((row) => ({ start: Number(row?.startValue), end: Number(row?.endValue) }))
|
||||||
|
.filter((row) => Number.isFinite(row.start) && Number.isFinite(row.end))
|
||||||
|
.concat([{ start: startValue, end: endValue }])
|
||||||
|
.sort((a, b) => a.start - b.start || a.end - b.end);
|
||||||
|
for (let i = 0; i < merged.length - 1; i++) {
|
||||||
|
if (merged[i].end !== merged[i + 1].start) {
|
||||||
|
return { left: merged[i], right: merged[i + 1] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assertRangeNoOverlap(form: Record<string, any>, startValue: number, endValue: number) {
|
||||||
|
const roundingId = Number(form.roundingId ?? editFormRightParams.roundingId ?? currentId.value?.id);
|
||||||
|
const excludeId = form.id != null && form.id !== '' ? Number(form.id) : undefined;
|
||||||
|
const exclude = Number.isFinite(excludeId) ? excludeId : undefined;
|
||||||
|
let rows = tableData.value;
|
||||||
|
if (roundingId > 0 && (totalCount.value > rows.length || rows.length === 0)) {
|
||||||
|
rows = await loadAllRoundingInfos(roundingId);
|
||||||
|
}
|
||||||
|
const hit = findOverlappingRange(startValue, endValue, rows, exclude);
|
||||||
|
if (hit) {
|
||||||
|
const message = t('channel.weightRounding.form.rules.rangeOverlap', {
|
||||||
|
current: `${startValue}~${endValue}`,
|
||||||
|
existed: `${hit.startValue}~${hit.endValue}`,
|
||||||
|
});
|
||||||
|
$baseMessage(message, 'warning');
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
const gap = findRangeGap(startValue, endValue, rows, exclude);
|
||||||
|
if (!gap) return;
|
||||||
|
const message = t('channel.weightRounding.form.rules.rangeGap', {
|
||||||
|
left: `${gap.left.start}~${gap.left.end}`,
|
||||||
|
right: `${gap.right.start}~${gap.right.end}`,
|
||||||
|
});
|
||||||
|
$baseMessage(message, 'warning');
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
const leftModalMode = ref<DialogType>('add');
|
const leftModalMode = ref<DialogType>('add');
|
||||||
const rightModalMode = ref<DialogType>('add');
|
const rightModalMode = ref<DialogType>('add');
|
||||||
|
|
||||||
@ -390,6 +465,7 @@
|
|||||||
throw new Error('missing-rounding-id');
|
throw new Error('missing-rounding-id');
|
||||||
}
|
}
|
||||||
const payload = beforeSubmitRoundingInfo(form);
|
const payload = beforeSubmitRoundingInfo(form);
|
||||||
|
await assertRangeNoOverlap(form, payload.startValue, payload.endValue);
|
||||||
await addWeightRoundingInfo({
|
await addWeightRoundingInfo({
|
||||||
roundingId,
|
roundingId,
|
||||||
startValue: payload.startValue,
|
startValue: payload.startValue,
|
||||||
@ -400,6 +476,7 @@
|
|||||||
|
|
||||||
async function apiEditWeightRoundingInfo(form: Record<string, any>) {
|
async function apiEditWeightRoundingInfo(form: Record<string, any>) {
|
||||||
const payload = beforeSubmitRoundingInfo(form);
|
const payload = beforeSubmitRoundingInfo(form);
|
||||||
|
await assertRangeNoOverlap(form, payload.startValue, payload.endValue);
|
||||||
await updateWeightRoundingInfo({
|
await updateWeightRoundingInfo({
|
||||||
id: form.id,
|
id: form.id,
|
||||||
roundingId: form.roundingId ?? currentId.value?.id,
|
roundingId: form.roundingId ?? currentId.value?.id,
|
||||||
|
|||||||
@ -147,6 +147,7 @@ function clearPriceContext() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.h5-assistant-shell {
|
.h5-assistant-shell {
|
||||||
|
position: relative;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
@ -163,6 +164,7 @@ function clearPriceContext() {
|
|||||||
:deep(.ai-advisor-shell),
|
:deep(.ai-advisor-shell),
|
||||||
:deep(.ai-advisor-app),
|
:deep(.ai-advisor-app),
|
||||||
:deep(.ai-advisor-layout) {
|
:deep(.ai-advisor-layout) {
|
||||||
|
position: relative;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
@ -193,9 +195,26 @@ function clearPriceContext() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
:deep(.ai-history-sidebar) {
|
:deep(.ai-history-sidebar) {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 20;
|
||||||
|
width: min(280px, 86%);
|
||||||
border-radius: 0 18px 18px 0;
|
border-radius: 0 18px 18px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 透明可点区域:点侧栏外关闭,但不加灰色遮罩 */
|
||||||
|
:deep(.ai-advisor-shell.ai-history-open .ai-history-backdrop) {
|
||||||
|
display: block !important;
|
||||||
|
position: absolute !important;
|
||||||
|
inset: 0 !important;
|
||||||
|
z-index: 19 !important;
|
||||||
|
border: none;
|
||||||
|
background: transparent !important;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
/* 运单上下文:移动端更紧凑 */
|
/* 运单上下文:移动端更紧凑 */
|
||||||
:deep(.ai-waybill-context) {
|
:deep(.ai-waybill-context) {
|
||||||
margin: 0 0 8px;
|
margin: 0 0 8px;
|
||||||
@ -226,27 +245,42 @@ function clearPriceContext() {
|
|||||||
color: var(--app-primary-color);
|
color: var(--app-primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 欢迎区 + MCP 能力 */
|
/* 欢迎区 + 快捷入口 */
|
||||||
:deep(.ai-welcome__hero) {
|
:deep(.ai-welcome__hero) {
|
||||||
padding: 14px 0 12px;
|
padding: 8px 2px 12px;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ai-welcome__avatar.nana-fab-wrap) {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.ai-welcome__hero h2) {
|
:deep(.ai-welcome__hero h2) {
|
||||||
font-size: 1.15rem;
|
font-size: 1.08rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.ai-welcome__hero p) {
|
:deep(.ai-welcome__hero p) {
|
||||||
font-size: 0.84rem;
|
font-size: 0.82rem;
|
||||||
|
color: var(--color-text-3, #86909c);
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.ai-mcp-grid) {
|
:deep(.ai-mcp-grid) {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:deep(.ai-mcp-grid--waybill) {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.ai-mcp-card) {
|
:deep(.ai-mcp-card) {
|
||||||
border-radius: 14px;
|
background: transparent;
|
||||||
padding: 12px 14px;
|
box-shadow: none;
|
||||||
|
min-height: 96px;
|
||||||
|
padding: 16px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid rgba(15, 23, 42, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.ai-followup) {
|
:deep(.ai-followup) {
|
||||||
|
|||||||
@ -10,34 +10,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<router-link
|
<router-link
|
||||||
v-for="item in leftTabs"
|
v-for="item in tabs"
|
||||||
:key="item.path"
|
|
||||||
:to="item.path"
|
|
||||||
replace
|
|
||||||
class="h5-dock__item"
|
|
||||||
:class="{ 'is-active': isActive(item.path) }"
|
|
||||||
>
|
|
||||||
<span class="h5-dock__icon-wrap">
|
|
||||||
<component :is="item.icon" class="h5-dock__icon" />
|
|
||||||
</span>
|
|
||||||
<span class="h5-dock__label">{{ t(item.labelKey) }}</span>
|
|
||||||
</router-link>
|
|
||||||
|
|
||||||
<button
|
|
||||||
v-if="!hideCreate"
|
|
||||||
type="button"
|
|
||||||
class="h5-dock__item h5-dock__item--create"
|
|
||||||
:aria-label="t('h5.tab.createOrder')"
|
|
||||||
@click="emit('create-order')"
|
|
||||||
>
|
|
||||||
<span class="h5-dock__icon-wrap h5-dock__icon-wrap--create">
|
|
||||||
<icon-plus :size="20" class="h5-dock__icon h5-dock__icon--plus" />
|
|
||||||
</span>
|
|
||||||
<span class="h5-dock__label">{{ t('h5.tab.createOrder') }}</span>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<router-link
|
|
||||||
v-for="item in rightTabs"
|
|
||||||
:key="item.path"
|
:key="item.path"
|
||||||
:to="item.path"
|
:to="item.path"
|
||||||
replace
|
replace
|
||||||
@ -53,6 +26,29 @@
|
|||||||
<span class="h5-dock__label">{{ t(item.labelKey) }}</span>
|
<span class="h5-dock__label">{{ t(item.labelKey) }}</span>
|
||||||
</router-link>
|
</router-link>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<div class="h5-dock-fab-slot" :class="{ 'is-hidden': hideCreate }">
|
||||||
|
<button
|
||||||
|
v-show="showFabHint && !hideCreate"
|
||||||
|
type="button"
|
||||||
|
class="h5-dock-fab-hint"
|
||||||
|
@click="dismissFabHint"
|
||||||
|
>
|
||||||
|
{{ t('h5.tab.createOrder') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="h5-dock-fab"
|
||||||
|
:tabindex="hideCreate ? -1 : 0"
|
||||||
|
:aria-hidden="hideCreate ? 'true' : 'false'"
|
||||||
|
:aria-label="t('h5.tab.createOrder')"
|
||||||
|
@click="onCreateOrder"
|
||||||
|
>
|
||||||
|
<span class="h5-dock-fab__glass" aria-hidden="true" />
|
||||||
|
<span class="h5-dock-fab__blob" aria-hidden="true" />
|
||||||
|
<icon-plus :size="26" class="h5-dock-fab__icon" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -60,9 +56,11 @@
|
|||||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { IconRobot, IconUser, IconPlus } from '@arco-design/web-vue/es/icon';
|
import { IconPlus } from '@arco-design/web-vue/es/icon';
|
||||||
import IconTabCalculator from '@/assets/icons/h5/tab-calculator.svg';
|
import IconTabCalculator from '@/assets/icons/h5/tab-calculator.svg';
|
||||||
import IconTabPackage from '@/assets/icons/h5/tab-package.svg';
|
import IconTabPackage from '@/assets/icons/h5/tab-package.svg';
|
||||||
|
import IconTabService from '@/assets/icons/h5/tab-service.svg';
|
||||||
|
import IconTabMe from '@/assets/icons/h5/tab-me.svg';
|
||||||
|
|
||||||
defineOptions({ name: 'H5AppTabBar' });
|
defineOptions({ name: 'H5AppTabBar' });
|
||||||
|
|
||||||
@ -86,23 +84,26 @@ const sliderStyle = ref<Record<string, string>>({
|
|||||||
opacity: '0',
|
opacity: '0',
|
||||||
});
|
});
|
||||||
const sliderPos = { x: 0, w: 0 };
|
const sliderPos = { x: 0, w: 0 };
|
||||||
|
const showFabHint = ref(false);
|
||||||
let settleTimer: ReturnType<typeof setTimeout> | null = null;
|
let settleTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
let hideCreateTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
let hintShowTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
|
let hintHideTimer: ReturnType<typeof setTimeout> | null = null;
|
||||||
let sliderAnimating = false;
|
let sliderAnimating = false;
|
||||||
|
|
||||||
const leftTabs = computed(() => [
|
const FAB_HINT_STORAGE_KEY = 'h5_create_order_fab_hint_seen';
|
||||||
{
|
|
||||||
path: '/h5/assistant',
|
const tabs = computed(() => [
|
||||||
labelKey: 'h5.tab.assistant',
|
|
||||||
icon: IconRobot,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/h5/price',
|
path: '/h5/price',
|
||||||
labelKey: 'h5.tab.price',
|
labelKey: 'h5.tab.price',
|
||||||
icon: IconTabCalculator,
|
icon: IconTabCalculator,
|
||||||
},
|
},
|
||||||
]);
|
{
|
||||||
|
path: '/h5/assistant',
|
||||||
const rightTabs = computed(() => [
|
labelKey: 'h5.tab.assistant',
|
||||||
|
icon: IconTabService,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/h5/orders',
|
path: '/h5/orders',
|
||||||
labelKey: 'h5.tab.orders',
|
labelKey: 'h5.tab.orders',
|
||||||
@ -112,7 +113,7 @@ const rightTabs = computed(() => [
|
|||||||
{
|
{
|
||||||
path: '/h5/me',
|
path: '/h5/me',
|
||||||
labelKey: 'h5.tab.me',
|
labelKey: 'h5.tab.me',
|
||||||
icon: IconUser,
|
icon: IconTabMe,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -120,6 +121,67 @@ function isActive(path: string) {
|
|||||||
return route.path === path || route.path.startsWith(`${path}/`);
|
return route.path === path || route.path.startsWith(`${path}/`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasSeenFabHint() {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem(FAB_HINT_STORAGE_KEY) === '1';
|
||||||
|
} catch {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function markFabHintSeen() {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(FAB_HINT_STORAGE_KEY, '1');
|
||||||
|
} catch {
|
||||||
|
/* 隐私模式无法写入时,至少本页不再反复弹出 */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearHintTimers() {
|
||||||
|
if (hintShowTimer) {
|
||||||
|
clearTimeout(hintShowTimer);
|
||||||
|
hintShowTimer = null;
|
||||||
|
}
|
||||||
|
if (hintHideTimer) {
|
||||||
|
clearTimeout(hintHideTimer);
|
||||||
|
hintHideTimer = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dismissFabHint() {
|
||||||
|
showFabHint.value = false;
|
||||||
|
clearHintTimers();
|
||||||
|
markFabHintSeen();
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleFabHint() {
|
||||||
|
clearHintTimers();
|
||||||
|
if (props.hideCreate || hasSeenFabHint()) {
|
||||||
|
showFabHint.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hintShowTimer = setTimeout(() => {
|
||||||
|
hintShowTimer = null;
|
||||||
|
if (props.hideCreate || hasSeenFabHint()) return;
|
||||||
|
showFabHint.value = true;
|
||||||
|
hintHideTimer = setTimeout(() => {
|
||||||
|
hintHideTimer = null;
|
||||||
|
dismissFabHint();
|
||||||
|
}, 4200);
|
||||||
|
}, 700);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCreateOrder() {
|
||||||
|
if (props.hideCreate) return;
|
||||||
|
dismissFabHint();
|
||||||
|
try {
|
||||||
|
navigator.vibrate?.(12);
|
||||||
|
} catch {
|
||||||
|
/* 部分 WebView 不支持震动 */
|
||||||
|
}
|
||||||
|
emit('create-order');
|
||||||
|
}
|
||||||
|
|
||||||
const SLIDER_INSET = 4;
|
const SLIDER_INSET = 4;
|
||||||
const LIQUID_STRETCH_MS = 120;
|
const LIQUID_STRETCH_MS = 120;
|
||||||
|
|
||||||
@ -210,6 +272,7 @@ function onViewportResize() {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
syncSlider(false);
|
syncSlider(false);
|
||||||
|
scheduleFabHint();
|
||||||
if (typeof ResizeObserver !== 'undefined') {
|
if (typeof ResizeObserver !== 'undefined') {
|
||||||
resizeObserver = new ResizeObserver(onViewportResize);
|
resizeObserver = new ResizeObserver(onViewportResize);
|
||||||
if (dockRef.value) resizeObserver.observe(dockRef.value);
|
if (dockRef.value) resizeObserver.observe(dockRef.value);
|
||||||
@ -219,16 +282,38 @@ onMounted(() => {
|
|||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
clearSettleTimer();
|
clearSettleTimer();
|
||||||
|
clearHintTimers();
|
||||||
|
if (hideCreateTimer) {
|
||||||
|
clearTimeout(hideCreateTimer);
|
||||||
|
hideCreateTimer = null;
|
||||||
|
}
|
||||||
resizeObserver?.disconnect();
|
resizeObserver?.disconnect();
|
||||||
window.removeEventListener('resize', onViewportResize);
|
window.removeEventListener('resize', onViewportResize);
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => [route.path, props.hideCreate] as const,
|
() => route.path,
|
||||||
() => {
|
() => {
|
||||||
syncSlider(true);
|
syncSlider(true);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.hideCreate,
|
||||||
|
(hidden) => {
|
||||||
|
if (hideCreateTimer) clearTimeout(hideCreateTimer);
|
||||||
|
hideCreateTimer = setTimeout(() => {
|
||||||
|
hideCreateTimer = null;
|
||||||
|
syncSlider(true);
|
||||||
|
}, 320);
|
||||||
|
if (hidden) {
|
||||||
|
showFabHint.value = false;
|
||||||
|
clearHintTimers();
|
||||||
|
} else {
|
||||||
|
scheduleFabHint();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
@ -239,7 +324,8 @@ watch(
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
align-items: center;
|
||||||
|
gap: 0;
|
||||||
/* 16px 侧边 + 半高圆角 ≈ iPhone 屏幕连续圆角 */
|
/* 16px 侧边 + 半高圆角 ≈ iPhone 屏幕连续圆角 */
|
||||||
padding: 0 var(--h5-dock-side-inset, 16px)
|
padding: 0 var(--h5-dock-side-inset, 16px)
|
||||||
var(--h5-dock-bottom-inset, max(8px, calc(env(safe-area-inset-bottom, 0px) - 12px)));
|
var(--h5-dock-bottom-inset, max(8px, calc(env(safe-area-inset-bottom, 0px) - 12px)));
|
||||||
@ -252,8 +338,9 @@ watch(
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
gap: 0;
|
gap: 0;
|
||||||
width: 100%;
|
|
||||||
height: var(--h5-dock-height, 68px);
|
height: var(--h5-dock-height, 68px);
|
||||||
min-height: var(--h5-dock-height, 68px);
|
min-height: var(--h5-dock-height, 68px);
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
@ -390,33 +477,11 @@ watch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active:not(.h5-dock__item--create) {
|
&:active {
|
||||||
transform: scale(0.96);
|
transform: scale(0.96);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.h5-dock__item--create {
|
|
||||||
color: var(--app-primary-color);
|
|
||||||
|
|
||||||
.h5-dock__icon-wrap--create {
|
|
||||||
color: #fff;
|
|
||||||
background: var(--app-primary-color);
|
|
||||||
box-shadow:
|
|
||||||
0 4px 14px color-mix(in srgb, var(--app-primary-color) 38%, transparent),
|
|
||||||
inset 0 1px 0 rgba(255, 255, 255, 0.28);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active .h5-dock__icon-wrap--create {
|
|
||||||
filter: brightness(0.96);
|
|
||||||
transform: scale(0.94);
|
|
||||||
}
|
|
||||||
|
|
||||||
.h5-dock__label {
|
|
||||||
color: var(--app-primary-color);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.h5-dock__icon-wrap {
|
.h5-dock__icon-wrap {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@ -428,24 +493,12 @@ watch(
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.h5-dock__icon-wrap--create {
|
|
||||||
width: 34px;
|
|
||||||
height: 34px;
|
|
||||||
border-radius: 50%;
|
|
||||||
transition: transform 0.18s ease, filter 0.18s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h5-dock__icon {
|
.h5-dock__icon {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
width: 1em;
|
width: 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.h5-dock__icon--plus {
|
|
||||||
color: #fff;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h5-dock__badge {
|
.h5-dock__badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -4px;
|
top: -4px;
|
||||||
@ -473,6 +526,182 @@ watch(
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.h5-dock-fab-slot {
|
||||||
|
position: relative;
|
||||||
|
flex: 0 0 var(--h5-dock-fab-size, 56px);
|
||||||
|
width: var(--h5-dock-fab-size, 56px);
|
||||||
|
height: var(--h5-dock-fab-size, 56px);
|
||||||
|
margin-left: 12px;
|
||||||
|
overflow: visible;
|
||||||
|
pointer-events: none;
|
||||||
|
transition:
|
||||||
|
flex-basis 0.32s cubic-bezier(0.32, 0.72, 0, 1),
|
||||||
|
width 0.32s cubic-bezier(0.32, 0.72, 0, 1),
|
||||||
|
margin-left 0.32s cubic-bezier(0.32, 0.72, 0, 1),
|
||||||
|
opacity 0.22s ease;
|
||||||
|
|
||||||
|
&.is-hidden {
|
||||||
|
overflow: hidden;
|
||||||
|
flex-basis: 0;
|
||||||
|
width: 0;
|
||||||
|
margin-left: 0;
|
||||||
|
opacity: 0;
|
||||||
|
|
||||||
|
.h5-dock-fab,
|
||||||
|
.h5-dock-fab-hint {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-dock-fab-hint {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
bottom: calc(100% + 10px);
|
||||||
|
z-index: 3;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
pointer-events: auto;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: #1d2129;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.2;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 100%;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border: 6px solid transparent;
|
||||||
|
border-top-color: #1d2129;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右侧独立圆形下单 FAB:液态玻璃 */
|
||||||
|
.h5-dock-fab {
|
||||||
|
position: relative;
|
||||||
|
pointer-events: auto;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: var(--h5-dock-fab-size, 56px);
|
||||||
|
height: var(--h5-dock-fab-size, 56px);
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
isolation: isolate;
|
||||||
|
color: var(--app-primary-color);
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
touch-action: manipulation;
|
||||||
|
transform-origin: center;
|
||||||
|
transition: transform 0.22s cubic-bezier(0.22, 1.2, 0.36, 1);
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.9);
|
||||||
|
|
||||||
|
.h5-dock-fab__blob {
|
||||||
|
transform: translate(18%, 22%) scale(1.28);
|
||||||
|
opacity: 0.95;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-dock-fab__icon {
|
||||||
|
transform: scale(0.92);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-dock-fab__glass {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
pointer-events: none;
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
rgba(255, 255, 255, 0.38) 0%,
|
||||||
|
rgba(255, 255, 255, 0.16) 100%
|
||||||
|
);
|
||||||
|
backdrop-filter: blur(32px) saturate(190%) brightness(1.08);
|
||||||
|
-webkit-backdrop-filter: blur(32px) saturate(190%) brightness(1.08);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
background: linear-gradient(
|
||||||
|
155deg,
|
||||||
|
rgba(255, 255, 255, 0.72) 0%,
|
||||||
|
rgba(255, 255, 255, 0.16) 36%,
|
||||||
|
rgba(255, 255, 255, 0) 62%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
padding: 1px;
|
||||||
|
background: linear-gradient(
|
||||||
|
160deg,
|
||||||
|
rgba(255, 255, 255, 0.95) 0%,
|
||||||
|
rgba(255, 255, 255, 0.22) 42%,
|
||||||
|
rgba(255, 255, 255, 0.4) 100%
|
||||||
|
);
|
||||||
|
-webkit-mask:
|
||||||
|
linear-gradient(#000 0 0) content-box,
|
||||||
|
linear-gradient(#000 0 0);
|
||||||
|
-webkit-mask-composite: xor;
|
||||||
|
mask:
|
||||||
|
linear-gradient(#000 0 0) content-box,
|
||||||
|
linear-gradient(#000 0 0);
|
||||||
|
mask-composite: exclude;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-dock-fab__blob {
|
||||||
|
position: absolute;
|
||||||
|
left: 8%;
|
||||||
|
top: 4%;
|
||||||
|
z-index: 1;
|
||||||
|
width: 62%;
|
||||||
|
height: 52%;
|
||||||
|
border-radius: 50%;
|
||||||
|
pointer-events: none;
|
||||||
|
background: radial-gradient(
|
||||||
|
circle at 40% 35%,
|
||||||
|
rgba(255, 255, 255, 0.78) 0%,
|
||||||
|
rgba(255, 255, 255, 0.18) 48%,
|
||||||
|
rgba(255, 255, 255, 0) 72%
|
||||||
|
);
|
||||||
|
filter: blur(1.2px);
|
||||||
|
opacity: 0.85;
|
||||||
|
transform: translate(0, 0) scale(1);
|
||||||
|
transition:
|
||||||
|
transform 0.42s cubic-bezier(0.32, 0.72, 0, 1),
|
||||||
|
opacity 0.22s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-dock-fab__icon {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
color: var(--app-primary-color);
|
||||||
|
pointer-events: none;
|
||||||
|
transition: transform 0.22s cubic-bezier(0.22, 1.2, 0.36, 1);
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes h5-dock-icon-pop {
|
@keyframes h5-dock-icon-pop {
|
||||||
0% {
|
0% {
|
||||||
transform: scale(0.88);
|
transform: scale(0.88);
|
||||||
@ -485,6 +714,21 @@ watch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 380px) {
|
||||||
|
.h5-dock__icon {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-dock__label {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-dock-fab-slot,
|
||||||
|
.h5-dock-fab {
|
||||||
|
--h5-dock-fab-size: 52px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.h5-dock__slider.is-ready {
|
.h5-dock__slider.is-ready {
|
||||||
transition: none;
|
transition: none;
|
||||||
@ -493,5 +737,10 @@ watch(
|
|||||||
.h5-dock__item.is-active .h5-dock__icon {
|
.h5-dock__item.is-active .h5-dock__icon {
|
||||||
animation: none;
|
animation: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.h5-dock-fab-slot,
|
||||||
|
.h5-dock-fab {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -4,7 +4,9 @@
|
|||||||
<component :is="icon" />
|
<component :is="icon" />
|
||||||
</div>
|
</div>
|
||||||
<p v-if="text">{{ text }}</p>
|
<p v-if="text">{{ text }}</p>
|
||||||
<slot />
|
<div v-if="$slots.default" class="h5-empty__actions">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -22,12 +24,12 @@ defineProps<{
|
|||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.h5-empty {
|
.h5-empty {
|
||||||
padding: 48px 12px;
|
padding: 48px 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #86909c;
|
color: var(--h5-text-3, #86909c);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
@ -41,20 +43,40 @@ defineProps<{
|
|||||||
width: 56px;
|
width: 56px;
|
||||||
height: 56px;
|
height: 56px;
|
||||||
margin: 0 auto 14px;
|
margin: 0 auto 14px;
|
||||||
border-radius: 16px;
|
border-radius: var(--h5-radius-sm, 12px);
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
font-size: 26px;
|
|
||||||
color: var(--app-primary-color);
|
color: var(--app-primary-color);
|
||||||
background: color-mix(in srgb, var(--app-primary-color) 12%, #fff);
|
background: var(--h5-primary-soft, color-mix(in srgb, var(--app-primary-color) 12%, #fff));
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.85);
|
|
||||||
|
:deep(svg),
|
||||||
|
:deep(.arco-icon) {
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
font-size: 26px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.h5-empty--inline .h5-empty__mark {
|
.h5-empty--inline .h5-empty__mark {
|
||||||
width: 44px;
|
width: 44px;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
font-size: 20px;
|
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
:deep(svg),
|
||||||
|
:deep(.arco-icon) {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-empty__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 14px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -84,16 +84,23 @@
|
|||||||
</a-spin>
|
</a-spin>
|
||||||
|
|
||||||
<footer class="h5-detail__foot">
|
<footer class="h5-detail__foot">
|
||||||
<button type="button" class="h5-detail__btn is-ghost" @click="askAdvisor">
|
<button
|
||||||
{{ t('h5.orders.askAdvisor') }}
|
v-if="advisorAsIcon"
|
||||||
|
type="button"
|
||||||
|
class="h5-detail__icon-btn"
|
||||||
|
:aria-label="t('h5.orders.askAdvisor')"
|
||||||
|
@click="askAdvisor"
|
||||||
|
>
|
||||||
|
<IconTabService class="h5-detail__icon" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="canModify"
|
v-if="canModify"
|
||||||
type="button"
|
type="button"
|
||||||
class="h5-detail__btn is-ghost"
|
class="h5-detail__icon-btn"
|
||||||
|
:aria-label="t('h5.orders.actions.edit')"
|
||||||
@click="openEdit"
|
@click="openEdit"
|
||||||
>
|
>
|
||||||
{{ t('h5.orders.actions.edit') }}
|
<IconEdit class="h5-detail__icon" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="isDraft"
|
v-if="isDraft"
|
||||||
@ -112,8 +119,14 @@
|
|||||||
>
|
>
|
||||||
{{ t('h5.orders.pay') }}
|
{{ t('h5.orders.pay') }}
|
||||||
</button>
|
</button>
|
||||||
<button v-else type="button" class="h5-detail__btn is-primary" @click="close">
|
<button
|
||||||
{{ t('h5.orders.detail.close') }}
|
v-else
|
||||||
|
type="button"
|
||||||
|
class="h5-detail__btn is-primary"
|
||||||
|
@click="askAdvisor"
|
||||||
|
>
|
||||||
|
<IconTabService class="h5-detail__btn-icon" />
|
||||||
|
{{ t('h5.orders.askAdvisor') }}
|
||||||
</button>
|
</button>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
@ -132,7 +145,7 @@ import { computed, ref, watch } from 'vue';
|
|||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { Message, Modal } from '@arco-design/web-vue';
|
import { Message, Modal } from '@arco-design/web-vue';
|
||||||
import { IconClose, IconClockCircle, IconCommon } from '@arco-design/web-vue/es/icon';
|
import { IconClose, IconClockCircle, IconCommon, IconEdit } from '@arco-design/web-vue/es/icon';
|
||||||
import { getOrderDetail, postOrderPreDeclare } from '@/api/order';
|
import { getOrderDetail, postOrderPreDeclare } from '@/api/order';
|
||||||
import { postGetDetailTimeline } from '@/api/TrackOrder/trackOrder';
|
import { postGetDetailTimeline } from '@/api/TrackOrder/trackOrder';
|
||||||
import { getDictListAll, type DictDto } from '@/api/system/dict';
|
import { getDictListAll, type DictDto } from '@/api/system/dict';
|
||||||
@ -146,6 +159,7 @@ import {
|
|||||||
buildAskAdvisorQuery,
|
buildAskAdvisorQuery,
|
||||||
buildWaybillContextFromRow,
|
buildWaybillContextFromRow,
|
||||||
} from '@/composables/useAskAdvisor';
|
} from '@/composables/useAskAdvisor';
|
||||||
|
import IconTabService from '@/assets/icons/h5/tab-service.svg';
|
||||||
import H5OrderPaySheet from './H5OrderPaySheet.vue';
|
import H5OrderPaySheet from './H5OrderPaySheet.vue';
|
||||||
import H5EmptyState from './H5EmptyState.vue';
|
import H5EmptyState from './H5EmptyState.vue';
|
||||||
|
|
||||||
@ -208,6 +222,8 @@ const waybillNo = computed(
|
|||||||
const statusCode = computed(() => resolveOrderStatusCode(detail.value));
|
const statusCode = computed(() => resolveOrderStatusCode(detail.value));
|
||||||
const isPendingPay = computed(() => statusCode.value === 'pendingPayment');
|
const isPendingPay = computed(() => statusCode.value === 'pendingPayment');
|
||||||
const isDraft = computed(() => statusCode.value === 'draft');
|
const isDraft = computed(() => statusCode.value === 'draft');
|
||||||
|
/** 有支付/预报主操作时,顾问收成图标;否则顾问作为主按钮突出 */
|
||||||
|
const advisorAsIcon = computed(() => isDraft.value || isPendingPay.value);
|
||||||
const canModify = computed(() =>
|
const canModify = computed(() =>
|
||||||
H5_MODIFY_STATUSES.includes(statusCode.value as OrderStatusCode),
|
H5_MODIFY_STATUSES.includes(statusCode.value as OrderStatusCode),
|
||||||
);
|
);
|
||||||
@ -676,30 +692,54 @@ watch(
|
|||||||
|
|
||||||
.h5-detail__foot {
|
.h5-detail__foot {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 12px 16px 14px;
|
padding: 12px 16px calc(14px + env(safe-area-inset-bottom, 0px));
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-top: 1px solid #eef0f3;
|
border-top: 1px solid #eef0f3;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.h5-detail__icon-btn {
|
||||||
|
flex: 0 0 48px;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid #e5e6eb;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fff;
|
||||||
|
color: #4e5969;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background: #f7f8fa;
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-detail__icon {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
font-size: 22px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.h5-detail__btn {
|
.h5-detail__btn {
|
||||||
flex: 1;
|
flex: 1 1 auto;
|
||||||
|
min-width: 0;
|
||||||
height: 48px;
|
height: 48px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
display: inline-flex;
|
||||||
&.is-ghost {
|
align-items: center;
|
||||||
border: 1px solid #e5e6eb;
|
justify-content: center;
|
||||||
background: #fff;
|
gap: 6px;
|
||||||
color: #4e5969;
|
-webkit-tap-highlight-color: transparent;
|
||||||
|
|
||||||
&:active {
|
|
||||||
background: #f7f8fa;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-primary {
|
&.is-primary {
|
||||||
border: none;
|
border: none;
|
||||||
@ -710,6 +750,19 @@ watch(
|
|||||||
&:active {
|
&:active {
|
||||||
opacity: 0.92;
|
opacity: 0.92;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.55;
|
||||||
|
cursor: not-allowed;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.h5-detail__btn-icon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
display: block;
|
||||||
|
color: currentColor;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export default {
|
|||||||
'h5.price.length': 'L',
|
'h5.price.length': 'L',
|
||||||
'h5.price.width': 'W',
|
'h5.price.width': 'W',
|
||||||
'h5.price.height': 'H',
|
'h5.price.height': 'H',
|
||||||
'h5.price.query': 'Check rates',
|
'h5.price.query': 'Get quotes',
|
||||||
'h5.price.querying': 'Checking…',
|
'h5.price.querying': 'Checking…',
|
||||||
'h5.price.editQuery': 'Edit',
|
'h5.price.editQuery': 'Edit',
|
||||||
'h5.price.reset': 'Reset',
|
'h5.price.reset': 'Reset',
|
||||||
|
|||||||
@ -38,7 +38,7 @@ export default {
|
|||||||
'h5.price.length': '长',
|
'h5.price.length': '长',
|
||||||
'h5.price.width': '宽',
|
'h5.price.width': '宽',
|
||||||
'h5.price.height': '高',
|
'h5.price.height': '高',
|
||||||
'h5.price.query': '一键查价',
|
'h5.price.query': '查询报价',
|
||||||
'h5.price.querying': '查价中…',
|
'h5.price.querying': '查价中…',
|
||||||
'h5.price.editQuery': '修改条件',
|
'h5.price.editQuery': '修改条件',
|
||||||
'h5.price.reset': '重置',
|
'h5.price.reset': '重置',
|
||||||
|
|||||||
@ -115,7 +115,7 @@
|
|||||||
<a-spin :loading="loading && orders.length > 0" class="h5-orders-spin">
|
<a-spin :loading="loading && orders.length > 0" class="h5-orders-spin">
|
||||||
<H5EmptyState
|
<H5EmptyState
|
||||||
v-if="!loading && !orders.length"
|
v-if="!loading && !orders.length"
|
||||||
:icon="IconList"
|
:icon="IconTabPackage"
|
||||||
:text="
|
:text="
|
||||||
isPickupSelection
|
isPickupSelection
|
||||||
? t('h5.orders.mergePickup.emptyPredeclared')
|
? t('h5.orders.mergePickup.emptyPredeclared')
|
||||||
@ -124,13 +124,12 @@
|
|||||||
>
|
>
|
||||||
<a-button
|
<a-button
|
||||||
v-if="!selectionMode"
|
v-if="!selectionMode"
|
||||||
type="outline"
|
class="app-btn-primary-soft"
|
||||||
size="small"
|
|
||||||
@click="h5Navigate(router, '/h5/price')"
|
@click="h5Navigate(router, '/h5/price')"
|
||||||
>
|
>
|
||||||
{{ t('h5.orders.emptyGoPrice') }}
|
{{ t('h5.orders.emptyGoPrice') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
<a-button v-else type="outline" size="small" @click="exitSelectionMode">
|
<a-button v-else @click="exitSelectionMode">
|
||||||
{{ t('h5.orders.mergePickup.cancel') }}
|
{{ t('h5.orders.mergePickup.cancel') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
</H5EmptyState>
|
</H5EmptyState>
|
||||||
@ -412,9 +411,10 @@ import { useRoute, useRouter } from 'vue-router';
|
|||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { Message, Modal } from '@arco-design/web-vue';
|
import { Message, Modal } from '@arco-design/web-vue';
|
||||||
import { IconCheck, IconCopy, IconLeft, IconList, IconSearch } from '@arco-design/web-vue/es/icon';
|
import { IconCheck, IconCopy, IconLeft, IconSearch } from '@arco-design/web-vue/es/icon';
|
||||||
import { h5Navigate } from '@/utils/device';
|
import { h5Navigate } from '@/utils/device';
|
||||||
import iconOrderPickup from '@/assets/icons/h5/order-pickup.svg?url';
|
import iconOrderPickup from '@/assets/icons/h5/order-pickup.svg?url';
|
||||||
|
import IconTabPackage from '@/assets/icons/h5/tab-package.svg';
|
||||||
import H5EmptyState from '../components/H5EmptyState.vue';
|
import H5EmptyState from '../components/H5EmptyState.vue';
|
||||||
import { H5_HIDE_CREATE_FAB_KEY } from '../composables/h5TabBarUi';
|
import { H5_HIDE_CREATE_FAB_KEY } from '../composables/h5TabBarUi';
|
||||||
import {
|
import {
|
||||||
@ -2124,11 +2124,6 @@ void bootstrap();
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.h5-orders-spin :deep(.h5-empty) {
|
|
||||||
p {
|
|
||||||
margin-bottom: 14px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.h5-order-card {
|
.h5-order-card {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
|||||||
@ -78,20 +78,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.h5-empty {
|
.h5-empty {
|
||||||
padding: 48px 12px;
|
padding: 48px 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 0 0 12px;
|
margin: 0;
|
||||||
color: #86909c;
|
color: var(--h5-text-3, #86909c);
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.h5-empty__mark {
|
.h5-empty__mark {
|
||||||
width: 52px;
|
width: 56px;
|
||||||
height: 52px;
|
height: 56px;
|
||||||
margin: 0 auto 12px;
|
margin: 0 auto 14px;
|
||||||
border-radius: 16px;
|
border-radius: var(--h5-radius-sm, 12px);
|
||||||
background: color-mix(in srgb, var(--app-primary-color) 12%, #fff);
|
background: var(--h5-primary-soft, color-mix(in srgb, var(--app-primary-color) 12%, #fff));
|
||||||
|
color: var(--app-primary-color);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,8 +27,10 @@
|
|||||||
--h5-dock-bottom-inset: max(8px, calc(var(--h5-safe-bottom) - 12px));
|
--h5-dock-bottom-inset: max(8px, calc(var(--h5-safe-bottom) - 12px));
|
||||||
--h5-dock-side-inset: 16px;
|
--h5-dock-side-inset: 16px;
|
||||||
--h5-dock-height: 68px;
|
--h5-dock-height: 68px;
|
||||||
|
--h5-dock-fab-size: 56px;
|
||||||
--h5-dock-radius: calc(var(--h5-dock-height) / 2);
|
--h5-dock-radius: calc(var(--h5-dock-height) / 2);
|
||||||
--h5-dock-offset: calc(var(--h5-dock-height) + var(--h5-dock-bottom-inset));
|
/* 内容区多留一点,避免圆钮阴影压住列表末行 */
|
||||||
|
--h5-dock-offset: calc(var(--h5-dock-height) + var(--h5-dock-bottom-inset) + 6px);
|
||||||
|
|
||||||
background: var(--h5-bg);
|
background: var(--h5-bg);
|
||||||
color: var(--h5-text);
|
color: var(--h5-text);
|
||||||
|
|||||||
@ -60,7 +60,7 @@
|
|||||||
<span class="sol-badge">合规推荐</span>
|
<span class="sol-badge">合规推荐</span>
|
||||||
</div>
|
</div>
|
||||||
<ul class="sol-list">
|
<ul class="sol-list">
|
||||||
<li>报关方式:简易 / 汇总均可</li>
|
<li>报关方式:汇总申报(正式报关单)</li>
|
||||||
<li>HS 编码:前 4 位即可</li>
|
<li>HS 编码:前 4 位即可</li>
|
||||||
<li>核心凭证:综服平台免税登记记录</li>
|
<li>核心凭证:综服平台免税登记记录</li>
|
||||||
<li>操作平台:单一窗口 + 综服平台</li>
|
<li>操作平台:单一窗口 + 综服平台</li>
|
||||||
@ -179,7 +179,7 @@ function handleClose() {
|
|||||||
|
|
||||||
const compareRows = [
|
const compareRows = [
|
||||||
{ label: '进项专票', refund: '必须有', exempt: '不需要', basic: '不需要' },
|
{ label: '进项专票', refund: '必须有', exempt: '不需要', basic: '不需要' },
|
||||||
{ label: '报关方式', refund: '汇总申报', exempt: '简易 / 汇总均可', basic: '统一简易申报' },
|
{ label: '报关方式', refund: '汇总申报', exempt: '汇总申报(正式报关单)', basic: '统一简易申报' },
|
||||||
{ label: 'HS 编码精度', refund: '10 位完整', exempt: '前 4 位即可', basic: '前 4 位批量' },
|
{ label: 'HS 编码精度', refund: '10 位完整', exempt: '前 4 位即可', basic: '前 4 位批量' },
|
||||||
{ label: '正式报关单', refund: '有(退税专用联)', exempt: '可有可无', basic: '物流商统一申报' },
|
{ label: '正式报关单', refund: '有(退税专用联)', exempt: '可有可无', basic: '物流商统一申报' },
|
||||||
{ label: '综试区要求', refund: '无强制要求', exempt: '需注册在综试区内', basic: '无要求' },
|
{ label: '综试区要求', refund: '无强制要求', exempt: '需注册在综试区内', basic: '无要求' },
|
||||||
|
|||||||
@ -314,6 +314,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
|||||||
const singleDomesticPickupProductOptions = ref<Array<{ value: string; label: string }>>([]);
|
const singleDomesticPickupProductOptions = ref<Array<{ value: string; label: string }>>([]);
|
||||||
const singleCargoAttributeOptions = ref<SingleSelectOption[]>([]);
|
const singleCargoAttributeOptions = ref<SingleSelectOption[]>([]);
|
||||||
const singleExportCustomsModeOptions = ref<SingleSelectOption[]>([]);
|
const singleExportCustomsModeOptions = ref<SingleSelectOption[]>([]);
|
||||||
|
const singleEcommercePlatformOptions = ref<SingleSelectOption[]>([]);
|
||||||
type WarehouseSelectOption = SingleSelectOption & {
|
type WarehouseSelectOption = SingleSelectOption & {
|
||||||
recipient?: string;
|
recipient?: string;
|
||||||
contactPhone?: string;
|
contactPhone?: string;
|
||||||
@ -2351,6 +2352,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
|||||||
productRes,
|
productRes,
|
||||||
cargoDictRows,
|
cargoDictRows,
|
||||||
exportCustomsDictRows,
|
exportCustomsDictRows,
|
||||||
|
ecommercePlatformDictRows,
|
||||||
warehouseRes,
|
warehouseRes,
|
||||||
currencyDictRows,
|
currencyDictRows,
|
||||||
countryRes,
|
countryRes,
|
||||||
@ -2360,6 +2362,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
|||||||
getSalesProductList({ page: 1, pageSize: 500, isActive: 2 }),
|
getSalesProductList({ page: 1, pageSize: 500, isActive: 2 }),
|
||||||
getDictListAll({ dictType: 'cargo_attribute' }),
|
getDictListAll({ dictType: 'cargo_attribute' }),
|
||||||
getDictListAll({ dictType: 'export_customs_mode' }),
|
getDictListAll({ dictType: 'export_customs_mode' }),
|
||||||
|
getDictListAll({ dictType: 'e-commerce_platform' }),
|
||||||
getWarehouseList({ page: 1, pageSize: 500 }),
|
getWarehouseList({ page: 1, pageSize: 500 }),
|
||||||
getDictListAll({ dictType: 'currency' }),
|
getDictListAll({ dictType: 'currency' }),
|
||||||
getCountryList(),
|
getCountryList(),
|
||||||
@ -2428,6 +2431,12 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
|||||||
singleForm.exportCustomsMode = defaultExportMode;
|
singleForm.exportCustomsMode = defaultExportMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
singleEcommercePlatformOptions.value = (ecommercePlatformDictRows ?? [])
|
||||||
|
.map(normalizeCargoDictRow)
|
||||||
|
.filter((d) => d.code)
|
||||||
|
.sort((a, b) => a.sort - b.sort || a.code.localeCompare(b.code))
|
||||||
|
.map((d) => ({ value: d.code, label: d.name || d.code }));
|
||||||
|
|
||||||
const whOpts = (warehouseRes.items ?? [])
|
const whOpts = (warehouseRes.items ?? [])
|
||||||
.map(normalizeWarehouseRow)
|
.map(normalizeWarehouseRow)
|
||||||
.filter((w) => w.id)
|
.filter((w) => w.id)
|
||||||
@ -2472,6 +2481,7 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
|||||||
singleDomesticPickupProductOptions.value = [];
|
singleDomesticPickupProductOptions.value = [];
|
||||||
singleCargoAttributeOptions.value = [];
|
singleCargoAttributeOptions.value = [];
|
||||||
singleExportCustomsModeOptions.value = [];
|
singleExportCustomsModeOptions.value = [];
|
||||||
|
singleEcommercePlatformOptions.value = [];
|
||||||
singleWarehouseOptions.value = [];
|
singleWarehouseOptions.value = [];
|
||||||
singleCurrencyOptions.value = [];
|
singleCurrencyOptions.value = [];
|
||||||
singleCountryOptions.value = [];
|
singleCountryOptions.value = [];
|
||||||
@ -3846,12 +3856,22 @@ import SelectDeclarationSkuModal from './components/SelectDeclarationSkuModal.vu
|
|||||||
:validate-status="fieldValidateStatus('platformCode')"
|
:validate-status="fieldValidateStatus('platformCode')"
|
||||||
:help="fieldHelp('platformCode')"
|
:help="fieldHelp('platformCode')"
|
||||||
>
|
>
|
||||||
<a-input
|
<a-select
|
||||||
v-model="singleForm.platformCode"
|
v-model="singleForm.platformCode"
|
||||||
:max-length="16"
|
allow-clear
|
||||||
|
allow-search
|
||||||
:placeholder="t('order.placeOrder.single.platformCodePlaceholder')"
|
:placeholder="t('order.placeOrder.single.platformCodePlaceholder')"
|
||||||
|
:loading="singleReferenceLoading"
|
||||||
:status="fieldValidateStatus('platformCode')"
|
:status="fieldValidateStatus('platformCode')"
|
||||||
/>
|
>
|
||||||
|
<a-option
|
||||||
|
v-for="opt in singleEcommercePlatformOptions"
|
||||||
|
:key="opt.value"
|
||||||
|
:value="opt.value"
|
||||||
|
>
|
||||||
|
{{ opt.label }}
|
||||||
|
</a-option>
|
||||||
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item
|
<a-form-item
|
||||||
class="dims-form-item"
|
class="dims-form-item"
|
||||||
|
|||||||
@ -6,12 +6,17 @@
|
|||||||
import { getSalesProductDetail } from '@/api/channel/salesProduct';
|
import { getSalesProductDetail } from '@/api/channel/salesProduct';
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
import type { RecommendAlternative, ShipVerdict } from '../recommendChannel';
|
import type { RecommendAlternative, ShipVerdict } from '../recommendChannel';
|
||||||
|
import {
|
||||||
|
groupPriceCheckFailures,
|
||||||
|
type PriceCheckFailureItem,
|
||||||
|
} from '../priceCheckFailures';
|
||||||
|
|
||||||
defineOptions({ name: 'PriceCheckChannelGuidePanel' });
|
defineOptions({ name: 'PriceCheckChannelGuidePanel' });
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
record: Record<string, any> | null;
|
record: Record<string, any> | null;
|
||||||
failureText?: string;
|
failureText?: string;
|
||||||
|
failureItems?: PriceCheckFailureItem[];
|
||||||
recommendReason?: string;
|
recommendReason?: string;
|
||||||
recommendVerdict?: ShipVerdict | '';
|
recommendVerdict?: ShipVerdict | '';
|
||||||
recommendEvidence?: string[];
|
recommendEvidence?: string[];
|
||||||
@ -35,6 +40,20 @@
|
|||||||
const verdict = computed(() => props.recommendVerdict || '');
|
const verdict = computed(() => props.recommendVerdict || '');
|
||||||
const evidence = computed(() => (props.recommendEvidence || []).filter(Boolean).slice(0, 3));
|
const evidence = computed(() => (props.recommendEvidence || []).filter(Boolean).slice(0, 3));
|
||||||
const alternatives = computed(() => (props.alternatives || []).slice(0, 3));
|
const alternatives = computed(() => (props.alternatives || []).slice(0, 3));
|
||||||
|
const showFailureBlock = computed(
|
||||||
|
() =>
|
||||||
|
!props.record &&
|
||||||
|
((props.failureItems && props.failureItems.length > 0) || !!props.failureText),
|
||||||
|
);
|
||||||
|
|
||||||
|
const failureGroups = computed(() => groupPriceCheckFailures(props.failureItems || []));
|
||||||
|
|
||||||
|
function failureStepLabel(step: string) {
|
||||||
|
const key = `order.priceCheck.guide.failureStep.${step}`;
|
||||||
|
const label = t(key);
|
||||||
|
return label === key ? step : label;
|
||||||
|
}
|
||||||
|
|
||||||
const showRecommendBlock = computed(
|
const showRecommendBlock = computed(
|
||||||
() =>
|
() =>
|
||||||
!!props.recommendReason ||
|
!!props.recommendReason ||
|
||||||
@ -216,9 +235,50 @@
|
|||||||
</p>
|
</p>
|
||||||
</a-spin>
|
</a-spin>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="failureText" class="guide-panel__failure">
|
<div v-else-if="showFailureBlock" class="guide-panel__failure">
|
||||||
<div class="guide-panel__failure-badge">{{ t('order.priceCheck.guide.failureTitle') }}</div>
|
<div class="guide-panel__failure-badge">{{ t('order.priceCheck.guide.failureTitle') }}</div>
|
||||||
<p class="guide-panel__failure-text">{{ failureText }}</p>
|
<p v-if="failureGroups.length" class="guide-panel__failure-summary">
|
||||||
|
{{
|
||||||
|
t('order.priceCheck.guide.failureSummary', {
|
||||||
|
productCount: failureGroups.length,
|
||||||
|
itemCount: failureItems?.length || 0,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</p>
|
||||||
|
<ol v-if="failureGroups.length" class="guide-panel__failure-list">
|
||||||
|
<li v-for="(group, gi) in failureGroups" :key="group.key" class="guide-panel__failure-product">
|
||||||
|
<div class="guide-panel__failure-product-head">
|
||||||
|
<span class="guide-panel__failure-index">{{ gi + 1 }}</span>
|
||||||
|
<div class="guide-panel__failure-product-title">
|
||||||
|
<strong>{{
|
||||||
|
group.productName ||
|
||||||
|
group.productCode ||
|
||||||
|
t('order.priceCheck.guide.failureUnknownProduct')
|
||||||
|
}}</strong>
|
||||||
|
<span v-if="group.productCode && group.productName" class="guide-panel__failure-code">
|
||||||
|
{{ group.productCode }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ul class="guide-panel__failure-reasons">
|
||||||
|
<li v-for="(item, ii) in group.items" :key="`${group.key}-${ii}`">
|
||||||
|
<div class="guide-panel__failure-meta">
|
||||||
|
<span v-if="item.cargoTypeName">
|
||||||
|
{{ t('order.priceCheck.guide.failureCargo', { name: item.cargoTypeName }) }}
|
||||||
|
</span>
|
||||||
|
<span v-if="item.warehouseName">
|
||||||
|
{{ t('order.priceCheck.guide.failureWarehouse', { name: item.warehouseName }) }}
|
||||||
|
</span>
|
||||||
|
<span v-if="item.step" class="guide-panel__failure-step">
|
||||||
|
{{ t('order.priceCheck.guide.failureStepLabel', { step: failureStepLabel(item.step) }) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="guide-panel__failure-reason">{{ item.reason }}</p>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
<p v-else-if="failureText" class="guide-panel__failure-text">{{ failureText }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="guide-panel__idle">
|
<div v-else class="guide-panel__idle">
|
||||||
<a-empty :description="t('order.priceCheck.guide.idle')" />
|
<a-empty :description="t('order.priceCheck.guide.idle')" />
|
||||||
@ -341,7 +401,9 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
min-height: 200px;
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.guide-panel__failure-badge {
|
.guide-panel__failure-badge {
|
||||||
@ -355,6 +417,100 @@
|
|||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.guide-panel__failure-summary {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--color-text-2);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-panel__failure-list {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-panel__failure-product {
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 1px solid var(--color-border-2);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--color-fill-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-panel__failure-product-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-panel__failure-index {
|
||||||
|
flex: none;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--app-primary-color);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-panel__failure-product-title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-text-1);
|
||||||
|
line-height: 20px;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-panel__failure-code {
|
||||||
|
color: var(--color-text-3);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-panel__failure-reasons {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-panel__failure-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-panel__failure-step {
|
||||||
|
color: var(--app-primary-color);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-panel__failure-reason {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
color: var(--color-text-2);
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.6;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
.guide-panel__failure-text {
|
.guide-panel__failure-text {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--color-text-2);
|
color: var(--color-text-2);
|
||||||
|
|||||||
@ -18,6 +18,11 @@
|
|||||||
import { recommendWithAi, type QuoteCandidate, type RecommendAlternative, type ShipVerdict } from './recommendChannel';
|
import { recommendWithAi, type QuoteCandidate, type RecommendAlternative, type ShipVerdict } from './recommendChannel';
|
||||||
import { normalizeCargoAttributeCode } from '@/utils/cargoAttribute';
|
import { normalizeCargoAttributeCode } from '@/utils/cargoAttribute';
|
||||||
import { openPriceAskAdvisor, type PriceAskIntent } from '@/composables/priceAskAdvisor';
|
import { openPriceAskAdvisor, type PriceAskIntent } from '@/composables/priceAskAdvisor';
|
||||||
|
import {
|
||||||
|
formatPriceCheckFailures,
|
||||||
|
normalizePriceCheckFailures,
|
||||||
|
type PriceCheckFailureItem,
|
||||||
|
} from './priceCheckFailures';
|
||||||
|
|
||||||
defineOptions({
|
defineOptions({
|
||||||
name: 'PriceCheck',
|
name: 'PriceCheck',
|
||||||
@ -417,36 +422,26 @@
|
|||||||
const queryResults = ref<any[]>([]);
|
const queryResults = ref<any[]>([]);
|
||||||
const queryLoading = ref(false);
|
const queryLoading = ref(false);
|
||||||
const queryFailureText = ref('');
|
const queryFailureText = ref('');
|
||||||
|
const queryFailureItems = ref<PriceCheckFailureItem[]>([]);
|
||||||
/** 单卡切换货物属性 / 仓库时的加载态 */
|
/** 单卡切换货物属性 / 仓库时的加载态 */
|
||||||
const recalcLoadingKeys = ref<Record<string, boolean>>({});
|
const recalcLoadingKeys = ref<Record<string, boolean>>({});
|
||||||
|
|
||||||
function formatPriceCheckFailures(
|
function failureStepLabel(step: string) {
|
||||||
failures: Array<{
|
const key = `order.priceCheck.guide.failureStep.${step}`;
|
||||||
reason?: string;
|
const label = t(key);
|
||||||
Reason?: string;
|
return label === key ? step : label;
|
||||||
step?: string;
|
}
|
||||||
Step?: string;
|
|
||||||
productName?: string;
|
function applyPriceCheckFailures(failures: unknown[]) {
|
||||||
ProductName?: string;
|
queryFailureItems.value = normalizePriceCheckFailures((failures ?? []) as any);
|
||||||
cargoTypeName?: string;
|
queryFailureText.value =
|
||||||
CargoTypeName?: string;
|
formatPriceCheckFailures((failures ?? []) as any, failureStepLabel) ||
|
||||||
warehouseName?: string;
|
t('order.priceCheck.table.empty');
|
||||||
WarehouseName?: string;
|
}
|
||||||
}>
|
|
||||||
) {
|
function clearPriceCheckFailures() {
|
||||||
return (failures ?? [])
|
queryFailureItems.value = [];
|
||||||
.map((f) => {
|
queryFailureText.value = '';
|
||||||
const reason = (f.reason ?? f.Reason ?? '').trim();
|
|
||||||
if (!reason) return '';
|
|
||||||
const step = (f.step ?? f.Step ?? '').trim();
|
|
||||||
const product = (f.productName ?? f.ProductName ?? '').trim();
|
|
||||||
const cargo = (f.cargoTypeName ?? f.CargoTypeName ?? '').trim();
|
|
||||||
const warehouse = (f.warehouseName ?? f.WarehouseName ?? '').trim();
|
|
||||||
const prefix = [product, cargo, warehouse, step].filter(Boolean).join(' / ');
|
|
||||||
return prefix ? `${prefix}:${reason}` : reason;
|
|
||||||
})
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(';');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentPage = ref(1);
|
const currentPage = ref(1);
|
||||||
@ -562,7 +557,7 @@
|
|||||||
}
|
}
|
||||||
hasQueried.value = true;
|
hasQueried.value = true;
|
||||||
queryLoading.value = true;
|
queryLoading.value = true;
|
||||||
queryFailureText.value = '';
|
clearPriceCheckFailures();
|
||||||
queryHint.value = null;
|
queryHint.value = null;
|
||||||
hintBannerDismissed.value = false;
|
hintBannerDismissed.value = false;
|
||||||
try {
|
try {
|
||||||
@ -593,13 +588,14 @@
|
|||||||
currentPage.value = 1;
|
currentPage.value = 1;
|
||||||
recalcLoadingKeys.value = {};
|
recalcLoadingKeys.value = {};
|
||||||
if (!queryResults.value.length) {
|
if (!queryResults.value.length) {
|
||||||
const failureText = formatPriceCheckFailures(failures ?? []);
|
applyPriceCheckFailures(failures ?? []);
|
||||||
queryFailureText.value = failureText || t('order.priceCheck.table.empty');
|
|
||||||
} else {
|
} else {
|
||||||
|
clearPriceCheckFailures();
|
||||||
Message.success(t('order.priceCheck.message.querySuccess', { count: queryResults.value.length }));
|
Message.success(t('order.priceCheck.message.querySuccess', { count: queryResults.value.length }));
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
const errText = e?.message || t('order.priceCheck.message.queryFailed');
|
const errText = e?.message || t('order.priceCheck.message.queryFailed');
|
||||||
|
queryFailureItems.value = [];
|
||||||
queryFailureText.value = errText;
|
queryFailureText.value = errText;
|
||||||
queryResults.value = [];
|
queryResults.value = [];
|
||||||
selectedGuideRecord.value = null;
|
selectedGuideRecord.value = null;
|
||||||
@ -631,7 +627,7 @@
|
|||||||
selectedGuideRecord.value = null;
|
selectedGuideRecord.value = null;
|
||||||
resetRecommendState();
|
resetRecommendState();
|
||||||
currentPage.value = 1;
|
currentPage.value = 1;
|
||||||
queryFailureText.value = '';
|
clearPriceCheckFailures();
|
||||||
sortMode.value = 'recommend';
|
sortMode.value = 'recommend';
|
||||||
couponInstanceId.value = undefined;
|
couponInstanceId.value = undefined;
|
||||||
availableCoupons.value = [];
|
availableCoupons.value = [];
|
||||||
@ -682,7 +678,7 @@
|
|||||||
const replacement =
|
const replacement =
|
||||||
nextRows.find((row) => String(row.productCode || '').trim() === productCode) || nextRows[0];
|
nextRows.find((row) => String(row.productCode || '').trim() === productCode) || nextRows[0];
|
||||||
if (!replacement) {
|
if (!replacement) {
|
||||||
const failureText = formatPriceCheckFailures(failures ?? []);
|
const failureText = formatPriceCheckFailures(failures ?? [], failureStepLabel);
|
||||||
Message.warning(failureText || t('order.priceCheck.message.recalcNoQuote'));
|
Message.warning(failureText || t('order.priceCheck.message.recalcNoQuote'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1278,7 +1274,13 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="!queryLoading && hasQueried" class="quote-empty">
|
<div v-else-if="!queryLoading && hasQueried" class="quote-empty">
|
||||||
<a-empty :description="queryFailureText || t('order.priceCheck.table.empty')">
|
<a-empty
|
||||||
|
:description="
|
||||||
|
queryFailureItems.length
|
||||||
|
? t('order.priceCheck.table.emptyWithFailures', { count: queryFailureItems.length })
|
||||||
|
: queryFailureText || t('order.priceCheck.table.empty')
|
||||||
|
"
|
||||||
|
>
|
||||||
<template #image />
|
<template #image />
|
||||||
<p class="empty-hint">{{ t('order.priceCheck.table.emptyHint') }}</p>
|
<p class="empty-hint">{{ t('order.priceCheck.table.emptyHint') }}</p>
|
||||||
</a-empty>
|
</a-empty>
|
||||||
@ -1292,6 +1294,7 @@
|
|||||||
<div class="price-check-aside">
|
<div class="price-check-aside">
|
||||||
<ChannelGuidePanel
|
<ChannelGuidePanel
|
||||||
:record="selectedGuideRecord"
|
:record="selectedGuideRecord"
|
||||||
|
:failure-items="!queryResults.length ? queryFailureItems : []"
|
||||||
:failure-text="!queryResults.length ? queryFailureText : ''"
|
:failure-text="!queryResults.length ? queryFailureText : ''"
|
||||||
:recommend-reason="recommendReason"
|
:recommend-reason="recommendReason"
|
||||||
:recommend-verdict="recommendVerdict"
|
:recommend-verdict="recommendVerdict"
|
||||||
|
|||||||
109
src/views/order/priceCheck/priceCheckFailures.ts
Normal file
109
src/views/order/priceCheck/priceCheckFailures.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
export type PriceCheckFailureRaw = {
|
||||||
|
productId?: number;
|
||||||
|
ProductId?: number;
|
||||||
|
productCode?: string;
|
||||||
|
ProductCode?: string;
|
||||||
|
productName?: string;
|
||||||
|
ProductName?: string;
|
||||||
|
cargoTypeName?: string;
|
||||||
|
CargoTypeName?: string;
|
||||||
|
warehouseName?: string;
|
||||||
|
WarehouseName?: string;
|
||||||
|
step?: string;
|
||||||
|
Step?: string;
|
||||||
|
reason?: string;
|
||||||
|
Reason?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PriceCheckFailureItem = {
|
||||||
|
productId: number;
|
||||||
|
productCode: string;
|
||||||
|
productName: string;
|
||||||
|
cargoTypeName: string;
|
||||||
|
warehouseName: string;
|
||||||
|
step: string;
|
||||||
|
reason: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PriceCheckFailureGroup = {
|
||||||
|
key: string;
|
||||||
|
productName: string;
|
||||||
|
productCode: string;
|
||||||
|
items: PriceCheckFailureItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const STEP_ORDER = ['product', 'snapshot', 'blacklist', 'weight', 'zone', 'price'];
|
||||||
|
|
||||||
|
function pick(raw: PriceCheckFailureRaw, camel: keyof PriceCheckFailureRaw, pascal: keyof PriceCheckFailureRaw) {
|
||||||
|
return String(raw[camel] ?? raw[pascal] ?? '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripReasonContextPrefix(reason: string) {
|
||||||
|
return reason.replace(/^\[[^\]]*(货物类型|仓库)[^\]]*\]\s*/, '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizePriceCheckFailures(failures: PriceCheckFailureRaw[] = []): PriceCheckFailureItem[] {
|
||||||
|
const items = (failures ?? [])
|
||||||
|
.map((raw) => {
|
||||||
|
const reason = stripReasonContextPrefix(pick(raw, 'reason', 'Reason'));
|
||||||
|
if (!reason) return null;
|
||||||
|
const productId = Number(raw.productId ?? raw.ProductId ?? 0) || 0;
|
||||||
|
return {
|
||||||
|
productId,
|
||||||
|
productCode: pick(raw, 'productCode', 'ProductCode'),
|
||||||
|
productName: pick(raw, 'productName', 'ProductName'),
|
||||||
|
cargoTypeName: pick(raw, 'cargoTypeName', 'CargoTypeName'),
|
||||||
|
warehouseName: pick(raw, 'warehouseName', 'WarehouseName'),
|
||||||
|
step: pick(raw, 'step', 'Step').toLowerCase(),
|
||||||
|
reason,
|
||||||
|
} as PriceCheckFailureItem;
|
||||||
|
})
|
||||||
|
.filter((item): item is PriceCheckFailureItem => !!item);
|
||||||
|
|
||||||
|
return items.sort((a, b) => {
|
||||||
|
const nameCmp = (a.productName || a.productCode).localeCompare(b.productName || b.productCode, 'zh');
|
||||||
|
if (nameCmp) return nameCmp;
|
||||||
|
const cargoCmp = a.cargoTypeName.localeCompare(b.cargoTypeName, 'zh');
|
||||||
|
if (cargoCmp) return cargoCmp;
|
||||||
|
const warehouseCmp = a.warehouseName.localeCompare(b.warehouseName, 'zh');
|
||||||
|
if (warehouseCmp) return warehouseCmp;
|
||||||
|
return STEP_ORDER.indexOf(a.step) - STEP_ORDER.indexOf(b.step);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function groupPriceCheckFailures(items: PriceCheckFailureItem[]): PriceCheckFailureGroup[] {
|
||||||
|
const groups: PriceCheckFailureGroup[] = [];
|
||||||
|
const index = new Map<string, PriceCheckFailureGroup>();
|
||||||
|
for (const item of items) {
|
||||||
|
const key = item.productId
|
||||||
|
? `id:${item.productId}`
|
||||||
|
: `name:${item.productName || item.productCode || '_'}`;
|
||||||
|
let group = index.get(key);
|
||||||
|
if (!group) {
|
||||||
|
group = {
|
||||||
|
key,
|
||||||
|
productName: item.productName,
|
||||||
|
productCode: item.productCode,
|
||||||
|
items: [],
|
||||||
|
};
|
||||||
|
index.set(key, group);
|
||||||
|
groups.push(group);
|
||||||
|
}
|
||||||
|
group.items.push(item);
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatPriceCheckFailures(
|
||||||
|
failures: PriceCheckFailureRaw[],
|
||||||
|
stepLabel: (step: string) => string,
|
||||||
|
) {
|
||||||
|
const items = normalizePriceCheckFailures(failures);
|
||||||
|
return items
|
||||||
|
.map((item, idx) => {
|
||||||
|
const product = item.productName || item.productCode || `产品${idx + 1}`;
|
||||||
|
const meta = [item.cargoTypeName, item.warehouseName, stepLabel(item.step)].filter(Boolean).join(' / ');
|
||||||
|
return `${idx + 1}. ${product}${meta ? `(${meta})` : ''}:${item.reason}`;
|
||||||
|
})
|
||||||
|
.join('\n');
|
||||||
|
}
|
||||||
@ -88,6 +88,7 @@ import { useI18n } from 'vue-i18n'
|
|||||||
import { Message, Modal } from '@arco-design/web-vue'
|
import { Message, Modal } from '@arco-design/web-vue'
|
||||||
import type { CustomerOpenApiCredentialDto } from '@/api/customer'
|
import type { CustomerOpenApiCredentialDto } from '@/api/customer'
|
||||||
import { CustomerService } from '@/api/customer'
|
import { CustomerService } from '@/api/customer'
|
||||||
|
import { useRbacUserStore } from '@/store/modules/rbac-user'
|
||||||
|
|
||||||
defineOptions({ name: 'CustomerOpenApiCredentialPanel' })
|
defineOptions({ name: 'CustomerOpenApiCredentialPanel' })
|
||||||
|
|
||||||
@ -107,8 +108,14 @@ const credential = reactive<CustomerOpenApiCredentialDto>({
|
|||||||
secretRevealed: false,
|
secretRevealed: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const rbacUserStore = useRbacUserStore()
|
||||||
|
|
||||||
const isAdminMode = () => props.customerId != null && props.customerId !== ''
|
const isAdminMode = () => props.customerId != null && props.customerId !== ''
|
||||||
|
|
||||||
|
function canCallMineApi() {
|
||||||
|
return rbacUserStore.isCustomerPortal()
|
||||||
|
}
|
||||||
|
|
||||||
function applyCredential(data: CustomerOpenApiCredentialDto) {
|
function applyCredential(data: CustomerOpenApiCredentialDto) {
|
||||||
Object.assign(credential, {
|
Object.assign(credential, {
|
||||||
id: data.id,
|
id: data.id,
|
||||||
@ -126,6 +133,7 @@ function applyCredential(data: CustomerOpenApiCredentialDto) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
|
if (!isAdminMode() && !canCallMineApi()) return
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const data = isAdminMode()
|
const data = isAdminMode()
|
||||||
@ -140,6 +148,7 @@ async function load() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleEnsure() {
|
async function handleEnsure() {
|
||||||
|
if (!isAdminMode() && !canCallMineApi()) return
|
||||||
busy.value = true
|
busy.value = true
|
||||||
try {
|
try {
|
||||||
const data = isAdminMode()
|
const data = isAdminMode()
|
||||||
@ -161,6 +170,7 @@ function handleReset() {
|
|||||||
title: t('user.customer.openapi.resetConfirmTitle'),
|
title: t('user.customer.openapi.resetConfirmTitle'),
|
||||||
content: t('user.customer.openapi.resetConfirmContent'),
|
content: t('user.customer.openapi.resetConfirmContent'),
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
|
if (!isAdminMode() && !canCallMineApi()) return
|
||||||
busy.value = true
|
busy.value = true
|
||||||
try {
|
try {
|
||||||
const data = isAdminMode()
|
const data = isAdminMode()
|
||||||
@ -178,6 +188,7 @@ function handleReset() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function onEnabledChange(value: string | number | boolean) {
|
async function onEnabledChange(value: string | number | boolean) {
|
||||||
|
if (!isAdminMode() && !canCallMineApi()) return
|
||||||
const enabled = Boolean(value)
|
const enabled = Boolean(value)
|
||||||
busy.value = true
|
busy.value = true
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -54,7 +54,7 @@
|
|||||||
|
|
||||||
<div ref="bodyRef" class="psd__body">
|
<div ref="bodyRef" class="psd__body">
|
||||||
<KeepAlive :max="5">
|
<KeepAlive :max="5">
|
||||||
<component :is="activePanel" />
|
<component :is="visibleModel ? activePanel : null" />
|
||||||
</KeepAlive>
|
</KeepAlive>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -119,7 +119,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- 5. 开放接口凭证(仅主客户) -->
|
<!-- 5. 开放接口凭证(仅主客户) -->
|
||||||
<section v-if="isMainCustomer" class="profile-card profile-card--full">
|
<section v-if="showOpenApiCredential" class="profile-card profile-card--full">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3 class="card-title">{{ t('user.customer.openapi.title') }}</h3>
|
<h3 class="card-title">{{ t('user.customer.openapi.title') }}</h3>
|
||||||
</div>
|
</div>
|
||||||
@ -231,7 +231,17 @@ const profile = ref<CustomerDto>({})
|
|||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const loadError = ref('')
|
const loadError = ref('')
|
||||||
|
|
||||||
const isMainCustomer = computed(() => profile.value.parentCustomerId == null)
|
const isCustomerPortal = computed(() => rbacUserStore.isCustomerPortal())
|
||||||
|
|
||||||
|
const isMainCustomer = computed(() => {
|
||||||
|
const id = Number(profile.value.id)
|
||||||
|
if (!Number.isFinite(id) || id <= 0) return false
|
||||||
|
return profile.value.parentCustomerId == null
|
||||||
|
})
|
||||||
|
|
||||||
|
const showOpenApiCredential = computed(
|
||||||
|
() => isCustomerPortal.value && isMainCustomer.value,
|
||||||
|
)
|
||||||
|
|
||||||
const editModalVisible = ref(false)
|
const editModalVisible = ref(false)
|
||||||
const editFormRef = ref()
|
const editFormRef = ref()
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import { Modal } from '@arco-design/web-vue'
|
import { Modal } from '@arco-design/web-vue'
|
||||||
import { IconDelete, IconImage, IconQuestionCircle } from '@arco-design/web-vue/es/icon'
|
import { IconDelete, IconDownload, IconImage, IconQuestionCircle } from '@arco-design/web-vue/es/icon'
|
||||||
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'
|
||||||
@ -42,6 +42,7 @@ import { buildStatusTagHtml, type StatusTagTone } from '@/utils/statusTag'
|
|||||||
import { useWarehouseAdminCustomer } from '@/composables/useWarehouseAdminCustomer'
|
import { useWarehouseAdminCustomer } from '@/composables/useWarehouseAdminCustomer'
|
||||||
import { setRapidPackageHostCookie, setRapidPackageWarehouseCookie, setRapidPackageCustomerCookie } from '@/utils/rapidPackagePluginHost'
|
import { setRapidPackageHostCookie, setRapidPackageWarehouseCookie, setRapidPackageCustomerCookie } from '@/utils/rapidPackagePluginHost'
|
||||||
import { useCurrentWarehouseStore } from '@/store'
|
import { useCurrentWarehouseStore } from '@/store'
|
||||||
|
import { PRINT_CONTROL_DOWNLOAD_URL } from '@/constants/printControl'
|
||||||
|
|
||||||
defineOptions({ name: 'WarehouseParcel' })
|
defineOptions({ name: 'WarehouseParcel' })
|
||||||
|
|
||||||
@ -661,6 +662,18 @@ onMounted(async () => {
|
|||||||
:items="slidingStatusTabs"
|
:items="slidingStatusTabs"
|
||||||
@change="handleStatusTabChange"
|
@change="handleStatusTabChange"
|
||||||
/>
|
/>
|
||||||
|
<p class="parcel-plugin-hint">
|
||||||
|
<span>{{ t('warehouse.parcel.pluginHint') }}</span>
|
||||||
|
<a
|
||||||
|
class="parcel-plugin-hint__link"
|
||||||
|
:href="PRINT_CONTROL_DOWNLOAD_URL"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<icon-download />
|
||||||
|
{{ t('warehouse.parcel.pluginHintDownload') }}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-toolbar">
|
<div class="table-toolbar">
|
||||||
<div class="toolbar-left">
|
<div class="toolbar-left">
|
||||||
@ -1055,6 +1068,33 @@ onMounted(async () => {
|
|||||||
@import '@/styles/warehouse-list-page.less';
|
@import '@/styles/warehouse-list-page.less';
|
||||||
@import '@/styles/warehouse-form-modal.less';
|
@import '@/styles/warehouse-form-modal.less';
|
||||||
|
|
||||||
|
.parcel-plugin-hint {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px 8px;
|
||||||
|
margin: 0;
|
||||||
|
min-width: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--color-text-3);
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parcel-plugin-hint__link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
color: var(--app-primary-color);
|
||||||
|
white-space: nowrap;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parcel-plugin-hint__link:hover {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
.parcel-detail-drawer {
|
.parcel-detail-drawer {
|
||||||
&__meta {
|
&__meta {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user