feat: Handsontable 混合表头与请求日志改造,并完善 H5 下单/登录体验
统一 HandsontablePro 液态筛选表头、批量输入与自动列增强;请求日志改用公共列表组件;同步收件地址解析与 H5/PC 下单相关改动。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
37d534edc3
commit
407a0f7be6
@ -320,6 +320,8 @@ itemList: [
|
|||||||
| `key` | `string` | 对应行数据字段名(如 `jobId`、`statusLabel`) |
|
| `key` | `string` | 对应行数据字段名(如 `jobId`、`statusLabel`) |
|
||||||
| `label` | `string` | 表头文案(支持 `t('...')`) |
|
| `label` | `string` | 表头文案(支持 `t('...')`) |
|
||||||
| `width` | `number` | 列宽(像素) |
|
| `width` | `number` | 列宽(像素) |
|
||||||
|
| `headerMode` | `'inline' \| 'icon' \| 'none'` | 混合表头:内嵌搜索 / 仅图标 / 纯展示;未设时由 `searchable` + `filterType` 推导 |
|
||||||
|
| `searchable` / `searchField` / `multiValue` | — | 表头搜索;`multiValue` 显示「多」批量入口 |
|
||||||
|
|
||||||
**正确示例:**
|
**正确示例:**
|
||||||
|
|
||||||
|
|||||||
@ -1650,6 +1650,63 @@ function throwParcelCreateBusinessError(body: any, fallbackMsg: string): never {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface OrderReceiverParseAddressResult {
|
||||||
|
receiver: {
|
||||||
|
recipientsName?: string
|
||||||
|
companyName?: string
|
||||||
|
phone?: string
|
||||||
|
telePhone?: string
|
||||||
|
email?: string
|
||||||
|
countryCode?: string
|
||||||
|
province?: string
|
||||||
|
city?: string
|
||||||
|
streetNumber?: string
|
||||||
|
streetAddress1?: string
|
||||||
|
streetAddress2?: string
|
||||||
|
streetAddress3?: string
|
||||||
|
zipCode?: string
|
||||||
|
}
|
||||||
|
isComplete: boolean
|
||||||
|
missingRequired: string[]
|
||||||
|
missingRecommended: string[]
|
||||||
|
warnings: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** POST /order/receiver/parse-address — 粘贴地址 AI 解析 */
|
||||||
|
export async function postOrderReceiverParseAddress(body: {
|
||||||
|
rawText: string
|
||||||
|
providerKey?: string
|
||||||
|
model?: string
|
||||||
|
}): Promise<OrderReceiverParseAddressResult> {
|
||||||
|
const res = await axiosInstance.post(orderUrl('/order/receiver/parse-address'), body)
|
||||||
|
assertBusinessOk(res.data)
|
||||||
|
const data = unwrapApiData<any>(res.data)
|
||||||
|
const receiver = data?.receiver ?? data?.Receiver ?? {}
|
||||||
|
const pick = (a: string, b: string) =>
|
||||||
|
String(receiver?.[a] ?? receiver?.[b] ?? '').trim() || undefined
|
||||||
|
return {
|
||||||
|
receiver: {
|
||||||
|
recipientsName: pick('recipientsName', 'RecipientsName'),
|
||||||
|
companyName: pick('companyName', 'CompanyName'),
|
||||||
|
phone: pick('phone', 'Phone'),
|
||||||
|
telePhone: pick('telePhone', 'TelePhone'),
|
||||||
|
email: pick('email', 'Email'),
|
||||||
|
countryCode: pick('countryCode', 'CountryCode'),
|
||||||
|
province: pick('province', 'Province'),
|
||||||
|
city: pick('city', 'City'),
|
||||||
|
streetNumber: pick('streetNumber', 'StreetNumber'),
|
||||||
|
streetAddress1: pick('streetAddress1', 'StreetAddress1'),
|
||||||
|
streetAddress2: pick('streetAddress2', 'StreetAddress2'),
|
||||||
|
streetAddress3: pick('streetAddress3', 'StreetAddress3'),
|
||||||
|
zipCode: pick('zipCode', 'ZipCode'),
|
||||||
|
},
|
||||||
|
isComplete: Boolean(data?.isComplete ?? data?.IsComplete),
|
||||||
|
missingRequired: (data?.missingRequired ?? data?.MissingRequired ?? []) as string[],
|
||||||
|
missingRecommended: (data?.missingRecommended ?? data?.MissingRecommended ?? []) as string[],
|
||||||
|
warnings: (data?.warnings ?? data?.Warnings ?? []) as string[],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** POST /order/parcel/create */
|
/** POST /order/parcel/create */
|
||||||
export async function postOrderParcelCreate(body: Record<string, any>): Promise<any> {
|
export async function postOrderParcelCreate(body: Record<string, any>): Promise<any> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
2
src/components.d.ts
vendored
2
src/components.d.ts
vendored
@ -8,6 +8,7 @@ export {}
|
|||||||
/* prettier-ignore */
|
/* prettier-ignore */
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
|
ActiveFilterBar: typeof import('./components/HandsontablePro/ActiveFilterBar.vue')['default']
|
||||||
AiAdvisorShell: typeof import('./components/NanaAssistant/AiAdvisorShell.vue')['default']
|
AiAdvisorShell: typeof import('./components/NanaAssistant/AiAdvisorShell.vue')['default']
|
||||||
ApiTest: typeof import('./components/ApiTest/index.vue')['default']
|
ApiTest: typeof import('./components/ApiTest/index.vue')['default']
|
||||||
BasicLayoutLeftRight: typeof import('./components/BasicLayoutLeftRight/index.vue')['default']
|
BasicLayoutLeftRight: typeof import('./components/BasicLayoutLeftRight/index.vue')['default']
|
||||||
@ -27,6 +28,7 @@ declare module 'vue' {
|
|||||||
FormWrapper: typeof import('./components/global-setting/form-wrapper.vue')['default']
|
FormWrapper: typeof import('./components/global-setting/form-wrapper.vue')['default']
|
||||||
GlobalSetting: typeof import('./components/global-setting/index.vue')['default']
|
GlobalSetting: typeof import('./components/global-setting/index.vue')['default']
|
||||||
HandsontablePro: typeof import('./components/HandsontablePro/index.vue')['default']
|
HandsontablePro: typeof import('./components/HandsontablePro/index.vue')['default']
|
||||||
|
HeaderFilterPopover: typeof import('./components/HandsontablePro/HeaderFilterPopover.vue')['default']
|
||||||
InterfaceDetail: typeof import('./components/InterfaceDetail/index.vue')['default']
|
InterfaceDetail: typeof import('./components/InterfaceDetail/index.vue')['default']
|
||||||
InvitePoster: typeof import('./components/InvitePoster.vue')['default']
|
InvitePoster: typeof import('./components/InvitePoster.vue')['default']
|
||||||
InviteQrImage: typeof import('./components/InviteQrImage.vue')['default']
|
InviteQrImage: typeof import('./components/InviteQrImage.vue')['default']
|
||||||
|
|||||||
124
src/components/HandsontablePro/ActiveFilterBar.vue
Normal file
124
src/components/HandsontablePro/ActiveFilterBar.vue
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
export type ActiveFilterItem = {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
text: string;
|
||||||
|
kind: 'filter' | 'sort';
|
||||||
|
};
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
items: ActiveFilterItem[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
clear: [key: string];
|
||||||
|
'clear-all': [];
|
||||||
|
'item-click': [key: string];
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="items.length" class="ht-active-filter-bar">
|
||||||
|
<span class="ht-active-filter-bar__title">已筛选</span>
|
||||||
|
<button
|
||||||
|
v-for="item in items"
|
||||||
|
:key="`${item.kind}-${item.key}`"
|
||||||
|
type="button"
|
||||||
|
class="ht-active-filter-bar__pill"
|
||||||
|
:class="{ 'is-sort': item.kind === 'sort' }"
|
||||||
|
:title="item.text"
|
||||||
|
@click="emit('item-click', item.key)"
|
||||||
|
>
|
||||||
|
<span class="ht-active-filter-bar__pill-text">{{ item.label }} · {{ item.text }}</span>
|
||||||
|
<span
|
||||||
|
class="ht-active-filter-bar__pill-x"
|
||||||
|
title="清除"
|
||||||
|
@click.stop="emit('clear', item.key)"
|
||||||
|
>×</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="ht-active-filter-bar__clear" @click="emit('clear-all')">
|
||||||
|
清除全部
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.ht-active-filter-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
background: color-mix(in srgb, var(--app-primary-color, #007979) 8%, #fff);
|
||||||
|
border: 1px solid #e5e6eb;
|
||||||
|
border-radius: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ht-active-filter-bar__title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #1d2129;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ht-active-filter-bar__pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
max-width: 280px;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0 6px 0 8px;
|
||||||
|
border: 1px solid color-mix(in srgb, var(--app-primary-color, #007979) 35%, #e5e6eb);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: #fff;
|
||||||
|
color: var(--app-primary-color, #007979);
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.is-sort {
|
||||||
|
border-color: #00b42a;
|
||||||
|
color: #00b42a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f7f8fa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ht-active-filter-bar__pill-text {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ht-active-filter-bar__pill-x {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
line-height: 14px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 2px;
|
||||||
|
color: #86909c;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #f2f3f5;
|
||||||
|
color: #1d2129;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ht-active-filter-bar__clear {
|
||||||
|
margin-left: auto;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: #86909c;
|
||||||
|
font-size: 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 4px;
|
||||||
|
height: 24px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--app-primary-color, #007979);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1134
src/components/HandsontablePro/HeaderFilterPopover.vue
Normal file
1134
src/components/HandsontablePro/HeaderFilterPopover.vue
Normal file
File diff suppressed because it is too large
Load Diff
104
src/components/HandsontablePro/filterValue.ts
Normal file
104
src/components/HandsontablePro/filterValue.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
/** 表头条件过滤值编码:__EMPTY__ / __eq__:x / __ne__:x / __gt__:x / __gte__:x / __lt__:x / __lte__:x / 明文(包含) */
|
||||||
|
|
||||||
|
export type FilterOp = 'contains' | 'equals' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'empty';
|
||||||
|
|
||||||
|
const OP_PREFIX_RE = /^__(eq|ne|gt|gte|lt|lte)__:(.*)$/s;
|
||||||
|
|
||||||
|
const OP_LABEL: Record<FilterOp, string> = {
|
||||||
|
contains: '包含',
|
||||||
|
equals: '等于',
|
||||||
|
ne: '不等于',
|
||||||
|
gt: '大于',
|
||||||
|
gte: '大于等于',
|
||||||
|
lt: '小于',
|
||||||
|
lte: '小于等于',
|
||||||
|
empty: '为空',
|
||||||
|
};
|
||||||
|
|
||||||
|
const PREFIX_TO_OP: Record<string, FilterOp> = {
|
||||||
|
eq: 'equals',
|
||||||
|
ne: 'ne',
|
||||||
|
gt: 'gt',
|
||||||
|
gte: 'gte',
|
||||||
|
lt: 'lt',
|
||||||
|
lte: 'lte',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function encodeFilterValue(op: FilterOp, value = ''): string {
|
||||||
|
const v = (value || '').trim();
|
||||||
|
if (op === 'empty') return '__EMPTY__';
|
||||||
|
if (op === 'contains') return v;
|
||||||
|
if (op === 'equals') return v ? `__eq__:${v}` : '';
|
||||||
|
if (op === 'ne') return v ? `__ne__:${v}` : '';
|
||||||
|
if (op === 'gt') return v ? `__gt__:${v}` : '';
|
||||||
|
if (op === 'gte') return v ? `__gte__:${v}` : '';
|
||||||
|
if (op === 'lt') return v ? `__lt__:${v}` : '';
|
||||||
|
if (op === 'lte') return v ? `__lte__:${v}` : '';
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseFilterValue(raw: string): { op: FilterOp; value: string } {
|
||||||
|
const text = (raw || '').trim();
|
||||||
|
if (!text) return { op: 'contains', value: '' };
|
||||||
|
if (text === '__EMPTY__') return { op: 'empty', value: '' };
|
||||||
|
const m = text.match(OP_PREFIX_RE);
|
||||||
|
if (m) {
|
||||||
|
return { op: PREFIX_TO_OP[m[1]] || 'contains', value: m[2] ?? '' };
|
||||||
|
}
|
||||||
|
return { op: 'contains', value: text };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatFilterOpDisplay(raw: string): string {
|
||||||
|
const { op, value } = parseFilterValue(raw);
|
||||||
|
if (op === 'empty') return OP_LABEL.empty;
|
||||||
|
if (!value) return '';
|
||||||
|
if (op === 'contains') return value;
|
||||||
|
return `${OP_LABEL[op]} ${value}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 是否需要在当前页做本地条件匹配(服务端不支持的运算符) */
|
||||||
|
export function needsLocalOpFilter(raw: string): boolean {
|
||||||
|
const { op } = parseFilterValue(raw);
|
||||||
|
return op === 'empty' || op === 'ne' || op === 'gt' || op === 'gte' || op === 'lt' || op === 'lte' || op === 'equals';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 提交给后端的明文检索值(高级运算本地处理,不透传编码) */
|
||||||
|
export function toServerSearchValue(raw: string): string {
|
||||||
|
const { op, value } = parseFilterValue(raw);
|
||||||
|
if (op === 'contains' || op === 'equals') return value;
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEmptyCellValue(val: any): boolean {
|
||||||
|
return val == null || String(val).trim() === '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function matchFilterValue(cellVal: any, raw: string, asNumber = false): boolean {
|
||||||
|
const { op, value } = parseFilterValue(raw);
|
||||||
|
if (op === 'empty') return isEmptyCellValue(cellVal);
|
||||||
|
if (!value) return true;
|
||||||
|
|
||||||
|
if (asNumber || op === 'gt' || op === 'gte' || op === 'lt' || op === 'lte') {
|
||||||
|
const n = Number(value);
|
||||||
|
const cv = Number(cellVal);
|
||||||
|
if (Number.isNaN(n)) return true;
|
||||||
|
if (Number.isNaN(cv) || isEmptyCellValue(cellVal)) return false;
|
||||||
|
if (op === 'equals' || (asNumber && op === 'contains')) return cv === n;
|
||||||
|
if (op === 'ne') return cv !== n;
|
||||||
|
if (op === 'gt') return cv > n;
|
||||||
|
if (op === 'gte') return cv >= n;
|
||||||
|
if (op === 'lt') return cv < n;
|
||||||
|
if (op === 'lte') return cv <= n;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cell = String(cellVal ?? '');
|
||||||
|
const cellLower = cell.toLowerCase();
|
||||||
|
const needle = value.toLowerCase();
|
||||||
|
if (op === 'equals') return cellLower === needle;
|
||||||
|
if (op === 'ne') return cellLower !== needle;
|
||||||
|
if (op === 'gte') return cellLower >= needle;
|
||||||
|
if (op === 'lte') return cellLower <= needle;
|
||||||
|
if (op === 'gt') return cellLower > needle;
|
||||||
|
if (op === 'lt') return cellLower < needle;
|
||||||
|
return cellLower.includes(needle);
|
||||||
|
}
|
||||||
96
src/components/HandsontablePro/hybridHeader.ts
Normal file
96
src/components/HandsontablePro/hybridHeader.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import type { ColumnDef, FilterType, HeaderMode } from './types';
|
||||||
|
|
||||||
|
/** 操作/展示列:不生成混合表头 */
|
||||||
|
const HEADER_NONE_KEY_RE =
|
||||||
|
/^(action|actions|operation|operate|ops|view|retry|cancel|image|avatar|checkbox|settings|config|btn|button)s?$/i;
|
||||||
|
|
||||||
|
/** 日期列 */
|
||||||
|
const DATE_KEY_RE =
|
||||||
|
/(Time|Date|At)$|^(create|update|modify|pay|ship|sign|receive|finish|bind|eligible|settle|arrival|expected|valid|auth|token|pickup|processed|finished|newestTrack)/i;
|
||||||
|
|
||||||
|
/** 更精确的日期字段 */
|
||||||
|
const DATE_KEY_STRICT_RE =
|
||||||
|
/(Time|Date|At)$/i;
|
||||||
|
|
||||||
|
/** 数值列 */
|
||||||
|
const NUMBER_KEY_RE =
|
||||||
|
/(Qty|Count|Amount|Points|Weight|Balance|Rate|Price|Quota|Discount|Span|Sort|Size|Height|Width|Length|Volume)$|^(min|max|total|available|frozen|qty|count|amount|points|weight|balance|quota|sort|retry|fail|extra)/i;
|
||||||
|
|
||||||
|
function looksLikeDateKey(key: string): boolean {
|
||||||
|
if (DATE_KEY_STRICT_RE.test(key)) return true;
|
||||||
|
// newestTrackTime 等
|
||||||
|
if (/trackTime|createTime|updateTime|payTime|shipTime/i.test(key)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function looksLikeNumberKey(key: string): boolean {
|
||||||
|
if (looksLikeDateKey(key)) return false;
|
||||||
|
return NUMBER_KEY_RE.test(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为未配置混合表头的列自动补齐 headerMode / filterType 等。
|
||||||
|
* - 已显式配置 headerMode / searchable / searchField / filterType 的列不覆盖
|
||||||
|
* - 默认 serverFilter=false(当前页过滤);页面若要走服务端可自行设 serverFilter:true + searchParams/@search
|
||||||
|
*/
|
||||||
|
export function enrichHybridColumn(col: ColumnDef, enabled: boolean): ColumnDef {
|
||||||
|
if (!enabled || !col?.key) return col;
|
||||||
|
|
||||||
|
// 已手工配置过:只补 searchField,不改模式
|
||||||
|
const alreadyConfigured = !!(
|
||||||
|
col.headerMode ||
|
||||||
|
col.searchable ||
|
||||||
|
col.searchField ||
|
||||||
|
col.filterType ||
|
||||||
|
col.filterable
|
||||||
|
);
|
||||||
|
if (alreadyConfigured) {
|
||||||
|
if ((col.searchable || col.filterable || col.filterType || col.headerMode === 'inline' || col.headerMode === 'icon') && !col.searchField) {
|
||||||
|
return { ...col, searchField: col.key };
|
||||||
|
}
|
||||||
|
return col;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HEADER_NONE_KEY_RE.test(col.key)) {
|
||||||
|
return { ...col, headerMode: 'none' as HeaderMode };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (looksLikeDateKey(col.key)) {
|
||||||
|
return {
|
||||||
|
...col,
|
||||||
|
headerMode: 'icon' as HeaderMode,
|
||||||
|
filterType: (col.filterType || 'date') as FilterType,
|
||||||
|
filterable: true,
|
||||||
|
searchable: true,
|
||||||
|
searchField: col.key,
|
||||||
|
sortable: col.sortable !== false,
|
||||||
|
serverFilter: col.serverFilter !== undefined ? col.serverFilter : false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (looksLikeNumberKey(col.key)) {
|
||||||
|
return {
|
||||||
|
...col,
|
||||||
|
headerMode: 'icon' as HeaderMode,
|
||||||
|
filterType: (col.filterType || 'number') as FilterType,
|
||||||
|
filterable: true,
|
||||||
|
searchable: true,
|
||||||
|
searchField: col.key,
|
||||||
|
sortable: col.sortable !== false,
|
||||||
|
serverFilter: col.serverFilter !== undefined ? col.serverFilter : false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...col,
|
||||||
|
headerMode: 'inline' as HeaderMode,
|
||||||
|
searchable: true,
|
||||||
|
searchField: col.key,
|
||||||
|
sortable: col.sortable !== false,
|
||||||
|
serverFilter: col.serverFilter !== undefined ? col.serverFilter : false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function enrichHybridColumns(columns: ColumnDef[], enabled: boolean): ColumnDef[] {
|
||||||
|
return (columns || []).map((c) => enrichHybridColumn(c, enabled));
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,10 +1,23 @@
|
|||||||
|
export type HeaderMode = 'inline' | 'icon' | 'none';
|
||||||
|
export type FilterType = 'text' | 'enum' | 'number' | 'date';
|
||||||
|
export type SortOrder = 'asc' | 'desc' | null;
|
||||||
|
export type SortMode = 'local' | 'remote';
|
||||||
|
|
||||||
export interface ColumnDef {
|
export interface ColumnDef {
|
||||||
key: string;
|
key: string;
|
||||||
label: string;
|
label: string;
|
||||||
width: number;
|
width: number;
|
||||||
|
/** 混合表头:内嵌输入 / 仅图标 / 纯展示;未设时由 searchable + filterType 推导 */
|
||||||
|
headerMode?: HeaderMode;
|
||||||
searchable?: boolean;
|
searchable?: boolean;
|
||||||
searchField?: string;
|
searchField?: string;
|
||||||
multiValue?: boolean;
|
multiValue?: boolean;
|
||||||
|
filterable?: boolean;
|
||||||
|
filterType?: FilterType;
|
||||||
|
filterOptions?: Array<{ label: string; value: string | number | boolean }>;
|
||||||
|
sortable?: boolean;
|
||||||
|
/** 默认 true;false 表示仅本页过滤 */
|
||||||
|
serverFilter?: boolean;
|
||||||
renderer?: (row: any, value: any) => string;
|
renderer?: (row: any, value: any) => string;
|
||||||
className?: string;
|
className?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
@ -33,4 +46,19 @@ export interface HandsontableProProps {
|
|||||||
persistColumnOrderKey?: string;
|
persistColumnOrderKey?: string;
|
||||||
lockedColumnKeys?: string[];
|
lockedColumnKeys?: string[];
|
||||||
enableColumnOrderConfig?: boolean;
|
enableColumnOrderConfig?: boolean;
|
||||||
|
enableHeaderSearch?: boolean;
|
||||||
|
/** 未配置 headerMode 的列自动补齐混合表头;默认 true */
|
||||||
|
autoHybridHeader?: boolean;
|
||||||
|
/** 原生 Handsontable 下拉过滤(本页);默认关闭,改用自研 Popover */
|
||||||
|
enableColumnFilter?: boolean;
|
||||||
|
/** 表头排序箭头 */
|
||||||
|
enableHeaderSort?: boolean;
|
||||||
|
/** 表头筛选 ▾(自研 Popover) */
|
||||||
|
enableHeaderFilter?: boolean;
|
||||||
|
/** 表格上方已筛选条件条 */
|
||||||
|
showActiveFilterBar?: boolean;
|
||||||
|
/** local=排当前页;remote=仅 emit sort-change */
|
||||||
|
sortMode?: SortMode;
|
||||||
|
sortField?: string | null;
|
||||||
|
sortOrder?: SortOrder;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -76,7 +76,7 @@ export default function setupUserLoginInfoGuard(router: Router) {
|
|||||||
typeof to.query.redirect === 'string' ? to.query.redirect : '';
|
typeof to.query.redirect === 'string' ? to.query.redirect : '';
|
||||||
const target = resolveCustomerEntryPath(redirect || CUSTOMER_H5_HOME_PATH);
|
const target = resolveCustomerEntryPath(redirect || CUSTOMER_H5_HOME_PATH);
|
||||||
next({
|
next({
|
||||||
path: isLogin() ? target : '/h5/me',
|
path: isLogin() ? target : '/h5/login',
|
||||||
query: !isLogin() && redirect ? { redirect: target } : {},
|
query: !isLogin() && redirect ? { redirect: target } : {},
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
@ -111,7 +111,7 @@ export default function setupUserLoginInfoGuard(router: Router) {
|
|||||||
console.error('客户门户 SSO 登录失败', error);
|
console.error('客户门户 SSO 登录失败', error);
|
||||||
}
|
}
|
||||||
next({
|
next({
|
||||||
path: preferH5Portal() ? '/h5/me' : '/',
|
path: preferH5Portal() ? '/h5/login' : '/',
|
||||||
query: {},
|
query: {},
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
@ -148,11 +148,11 @@ export default function setupUserLoginInfoGuard(router: Router) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// H5 客户中心:未登录统一引导到「我的」(含微信注册/绑定),避免跳 PC 登录大页
|
// H5 客户中心:未登录统一引导到 /h5/login,避免「我的」再套一套登录页
|
||||||
if (!isLogin() && String(to.path || '').startsWith('/h5')) {
|
if (!isLogin() && String(to.path || '').startsWith('/h5')) {
|
||||||
NProgress.done();
|
NProgress.done();
|
||||||
next({
|
next({
|
||||||
path: '/h5/me',
|
path: '/h5/login',
|
||||||
query: { redirect: to.fullPath },
|
query: { redirect: to.fullPath },
|
||||||
replace: true,
|
replace: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -64,7 +64,7 @@ const router = createRouter({
|
|||||||
const token =
|
const token =
|
||||||
localStorage.getItem('rbac_access_token') ||
|
localStorage.getItem('rbac_access_token') ||
|
||||||
localStorage.getItem('token');
|
localStorage.getItem('token');
|
||||||
return token ? '/h5/orders' : '/h5/me';
|
return token ? '/h5/orders' : '/h5/login';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -111,7 +111,7 @@ const router = createRouter({
|
|||||||
name: 'H5Me',
|
name: 'H5Me',
|
||||||
component: () => import('@/views/h5/customer-center/me/index.vue'),
|
component: () => import('@/views/h5/customer-center/me/index.vue'),
|
||||||
meta: {
|
meta: {
|
||||||
requiresAuth: false,
|
requiresAuth: true,
|
||||||
locale: 'h5.tab.me',
|
locale: 'h5.tab.me',
|
||||||
h5HideHeader: true,
|
h5HideHeader: true,
|
||||||
},
|
},
|
||||||
@ -190,7 +190,7 @@ const router = createRouter({
|
|||||||
const token =
|
const token =
|
||||||
localStorage.getItem('rbac_access_token') ||
|
localStorage.getItem('rbac_access_token') ||
|
||||||
localStorage.getItem('token');
|
localStorage.getItem('token');
|
||||||
return token ? '/h5/orders' : '/h5/me';
|
return token ? '/h5/orders' : '/h5/login';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@ -206,11 +206,11 @@ export function resolvePostLogoutLogin(options?: {
|
|||||||
routeName = resolveLoginRouteNameFromPath(path)
|
routeName = resolveLoginRouteNameFromPath(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 手机/微信客户:回 H5「我的」登录引导,不进 PC 登录大页
|
// 手机/微信客户:回 H5 登录页,不进 PC 登录大页
|
||||||
if (routeName === CUSTOMER_LOGIN_ROUTE_NAME && preferH5Portal()) {
|
if (routeName === CUSTOMER_LOGIN_ROUTE_NAME && preferH5Portal()) {
|
||||||
return {
|
return {
|
||||||
name: CUSTOMER_LOGIN_ROUTE_NAME,
|
name: CUSTOMER_LOGIN_ROUTE_NAME,
|
||||||
path: '/h5/me',
|
path: '/h5/login',
|
||||||
query: path.startsWith('/h5')
|
query: path.startsWith('/h5')
|
||||||
? { redirect: path }
|
? { redirect: path }
|
||||||
: { redirect: resolveCustomerEntryPath(path) },
|
: { redirect: resolveCustomerEntryPath(path) },
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -16,6 +16,7 @@
|
|||||||
:stretch-columns="true"
|
:stretch-columns="true"
|
||||||
:height="handsontableHeight"
|
:height="handsontableHeight"
|
||||||
:enable-header-search="false"
|
:enable-header-search="false"
|
||||||
|
:auto-hybrid-header="false"
|
||||||
:enable-column-filter="false"
|
:enable-column-filter="false"
|
||||||
@action-click="handleActionClick"
|
@action-click="handleActionClick"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -551,6 +551,15 @@
|
|||||||
>
|
>
|
||||||
{{ t('h5.placeOrder.prev') }}
|
{{ t('h5.placeOrder.prev') }}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
v-if="step === 1"
|
||||||
|
type="button"
|
||||||
|
class="h5-po__btn is-ai"
|
||||||
|
:disabled="submitting"
|
||||||
|
@click="openReceiverAiParse"
|
||||||
|
>
|
||||||
|
{{ t('h5.placeOrder.aiParse') }}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
v-if="step < 2"
|
v-if="step < 2"
|
||||||
type="button"
|
type="button"
|
||||||
@ -573,6 +582,56 @@
|
|||||||
</div>
|
</div>
|
||||||
</a-drawer>
|
</a-drawer>
|
||||||
|
|
||||||
|
<a-modal
|
||||||
|
v-model:visible="receiverAiVisible"
|
||||||
|
:title="t('h5.placeOrder.aiParseTitle')"
|
||||||
|
:footer="false"
|
||||||
|
unmount-on-close
|
||||||
|
width="92%"
|
||||||
|
:mask-closable="!receiverParseLoading"
|
||||||
|
>
|
||||||
|
<div class="h5-po-ai">
|
||||||
|
<p class="h5-po-ai__hint">{{ t('h5.placeOrder.aiParseHint') }}</p>
|
||||||
|
<a-textarea
|
||||||
|
v-model="receiverPasteText"
|
||||||
|
class="h5-po-ai__textarea"
|
||||||
|
:auto-size="{ minRows: 5, maxRows: 10 }"
|
||||||
|
:placeholder="t('h5.placeOrder.aiParsePlaceholder')"
|
||||||
|
:disabled="receiverParseLoading"
|
||||||
|
/>
|
||||||
|
<p
|
||||||
|
v-if="receiverParseTip"
|
||||||
|
class="h5-po-ai__tip"
|
||||||
|
:class="`is-${receiverParseTip.tone}`"
|
||||||
|
>
|
||||||
|
{{ receiverParseTip.text }}
|
||||||
|
</p>
|
||||||
|
<div class="h5-po-ai__actions">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="h5-po__btn is-ghost"
|
||||||
|
:disabled="receiverParseLoading"
|
||||||
|
@click="receiverAiVisible = false"
|
||||||
|
>
|
||||||
|
{{ t('h5.placeOrder.aiParseCancel') }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="h5-po__btn is-primary"
|
||||||
|
:disabled="receiverParseLoading"
|
||||||
|
@click="handleReceiverAiParse"
|
||||||
|
>
|
||||||
|
<span v-if="receiverParseLoading" class="h5-po__loading" aria-hidden="true" />
|
||||||
|
{{
|
||||||
|
receiverParseLoading
|
||||||
|
? t('h5.placeOrder.aiParseLoading')
|
||||||
|
: t('h5.placeOrder.aiParseConfirm')
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a-modal>
|
||||||
|
|
||||||
<a-modal
|
<a-modal
|
||||||
v-model:visible="createFailVisible"
|
v-model:visible="createFailVisible"
|
||||||
:title="
|
:title="
|
||||||
@ -658,7 +717,7 @@ 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 } from '@arco-design/web-vue/es/icon';
|
import { IconClose } from '@arco-design/web-vue/es/icon';
|
||||||
import { postOrderDelete, postOrderParcelCreate } from '@/api/order';
|
import { postOrderDelete, postOrderParcelCreate, postOrderReceiverParseAddress } from '@/api/order';
|
||||||
import { createSenderAddress, getSenderAddressList } from '@/api/order/senderAddress';
|
import { createSenderAddress, getSenderAddressList } from '@/api/order/senderAddress';
|
||||||
import { getSalesProductList } from '@/api/channel/salesProduct';
|
import { getSalesProductList } from '@/api/channel/salesProduct';
|
||||||
import { getDictListAll } from '@/api/system/dict';
|
import { getDictListAll } from '@/api/system/dict';
|
||||||
@ -708,6 +767,10 @@ const senderLoading = ref(false);
|
|||||||
const showSenderForm = ref(false);
|
const showSenderForm = ref(false);
|
||||||
const savingSender = ref(false);
|
const savingSender = ref(false);
|
||||||
const showReceiverMore = ref(false);
|
const showReceiverMore = ref(false);
|
||||||
|
const receiverAiVisible = ref(false);
|
||||||
|
const receiverPasteText = ref('');
|
||||||
|
const receiverParseLoading = ref(false);
|
||||||
|
const receiverParseTip = ref<{ tone: 'ok' | 'warn' | 'danger'; text: string } | null>(null);
|
||||||
/** 申报品「更多」展开状态,按 item._key */
|
/** 申报品「更多」展开状态,按 item._key */
|
||||||
const openItemMoreKeys = ref<Set<string>>(new Set());
|
const openItemMoreKeys = ref<Set<string>>(new Set());
|
||||||
const senderForm = reactive({
|
const senderForm = reactive({
|
||||||
@ -1062,6 +1125,107 @@ function goNext() {
|
|||||||
step.value += 1;
|
step.value += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openReceiverAiParse() {
|
||||||
|
receiverParseTip.value = null;
|
||||||
|
receiverAiVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyParsedReceiver(parsed: Record<string, string | undefined>) {
|
||||||
|
const assignKeys = [
|
||||||
|
'recipientsName',
|
||||||
|
'companyName',
|
||||||
|
'phone',
|
||||||
|
'telePhone',
|
||||||
|
'email',
|
||||||
|
'countryCode',
|
||||||
|
'province',
|
||||||
|
'city',
|
||||||
|
'streetNumber',
|
||||||
|
'streetAddress1',
|
||||||
|
'streetAddress2',
|
||||||
|
'streetAddress3',
|
||||||
|
'zipCode',
|
||||||
|
] as const;
|
||||||
|
for (const key of assignKeys) {
|
||||||
|
const val = trim(parsed[key] || '');
|
||||||
|
if (val) {
|
||||||
|
(form.receiver as Record<string, string>)[key] = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const needMore = Boolean(
|
||||||
|
trim(parsed.companyName || '') ||
|
||||||
|
trim(parsed.telePhone || '') ||
|
||||||
|
trim(parsed.email || '') ||
|
||||||
|
trim(parsed.streetNumber || '') ||
|
||||||
|
trim(parsed.streetAddress2 || '') ||
|
||||||
|
trim(parsed.streetAddress3 || ''),
|
||||||
|
);
|
||||||
|
if (needMore) {
|
||||||
|
showReceiverMore.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildReceiverParseTip(result: {
|
||||||
|
isComplete: boolean;
|
||||||
|
missingRequired: string[];
|
||||||
|
missingRecommended: string[];
|
||||||
|
warnings: string[];
|
||||||
|
}) {
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (result.missingRequired.length) {
|
||||||
|
parts.push(`${t('h5.placeOrder.aiParseMissingRequired')}${result.missingRequired.join('、')}`);
|
||||||
|
}
|
||||||
|
if (result.missingRecommended.length) {
|
||||||
|
parts.push(`${t('h5.placeOrder.aiParseMissingRecommended')}${result.missingRecommended.join('、')}`);
|
||||||
|
}
|
||||||
|
if (result.warnings.length) {
|
||||||
|
parts.push(result.warnings.slice(0, 2).join(';'));
|
||||||
|
}
|
||||||
|
if (result.isComplete && !result.missingRecommended.length && !result.warnings.length) {
|
||||||
|
receiverParseTip.value = { tone: 'ok', text: t('h5.placeOrder.aiParseComplete') };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result.isComplete) {
|
||||||
|
receiverParseTip.value = {
|
||||||
|
tone: 'warn',
|
||||||
|
text: parts.length ? `${t('h5.placeOrder.aiParseRequiredOk')}${parts.join('。')}` : t('h5.placeOrder.aiParseRequiredOkShort'),
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
receiverParseTip.value = {
|
||||||
|
tone: 'danger',
|
||||||
|
text: parts.length ? parts.join('。') : t('h5.placeOrder.aiParseIncomplete'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleReceiverAiParse() {
|
||||||
|
const raw = receiverPasteText.value.trim();
|
||||||
|
if (!raw) {
|
||||||
|
Message.warning(t('h5.placeOrder.aiParseEmpty'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (receiverParseLoading.value) return;
|
||||||
|
receiverParseLoading.value = true;
|
||||||
|
receiverParseTip.value = null;
|
||||||
|
try {
|
||||||
|
const result = await postOrderReceiverParseAddress({ rawText: raw });
|
||||||
|
applyParsedReceiver(result.receiver);
|
||||||
|
buildReceiverParseTip(result);
|
||||||
|
Message.success(t('h5.placeOrder.aiParseSuccess'));
|
||||||
|
if (result.isComplete) {
|
||||||
|
receiverAiVisible.value = false;
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
receiverParseTip.value = {
|
||||||
|
tone: 'danger',
|
||||||
|
text: e?.message || t('h5.placeOrder.aiParseFailed'),
|
||||||
|
};
|
||||||
|
Message.error(e?.message || t('h5.placeOrder.aiParseFailed'));
|
||||||
|
} finally {
|
||||||
|
receiverParseLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function goToStep(idx: number) {
|
function goToStep(idx: number) {
|
||||||
if (idx === step.value || submitting.value) return;
|
if (idx === step.value || submitting.value) return;
|
||||||
if (idx < step.value) {
|
if (idx < step.value) {
|
||||||
@ -1558,6 +1722,10 @@ watch(
|
|||||||
submitting.value = false;
|
submitting.value = false;
|
||||||
showSenderForm.value = false;
|
showSenderForm.value = false;
|
||||||
resetSenderForm();
|
resetSenderForm();
|
||||||
|
receiverAiVisible.value = false;
|
||||||
|
receiverPasteText.value = '';
|
||||||
|
receiverParseTip.value = null;
|
||||||
|
receiverParseLoading.value = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
resetForm();
|
resetForm();
|
||||||
@ -2046,7 +2214,7 @@ watch(
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
|
||||||
&.is-ghost {
|
&.is-ghost {
|
||||||
flex: 0 0 96px;
|
flex: 0 0 88px;
|
||||||
background: #f2f3f5;
|
background: #f2f3f5;
|
||||||
color: #4e5969;
|
color: #4e5969;
|
||||||
|
|
||||||
@ -2055,6 +2223,20 @@ watch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.is-ai {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-width: 88px;
|
||||||
|
padding: 0 12px;
|
||||||
|
background: color-mix(in srgb, var(--app-primary-color) 12%, #fff);
|
||||||
|
color: var(--app-primary-color);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--app-primary-color) 35%, #e5e6eb);
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
background: color-mix(in srgb, var(--app-primary-color) 18%, #fff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.is-primary {
|
&.is-primary {
|
||||||
background: var(--app-primary-color);
|
background: var(--app-primary-color);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
@ -2075,6 +2257,58 @@ watch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.h5-po-ai {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-po-ai__hint {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.45;
|
||||||
|
color: #86909c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-po-ai__textarea {
|
||||||
|
:deep(textarea) {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-po-ai__tip {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.45;
|
||||||
|
|
||||||
|
&.is-ok {
|
||||||
|
color: #00a870;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-warn {
|
||||||
|
color: #d48806;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-danger {
|
||||||
|
color: #f53f3f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.h5-po-ai__actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 4px;
|
||||||
|
|
||||||
|
.h5-po__btn {
|
||||||
|
height: 44px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-ghost {
|
||||||
|
flex: 0 0 88px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.h5-po__loading {
|
.h5-po__loading {
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
import { Message } from '@arco-design/web-vue';
|
||||||
import { IconWechat } from '@arco-design/web-vue/es/icon';
|
import { IconWechat } from '@arco-design/web-vue/es/icon';
|
||||||
import { isWeChatBrowser } from '@/utils/device';
|
import { isWeChatBrowser } from '@/utils/device';
|
||||||
import { redirectToWeChatOAuth } from '@/utils/wechatOAuth';
|
import { redirectToWeChatOAuth } from '@/utils/wechatOAuth';
|
||||||
@ -12,11 +13,13 @@ const props = withDefaults(
|
|||||||
afterLoginPath?: string;
|
afterLoginPath?: string;
|
||||||
long?: boolean;
|
long?: boolean;
|
||||||
size?: 'mini' | 'small' | 'medium' | 'large';
|
size?: 'mini' | 'small' | 'medium' | 'large';
|
||||||
|
disabled?: boolean;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
afterLoginPath: '/h5/orders',
|
afterLoginPath: '/h5/orders',
|
||||||
long: true,
|
long: false,
|
||||||
size: 'large',
|
size: 'large',
|
||||||
|
disabled: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -25,8 +28,11 @@ const jumping = ref(false);
|
|||||||
const inWeChat = computed(() => isWeChatBrowser());
|
const inWeChat = computed(() => isWeChatBrowser());
|
||||||
|
|
||||||
function handleClick() {
|
function handleClick() {
|
||||||
if (jumping.value) return;
|
if (jumping.value || props.disabled) return;
|
||||||
if (!inWeChat.value) return;
|
if (!inWeChat.value) {
|
||||||
|
Message.warning(t('h5.login.openInWechat'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
jumping.value = true;
|
jumping.value = true;
|
||||||
redirectToWeChatOAuth('login', null, { afterLoginPath: props.afterLoginPath });
|
redirectToWeChatOAuth('login', null, { afterLoginPath: props.afterLoginPath });
|
||||||
}
|
}
|
||||||
@ -35,37 +41,35 @@ function handleClick() {
|
|||||||
<template>
|
<template>
|
||||||
<div class="wx-oauth-login">
|
<div class="wx-oauth-login">
|
||||||
<a-button
|
<a-button
|
||||||
v-if="inWeChat"
|
|
||||||
type="primary"
|
|
||||||
class="wx-oauth-login__btn"
|
class="wx-oauth-login__btn"
|
||||||
:long="long"
|
:long="long"
|
||||||
:size="size"
|
:size="size"
|
||||||
:loading="jumping"
|
:loading="jumping"
|
||||||
|
:disabled="disabled || (!inWeChat && !jumping)"
|
||||||
@click="handleClick"
|
@click="handleClick"
|
||||||
>
|
>
|
||||||
<template #icon><icon-wechat /></template>
|
<template #icon><icon-wechat /></template>
|
||||||
{{ jumping ? t('h5.login.oauthRedirecting') : t('h5.login.oauthLogin') }}
|
{{ jumping ? t('h5.login.oauthRedirecting') : t('h5.login.oauthLogin') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
<template v-else>
|
<p v-if="!inWeChat" class="wx-oauth-login__tip">{{ t('h5.login.openInWechat') }}</p>
|
||||||
<p class="wx-oauth-login__tip">{{ t('h5.login.openInWechat') }}</p>
|
|
||||||
<a-button type="primary" class="wx-oauth-login__btn" :long="long" :size="size" disabled>
|
|
||||||
<template #icon><icon-wechat /></template>
|
|
||||||
{{ t('h5.login.oauthLogin') }}
|
|
||||||
</a-button>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.wx-oauth-login {
|
.wx-oauth-login {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 8px 0 4px;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wx-oauth-login__btn {
|
.wx-oauth-login__btn {
|
||||||
height: 44px;
|
width: 50%;
|
||||||
border-radius: 10px;
|
min-width: 148px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 999px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
color: #fff !important;
|
||||||
background: #07c160 !important;
|
background: #07c160 !important;
|
||||||
border-color: #07c160 !important;
|
border-color: #07c160 !important;
|
||||||
|
|
||||||
@ -74,12 +78,18 @@ function handleClick() {
|
|||||||
background: #06ad56 !important;
|
background: #06ad56 !important;
|
||||||
border-color: #06ad56 !important;
|
border-color: #06ad56 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
color: #fff !important;
|
||||||
|
background: #94d3ad !important;
|
||||||
|
border-color: #94d3ad !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.wx-oauth-login__tip {
|
.wx-oauth-login__tip {
|
||||||
margin: 0 0 12px;
|
margin: 10px 0 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
color: #86909c;
|
color: #86909c;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -163,16 +163,18 @@ export default {
|
|||||||
'h5.wxQr.success': 'Signed in',
|
'h5.wxQr.success': 'Signed in',
|
||||||
|
|
||||||
'h5.login.title': 'Sign in',
|
'h5.login.title': 'Sign in',
|
||||||
'h5.login.brand': 'Customer Center',
|
'h5.login.brand': 'IDE Customer Center',
|
||||||
|
'h5.login.heroSub': 'Sign in with account password, or authorize in WeChat',
|
||||||
'h5.login.tab.wechat': 'WeChat',
|
'h5.login.tab.wechat': 'WeChat',
|
||||||
'h5.login.tab.password': 'Account',
|
'h5.login.tab.password': 'Account',
|
||||||
'h5.login.switchPassword': 'Sign in with password',
|
'h5.login.switchPassword': 'Sign in with password',
|
||||||
'h5.login.switchWechat': 'WeChat authorize sign-in',
|
'h5.login.switchWechat': 'WeChat authorize sign-in',
|
||||||
|
'h5.login.or': 'or',
|
||||||
'h5.login.oauthLogin': 'Sign in with WeChat',
|
'h5.login.oauthLogin': 'Sign in with WeChat',
|
||||||
'h5.login.oauthRedirecting': 'Opening WeChat authorization…',
|
'h5.login.oauthRedirecting': 'Opening WeChat authorization…',
|
||||||
'h5.login.oauthLoggingIn': 'Authorized. Signing you in…',
|
'h5.login.oauthLoggingIn': 'Authorized. Signing you in…',
|
||||||
'h5.login.oauthFailed': 'WeChat sign-in failed. Please retry.',
|
'h5.login.oauthFailed': 'WeChat sign-in failed. Please retry.',
|
||||||
'h5.login.openInWechat': 'Open this page in WeChat, then tap the button below to authorize.',
|
'h5.login.openInWechat': 'Open this page in WeChat to use WeChat authorize sign-in',
|
||||||
'h5.login.accountPlaceholder': 'Phone / email / customer code',
|
'h5.login.accountPlaceholder': 'Phone / email / customer code',
|
||||||
'h5.login.passwordPlaceholder': 'Enter password',
|
'h5.login.passwordPlaceholder': 'Enter password',
|
||||||
'h5.login.accountRequired': 'Please enter account',
|
'h5.login.accountRequired': 'Please enter account',
|
||||||
@ -307,6 +309,24 @@ export default {
|
|||||||
'h5.placeOrder.err.itemLine': 'Item {n}: {msg}',
|
'h5.placeOrder.err.itemLine': 'Item {n}: {msg}',
|
||||||
'h5.placeOrder.prev': 'Back',
|
'h5.placeOrder.prev': 'Back',
|
||||||
'h5.placeOrder.next': 'Next',
|
'h5.placeOrder.next': 'Next',
|
||||||
|
'h5.placeOrder.aiParse': 'AI parse',
|
||||||
|
'h5.placeOrder.aiParseTitle': 'Paste address · AI parse',
|
||||||
|
'h5.placeOrder.aiParseHint':
|
||||||
|
'Paste name, phone, address and ZIP together — we will fill the recipient fields',
|
||||||
|
'h5.placeOrder.aiParsePlaceholder':
|
||||||
|
'e.g. John Smith, +1 310-555-0199, 1001 Sunset Blvd, Los Angeles, CA 90001, USA',
|
||||||
|
'h5.placeOrder.aiParseCancel': 'Close',
|
||||||
|
'h5.placeOrder.aiParseConfirm': 'AI parse',
|
||||||
|
'h5.placeOrder.aiParseLoading': 'Parsing…',
|
||||||
|
'h5.placeOrder.aiParseEmpty': 'Paste an address first',
|
||||||
|
'h5.placeOrder.aiParseSuccess': 'Recipient fields updated',
|
||||||
|
'h5.placeOrder.aiParseFailed': 'Failed to parse address',
|
||||||
|
'h5.placeOrder.aiParseComplete': 'Address looks complete and was filled in',
|
||||||
|
'h5.placeOrder.aiParseRequiredOk': 'Required fields OK. ',
|
||||||
|
'h5.placeOrder.aiParseRequiredOkShort': 'Required fields OK — please double-check details',
|
||||||
|
'h5.placeOrder.aiParseIncomplete': 'Still incomplete — please fill remaining fields',
|
||||||
|
'h5.placeOrder.aiParseMissingRequired': 'Missing required: ',
|
||||||
|
'h5.placeOrder.aiParseMissingRecommended': 'Suggested: ',
|
||||||
'h5.placeOrder.submit': 'Submit order',
|
'h5.placeOrder.submit': 'Submit order',
|
||||||
'h5.placeOrder.submitting': 'Submitting…',
|
'h5.placeOrder.submitting': 'Submitting…',
|
||||||
'h5.placeOrder.successTitle': 'Order created',
|
'h5.placeOrder.successTitle': 'Order created',
|
||||||
|
|||||||
@ -162,16 +162,18 @@ export default {
|
|||||||
'h5.wxQr.success': '登录成功',
|
'h5.wxQr.success': '登录成功',
|
||||||
|
|
||||||
'h5.login.title': '登录',
|
'h5.login.title': '登录',
|
||||||
'h5.login.brand': '客户中心',
|
'h5.login.brand': 'IDE客户中心',
|
||||||
|
'h5.login.heroSub': '使用账号密码登录,或在微信内一键授权',
|
||||||
'h5.login.tab.wechat': '微信登录',
|
'h5.login.tab.wechat': '微信登录',
|
||||||
'h5.login.tab.password': '账号登录',
|
'h5.login.tab.password': '账号登录',
|
||||||
'h5.login.switchPassword': '账号密码登录',
|
'h5.login.switchPassword': '账号密码登录',
|
||||||
'h5.login.switchWechat': '微信授权登录',
|
'h5.login.switchWechat': '微信授权登录',
|
||||||
|
'h5.login.or': '或',
|
||||||
'h5.login.oauthLogin': '微信授权登录',
|
'h5.login.oauthLogin': '微信授权登录',
|
||||||
'h5.login.oauthRedirecting': '正在打开微信授权…',
|
'h5.login.oauthRedirecting': '正在打开微信授权…',
|
||||||
'h5.login.oauthLoggingIn': '授权成功,正在登录…',
|
'h5.login.oauthLoggingIn': '授权成功,正在登录…',
|
||||||
'h5.login.oauthFailed': '微信授权登录失败,请重试',
|
'h5.login.oauthFailed': '微信授权登录失败,请重试',
|
||||||
'h5.login.openInWechat': '请在微信内打开本页,点击下方按钮一键授权登录',
|
'h5.login.openInWechat': '请在微信内打开本页,即可使用微信授权登录',
|
||||||
'h5.login.accountPlaceholder': '手机号 / 邮箱 / 客户编号',
|
'h5.login.accountPlaceholder': '手机号 / 邮箱 / 客户编号',
|
||||||
'h5.login.passwordPlaceholder': '请输入密码',
|
'h5.login.passwordPlaceholder': '请输入密码',
|
||||||
'h5.login.accountRequired': '请输入账号',
|
'h5.login.accountRequired': '请输入账号',
|
||||||
@ -306,6 +308,23 @@ export default {
|
|||||||
'h5.placeOrder.err.itemLine': '第 {n} 条:{msg}',
|
'h5.placeOrder.err.itemLine': '第 {n} 条:{msg}',
|
||||||
'h5.placeOrder.prev': '上一步',
|
'h5.placeOrder.prev': '上一步',
|
||||||
'h5.placeOrder.next': '下一步',
|
'h5.placeOrder.next': '下一步',
|
||||||
|
'h5.placeOrder.aiParse': 'AI解析',
|
||||||
|
'h5.placeOrder.aiParseTitle': '粘贴地址 · AI 解析',
|
||||||
|
'h5.placeOrder.aiParseHint': '把姓名、电话、地址、邮编整段粘贴进来,解析后自动填入收件信息',
|
||||||
|
'h5.placeOrder.aiParsePlaceholder':
|
||||||
|
'例:John Smith, +1 310-555-0199, 1001 Sunset Blvd, Los Angeles, CA 90001, USA\n或:张三 13800138000 广东省深圳市南山区科技园南路 XX 号 518000',
|
||||||
|
'h5.placeOrder.aiParseCancel': '关闭',
|
||||||
|
'h5.placeOrder.aiParseConfirm': 'AI解析',
|
||||||
|
'h5.placeOrder.aiParseLoading': '解析中…',
|
||||||
|
'h5.placeOrder.aiParseEmpty': '请先粘贴收件人地址文本',
|
||||||
|
'h5.placeOrder.aiParseSuccess': '已解析并填入收件人信息',
|
||||||
|
'h5.placeOrder.aiParseFailed': '地址解析失败,请稍后重试',
|
||||||
|
'h5.placeOrder.aiParseComplete': '地址信息完整,已填入表单',
|
||||||
|
'h5.placeOrder.aiParseRequiredOk': '必填已齐。',
|
||||||
|
'h5.placeOrder.aiParseRequiredOkShort': '必填已齐,请确认地址细节',
|
||||||
|
'h5.placeOrder.aiParseIncomplete': '解析后仍不完整,请手动补全',
|
||||||
|
'h5.placeOrder.aiParseMissingRequired': '仍缺必填:',
|
||||||
|
'h5.placeOrder.aiParseMissingRecommended': '建议补全:',
|
||||||
'h5.placeOrder.submit': '提交下单',
|
'h5.placeOrder.submit': '提交下单',
|
||||||
'h5.placeOrder.submitting': '提交中…',
|
'h5.placeOrder.submitting': '提交中…',
|
||||||
'h5.placeOrder.successTitle': '下单成功',
|
'h5.placeOrder.successTitle': '下单成功',
|
||||||
|
|||||||
@ -15,46 +15,12 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="h5-login-card">
|
<section class="h5-login-card">
|
||||||
<div class="h5-login-tabs" role="tablist">
|
<div v-if="oauthLoading" class="h5-login-oauth-banner">
|
||||||
<button
|
<a-spin :size="18" />
|
||||||
type="button"
|
<span>{{ t('h5.login.oauthLoggingIn') }}</span>
|
||||||
role="tab"
|
|
||||||
class="h5-login-tab"
|
|
||||||
:class="{ 'is-active': activeTab === 'wechat' }"
|
|
||||||
:aria-selected="activeTab === 'wechat'"
|
|
||||||
@click="switchToWechat"
|
|
||||||
>
|
|
||||||
{{ t('h5.login.tab.wechat') }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
role="tab"
|
|
||||||
class="h5-login-tab"
|
|
||||||
:class="{ 'is-active': activeTab === 'password' }"
|
|
||||||
:aria-selected="activeTab === 'password'"
|
|
||||||
@click="switchToPassword"
|
|
||||||
>
|
|
||||||
{{ t('h5.login.tab.password') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="h5-login-stage" :data-mode="activeTab">
|
<form class="h5-login-form" @submit.prevent="handleSubmit">
|
||||||
<div
|
|
||||||
class="h5-login-pane"
|
|
||||||
:class="{ 'is-active': activeTab === 'wechat' }"
|
|
||||||
:aria-hidden="activeTab !== 'wechat'"
|
|
||||||
>
|
|
||||||
<WechatOAuthLoginButton :after-login-path="resolveRedirectPath()" />
|
|
||||||
<p v-if="oauthLoading" class="h5-login-oauth-status">{{ t('h5.login.oauthLoggingIn') }}</p>
|
|
||||||
<p v-else-if="errorBox && activeTab === 'wechat'" class="h5-login-error">{{ errorBox }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form
|
|
||||||
class="h5-login-pane h5-login-form"
|
|
||||||
:class="{ 'is-active': activeTab === 'password' }"
|
|
||||||
:aria-hidden="activeTab !== 'password'"
|
|
||||||
@submit.prevent="handleSubmit"
|
|
||||||
>
|
|
||||||
<a-input
|
<a-input
|
||||||
v-model="form.account"
|
v-model="form.account"
|
||||||
size="large"
|
size="large"
|
||||||
@ -94,11 +60,20 @@
|
|||||||
size="large"
|
size="large"
|
||||||
class="h5-login-submit"
|
class="h5-login-submit"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
|
:disabled="oauthLoading"
|
||||||
>
|
>
|
||||||
{{ loading ? t('h5.login.submitting') : t('h5.login.submit') }}
|
{{ loading ? t('h5.login.submitting') : t('h5.login.submit') }}
|
||||||
</a-button>
|
</a-button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div class="h5-login-divider">
|
||||||
|
<span>{{ t('h5.login.or') }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<WechatOAuthLoginButton
|
||||||
|
:after-login-path="resolveRedirectPath()"
|
||||||
|
:disabled="loading || oauthLoading"
|
||||||
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="h5-login-extra">
|
<div class="h5-login-extra">
|
||||||
@ -130,7 +105,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { nextTick, onMounted, reactive, ref, watch } from 'vue';
|
import { nextTick, onMounted, reactive, ref } 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';
|
||||||
@ -157,7 +132,6 @@ const route = useRoute();
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const rbacUserStore = useRbacUserStore();
|
const rbacUserStore = useRbacUserStore();
|
||||||
|
|
||||||
const activeTab = ref<'wechat' | 'password'>('wechat');
|
|
||||||
const form = reactive({ account: '', password: '' });
|
const form = reactive({ account: '', password: '' });
|
||||||
const agreementChecked = ref(false);
|
const agreementChecked = ref(false);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
@ -177,26 +151,12 @@ const termsText = `一、使用须知
|
|||||||
二、服务说明
|
二、服务说明
|
||||||
系统展示的运费、时效为估算或报价结果,最终以订单确认与仓库计费为准。异常订单请及时联系客服或使用 AI 顾问。`;
|
系统展示的运费、时效为估算或报价结果,最终以订单确认与仓库计费为准。异常订单请及时联系客服或使用 AI 顾问。`;
|
||||||
|
|
||||||
function switchToWechat() {
|
|
||||||
errorBox.value = '';
|
|
||||||
activeTab.value = 'wechat';
|
|
||||||
if (route.query.tab === 'password') {
|
|
||||||
const { tab: _tab, ...rest } = route.query;
|
|
||||||
router.replace({ path: route.path, query: rest });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function switchToPassword() {
|
|
||||||
errorBox.value = '';
|
|
||||||
activeTab.value = 'password';
|
|
||||||
}
|
|
||||||
|
|
||||||
function goBack() {
|
function goBack() {
|
||||||
if (window.history.length > 1) {
|
if (window.history.length > 1) {
|
||||||
router.back();
|
router.back();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
router.replace('/h5/me');
|
router.replace('/h5/orders');
|
||||||
}
|
}
|
||||||
|
|
||||||
function goRegister() {
|
function goRegister() {
|
||||||
@ -261,7 +221,7 @@ async function finishLogin() {
|
|||||||
async function consumeOAuthCode(code: string) {
|
async function consumeOAuthCode(code: string) {
|
||||||
if (oauthLoading.value) return;
|
if (oauthLoading.value) return;
|
||||||
oauthLoading.value = true;
|
oauthLoading.value = true;
|
||||||
activeTab.value = 'wechat';
|
errorBox.value = '';
|
||||||
try {
|
try {
|
||||||
const result = await wxOAuthLogin(code);
|
const result = await wxOAuthLogin(code);
|
||||||
const ok = await rbacUserStore.loginByWxResult(result);
|
const ok = await rbacUserStore.loginByWxResult(result);
|
||||||
@ -283,7 +243,7 @@ async function consumeOAuthCode(code: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
if (loading.value) return;
|
if (loading.value || oauthLoading.value) return;
|
||||||
errorBox.value = '';
|
errorBox.value = '';
|
||||||
|
|
||||||
if (!agreementChecked.value) {
|
if (!agreementChecked.value) {
|
||||||
@ -322,20 +282,6 @@ async function handleSubmit() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
|
||||||
() => route.query.tab,
|
|
||||||
(tab) => {
|
|
||||||
if (tab === 'password') {
|
|
||||||
activeTab.value = 'password';
|
|
||||||
errorBox.value = '';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
activeTab.value = 'wechat';
|
|
||||||
errorBox.value = '';
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
);
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
if (isLogin()) {
|
if (isLogin()) {
|
||||||
router.replace(resolveRedirectPath());
|
router.replace(resolveRedirectPath());
|
||||||
@ -348,7 +294,6 @@ onMounted(async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 微信内且未带 code:可一键拉起授权(用户点按钮亦可)
|
|
||||||
if (isWeChatBrowser() && route.query.auto === '1') {
|
if (isWeChatBrowser() && route.query.auto === '1') {
|
||||||
redirectToWeChatOAuth('login', null, { afterLoginPath: resolveRedirectPath() });
|
redirectToWeChatOAuth('login', null, { afterLoginPath: resolveRedirectPath() });
|
||||||
}
|
}
|
||||||
@ -417,17 +362,17 @@ onMounted(async () => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 420px;
|
max-width: 420px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 20px 16px calc(28px + env(safe-area-inset-bottom, 0px));
|
padding: 12px 16px calc(28px + env(safe-area-inset-bottom, 0px));
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: flex-start;
|
||||||
animation: h5-login-rise 0.48s var(--login-ease) both;
|
animation: h5-login-rise 0.48s var(--login-ease) both;
|
||||||
}
|
}
|
||||||
|
|
||||||
.h5-login-hero {
|
.h5-login-hero {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 0 8px 14px;
|
padding: 8px 8px 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.h5-login-hero__logo {
|
.h5-login-hero__logo {
|
||||||
@ -457,87 +402,28 @@ onMounted(async () => {
|
|||||||
.h5-login-card {
|
.h5-login-card {
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 22px;
|
border-radius: 22px;
|
||||||
padding: 14px 18px 20px;
|
padding: 22px 18px 20px;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 1px 0 rgba(255, 255, 255, 0.8) inset,
|
0 1px 0 rgba(255, 255, 255, 0.8) inset,
|
||||||
0 12px 32px rgba(15, 23, 42, 0.06);
|
0 12px 32px rgba(15, 23, 42, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
.h5-login-tabs {
|
.h5-login-oauth-banner {
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: 1fr 1fr;
|
align-items: center;
|
||||||
gap: 4px;
|
justify-content: center;
|
||||||
margin-bottom: 16px;
|
gap: 8px;
|
||||||
padding: 4px;
|
margin: -6px 0 14px;
|
||||||
border-radius: 12px;
|
padding: 10px 12px;
|
||||||
background: #f2f3f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h5-login-tab {
|
|
||||||
height: 36px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background: transparent;
|
background: #f2f3f5;
|
||||||
color: #86909c;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
transition:
|
|
||||||
background 0.2s var(--login-ease),
|
|
||||||
color 0.2s var(--login-ease),
|
|
||||||
box-shadow 0.2s var(--login-ease);
|
|
||||||
|
|
||||||
&.is-active {
|
|
||||||
background: #fff;
|
|
||||||
color: var(--app-primary-color);
|
|
||||||
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active:not(.is-active) {
|
|
||||||
color: #4e5969;
|
color: #4e5969;
|
||||||
}
|
font-size: 13px;
|
||||||
}
|
|
||||||
|
|
||||||
.h5-login-stage {
|
|
||||||
position: relative;
|
|
||||||
display: grid;
|
|
||||||
min-height: 360px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h5-login-pane {
|
|
||||||
grid-area: 1 / 1;
|
|
||||||
width: 100%;
|
|
||||||
opacity: 0;
|
|
||||||
visibility: hidden;
|
|
||||||
pointer-events: none;
|
|
||||||
transform: translate3d(16px, 0, 0) scale(0.985);
|
|
||||||
transition:
|
|
||||||
opacity 0.34s var(--login-ease),
|
|
||||||
transform 0.34s var(--login-ease),
|
|
||||||
visibility 0.34s;
|
|
||||||
|
|
||||||
&.is-active {
|
|
||||||
opacity: 1;
|
|
||||||
visibility: visible;
|
|
||||||
pointer-events: auto;
|
|
||||||
transform: translate3d(0, 0, 0) scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.h5-login-stage[data-mode='password'] .h5-login-pane:not(.is-active) {
|
|
||||||
transform: translate3d(-16px, 0, 0) scale(0.985);
|
|
||||||
}
|
|
||||||
|
|
||||||
.h5-login-stage[data-mode='wechat'] .h5-login-pane:not(.is-active) {
|
|
||||||
transform: translate3d(16px, 0, 0) scale(0.985);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.h5-login-form {
|
.h5-login-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
|
||||||
min-height: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.h5-login-input {
|
.h5-login-input {
|
||||||
@ -575,13 +461,6 @@ onMounted(async () => {
|
|||||||
animation: h5-login-rise 0.28s var(--login-ease) both;
|
animation: h5-login-rise 0.28s var(--login-ease) both;
|
||||||
}
|
}
|
||||||
|
|
||||||
.h5-login-oauth-status {
|
|
||||||
margin: 12px 0 0;
|
|
||||||
text-align: center;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #86909c;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h5-login-agree {
|
.h5-login-agree {
|
||||||
margin: 4px 0 0;
|
margin: 4px 0 0;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
@ -597,9 +476,9 @@ onMounted(async () => {
|
|||||||
|
|
||||||
.h5-login-submit {
|
.h5-login-submit {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
height: 50px !important;
|
height: 48px !important;
|
||||||
border-radius: 14px !important;
|
border-radius: 12px !important;
|
||||||
font-size: 17px;
|
font-size: 16px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
box-shadow: 0 8px 18px color-mix(in srgb, var(--app-primary-color) 26%, transparent);
|
box-shadow: 0 8px 18px color-mix(in srgb, var(--app-primary-color) 26%, transparent);
|
||||||
transition: transform 0.2s var(--login-ease), box-shadow 0.2s var(--login-ease) !important;
|
transition: transform 0.2s var(--login-ease), box-shadow 0.2s var(--login-ease) !important;
|
||||||
@ -609,6 +488,27 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.h5-login-divider {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin: 18px 0 10px;
|
||||||
|
color: #c9cdd4;
|
||||||
|
font-size: 12px;
|
||||||
|
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background: #e5e6eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.h5-login-extra {
|
.h5-login-extra {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@ -683,17 +583,11 @@ onMounted(async () => {
|
|||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.h5-login-main,
|
.h5-login-main,
|
||||||
.h5-login-pane,
|
|
||||||
.h5-login-tab,
|
|
||||||
.h5-login-text-btn,
|
.h5-login-text-btn,
|
||||||
.h5-login-error,
|
.h5-login-error,
|
||||||
.h5-login-submit {
|
.h5-login-submit {
|
||||||
animation: none !important;
|
animation: none !important;
|
||||||
transition: none !important;
|
transition: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.h5-login-pane {
|
|
||||||
transform: none;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,55 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="h5-me-page" :class="{ 'h5-me-page--guest': !loggedIn }">
|
<div class="h5-me-page">
|
||||||
<!-- 访客:微信授权登录(无二维码);新用户走注册/绑定 -->
|
|
||||||
<template v-if="!loggedIn">
|
|
||||||
<section class="h5-me-guest h5-enter">
|
|
||||||
<div class="h5-me-guest__brand">
|
|
||||||
<img :src="logoImg" alt="" class="h5-me-guest__logo" />
|
|
||||||
<h2 class="h5-me-guest__title">{{ t('h5.login.brand') }}</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="h5-me-guest__card">
|
|
||||||
<div class="h5-me-guest__tabs" role="tablist">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
role="tab"
|
|
||||||
class="h5-me-guest__tab is-active"
|
|
||||||
aria-selected="true"
|
|
||||||
>
|
|
||||||
{{ t('h5.login.tab.wechat') }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
role="tab"
|
|
||||||
class="h5-me-guest__tab"
|
|
||||||
aria-selected="false"
|
|
||||||
@click="goPasswordLogin"
|
|
||||||
>
|
|
||||||
{{ t('h5.login.tab.password') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<WechatOAuthLoginButton :after-login-path="resolvePostLoginPath()" />
|
|
||||||
<p v-if="route.query.redirect" class="h5-me-tip">{{ t('h5.me.needLogin') }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="h5-me-guest__extra">
|
|
||||||
<p class="h5-me-guest__hint">{{ t('h5.login.newUserHint') }}</p>
|
|
||||||
<div class="h5-me-guest__actions">
|
|
||||||
<button type="button" class="h5-me-text-btn is-primary" @click="goWechatRegister">
|
|
||||||
<icon-user-add :size="18" />
|
|
||||||
<span>{{ t('h5.login.goRegister') }}</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="h5-me-text-btn" @click="goWechatBind">
|
|
||||||
<icon-link :size="18" />
|
|
||||||
<span>{{ t('h5.login.goBind') }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- 已登录 -->
|
|
||||||
<template v-else>
|
|
||||||
<a-spin :loading="loading" class="h5-me-spin">
|
<a-spin :loading="loading" class="h5-me-spin">
|
||||||
<div v-if="loading && !profile.id" class="h5-me-skel">
|
<div v-if="loading && !profile.id" class="h5-me-skel">
|
||||||
<div class="h5-me-skel__hero" />
|
<div class="h5-me-skel__hero" />
|
||||||
@ -221,14 +171,13 @@
|
|||||||
<H5PasswordSheet v-model:visible="pwdVisible" @success="onPasswordChanged" />
|
<H5PasswordSheet v-model:visible="pwdVisible" @success="onPasswordChanged" />
|
||||||
<H5AddressSheet v-model:visible="addressVisible" />
|
<H5AddressSheet v-model:visible="addressVisible" />
|
||||||
<H5PickupOrdersSheet v-model:visible="pickupVisible" @loaded="onPickupLoaded" />
|
<H5PickupOrdersSheet v-model:visible="pickupVisible" @loaded="onPickupLoaded" />
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
import { preloadInvitePosterBg } from '@/utils/invitePoster';
|
import { preloadInvitePosterBg } from '@/utils/invitePoster';
|
||||||
import { useRoute, 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 {
|
import {
|
||||||
@ -237,8 +186,6 @@ import {
|
|||||||
IconEdit,
|
IconEdit,
|
||||||
IconLock,
|
IconLock,
|
||||||
IconLocation,
|
IconLocation,
|
||||||
IconUserAdd,
|
|
||||||
IconLink,
|
|
||||||
IconLanguage,
|
IconLanguage,
|
||||||
IconWechat,
|
IconWechat,
|
||||||
} from '@arco-design/web-vue/es/icon';
|
} from '@arco-design/web-vue/es/icon';
|
||||||
@ -249,18 +196,15 @@ import { getMyCoupons, getMemberOverview } from '@/api/marketing/portal';
|
|||||||
import { isLogin } from '@/utils/auth';
|
import { isLogin } from '@/utils/auth';
|
||||||
import { resolveAvatarUrl } from '@/utils/avatar';
|
import { resolveAvatarUrl } from '@/utils/avatar';
|
||||||
import { getMapIdFromAccessToken } from '@/utils/jwt';
|
import { getMapIdFromAccessToken } from '@/utils/jwt';
|
||||||
import { h5Navigate, isWeChatBrowser } from '@/utils/device';
|
import { h5Navigate } from '@/utils/device';
|
||||||
import { redirectToWeChatOAuth } from '@/utils/wechatOAuth';
|
|
||||||
import { jumpToWechatOfficialAccount } from '@/utils/wechatFollowJump';
|
import { jumpToWechatOfficialAccount } from '@/utils/wechatFollowJump';
|
||||||
import { useUserStore } from '@/store';
|
import { useUserStore } from '@/store';
|
||||||
import { useRbacUserStore } from '@/store/modules/rbac-user';
|
import { useRbacUserStore } from '@/store/modules/rbac-user';
|
||||||
import useLocale from '@/hooks/locale';
|
import useLocale from '@/hooks/locale';
|
||||||
import logoImg from '@/assets/logo2.png';
|
|
||||||
import iconServiceCoupon from '@/assets/icons/h5/service-coupon.svg?url';
|
import iconServiceCoupon from '@/assets/icons/h5/service-coupon.svg?url';
|
||||||
import iconServiceInvite from '@/assets/icons/h5/service-invite.svg?url';
|
import iconServiceInvite from '@/assets/icons/h5/service-invite.svg?url';
|
||||||
import iconServiceCommission from '@/assets/icons/h5/service-commission.svg?url';
|
import iconServiceCommission from '@/assets/icons/h5/service-commission.svg?url';
|
||||||
import iconOrderPickup from '@/assets/icons/h5/order-pickup.svg?url';
|
import iconOrderPickup from '@/assets/icons/h5/order-pickup.svg?url';
|
||||||
import WechatOAuthLoginButton from '../components/WechatOAuthLoginButton.vue';
|
|
||||||
import H5JourneyGuide from '../components/H5JourneyGuide.vue';
|
import H5JourneyGuide from '../components/H5JourneyGuide.vue';
|
||||||
import H5CreateOrderFlow from '../components/H5CreateOrderFlow.vue';
|
import H5CreateOrderFlow from '../components/H5CreateOrderFlow.vue';
|
||||||
import H5RechargeSheet from '../components/H5RechargeSheet.vue';
|
import H5RechargeSheet from '../components/H5RechargeSheet.vue';
|
||||||
@ -278,12 +222,10 @@ const LOW_BALANCE_THRESHOLD = 100;
|
|||||||
|
|
||||||
const { t, locale } = useI18n();
|
const { t, locale } = useI18n();
|
||||||
const { changeLocale } = useLocale();
|
const { changeLocale } = useLocale();
|
||||||
const route = useRoute();
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const rbacUserStore = useRbacUserStore();
|
const rbacUserStore = useRbacUserStore();
|
||||||
|
|
||||||
const loggedIn = ref(isLogin());
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const profile = ref<CustomerDto>({});
|
const profile = ref<CustomerDto>({});
|
||||||
const couponCount = ref(0);
|
const couponCount = ref(0);
|
||||||
@ -439,35 +381,6 @@ async function copyText(text?: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolvePostLoginPath() {
|
|
||||||
const redirect = route.query.redirect;
|
|
||||||
if (typeof redirect === 'string' && redirect.startsWith('/h5')) {
|
|
||||||
return redirect;
|
|
||||||
}
|
|
||||||
return '/h5/orders';
|
|
||||||
}
|
|
||||||
|
|
||||||
function goWechatRegister() {
|
|
||||||
if (isWeChatBrowser()) {
|
|
||||||
redirectToWeChatOAuth('register');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
h5Navigate(router, '/wechat/register');
|
|
||||||
}
|
|
||||||
|
|
||||||
function goWechatBind() {
|
|
||||||
if (isWeChatBrowser()) {
|
|
||||||
redirectToWeChatOAuth('bind');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
h5Navigate(router, '/wechat/bind');
|
|
||||||
}
|
|
||||||
|
|
||||||
function goPasswordLogin() {
|
|
||||||
const redirect = typeof route.query.redirect === 'string' ? route.query.redirect : '/h5/orders';
|
|
||||||
h5Navigate(router, { path: '/h5/login', query: { redirect, tab: 'password' } });
|
|
||||||
}
|
|
||||||
|
|
||||||
function goRecharge() {
|
function goRecharge() {
|
||||||
rechargeVisible.value = true;
|
rechargeVisible.value = true;
|
||||||
}
|
}
|
||||||
@ -479,8 +392,7 @@ function onProfileSaved(payload: { customerName: string; phone: string; email: s
|
|||||||
async function onPasswordChanged() {
|
async function onPasswordChanged() {
|
||||||
await userStore.logout();
|
await userStore.logout();
|
||||||
rbacUserStore.logout();
|
rbacUserStore.logout();
|
||||||
loggedIn.value = false;
|
router.replace('/h5/login');
|
||||||
router.replace('/h5/me');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleLocale() {
|
function toggleLocale() {
|
||||||
@ -508,8 +420,7 @@ function handleLogout() {
|
|||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
await userStore.logout();
|
await userStore.logout();
|
||||||
rbacUserStore.logout();
|
rbacUserStore.logout();
|
||||||
loggedIn.value = false;
|
router.replace('/h5/login');
|
||||||
router.replace('/h5/me');
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -547,7 +458,7 @@ async function loadExtras() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadProfile() {
|
async function loadProfile() {
|
||||||
if (!loggedIn.value) return;
|
if (!isLogin()) return;
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
const mapId = getMapIdFromAccessToken(rbacUserStore.accessToken);
|
const mapId = getMapIdFromAccessToken(rbacUserStore.accessToken);
|
||||||
@ -562,7 +473,10 @@ async function loadProfile() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loggedIn.value = isLogin();
|
if (!isLogin()) {
|
||||||
|
router.replace({ path: '/h5/login', query: { redirect: '/h5/me' } });
|
||||||
|
return;
|
||||||
|
}
|
||||||
void loadProfile();
|
void loadProfile();
|
||||||
void preloadInvitePosterBg();
|
void preloadInvitePosterBg();
|
||||||
});
|
});
|
||||||
@ -578,10 +492,6 @@ onMounted(() => {
|
|||||||
padding: 20px 16px 28px;
|
padding: 20px 16px 28px;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
background: linear-gradient(180deg, color-mix(in srgb, var(--me-teal) 12%, #f3f6f7) 0%, #f3f6f7 180px);
|
background: linear-gradient(180deg, color-mix(in srgb, var(--me-teal) 12%, #f3f6f7) 0%, #f3f6f7 180px);
|
||||||
|
|
||||||
&--guest {
|
|
||||||
padding-top: 16px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.h5-me-spin {
|
.h5-me-spin {
|
||||||
@ -1121,133 +1031,6 @@ onMounted(() => {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 访客(对齐 /h5/login) */
|
|
||||||
.h5-me-guest {
|
|
||||||
--me-login-ease: cubic-bezier(0.22, 1, 0.36, 1);
|
|
||||||
padding: 8px 0 12px;
|
|
||||||
max-width: 420px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h5-me-guest__brand {
|
|
||||||
padding: 12px 8px 14px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h5-me-guest__logo {
|
|
||||||
display: block;
|
|
||||||
width: 56px;
|
|
||||||
height: 56px;
|
|
||||||
margin: 0 auto 10px;
|
|
||||||
object-fit: contain;
|
|
||||||
border-radius: 16px;
|
|
||||||
background: #fff;
|
|
||||||
box-shadow: 0 8px 22px color-mix(in srgb, var(--me-teal) 18%, transparent);
|
|
||||||
-webkit-touch-callout: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-drag: none;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h5-me-guest__title {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 22px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--me-ink);
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h5-me-guest__card {
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 22px;
|
|
||||||
padding: 14px 18px 20px;
|
|
||||||
box-shadow:
|
|
||||||
0 1px 0 rgba(255, 255, 255, 0.8) inset,
|
|
||||||
0 12px 32px rgba(15, 23, 42, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.h5-me-guest__tabs {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 4px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
padding: 4px;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: #f2f3f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h5-me-guest__tab {
|
|
||||||
height: 36px;
|
|
||||||
border: none;
|
|
||||||
border-radius: 10px;
|
|
||||||
background: transparent;
|
|
||||||
color: #86909c;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&.is-active {
|
|
||||||
background: #fff;
|
|
||||||
color: var(--me-teal);
|
|
||||||
box-shadow: 0 2px 8px rgba(15, 23, 42, 0.06);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.h5-me-guest__extra {
|
|
||||||
margin-top: 18px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h5-me-guest__hint {
|
|
||||||
margin: 0 0 12px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: #86909c;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h5-me-guest__actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: stretch;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.h5-me-text-btn {
|
|
||||||
flex: 1;
|
|
||||||
max-width: 168px;
|
|
||||||
min-height: 44px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 0 12px;
|
|
||||||
border: 1px solid #e5e6eb;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: #fff;
|
|
||||||
color: #4e5969;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-primary {
|
|
||||||
border-color: color-mix(in srgb, var(--me-teal) 35%, #e5e6eb);
|
|
||||||
background: color-mix(in srgb, var(--me-teal) 10%, #fff);
|
|
||||||
color: var(--me-teal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.h5-me-tip {
|
|
||||||
margin: 14px 0 0;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--me-accent);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.h5-me-skel__hero,
|
.h5-me-skel__hero,
|
||||||
.h5-me-skel__card {
|
.h5-me-skel__card {
|
||||||
|
|||||||
@ -17,6 +17,7 @@
|
|||||||
deleteOrderTemplateCustom,
|
deleteOrderTemplateCustom,
|
||||||
postOrderUpload,
|
postOrderUpload,
|
||||||
getOrderBatchJob,
|
getOrderBatchJob,
|
||||||
|
postOrderReceiverParseAddress,
|
||||||
} from '@/api/order';
|
} from '@/api/order';
|
||||||
import { getSenderAddressList } from '@/api/order/senderAddress';
|
import { getSenderAddressList } from '@/api/order/senderAddress';
|
||||||
import { getSalesProductList } from '@/api/channel/salesProduct';
|
import { getSalesProductList } from '@/api/channel/salesProduct';
|
||||||
@ -65,6 +66,13 @@
|
|||||||
const singleShowFieldErrors = ref(false);
|
const singleShowFieldErrors = ref(false);
|
||||||
/** 收件人选填字段默认折叠 */
|
/** 收件人选填字段默认折叠 */
|
||||||
const showRecipientOptional = ref(false);
|
const showRecipientOptional = ref(false);
|
||||||
|
/** 粘贴地址 → AI 解析 */
|
||||||
|
const receiverPasteText = ref('');
|
||||||
|
const receiverParseLoading = ref(false);
|
||||||
|
const receiverParseTip = ref<{
|
||||||
|
tone: 'ok' | 'warn' | 'danger';
|
||||||
|
text: string;
|
||||||
|
} | null>(null);
|
||||||
/** 申报表滚动面板高度(可拖拽调节) */
|
/** 申报表滚动面板高度(可拖拽调节) */
|
||||||
const declarationPanelHeight = ref(280);
|
const declarationPanelHeight = ref(280);
|
||||||
const DECLARATION_PANEL_MIN = 160;
|
const DECLARATION_PANEL_MIN = 160;
|
||||||
@ -901,6 +909,108 @@
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyParsedReceiver(parsed: Record<string, string | undefined>) {
|
||||||
|
const assignKeys = [
|
||||||
|
'recipientsName',
|
||||||
|
'companyName',
|
||||||
|
'phone',
|
||||||
|
'telePhone',
|
||||||
|
'email',
|
||||||
|
'countryCode',
|
||||||
|
'province',
|
||||||
|
'city',
|
||||||
|
'streetNumber',
|
||||||
|
'streetAddress1',
|
||||||
|
'streetAddress2',
|
||||||
|
'streetAddress3',
|
||||||
|
'zipCode',
|
||||||
|
] as const;
|
||||||
|
for (const key of assignKeys) {
|
||||||
|
const val = formTrim(parsed[key] || '');
|
||||||
|
if (val) {
|
||||||
|
;(singleForm.receiver as Record<string, string>)[key] = val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const needOptional = Boolean(
|
||||||
|
formTrim(parsed.companyName || '') ||
|
||||||
|
formTrim(parsed.telePhone || '') ||
|
||||||
|
formTrim(parsed.email || '') ||
|
||||||
|
formTrim(parsed.province || '') ||
|
||||||
|
formTrim(parsed.city || '') ||
|
||||||
|
formTrim(parsed.streetNumber || '') ||
|
||||||
|
formTrim(parsed.streetAddress2 || '') ||
|
||||||
|
formTrim(parsed.streetAddress3 || ''),
|
||||||
|
);
|
||||||
|
if (needOptional) {
|
||||||
|
showRecipientOptional.value = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildReceiverParseTip(result: {
|
||||||
|
isComplete: boolean;
|
||||||
|
missingRequired: string[];
|
||||||
|
missingRecommended: string[];
|
||||||
|
warnings: string[];
|
||||||
|
}) {
|
||||||
|
const parts: string[] = [];
|
||||||
|
if (result.missingRequired.length) {
|
||||||
|
parts.push(`仍缺必填:${result.missingRequired.join('、')}`);
|
||||||
|
}
|
||||||
|
if (result.missingRecommended.length) {
|
||||||
|
parts.push(`建议补全:${result.missingRecommended.join('、')}`);
|
||||||
|
}
|
||||||
|
if (result.warnings.length) {
|
||||||
|
parts.push(result.warnings.slice(0, 2).join(';'));
|
||||||
|
}
|
||||||
|
if (result.isComplete && !result.missingRecommended.length && !result.warnings.length) {
|
||||||
|
receiverParseTip.value = { tone: 'ok', text: '地址信息完整,已填入下方字段,请确认后下单' };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (result.isComplete) {
|
||||||
|
receiverParseTip.value = {
|
||||||
|
tone: 'warn',
|
||||||
|
text: parts.length ? `必填已齐。${parts.join('。')}` : '必填已齐,请确认地址细节',
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
receiverParseTip.value = {
|
||||||
|
tone: 'danger',
|
||||||
|
text: parts.length ? parts.join('。') : '解析后仍不完整,请手动补全',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleReceiverAiParse() {
|
||||||
|
const raw = receiverPasteText.value.trim();
|
||||||
|
if (!raw) {
|
||||||
|
Message.warning('请先粘贴收件人地址文本');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (receiverParseLoading.value) return;
|
||||||
|
receiverParseLoading.value = true;
|
||||||
|
receiverParseTip.value = null;
|
||||||
|
try {
|
||||||
|
const result = await postOrderReceiverParseAddress({ rawText: raw });
|
||||||
|
applyParsedReceiver(result.receiver);
|
||||||
|
buildReceiverParseTip(result);
|
||||||
|
Message.success('已解析并填入收件人信息');
|
||||||
|
} catch (e: any) {
|
||||||
|
receiverParseTip.value = {
|
||||||
|
tone: 'danger',
|
||||||
|
text: e?.message || '地址解析失败,请稍后重试',
|
||||||
|
};
|
||||||
|
Message.error(e?.message || '地址解析失败');
|
||||||
|
} finally {
|
||||||
|
receiverParseLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onReceiverPasteKeydown(e: KeyboardEvent) {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
void handleReceiverAiParse();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function isSingleDeclarationComplete() {
|
function isSingleDeclarationComplete() {
|
||||||
const lines = singleForm.items || [];
|
const lines = singleForm.items || [];
|
||||||
if (!lines.length) return false;
|
if (!lines.length) return false;
|
||||||
@ -2677,13 +2787,56 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section
|
<section
|
||||||
class="single-info-card liquid-card"
|
class="single-info-card liquid-card single-info-card--recipient"
|
||||||
:ref="(el) => (singlePanelRefs.recipient = (el as HTMLElement | null))"
|
:ref="(el) => (singlePanelRefs.recipient = (el as HTMLElement | null))"
|
||||||
>
|
>
|
||||||
<header class="single-info-card__header">
|
<header class="single-info-card__header">
|
||||||
<span class="single-section-index">3</span>
|
<span class="single-section-index">3</span>
|
||||||
<span class="single-section-title">{{ t('order.placeOrder.single.stepRecipient') }}</span>
|
<span class="single-section-title">{{ t('order.placeOrder.single.stepRecipient') }}</span>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<div class="receiver-split">
|
||||||
|
<aside class="receiver-split__paste">
|
||||||
|
<div class="receiver-ai-paste">
|
||||||
|
<div class="receiver-ai-paste__head">
|
||||||
|
<span class="receiver-ai-paste__title">粘贴地址 · AI 解析</span>
|
||||||
|
<span class="receiver-ai-paste__hint">中/英整段粘贴,Ctrl+Enter 解析</span>
|
||||||
|
</div>
|
||||||
|
<a-textarea
|
||||||
|
v-model="receiverPasteText"
|
||||||
|
class="receiver-ai-paste__textarea"
|
||||||
|
:auto-size="{ minRows: 8, maxRows: 16 }"
|
||||||
|
placeholder="例:John Smith, +1 310-555-0199, 1001 Sunset Blvd, Los Angeles, CA 90001, USA 或:张三 13800138000 广东省深圳市南山区科技园南路 XX 号 518000"
|
||||||
|
@keydown="onReceiverPasteKeydown"
|
||||||
|
/>
|
||||||
|
<div class="receiver-ai-paste__actions">
|
||||||
|
<a-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
:loading="receiverParseLoading"
|
||||||
|
@click="handleReceiverAiParse"
|
||||||
|
>
|
||||||
|
AI解析
|
||||||
|
</a-button>
|
||||||
|
<a-button
|
||||||
|
v-if="receiverPasteText"
|
||||||
|
size="small"
|
||||||
|
@click="receiverPasteText = ''; receiverParseTip = null"
|
||||||
|
>
|
||||||
|
清空
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
<p
|
||||||
|
v-if="receiverParseTip"
|
||||||
|
class="receiver-ai-paste__tip"
|
||||||
|
:class="`is-${receiverParseTip.tone}`"
|
||||||
|
>
|
||||||
|
{{ receiverParseTip.text }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<div class="receiver-split__form">
|
||||||
<div class="single-form-grid single-form-grid--compact">
|
<div class="single-form-grid single-form-grid--compact">
|
||||||
<a-form-item
|
<a-form-item
|
||||||
label="收件人名称"
|
label="收件人名称"
|
||||||
@ -2766,6 +2919,8 @@
|
|||||||
<a-form-item label="地址2"><a-input v-model="singleForm.receiver.streetAddress2" /></a-form-item>
|
<a-form-item label="地址2"><a-input v-model="singleForm.receiver.streetAddress2" /></a-form-item>
|
||||||
<a-form-item label="地址3"><a-input v-model="singleForm.receiver.streetAddress3" /></a-form-item>
|
<a-form-item label="地址3"><a-input v-model="singleForm.receiver.streetAddress3" /></a-form-item>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -3669,6 +3824,72 @@
|
|||||||
border-top: 1px dashed rgba(45, 140, 140, 0.18);
|
border-top: 1px dashed rgba(45, 140, 140, 0.18);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.receiver-ai-paste {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 10px;
|
||||||
|
border: 1px dashed color-mix(in srgb, var(--app-primary-color) 35%, #e5e6eb);
|
||||||
|
background: color-mix(in srgb, var(--app-primary-color) 6%, #fff);
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.receiver-ai-paste__head {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.receiver-ai-paste__title {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1d2129;
|
||||||
|
}
|
||||||
|
|
||||||
|
.receiver-ai-paste__hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #86909c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.receiver-ai-paste__textarea {
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
:deep(textarea) {
|
||||||
|
min-height: 160px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.receiver-ai-paste__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.receiver-ai-paste__tip {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.45;
|
||||||
|
|
||||||
|
&.is-ok {
|
||||||
|
color: #00a870;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-warn {
|
||||||
|
color: #d48806;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-danger {
|
||||||
|
color: #f53f3f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.declaration-card {
|
.declaration-card {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -4757,7 +4978,7 @@
|
|||||||
|
|
||||||
.single-cards-row {
|
.single-cards-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
@ -4768,6 +4989,27 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.single-info-card--recipient {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.receiver-split {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(180px, 0.9fr) minmax(0, 1.2fr);
|
||||||
|
gap: 12px;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.receiver-split__paste,
|
||||||
|
.receiver-split__form {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.receiver-split__form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.single-info-card__header {
|
.single-info-card__header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -5340,11 +5582,21 @@
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.receiver-split {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.single-form-grid {
|
.single-form-grid {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1100px) {
|
||||||
|
.receiver-split {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.single-form-grid {
|
.single-form-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
@ -81,7 +81,6 @@
|
|||||||
:stretch-columns="true"
|
:stretch-columns="true"
|
||||||
:height="handsontableHeight"
|
:height="handsontableHeight"
|
||||||
:read-only="true"
|
:read-only="true"
|
||||||
:enable-header-search="false"
|
|
||||||
:enable-column-filter="false"
|
:enable-column-filter="false"
|
||||||
:serial-no-header="t('system.announcement.table.index')"
|
:serial-no-header="t('system.announcement.table.index')"
|
||||||
:action-column-header="t('system.announcement.table.actions')"
|
:action-column-header="t('system.announcement.table.actions')"
|
||||||
|
|||||||
@ -38,6 +38,7 @@
|
|||||||
app: userStore.appKey,
|
app: userStore.appKey,
|
||||||
});
|
});
|
||||||
const searchKeyword = ref('');
|
const searchKeyword = ref('');
|
||||||
|
const headerSearchParams = reactive<Record<string, any>>({});
|
||||||
|
|
||||||
const tableData = ref<any[]>([]);
|
const tableData = ref<any[]>([]);
|
||||||
const totalCount = ref(0);
|
const totalCount = ref(0);
|
||||||
@ -498,7 +499,7 @@
|
|||||||
:total="totalCount"
|
:total="totalCount"
|
||||||
:current-page="currentPage"
|
:current-page="currentPage"
|
||||||
:page-size="pageSize"
|
:page-size="pageSize"
|
||||||
:search-params="{}"
|
:search-params="headerSearchParams"
|
||||||
:show-action-column="true"
|
:show-action-column="true"
|
||||||
:action-column-position="'end'"
|
:action-column-position="'end'"
|
||||||
:action-column-width="110"
|
:action-column-width="110"
|
||||||
|
|||||||
@ -33,6 +33,7 @@
|
|||||||
app: userStore.appKey,
|
app: userStore.appKey,
|
||||||
});
|
});
|
||||||
const searchKeyword = ref('');
|
const searchKeyword = ref('');
|
||||||
|
const headerSearchParams = reactive<Record<string, any>>({});
|
||||||
|
|
||||||
const tableData = ref<any[]>([]);
|
const tableData = ref<any[]>([]);
|
||||||
const totalCount = ref(0);
|
const totalCount = ref(0);
|
||||||
@ -345,7 +346,7 @@
|
|||||||
:total="totalCount"
|
:total="totalCount"
|
||||||
:current-page="currentPage"
|
:current-page="currentPage"
|
||||||
:page-size="pageSize"
|
:page-size="pageSize"
|
||||||
:search-params="{}"
|
:search-params="headerSearchParams"
|
||||||
:show-action-column="true"
|
:show-action-column="true"
|
||||||
:action-column-position="'end'"
|
:action-column-position="'end'"
|
||||||
:action-column-width="110"
|
:action-column-width="110"
|
||||||
|
|||||||
@ -67,6 +67,11 @@
|
|||||||
:columns="detailColumns"
|
:columns="detailColumns"
|
||||||
:data="detailTableData"
|
:data="detailTableData"
|
||||||
:height="tableHeight"
|
:height="tableHeight"
|
||||||
|
:enable-header-search="false"
|
||||||
|
:auto-hybrid-header="false"
|
||||||
|
:enable-header-filter="false"
|
||||||
|
:enable-header-sort="false"
|
||||||
|
:show-active-filter-bar="false"
|
||||||
@selection-change="handleDetailSelectionChange"
|
@selection-change="handleDetailSelectionChange"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -80,6 +85,11 @@
|
|||||||
:columns="pushRecordColumns"
|
:columns="pushRecordColumns"
|
||||||
:data="pushRecordTableData"
|
:data="pushRecordTableData"
|
||||||
:height="tableHeight"
|
:height="tableHeight"
|
||||||
|
:enable-header-search="false"
|
||||||
|
:auto-hybrid-header="false"
|
||||||
|
:enable-header-filter="false"
|
||||||
|
:enable-header-sort="false"
|
||||||
|
:show-active-filter-bar="false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -93,6 +103,11 @@
|
|||||||
:data="logTableData"
|
:data="logTableData"
|
||||||
:height="tableHeight"
|
:height="tableHeight"
|
||||||
:stretch-columns="true"
|
:stretch-columns="true"
|
||||||
|
:enable-header-search="false"
|
||||||
|
:auto-hybrid-header="false"
|
||||||
|
:enable-header-filter="false"
|
||||||
|
:enable-header-sort="false"
|
||||||
|
:show-active-filter-bar="false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -106,6 +121,11 @@
|
|||||||
:columns="pullConfigColumns"
|
:columns="pullConfigColumns"
|
||||||
:data="pullConfigTableData"
|
:data="pullConfigTableData"
|
||||||
:height="pullConfigHandsontableHeight"
|
:height="pullConfigHandsontableHeight"
|
||||||
|
:enable-header-search="false"
|
||||||
|
:auto-hybrid-header="false"
|
||||||
|
:enable-header-filter="false"
|
||||||
|
:enable-header-sort="false"
|
||||||
|
:show-active-filter-bar="false"
|
||||||
/>
|
/>
|
||||||
<a-empty
|
<a-empty
|
||||||
v-else-if="!pullConfigLoading"
|
v-else-if="!pullConfigLoading"
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { useI18n } from 'vue-i18n';
|
|||||||
import { $baseMessage } from '@/utils';
|
import { $baseMessage } from '@/utils';
|
||||||
import { postGetTrackOrderList } from '@/api/TrackOrder/trackOrder';
|
import { postGetTrackOrderList } from '@/api/TrackOrder/trackOrder';
|
||||||
import { convertToTime } from '../utils';
|
import { convertToTime } from '../utils';
|
||||||
|
import { toServerSearchValue } from '@/components/HandsontablePro/filterValue';
|
||||||
|
|
||||||
export function useTrackOrderData(options: {
|
export function useTrackOrderData(options: {
|
||||||
userStore: any;
|
userStore: any;
|
||||||
@ -37,8 +38,9 @@ export function useTrackOrderData(options: {
|
|||||||
// 合并searchInfo和searchParams,优先使用searchParams中的值
|
// 合并searchInfo和searchParams,优先使用searchParams中的值
|
||||||
// 处理客户单号:如果是逗号分隔的字符串,转换为数组
|
// 处理客户单号:如果是逗号分隔的字符串,转换为数组
|
||||||
let orderNos: string[] = [];
|
let orderNos: string[] = [];
|
||||||
if (searchParams.orderNumber) {
|
const orderNumberPlain = toServerSearchValue(searchParams.orderNumber || '');
|
||||||
orderNos = searchParams.orderNumber.split(',').map(v => v.trim()).filter(Boolean);
|
if (orderNumberPlain) {
|
||||||
|
orderNos = orderNumberPlain.split(',').map((v: string) => v.trim()).filter(Boolean);
|
||||||
} else if ((searchInfo as any).orderNos && Array.isArray((searchInfo as any).orderNos)) {
|
} else if ((searchInfo as any).orderNos && Array.isArray((searchInfo as any).orderNos)) {
|
||||||
orderNos = (searchInfo as any).orderNos;
|
orderNos = (searchInfo as any).orderNos;
|
||||||
}
|
}
|
||||||
@ -50,24 +52,24 @@ export function useTrackOrderData(options: {
|
|||||||
// 优先使用表头搜索的客户单号(已转换为数组),否则使用表单的客户单号
|
// 优先使用表头搜索的客户单号(已转换为数组),否则使用表单的客户单号
|
||||||
orderNos: orderNos,
|
orderNos: orderNos,
|
||||||
trackOrderType: trackOrderType,
|
trackOrderType: trackOrderType,
|
||||||
customer: searchParams.customer || '',
|
customer: toServerSearchValue(searchParams.customer || ''),
|
||||||
// 添加所有新的搜索字段(尾程单号和其他单号保持逗号分隔的字符串格式)
|
// 添加所有新的搜索字段(尾程单号和其他单号保持逗号分隔的字符串格式)
|
||||||
trackNo: searchParams.trackNo || '',
|
trackNo: toServerSearchValue(searchParams.trackNo || ''),
|
||||||
otherNo: searchParams.otherNo || '',
|
otherNo: toServerSearchValue(searchParams.otherNo || ''),
|
||||||
product: searchParams.product || '',
|
product: toServerSearchValue(searchParams.product || ''),
|
||||||
channel: searchParams.channel || '',
|
channel: toServerSearchValue(searchParams.channel || ''),
|
||||||
apiCode: searchParams.apiCode || '',
|
apiCode: toServerSearchValue(searchParams.apiCode || ''),
|
||||||
countryCode: searchParams.countryCode || '',
|
countryCode: toServerSearchValue(searchParams.countryCode || ''),
|
||||||
province: searchParams.province || '',
|
province: toServerSearchValue(searchParams.province || ''),
|
||||||
city: searchParams.city || '',
|
city: toServerSearchValue(searchParams.city || ''),
|
||||||
postcode: searchParams.postcode || '',
|
postcode: toServerSearchValue(searchParams.postcode || ''),
|
||||||
email: searchParams.email || '',
|
email: toServerSearchValue(searchParams.email || ''),
|
||||||
newestTrackStatus: searchParams.newestTrackStatus || '',
|
newestTrackStatus: toServerSearchValue(searchParams.newestTrackStatus || ''),
|
||||||
newestTrackContent: searchParams.newestTrackContent || '',
|
newestTrackContent: toServerSearchValue(searchParams.newestTrackContent || ''),
|
||||||
newestTrackEnContent: searchParams.newestTrackEnContent || '',
|
newestTrackEnContent: toServerSearchValue(searchParams.newestTrackEnContent || ''),
|
||||||
newestTrackTime: searchParams.newestTrackTime || '',
|
newestTrackTime: searchParams.newestTrackTime || '',
|
||||||
newestTrackTimeZone: searchParams.newestTrackTimeZone || '',
|
newestTrackTimeZone: toServerSearchValue(searchParams.newestTrackTimeZone || ''),
|
||||||
newestTrackPlace: searchParams.newestTrackPlace || '',
|
newestTrackPlace: toServerSearchValue(searchParams.newestTrackPlace || ''),
|
||||||
isFinish: searchParams.isFinish !== '' ? searchParams.isFinish : undefined,
|
isFinish: searchParams.isFinish !== '' ? searchParams.isFinish : undefined,
|
||||||
createTimeStart: (searchParams as any).createTimeStart || (searchInfo as any).createTimeStart || '',
|
createTimeStart: (searchParams as any).createTimeStart || (searchInfo as any).createTimeStart || '',
|
||||||
createTimeEnd: (searchParams as any).createTimeEnd || (searchInfo as any).createTimeEnd || '',
|
createTimeEnd: (searchParams as any).createTimeEnd || (searchInfo as any).createTimeEnd || '',
|
||||||
|
|||||||
@ -4,29 +4,63 @@ import { useI18n } from 'vue-i18n';
|
|||||||
export function useTableColumns() {
|
export function useTableColumns() {
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
// headerMode:inline 内嵌搜 / icon 仅图标(条件面板) / none 纯展示
|
||||||
const columns = computed(() => [
|
const columns = computed(() => [
|
||||||
{ key: 'customer', label: t('track.order.table.customerShortName'), width: 120, searchable: true, searchField: 'customer' },
|
{ key: 'customer', label: t('track.order.table.customerShortName'), width: 140, searchable: true, searchField: 'customer', headerMode: 'inline' as const },
|
||||||
{ key: 'orderNumber', label: t('track.order.table.orderNo'), width: 180, searchable: true, searchField: 'orderNumber', multiValue: true },
|
{ key: 'orderNumber', label: t('track.order.table.orderNo'), width: 200, searchable: true, searchField: 'orderNumber', multiValue: true, headerMode: 'inline' as const },
|
||||||
{ key: 'trackNo', label: t('track.order.table.trackNo'), width: 180, searchable: true, searchField: 'trackNo', multiValue: true },
|
{ key: 'trackNo', label: t('track.order.table.trackNo'), width: 200, searchable: true, searchField: 'trackNo', multiValue: true, headerMode: 'inline' as const },
|
||||||
{ key: 'otherNo', label: t('track.order.table.otherNo'), width: 180, searchable: true, searchField: 'otherNo', multiValue: true },
|
{ key: 'otherNo', label: t('track.order.table.otherNo'), width: 200, searchable: true, searchField: 'otherNo', multiValue: true, headerMode: 'inline' as const },
|
||||||
{ key: 'product', label: t('track.order.table.product'), width: 150, searchable: true, searchField: 'product' },
|
{ key: 'product', label: t('track.order.table.product'), width: 150, searchable: true, searchField: 'product', headerMode: 'inline' as const },
|
||||||
{ key: 'channel', label: t('track.order.table.channel'), width: 150, searchable: true, searchField: 'channel' },
|
{ key: 'channel', label: t('track.order.table.channel'), width: 150, searchable: true, searchField: 'channel', headerMode: 'inline' as const },
|
||||||
{ key: 'apiCode', label: t('track.order.table.apiCode'), width: 150, searchable: true, searchField: 'apiCode' },
|
{ key: 'apiCode', label: t('track.order.table.apiCode'), width: 150, searchable: true, searchField: 'apiCode', headerMode: 'inline' as const },
|
||||||
{ key: 'countryCode', label: t('track.order.table.countryCode'), width: 100, searchable: true, searchField: 'countryCode' },
|
{ key: 'countryCode', label: t('track.order.table.countryCode'), width: 110, searchable: true, searchField: 'countryCode', headerMode: 'inline' as const },
|
||||||
{ key: 'isFinish', label: t('track.order.table.isComplete'), width: 100, searchable: true, searchField: 'isFinish' },
|
{
|
||||||
{ key: 'newestTrackStatus', label: t('track.order.table.latestTrajectoryStatus'), width: 150, searchable: true, searchField: 'newestTrackStatus' },
|
key: 'isFinish',
|
||||||
{ key: 'newestTrackContent', label: t('track.order.table.latestTrajectoryContent'), width: 250, searchable: true, searchField: 'newestTrackContent' },
|
label: t('track.order.table.isComplete'),
|
||||||
{ key: 'newestTrackEnContent', label: t('track.order.table.latestTrajectoryContentEn'), width: 250, searchable: true, searchField: 'newestTrackEnContent' },
|
width: 110,
|
||||||
{ key: 'newestTrackTime', label: t('track.order.table.latestTrajectoryTime'), width: 180, searchable: true, searchField: 'newestTrackTime' },
|
searchable: true,
|
||||||
{ key: 'newestTrackTimeZone', label: t('track.order.table.latestTrajectoryTimezone'), width: 120, searchable: true, searchField: 'newestTrackTimeZone' },
|
searchField: 'isFinish',
|
||||||
{ key: 'newestTrackPlace', label: t('track.order.table.latestTrajectoryLocation'), width: 200, searchable: true, searchField: 'newestTrackPlace' },
|
headerMode: 'icon' as const,
|
||||||
{ key: 'province', label: t('track.order.table.province'), width: 120, searchable: true, searchField: 'province' },
|
filterType: 'enum' as const,
|
||||||
{ key: 'city', label: t('track.order.table.city'), width: 120, searchable: true, searchField: 'city' },
|
filterable: true,
|
||||||
{ key: 'postcode', label: t('track.order.table.postcode'), width: 100, searchable: true, searchField: 'postcode' },
|
filterOptions: [
|
||||||
{ key: 'email', label: t('track.order.table.email'), width: 200, searchable: true, searchField: 'email' },
|
{ label: '是', value: 'true' },
|
||||||
{ key: 'estimatedDeliveryDate', label: t('track.order.table.estimatedDeliveryTime'), width: 180, searchable: false },
|
{ label: '否', value: 'false' },
|
||||||
{ key: 'destinationTimeZone', label: t('track.order.table.deliveryTimezone'), width: 120, searchable: false },
|
],
|
||||||
{ key: 'createTime', label: t('track.order.table.createTime'), width: 180, searchable: true, searchField: 'createTime' },
|
},
|
||||||
|
{ key: 'newestTrackStatus', label: t('track.order.table.latestTrajectoryStatus'), width: 150, searchable: true, searchField: 'newestTrackStatus', headerMode: 'inline' as const },
|
||||||
|
{ key: 'newestTrackContent', label: t('track.order.table.latestTrajectoryContent'), width: 250, searchable: true, searchField: 'newestTrackContent', headerMode: 'inline' as const },
|
||||||
|
{ key: 'newestTrackEnContent', label: t('track.order.table.latestTrajectoryContentEn'), width: 250, searchable: true, searchField: 'newestTrackEnContent', headerMode: 'inline' as const },
|
||||||
|
{
|
||||||
|
key: 'newestTrackTime',
|
||||||
|
label: t('track.order.table.latestTrajectoryTime'),
|
||||||
|
width: 180,
|
||||||
|
searchable: true,
|
||||||
|
searchField: 'newestTrackTime',
|
||||||
|
headerMode: 'icon' as const,
|
||||||
|
filterType: 'date' as const,
|
||||||
|
filterable: true,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
{ key: 'newestTrackTimeZone', label: t('track.order.table.latestTrajectoryTimezone'), width: 120, searchable: true, searchField: 'newestTrackTimeZone', headerMode: 'inline' as const },
|
||||||
|
{ key: 'newestTrackPlace', label: t('track.order.table.latestTrajectoryLocation'), width: 200, searchable: true, searchField: 'newestTrackPlace', headerMode: 'inline' as const },
|
||||||
|
{ key: 'province', label: t('track.order.table.province'), width: 120, searchable: true, searchField: 'province', headerMode: 'inline' as const },
|
||||||
|
{ key: 'city', label: t('track.order.table.city'), width: 120, searchable: true, searchField: 'city', headerMode: 'inline' as const },
|
||||||
|
{ key: 'postcode', label: t('track.order.table.postcode'), width: 100, searchable: true, searchField: 'postcode', headerMode: 'inline' as const },
|
||||||
|
{ key: 'email', label: t('track.order.table.email'), width: 200, searchable: true, searchField: 'email', headerMode: 'inline' as const },
|
||||||
|
{ key: 'estimatedDeliveryDate', label: t('track.order.table.estimatedDeliveryTime'), width: 180, searchable: false, headerMode: 'none' as const, sortable: true },
|
||||||
|
{ key: 'destinationTimeZone', label: t('track.order.table.deliveryTimezone'), width: 120, searchable: false, headerMode: 'none' as const },
|
||||||
|
{
|
||||||
|
key: 'createTime',
|
||||||
|
label: t('track.order.table.createTime'),
|
||||||
|
width: 180,
|
||||||
|
searchable: true,
|
||||||
|
searchField: 'createTime',
|
||||||
|
headerMode: 'icon' as const,
|
||||||
|
filterType: 'date' as const,
|
||||||
|
filterable: true,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -33,6 +33,7 @@
|
|||||||
app: userStore.appKey,
|
app: userStore.appKey,
|
||||||
});
|
});
|
||||||
const searchKeyword = ref('');
|
const searchKeyword = ref('');
|
||||||
|
const headerSearchParams = reactive<Record<string, any>>({});
|
||||||
|
|
||||||
const tableData = ref<any[]>([]);
|
const tableData = ref<any[]>([]);
|
||||||
const totalCount = ref(0);
|
const totalCount = ref(0);
|
||||||
@ -383,7 +384,7 @@
|
|||||||
:total="totalCount"
|
:total="totalCount"
|
||||||
:current-page="currentPage"
|
:current-page="currentPage"
|
||||||
:page-size="pageSize"
|
:page-size="pageSize"
|
||||||
:search-params="{}"
|
:search-params="headerSearchParams"
|
||||||
:show-action-column="true"
|
:show-action-column="true"
|
||||||
:action-column-position="'end'"
|
:action-column-position="'end'"
|
||||||
:action-column-width="110"
|
:action-column-width="110"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user