Compare commits
No commits in common. "8c47c2772a93d1377f902993fbb793b33aeaaa1d" and "9922d20141a98ae4fccd100c89ac029c005a78d9" have entirely different histories.
8c47c2772a
...
9922d20141
@ -244,17 +244,13 @@ export function mapKpiAreaInfoToExcelRow(item: Record<string, unknown>): KpiArea
|
||||
}
|
||||
}
|
||||
|
||||
export type KpiAreaInfoImportMode = 'incremental' | 'full'
|
||||
|
||||
/** POST /channel/kpi-area/info/import multipart */
|
||||
export async function importKpiAreaInfo(
|
||||
areaId: number,
|
||||
file: File,
|
||||
importMode: KpiAreaInfoImportMode = 'incremental'
|
||||
file: File
|
||||
): Promise<KpiAreaInfoImportResult> {
|
||||
const form = new FormData()
|
||||
form.append('areaId', String(areaId))
|
||||
form.append('importMode', importMode)
|
||||
form.append('file', file)
|
||||
const res = await axiosInstance.post(omsUrl('/channel/kpi-area/info/import'), form)
|
||||
assertBusinessOk(res.data)
|
||||
@ -270,21 +266,3 @@ export async function importKpiAreaInfo(
|
||||
failures,
|
||||
}
|
||||
}
|
||||
|
||||
/** 分区包版本列表(不含正文) */
|
||||
export async function getKpiAreaPublishList(areaId: number | string) {
|
||||
const res = await axiosInstance.get(omsUrl('/channel/kpi-area/publish/list'), {
|
||||
params: { areaId },
|
||||
})
|
||||
const data = unwrapData<any>(res.data)
|
||||
const items = Array.isArray(data) ? data : data?.items ?? data?.Items ?? []
|
||||
return items as Array<Record<string, any>>
|
||||
}
|
||||
|
||||
/** 单条分区包(含正文) */
|
||||
export async function getKpiAreaPublishRecord(id: number | string) {
|
||||
const res = await axiosInstance.get(omsUrl('/channel/kpi-area/publish/get'), {
|
||||
params: { id },
|
||||
})
|
||||
return unwrapData<Record<string, any>>(res.data) ?? res.data?.data ?? res.data
|
||||
}
|
||||
|
||||
@ -199,24 +199,6 @@ export async function getSalesProductPublishPreview(params: {
|
||||
return unwrapData<Record<string, unknown>>(res.data)
|
||||
}
|
||||
|
||||
/** 发布同步态(轻量,不含快照正文) */
|
||||
export async function getSalesProductPublishSyncStatus(params: {
|
||||
salesProductId: number | string
|
||||
operatorType: 'rules' | 'price' | string
|
||||
}) {
|
||||
const res = await axiosInstance.get(omsUrl('/channel/sales-product/publish/sync-status'), { params })
|
||||
return unwrapData<{
|
||||
pending?: boolean
|
||||
Pending?: boolean
|
||||
onlineVersion?: string | null
|
||||
OnlineVersion?: string | null
|
||||
onlinePublishTime?: string | null
|
||||
OnlinePublishTime?: string | null
|
||||
pendingContentHash?: string | null
|
||||
onlineContentHash?: string | null
|
||||
}>(res.data)
|
||||
}
|
||||
|
||||
export async function publishSalesProduct(body: {
|
||||
salesProductId: number | string
|
||||
operatorType: string
|
||||
|
||||
@ -279,26 +279,6 @@ export async function getServiceChannelPublishPreview(params: {
|
||||
return (body?.data ?? body?.Data ?? body) as Record<string, unknown>
|
||||
}
|
||||
|
||||
/** 发布同步态(轻量,不含快照正文),与产品卖价对称 */
|
||||
export async function getServiceChannelPublishSyncStatus(params: {
|
||||
serviceChannelId: number | string
|
||||
operatorType: 'rules' | 'price' | string
|
||||
}) {
|
||||
const res = await axiosInstance.get(omsUrl('/channel/service-channel/publish/sync-status'), { params })
|
||||
const body = res.data
|
||||
assertBusinessOk(body)
|
||||
return (body?.data ?? body?.Data ?? body) as {
|
||||
pending?: boolean
|
||||
Pending?: boolean
|
||||
onlineVersion?: string | null
|
||||
OnlineVersion?: string | null
|
||||
onlinePublishTime?: string | null
|
||||
OnlinePublishTime?: string | null
|
||||
pendingContentHash?: string | null
|
||||
onlineContentHash?: string | null
|
||||
}
|
||||
}
|
||||
|
||||
export async function publishServiceChannel(body: {
|
||||
serviceChannelId: number | string
|
||||
operatorType: string
|
||||
|
||||
1
src/components.d.ts
vendored
1
src/components.d.ts
vendored
@ -35,7 +35,6 @@ declare module 'vue' {
|
||||
JsonEditorPro: typeof import('./components/JsonEditorPro/index.vue')['default']
|
||||
MarkdownDocPanel: typeof import('./components/MarkdownDocPanel/index.vue')['default']
|
||||
MarkdownViewer: typeof import('./components/MarkdownViewer/index.vue')['default']
|
||||
MemberLevelBadge: typeof import('./components/MemberLevelBadge/index.vue')['default']
|
||||
MenuIconPicker: typeof import('./components/MenuIconPicker/index.vue')['default']
|
||||
ModalForm: typeof import('./components/ModalForm/index.vue')['default']
|
||||
ModalPro: typeof import('./components/ModalPro/index.vue')['default']
|
||||
|
||||
@ -590,13 +590,13 @@
|
||||
.custom-left-menu {
|
||||
--clm-bg: #ffffff;
|
||||
--clm-text: #1a1c20;
|
||||
--clm-muted: #6b7280;
|
||||
--clm-faint: #9ca3af;
|
||||
--clm-muted: #8a8f98;
|
||||
--clm-faint: #b0b4ba;
|
||||
--clm-hover: rgba(15, 16, 17, 0.04);
|
||||
--clm-selected: var(--app-primary-color-light);
|
||||
--clm-selected-text: var(--app-primary-color);
|
||||
--clm-selected-border: color-mix(in srgb, var(--app-primary-color) 40%, transparent);
|
||||
--clm-border: rgba(15, 16, 17, 0.12);
|
||||
--clm-selected-border: color-mix(in srgb, var(--app-primary-color) 32%, transparent);
|
||||
--clm-border: rgba(15, 16, 17, 0.08);
|
||||
--clm-radius: 8px;
|
||||
/* 列表卡片统一高度 */
|
||||
--clm-node-title-line-height: 18px;
|
||||
@ -856,58 +856,54 @@
|
||||
:deep(.arco-tree-node-line),
|
||||
:deep(.arco-tree-node-line-top),
|
||||
:deep(.arco-tree-node-line-bottom) {
|
||||
border-color: var(--clm-border) !important;
|
||||
border-color: rgba(15, 16, 17, 0.08) !important;
|
||||
}
|
||||
|
||||
/*
|
||||
* 层级树(组织等)节点本身就是可选内容,不能套用「分组头」透明边框/淡字样式。
|
||||
*/
|
||||
.tree-node {
|
||||
margin: calc(var(--clm-node-gap) / 2) 0;
|
||||
padding: 8px 10px;
|
||||
padding: var(--clm-node-padding-y) 8px;
|
||||
border-radius: var(--clm-radius);
|
||||
border: 1px solid var(--clm-border);
|
||||
background: var(--color-fill-1, #f7f8fa);
|
||||
border: 1px solid rgba(15, 16, 17, 0.06);
|
||||
background: transparent;
|
||||
box-sizing: border-box;
|
||||
height: auto;
|
||||
min-height: 44px;
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
height: var(--clm-node-height);
|
||||
min-height: var(--clm-node-height);
|
||||
max-height: var(--clm-node-height);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
align-items: stretch;
|
||||
|
||||
&.tree-node--branch,
|
||||
&.tree-node--leaf {
|
||||
background: var(--color-fill-1, #f7f8fa);
|
||||
border-color: var(--clm-border);
|
||||
&.tree-node--with-code {
|
||||
min-height: var(--clm-node-height);
|
||||
}
|
||||
|
||||
&.tree-node--branch {
|
||||
height: auto;
|
||||
min-height: 34px;
|
||||
max-height: none;
|
||||
margin: calc(var(--clm-node-gap) + 4px) 0 calc(var(--clm-node-gap) / 2);
|
||||
padding: 6px 8px;
|
||||
border-color: transparent;
|
||||
background: transparent;
|
||||
overflow: visible;
|
||||
|
||||
.node-second-row-spacer-cell {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.node-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--clm-text);
|
||||
}
|
||||
|
||||
.node-subtitle {
|
||||
color: var(--clm-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.node-subtitle-cell {
|
||||
padding-top: 2px;
|
||||
padding-left: 11px;
|
||||
}
|
||||
&.tree-node--leaf {
|
||||
background: var(--color-fill-1);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--clm-hover);
|
||||
border-color: rgba(15, 16, 17, 0.18);
|
||||
border-color: rgba(15, 16, 17, 0.1);
|
||||
}
|
||||
|
||||
&.tree-node--branch:hover {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
@ -918,29 +914,39 @@
|
||||
|
||||
.node-title {
|
||||
color: var(--clm-selected-text) !important;
|
||||
font-weight: 600;
|
||||
font-weight: 560;
|
||||
}
|
||||
|
||||
.node-subtitle {
|
||||
color: var(--clm-muted) !important;
|
||||
}
|
||||
|
||||
.node-title::before {
|
||||
background: var(--app-primary-color);
|
||||
&.tree-node--branch {
|
||||
background: transparent !important;
|
||||
border-color: transparent !important;
|
||||
|
||||
.node-title {
|
||||
color: var(--clm-muted) !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.node-title::before {
|
||||
background: var(--app-primary-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tree-node--depth-1 .node-title {
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
color: var(--clm-text);
|
||||
.tree-node--depth-1.tree-node--branch .node-title {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
color: var(--clm-muted);
|
||||
}
|
||||
|
||||
.tree-node--depth-2 .node-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--clm-text);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tree-node--depth-3,
|
||||
@ -948,26 +954,25 @@
|
||||
.tree-node--depth-5,
|
||||
.tree-node--depth-6 {
|
||||
.node-title {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--clm-text);
|
||||
font-size: 11px;
|
||||
font-weight: 450;
|
||||
}
|
||||
}
|
||||
|
||||
.tree-node .node-title::before {
|
||||
.tree-node--branch .node-title::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 3px;
|
||||
height: 12px;
|
||||
margin-right: 8px;
|
||||
border-radius: 2px;
|
||||
background: color-mix(in srgb, var(--app-primary-color) 70%, var(--clm-muted));
|
||||
background: color-mix(in srgb, var(--app-primary-color) 55%, var(--clm-faint));
|
||||
vertical-align: -2px;
|
||||
}
|
||||
|
||||
.node-subtitle {
|
||||
color: var(--clm-muted);
|
||||
font-size: 12px;
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,134 +0,0 @@
|
||||
<template>
|
||||
<span
|
||||
v-if="displayName"
|
||||
class="member-level-badge"
|
||||
:class="[`is-${size}`, tone ? `is-${tone.key}` : 'is-fallback']"
|
||||
:style="badgeStyle"
|
||||
:title="displayName"
|
||||
>
|
||||
<img
|
||||
v-if="resolvedIconUrl"
|
||||
class="member-level-badge__img"
|
||||
:src="resolvedIconUrl"
|
||||
alt=""
|
||||
draggable="false"
|
||||
/>
|
||||
<span
|
||||
v-else-if="tone"
|
||||
class="member-level-badge__glyph"
|
||||
aria-hidden="true"
|
||||
v-html="glyphHtml"
|
||||
/>
|
||||
<span v-else class="member-level-badge__glyph member-level-badge__glyph--text" aria-hidden="true">
|
||||
{{ displayName.slice(0, 1) }}
|
||||
</span>
|
||||
<span class="member-level-badge__name">{{ displayName }}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
import {
|
||||
getMemberLevelTone,
|
||||
renderMemberLevelGlyphSvg,
|
||||
} from '@/utils/memberLevelIcon'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
levelCode?: string | null
|
||||
levelName?: string | null
|
||||
iconUrl?: string | null
|
||||
size?: 'sm' | 'md'
|
||||
}>(),
|
||||
{
|
||||
levelCode: '',
|
||||
levelName: '',
|
||||
iconUrl: '',
|
||||
size: 'sm',
|
||||
},
|
||||
)
|
||||
|
||||
const displayName = computed(() => String(props.levelName || '').trim())
|
||||
const tone = computed(() => getMemberLevelTone(props.levelCode, props.levelName))
|
||||
const resolvedIconUrl = computed(() => String(props.iconUrl || '').trim())
|
||||
const glyphHtml = computed(() =>
|
||||
tone.value ? renderMemberLevelGlyphSvg(tone.value.key, props.size === 'md' ? 16 : 14) : '',
|
||||
)
|
||||
const badgeStyle = computed(() => {
|
||||
if (!tone.value) return undefined
|
||||
return {
|
||||
'--mlb-color': tone.value.color,
|
||||
'--mlb-bg': tone.value.bg,
|
||||
'--mlb-border': tone.value.border,
|
||||
} as Record<string, string>
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.member-level-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
max-width: 100%;
|
||||
min-height: 22px;
|
||||
padding: 2px 8px 2px 6px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--mlb-border, var(--color-border-2));
|
||||
background: var(--mlb-bg, var(--color-fill-2));
|
||||
color: var(--mlb-color, var(--app-primary-color));
|
||||
vertical-align: middle;
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.member-level-badge.is-md {
|
||||
min-height: 26px;
|
||||
gap: 6px;
|
||||
padding: 3px 10px 3px 7px;
|
||||
}
|
||||
|
||||
.member-level-badge__img,
|
||||
.member-level-badge__glyph {
|
||||
flex-shrink: 0;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.member-level-badge.is-md .member-level-badge__img,
|
||||
.member-level-badge.is-md .member-level-badge__glyph {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.member-level-badge__img {
|
||||
object-fit: contain;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.member-level-badge__glyph :deep(svg) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.member-level-badge__glyph--text {
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.member-level-badge__name {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.member-level-badge.is-md .member-level-badge__name {
|
||||
font-size: 13px;
|
||||
}
|
||||
</style>
|
||||
@ -158,13 +158,7 @@
|
||||
class="publish-diff-synced-panel"
|
||||
>
|
||||
<slot name="synced">
|
||||
<pre
|
||||
v-if="(modifiedJson?.length || 0) <= 120000"
|
||||
class="diff-pre synced-preview"
|
||||
>{{ modifiedJson }}</pre>
|
||||
<p v-else class="raw-json-placeholder">
|
||||
快照较大(约 {{ Math.round((modifiedJson?.length || 0) / 1024) }} KB),已省略全文预览以免卡顿。
|
||||
</p>
|
||||
<pre class="diff-pre synced-preview">{{ modifiedJson }}</pre>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
@ -240,8 +234,7 @@
|
||||
@toggle="onRawToggle"
|
||||
>
|
||||
<summary>{{ rawSummaryText }}</summary>
|
||||
<!-- 大快照仅在展开后挂载,避免未展开就把 MB 级文本塞进 DOM 卡死 -->
|
||||
<div v-if="rawDetailsOpen" class="diff-boxes">
|
||||
<div class="diff-boxes">
|
||||
<div class="diff-box">
|
||||
<div class="diff-box-title">{{ originalTitle }}</div>
|
||||
<pre class="diff-pre">{{ originalJson || '{}' }}</pre>
|
||||
@ -251,7 +244,6 @@
|
||||
<pre class="diff-pre">{{ modifiedJson || '{}' }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="raw-json-placeholder">展开后加载原始 JSON…</p>
|
||||
</details>
|
||||
|
||||
<div v-else-if="rawMode === 'always'" class="diff-boxes">
|
||||
@ -479,15 +471,6 @@
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.raw-json-placeholder {
|
||||
margin: 8px 0 0;
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
background: var(--color-fill-1, #f7f8fa);
|
||||
color: var(--color-text-3);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.raw-json-details {
|
||||
border: 1px solid var(--color-border-2);
|
||||
border-radius: 6px;
|
||||
|
||||
@ -257,11 +257,10 @@ const modelTab = computed({
|
||||
.shell-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px 24px;
|
||||
padding: 10px 24px;
|
||||
border-bottom: 1px solid var(--color-border-2);
|
||||
background: var(--color-fill-1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
|
||||
@ -1,47 +1,68 @@
|
||||
<template>
|
||||
<a-drawer
|
||||
:visible="visible"
|
||||
:width="drawerWidth"
|
||||
:width="640"
|
||||
unmount-on-close
|
||||
:esc-to-close="true"
|
||||
:mask-closable="!confirmLoading"
|
||||
:header="false"
|
||||
:footer="false"
|
||||
class="user-edit-drawer"
|
||||
:drawer-style="drawerShellStyle"
|
||||
:body-style="drawerBodyStyle"
|
||||
:footer="false"
|
||||
@update:visible="onVisibleChange"
|
||||
@cancel="handleCancel"
|
||||
>
|
||||
<div class="ue-drawer">
|
||||
<header class="ue-drawer__bar">
|
||||
<div class="ue-drawer__bar-main">
|
||||
<h2 class="ue-drawer__title">
|
||||
{{ isEdit ? t('system.user.modal.edit') : t('system.user.modal.add') }}
|
||||
</h2>
|
||||
<span class="ue-drawer__chip">{{ t('system.user.drawer.eyebrow') }}</span>
|
||||
<button
|
||||
v-if="isEdit && canResetPassword"
|
||||
type="button"
|
||||
class="ue-drawer__action"
|
||||
:disabled="resetPwdLoading || confirmLoading"
|
||||
@click="openResetPasswordModal"
|
||||
>
|
||||
{{ t('system.user.actions.resetPassword') }}
|
||||
<header class="ue-drawer__header">
|
||||
<div class="ue-drawer__title-row">
|
||||
<div>
|
||||
<p class="ue-drawer__eyebrow">{{ t('system.user.drawer.eyebrow') }}</p>
|
||||
<h3 class="ue-drawer__title">
|
||||
{{ isEdit ? t('system.user.modal.edit') : t('system.user.modal.add') }}
|
||||
</h3>
|
||||
</div>
|
||||
<button type="button" class="ue-drawer__close" @click="handleCancel">
|
||||
<icon-close />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="ue-drawer__close"
|
||||
:aria-label="t('common.cancel')"
|
||||
:disabled="confirmLoading"
|
||||
@click="handleCancel"
|
||||
>
|
||||
<icon-close />
|
||||
</button>
|
||||
<p class="ue-drawer__desc">{{ t('system.user.drawer.desc') }}</p>
|
||||
</header>
|
||||
|
||||
<div class="ue-drawer__body">
|
||||
<section class="ue-preview">
|
||||
<div class="ue-preview__avatar">
|
||||
<img v-if="avatarUrl" :src="avatarUrl" alt="" />
|
||||
<span v-else>{{ previewInitial }}</span>
|
||||
</div>
|
||||
<div class="ue-preview__meta">
|
||||
<h4 class="ue-preview__name">
|
||||
{{ formState.name || t('system.user.drawer.nameFallback') }}
|
||||
</h4>
|
||||
<p class="ue-preview__account">
|
||||
{{ formState.account || t('system.user.drawer.accountFallback') }}
|
||||
<template v-if="formState.employeeNo"> · {{ formState.employeeNo }}</template>
|
||||
</p>
|
||||
<div class="ue-preview__tags">
|
||||
<span v-if="formState.phone" class="ue-preview__tag">{{ formState.phone }}</span>
|
||||
<span v-if="orgDisplayName" class="ue-preview__tag">{{ orgDisplayName }}</span>
|
||||
<span
|
||||
v-if="employeeFormState.employmentStatus"
|
||||
class="ue-preview__tag ue-preview__tag--accent"
|
||||
>
|
||||
{{ employmentStatusText }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isEdit && canResetPassword" class="ue-preview__actions">
|
||||
<button
|
||||
type="button"
|
||||
class="ue-preview__action"
|
||||
:disabled="resetPwdLoading"
|
||||
@click="openResetPasswordModal"
|
||||
>
|
||||
{{ t('system.user.actions.resetPassword') }}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<a-form
|
||||
ref="formRef"
|
||||
class="ue-form"
|
||||
@ -51,176 +72,179 @@
|
||||
:label-col-props="{ span: 24 }"
|
||||
:wrapper-col-props="{ span: 24 }"
|
||||
>
|
||||
<section class="ue-card">
|
||||
<div class="ue-card__head">
|
||||
<h4 class="ue-card__title">{{ t('system.user.drawer.section.account') }}</h4>
|
||||
<span class="ue-card__hint">{{ t('system.user.drawer.section.accountHint') }}</span>
|
||||
<section class="ue-section">
|
||||
<div class="ue-section__head">
|
||||
<span class="ue-section__index">01</span>
|
||||
<div>
|
||||
<h5 class="ue-section__title">{{ t('system.user.drawer.section.account') }}</h5>
|
||||
<p class="ue-section__hint">{{ t('system.user.drawer.section.accountHint') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ue-card__body">
|
||||
<a-row :gutter="[16, 0]">
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('system.user.form.name')" field="name">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('system.user.form.name')" field="name">
|
||||
<a-input
|
||||
v-model="formState.name"
|
||||
:placeholder="t('system.user.form.namePlaceholder')"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('system.user.form.account')" field="account">
|
||||
<a-input
|
||||
v-model="formState.account"
|
||||
:placeholder="t('system.user.form.accountPlaceholder')"
|
||||
:disabled="isEdit"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-if="!isEdit" :span="12">
|
||||
<a-form-item :label="t('system.user.form.password')" field="password">
|
||||
<a-input-password
|
||||
v-model="formState.password"
|
||||
:placeholder="t('system.user.form.passwordPlaceholder')"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('system.user.form.email')" field="email">
|
||||
<a-input
|
||||
v-model="formState.email"
|
||||
:placeholder="t('system.user.form.emailPlaceholder')"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('system.user.form.organization')" field="organizationId">
|
||||
<a-tree-select
|
||||
v-model="formState.organizationId"
|
||||
:placeholder="t('system.user.form.organizationPlaceholder')"
|
||||
:data="organizationTreeData"
|
||||
allow-clear
|
||||
:loading="organizationLoading"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('system.user.form.avatar')" field="avatar">
|
||||
<a-input
|
||||
v-model="formState.avatar"
|
||||
:placeholder="t('system.user.form.avatarPlaceholder')"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-if="!isEdit" :span="24">
|
||||
<a-form-item :label="t('system.user.form.roles')" field="roleIds">
|
||||
<a-select
|
||||
v-model="formState.roleIds"
|
||||
multiple
|
||||
allow-clear
|
||||
allow-search
|
||||
:placeholder="t('system.user.form.rolesPlaceholder')"
|
||||
:options="roleOptions"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-if="isEdit && advisorInviteUrl" :span="24">
|
||||
<a-form-item :label="t('system.user.form.advisorInviteLink')">
|
||||
<div class="ue-invite-row">
|
||||
<a-input
|
||||
v-model="formState.name"
|
||||
:placeholder="t('system.user.form.namePlaceholder')"
|
||||
allow-clear
|
||||
class="ue-invite-input"
|
||||
:model-value="advisorInviteUrl"
|
||||
readonly
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('system.user.form.account')" field="account">
|
||||
<a-input
|
||||
v-model="formState.account"
|
||||
:placeholder="t('system.user.form.accountPlaceholder')"
|
||||
:disabled="isEdit"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-if="!isEdit" :span="12">
|
||||
<a-form-item :label="t('system.user.form.password')" field="password">
|
||||
<a-input-password
|
||||
v-model="formState.password"
|
||||
:placeholder="t('system.user.form.passwordPlaceholder')"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('system.user.form.email')" field="email">
|
||||
<a-input
|
||||
v-model="formState.email"
|
||||
:placeholder="t('system.user.form.emailPlaceholder')"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('system.user.form.organization')" field="organizationId">
|
||||
<a-tree-select
|
||||
v-model="formState.organizationId"
|
||||
:placeholder="t('system.user.form.organizationPlaceholder')"
|
||||
:data="organizationTreeData"
|
||||
allow-clear
|
||||
:loading="organizationLoading"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('system.user.form.avatar')" field="avatar">
|
||||
<a-input
|
||||
v-model="formState.avatar"
|
||||
:placeholder="t('system.user.form.avatarPlaceholder')"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-if="!isEdit" :span="24">
|
||||
<a-form-item :label="t('system.user.form.roles')" field="roleIds">
|
||||
<a-select
|
||||
v-model="formState.roleIds"
|
||||
multiple
|
||||
allow-clear
|
||||
allow-search
|
||||
:placeholder="t('system.user.form.rolesPlaceholder')"
|
||||
:options="roleOptions"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col v-if="isEdit && advisorInviteUrl" :span="24">
|
||||
<a-form-item :label="t('system.user.form.advisorInviteLink')">
|
||||
<div class="ue-invite-row">
|
||||
<a-input
|
||||
class="ue-invite-input"
|
||||
:model-value="advisorInviteUrl"
|
||||
readonly
|
||||
/>
|
||||
<a-button type="primary" class="ue-invite-copy" @click="copyAdvisorInviteLink">
|
||||
{{ t('system.user.actions.copyInviteLink') }}
|
||||
</a-button>
|
||||
</div>
|
||||
<template #extra>
|
||||
<span class="ue-invite-hint">{{ t('system.user.form.advisorInviteHint') }}</span>
|
||||
<a-button type="primary" class="ue-invite-copy" @click="copyAdvisorInviteLink">
|
||||
{{ t('system.user.actions.copyInviteLink') }}
|
||||
</a-button>
|
||||
</div>
|
||||
<template #extra>
|
||||
<span class="ue-invite-hint">{{ t('system.user.form.advisorInviteHint') }}</span>
|
||||
</template>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</section>
|
||||
|
||||
<section class="ue-section">
|
||||
<div class="ue-section__head">
|
||||
<span class="ue-section__index">02</span>
|
||||
<div>
|
||||
<h5 class="ue-section__title">{{ t('system.user.drawer.section.contact') }}</h5>
|
||||
<p class="ue-section__hint">{{ t('system.user.drawer.section.contactHint') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('system.user.form.phone')" field="phone">
|
||||
<a-input
|
||||
v-model="formState.phone"
|
||||
:placeholder="t('system.user.form.phonePlaceholder')"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('system.user.form.employeeNo')" field="employeeNo">
|
||||
<a-input
|
||||
v-model="formState.employeeNo"
|
||||
:placeholder="t('system.user.form.employeeNoPlaceholder')"
|
||||
:readonly="isEdit"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('system.user.form.wechatId')" field="wechatId">
|
||||
<a-input
|
||||
v-model="formState.wechatId"
|
||||
:placeholder="t('system.user.form.wechatIdPlaceholder')"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('system.user.form.wecomKfUrl')" field="wecomKfUrl">
|
||||
<a-input
|
||||
v-model="formState.wecomKfUrl"
|
||||
:placeholder="t('system.user.form.wecomKfUrlPlaceholder')"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('system.user.form.wechatQrCode')" field="wechatQrFiles">
|
||||
<a-upload
|
||||
v-model:file-list="formState.wechatQrFiles"
|
||||
list-type="picture-card"
|
||||
:limit="1"
|
||||
accept="image/*"
|
||||
:custom-request="handleWechatCustomRequest"
|
||||
>
|
||||
<template #upload-button>
|
||||
<div class="ue-upload-placeholder">
|
||||
<icon-plus />
|
||||
<span>{{ t('system.user.form.wechatQrUpload') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</a-upload>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</section>
|
||||
|
||||
<section class="ue-card">
|
||||
<div class="ue-card__head">
|
||||
<h4 class="ue-card__title">{{ t('system.user.drawer.section.contact') }}</h4>
|
||||
<span class="ue-card__hint">{{ t('system.user.drawer.section.contactHint') }}</span>
|
||||
</div>
|
||||
<div class="ue-card__body">
|
||||
<a-row :gutter="[16, 0]">
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('system.user.form.phone')" field="phone">
|
||||
<a-input
|
||||
v-model="formState.phone"
|
||||
:placeholder="t('system.user.form.phonePlaceholder')"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('system.user.form.employeeNo')" field="employeeNo">
|
||||
<a-input
|
||||
v-model="formState.employeeNo"
|
||||
:placeholder="t('system.user.form.employeeNoPlaceholder')"
|
||||
:readonly="isEdit"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('system.user.form.wechatId')" field="wechatId">
|
||||
<a-input
|
||||
v-model="formState.wechatId"
|
||||
:placeholder="t('system.user.form.wechatIdPlaceholder')"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('system.user.form.wecomKfUrl')" field="wecomKfUrl">
|
||||
<a-input
|
||||
v-model="formState.wecomKfUrl"
|
||||
:placeholder="t('system.user.form.wecomKfUrlPlaceholder')"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('system.user.form.wechatQrCode')" field="wechatQrFiles">
|
||||
<a-upload
|
||||
v-model:file-list="formState.wechatQrFiles"
|
||||
list-type="picture-card"
|
||||
:limit="1"
|
||||
accept="image/*"
|
||||
:custom-request="handleWechatCustomRequest"
|
||||
>
|
||||
<template #upload-button>
|
||||
<div class="ue-upload-placeholder">
|
||||
<icon-plus />
|
||||
<span>{{ t('system.user.form.wechatQrUpload') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</a-upload>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="ue-card">
|
||||
<div class="ue-card__head">
|
||||
<h4 class="ue-card__title">{{ t('system.employee.section') }}</h4>
|
||||
<span class="ue-card__hint">{{ t('system.user.drawer.section.employeeHint') }}</span>
|
||||
</div>
|
||||
<div class="ue-card__body">
|
||||
<EmployeeProfileFields :model-value="employeeFormState" hide-title dense />
|
||||
<section class="ue-section">
|
||||
<div class="ue-section__head">
|
||||
<span class="ue-section__index">03</span>
|
||||
<div>
|
||||
<h5 class="ue-section__title">{{ t('system.employee.section') }}</h5>
|
||||
<p class="ue-section__hint">{{ t('system.user.drawer.section.employeeHint') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<EmployeeProfileFields :model-value="employeeFormState" hide-title dense />
|
||||
</section>
|
||||
</a-form>
|
||||
</div>
|
||||
@ -279,6 +303,7 @@ import EmployeeProfileFields from '@/components/EmployeeProfileFields/index.vue'
|
||||
import { resetUserPassword } from '@/api/rbac/internal-user'
|
||||
import type { EmployeeFormState } from '@/types/employee-profile'
|
||||
import type { UserFormExtState } from '@/types/rbac-user-profile'
|
||||
import { resolveAvatarUrl } from '@/utils/avatar'
|
||||
import { buildAdvisorInviteRegisterUrl } from '@/utils/inviteLink'
|
||||
import { hasPermission } from '@/utils/rbac-permission'
|
||||
import { $baseMessage } from '@/utils'
|
||||
@ -337,23 +362,43 @@ const canResetPassword = computed(
|
||||
() => hasPermission('Rbac.Users.ResetPassword') || hasPermission('Rbac.Users.Update'),
|
||||
)
|
||||
|
||||
const drawerWidth = computed(() => {
|
||||
if (typeof window !== 'undefined' && window.innerWidth < 768) return '100%'
|
||||
return 640
|
||||
const avatarUrl = computed(() => {
|
||||
const raw = props.formState.avatar?.trim()
|
||||
return raw ? resolveAvatarUrl(raw) : ''
|
||||
})
|
||||
|
||||
const drawerShellStyle = {
|
||||
borderRadius: '16px 0 0 16px',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '-12px 0 40px rgba(15, 23, 42, 0.12)',
|
||||
}
|
||||
const previewInitial = computed(() => {
|
||||
const name = props.formState.name?.trim()
|
||||
if (name) return name.slice(0, 1)
|
||||
const account = props.formState.account?.trim()
|
||||
return account ? account.slice(0, 1).toUpperCase() : 'U'
|
||||
})
|
||||
|
||||
const drawerBodyStyle = {
|
||||
padding: '0',
|
||||
background: 'var(--app-page-bg)',
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
}
|
||||
const orgDisplayName = computed(() => {
|
||||
const id = props.formState.organizationId
|
||||
if (!id) return ''
|
||||
const find = (nodes: any[]): string => {
|
||||
for (const node of nodes || []) {
|
||||
if (node.value === id || node.key === id || node.id === id) {
|
||||
return String(node.label || node.title || node.name || '')
|
||||
}
|
||||
const child = find(node.children || [])
|
||||
if (child) return child
|
||||
}
|
||||
return ''
|
||||
}
|
||||
return find(props.organizationTreeData || [])
|
||||
})
|
||||
|
||||
const employmentStatusText = computed(() => {
|
||||
const map: Record<number, string> = {
|
||||
1: t('system.employee.status.probation'),
|
||||
2: t('system.employee.status.regular'),
|
||||
3: t('system.employee.status.suspended'),
|
||||
4: t('system.employee.status.resigned'),
|
||||
}
|
||||
return map[props.employeeFormState.employmentStatus] || ''
|
||||
})
|
||||
|
||||
const advisorInviteUrl = computed(() =>
|
||||
buildAdvisorInviteRegisterUrl(String(props.formState.account || '')),
|
||||
@ -469,105 +514,77 @@ defineExpose({
|
||||
|
||||
<style scoped lang="less">
|
||||
.ue-drawer {
|
||||
--ue-ink: #1a2e38;
|
||||
--ue-muted: rgba(26, 46, 56, 0.68);
|
||||
--ue-soft: rgba(26, 46, 56, 0.48);
|
||||
--ue-radius: 16px;
|
||||
--ue-edge: var(--app-acrylic-border);
|
||||
--ue-panel-bg: var(--app-acrylic-bg);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
background: var(--app-page-bg);
|
||||
outline: none;
|
||||
margin: -16px;
|
||||
background: var(--app-page-bg, #f0f3f4);
|
||||
}
|
||||
|
||||
.ue-drawer__bar {
|
||||
.ue-drawer__header {
|
||||
flex-shrink: 0;
|
||||
padding: 18px 20px 14px;
|
||||
border-bottom: 1px solid var(--ue-edge);
|
||||
background: var(--ue-panel-bg);
|
||||
backdrop-filter: var(--app-acrylic-blur);
|
||||
-webkit-backdrop-filter: var(--app-acrylic-blur);
|
||||
}
|
||||
|
||||
.ue-drawer__title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
margin: 12px 12px 0;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.78);
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(155deg, rgba(255, 255, 255, 0.92) 0%, rgba(255, 255, 255, 0.72) 100%);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.9),
|
||||
0 4px 14px rgba(0, 121, 121, 0.05);
|
||||
}
|
||||
|
||||
.ue-drawer__bar-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
.ue-drawer__eyebrow {
|
||||
margin: 0 0 4px;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--app-primary-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ue-drawer__title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-size: 18px;
|
||||
line-height: 1.35;
|
||||
font-weight: 700;
|
||||
color: var(--color-text-1);
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
color: var(--ue-ink);
|
||||
}
|
||||
|
||||
.ue-drawer__chip {
|
||||
flex-shrink: 0;
|
||||
height: 24px;
|
||||
padding: 0 8px;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--app-primary-color) 10%, var(--app-card-bg));
|
||||
color: var(--app-primary-color);
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.ue-drawer__action {
|
||||
flex-shrink: 0;
|
||||
height: 24px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid color-mix(in srgb, var(--app-primary-color) 28%, var(--color-border-2));
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: var(--app-primary-color);
|
||||
font-size: 12px;
|
||||
line-height: 22px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s, border-color 0.2s;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: color-mix(in srgb, var(--app-primary-color) 8%, var(--app-card-bg));
|
||||
border-color: var(--app-primary-color);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.ue-drawer__desc {
|
||||
margin: 8px 0 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
color: var(--ue-muted);
|
||||
}
|
||||
|
||||
.ue-drawer__close {
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
color: var(--color-text-2);
|
||||
border: 1px solid var(--ue-edge);
|
||||
border-radius: 12px;
|
||||
background: var(--ue-panel-bg);
|
||||
color: var(--ue-muted);
|
||||
cursor: pointer;
|
||||
transition: color 0.2s, background 0.2s, border-color 0.2s;
|
||||
transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
&:hover {
|
||||
border-color: color-mix(in srgb, var(--app-primary-color) 22%, var(--ue-edge));
|
||||
background: var(--app-acrylic-hover-bg);
|
||||
color: var(--app-primary-color);
|
||||
border-color: color-mix(in srgb, var(--app-primary-color) 30%, var(--app-card-bg));
|
||||
background: color-mix(in srgb, var(--app-primary-color) 8%, var(--app-card-bg));
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
@ -575,44 +592,172 @@ defineExpose({
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 10px 12px 16px;
|
||||
padding: 16px 16px 10px;
|
||||
}
|
||||
|
||||
.ue-card {
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.78);
|
||||
border-radius: 14px;
|
||||
background: var(--app-card-bg);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.9),
|
||||
0 4px 14px rgba(0, 121, 121, 0.04);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ue-card__head {
|
||||
.ue-preview {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
padding: 14px 16px 0;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
margin-bottom: 14px;
|
||||
padding: 14px 16px;
|
||||
border-radius: var(--ue-radius);
|
||||
border: 1px solid var(--ue-edge);
|
||||
background: var(--ue-panel-bg);
|
||||
backdrop-filter: var(--app-acrylic-blur);
|
||||
-webkit-backdrop-filter: var(--app-acrylic-blur);
|
||||
box-shadow: none;
|
||||
color: var(--ue-ink);
|
||||
}
|
||||
|
||||
.ue-card__title {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
.ue-preview__meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ue-preview__actions {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.ue-preview__action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--ue-edge);
|
||||
border-radius: 12px;
|
||||
background: var(--ue-panel-bg);
|
||||
color: var(--app-primary-color);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
border-color: color-mix(in srgb, var(--app-primary-color) 22%, var(--ue-edge));
|
||||
background: var(--app-acrylic-hover-bg);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.ue-card__hint {
|
||||
.ue-reset-tip {
|
||||
margin: 0 0 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--ue-muted);
|
||||
}
|
||||
|
||||
.ue-preview__avatar {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
border: 1px solid var(--ue-edge);
|
||||
background: color-mix(in srgb, var(--app-primary-color) 12%, #fff);
|
||||
color: var(--app-primary-color);
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.ue-preview__name {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
line-height: 1.3;
|
||||
color: var(--ue-ink);
|
||||
}
|
||||
|
||||
.ue-preview__account {
|
||||
margin: 4px 0 0;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
line-height: 1.4;
|
||||
color: var(--ue-soft);
|
||||
}
|
||||
|
||||
.ue-card__body {
|
||||
padding: 12px 16px 4px;
|
||||
.ue-preview__tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.ue-preview__tag {
|
||||
display: inline-flex;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
border: 1px solid var(--ue-edge);
|
||||
background: color-mix(in srgb, #fff 55%, transparent);
|
||||
color: var(--ue-muted);
|
||||
|
||||
&--accent {
|
||||
border-color: color-mix(in srgb, var(--app-primary-color) 22%, var(--ue-edge));
|
||||
background: color-mix(in srgb, var(--app-primary-color) 10%, #fff);
|
||||
color: var(--app-primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.ue-section {
|
||||
margin-bottom: 14px;
|
||||
padding: 14px 14px 6px;
|
||||
border-radius: var(--ue-radius);
|
||||
border: 1px solid var(--ue-edge);
|
||||
background: var(--ue-panel-bg);
|
||||
backdrop-filter: var(--app-acrylic-blur);
|
||||
-webkit-backdrop-filter: var(--app-acrylic-blur);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.ue-section__head {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.ue-section__index {
|
||||
flex-shrink: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--app-primary-color);
|
||||
border: 1px solid color-mix(in srgb, var(--app-primary-color) 18%, var(--ue-edge));
|
||||
background: color-mix(in srgb, var(--app-primary-color) 10%, #fff);
|
||||
}
|
||||
|
||||
.ue-section__title {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: var(--ue-ink);
|
||||
}
|
||||
|
||||
.ue-section__hint {
|
||||
margin: 2px 0 0;
|
||||
font-size: 12px;
|
||||
color: var(--ue-soft);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.ue-form {
|
||||
@ -623,7 +768,7 @@ defineExpose({
|
||||
:deep(.arco-form-item-label) {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
color: var(--ue-ink);
|
||||
}
|
||||
|
||||
:deep(.arco-input-wrapper),
|
||||
@ -631,9 +776,10 @@ defineExpose({
|
||||
:deep(.arco-select-view-multiple),
|
||||
:deep(.arco-textarea-wrapper),
|
||||
:deep(.arco-input-number),
|
||||
:deep(.arco-picker),
|
||||
:deep(.arco-tree-select-view) {
|
||||
border-radius: 8px;
|
||||
:deep(.arco-picker) {
|
||||
border-radius: 12px;
|
||||
border-color: color-mix(in srgb, var(--ue-edge) 70%, #d0d5dd);
|
||||
background: color-mix(in srgb, #fff 72%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
@ -661,43 +807,43 @@ defineExpose({
|
||||
|
||||
.ue-invite-copy {
|
||||
flex-shrink: 0;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.ue-invite-hint {
|
||||
color: var(--color-text-3);
|
||||
color: var(--ue-soft);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.ue-reset-tip {
|
||||
margin: 0 0 12px;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
|
||||
.ue-drawer__footer {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin: 0 12px 12px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.78);
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(155deg, rgba(255, 255, 255, 0.92) 0%, rgba(255, 255, 255, 0.72) 100%);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.9),
|
||||
0 4px 14px rgba(0, 121, 121, 0.05);
|
||||
padding: 14px 16px 16px;
|
||||
border-top: 1px solid var(--ue-edge);
|
||||
background: var(--ue-panel-bg);
|
||||
backdrop-filter: var(--app-acrylic-blur);
|
||||
-webkit-backdrop-filter: var(--app-acrylic-blur);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.user-edit-drawer {
|
||||
.arco-drawer-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.arco-drawer-body {
|
||||
padding: 0 !important;
|
||||
padding: 16px;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background: var(--app-page-bg, #f0f3f4);
|
||||
}
|
||||
|
||||
.arco-drawer {
|
||||
background: var(--app-page-bg, #f0f3f4);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -102,6 +102,8 @@ export default {
|
||||
'marketing.memberLevel.section.pricingHint': 'Rolling 12-month points; discount on base freight only',
|
||||
'marketing.memberLevel.section.display': 'Display & benefits',
|
||||
'marketing.memberLevel.section.displayHint': 'Portal display only; does not affect pricing',
|
||||
'marketing.memberLevel.preview.nameFallback': 'Untitled level',
|
||||
'marketing.memberLevel.preview.codeFallback': 'Code pending',
|
||||
'menu.arcoWebsite': 'Arco Design',
|
||||
'menu.faq': 'FAQ',
|
||||
'menu.example': 'Example',
|
||||
@ -1368,23 +1370,17 @@ export default {
|
||||
'system.dataPerm.templates.search': 'Search name or code',
|
||||
'system.dataPerm.templates.title': 'Permission templates',
|
||||
'system.dataPerm.templates.loading': 'Loading templates...',
|
||||
'system.dataPerm.templates.emptySelect': 'Select a template on the left, or click New template',
|
||||
'system.dataPerm.templates.emptyDesc':
|
||||
'Flow: ① name & code → ② configure rules → ③ save, then link a role and apply to role/users',
|
||||
'system.dataPerm.templates.save': 'Save template',
|
||||
'system.dataPerm.templates.emptySelect': 'Select a template on the left, or click Add',
|
||||
'system.dataPerm.templates.emptyDesc': 'Templates define five-dimension permission rules and can be applied to users or roles',
|
||||
'system.dataPerm.templates.save': 'Save',
|
||||
'system.dataPerm.templates.apply': 'Apply template',
|
||||
'system.dataPerm.templates.field.name': 'Template name',
|
||||
'system.dataPerm.templates.field.namePlaceholder': 'e.g. Sales staff data scope',
|
||||
'system.dataPerm.templates.field.code': 'Template code',
|
||||
'system.dataPerm.templates.field.codePlaceholder': 'Letters/numbers, e.g. SalesStaff',
|
||||
'system.dataPerm.templates.field.description': 'Purpose',
|
||||
'system.dataPerm.templates.field.descriptionPlaceholder':
|
||||
'For teammates: who this is for and what data they can see',
|
||||
'system.dataPerm.templates.field.name': 'Name',
|
||||
'system.dataPerm.templates.field.code': 'Code',
|
||||
'system.dataPerm.templates.field.description': 'Description',
|
||||
'system.dataPerm.templates.field.roleCode': 'Bound role code',
|
||||
'system.dataPerm.templates.field.bindRole': 'Linked job role',
|
||||
'system.dataPerm.templates.field.bindRolePlaceholder': 'Optional RBAC role tag',
|
||||
'system.dataPerm.templates.field.bindRoleExtra':
|
||||
'Only tags which job this template belongs to — it does not grant access. Use A / B below to apply.',
|
||||
'system.dataPerm.templates.field.bindRole': 'Linked role',
|
||||
'system.dataPerm.templates.field.bindRolePlaceholder': 'Select an RBAC role',
|
||||
'system.dataPerm.templates.field.bindRoleExtra': 'Saved as roleCode — identifies which job role this template belongs to',
|
||||
|
||||
// Order - Batch Jobs
|
||||
'order.batchJobs.filter.jobId': 'Job ID',
|
||||
@ -1457,27 +1453,14 @@ export default {
|
||||
'order.batchJobs.action.importRow': 'Order Import',
|
||||
'order.batchJobs.action.unknown': 'Other',
|
||||
|
||||
'system.dataPerm.templates.section.identity': '① What this template is',
|
||||
'system.dataPerm.templates.section.identityHint':
|
||||
'Name and code help you find it in the list; you can configure rules before saving',
|
||||
'system.dataPerm.templates.section.bindApply': '③ Link role & apply (after save)',
|
||||
'system.dataPerm.templates.section.bindApply': 'Role link & apply',
|
||||
'system.dataPerm.templates.section.applyNow': 'Apply now',
|
||||
'system.dataPerm.templates.bindApplyHint':
|
||||
'Linking a role ≠ granting access. Save and finish rules first, then use A for the role or B for users.',
|
||||
'system.dataPerm.templates.flow.step1': 'Identity',
|
||||
'system.dataPerm.templates.flow.step2': 'Rules',
|
||||
'system.dataPerm.templates.flow.step3': 'Apply',
|
||||
'system.dataPerm.templates.flow.goItemsTitle': 'Next: configure permission rules',
|
||||
'system.dataPerm.templates.flow.goItemsDesc':
|
||||
'Rules define org / data scope / secret level access. Come back here to apply after editing.',
|
||||
'system.dataPerm.templates.flow.goItemsBtn': 'Configure rules',
|
||||
'system.dataPerm.templates.applyToRole': 'A. Sync to linked role',
|
||||
'system.dataPerm.templates.applyToRoleDesc':
|
||||
'Write current rules into the role data permissions (replaces role direct config)',
|
||||
'system.dataPerm.templates.bindApplyHint': 'Linking a role is metadata only. Use "Apply now" below to write template items into role or user permissions. Linking does not apply by itself.',
|
||||
'system.dataPerm.templates.applyToRole': 'Sync to linked role',
|
||||
'system.dataPerm.templates.applyToRoleDesc': 'Write template items into the role\'s data permissions (replaces role direct config)',
|
||||
'system.dataPerm.templates.applyToRoleBtn': 'Write to role',
|
||||
'system.dataPerm.templates.applyToUsers': 'B. Apply to selected users',
|
||||
'system.dataPerm.templates.applyToUsersDesc':
|
||||
'Batch write user direct permissions — for exceptions or temporary grants; role itself unchanged',
|
||||
'system.dataPerm.templates.applyToUsers': 'Apply to users',
|
||||
'system.dataPerm.templates.applyToUsersDesc': 'Batch write direct user data permissions; replace or merge',
|
||||
'system.dataPerm.templates.applyToUsersBtn': 'Apply to users',
|
||||
'system.dataPerm.templates.field.targetUsers': 'Target users',
|
||||
'system.dataPerm.templates.field.targetUsersPlaceholder': 'Search and select internal users',
|
||||
@ -1485,24 +1468,18 @@ export default {
|
||||
'system.dataPerm.templates.field.temporary': 'Temporary grant (users only)',
|
||||
'system.dataPerm.templates.field.effectiveTime': 'Effective from',
|
||||
'system.dataPerm.templates.field.expireTime': 'Expires at',
|
||||
'system.dataPerm.templates.mode.replace': 'Replace direct config (usual)',
|
||||
'system.dataPerm.templates.mode.replace': 'Replace direct config',
|
||||
'system.dataPerm.templates.mode.merge': 'Merge with existing',
|
||||
'system.dataPerm.templates.applyNeedSave':
|
||||
'Click Save template first — then you can link a role and apply',
|
||||
'system.dataPerm.templates.applyNeedSave': 'Save the template before applying',
|
||||
'system.dataPerm.templates.bindRoleFirst': 'Select a linked role first',
|
||||
'system.dataPerm.templates.selectUsersFirst': 'Select target users',
|
||||
'system.dataPerm.templates.expireTimeRequired': 'Expire time is required for temporary grants',
|
||||
'system.dataPerm.templates.applySuccess': 'Applied to {count} target(s)',
|
||||
'system.dataPerm.templates.applyPartial': '{success} succeeded, {fail} failed',
|
||||
'system.dataPerm.templates.roleCodePlaceholder': 'Optional, auto-apply when role is assigned',
|
||||
'system.dataPerm.templates.itemsTitle': 'Template items',
|
||||
'system.dataPerm.templates.itemsGuideTitle': '② How to configure rules',
|
||||
'system.dataPerm.templates.itemsHintForm':
|
||||
'Each rule picks a dimension (org / data scope / biz tag / secret / filter). Fill the form — no JSON. Use presets for common job roles.',
|
||||
'system.dataPerm.templates.itemsEmptyHint':
|
||||
'Unsure? Start from the “Sales staff” preset, then tweak for your job role',
|
||||
'system.dataPerm.templates.itemIndex': 'Rule {index}',
|
||||
'system.dataPerm.templates.addExample': 'Generate from preset',
|
||||
'system.dataPerm.templates.itemsTitle': '模板明细',
|
||||
'system.dataPerm.templates.itemsHintForm': 'Select a permission type and fill the form — no JSON required',
|
||||
'system.dataPerm.templates.addExample': 'Add example preset',
|
||||
'system.dataPerm.templates.exampleAdded': 'Added example "{name}"',
|
||||
'system.dataPerm.templates.itemInvalid': 'Item #{index} is incomplete: {field}',
|
||||
'system.dataPerm.templates.example.salesStaff': 'Sales staff',
|
||||
@ -1541,7 +1518,7 @@ export default {
|
||||
'system.dataPerm.templates.form.invalid.filterResource': 'Please select a module',
|
||||
'system.dataPerm.templates.form.invalid.filterCondition': 'Add at least one filter condition',
|
||||
'system.dataPerm.templates.itemJsonPlaceholder': 'JSON, e.g. {"dataScope":1}',
|
||||
'system.dataPerm.templates.addItem': 'Add a rule',
|
||||
'system.dataPerm.templates.addItem': 'Add item row',
|
||||
'system.dataPerm.templates.removeItem': 'Remove',
|
||||
'system.dataPerm.templates.nameCodeRequired': 'Name and code are required',
|
||||
'system.dataPerm.templates.invalidJson': 'Invalid JSON in template items',
|
||||
@ -1550,15 +1527,15 @@ export default {
|
||||
'system.dataPerm.templates.deleteSuccess': 'Deleted',
|
||||
'system.dataPerm.templates.builtinDeleteDenied': 'Built-in templates cannot be deleted',
|
||||
'system.dataPerm.templates.deleteConfirm': 'Delete template "{name}"?',
|
||||
'system.dataPerm.templates.tab.basic': 'Basics',
|
||||
'system.dataPerm.templates.tab.items': 'Rules',
|
||||
'system.dataPerm.templates.tab.basic': 'Basic info',
|
||||
'system.dataPerm.templates.tab.items': 'Template items',
|
||||
'system.dataPerm.templates.itemsHint': 'Each row is a JSON config for one permission dimension',
|
||||
'system.dataPerm.templates.itemsEmpty': 'No permission rules yet',
|
||||
'system.dataPerm.templates.itemsEmpty': 'No items yet. Click below to add one.',
|
||||
'system.dataPerm.templates.jsonPreset': 'JSON preset',
|
||||
'system.dataPerm.templates.dimensionPlaceholder': 'Select dimension',
|
||||
'system.dataPerm.templates.tag.creating': 'Creating',
|
||||
'system.dataPerm.templates.tag.builtin': 'Built-in',
|
||||
'system.dataPerm.templates.stat.items': 'Rules',
|
||||
'system.dataPerm.templates.stat.items': 'Items',
|
||||
'system.dataPerm.templates.stat.dimensions': 'Dimensions',
|
||||
|
||||
'menu.xhs': 'Xiaohongshu',
|
||||
@ -2251,7 +2228,6 @@ export default {
|
||||
'order.myOrders.drawer.declaration': 'Declaration',
|
||||
'order.myOrders.drawer.senderName': 'Sender Name',
|
||||
'order.myOrders.drawer.company': 'Company',
|
||||
'order.myOrders.drawer.country': 'Country',
|
||||
'order.myOrders.drawer.destCountry': 'Country',
|
||||
'order.myOrders.drawer.province': 'Province',
|
||||
'order.myOrders.drawer.city': 'City',
|
||||
@ -5090,7 +5066,6 @@ export default {
|
||||
'channel.salesProducts.publish.legend.same': 'Unchanged',
|
||||
'channel.salesProducts.publish.badge.pending': 'Pending',
|
||||
'channel.salesProducts.publish.badge.synced': 'Synced',
|
||||
'channel.salesProducts.publish.badge.checking': 'Checking…',
|
||||
|
||||
'channel.salesProducts.rules.schemeTitle': 'Shipping scheme',
|
||||
'channel.salesProducts.rules.scheme.costLowest': 'Lowest cost',
|
||||
@ -5500,7 +5475,6 @@ export default {
|
||||
'channel.serviceChannels.publish.legend.remove': 'Removed',
|
||||
'channel.serviceChannels.publish.legend.change': 'Changed',
|
||||
'channel.serviceChannels.publish.legend.same': 'Unchanged',
|
||||
'channel.serviceChannels.publish.badge.checking': 'Checking…',
|
||||
'channel.serviceChannels.publish.badge.pending': 'Pending',
|
||||
'channel.serviceChannels.publish.badge.synced': 'Synced',
|
||||
|
||||
@ -5515,21 +5489,6 @@ export default {
|
||||
'channel.area.actions.delete': 'Delete',
|
||||
'channel.area.actions.import': 'Import',
|
||||
'channel.area.actions.export': 'Export',
|
||||
'channel.area.actions.packVersions': 'Pack versions',
|
||||
'channel.area.pack.modalTitle': 'Area packs — {name}',
|
||||
'channel.area.pack.tip': 'Packs are pinned on sales price / rules publish. Billing loads by version; missing packs fail hard.',
|
||||
'channel.area.pack.empty': 'No packs yet. Publish sales price or rules to create one.',
|
||||
'channel.area.pack.emptyContent': '(empty)',
|
||||
'channel.area.pack.viewContent': 'View',
|
||||
'channel.area.pack.detailTitle': 'Pack content — {version}',
|
||||
'channel.area.pack.loadFailed': 'Failed to load pack list',
|
||||
'channel.area.pack.loadDetailFailed': 'Failed to load pack content',
|
||||
'channel.area.pack.col.version': 'Version',
|
||||
'channel.area.pack.col.status': 'Status',
|
||||
'channel.area.pack.col.contentHash': 'Content hash',
|
||||
'channel.area.pack.col.publishUser': 'Publisher',
|
||||
'channel.area.pack.col.publishTime': 'Published at',
|
||||
'channel.area.pack.col.description': 'Description',
|
||||
'channel.area.pagination.total': 'Total {count} records',
|
||||
'channel.area.pagination.prev': 'Previous',
|
||||
'channel.area.pagination.next': 'Next',
|
||||
@ -5626,16 +5585,7 @@ export default {
|
||||
'channel.area.message.deleteFailed': 'Delete failed. Please try again later.',
|
||||
'channel.area.import.modalTitle': 'Import area details',
|
||||
'channel.area.import.tip':
|
||||
'Download the template first. Country code is required (country name is filled from the code). Put match dimension/mode codes from the Enum sheet. One row, one dimension: postcode rules if filled; state only = state match; state+city = city match; empty = country fallback.',
|
||||
'channel.area.import.modeLabel': 'Import mode',
|
||||
'channel.area.import.mode.incremental': 'Incremental',
|
||||
'channel.area.import.mode.full': 'Full replace',
|
||||
'channel.area.import.mode.incrementalTip':
|
||||
'Keep existing details; exact duplicate rules in this area are skipped, not overwritten.',
|
||||
'channel.area.import.mode.fullTip':
|
||||
'Clear all details in this area first, then import the Excel rows (irreversible — export a backup first).',
|
||||
'channel.area.import.mode.fullConfirm':
|
||||
'Full import will delete all details in this area, then write the Excel data. Continue?',
|
||||
'Download the template first. Import is incremental: exact duplicate rules in this area are skipped, not overwritten. Country code is required (country name is filled from the code). Put match dimension/mode codes from the Enum sheet. One row, one dimension: postcode rules if filled; state only = state match; state+city = city match; empty = country fallback.',
|
||||
'channel.area.import.downloadTemplate': 'Download template',
|
||||
'channel.area.import.submit': 'Import',
|
||||
'channel.area.import.downloadSuccess': 'Template downloaded',
|
||||
|
||||
@ -116,11 +116,8 @@ export default {
|
||||
'marketing.couponTemplate.grantChannel.route': 'Route',
|
||||
'marketing.couponTemplate.grantChannel.festival': 'Festival',
|
||||
'marketing.couponTemplate.validType.fixed': 'Fixed dates',
|
||||
'marketing.couponTemplate.validType.fixedDesc': 'Valid between start and end time',
|
||||
'marketing.couponTemplate.validType.afterReceive': 'N days after claim',
|
||||
'marketing.couponTemplate.validType.afterReceiveDesc': 'Countdown starts on claim day',
|
||||
'marketing.couponTemplate.validType.afterUnlock': 'N days after unlock',
|
||||
'marketing.couponTemplate.validType.afterUnlockDesc': 'Countdown starts on unlock day',
|
||||
'marketing.couponTemplate.actions.publish': 'Publish',
|
||||
'marketing.couponTemplate.actions.end': 'End',
|
||||
'marketing.couponTemplate.publishConfirmTitle': 'Publish template',
|
||||
|
||||
@ -116,11 +116,8 @@ export default {
|
||||
'marketing.couponTemplate.grantChannel.route': '线路定向券',
|
||||
'marketing.couponTemplate.grantChannel.festival': '节日券',
|
||||
'marketing.couponTemplate.validType.fixed': '固定日期',
|
||||
'marketing.couponTemplate.validType.fixedDesc': '指定起止时间生效',
|
||||
'marketing.couponTemplate.validType.afterReceive': '领取后 N 天',
|
||||
'marketing.couponTemplate.validType.afterReceiveDesc': '领券当日起算有效天数',
|
||||
'marketing.couponTemplate.validType.afterUnlock': '解锁后 N 天',
|
||||
'marketing.couponTemplate.validType.afterUnlockDesc': '解锁当日起算有效天数',
|
||||
'marketing.couponTemplate.actions.publish': '发布',
|
||||
'marketing.couponTemplate.actions.end': '停发',
|
||||
'marketing.couponTemplate.publishConfirmTitle': '确认发布券模板',
|
||||
|
||||
@ -103,6 +103,8 @@ export default {
|
||||
'marketing.memberLevel.section.pricingHint': '滚动 12 个月积分定级;折扣只打基础运费',
|
||||
'marketing.memberLevel.section.display': '展示与权益',
|
||||
'marketing.memberLevel.section.displayHint': '门户展示用,不影响计价逻辑',
|
||||
'marketing.memberLevel.preview.nameFallback': '未命名等级',
|
||||
'marketing.memberLevel.preview.codeFallback': '编码待填写',
|
||||
'menu.arcoWebsite': 'Arco Design',
|
||||
'menu.faq': '常见问题',
|
||||
'menu.example': '示例页',
|
||||
@ -2112,7 +2114,6 @@ export default {
|
||||
'order.myOrders.drawer.declaration': '申报信息',
|
||||
'order.myOrders.drawer.senderName': '发件人姓名',
|
||||
'order.myOrders.drawer.company': '公司',
|
||||
'order.myOrders.drawer.country': '国家',
|
||||
'order.myOrders.drawer.destCountry': '国家',
|
||||
'order.myOrders.drawer.province': '州省',
|
||||
'order.myOrders.drawer.city': '城市',
|
||||
@ -3558,39 +3559,25 @@ export default {
|
||||
'system.dataPerm.templates.search': '搜索模板名称/编码',
|
||||
'system.dataPerm.templates.title': '权限模板',
|
||||
'system.dataPerm.templates.loading': '正在加载模板列表...',
|
||||
'system.dataPerm.templates.emptySelect': '请从左侧选择模板,或点击「新建模板」',
|
||||
'system.dataPerm.templates.emptyDesc':
|
||||
'配置顺序:① 填写名称编码 → ② 配置权限规则 → ③ 保存后关联角色并套用到岗位/用户',
|
||||
'system.dataPerm.templates.save': '保存模板',
|
||||
'system.dataPerm.templates.emptySelect': '请从左侧选择模板,或点击新建',
|
||||
'system.dataPerm.templates.emptyDesc': '权限模板可批量定义五维权限规则,并一键套用到用户或角色',
|
||||
'system.dataPerm.templates.save': '保存',
|
||||
'system.dataPerm.templates.apply': '套用模板',
|
||||
'system.dataPerm.templates.field.name': '模板名称',
|
||||
'system.dataPerm.templates.field.namePlaceholder': '如:普通销售数据权限',
|
||||
'system.dataPerm.templates.field.code': '模板编码',
|
||||
'system.dataPerm.templates.field.codePlaceholder': '英文/数字,如 SalesStaff',
|
||||
'system.dataPerm.templates.field.description': '用途说明',
|
||||
'system.dataPerm.templates.field.descriptionPlaceholder': '写给同事看:这份模板给谁用、能看什么数据',
|
||||
'system.dataPerm.templates.field.name': '名称',
|
||||
'system.dataPerm.templates.field.code': '编码',
|
||||
'system.dataPerm.templates.field.description': '说明',
|
||||
'system.dataPerm.templates.field.roleCode': '绑定角色 Code',
|
||||
'system.dataPerm.templates.field.bindRole': '关联岗位角色',
|
||||
'system.dataPerm.templates.field.bindRolePlaceholder': '选择要打标的 RBAC 角色(可空)',
|
||||
'system.dataPerm.templates.field.bindRoleExtra':
|
||||
'只是给模板打上「属于哪个岗位」的标签,不会自动改权限;真正生效请用下方 A / B 操作',
|
||||
'system.dataPerm.templates.section.identity': '① 模板是什么',
|
||||
'system.dataPerm.templates.section.identityHint': '先起好名称和编码,方便在左侧列表里辨认;保存前可先去配置规则',
|
||||
'system.dataPerm.templates.section.bindApply': '③ 关联角色并套用(保存后才能生效)',
|
||||
'system.dataPerm.templates.field.bindRole': '关联角色',
|
||||
'system.dataPerm.templates.field.bindRolePlaceholder': '选择要关联的 RBAC 角色',
|
||||
'system.dataPerm.templates.field.bindRoleExtra': '保存后写入 roleCode,用于标识「哪个岗位对应这份模板」',
|
||||
'system.dataPerm.templates.section.bindApply': '关联角色与套用',
|
||||
'system.dataPerm.templates.section.applyNow': '立即生效',
|
||||
'system.dataPerm.templates.bindApplyHint':
|
||||
'关联角色 ≠ 已授权。请先保存模板并配好「权限规则」,再选 A 写入角色,或选 B 套用到具体用户。',
|
||||
'system.dataPerm.templates.flow.step1': '填身份',
|
||||
'system.dataPerm.templates.flow.step2': '配规则',
|
||||
'system.dataPerm.templates.flow.step3': '套用生效',
|
||||
'system.dataPerm.templates.flow.goItemsTitle': '下一步:配置权限规则',
|
||||
'system.dataPerm.templates.flow.goItemsDesc': '规则决定「能看哪些组织 / 数据范围 / 密级」等,配完再回来套用',
|
||||
'system.dataPerm.templates.flow.goItemsBtn': '去配置规则',
|
||||
'system.dataPerm.templates.applyToRole': 'A. 同步到关联角色',
|
||||
'system.dataPerm.templates.applyToRoleDesc': '把当前规则写入该角色的数据权限(覆盖角色上原有直配)',
|
||||
'system.dataPerm.templates.bindApplyHint': '「关联角色」是模板元数据,标明岗位归属;下方「立即生效」才会把明细写入角色或用户的权限配置。两者不重复:关联不等于已套用。',
|
||||
'system.dataPerm.templates.applyToRole': '同步到关联角色',
|
||||
'system.dataPerm.templates.applyToRoleDesc': '将本模板明细写入该角色的数据权限(覆盖角色直配)',
|
||||
'system.dataPerm.templates.applyToRoleBtn': '写入角色权限',
|
||||
'system.dataPerm.templates.applyToUsers': 'B. 套用到指定用户',
|
||||
'system.dataPerm.templates.applyToUsersDesc': '批量写入用户直配权限;适合特例或临时授权,不影响角色本身',
|
||||
'system.dataPerm.templates.applyToUsers': '套用到指定用户',
|
||||
'system.dataPerm.templates.applyToUsersDesc': '批量写入用户的直配数据权限,可选择覆盖或合并',
|
||||
'system.dataPerm.templates.applyToUsersBtn': '套用到用户',
|
||||
'system.dataPerm.templates.field.targetUsers': '目标用户',
|
||||
'system.dataPerm.templates.field.targetUsersPlaceholder': '搜索并选择内部用户',
|
||||
@ -3598,9 +3585,9 @@ export default {
|
||||
'system.dataPerm.templates.field.temporary': '临时授权(仅对用户生效)',
|
||||
'system.dataPerm.templates.field.effectiveTime': '生效时间',
|
||||
'system.dataPerm.templates.field.expireTime': '过期时间',
|
||||
'system.dataPerm.templates.mode.replace': '覆盖直配(常用)',
|
||||
'system.dataPerm.templates.mode.replace': '覆盖直配',
|
||||
'system.dataPerm.templates.mode.merge': '与现有合并',
|
||||
'system.dataPerm.templates.applyNeedSave': '新建模板请先点右上角「保存模板」,保存后才能关联角色并套用',
|
||||
'system.dataPerm.templates.applyNeedSave': '请先保存模板,再进行套用操作',
|
||||
'system.dataPerm.templates.bindRoleFirst': '请先选择关联角色',
|
||||
'system.dataPerm.templates.selectUsersFirst': '请选择目标用户',
|
||||
'system.dataPerm.templates.expireTimeRequired': '临时授权需填写过期时间',
|
||||
@ -3609,8 +3596,8 @@ export default {
|
||||
'system.dataPerm.templates.roleCodePlaceholder': '可选,分配角色时自动套用',
|
||||
'system.dataPerm.templates.itemsTitle': '模板明细',
|
||||
'system.dataPerm.templates.itemJsonPlaceholder': 'JSON,如 {"dataScope":1}',
|
||||
'system.dataPerm.templates.addItem': '添加一条规则',
|
||||
'system.dataPerm.templates.removeItem': '删除',
|
||||
'system.dataPerm.templates.addItem': '添加明细行',
|
||||
'system.dataPerm.templates.removeItem': '删',
|
||||
'system.dataPerm.templates.nameCodeRequired': '请填写名称和编码',
|
||||
'system.dataPerm.templates.invalidJson': '明细 JSON 格式不正确',
|
||||
'system.dataPerm.templates.createSuccess': '创建成功',
|
||||
@ -3619,14 +3606,10 @@ export default {
|
||||
'system.dataPerm.templates.builtinDeleteDenied': '内置模板不可删除',
|
||||
'system.dataPerm.templates.deleteConfirm': '确定删除模板「{name}」?',
|
||||
'system.dataPerm.templates.tab.basic': '基本信息',
|
||||
'system.dataPerm.templates.tab.items': '权限规则',
|
||||
'system.dataPerm.templates.itemsGuideTitle': '② 权限规则怎么配',
|
||||
'system.dataPerm.templates.tab.items': '模板明细',
|
||||
'system.dataPerm.templates.itemsHint': '每条明细对应一个维度的权限 JSON 配置',
|
||||
'system.dataPerm.templates.itemsHintForm':
|
||||
'每条规则选一种维度(组织 / 数据范围 / 业务标签 / 密级 / 过滤),填表即可,不用写 JSON。可先用「示例配置」一键生成常用岗位。',
|
||||
'system.dataPerm.templates.itemsEmptyHint': '不会配时可先点「普通销售」示例,再按实际岗位微调',
|
||||
'system.dataPerm.templates.itemIndex': '规则 {index}',
|
||||
'system.dataPerm.templates.addExample': '用示例快速生成',
|
||||
'system.dataPerm.templates.itemsHintForm': '选择权限类型后填写表单即可,无需手写 JSON',
|
||||
'system.dataPerm.templates.addExample': '添加示例配置',
|
||||
'system.dataPerm.templates.exampleAdded': '已添加示例「{name}」',
|
||||
'system.dataPerm.templates.itemInvalid': '第 {index} 条明细未填写完整:{field}',
|
||||
'system.dataPerm.templates.example.salesStaff': '普通销售',
|
||||
@ -3664,12 +3647,12 @@ export default {
|
||||
'system.dataPerm.templates.form.invalid.secretLevels': '请至少勾选一个密级',
|
||||
'system.dataPerm.templates.form.invalid.filterResource': '请选择适用模块',
|
||||
'system.dataPerm.templates.form.invalid.filterCondition': '请至少填写一条过滤条件',
|
||||
'system.dataPerm.templates.itemsEmpty': '还没有权限规则',
|
||||
'system.dataPerm.templates.itemsEmpty': '暂无明细,点击下方按钮添加',
|
||||
'system.dataPerm.templates.jsonPreset': 'JSON 预设',
|
||||
'system.dataPerm.templates.dimensionPlaceholder': '选择权限维度',
|
||||
'system.dataPerm.templates.dimensionPlaceholder': '选择维度',
|
||||
'system.dataPerm.templates.tag.creating': '新建中',
|
||||
'system.dataPerm.templates.tag.builtin': '内置',
|
||||
'system.dataPerm.templates.stat.items': '规则数',
|
||||
'system.dataPerm.templates.stat.items': '明细数',
|
||||
'system.dataPerm.templates.stat.dimensions': '维度数',
|
||||
'system.dict.import.modalTitle': '批量导入字典',
|
||||
'system.dict.import.tip':
|
||||
@ -5119,7 +5102,6 @@ export default {
|
||||
'channel.salesProducts.publish.legend.same': '不变',
|
||||
'channel.salesProducts.publish.badge.pending': '待发布',
|
||||
'channel.salesProducts.publish.badge.synced': '已同步',
|
||||
'channel.salesProducts.publish.badge.checking': '检测中…',
|
||||
|
||||
'channel.salesProducts.rules.schemeTitle': '排货方案',
|
||||
'channel.salesProducts.rules.scheme.costLowest': '成本最低',
|
||||
@ -5529,7 +5511,6 @@ export default {
|
||||
'channel.serviceChannels.publish.legend.remove': '删除',
|
||||
'channel.serviceChannels.publish.legend.change': '修改',
|
||||
'channel.serviceChannels.publish.legend.same': '不变',
|
||||
'channel.serviceChannels.publish.badge.checking': '检测中…',
|
||||
'channel.serviceChannels.publish.badge.pending': '待发布',
|
||||
'channel.serviceChannels.publish.badge.synced': '已同步',
|
||||
|
||||
@ -5615,13 +5596,7 @@ export default {
|
||||
'channel.area.search.button.query': '查询',
|
||||
'channel.area.search.button.reset': '重置',
|
||||
'channel.area.import.modalTitle': '导入分区明细',
|
||||
'channel.area.import.tip': '请先下载模板。国家代码必填(无需填国家名称,导入时按代码自动带出)。匹配维度、匹配方式请填 Sheet「枚举值」中的编码。一行一个维度:填邮编则为邮编规则(只填起始=前缀;起止相同=精确;起止不同=闭区间);不填邮编只填州=州匹配;州+城市=城市匹配;都空=仅国家兜底。',
|
||||
'channel.area.import.modeLabel': '导入方式',
|
||||
'channel.area.import.mode.incremental': '增量导入',
|
||||
'channel.area.import.mode.full': '全量导入',
|
||||
'channel.area.import.mode.incrementalTip': '保留本分区已有明细;与已有规则完全相同的行会跳过,不会覆盖。',
|
||||
'channel.area.import.mode.fullTip': '先清空本分区全部明细,再按 Excel 全量写入(不可恢复,请先导出备份)。',
|
||||
'channel.area.import.mode.fullConfirm': '全量导入将先删除本分区全部明细,再写入 Excel 数据,确认继续?',
|
||||
'channel.area.import.tip': '请先下载模板。导入为增量写入:与本分区已有规则完全相同的行会跳过,不会覆盖。国家代码必填(无需填国家名称,导入时按代码自动带出)。匹配维度、匹配方式请填 Sheet「枚举值」中的编码。一行一个维度:填邮编则为邮编规则(只填起始=前缀;起止相同=精确;起止不同=闭区间);不填邮编只填州=州匹配;州+城市=城市匹配;都空=仅国家兜底。',
|
||||
'channel.area.import.downloadTemplate': '下载模板',
|
||||
'channel.area.import.submit': '开始导入',
|
||||
'channel.area.import.downloadSuccess': '模板下载成功',
|
||||
@ -5638,21 +5613,6 @@ export default {
|
||||
'channel.area.export.failed': '导出失败',
|
||||
'channel.area.actions.import': '导入',
|
||||
'channel.area.actions.export': '导出',
|
||||
'channel.area.actions.packVersions': '分区包版本',
|
||||
'channel.area.pack.modalTitle': '分区包版本 — {name}',
|
||||
'channel.area.pack.tip': '卖价/规则发布时自动钉包;计费按版本加载,包缺失将严格失败。',
|
||||
'channel.area.pack.empty': '暂无分区包,发布卖价或规则后生成',
|
||||
'channel.area.pack.emptyContent': '(无正文)',
|
||||
'channel.area.pack.viewContent': '查看',
|
||||
'channel.area.pack.detailTitle': '分区包正文 — {version}',
|
||||
'channel.area.pack.loadFailed': '加载分区包列表失败',
|
||||
'channel.area.pack.loadDetailFailed': '加载分区包正文失败',
|
||||
'channel.area.pack.col.version': '版本号',
|
||||
'channel.area.pack.col.status': '状态',
|
||||
'channel.area.pack.col.contentHash': '内容哈希',
|
||||
'channel.area.pack.col.publishUser': '发布人',
|
||||
'channel.area.pack.col.publishTime': '发布时间',
|
||||
'channel.area.pack.col.description': '说明',
|
||||
|
||||
// 渠道配置页通用
|
||||
'channel.config.emptySelect': '请先在左侧选择配置项',
|
||||
|
||||
@ -1,47 +0,0 @@
|
||||
/**
|
||||
* 计费因素「因素值」行内标签:按字符宽度估算可展示数量,避免固定 3 个浪费空间。
|
||||
* 与 .factor-value-tag max-width:148px / small closable 样式对齐。
|
||||
*/
|
||||
|
||||
const TAG_MAX_WIDTH = 148
|
||||
const TAG_CHROME_WIDTH = 30 // 内边距 + 关闭按钮
|
||||
const TAG_GAP = 4
|
||||
const ADD_BTN_WIDTH = 32
|
||||
const MORE_BTN_WIDTH = 44
|
||||
/** 序号 + 因素 + 操作列及沟槽,用于无实测 cell 时估算因素值列宽 */
|
||||
const OTHER_COLUMNS_WIDTH = 52 + 140 + 64 + 24
|
||||
|
||||
export function estimateFactorValueTagWidth(label: string): number {
|
||||
let text = 0
|
||||
for (const ch of [...String(label ?? '')]) {
|
||||
text += /[\u4e00-\u9fff\u3000-\u303f\uff00-\uffef]/.test(ch) ? 12 : 7
|
||||
}
|
||||
return Math.min(TAG_MAX_WIDTH, Math.max(36, Math.ceil(text) + TAG_CHROME_WIDTH))
|
||||
}
|
||||
|
||||
export function estimateFactorValuesColumnWidth(tableWrapWidth: number): number {
|
||||
return Math.max(200, Math.floor(Number(tableWrapWidth) || 0) - OTHER_COLUMNS_WIDTH)
|
||||
}
|
||||
|
||||
/** 在 availableWidth 内最多可展示几个标签(含关闭钮);装不下时预留 +N */
|
||||
export function resolveVisibleFactorValueCount(
|
||||
labels: string[],
|
||||
availableWidth: number,
|
||||
): number {
|
||||
if (!labels.length) return 0
|
||||
|
||||
const widths = labels.map((label) => estimateFactorValueTagWidth(label))
|
||||
const sumWidth = (n: number) =>
|
||||
widths.slice(0, n).reduce((acc, w, i) => acc + w + (i > 0 ? TAG_GAP : 0), 0)
|
||||
|
||||
const fullBudget = Math.max(60, availableWidth - ADD_BTN_WIDTH)
|
||||
if (sumWidth(labels.length) <= fullBudget) return labels.length
|
||||
|
||||
const withMoreBudget = Math.max(60, availableWidth - ADD_BTN_WIDTH - MORE_BTN_WIDTH)
|
||||
let best = 1
|
||||
for (let n = 1; n < labels.length; n++) {
|
||||
if (sumWidth(n) <= withMoreBudget) best = n
|
||||
else break
|
||||
}
|
||||
return best
|
||||
}
|
||||
@ -1,156 +0,0 @@
|
||||
/**
|
||||
* 会员等级内置图标(V0~V5),与滚动积分体系对齐。
|
||||
* iconUrl 有值时优先用远程图;否则按 levelCode / 名称关键词回落本套图标。
|
||||
*/
|
||||
|
||||
export type MemberLevelKey = 'V0' | 'V1' | 'V2' | 'V3' | 'V4' | 'V5'
|
||||
|
||||
export interface MemberLevelTone {
|
||||
key: MemberLevelKey
|
||||
color: string
|
||||
bg: string
|
||||
border: string
|
||||
labelZh: string
|
||||
}
|
||||
|
||||
const TONES: Record<MemberLevelKey, MemberLevelTone> = {
|
||||
V0: {
|
||||
key: 'V0',
|
||||
color: '#64748b',
|
||||
bg: 'color-mix(in srgb, #64748b 12%, #fff)',
|
||||
border: 'color-mix(in srgb, #64748b 28%, #e5e6eb)',
|
||||
labelZh: '普通会员',
|
||||
},
|
||||
V1: {
|
||||
key: 'V1',
|
||||
color: '#b87333',
|
||||
bg: 'color-mix(in srgb, #b87333 14%, #fff)',
|
||||
border: 'color-mix(in srgb, #b87333 32%, #e5e6eb)',
|
||||
labelZh: '铜卡会员',
|
||||
},
|
||||
V2: {
|
||||
key: 'V2',
|
||||
color: '#6b7c8f',
|
||||
bg: 'color-mix(in srgb, #6b7c8f 12%, #fff)',
|
||||
border: 'color-mix(in srgb, #6b7c8f 30%, #e5e6eb)',
|
||||
labelZh: '银卡会员',
|
||||
},
|
||||
V3: {
|
||||
key: 'V3',
|
||||
color: '#c9a227',
|
||||
bg: 'color-mix(in srgb, #c9a227 16%, #fff)',
|
||||
border: 'color-mix(in srgb, #c9a227 36%, #e5e6eb)',
|
||||
labelZh: '金卡会员',
|
||||
},
|
||||
V4: {
|
||||
key: 'V4',
|
||||
color: '#3d7ec9',
|
||||
bg: 'color-mix(in srgb, #3d7ec9 14%, #fff)',
|
||||
border: 'color-mix(in srgb, #3d7ec9 34%, #e5e6eb)',
|
||||
labelZh: '钻石会员',
|
||||
},
|
||||
V5: {
|
||||
key: 'V5',
|
||||
color: '#a67c2d',
|
||||
bg: 'color-mix(in srgb, #a67c2d 16%, #fff)',
|
||||
border: 'color-mix(in srgb, #a67c2d 38%, #e5e6eb)',
|
||||
labelZh: '皇冠会员',
|
||||
},
|
||||
}
|
||||
|
||||
/** 简洁几何图标:普通 / 铜 / 银 / 金 / 钻石 / 皇冠 */
|
||||
const GLYPHS: Record<MemberLevelKey, string> = {
|
||||
V0: `<circle cx="12" cy="12" r="7.2" fill="none" stroke="currentColor" stroke-width="1.8"/><circle cx="12" cy="12" r="2.2" fill="currentColor"/>`,
|
||||
V1: `<path d="M12 3.2 18.4 6.4v5.2c0 4.1-2.7 7.4-6.4 8.8-3.7-1.4-6.4-4.7-6.4-8.8V6.4L12 3.2z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/><path d="M9.2 12.2h5.6M12 9.4v5.6" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/>`,
|
||||
V2: `<path d="M12 3.4 18.2 6.6v5c0 3.9-2.6 7.1-6.2 8.5-3.6-1.4-6.2-4.6-6.2-8.5v-5L12 3.4z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/><path d="M9.4 12.6 12 10.2l2.6 2.4-1 3.2H10.4l-1-3.2z" fill="currentColor"/>`,
|
||||
V3: `<path d="M12 3.2 14.2 8.4l5.6.5-4.2 3.7 1.3 5.4L12 15.4 7.1 18l1.3-5.4-4.2-3.7 5.6-.5L12 3.2z" fill="currentColor"/>`,
|
||||
V4: `<path d="M12 3.6 19.2 12 12 20.4 4.8 12 12 3.6z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/><path d="M12 7.4 15.8 12 12 16.6 8.2 12 12 7.4z" fill="currentColor"/>`,
|
||||
V5: `<path d="M4.6 9.2 7.8 16.4h8.4l3.2-7.2-3.6 2-2.4-4.4L12 10l-1.4-3.4-2.4 4.4-3.6-2z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linejoin="round"/><path d="M7.8 16.4h8.4V18H7.8z" fill="currentColor"/>`,
|
||||
}
|
||||
|
||||
export function resolveMemberLevelKey(
|
||||
code?: string | null,
|
||||
name?: string | null,
|
||||
): MemberLevelKey | null {
|
||||
const c = String(code || '')
|
||||
.trim()
|
||||
.toUpperCase()
|
||||
if (/^V[0-5]$/.test(c)) return c as MemberLevelKey
|
||||
if (c === 'NORMAL' || c === 'DEFAULT') return 'V0'
|
||||
if (c === 'COPPER' || c === 'BRONZE') return 'V1'
|
||||
if (c === 'SILVER') return 'V2'
|
||||
if (c === 'GOLD') return 'V3'
|
||||
if (c === 'DIAMOND') return 'V4'
|
||||
if (c === 'CROWN' || c === 'KING') return 'V5'
|
||||
|
||||
const n = String(name || '')
|
||||
if (!n) return null
|
||||
if (/皇冠|crown/i.test(n)) return 'V5'
|
||||
if (/钻石|diamond/i.test(n)) return 'V4'
|
||||
if (/金卡|黄金|gold/i.test(n)) return 'V3'
|
||||
if (/银卡|白银|silver/i.test(n)) return 'V2'
|
||||
if (/铜卡|青铜|copper|bronze/i.test(n)) return 'V1'
|
||||
if (/普通|标准|normal|basic/i.test(n)) return 'V0'
|
||||
return null
|
||||
}
|
||||
|
||||
export function getMemberLevelTone(
|
||||
code?: string | null,
|
||||
name?: string | null,
|
||||
): MemberLevelTone | null {
|
||||
const key = resolveMemberLevelKey(code, name)
|
||||
return key ? TONES[key] : null
|
||||
}
|
||||
|
||||
export function renderMemberLevelGlyphSvg(
|
||||
key: MemberLevelKey,
|
||||
size = 16,
|
||||
): string {
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="${size}" height="${size}" fill="none" aria-hidden="true">${GLYPHS[key]}</svg>`
|
||||
}
|
||||
|
||||
/** Handsontable 等需要 HTML 字符串的场景 */
|
||||
export function renderMemberLevelCellHtml(opts: {
|
||||
levelCode?: string | null
|
||||
levelName?: string | null
|
||||
iconUrl?: string | null
|
||||
size?: number
|
||||
}): string {
|
||||
const name = String(opts.levelName || '').trim() || '—'
|
||||
const escaped = name
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
const tone = getMemberLevelTone(opts.levelCode, opts.levelName)
|
||||
const size = opts.size ?? 16
|
||||
const iconUrl = String(opts.iconUrl || '').trim()
|
||||
let glyph = ''
|
||||
if (iconUrl) {
|
||||
const safeUrl = iconUrl.replace(/"/g, '')
|
||||
glyph = `<img class="ml-level-glyph" src="${safeUrl}" alt="" width="${size}" height="${size}" />`
|
||||
} else if (tone) {
|
||||
glyph = `<span class="ml-level-glyph" style="color:${tone.color}">${renderMemberLevelGlyphSvg(tone.key, size)}</span>`
|
||||
} else {
|
||||
glyph = `<span class="ml-level-glyph ml-level-glyph--fallback">${escaped.slice(0, 1)}</span>`
|
||||
}
|
||||
const color = tone?.color || 'var(--app-primary-color)'
|
||||
const bg = tone?.bg || 'color-mix(in srgb, var(--app-primary-color) 10%, #fff)'
|
||||
const border = tone?.border || 'color-mix(in srgb, var(--app-primary-color) 24%, #e5e6eb)'
|
||||
return `<span class="ml-level-cell" style="--ml-color:${color};--ml-bg:${bg};--ml-border:${border}">${glyph}<span class="ml-level-name">${escaped}</span></span>`
|
||||
}
|
||||
|
||||
export function pickMemberLevelFromOverview(overview: Record<string, unknown> | null | undefined) {
|
||||
if (!overview) {
|
||||
return { code: '', name: '', iconUrl: '' }
|
||||
}
|
||||
return {
|
||||
code: String(
|
||||
overview.memberLevelCode ?? overview.MemberLevelCode ?? overview.levelCode ?? overview.LevelCode ?? '',
|
||||
),
|
||||
name: String(
|
||||
overview.memberLevelName ?? overview.MemberLevelName ?? overview.levelName ?? overview.LevelName ?? '',
|
||||
),
|
||||
iconUrl: String(overview.iconUrl ?? overview.IconUrl ?? ''),
|
||||
}
|
||||
}
|
||||
@ -3,17 +3,6 @@
|
||||
* 界面与快照统一存为 (0-0.5] 形式,便于 0.312 等重量命中。
|
||||
*/
|
||||
|
||||
/** 重量因素值下拉:0.5 步进至 30,其后固定 999(卖价/买价共用) */
|
||||
export function buildWeightFactorSelectOptions(): Array<{ label: string; value: string }> {
|
||||
return [
|
||||
...Array.from({ length: 60 }, (_, index) => {
|
||||
const v = ((index + 1) * 0.5).toFixed(1).replace(/\.0$/, '');
|
||||
return { label: v, value: v };
|
||||
}),
|
||||
{ label: '999', value: '999' },
|
||||
];
|
||||
}
|
||||
|
||||
export function isWeightPricingFactor(factorKey: string): boolean {
|
||||
const k = String(factorKey ?? '').trim();
|
||||
if (!k) return false;
|
||||
|
||||
@ -179,58 +179,21 @@ export function parseSnapshotJson(raw: unknown, depth = 0): unknown {
|
||||
return raw
|
||||
}
|
||||
|
||||
/** 超过该长度不做缩进美化,显著降低 stringify / DOM 成本 */
|
||||
const MAX_PRETTY_JSON_CHARS = 120_000
|
||||
|
||||
export function formatSnapshotJson(raw: unknown): string {
|
||||
try {
|
||||
const parsed = parseSnapshotJson(raw)
|
||||
const normalized = normalizeSnapshotForDiff(parsed)
|
||||
const target = normalized ?? parsed ?? {}
|
||||
const compact = JSON.stringify(target)
|
||||
if (compact.length > MAX_PRETTY_JSON_CHARS) return compact
|
||||
return JSON.stringify(target, null, 2)
|
||||
return JSON.stringify(normalized ?? parsed ?? {}, null, 2)
|
||||
} catch {
|
||||
return typeof raw === 'string' ? raw : '{}'
|
||||
}
|
||||
}
|
||||
|
||||
/** 规范化后的深比较:有差异时尽早返回,避免大快照双份 JSON.stringify 占内存/卡主线程 */
|
||||
function deepEqualNormalized(a: unknown, b: unknown): boolean {
|
||||
if (Object.is(a, b)) return true
|
||||
if (a === undefined || b === undefined) return a === b
|
||||
if (a === null || b === null) return a === b
|
||||
if (typeof a !== typeof b) return false
|
||||
|
||||
if (Array.isArray(a) && Array.isArray(b)) {
|
||||
if (a.length !== b.length) return false
|
||||
for (let i = 0; i < a.length; i += 1) {
|
||||
if (!deepEqualNormalized(a[i], b[i])) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if (isPlainObject(a) && isPlainObject(b)) {
|
||||
const aKeys = Object.keys(a)
|
||||
const bKeys = Object.keys(b)
|
||||
if (aKeys.length !== bKeys.length) return false
|
||||
for (const key of aKeys) {
|
||||
if (!Object.prototype.hasOwnProperty.call(b, key)) return false
|
||||
if (!deepEqualNormalized(a[key], b[key])) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
export function snapshotsSemanticallyEqual(originalRaw: unknown, modifiedRaw: unknown): boolean {
|
||||
try {
|
||||
const originalNorm = normalizeSnapshotForDiff(parseSnapshotJson(originalRaw))
|
||||
const modifiedNorm = normalizeSnapshotForDiff(parseSnapshotJson(modifiedRaw))
|
||||
const original = originalNorm === undefined ? {} : originalNorm
|
||||
const modified = modifiedNorm === undefined ? {} : modifiedNorm
|
||||
return deepEqualNormalized(original, modified)
|
||||
const original = normalizeSnapshotForDiff(parseSnapshotJson(originalRaw))
|
||||
const modified = normalizeSnapshotForDiff(parseSnapshotJson(modifiedRaw))
|
||||
return JSON.stringify(original) === JSON.stringify(modified)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
@ -269,7 +232,7 @@ function flattenDiffParts(diff: ReturnType<typeof diffJson>) {
|
||||
}
|
||||
|
||||
/** 超过该字符量时跳过 diffJson 逐行展开,避免卖价大快照卡死主线程 */
|
||||
const MAX_INLINE_DIFF_CHARS = 120_000
|
||||
const MAX_INLINE_DIFF_CHARS = 400_000
|
||||
|
||||
export function computeSnapshotDiff(
|
||||
originalRaw: unknown,
|
||||
|
||||
@ -12,22 +12,6 @@
|
||||
>
|
||||
<div class="kpi-area-import-modal">
|
||||
<div class="import-tip">{{ t('channel.area.import.tip') }}</div>
|
||||
<div class="import-mode">
|
||||
<div class="import-mode-label">{{ t('channel.area.import.modeLabel') }}</div>
|
||||
<SlidingTabs
|
||||
v-model="importMode"
|
||||
size="mini"
|
||||
:aria-label="t('channel.area.import.modeLabel')"
|
||||
:items="importModeTabs"
|
||||
/>
|
||||
<div class="import-mode-tip">
|
||||
{{
|
||||
importMode === 'full'
|
||||
? t('channel.area.import.mode.fullTip')
|
||||
: t('channel.area.import.mode.incrementalTip')
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<div class="import-actions">
|
||||
<a-button class="app-btn-primary-soft" :loading="downloading" @click="handleDownloadTemplate">
|
||||
<template #icon>
|
||||
@ -66,16 +50,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import type { FileItem } from '@arco-design/web-vue';
|
||||
import { Message, Modal } from '@arco-design/web-vue';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { IconDownload } from '@arco-design/web-vue/es/icon';
|
||||
import SlidingTabs from '@/components/SlidingTabs/index.vue';
|
||||
import {
|
||||
downloadKpiAreaInfoImportTemplate,
|
||||
importKpiAreaInfo,
|
||||
type KpiAreaInfoImportMode,
|
||||
type KpiAreaInfoImportResult,
|
||||
} from '@/api/channel/kpiArea';
|
||||
|
||||
@ -95,19 +77,13 @@
|
||||
const selectedFile = ref<File | null>(null);
|
||||
const downloading = ref(false);
|
||||
const importing = ref(false);
|
||||
const importMode = ref<KpiAreaInfoImportMode>('incremental');
|
||||
const importResult = ref<KpiAreaInfoImportResult | null>(null);
|
||||
const importModeTabs = computed(() => [
|
||||
{ key: 'incremental', label: t('channel.area.import.mode.incremental') },
|
||||
{ key: 'full', label: t('channel.area.import.mode.full') },
|
||||
]);
|
||||
|
||||
function open(id: number) {
|
||||
areaId.value = id;
|
||||
fileList.value = [];
|
||||
selectedFile.value = null;
|
||||
importResult.value = null;
|
||||
importMode.value = 'incremental';
|
||||
visible.value = true;
|
||||
}
|
||||
|
||||
@ -129,20 +105,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
function confirmFullImport(): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
Modal.warning({
|
||||
title: t('channel.area.modal.warningTitle'),
|
||||
content: t('channel.area.import.mode.fullConfirm'),
|
||||
hideCancel: false,
|
||||
okText: t('channel.area.import.submit'),
|
||||
cancelText: '取消',
|
||||
onOk: () => resolve(true),
|
||||
onCancel: () => resolve(false),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function handleBeforeOk() {
|
||||
if (!areaId.value) {
|
||||
return false;
|
||||
@ -152,16 +114,9 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
if (importMode.value === 'full') {
|
||||
const confirmed = await confirmFullImport();
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
importing.value = true;
|
||||
try {
|
||||
const result = await importKpiAreaInfo(areaId.value, selectedFile.value, importMode.value);
|
||||
const result = await importKpiAreaInfo(areaId.value, selectedFile.value);
|
||||
importResult.value = result;
|
||||
if (result.successCount > 0 || result.skipCount > 0) {
|
||||
emit('success');
|
||||
@ -205,24 +160,6 @@
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.import-mode {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.import-mode-label {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-1);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.import-mode-tip {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.import-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
|
||||
@ -84,16 +84,6 @@
|
||||
<a-button :disabled="!currentId?.id" :loading="exporting" @click="handleExport">
|
||||
{{ t('channel.area.actions.export') }}
|
||||
</a-button>
|
||||
<a-button
|
||||
status="danger"
|
||||
:disabled="!currentId?.id"
|
||||
@click="handleToolbarDelete"
|
||||
>
|
||||
{{ t('channel.area.actions.delete') }}
|
||||
</a-button>
|
||||
<a-button :disabled="!currentId?.id" @click="openPackModal">
|
||||
{{ t('channel.area.actions.packVersions') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -150,104 +140,6 @@
|
||||
:area-type="importAreaType"
|
||||
@success="onImportSuccess"
|
||||
/>
|
||||
|
||||
<a-modal
|
||||
v-model:visible="packModalVisible"
|
||||
:title="t('channel.area.pack.modalTitle', { name: selectedAreaName || '--' })"
|
||||
:width="880"
|
||||
:footer="false"
|
||||
unmount-on-close
|
||||
>
|
||||
<div class="pack-modal-toolbar table-toolbar">
|
||||
<div class="toolbar-left">
|
||||
<span class="pack-modal-tip">{{ t('channel.area.pack.tip') }}</span>
|
||||
</div>
|
||||
<div class="toolbar-right">
|
||||
<a-button size="small" :loading="packLoading" @click="loadPackList">
|
||||
{{ t('channel.area.search.refresh') }}
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
<a-table
|
||||
row-key="id"
|
||||
:loading="packLoading"
|
||||
:data="packList"
|
||||
:pagination="false"
|
||||
:bordered="{ cell: true }"
|
||||
:scroll="{ y: 420 }"
|
||||
>
|
||||
<template #columns>
|
||||
<a-table-column
|
||||
:title="t('channel.area.pack.col.version')"
|
||||
data-index="version"
|
||||
:width="160"
|
||||
/>
|
||||
<a-table-column
|
||||
:title="t('channel.area.pack.col.status')"
|
||||
data-index="status"
|
||||
:width="88"
|
||||
/>
|
||||
<a-table-column
|
||||
:title="t('channel.area.pack.col.contentHash')"
|
||||
data-index="contentHash"
|
||||
:width="140"
|
||||
>
|
||||
<template #cell="{ record }">
|
||||
<a-typography-text :copyable="{ text: record.contentHash }">
|
||||
{{ shortHash(record.contentHash) }}
|
||||
</a-typography-text>
|
||||
</template>
|
||||
</a-table-column>
|
||||
<a-table-column
|
||||
:title="t('channel.area.pack.col.publishUser')"
|
||||
data-index="publishUser"
|
||||
:width="100"
|
||||
/>
|
||||
<a-table-column
|
||||
:title="t('channel.area.pack.col.publishTime')"
|
||||
data-index="publishTime"
|
||||
:width="170"
|
||||
/>
|
||||
<a-table-column
|
||||
:title="t('channel.area.pack.col.description')"
|
||||
data-index="description"
|
||||
ellipsis
|
||||
tooltip
|
||||
/>
|
||||
<a-table-column
|
||||
:title="t('common.actions')"
|
||||
:width="88"
|
||||
align="center"
|
||||
fixed="right"
|
||||
>
|
||||
<template #cell="{ record }">
|
||||
<a-button type="text" size="mini" @click="viewPackDetail(record)">
|
||||
{{ t('channel.area.pack.viewContent') }}
|
||||
</a-button>
|
||||
</template>
|
||||
</a-table-column>
|
||||
</template>
|
||||
<template #empty>
|
||||
<a-empty :description="t('channel.area.pack.empty')" />
|
||||
</template>
|
||||
</a-table>
|
||||
</a-modal>
|
||||
|
||||
<a-modal
|
||||
v-model:visible="packDetailVisible"
|
||||
:title="packDetailTitle"
|
||||
:width="720"
|
||||
:footer="false"
|
||||
unmount-on-close
|
||||
>
|
||||
<a-spin :loading="packDetailLoading" style="width: 100%">
|
||||
<pre class="pack-detail-pre">{{
|
||||
packDetailLoading
|
||||
? ''
|
||||
: packDetailJson || t('channel.area.pack.emptyContent')
|
||||
}}</pre>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
</BasicLayoutLeftRight>
|
||||
</div>
|
||||
@ -307,7 +199,6 @@
|
||||
returnEditRowParams,
|
||||
returnRightEditRowParams,
|
||||
handleDelData,
|
||||
handleToolbarDelete,
|
||||
handleDelRightData,
|
||||
handlePageChange,
|
||||
handlePageSizeChange,
|
||||
@ -318,17 +209,6 @@
|
||||
onImportSuccess,
|
||||
exporting,
|
||||
handleExport,
|
||||
packModalVisible,
|
||||
packLoading,
|
||||
packList,
|
||||
packDetailVisible,
|
||||
packDetailLoading,
|
||||
packDetailTitle,
|
||||
packDetailJson,
|
||||
shortHash,
|
||||
openPackModal,
|
||||
loadPackList,
|
||||
viewPackDetail,
|
||||
} = useKpiAreaPage(props.areaType);
|
||||
</script>
|
||||
|
||||
@ -343,30 +223,4 @@
|
||||
color: var(--color-text-2);
|
||||
border-bottom: 1px solid var(--color-border-2);
|
||||
}
|
||||
|
||||
.pack-modal-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.pack-modal-tip {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.pack-detail-pre {
|
||||
margin: 0;
|
||||
max-height: 520px;
|
||||
overflow: auto;
|
||||
padding: 12px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
background: #f7f8fa;
|
||||
border-radius: 4px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -11,8 +11,6 @@ import {
|
||||
postForewarningServiceKpiAreaGetKpiAreas,
|
||||
exportKpiAreaInfoExcel,
|
||||
mapKpiAreaInfoToExcelRow,
|
||||
getKpiAreaPublishList,
|
||||
getKpiAreaPublishRecord,
|
||||
} from '@/api/channel/kpiArea';
|
||||
import { getCountryList } from '@/api/system/country';
|
||||
import {
|
||||
@ -596,92 +594,6 @@ export function useKpiAreaPage(areaType: KpiAreaType) {
|
||||
currentId.value = item;
|
||||
currentPage.value = 1;
|
||||
loadTableData();
|
||||
packList.value = [];
|
||||
}
|
||||
|
||||
type PackRow = {
|
||||
id: number;
|
||||
version: string;
|
||||
status: string;
|
||||
contentHash: string;
|
||||
publishUser: string;
|
||||
publishTime: string;
|
||||
description: string;
|
||||
areaType: string;
|
||||
};
|
||||
|
||||
const packModalVisible = ref(false);
|
||||
const packLoading = ref(false);
|
||||
const packList = ref<PackRow[]>([]);
|
||||
const packDetailVisible = ref(false);
|
||||
const packDetailLoading = ref(false);
|
||||
const packDetailTitle = ref('');
|
||||
const packDetailJson = ref('');
|
||||
|
||||
function mapPackRow(raw: Record<string, any>): PackRow {
|
||||
return {
|
||||
id: Number(raw.id ?? raw.Id ?? 0),
|
||||
version: String(raw.version ?? raw.Version ?? ''),
|
||||
status: String(raw.status ?? raw.Status ?? ''),
|
||||
contentHash: String(raw.contentHash ?? raw.ContentHash ?? ''),
|
||||
publishUser: String(raw.publishUser ?? raw.PublishUser ?? ''),
|
||||
publishTime: String(raw.publishTime ?? raw.PublishTime ?? ''),
|
||||
description: String(raw.description ?? raw.Description ?? ''),
|
||||
areaType: String(raw.areaType ?? raw.AreaType ?? ''),
|
||||
};
|
||||
}
|
||||
|
||||
function shortHash(hash: string) {
|
||||
const s = String(hash || '').trim();
|
||||
if (s.length <= 12) return s || '--';
|
||||
return `${s.slice(0, 8)}…${s.slice(-4)}`;
|
||||
}
|
||||
|
||||
async function openPackModal() {
|
||||
if (!guardAreaSelected()) return;
|
||||
packModalVisible.value = true;
|
||||
await loadPackList();
|
||||
}
|
||||
|
||||
async function loadPackList() {
|
||||
const areaId = currentId.value?.id;
|
||||
if (areaId == null) {
|
||||
packList.value = [];
|
||||
return;
|
||||
}
|
||||
packLoading.value = true;
|
||||
try {
|
||||
const items = await getKpiAreaPublishList(areaId);
|
||||
packList.value = (items || []).map(mapPackRow).filter((x) => x.id > 0);
|
||||
} catch (e: any) {
|
||||
packList.value = [];
|
||||
$baseMessage(e?.message || t('channel.area.pack.loadFailed'), 'error');
|
||||
} finally {
|
||||
packLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function viewPackDetail(row: PackRow) {
|
||||
if (!row?.id) return;
|
||||
packDetailTitle.value = t('channel.area.pack.detailTitle', { version: row.version || row.id });
|
||||
packDetailVisible.value = true;
|
||||
packDetailLoading.value = true;
|
||||
packDetailJson.value = '';
|
||||
try {
|
||||
const detail = await getKpiAreaPublishRecord(row.id);
|
||||
const content = detail?.content ?? detail?.Content ?? null;
|
||||
packDetailJson.value =
|
||||
content == null
|
||||
? ''
|
||||
: typeof content === 'string'
|
||||
? content
|
||||
: JSON.stringify(content, null, 2);
|
||||
} catch (e: any) {
|
||||
packDetailJson.value = '';
|
||||
$baseMessage(e?.message || t('channel.area.pack.loadDetailFailed'), 'error');
|
||||
} finally {
|
||||
packDetailLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function guardAreaSelected() {
|
||||
@ -803,15 +715,6 @@ export function useKpiAreaPage(areaType: KpiAreaType) {
|
||||
});
|
||||
}
|
||||
|
||||
function handleToolbarDelete() {
|
||||
const row = currentId.value;
|
||||
if (row?.id == null) {
|
||||
$baseMessage(t('channel.area.import.needSelectArea'), 'warning');
|
||||
return;
|
||||
}
|
||||
handleDelData(row.id, row.name);
|
||||
}
|
||||
|
||||
function handleDelRightData(id: number) {
|
||||
$baseConfirm({
|
||||
content: t('channel.area.modal.deleteConfirmDetail'),
|
||||
@ -1026,7 +929,6 @@ export function useKpiAreaPage(areaType: KpiAreaType) {
|
||||
returnEditRowParams,
|
||||
returnRightEditRowParams,
|
||||
handleDelData,
|
||||
handleToolbarDelete,
|
||||
handleDelRightData,
|
||||
handlePageChange,
|
||||
handlePageSizeChange,
|
||||
@ -1038,16 +940,5 @@ export function useKpiAreaPage(areaType: KpiAreaType) {
|
||||
exporting,
|
||||
handleExport,
|
||||
guardAreaSelected,
|
||||
packModalVisible,
|
||||
packLoading,
|
||||
packList,
|
||||
packDetailVisible,
|
||||
packDetailLoading,
|
||||
packDetailTitle,
|
||||
packDetailJson,
|
||||
shortHash,
|
||||
openPackModal,
|
||||
loadPackList,
|
||||
viewPackDetail,
|
||||
};
|
||||
}
|
||||
|
||||
@ -211,20 +211,16 @@
|
||||
}
|
||||
|
||||
function applyMergeCells() {
|
||||
if (!hot || !ready.value) return;
|
||||
if (!hot) return;
|
||||
rememberFactorValues();
|
||||
const option = mergeCellsOption();
|
||||
try {
|
||||
if (!option) {
|
||||
const plugin = hot.getPlugin?.('mergeCells') as { clearCollections?: () => void } | undefined;
|
||||
plugin?.clearCollections?.();
|
||||
}
|
||||
hot.updateSettings({ mergeCells: option }, false);
|
||||
restoreFactorValues();
|
||||
hot.render();
|
||||
} catch {
|
||||
/* mergeCells 插件未就绪 */
|
||||
if (!option) {
|
||||
const plugin = hot.getPlugin('mergeCells') as { clearCollections?: () => void } | undefined;
|
||||
plugin?.clearCollections?.();
|
||||
}
|
||||
hot.updateSettings({ mergeCells: option }, false);
|
||||
restoreFactorValues();
|
||||
hot.render();
|
||||
}
|
||||
|
||||
function decorateRows(rows: any[]): any[] {
|
||||
@ -277,46 +273,32 @@
|
||||
}
|
||||
|
||||
function applyRowSelection(indexes: number[]) {
|
||||
if (!hot || !ready.value) {
|
||||
publishSelection(indexes);
|
||||
return;
|
||||
}
|
||||
if (!hot) return;
|
||||
selectionLock += 1;
|
||||
try {
|
||||
if (!indexes.length) {
|
||||
hot.deselectCell?.();
|
||||
hot.deselectCell();
|
||||
} else {
|
||||
const lastCol = Math.max(hot.countCols() - 1, 0);
|
||||
const specs = groupConsecutive(indexes).map(
|
||||
([from, to]) => [from, 0, to, lastCol] as [number, number, number, number],
|
||||
);
|
||||
hot.selectCells?.(specs, false, false);
|
||||
hot.selectCells(specs, false, false);
|
||||
}
|
||||
} catch {
|
||||
// HT 插件未就绪时仅同步选中态,避免 clear null
|
||||
} finally {
|
||||
selectionLock -= 1;
|
||||
}
|
||||
publishSelection(indexes);
|
||||
try {
|
||||
hot.render?.();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
hot.render();
|
||||
}
|
||||
|
||||
function selectAll() {
|
||||
if (!hot || !ready.value) return;
|
||||
const count = props.data?.length || 0;
|
||||
if (!count) return;
|
||||
applyRowSelection(Array.from({ length: count }, (_, index) => index));
|
||||
}
|
||||
|
||||
function clearSelection() {
|
||||
if (!hot || !ready.value) {
|
||||
publishSelection([]);
|
||||
return;
|
||||
}
|
||||
applyRowSelection([]);
|
||||
}
|
||||
|
||||
|
||||
@ -14,7 +14,6 @@
|
||||
getSalesProductPublishRecord,
|
||||
getSalesProductPublishRulesList,
|
||||
getSalesProductPublishPreview,
|
||||
getSalesProductPublishSyncStatus,
|
||||
publishSalesProduct,
|
||||
rollbackSalesProductPublish,
|
||||
saveSalesProductAllocation,
|
||||
@ -32,16 +31,11 @@
|
||||
import { getDictListAll, getDictListByTypes } from '@/api/system/dict';
|
||||
import { getServiceChannelOptions } from '@/api/channel/serviceChannel';
|
||||
import {
|
||||
buildWeightFactorSelectOptions,
|
||||
canonicalWeightFactorCellForApi,
|
||||
isWeightPricingFactor,
|
||||
sortFactorValuesBySize,
|
||||
weightValueToIntervalCell,
|
||||
} from '@/utils/priceWeightInterval';
|
||||
import {
|
||||
estimateFactorValuesColumnWidth,
|
||||
resolveVisibleFactorValueCount,
|
||||
} from '@/utils/factorValueTagPreview';
|
||||
import { IconPlus, IconDelete, IconSearch, IconEdit, IconDragArrow, IconQuestionCircleFill, IconQuestionCircle, IconSync } from '@arco-design/web-vue/es/icon';
|
||||
import FormLabelWithPreview from './components/FormLabelWithPreview.vue';
|
||||
import BillingConditionEditor from './components/BillingConditionEditor.vue';
|
||||
@ -65,6 +59,7 @@
|
||||
import {
|
||||
computeSnapshotDiff,
|
||||
formatSnapshotJson,
|
||||
snapshotsSemanticallyEqual,
|
||||
type SnapshotDiffRow,
|
||||
} from '@/utils/publishSnapshotDiff';
|
||||
import dayjs from 'dayjs';
|
||||
@ -413,8 +408,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
/** 重量因素值:与买价共用(0.5 步进至 30,其后固定 999) */
|
||||
const weightFactorOptions: IdSelectOption[] = buildWeightFactorSelectOptions();
|
||||
/** 重量因素值:0.5 步进至 30,其后固定 999 */
|
||||
const weightFactorOptions: IdSelectOption[] = [
|
||||
...Array.from({ length: 60 }, (_, index) => {
|
||||
const v = ((index + 1) * 0.5).toFixed(1).replace(/\.0$/, '');
|
||||
return { label: v, value: v };
|
||||
}),
|
||||
{ label: '999', value: '999' },
|
||||
];
|
||||
|
||||
function filterKpiAreasByType(items: any[], areaType: string) {
|
||||
const want = areaType.toLowerCase();
|
||||
@ -1228,12 +1229,20 @@
|
||||
.map((r) => `${r.id}:${r.modifyTime}:${r.status ? 1 : 0}`)
|
||||
.join(',')}`;
|
||||
if (sig === pricePublishCheckedSig) return;
|
||||
// 立刻拉预览/线上快照(网络与表格并行);重 CPU 对比在请求返回后 yield 再跑
|
||||
void refreshPricePublishSyncStatus(productId).then(() => {
|
||||
if (seq === priceTabLoadSeq && selectedProduct.value === productId) {
|
||||
pricePublishCheckedSig = sig;
|
||||
}
|
||||
});
|
||||
// 发布态检测放到空闲时段,避免与表格首屏抢主线程
|
||||
const runPublishCheck = () => {
|
||||
if (seq !== priceTabLoadSeq || selectedProduct.value !== productId) return;
|
||||
void refreshPricePublishSyncStatus(productId).then(() => {
|
||||
if (seq === priceTabLoadSeq && selectedProduct.value === productId) {
|
||||
pricePublishCheckedSig = sig;
|
||||
}
|
||||
});
|
||||
};
|
||||
if (typeof window !== 'undefined' && 'requestIdleCallback' in window) {
|
||||
(window as any).requestIdleCallback(runPublishCheck, { timeout: 1200 });
|
||||
} else {
|
||||
window.setTimeout(runPublishCheck, 300);
|
||||
}
|
||||
} catch (e) {
|
||||
if (seq !== priceTabLoadSeq) return;
|
||||
console.error(e);
|
||||
@ -1409,23 +1418,24 @@
|
||||
async function refreshPricePublishSyncStatus(productId?: number) {
|
||||
const pid = productId ?? selectedProduct.value;
|
||||
if (pid == null || !priceTableData.value.length) {
|
||||
pricePublishChecking.value = false;
|
||||
pricePublishPending.value = null;
|
||||
return;
|
||||
}
|
||||
const seq = priceTabLoadSeq;
|
||||
pricePublishChecking.value = true;
|
||||
try {
|
||||
const status = await getSalesProductPublishSyncStatus({
|
||||
const preview = await getSalesProductPublishPreview({
|
||||
salesProductId: pid,
|
||||
operatorType: 'price',
|
||||
});
|
||||
if (seq !== priceTabLoadSeq || selectedProduct.value !== pid) return;
|
||||
pricePublishPending.value = !!(status?.pending ?? status?.Pending);
|
||||
const online = await loadLatestPublishSnapshot(pid, 'price');
|
||||
if (online == null) {
|
||||
pricePublishPending.value = true;
|
||||
return;
|
||||
}
|
||||
// 仅判断「有没有差异」:禁止走 computeSnapshotDiff/diffJson。
|
||||
// 大卖价快照会生成海量 diff 行,主线程直接卡死(与表格组件无关)。
|
||||
pricePublishPending.value = !snapshotsSemanticallyEqual(online, preview);
|
||||
} catch {
|
||||
if (seq === priceTabLoadSeq) pricePublishPending.value = null;
|
||||
} finally {
|
||||
if (seq === priceTabLoadSeq) pricePublishChecking.value = false;
|
||||
pricePublishPending.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1573,12 +1583,10 @@
|
||||
priceTabLoadedProductId = null;
|
||||
pricePublishCheckedSig = '';
|
||||
pricePublishPending.value = null;
|
||||
pricePublishChecking.value = false;
|
||||
effectiveVersion.value = '--';
|
||||
effectiveTime.value = '--';
|
||||
} else if (activeTab.value === 'price') {
|
||||
pricePublishPending.value = null;
|
||||
pricePublishChecking.value = false;
|
||||
}
|
||||
if (productChanged && activeTab.value !== 'rules') {
|
||||
rulesTabLoadedProductId = null;
|
||||
@ -1844,7 +1852,6 @@
|
||||
const publishDiffLength = ref(0);
|
||||
const publishDiffRows = ref<SnapshotDiffRow[]>([]);
|
||||
const pricePublishPending = ref<boolean | null>(null);
|
||||
const pricePublishChecking = ref(false);
|
||||
|
||||
const publishDiffStatus = computed(() => {
|
||||
if (!publishHasOnlineSnapshot.value) return 'first';
|
||||
@ -1860,53 +1867,6 @@
|
||||
publishDiffRows.value = rows;
|
||||
}
|
||||
|
||||
function yieldToMain() {
|
||||
return new Promise<void>((resolve) => {
|
||||
window.setTimeout(resolve, 0);
|
||||
});
|
||||
}
|
||||
|
||||
let publishJsonFormatTask: Promise<void> | null = null;
|
||||
let publishJsonFormatGen = 0;
|
||||
|
||||
/** 后台格式化原始 JSON:不阻塞弹窗首屏;展开「高级 JSON」/开 AI 前再确保就绪 */
|
||||
function schedulePublishJsonFormat() {
|
||||
if (publishJsonFormatTask) return publishJsonFormatTask;
|
||||
const pendingRaw = publishPendingRaw.value;
|
||||
const onlineRaw = publishOnlineRaw.value;
|
||||
if (pendingRaw == null && onlineRaw == null) return Promise.resolve();
|
||||
const gen = publishJsonFormatGen;
|
||||
publishJsonFormatTask = (async () => {
|
||||
try {
|
||||
await yieldToMain();
|
||||
if (gen !== publishJsonFormatGen) return;
|
||||
if (pendingRaw != null && !modifiedModel.value) {
|
||||
modifiedModel.value = formatSnapshotJson(pendingRaw);
|
||||
}
|
||||
await yieldToMain();
|
||||
if (gen !== publishJsonFormatGen) return;
|
||||
if (onlineRaw != null && !originalValue.value) {
|
||||
originalValue.value = formatSnapshotJson(onlineRaw);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[salesProducts] format publish json failed', error);
|
||||
} finally {
|
||||
if (gen === publishJsonFormatGen) publishJsonFormatTask = null;
|
||||
}
|
||||
})();
|
||||
return publishJsonFormatTask;
|
||||
}
|
||||
|
||||
async function ensurePublishJsonFormatted() {
|
||||
if (modifiedModel.value && originalValue.value) return;
|
||||
publishModalLoading.value = true;
|
||||
try {
|
||||
await schedulePublishJsonFormat();
|
||||
} finally {
|
||||
publishModalLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 用后端 publish/preview 接口展示真实快照(含黑名单/计重等内嵌明细) */
|
||||
async function openPublishDiffModal(operatorType: 'rules' | 'price') {
|
||||
const pid = selectedProduct.value;
|
||||
@ -1915,46 +1875,19 @@
|
||||
return;
|
||||
}
|
||||
publishOperatorType.value = operatorType;
|
||||
publishModalLoading.value = true;
|
||||
publishJsonFormatGen += 1;
|
||||
publishJsonFormatTask = null;
|
||||
publishDiffRows.value = [];
|
||||
publishDiffLength.value = 0;
|
||||
publishHasOnlineSnapshot.value = false;
|
||||
originalValue.value = '';
|
||||
modifiedModel.value = '';
|
||||
publishOnlineRaw.value = null;
|
||||
publishPendingRaw.value = null;
|
||||
pricePublishShowRawJson.value = false;
|
||||
const preview = await getSalesProductPublishPreview({
|
||||
salesProductId: pid,
|
||||
operatorType,
|
||||
});
|
||||
modifiedModel.value = formatSnapshotJson(preview);
|
||||
const online = await loadLatestPublishSnapshot(Number(pid), operatorType);
|
||||
const hasOnline = online != null;
|
||||
originalValue.value = hasOnline ? formatSnapshotJson(online) : formatSnapshotJson({});
|
||||
applyPublishDiffState(online ?? {}, preview, hasOnline);
|
||||
showOnlyDiff.value = publishDiffLength.value > 0;
|
||||
clearPricePublishAiReport();
|
||||
// 先打开弹窗再算大快照,避免长时间白屏/看起来像卡死
|
||||
publishModalMethods.openModal();
|
||||
await nextTick();
|
||||
await yieldToMain();
|
||||
|
||||
try {
|
||||
const preview = await getSalesProductPublishPreview({
|
||||
salesProductId: pid,
|
||||
operatorType,
|
||||
});
|
||||
publishPendingRaw.value = preview;
|
||||
await yieldToMain();
|
||||
|
||||
const online = await loadLatestPublishSnapshot(Number(pid), operatorType);
|
||||
const hasOnline = online != null;
|
||||
publishOnlineRaw.value = hasOnline ? online : {};
|
||||
await yieldToMain();
|
||||
|
||||
applyPublishDiffState(publishOnlineRaw.value ?? {}, preview, hasOnline);
|
||||
showOnlyDiff.value = publishDiffLength.value > 0;
|
||||
// JSON 美化放到后台,首屏只展示 diff 摘要
|
||||
void schedulePublishJsonFormat();
|
||||
} catch (error) {
|
||||
publishModalMethods.closeModal();
|
||||
throw error;
|
||||
} finally {
|
||||
publishModalLoading.value = false;
|
||||
}
|
||||
publishModalMethods.openModal();
|
||||
}
|
||||
|
||||
const handlePublishPrice = async () => {
|
||||
@ -1967,7 +1900,6 @@
|
||||
Message.warning('暂无可发布的费用项');
|
||||
return;
|
||||
}
|
||||
if (publishModalLoading.value) return;
|
||||
try {
|
||||
await openPublishDiffModal('price');
|
||||
} catch (e: any) {
|
||||
@ -2144,9 +2076,8 @@
|
||||
pricePublishHighRiskAck.value = false;
|
||||
}
|
||||
|
||||
async function openPricePublishAiAssist() {
|
||||
function openPricePublishAiAssist() {
|
||||
if (publishOperatorType.value !== 'price') return;
|
||||
await ensurePublishJsonFormatted();
|
||||
pricePublishAiVisible.value = true;
|
||||
}
|
||||
|
||||
@ -2205,22 +2136,14 @@
|
||||
const [registerPublishModal, publishModalMethods] = useModal() as [any, any];
|
||||
const modifiedModel = ref<string>('');
|
||||
const originalValue = ref<string | undefined>('');
|
||||
/** 未格式化的原始快照,供后台 stringify,避免阻塞弹窗 */
|
||||
const publishPendingRaw = ref<unknown>(null);
|
||||
const publishOnlineRaw = ref<unknown>(null);
|
||||
const publishModalLoading = ref(false);
|
||||
const showOnlyDiff = ref(true);
|
||||
const publishConfirmVisible = ref(false);
|
||||
const publishDescription = ref('');
|
||||
const publishConfirmDate = ref(dayjs());
|
||||
|
||||
// 已在打开弹窗时格式化过,禁止再次 formatSnapshotJson(大快照会二次卡死)
|
||||
const getOriginalValue = computed(() => originalValue.value || '{}');
|
||||
const getModifiedModel = computed(() => modifiedModel.value || '{}');
|
||||
const getOriginalValue = computed(() => formatSnapshotJson(originalValue.value || {}));
|
||||
|
||||
watch(pricePublishShowRawJson, (open) => {
|
||||
if (open) void ensurePublishJsonFormatted();
|
||||
});
|
||||
const getModifiedModel = computed(() => formatSnapshotJson(modifiedModel.value || {}));
|
||||
|
||||
const handlePublishConfirm = () => {
|
||||
if (publishDiffStatus.value === 'synced') {
|
||||
@ -2883,17 +2806,12 @@
|
||||
billingFactorsHeightRaf = requestAnimationFrame(() => {
|
||||
billingFactorsHeightRaf = 0;
|
||||
syncBillingFactorsTableHeight();
|
||||
syncFactorValuesCellWidth();
|
||||
});
|
||||
});
|
||||
billingFactorsResizeObserver.observe(el);
|
||||
nextTick(() => {
|
||||
syncBillingFactorsTableHeight(true);
|
||||
syncFactorValuesCellWidth();
|
||||
setTimeout(() => {
|
||||
syncBillingFactorsTableHeight(true);
|
||||
syncFactorValuesCellWidth();
|
||||
}, 80);
|
||||
setTimeout(() => syncBillingFactorsTableHeight(true), 80);
|
||||
});
|
||||
}
|
||||
|
||||
@ -3154,33 +3072,16 @@
|
||||
row.factorValues.splice(valueIndex, 1);
|
||||
};
|
||||
|
||||
/** 因素值列可用宽度(随表格 resize 更新),用于按字符宽动态算可见标签数 */
|
||||
const factorValuesCellWidth = ref(520);
|
||||
|
||||
function syncFactorValuesCellWidth() {
|
||||
const el = billingFactorsTableWrapRef.value;
|
||||
if (!el) return;
|
||||
const cell = el.querySelector('.factor-values-cell') as HTMLElement | null;
|
||||
const w = cell?.clientWidth || estimateFactorValuesColumnWidth(el.clientWidth);
|
||||
if (w > 0 && Math.abs(w - factorValuesCellWidth.value) > 2) {
|
||||
factorValuesCellWidth.value = w;
|
||||
}
|
||||
}
|
||||
/** 因素值预览:行内最多展示条数,超出用 +N 弹层 */
|
||||
const FACTOR_VALUE_PREVIEW_LIMIT = 10;
|
||||
|
||||
function getFactorValuePreview(row: any) {
|
||||
const values = Array.isArray(row?.factorValues) ? row.factorValues : [];
|
||||
const items = values.map((value: string, index: number) => {
|
||||
const label = formatFactorValueTagDisplay(row.factor, value) || String(value ?? '');
|
||||
return { value, index, label };
|
||||
});
|
||||
const limit = resolveVisibleFactorValueCount(
|
||||
items.map((it) => it.label),
|
||||
factorValuesCellWidth.value,
|
||||
);
|
||||
const limit = FACTOR_VALUE_PREVIEW_LIMIT;
|
||||
return {
|
||||
items,
|
||||
visible: items.slice(0, limit),
|
||||
extraCount: Math.max(0, items.length - limit),
|
||||
values,
|
||||
visible: values.slice(0, limit),
|
||||
extraCount: Math.max(0, values.length - limit),
|
||||
};
|
||||
}
|
||||
|
||||
@ -3804,27 +3705,15 @@
|
||||
|
||||
function clearPriceListChecked() {
|
||||
priceListSelectedIds.value = [];
|
||||
try {
|
||||
priceListHotRef.value?.clearSelection?.();
|
||||
} catch {
|
||||
// Handsontable / mergeCells 未就绪时忽略
|
||||
}
|
||||
priceListHotRef.value?.clearSelection();
|
||||
}
|
||||
|
||||
function selectAllPriceListRows() {
|
||||
try {
|
||||
priceListHotRef.value?.selectAll?.();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
priceListHotRef.value?.selectAll();
|
||||
}
|
||||
|
||||
function clearAllPriceListRowsSelection() {
|
||||
try {
|
||||
priceListHotRef.value?.clearSelection?.();
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
priceListHotRef.value?.clearSelection();
|
||||
}
|
||||
|
||||
const openBatchFillModal = (onlyEmpty = false) => {
|
||||
@ -4056,8 +3945,7 @@
|
||||
coercePriceListRowsFactorCells();
|
||||
normalizePriceListFormulaDisplays();
|
||||
capturePriceListCustomerCodesSnapshot();
|
||||
priceListSelectedIds.value = [];
|
||||
void nextTick(() => clearPriceListChecked());
|
||||
clearPriceListChecked();
|
||||
if (priceModalVisible.value && priceDrawerActiveTab.value === 'basic') {
|
||||
refreshBillingFactorsTable();
|
||||
}
|
||||
@ -4088,12 +3976,12 @@
|
||||
chargingCondition: record.chargingCondition || '无',
|
||||
});
|
||||
|
||||
// 列表行已带 factors/details,先本地回填并立刻打开弹窗(勿在 HT 挂载前 clear)
|
||||
// 列表行已带 factors/details,先本地回填并立刻打开弹窗
|
||||
billingFactorsData.value = normalizeBillingFactorsFromApi(record?.factors);
|
||||
replacePriceListData(mapSalesProductDetailsToPriceListRows(record?.details || []));
|
||||
coercePriceListRowsFactorCells();
|
||||
capturePriceListCustomerCodesSnapshot();
|
||||
priceListSelectedIds.value = [];
|
||||
clearPriceListChecked();
|
||||
|
||||
// 列表行常无 details:先开基础信息保证即时反馈,数据就绪后再切价格表
|
||||
priceDrawerActiveTab.value = 'basic';
|
||||
@ -4457,7 +4345,6 @@
|
||||
<a-button
|
||||
type="primary"
|
||||
size="small"
|
||||
:loading="publishModalLoading"
|
||||
@click="handlePublishPrice"
|
||||
>
|
||||
{{ t('channel.salesProducts.actions.publishPrice') }}
|
||||
@ -4527,15 +4414,7 @@
|
||||
{{ t('channel.salesProducts.price.effectiveVersion', { version: effectiveVersion }) }}
|
||||
<span style="margin-left: 16px; color: var(--color-text-3);">生效时间:{{ effectiveTime }}</span>
|
||||
<a-tag
|
||||
v-if="pricePublishChecking && pricePublishPending == null"
|
||||
color="gray"
|
||||
size="small"
|
||||
class="price-sync-tag"
|
||||
>
|
||||
{{ t('channel.salesProducts.publish.badge.checking') }}
|
||||
</a-tag>
|
||||
<a-tag
|
||||
v-else-if="pricePublishPending === true"
|
||||
v-if="pricePublishPending === true"
|
||||
color="orangered"
|
||||
size="small"
|
||||
class="price-sync-tag"
|
||||
@ -5365,17 +5244,16 @@
|
||||
:key="row.id"
|
||||
class="factor-values-cell"
|
||||
>
|
||||
<template v-if="preview.items.length">
|
||||
<template v-if="preview.values.length">
|
||||
<a-tag
|
||||
v-for="item in preview.visible"
|
||||
:key="`${row.id}-v-${item.index}`"
|
||||
v-for="(value, valueIndex) in preview.visible"
|
||||
:key="`${row.id}-v-${valueIndex}`"
|
||||
size="small"
|
||||
closable
|
||||
class="factor-value-tag"
|
||||
:title="item.label"
|
||||
@close="handleDeleteFactorValue(row, item.index)"
|
||||
@close="handleDeleteFactorValue(row, valueIndex)"
|
||||
>
|
||||
<span class="factor-value-tag__text">{{ item.label }}</span>
|
||||
{{ formatFactorValueTagDisplay(row.factor, value) }}
|
||||
</a-tag>
|
||||
<a-popover
|
||||
v-if="preview.extraCount > 0"
|
||||
@ -5389,19 +5267,17 @@
|
||||
<template #content>
|
||||
<div class="factor-values-popover">
|
||||
<div class="factor-values-popover__title">
|
||||
已选 {{ preview.items.length }} 项
|
||||
已选 {{ preview.values.length }} 项
|
||||
</div>
|
||||
<div class="factor-values-popover__list">
|
||||
<a-tag
|
||||
v-for="item in preview.items"
|
||||
:key="`${row.id}-all-${item.index}`"
|
||||
v-for="(value, valueIndex) in preview.values"
|
||||
:key="`${row.id}-all-${valueIndex}`"
|
||||
size="small"
|
||||
closable
|
||||
class="factor-value-tag factor-value-tag--popover"
|
||||
:title="item.label"
|
||||
@close="handleDeleteFactorValue(row, item.index)"
|
||||
@close="handleDeleteFactorValue(row, valueIndex)"
|
||||
>
|
||||
<span class="factor-value-tag__text">{{ item.label }}</span>
|
||||
{{ formatFactorValueTagDisplay(row.factor, value) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
@ -5902,11 +5778,6 @@
|
||||
:body-style="publishModalBodyStyle"
|
||||
@register="registerPublishModal"
|
||||
>
|
||||
<a-spin
|
||||
:loading="publishModalLoading"
|
||||
tip="正在加载发布快照,大价格表可能需要几秒…"
|
||||
class="publish-modal-spin"
|
||||
>
|
||||
<div class="release-content slim">
|
||||
<!-- 价格:流程卡片 -->
|
||||
<div v-if="isPricePublishFlow && publishDiffLength > 0" class="publish-flow">
|
||||
@ -6202,7 +6073,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-spin>
|
||||
</ModalPro>
|
||||
|
||||
<PricePublishAiAssistDrawer
|
||||
@ -6901,34 +6771,19 @@
|
||||
|
||||
.factor-values-cell {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
max-height: 48px;
|
||||
overflow: hidden;
|
||||
|
||||
.factor-value-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex: 0 1 auto;
|
||||
max-width: 148px;
|
||||
min-width: 0;
|
||||
flex-shrink: 0;
|
||||
max-width: 88px;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
|
||||
.factor-value-tag__text {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
:deep(.arco-tag-close-btn) {
|
||||
flex-shrink: 0;
|
||||
margin-left: 4px;
|
||||
}
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.factor-value-more {
|
||||
@ -6970,16 +6825,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.factor-value-tag--popover {
|
||||
max-width: 100%;
|
||||
|
||||
.factor-value-tag__text {
|
||||
white-space: normal;
|
||||
overflow: visible;
|
||||
text-overflow: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.factor-values-popover {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -7186,16 +7031,13 @@
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
display: block;
|
||||
|
||||
:deep(.arco-spin-children) {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -7405,14 +7247,7 @@
|
||||
|
||||
.publish-record-content-spin {
|
||||
display: block;
|
||||
width: 100%;
|
||||
/* 高度随正文,由外层 .code-content 滚动,避免 spin 把面板撑破 */
|
||||
height: auto;
|
||||
min-height: 80px;
|
||||
|
||||
:deep(.arco-spin-children) {
|
||||
height: auto;
|
||||
}
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.publish-records-toolbar {
|
||||
@ -7434,7 +7269,6 @@
|
||||
.publish-records-container {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
gap: 0;
|
||||
border: 1px solid var(--color-border-2, #e8e8e8);
|
||||
@ -7446,8 +7280,6 @@
|
||||
.publish-records-left {
|
||||
width: 320px;
|
||||
flex-shrink: 0;
|
||||
min-height: 0;
|
||||
align-self: stretch;
|
||||
border-right: 1px solid var(--color-border-2, #e8e8e8);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -7458,7 +7290,6 @@
|
||||
.version-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -7583,8 +7414,6 @@
|
||||
.publish-records-right {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px 20px;
|
||||
@ -7738,18 +7567,6 @@
|
||||
}
|
||||
|
||||
// 发布弹窗样式
|
||||
.publish-modal-spin {
|
||||
display: block;
|
||||
width: 100%;
|
||||
min-height: 240px;
|
||||
|
||||
:deep(.arco-spin-container),
|
||||
:deep(.arco-spin) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.release-content.slim {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@ -51,7 +51,7 @@ const SKILL_CORE = `你是 OMS「销售产品卖价发布」风险分析助手
|
||||
4. factors / factorValues / factorValuesJson 维度错位或漏维
|
||||
5. customerGroupValues 客户分组价漏改、串组、整组清空
|
||||
6. chargingCondition / costUnit / costItem 语义变化
|
||||
7. priceArea / priceAreaId / priceAreaRefs 分区包引用错绑
|
||||
7. priceArea / priceAreaId / priceAreaInfos 分区错绑
|
||||
8. 首次发布(无线上快照)时,检查待发布是否像「未完成草稿」
|
||||
9. 相对生效版的「大面积替换」感(大量行删除+新增)
|
||||
|
||||
@ -120,7 +120,7 @@ function slimPriceSnapshot(raw: unknown): unknown {
|
||||
salesProductId: obj.salesProductId ?? obj.SalesProductId,
|
||||
priceAreaId: obj.priceAreaId ?? obj.PriceAreaId,
|
||||
priceAreaName: obj.priceAreaName ?? obj.PriceAreaName,
|
||||
priceAreaRefs: obj.priceAreaRefs ?? obj.PriceAreaRefs,
|
||||
priceAreaInfos: obj.priceAreaInfos ?? obj.PriceAreaInfos,
|
||||
prices: slimPrices,
|
||||
}
|
||||
}
|
||||
|
||||
@ -287,15 +287,7 @@
|
||||
{{ t('channel.salesProducts.price.effectiveVersion', { version: effectiveVersion }) }}
|
||||
<span style="margin-left: 16px; color: var(--color-text-3);">生效时间:{{ effectiveTime }}</span>
|
||||
<a-tag
|
||||
v-if="pricePublishChecking && pricePublishPending == null"
|
||||
color="gray"
|
||||
size="small"
|
||||
class="price-sync-tag"
|
||||
>
|
||||
{{ t('channel.serviceChannels.publish.badge.checking') }}
|
||||
</a-tag>
|
||||
<a-tag
|
||||
v-else-if="pricePublishPending === true"
|
||||
v-if="pricePublishPending === true"
|
||||
color="orangered"
|
||||
size="small"
|
||||
class="price-sync-tag"
|
||||
@ -675,17 +667,16 @@
|
||||
:key="row.id"
|
||||
class="factor-values-cell"
|
||||
>
|
||||
<template v-if="preview.items.length">
|
||||
<template v-if="preview.values.length">
|
||||
<a-tag
|
||||
v-for="item in preview.visible"
|
||||
:key="`${row.id}-v-${item.index}`"
|
||||
v-for="(value, valueIndex) in preview.visible"
|
||||
:key="`${row.id}-v-${valueIndex}`"
|
||||
size="small"
|
||||
closable
|
||||
class="factor-value-tag"
|
||||
:title="item.label"
|
||||
@close="handleDeleteFactorValue(row, item.index)"
|
||||
@close="handleDeleteFactorValue(row, valueIndex)"
|
||||
>
|
||||
<span class="factor-value-tag__text">{{ item.label }}</span>
|
||||
{{ formatFactorValueTagDisplay(row.factor, value) }}
|
||||
</a-tag>
|
||||
<a-popover
|
||||
v-if="preview.extraCount > 0"
|
||||
@ -699,19 +690,17 @@
|
||||
<template #content>
|
||||
<div class="factor-values-popover">
|
||||
<div class="factor-values-popover__title">
|
||||
已选 {{ preview.items.length }} 项
|
||||
已选 {{ preview.values.length }} 项
|
||||
</div>
|
||||
<div class="factor-values-popover__list">
|
||||
<a-tag
|
||||
v-for="item in preview.items"
|
||||
:key="`${row.id}-all-${item.index}`"
|
||||
v-for="(value, valueIndex) in preview.values"
|
||||
:key="`${row.id}-all-${valueIndex}`"
|
||||
size="small"
|
||||
closable
|
||||
class="factor-value-tag factor-value-tag--popover"
|
||||
:title="item.label"
|
||||
@close="handleDeleteFactorValue(row, item.index)"
|
||||
@close="handleDeleteFactorValue(row, valueIndex)"
|
||||
>
|
||||
<span class="factor-value-tag__text">{{ item.label }}</span>
|
||||
{{ formatFactorValueTagDisplay(row.factor, value) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
@ -1430,6 +1419,7 @@ import PriceListHotTable, {
|
||||
} from '@/views/channel/salesProducts/components/PriceListHotTable.vue'
|
||||
import {
|
||||
computeSnapshotDiff,
|
||||
snapshotsSemanticallyEqual,
|
||||
formatSnapshotJson,
|
||||
type SnapshotDiffRow,
|
||||
} from '@/utils/publishSnapshotDiff'
|
||||
@ -1437,16 +1427,11 @@ import dayjs from 'dayjs'
|
||||
import * as XLSX from 'xlsx'
|
||||
import Sortable from 'sortablejs'
|
||||
import {
|
||||
buildWeightFactorSelectOptions,
|
||||
canonicalWeightFactorCellForApi,
|
||||
isWeightPricingFactor,
|
||||
sortFactorValuesBySize,
|
||||
weightValueToIntervalCell,
|
||||
} from '@/utils/priceWeightInterval'
|
||||
import {
|
||||
estimateFactorValuesColumnWidth,
|
||||
resolveVisibleFactorValueCount,
|
||||
} from '@/utils/factorValueTagPreview'
|
||||
import { getAppGetList } from '@/api/ApplicationServices/apps'
|
||||
import {
|
||||
getServiceProviderGetServiceProviderDropdownList,
|
||||
@ -1465,7 +1450,6 @@ import {
|
||||
getServiceChannelPrice,
|
||||
getServiceChannelPriceList,
|
||||
getServiceChannelPublishPreview,
|
||||
getServiceChannelPublishSyncStatus,
|
||||
getServiceChannelPublishPriceList,
|
||||
getServiceChannelPublishRulesList,
|
||||
publishServiceChannel,
|
||||
@ -1783,8 +1767,10 @@ function formatPriceTableFactorCell(factorIndex: number | undefined, raw: string
|
||||
|
||||
type IdSelectOption = { label: string; value: string }
|
||||
|
||||
/** 重量因素值:与产品卖价共用(0.5 步进至 30,其后固定 999) */
|
||||
const weightFactorOptions: IdSelectOption[] = buildWeightFactorSelectOptions()
|
||||
const weightFactorOptions: IdSelectOption[] = Array.from({ length: 20 }, (_, index) => {
|
||||
const v = ((index + 1) * 0.5).toFixed(1).replace(/\.0$/, '')
|
||||
return { label: v, value: v }
|
||||
})
|
||||
|
||||
function filterKpiAreasByType(items: any[], areaType: string) {
|
||||
const want = areaType.toLowerCase()
|
||||
@ -2787,33 +2773,16 @@ function clearAllPriceListRowsSelection() {
|
||||
priceListHotRef.value?.clearSelection()
|
||||
}
|
||||
|
||||
/** 因素值列可用宽度(随表格 resize 更新),用于按字符宽动态算可见标签数 */
|
||||
const factorValuesCellWidth = ref(520)
|
||||
|
||||
function syncFactorValuesCellWidth() {
|
||||
const el = billingFactorsTableWrapRef.value
|
||||
if (!el) return
|
||||
const cell = el.querySelector('.factor-values-cell') as HTMLElement | null
|
||||
const w = cell?.clientWidth || estimateFactorValuesColumnWidth(el.clientWidth)
|
||||
if (w > 0 && Math.abs(w - factorValuesCellWidth.value) > 2) {
|
||||
factorValuesCellWidth.value = w
|
||||
}
|
||||
}
|
||||
/** 因素值预览:行内最多展示条数,超出用 +N 弹层 */
|
||||
const FACTOR_VALUE_PREVIEW_LIMIT = 10
|
||||
|
||||
function getFactorValuePreview(row: any) {
|
||||
const values = Array.isArray(row?.factorValues) ? row.factorValues : []
|
||||
const items = values.map((value: string, index: number) => {
|
||||
const label = formatFactorValueTagDisplay(row.factor, value) || String(value ?? '')
|
||||
return { value, index, label }
|
||||
})
|
||||
const limit = resolveVisibleFactorValueCount(
|
||||
items.map((it) => it.label),
|
||||
factorValuesCellWidth.value,
|
||||
)
|
||||
const limit = FACTOR_VALUE_PREVIEW_LIMIT
|
||||
return {
|
||||
items,
|
||||
visible: items.slice(0, limit),
|
||||
extraCount: Math.max(0, items.length - limit),
|
||||
values,
|
||||
visible: values.slice(0, limit),
|
||||
extraCount: Math.max(0, values.length - limit),
|
||||
}
|
||||
}
|
||||
|
||||
@ -4097,7 +4066,6 @@ const publishHasOnlineSnapshot = ref(false)
|
||||
const publishDiffLength = ref(0)
|
||||
const publishDiffRows = ref<SnapshotDiffRow[]>([])
|
||||
const pricePublishPending = ref<boolean | null>(null)
|
||||
const pricePublishChecking = ref(false)
|
||||
|
||||
const publishDiffStatus = computed(() => {
|
||||
if (!publishHasOnlineSnapshot.value) return 'first'
|
||||
@ -4109,21 +4077,25 @@ const publishDiffStatus = computed(() => {
|
||||
async function refreshPricePublishSyncStatus(serviceChannelId?: number | string) {
|
||||
const cid = serviceChannelId ?? (selectedChannel.value as any)?.id
|
||||
if (cid == null || !priceTableData.value.length) {
|
||||
pricePublishChecking.value = false
|
||||
pricePublishPending.value = null
|
||||
return
|
||||
}
|
||||
pricePublishChecking.value = true
|
||||
try {
|
||||
const status = await getServiceChannelPublishSyncStatus({
|
||||
const preview = await getServiceChannelPublishPreview({
|
||||
serviceChannelId: cid,
|
||||
operatorType: 'price',
|
||||
})
|
||||
pricePublishPending.value = !!(status?.pending ?? status?.Pending)
|
||||
const pub = await getServiceChannelPublishPriceList({ serviceChannelId: cid, page: 1, pageSize: 1 })
|
||||
const latest = pub.items?.[0] as Record<string, any> | undefined
|
||||
const online = latest?.content ?? latest?.Content
|
||||
if (online == null) {
|
||||
pricePublishPending.value = true
|
||||
return
|
||||
}
|
||||
// 仅判断有无差异,禁止大快照走 diffJson(会卡死整页)
|
||||
pricePublishPending.value = !snapshotsSemanticallyEqual(online, preview)
|
||||
} catch {
|
||||
pricePublishPending.value = null
|
||||
} finally {
|
||||
pricePublishChecking.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@ -4566,17 +4538,12 @@ function setupBillingFactorsResizeObserver() {
|
||||
billingFactorsHeightRaf = requestAnimationFrame(() => {
|
||||
billingFactorsHeightRaf = 0
|
||||
syncBillingFactorsTableHeight()
|
||||
syncFactorValuesCellWidth()
|
||||
})
|
||||
})
|
||||
billingFactorsResizeObserver.observe(el)
|
||||
nextTick(() => {
|
||||
syncBillingFactorsTableHeight(true)
|
||||
syncFactorValuesCellWidth()
|
||||
setTimeout(() => {
|
||||
syncBillingFactorsTableHeight(true)
|
||||
syncFactorValuesCellWidth()
|
||||
}, 80)
|
||||
setTimeout(() => syncBillingFactorsTableHeight(true), 80)
|
||||
})
|
||||
}
|
||||
|
||||
@ -5614,34 +5581,19 @@ const handleConfirmPublish = async () => {
|
||||
|
||||
.factor-values-cell {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
max-height: 48px;
|
||||
overflow: hidden;
|
||||
|
||||
.factor-value-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex: 0 1 auto;
|
||||
max-width: 148px;
|
||||
min-width: 0;
|
||||
flex-shrink: 0;
|
||||
max-width: 88px;
|
||||
overflow: hidden;
|
||||
vertical-align: middle;
|
||||
|
||||
.factor-value-tag__text {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
:deep(.arco-tag-close-btn) {
|
||||
flex-shrink: 0;
|
||||
margin-left: 4px;
|
||||
}
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.factor-value-more {
|
||||
@ -5683,16 +5635,6 @@ const handleConfirmPublish = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
.factor-value-tag--popover {
|
||||
max-width: 100%;
|
||||
|
||||
.factor-value-tag__text {
|
||||
white-space: normal;
|
||||
overflow: visible;
|
||||
text-overflow: unset;
|
||||
}
|
||||
}
|
||||
|
||||
.factor-values-popover {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@ -16,38 +16,20 @@
|
||||
<div class="h5-openapi__handle" aria-hidden="true" />
|
||||
<header class="h5-openapi__head">
|
||||
<h3>{{ t('h5.me.openapi') }}</h3>
|
||||
<div class="h5-openapi__tools">
|
||||
<button
|
||||
v-if="openapiHasCredential"
|
||||
type="button"
|
||||
class="h5-openapi__tool"
|
||||
:disabled="openapiBusy"
|
||||
:title="t('user.customer.openapi.resetSecret')"
|
||||
:aria-label="t('user.customer.openapi.resetSecret')"
|
||||
@click="openapiPanelRef?.resetSecret()"
|
||||
>
|
||||
<icon-refresh />
|
||||
</button>
|
||||
<button type="button" class="h5-openapi__close" @click="close">
|
||||
<icon-close />
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="h5-openapi__close" @click="close">
|
||||
<icon-close />
|
||||
</button>
|
||||
</header>
|
||||
<div class="h5-openapi__body">
|
||||
<CustomerOpenApiCredentialPanel
|
||||
v-if="visible"
|
||||
ref="openapiPanelRef"
|
||||
@status="onOpenapiStatus"
|
||||
/>
|
||||
<CustomerOpenApiCredentialPanel v-if="visible" />
|
||||
</div>
|
||||
</div>
|
||||
</a-drawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { IconClose, IconRefresh } from '@arco-design/web-vue/es/icon'
|
||||
import { IconClose } from '@arco-design/web-vue/es/icon'
|
||||
import CustomerOpenApiCredentialPanel from '@/views/user/customer/components/CustomerOpenApiCredentialPanel.vue'
|
||||
|
||||
defineOptions({ name: 'H5OpenApiCredentialSheet' })
|
||||
@ -61,14 +43,6 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const openapiPanelRef = ref<{ resetSecret: () => void } | null>(null)
|
||||
const openapiHasCredential = ref(false)
|
||||
const openapiBusy = ref(false)
|
||||
|
||||
function onOpenapiStatus(payload: { hasCredential: boolean; busy: boolean }) {
|
||||
openapiHasCredential.value = payload.hasCredential
|
||||
openapiBusy.value = payload.busy
|
||||
}
|
||||
|
||||
const drawerStyle = {
|
||||
borderRadius: '20px 20px 0 0',
|
||||
@ -107,38 +81,11 @@ function close() {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.h5-openapi__tools {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.h5-openapi__tool,
|
||||
.h5-openapi__close {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin: 0;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
padding: 0;
|
||||
padding: 4px;
|
||||
color: var(--color-text-2);
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.h5-openapi__tool:hover:not(:disabled),
|
||||
.h5-openapi__close:hover {
|
||||
color: var(--app-primary-color);
|
||||
background: color-mix(in srgb, var(--app-primary-color) 8%, transparent);
|
||||
}
|
||||
|
||||
.h5-openapi__tool:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.h5-openapi__body {
|
||||
|
||||
@ -9,22 +9,7 @@
|
||||
|
||||
<template v-else>
|
||||
<section class="h5-me-hero h5-enter" :class="{ 'is-low': isLowBalance }">
|
||||
<span v-if="levelBadge" class="h5-me-vip" :class="levelToneClass" :style="levelToneStyle">
|
||||
<img
|
||||
v-if="memberLevel.iconUrl"
|
||||
class="h5-me-vip__icon"
|
||||
:src="memberLevel.iconUrl"
|
||||
alt=""
|
||||
draggable="false"
|
||||
/>
|
||||
<span
|
||||
v-else-if="levelGlyphHtml"
|
||||
class="h5-me-vip__icon"
|
||||
aria-hidden="true"
|
||||
v-html="levelGlyphHtml"
|
||||
/>
|
||||
<em class="h5-me-vip__name">{{ levelBadge }}</em>
|
||||
</span>
|
||||
<span v-if="levelBadge" class="h5-me-vip">{{ levelBadge }}</span>
|
||||
|
||||
<div class="h5-me-hero__top">
|
||||
<img :src="displayAvatar" alt="" class="h5-me-avatar" />
|
||||
@ -256,11 +241,6 @@ import {
|
||||
} from '@/api/customer/resolve';
|
||||
import { getPickupOrderList } from '@/api/order/pickupOrder';
|
||||
import { getMyCoupons, getMemberOverview } from '@/api/marketing/portal';
|
||||
import {
|
||||
getMemberLevelTone,
|
||||
pickMemberLevelFromOverview,
|
||||
renderMemberLevelGlyphSvg,
|
||||
} from '@/utils/memberLevelIcon';
|
||||
import { isLogin } from '@/utils/auth';
|
||||
import { resolveAvatarUrl } from '@/utils/avatar';
|
||||
import { getMapIdFromAccessToken } from '@/utils/jwt';
|
||||
@ -432,26 +412,10 @@ const customerGroupLabel = computed(() => {
|
||||
return label !== key ? label : '';
|
||||
});
|
||||
|
||||
const memberLevel = computed(() => pickMemberLevelFromOverview(memberOverview.value));
|
||||
const memberLevelName = computed(() => memberLevel.value.name);
|
||||
const memberLevelName = computed(() =>
|
||||
String(memberOverview.value?.memberLevelName ?? memberOverview.value?.MemberLevelName ?? ''),
|
||||
);
|
||||
const levelBadge = computed(() => memberLevelName.value || customerGroupLabel.value);
|
||||
const levelTone = computed(() =>
|
||||
getMemberLevelTone(memberLevel.value.code, levelBadge.value),
|
||||
);
|
||||
const levelToneClass = computed(() => (levelTone.value ? `is-${levelTone.value.key}` : ''));
|
||||
const levelToneStyle = computed(() => {
|
||||
if (!levelTone.value) return undefined;
|
||||
const c = levelTone.value.color;
|
||||
return {
|
||||
'--me-vip-accent': c,
|
||||
'--me-vip-from': `color-mix(in srgb, ${c} 78%, #1a1a1a)`,
|
||||
'--me-vip-to': c,
|
||||
'--me-vip-glow': `color-mix(in srgb, ${c} 35%, transparent)`,
|
||||
} as Record<string, string>;
|
||||
});
|
||||
const levelGlyphHtml = computed(() =>
|
||||
levelTone.value ? renderMemberLevelGlyphSvg(levelTone.value.key, 12) : '',
|
||||
);
|
||||
|
||||
const totalPoints = computed(() =>
|
||||
Number(
|
||||
@ -791,22 +755,19 @@ onMounted(() => {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
/* 右上角错落等级角标:底色跟等级色走,不再被墨色冲淡 */
|
||||
/* 右上角错落等级角标:允许探出边框 */
|
||||
.h5-me-vip {
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
top: -8px;
|
||||
right: -2px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
max-width: 56%;
|
||||
padding: 6px 14px 7px 10px;
|
||||
max-width: 48%;
|
||||
padding: 6px 14px 7px 12px;
|
||||
border-radius: 10px 4px 4px 12px;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--me-vip-from, var(--me-ink)) 0%,
|
||||
var(--me-vip-to, var(--me-teal)) 100%
|
||||
var(--me-ink) 0%,
|
||||
color-mix(in srgb, var(--me-ink) 72%, var(--me-teal)) 100%
|
||||
);
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
@ -818,36 +779,10 @@ onMounted(() => {
|
||||
text-overflow: ellipsis;
|
||||
transform: none;
|
||||
box-shadow:
|
||||
0 4px 12px var(--me-vip-glow, rgba(20, 38, 55, 0.2)),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.22);
|
||||
0 4px 12px rgba(20, 38, 55, 0.2),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.18);
|
||||
pointer-events: none;
|
||||
|
||||
.h5-me-vip__icon {
|
||||
flex-shrink: 0;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
object-fit: contain;
|
||||
color: #fff;
|
||||
filter: drop-shadow(0 1px 0 rgba(0, 0, 0, 0.12));
|
||||
|
||||
:deep(svg) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.h5-me-vip__name {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-style: normal;
|
||||
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
|
||||
@ -8,14 +8,6 @@
|
||||
<p class="banner-hello__line">
|
||||
<span class="banner-hello__greet">{{ greetText }}</span>
|
||||
<span class="banner-hello__name">{{ displayName }}</span>
|
||||
<MemberLevelBadge
|
||||
v-if="showBalance && memberLevel.name"
|
||||
class="banner-hello__level"
|
||||
:level-code="memberLevel.code"
|
||||
:level-name="memberLevel.name"
|
||||
:icon-url="memberLevel.iconUrl"
|
||||
size="sm"
|
||||
/>
|
||||
</p>
|
||||
<div v-if="!compact" class="banner-local-clock" aria-live="polite">
|
||||
<span class="banner-local-clock__label">{{ t('workplace.clock.local') }}</span>
|
||||
@ -43,18 +35,15 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue';
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useUserStore } from '@/store';
|
||||
import { useRbacUserStore } from '@/store/modules/rbac-user';
|
||||
import { resolveCurrentCustomer, peekCachedCurrentCustomer } from '@/api/customer/resolve';
|
||||
import { CUSTOMER_BALANCE_CHANGED_EVENT } from '@/api/customer/accountFlow';
|
||||
import { getMemberOverview } from '@/api/marketing/portal';
|
||||
import { getMapIdFromAccessToken } from '@/utils/jwt';
|
||||
import { isValidAvatarUrl, resolveAvatarUrl } from '@/utils/avatar';
|
||||
import { pickMemberLevelFromOverview } from '@/utils/memberLevelIcon';
|
||||
import MemberLevelBadge from '@/components/MemberLevelBadge/index.vue';
|
||||
|
||||
defineProps<{
|
||||
/** 员工工作台:一行问候,避免与关照重复 */
|
||||
@ -72,7 +61,6 @@
|
||||
const customerBalance = ref<number | null>(null);
|
||||
const balanceLoading = ref(false);
|
||||
const wechatAvatarRaw = ref('');
|
||||
const memberLevel = reactive({ code: '', name: '', iconUrl: '' });
|
||||
const now = ref(new Date());
|
||||
let timerId: number | undefined;
|
||||
|
||||
@ -141,31 +129,10 @@
|
||||
wechatAvatarRaw.value = isValidAvatarUrl(next) ? String(next).trim() : '';
|
||||
}
|
||||
|
||||
function applyMemberLevel(overview: Record<string, unknown> | null) {
|
||||
const next = pickMemberLevelFromOverview(overview);
|
||||
memberLevel.code = next.code;
|
||||
memberLevel.name = next.name;
|
||||
memberLevel.iconUrl = next.iconUrl;
|
||||
}
|
||||
|
||||
async function loadMemberLevel() {
|
||||
if (!rbacUserStore.isCustomerPortal() || !rbacUserStore.isLoggedIn) {
|
||||
applyMemberLevel(null);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const overview = (await getMemberOverview()) as Record<string, unknown>;
|
||||
applyMemberLevel(overview);
|
||||
} catch {
|
||||
applyMemberLevel(null);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCustomerProfile() {
|
||||
if (!rbacUserStore.isCustomerPortal() || !rbacUserStore.isLoggedIn) {
|
||||
customerBalance.value = null;
|
||||
wechatAvatarRaw.value = '';
|
||||
applyMemberLevel(null);
|
||||
return;
|
||||
}
|
||||
const mapId = getMapIdFromAccessToken(rbacUserStore.accessToken);
|
||||
@ -181,7 +148,6 @@
|
||||
}
|
||||
|
||||
balanceLoading.value = true;
|
||||
void loadMemberLevel();
|
||||
try {
|
||||
const data = await resolveCurrentCustomer({ mapId, account, force: true });
|
||||
applyWechatAvatar(data);
|
||||
@ -283,17 +249,12 @@
|
||||
|
||||
.banner-hello__line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.banner-hello__level {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.banner-hello__greet {
|
||||
flex-shrink: 0;
|
||||
color: var(--color-text-3);
|
||||
|
||||
@ -79,28 +79,7 @@
|
||||
:default-span="12"
|
||||
:label-col-props="{ span: 24 }"
|
||||
:wrapper-col-props="{ span: 24 }"
|
||||
>
|
||||
<template #validType="{ model }">
|
||||
<div class="valid-type-options" role="radiogroup">
|
||||
<button
|
||||
v-for="opt in validTypeOptions"
|
||||
:key="opt.value"
|
||||
type="button"
|
||||
class="valid-type-option"
|
||||
role="radio"
|
||||
:aria-checked="Number(model.validType) === opt.value"
|
||||
:class="{ 'is-active': Number(model.validType) === opt.value }"
|
||||
@click="model.validType = opt.value"
|
||||
>
|
||||
<span class="valid-type-option__radio" aria-hidden="true" />
|
||||
<span class="valid-type-option__text">
|
||||
<span class="valid-type-option__title">{{ opt.label }}</span>
|
||||
<span class="valid-type-option__desc">{{ opt.desc }}</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</FormPro>
|
||||
/>
|
||||
</div>
|
||||
</ModalPro>
|
||||
</MarketingPageShell>
|
||||
@ -203,24 +182,6 @@ const grantChannelOptions = computed(() => [
|
||||
{ label: t('marketing.couponTemplate.grantChannel.festival'), value: 'festival' },
|
||||
])
|
||||
|
||||
const validTypeOptions = computed(() => [
|
||||
{
|
||||
value: 1,
|
||||
label: t('marketing.couponTemplate.validType.fixed'),
|
||||
desc: t('marketing.couponTemplate.validType.fixedDesc'),
|
||||
},
|
||||
{
|
||||
value: 2,
|
||||
label: t('marketing.couponTemplate.validType.afterReceive'),
|
||||
desc: t('marketing.couponTemplate.validType.afterReceiveDesc'),
|
||||
},
|
||||
{
|
||||
value: 3,
|
||||
label: t('marketing.couponTemplate.validType.afterUnlock'),
|
||||
desc: t('marketing.couponTemplate.validType.afterUnlockDesc'),
|
||||
},
|
||||
])
|
||||
|
||||
const itemList = computed(() => [
|
||||
{
|
||||
field: 'templateCode',
|
||||
@ -322,11 +283,17 @@ const itemList = computed(() => [
|
||||
{
|
||||
field: 'validType',
|
||||
label: t('marketing.couponTemplate.form.validType'),
|
||||
comp: 'slot',
|
||||
span: 24,
|
||||
comp: 'a-radio-group',
|
||||
required: true,
|
||||
rules: [{ required: true, message: t('marketing.couponTemplate.form.validType') }],
|
||||
compProps: {},
|
||||
compProps: {
|
||||
type: 'button',
|
||||
options: [
|
||||
{ label: t('marketing.couponTemplate.validType.fixed'), value: 1 },
|
||||
{ label: t('marketing.couponTemplate.validType.afterReceive'), value: 2 },
|
||||
{ label: t('marketing.couponTemplate.validType.afterUnlock'), value: 3 },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'validStartTime',
|
||||
@ -700,91 +667,15 @@ onMounted(() => loadList())
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.valid-type-options {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
:deep(.arco-radio-group-button) {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
||||
.valid-type-option {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
padding: 12px 12px;
|
||||
border: 1px solid var(--color-border-2);
|
||||
border-radius: 10px;
|
||||
background: var(--app-card-bg);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.18s ease,
|
||||
background 0.18s ease,
|
||||
box-shadow 0.18s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: color-mix(in srgb, var(--app-primary-color) 40%, var(--color-border-2));
|
||||
background: color-mix(in srgb, var(--app-primary-color) 4%, var(--app-card-bg));
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
border-color: color-mix(in srgb, var(--app-primary-color) 55%, var(--color-border-2));
|
||||
background: color-mix(in srgb, var(--app-primary-color) 8%, var(--app-card-bg));
|
||||
box-shadow: 0 4px 12px color-mix(in srgb, var(--app-primary-color) 12%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.valid-type-option__radio {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: 2px;
|
||||
border: 1.5px solid var(--color-border-3, #c9cdd4);
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
|
||||
.is-active & {
|
||||
border-color: var(--app-primary-color);
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 3px;
|
||||
border-radius: 50%;
|
||||
background: var(--app-primary-color);
|
||||
.arco-radio-button {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.valid-type-option__text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.valid-type-option__title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.valid-type-option__desc {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.valid-type-options {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -52,41 +52,70 @@
|
||||
|
||||
<a-drawer
|
||||
v-model:visible="drawerVisible"
|
||||
:width="drawerWidth"
|
||||
:width="560"
|
||||
unmount-on-close
|
||||
:esc-to-close="true"
|
||||
:mask-closable="!drawerLoading"
|
||||
:header="false"
|
||||
:footer="false"
|
||||
class="member-level-drawer"
|
||||
:drawer-style="drawerShellStyle"
|
||||
:body-style="drawerBodyStyle"
|
||||
:footer="false"
|
||||
@cancel="handleDrawerCancel"
|
||||
>
|
||||
<div class="ml-drawer">
|
||||
<header class="ml-drawer__bar">
|
||||
<div class="ml-drawer__bar-main">
|
||||
<h2 class="ml-drawer__title">
|
||||
{{
|
||||
editingId
|
||||
? t('marketing.memberLevel.modal.edit')
|
||||
: t('marketing.memberLevel.modal.add')
|
||||
}}
|
||||
</h2>
|
||||
<span class="ml-drawer__chip">{{ t('menu.marketing.memberLevel') }}</span>
|
||||
<header class="ml-drawer__header">
|
||||
<div class="ml-drawer__title-row">
|
||||
<div>
|
||||
<p class="ml-drawer__eyebrow">{{ t('menu.marketing.memberLevel') }}</p>
|
||||
<h3 class="ml-drawer__title">
|
||||
{{
|
||||
editingId
|
||||
? t('marketing.memberLevel.modal.edit')
|
||||
: t('marketing.memberLevel.modal.add')
|
||||
}}
|
||||
</h3>
|
||||
</div>
|
||||
<button type="button" class="ml-drawer__close" @click="handleDrawerCancel">
|
||||
<icon-close />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-drawer__close"
|
||||
:aria-label="t('common.cancel')"
|
||||
:disabled="drawerLoading"
|
||||
@click="handleDrawerCancel"
|
||||
>
|
||||
<icon-close />
|
||||
</button>
|
||||
<p class="ml-drawer__desc">{{ t('marketing.memberLevel.modalTip') }}</p>
|
||||
</header>
|
||||
|
||||
<div class="ml-drawer__body">
|
||||
<section class="ml-preview" :class="{ 'is-disabled': formState.status === 1 }">
|
||||
<div class="ml-preview__badge">
|
||||
<span class="ml-preview__dot" />
|
||||
{{
|
||||
formState.status === 2
|
||||
? t('common.enabled')
|
||||
: t('common.disabled')
|
||||
}}
|
||||
</div>
|
||||
<div class="ml-preview__main">
|
||||
<div class="ml-preview__icon" aria-hidden="true">
|
||||
<img v-if="formState.iconUrl" :src="formState.iconUrl" alt="" />
|
||||
<span v-else>{{ previewInitial }}</span>
|
||||
</div>
|
||||
<div class="ml-preview__meta">
|
||||
<h4 class="ml-preview__name">
|
||||
{{ formState.levelName || t('marketing.memberLevel.preview.nameFallback') }}
|
||||
</h4>
|
||||
<p class="ml-preview__code">
|
||||
{{ formState.levelCode || t('marketing.memberLevel.preview.codeFallback') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-preview__stats">
|
||||
<div class="ml-preview__stat">
|
||||
<span class="ml-preview__stat-label">{{ t('marketing.memberLevel.form.minPoints') }}</span>
|
||||
<span class="ml-preview__stat-value">{{ formState.minPoints ?? 0 }}</span>
|
||||
</div>
|
||||
<div class="ml-preview__stat">
|
||||
<span class="ml-preview__stat-label">{{ t('marketing.memberLevel.form.extraDiscountRate') }}</span>
|
||||
<span class="ml-preview__stat-value">{{ previewDiscountText }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<a-form
|
||||
ref="formRef"
|
||||
class="ml-form"
|
||||
@ -96,166 +125,148 @@
|
||||
:label-col-props="{ span: 24 }"
|
||||
:wrapper-col-props="{ span: 24 }"
|
||||
>
|
||||
<section class="ml-card">
|
||||
<div class="ml-card__head">
|
||||
<h4 class="ml-card__title">{{ t('marketing.memberLevel.section.basic') }}</h4>
|
||||
<span class="ml-card__hint">{{ t('marketing.memberLevel.section.basicHint') }}</span>
|
||||
</div>
|
||||
<div class="ml-card__body">
|
||||
<a-row :gutter="[16, 0]">
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
:label="t('marketing.memberLevel.form.levelCode')"
|
||||
field="levelCode"
|
||||
:tooltip="t('marketing.memberLevel.form.levelCodeExtra')"
|
||||
>
|
||||
<a-input
|
||||
v-model="formState.levelCode"
|
||||
:placeholder="t('marketing.memberLevel.form.levelCodePlaceholder')"
|
||||
:max-length="32"
|
||||
:disabled="!!editingId"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('marketing.memberLevel.form.levelName')" field="levelName">
|
||||
<a-input
|
||||
v-model="formState.levelName"
|
||||
:placeholder="t('marketing.memberLevel.form.levelNamePlaceholder')"
|
||||
:max-length="64"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('marketing.memberLevel.form.status')" field="status">
|
||||
<div class="ml-status-chips" role="group">
|
||||
<button
|
||||
type="button"
|
||||
class="ml-status-chip"
|
||||
:class="{ 'is-active': formState.status === 2 }"
|
||||
@click="formState.status = 2"
|
||||
>
|
||||
<span class="ml-status-chip__dot" />
|
||||
{{ t('common.enabled') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="ml-status-chip"
|
||||
:class="{ 'is-active': formState.status === 1 }"
|
||||
@click="formState.status = 1"
|
||||
>
|
||||
<span class="ml-status-chip__dot" />
|
||||
{{ t('common.disabled') }}
|
||||
</button>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
:label="t('marketing.memberLevel.form.sort')"
|
||||
field="sort"
|
||||
:tooltip="t('marketing.memberLevel.form.sortExtra')"
|
||||
>
|
||||
<a-input-number
|
||||
v-model="formState.sort"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
style="width: 100%"
|
||||
placeholder="0"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="ml-card">
|
||||
<div class="ml-card__head">
|
||||
<h4 class="ml-card__title">{{ t('marketing.memberLevel.section.pricing') }}</h4>
|
||||
<span class="ml-card__hint">{{ t('marketing.memberLevel.section.pricingHint') }}</span>
|
||||
</div>
|
||||
<div class="ml-card__body">
|
||||
<a-row :gutter="[16, 0]">
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
:label="t('marketing.memberLevel.form.minPoints')"
|
||||
field="minPoints"
|
||||
:tooltip="t('marketing.memberLevel.form.minPointsExtra')"
|
||||
>
|
||||
<a-input-number
|
||||
v-model="formState.minPoints"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
style="width: 100%"
|
||||
placeholder="0"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
:label="t('marketing.memberLevel.form.extraDiscountRate')"
|
||||
field="extraDiscountRate"
|
||||
:tooltip="t('marketing.memberLevel.form.extraDiscountRateExtra')"
|
||||
>
|
||||
<div class="ml-discount-field">
|
||||
<a-input-number
|
||||
v-model="formState.extraDiscountRate"
|
||||
:min="0"
|
||||
:max="0.05"
|
||||
:step="0.01"
|
||||
:precision="4"
|
||||
style="width: 100%"
|
||||
placeholder="0"
|
||||
/>
|
||||
<span class="ml-discount-badge">{{ previewDiscountText }}</span>
|
||||
</div>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<div class="ml-switch-row">
|
||||
<div class="ml-switch-row__text">
|
||||
<div class="ml-switch-row__label">
|
||||
{{ t('marketing.memberLevel.form.canDowngrade') }}
|
||||
</div>
|
||||
<p class="ml-switch-row__hint">
|
||||
{{ t('marketing.memberLevel.form.canDowngradeExtra') }}
|
||||
</p>
|
||||
</div>
|
||||
<a-switch v-model="formState.canDowngrade" />
|
||||
<section class="ml-section">
|
||||
<div class="ml-section__head">
|
||||
<span class="ml-section__index">01</span>
|
||||
<div>
|
||||
<h5 class="ml-section__title">{{ t('marketing.memberLevel.section.basic') }}</h5>
|
||||
<p class="ml-section__hint">{{ t('marketing.memberLevel.section.basicHint') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
:label="t('marketing.memberLevel.form.levelCode')"
|
||||
field="levelCode"
|
||||
:tooltip="t('marketing.memberLevel.form.levelCodeExtra')"
|
||||
>
|
||||
<a-input
|
||||
v-model="formState.levelCode"
|
||||
:placeholder="t('marketing.memberLevel.form.levelCodePlaceholder')"
|
||||
:max-length="32"
|
||||
:disabled="!!editingId"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('marketing.memberLevel.form.levelName')" field="levelName">
|
||||
<a-input
|
||||
v-model="formState.levelName"
|
||||
:placeholder="t('marketing.memberLevel.form.levelNamePlaceholder')"
|
||||
:max-length="64"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('marketing.memberLevel.form.status')" field="status">
|
||||
<a-radio-group v-model="formState.status" type="button" class="ml-status-group">
|
||||
<a-radio :value="2">{{ t('common.enabled') }}</a-radio>
|
||||
<a-radio :value="1">{{ t('common.disabled') }}</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
:label="t('marketing.memberLevel.form.sort')"
|
||||
field="sort"
|
||||
:tooltip="t('marketing.memberLevel.form.sortExtra')"
|
||||
>
|
||||
<a-input-number
|
||||
v-model="formState.sort"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
style="width: 100%"
|
||||
placeholder="0"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</section>
|
||||
|
||||
<section class="ml-card">
|
||||
<div class="ml-card__head">
|
||||
<h4 class="ml-card__title">{{ t('marketing.memberLevel.section.display') }}</h4>
|
||||
<span class="ml-card__hint">{{ t('marketing.memberLevel.section.displayHint') }}</span>
|
||||
<section class="ml-section">
|
||||
<div class="ml-section__head">
|
||||
<span class="ml-section__index">02</span>
|
||||
<div>
|
||||
<h5 class="ml-section__title">{{ t('marketing.memberLevel.section.pricing') }}</h5>
|
||||
<p class="ml-section__hint">{{ t('marketing.memberLevel.section.pricingHint') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-card__body">
|
||||
<a-form-item
|
||||
:label="t('marketing.memberLevel.form.iconUrl')"
|
||||
field="iconUrl"
|
||||
:tooltip="t('marketing.memberLevel.form.iconUrlExtra')"
|
||||
>
|
||||
<a-input
|
||||
v-model="formState.iconUrl"
|
||||
:placeholder="t('marketing.memberLevel.form.iconUrlPlaceholder')"
|
||||
:max-length="256"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('marketing.memberLevel.form.benefitDesc')" field="benefitDesc">
|
||||
<a-textarea
|
||||
v-model="formState.benefitDesc"
|
||||
:auto-size="{ minRows: 3, maxRows: 5 }"
|
||||
:max-length="500"
|
||||
show-word-limit
|
||||
:placeholder="t('marketing.memberLevel.form.benefitDescPlaceholder')"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
:label="t('marketing.memberLevel.form.minPoints')"
|
||||
field="minPoints"
|
||||
:tooltip="t('marketing.memberLevel.form.minPointsExtra')"
|
||||
>
|
||||
<a-input-number
|
||||
v-model="formState.minPoints"
|
||||
:min="0"
|
||||
:precision="0"
|
||||
style="width: 100%"
|
||||
placeholder="0"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
:label="t('marketing.memberLevel.form.extraDiscountRate')"
|
||||
field="extraDiscountRate"
|
||||
:tooltip="t('marketing.memberLevel.form.extraDiscountRateExtra')"
|
||||
>
|
||||
<a-input-number
|
||||
v-model="formState.extraDiscountRate"
|
||||
:min="0"
|
||||
:max="0.05"
|
||||
:step="0.01"
|
||||
:precision="4"
|
||||
style="width: 100%"
|
||||
placeholder="0"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
:label="t('marketing.memberLevel.form.canDowngrade')"
|
||||
field="canDowngrade"
|
||||
:tooltip="t('marketing.memberLevel.form.canDowngradeExtra')"
|
||||
>
|
||||
<a-switch v-model="formState.canDowngrade" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</section>
|
||||
|
||||
<section class="ml-section">
|
||||
<div class="ml-section__head">
|
||||
<span class="ml-section__index">03</span>
|
||||
<div>
|
||||
<h5 class="ml-section__title">{{ t('marketing.memberLevel.section.display') }}</h5>
|
||||
<p class="ml-section__hint">{{ t('marketing.memberLevel.section.displayHint') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<a-form-item
|
||||
:label="t('marketing.memberLevel.form.iconUrl')"
|
||||
field="iconUrl"
|
||||
:tooltip="t('marketing.memberLevel.form.iconUrlExtra')"
|
||||
>
|
||||
<a-input
|
||||
v-model="formState.iconUrl"
|
||||
:placeholder="t('marketing.memberLevel.form.iconUrlPlaceholder')"
|
||||
:max-length="256"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('marketing.memberLevel.form.benefitDesc')" field="benefitDesc">
|
||||
<a-textarea
|
||||
v-model="formState.benefitDesc"
|
||||
:auto-size="{ minRows: 3, maxRows: 5 }"
|
||||
:max-length="500"
|
||||
show-word-limit
|
||||
:placeholder="t('marketing.memberLevel.form.benefitDescPlaceholder')"
|
||||
/>
|
||||
</a-form-item>
|
||||
</section>
|
||||
</a-form>
|
||||
</div>
|
||||
@ -288,7 +299,6 @@ import {
|
||||
getMemberLevelList,
|
||||
updateMemberLevel,
|
||||
} from '@/api/marketing/memberLevel'
|
||||
import { renderMemberLevelCellHtml } from '@/utils/memberLevelIcon'
|
||||
import MarketingPageShell from '../components/MarketingPageShell.vue'
|
||||
import MarketingPageTipIcon from '../components/MarketingPageTipIcon.vue'
|
||||
|
||||
@ -346,25 +356,14 @@ const formRules = computed(() => ({
|
||||
status: [{ required: true, message: t('marketing.memberLevel.form.status') }],
|
||||
}))
|
||||
|
||||
const previewDiscountText = computed(() => formatFreightOff(formState.extraDiscountRate))
|
||||
|
||||
const drawerWidth = computed(() => {
|
||||
if (typeof window !== 'undefined' && window.innerWidth < 768) return '100%'
|
||||
return 640
|
||||
const previewInitial = computed(() => {
|
||||
const name = formState.levelName?.trim()
|
||||
if (name) return name.slice(0, 1)
|
||||
const code = formState.levelCode?.trim()
|
||||
return code ? code.slice(0, 1).toUpperCase() : 'M'
|
||||
})
|
||||
|
||||
const drawerShellStyle = {
|
||||
borderRadius: '16px 0 0 16px',
|
||||
overflow: 'hidden',
|
||||
boxShadow: '-12px 0 40px rgba(15, 23, 42, 0.12)',
|
||||
}
|
||||
|
||||
const drawerBodyStyle = {
|
||||
padding: '0',
|
||||
background: 'var(--app-page-bg)',
|
||||
height: '100%',
|
||||
overflow: 'hidden',
|
||||
}
|
||||
const previewDiscountText = computed(() => formatFreightOff(formState.extraDiscountRate))
|
||||
|
||||
function formatFreightOff(rate: unknown) {
|
||||
const n = Number(rate || 0)
|
||||
@ -415,18 +414,7 @@ function mapRow(item: any): MemberLevelRow {
|
||||
|
||||
const hotColumns = computed(() => [
|
||||
{ key: 'levelCode', label: t('marketing.memberLevel.form.levelCode'), width: 100 },
|
||||
{
|
||||
key: 'levelName',
|
||||
label: t('marketing.memberLevel.form.levelName'),
|
||||
width: 160,
|
||||
renderer: (row: MemberLevelRow) =>
|
||||
renderMemberLevelCellHtml({
|
||||
levelCode: row.levelCode,
|
||||
levelName: row.levelName,
|
||||
iconUrl: row.iconUrl,
|
||||
size: 16,
|
||||
}),
|
||||
},
|
||||
{ key: 'levelName', label: t('marketing.memberLevel.form.levelName'), width: 120 },
|
||||
{ key: 'minPoints', label: t('marketing.memberLevel.form.minPoints'), width: 100 },
|
||||
{
|
||||
key: 'extraDiscountRate',
|
||||
@ -575,76 +563,67 @@ onMounted(() => loadList())
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
background: var(--app-page-bg);
|
||||
outline: none;
|
||||
margin: -16px;
|
||||
}
|
||||
|
||||
.ml-drawer__bar {
|
||||
.ml-drawer__header {
|
||||
flex-shrink: 0;
|
||||
padding: 20px 24px 16px;
|
||||
background:
|
||||
linear-gradient(
|
||||
135deg,
|
||||
color-mix(in srgb, var(--app-primary-color) 12%, var(--app-card-bg)) 0%,
|
||||
var(--app-card-bg) 58%
|
||||
);
|
||||
border-bottom: 1px solid var(--color-border-2, var(--color-border-2));
|
||||
}
|
||||
|
||||
.ml-drawer__title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-shrink: 0;
|
||||
margin: 12px 12px 0;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.78);
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(155deg, rgba(255, 255, 255, 0.92) 0%, rgba(255, 255, 255, 0.72) 100%);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.9),
|
||||
0 4px 14px rgba(0, 121, 121, 0.05);
|
||||
}
|
||||
|
||||
.ml-drawer__bar-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
.ml-drawer__eyebrow {
|
||||
margin: 0 0 4px;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.04em;
|
||||
color: var(--app-primary-color);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ml-drawer__title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: var(--color-text-1);
|
||||
line-height: 1.3;
|
||||
white-space: nowrap;
|
||||
font-size: 18px;
|
||||
line-height: 1.35;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1, #1d2129);
|
||||
}
|
||||
|
||||
.ml-drawer__chip {
|
||||
flex-shrink: 0;
|
||||
height: 24px;
|
||||
padding: 0 8px;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--app-primary-color) 10%, var(--app-card-bg));
|
||||
color: var(--app-primary-color);
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
.ml-drawer__desc {
|
||||
margin: 10px 0 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
color: var(--color-text-2, #4e5969);
|
||||
}
|
||||
|
||||
.ml-drawer__close {
|
||||
flex-shrink: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.8);
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
color: var(--color-text-2);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
color: var(--color-text-2, #4e5969);
|
||||
cursor: pointer;
|
||||
transition: color 0.2s, background 0.2s, border-color 0.2s;
|
||||
transition: background 0.2s ease, color 0.2s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
&:hover {
|
||||
background: color-mix(in srgb, var(--app-primary-color) 12%, var(--app-card-bg));
|
||||
color: var(--app-primary-color);
|
||||
border-color: color-mix(in srgb, var(--app-primary-color) 30%, var(--app-card-bg));
|
||||
background: color-mix(in srgb, var(--app-primary-color) 8%, var(--app-card-bg));
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
@ -652,44 +631,169 @@ onMounted(() => loadList())
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 10px 12px 16px;
|
||||
padding: 16px 24px 8px;
|
||||
}
|
||||
|
||||
.ml-card {
|
||||
.ml-preview {
|
||||
margin-bottom: 18px;
|
||||
padding: 16px;
|
||||
border-radius: 12px;
|
||||
background:
|
||||
radial-gradient(
|
||||
120% 140% at 0% 0%,
|
||||
color-mix(in srgb, #fff 28%, transparent),
|
||||
transparent 55%
|
||||
),
|
||||
linear-gradient(
|
||||
135deg,
|
||||
color-mix(in srgb, var(--app-primary-color) 92%, #102020) 0%,
|
||||
color-mix(in srgb, var(--app-primary-color) 62%, #1c3333) 100%
|
||||
);
|
||||
color: #fff;
|
||||
box-shadow: 0 10px 28px color-mix(in srgb, var(--app-primary-color) 28%, transparent);
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
&.is-disabled {
|
||||
opacity: 0.72;
|
||||
filter: grayscale(0.25);
|
||||
}
|
||||
}
|
||||
|
||||
.ml-preview__badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.78);
|
||||
border-radius: 14px;
|
||||
background: var(--app-card-bg);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.9),
|
||||
0 4px 14px rgba(0, 121, 121, 0.04);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ml-card__head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
padding: 14px 16px 0;
|
||||
}
|
||||
|
||||
.ml-card__title {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.ml-card__hint {
|
||||
padding: 2px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
line-height: 1.4;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
.ml-card__body {
|
||||
padding: 12px 16px 4px;
|
||||
.ml-preview__dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #00b42a;
|
||||
}
|
||||
|
||||
.ml-preview.is-disabled .ml-preview__dot {
|
||||
background: #c9cdd4;
|
||||
}
|
||||
|
||||
.ml-preview__main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.ml-preview__icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(255, 255, 255, 0.14);
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.ml-preview__name {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 650;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.ml-preview__code {
|
||||
margin: 4px 0 0;
|
||||
font-size: 12px;
|
||||
opacity: 0.72;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.ml-preview__stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ml-preview__stat {
|
||||
padding: 10px 10px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ml-preview__stat-label {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
opacity: 0.7;
|
||||
margin-bottom: 4px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ml-preview__stat-value {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ml-section {
|
||||
margin-bottom: 18px;
|
||||
padding: 14px 14px 4px;
|
||||
border-radius: 10px;
|
||||
background: var(--color-fill-2);
|
||||
border: 1px solid var(--color-border-2, var(--color-border-2));
|
||||
}
|
||||
|
||||
.ml-section__head {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.ml-section__index {
|
||||
flex-shrink: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: var(--app-primary-color);
|
||||
background: color-mix(in srgb, var(--app-primary-color) 14%, var(--app-card-bg));
|
||||
}
|
||||
|
||||
.ml-section__title {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1, #1d2129);
|
||||
}
|
||||
|
||||
.ml-section__hint {
|
||||
margin: 2px 0 0;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3, #86909c);
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.ml-form {
|
||||
@ -700,189 +804,48 @@ onMounted(() => loadList())
|
||||
:deep(.arco-form-item-label) {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
color: var(--color-text-1, #1d2129);
|
||||
}
|
||||
|
||||
:deep(.arco-input-wrapper),
|
||||
:deep(.arco-select-view-single),
|
||||
:deep(.arco-textarea-wrapper),
|
||||
:deep(.arco-input-number) {
|
||||
border-radius: 8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
.ml-status-chips {
|
||||
.ml-status-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ml-status-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
flex: 1;
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.78);
|
||||
border-radius: 999px;
|
||||
background: linear-gradient(155deg, rgba(255, 255, 255, 0.88) 0%, rgba(255, 255, 255, 0.62) 100%);
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.9);
|
||||
color: var(--color-text-2);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s, color 0.2s, background 0.2s, box-shadow 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: var(--app-primary-color);
|
||||
border-color: color-mix(in srgb, var(--app-primary-color) 35%, var(--app-card-bg));
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
color: var(--app-primary-color);
|
||||
border-color: color-mix(in srgb, var(--app-primary-color) 40%, var(--app-card-bg));
|
||||
background: color-mix(in srgb, var(--app-primary-color) 10%, var(--app-card-bg));
|
||||
font-weight: 600;
|
||||
box-shadow: 0 4px 12px rgba(0, 121, 121, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
.ml-status-chip__dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #c9cdd4;
|
||||
|
||||
.is-active & {
|
||||
background: var(--app-primary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.ml-discount-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
:deep(.arco-input-number) {
|
||||
:deep(.arco-radio-button) {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.ml-discount-badge {
|
||||
flex-shrink: 0;
|
||||
max-width: 88px;
|
||||
height: 28px;
|
||||
padding: 0 8px;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--app-primary-color) 10%, #fff);
|
||||
color: var(--app-primary-color);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 28px;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ml-switch-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
margin: 0 0 14px;
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid color-mix(in srgb, var(--app-primary-color) 14%, var(--color-border-2));
|
||||
background: color-mix(in srgb, var(--app-primary-color) 5%, var(--app-card-bg));
|
||||
}
|
||||
|
||||
.ml-switch-row__text {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ml-switch-row__label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.ml-switch-row__hint {
|
||||
margin: 4px 0 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.ml-drawer__footer {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin: 0 12px 12px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.78);
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(155deg, rgba(255, 255, 255, 0.92) 0%, rgba(255, 255, 255, 0.72) 100%);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.9),
|
||||
0 4px 14px rgba(0, 121, 121, 0.05);
|
||||
padding: 14px 24px 18px;
|
||||
border-top: 1px solid var(--color-border-2, var(--color-border-2));
|
||||
background: var(--app-card-bg);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
.member-level-drawer {
|
||||
.arco-drawer-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.arco-drawer-body {
|
||||
padding: 0 !important;
|
||||
padding: 16px;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.ml-level-cell {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
max-width: 100%;
|
||||
min-height: 24px;
|
||||
padding: 2px 8px 2px 4px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--ml-border);
|
||||
background: var(--ml-bg);
|
||||
color: var(--ml-color);
|
||||
vertical-align: middle;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.ml-level-glyph {
|
||||
flex-shrink: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
object-fit: contain;
|
||||
|
||||
svg {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.ml-level-glyph--fallback {
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.ml-level-name {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -27,9 +27,7 @@
|
||||
@delete-node="handleDeleteNode"
|
||||
>
|
||||
<template #node-second-row-extra="{ nodeData }">
|
||||
<span v-if="nodeData?.isBuiltIn" class="built-in-tag">{{
|
||||
t('system.dataPerm.templates.tag.builtin')
|
||||
}}</span>
|
||||
<span v-if="nodeData?.isBuiltIn" class="built-in-tag">内置</span>
|
||||
</template>
|
||||
</CustomLeftMenu>
|
||||
|
||||
@ -41,6 +39,7 @@
|
||||
:title="panelTitle"
|
||||
:subtitle="isCreating ? '' : form.code"
|
||||
:tags="templateTags"
|
||||
:stats="templateStats"
|
||||
:tabs="templateTabs"
|
||||
v-model:active-tab="activeTab"
|
||||
:loading="detailLoading"
|
||||
@ -50,28 +49,6 @@
|
||||
<icon-file />
|
||||
</div>
|
||||
</template>
|
||||
<template #header-extra>
|
||||
<ol class="tpl-flow">
|
||||
<li
|
||||
class="tpl-flow__item"
|
||||
:class="{ 'is-active': activeTab === 'basic', 'is-done': !!form.name && !!form.code }"
|
||||
>
|
||||
<span class="tpl-flow__num">1</span>
|
||||
<span class="tpl-flow__text">{{ t('system.dataPerm.templates.flow.step1') }}</span>
|
||||
</li>
|
||||
<li
|
||||
class="tpl-flow__item"
|
||||
:class="{ 'is-active': activeTab === 'items', 'is-done': form.items.length > 0 }"
|
||||
>
|
||||
<span class="tpl-flow__num">2</span>
|
||||
<span class="tpl-flow__text">{{ t('system.dataPerm.templates.flow.step2') }}</span>
|
||||
</li>
|
||||
<li class="tpl-flow__item" :class="{ 'is-done': canApply && !!bindForm.boundRoleId }">
|
||||
<span class="tpl-flow__num">3</span>
|
||||
<span class="tpl-flow__text">{{ t('system.dataPerm.templates.flow.step3') }}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</template>
|
||||
<template #actions>
|
||||
<a-button
|
||||
v-if="hasPermission('Rbac.DataPerm.Template')"
|
||||
@ -83,198 +60,138 @@
|
||||
</a-button>
|
||||
</template>
|
||||
|
||||
<div v-show="activeTab === 'basic'" class="tpl-pane">
|
||||
<section class="tpl-card">
|
||||
<div class="tpl-card__head">
|
||||
<div>
|
||||
<h3 class="tpl-card__title">{{ t('system.dataPerm.templates.section.identity') }}</h3>
|
||||
<p class="tpl-card__hint">{{ t('system.dataPerm.templates.section.identityHint') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tpl-card__body">
|
||||
<a-form :model="form" layout="vertical" class="template-form">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('system.dataPerm.templates.field.name')" required>
|
||||
<a-input
|
||||
v-model="form.name"
|
||||
:placeholder="t('system.dataPerm.templates.field.namePlaceholder')"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('system.dataPerm.templates.field.code')" required>
|
||||
<a-input
|
||||
v-model="form.code"
|
||||
:placeholder="t('system.dataPerm.templates.field.codePlaceholder')"
|
||||
:disabled="!isCreating && form.isBuiltIn"
|
||||
allow-clear
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="24">
|
||||
<a-form-item :label="t('system.dataPerm.templates.field.description')">
|
||||
<a-textarea
|
||||
v-model="form.description"
|
||||
:placeholder="t('system.dataPerm.templates.field.descriptionPlaceholder')"
|
||||
:auto-size="{ minRows: 2, maxRows: 4 }"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form>
|
||||
</div>
|
||||
</section>
|
||||
<div v-show="activeTab === 'basic'">
|
||||
<div class="section-card">
|
||||
<h3 class="section-card-title">{{ t('system.dataPerm.templates.tab.basic') }}</h3>
|
||||
<a-form :model="form" layout="vertical" class="template-form">
|
||||
<a-row :gutter="20">
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('system.dataPerm.templates.field.name')" required>
|
||||
<a-input v-model="form.name" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('system.dataPerm.templates.field.code')" required>
|
||||
<a-input v-model="form.code" :disabled="!isCreating && form.isBuiltIn" />
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-form-item :label="t('system.dataPerm.templates.field.description')">
|
||||
<a-textarea v-model="form.description" :auto-size="{ minRows: 2 }" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</div>
|
||||
|
||||
<section class="tpl-next-card">
|
||||
<div class="tpl-next-card__main">
|
||||
<p class="tpl-next-card__title">{{ t('system.dataPerm.templates.flow.goItemsTitle') }}</p>
|
||||
<p class="tpl-next-card__desc">{{ t('system.dataPerm.templates.flow.goItemsDesc') }}</p>
|
||||
</div>
|
||||
<a-button type="primary" @click="activeTab = 'items'">
|
||||
{{ t('system.dataPerm.templates.flow.goItemsBtn') }}
|
||||
<template v-if="form.items.length">({{ form.items.length }})</template>
|
||||
</a-button>
|
||||
</section>
|
||||
<div class="section-card">
|
||||
<h3 class="section-card-title">{{ t('system.dataPerm.templates.section.bindApply') }}</h3>
|
||||
<a-alert type="info" class="bind-alert">
|
||||
{{ t('system.dataPerm.templates.bindApplyHint') }}
|
||||
</a-alert>
|
||||
|
||||
<section class="tpl-card">
|
||||
<div class="tpl-card__head">
|
||||
<div>
|
||||
<h3 class="tpl-card__title">{{ t('system.dataPerm.templates.section.bindApply') }}</h3>
|
||||
<p class="tpl-card__hint">{{ t('system.dataPerm.templates.bindApplyHint') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tpl-card__body">
|
||||
<a-form :model="bindForm" layout="vertical" class="template-form">
|
||||
<a-form-item
|
||||
:label="t('system.dataPerm.templates.field.bindRole')"
|
||||
:extra="t('system.dataPerm.templates.field.bindRoleExtra')"
|
||||
<a-form :model="bindForm" layout="vertical" class="template-form">
|
||||
<a-form-item
|
||||
:label="t('system.dataPerm.templates.field.bindRole')"
|
||||
:extra="t('system.dataPerm.templates.field.bindRoleExtra')"
|
||||
>
|
||||
<a-select
|
||||
v-model="bindForm.boundRoleId"
|
||||
:options="roleOptions"
|
||||
:placeholder="t('system.dataPerm.templates.field.bindRolePlaceholder')"
|
||||
allow-clear
|
||||
allow-search
|
||||
:loading="rolesLoading"
|
||||
@change="onBoundRoleChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<template v-if="canApply">
|
||||
<a-divider orientation="left">{{ t('system.dataPerm.templates.section.applyNow') }}</a-divider>
|
||||
|
||||
<div class="apply-row">
|
||||
<div class="apply-row-text">
|
||||
<span class="apply-row-title">{{ t('system.dataPerm.templates.applyToRole') }}</span>
|
||||
<span class="apply-row-desc">{{ t('system.dataPerm.templates.applyToRoleDesc') }}</span>
|
||||
</div>
|
||||
<a-button
|
||||
:disabled="!bindForm.boundRoleId"
|
||||
:loading="roleApplyLoading"
|
||||
@click="applyToBoundRole"
|
||||
>
|
||||
<a-select
|
||||
v-model="bindForm.boundRoleId"
|
||||
:options="roleOptions"
|
||||
:placeholder="t('system.dataPerm.templates.field.bindRolePlaceholder')"
|
||||
allow-clear
|
||||
allow-search
|
||||
:loading="rolesLoading"
|
||||
@change="onBoundRoleChange"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{ t('system.dataPerm.templates.applyToRoleBtn') }}
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<template v-if="canApply">
|
||||
<div class="tpl-apply-grid">
|
||||
<div class="tpl-apply-block">
|
||||
<div class="tpl-apply-block__head">
|
||||
<span class="tpl-apply-block__badge">A</span>
|
||||
<div>
|
||||
<p class="tpl-apply-block__title">{{ t('system.dataPerm.templates.applyToRole') }}</p>
|
||||
<p class="tpl-apply-block__desc">{{ t('system.dataPerm.templates.applyToRoleDesc') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<a-button
|
||||
type="primary"
|
||||
:disabled="!bindForm.boundRoleId"
|
||||
:loading="roleApplyLoading"
|
||||
@click="applyToBoundRole"
|
||||
>
|
||||
{{ t('system.dataPerm.templates.applyToRoleBtn') }}
|
||||
</a-button>
|
||||
</div>
|
||||
<a-divider />
|
||||
|
||||
<div class="tpl-apply-block">
|
||||
<div class="tpl-apply-block__head">
|
||||
<span class="tpl-apply-block__badge">B</span>
|
||||
<div>
|
||||
<p class="tpl-apply-block__title">{{ t('system.dataPerm.templates.applyToUsers') }}</p>
|
||||
<p class="tpl-apply-block__desc">{{ t('system.dataPerm.templates.applyToUsersDesc') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<a-form :model="applyForm" layout="vertical" class="template-form">
|
||||
<a-form-item :label="t('system.dataPerm.templates.field.targetUsers')" required>
|
||||
<a-select
|
||||
v-model="applyForm.targetUserIds"
|
||||
:options="userOptions"
|
||||
multiple
|
||||
allow-search
|
||||
:placeholder="t('system.dataPerm.templates.field.targetUsersPlaceholder')"
|
||||
:loading="usersLoading"
|
||||
@search="searchUsers"
|
||||
<div class="apply-user-block">
|
||||
<p class="apply-block-title">{{ t('system.dataPerm.templates.applyToUsers') }}</p>
|
||||
<p class="apply-block-desc">{{ t('system.dataPerm.templates.applyToUsersDesc') }}</p>
|
||||
<a-form :model="applyForm" layout="vertical">
|
||||
<a-form-item :label="t('system.dataPerm.templates.field.targetUsers')" required>
|
||||
<a-select
|
||||
v-model="applyForm.targetUserIds"
|
||||
:options="userOptions"
|
||||
multiple
|
||||
allow-search
|
||||
:placeholder="t('system.dataPerm.templates.field.targetUsersPlaceholder')"
|
||||
:loading="usersLoading"
|
||||
@search="searchUsers"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('system.dataPerm.templates.field.applyMode')">
|
||||
<a-radio-group v-model="applyForm.mode">
|
||||
<a-radio value="replace">{{ t('system.dataPerm.templates.mode.replace') }}</a-radio>
|
||||
<a-radio value="merge">{{ t('system.dataPerm.templates.mode.merge') }}</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-checkbox v-model="applyForm.temporaryEnabled">
|
||||
{{ t('system.dataPerm.templates.field.temporary') }}
|
||||
</a-checkbox>
|
||||
</a-form-item>
|
||||
<a-row v-if="applyForm.temporaryEnabled" :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('system.dataPerm.templates.field.effectiveTime')">
|
||||
<a-date-picker
|
||||
v-model="applyForm.effectiveTime"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DDTHH:mm:ss[Z]"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item :label="t('system.dataPerm.templates.field.applyMode')">
|
||||
<div class="tpl-mode-chips" role="group">
|
||||
<button
|
||||
type="button"
|
||||
class="tpl-mode-chip"
|
||||
:class="{ 'is-active': applyForm.mode === 'replace' }"
|
||||
@click="applyForm.mode = 'replace'"
|
||||
>
|
||||
{{ t('system.dataPerm.templates.mode.replace') }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="tpl-mode-chip"
|
||||
:class="{ 'is-active': applyForm.mode === 'merge' }"
|
||||
@click="applyForm.mode = 'merge'"
|
||||
>
|
||||
{{ t('system.dataPerm.templates.mode.merge') }}
|
||||
</button>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('system.dataPerm.templates.field.expireTime')" required>
|
||||
<a-date-picker
|
||||
v-model="applyForm.expireTime"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DDTHH:mm:ss[Z]"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-checkbox v-model="applyForm.temporaryEnabled">
|
||||
{{ t('system.dataPerm.templates.field.temporary') }}
|
||||
</a-checkbox>
|
||||
</a-form-item>
|
||||
<a-row v-if="applyForm.temporaryEnabled" :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('system.dataPerm.templates.field.effectiveTime')">
|
||||
<a-date-picker
|
||||
v-model="applyForm.effectiveTime"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DDTHH:mm:ss[Z]"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item :label="t('system.dataPerm.templates.field.expireTime')" required>
|
||||
<a-date-picker
|
||||
v-model="applyForm.expireTime"
|
||||
show-time
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DDTHH:mm:ss[Z]"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-button type="primary" :loading="applyLoading" @click="applyToUsers">
|
||||
{{ t('system.dataPerm.templates.applyToUsersBtn') }}
|
||||
</a-button>
|
||||
</a-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="tpl-warn">
|
||||
{{ t('system.dataPerm.templates.applyNeedSave') }}
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-button type="primary" :loading="applyLoading" @click="applyToUsers">
|
||||
{{ t('system.dataPerm.templates.applyToUsersBtn') }}
|
||||
</a-button>
|
||||
</a-form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<a-alert v-else type="warning" class="bind-alert">
|
||||
{{ t('system.dataPerm.templates.applyNeedSave') }}
|
||||
</a-alert>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="activeTab === 'items'" class="tpl-pane">
|
||||
<div class="tpl-items-guide">
|
||||
<div class="tpl-items-guide__text">
|
||||
<p class="tpl-items-guide__title">{{ t('system.dataPerm.templates.itemsGuideTitle') }}</p>
|
||||
<p class="tpl-items-guide__desc">{{ t('system.dataPerm.templates.itemsHintForm') }}</p>
|
||||
</div>
|
||||
<div v-show="activeTab === 'items'">
|
||||
<div class="sticky-toolbar">
|
||||
<span class="items-hint">{{ t('system.dataPerm.templates.itemsHintForm') }}</span>
|
||||
<a-space wrap>
|
||||
<a-dropdown @select="applyExamplePreset">
|
||||
<a-button>
|
||||
<a-button type="outline">
|
||||
{{ t('system.dataPerm.templates.addExample') }}
|
||||
<icon-down />
|
||||
</a-button>
|
||||
@ -287,15 +204,13 @@
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
<a-button type="primary" @click="addItem">
|
||||
{{ t('system.dataPerm.templates.addItem') }}
|
||||
<a-button type="dashed" @click="addItem">{{ t('system.dataPerm.templates.addItem') }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<div v-if="form.items.length === 0" class="tpl-items-empty">
|
||||
<p class="tpl-items-empty__title">{{ t('system.dataPerm.templates.itemsEmpty') }}</p>
|
||||
<p class="tpl-items-empty__desc">{{ t('system.dataPerm.templates.itemsEmptyHint') }}</p>
|
||||
<div v-if="form.items.length === 0" class="items-empty section-card">
|
||||
<p>{{ t('system.dataPerm.templates.itemsEmpty') }}</p>
|
||||
<a-space>
|
||||
<a-button type="primary" @click="addItem">{{ t('system.dataPerm.templates.addItem') }}</a-button>
|
||||
<a-button @click="applyExamplePreset('salesStaff')">
|
||||
@ -304,26 +219,26 @@
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<div v-for="(item, idx) in form.items" :key="item._key" class="item-card">
|
||||
<div v-for="(item, idx) in form.items" :key="item._key" class="item-card section-card">
|
||||
<div class="item-card-head">
|
||||
<div class="item-card-head-left">
|
||||
<span class="item-index">{{ t('system.dataPerm.templates.itemIndex', { index: idx + 1 }) }}</span>
|
||||
<span class="item-index">#{{ idx + 1 }}</span>
|
||||
<a-select
|
||||
v-model="item.form.dimensionType"
|
||||
:options="dimensionOptions"
|
||||
style="width: 168px"
|
||||
style="width: 160px"
|
||||
:placeholder="t('system.dataPerm.templates.dimensionPlaceholder')"
|
||||
@change="(v: string) => onDimensionChange(item, v)"
|
||||
/>
|
||||
<a-tag :color="dimensionColor(item.form.dimensionType)" size="small">
|
||||
{{ dimensionLabel(item.form.dimensionType) }}
|
||||
</a-tag>
|
||||
</div>
|
||||
<a-button type="text" status="danger" size="mini" @click="form.items.splice(idx, 1)">
|
||||
{{ t('system.dataPerm.templates.removeItem') }}
|
||||
</a-button>
|
||||
</div>
|
||||
<div class="item-card-body">
|
||||
<p v-if="dimensionTip(item.form.dimensionType)" class="item-tip">
|
||||
{{ dimensionTip(item.form.dimensionType) }}
|
||||
</p>
|
||||
<TemplateItemForm
|
||||
v-model="item.form"
|
||||
:org-tree="orgTree"
|
||||
@ -345,7 +260,7 @@
|
||||
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import { IconFile, IconDown } from '@arco-design/web-vue/es/icon'
|
||||
import { IconFile, IconPlus, IconDown } from '@arco-design/web-vue/es/icon'
|
||||
import CustomLeftMenu from '@/components/CustomLeftMenu/index.vue'
|
||||
import RbacDetailShell from '@/components/RbacSplit/RbacDetailShell.vue'
|
||||
import type { RbacDetailTag } from '@/components/RbacSplit/RbacDetailShell.vue'
|
||||
@ -476,7 +391,7 @@ const templateTags = computed((): RbacDetailTag[] => {
|
||||
if (isCreating.value) {
|
||||
tags.push({ label: t('system.dataPerm.templates.tag.creating'), color: 'orangered' })
|
||||
} else if (form.isBuiltIn) {
|
||||
tags.push({ label: t('system.dataPerm.templates.tag.builtin'), color: 'gray' })
|
||||
tags.push({ label: t('system.dataPerm.templates.tag.builtin'), color: 'arcoblue' })
|
||||
}
|
||||
if (form.roleCode) {
|
||||
tags.push({ label: form.roleCode, color: 'purple' })
|
||||
@ -484,6 +399,14 @@ const templateTags = computed((): RbacDetailTag[] => {
|
||||
return tags
|
||||
})
|
||||
|
||||
const templateStats = computed(() => [
|
||||
{ label: t('system.dataPerm.templates.stat.items'), value: form.items.length },
|
||||
{
|
||||
label: t('system.dataPerm.templates.stat.dimensions'),
|
||||
value: new Set(form.items.map((i) => i.form.dimensionType)).size,
|
||||
},
|
||||
])
|
||||
|
||||
const templateTabs = computed(() => [
|
||||
{ key: 'basic', label: t('system.dataPerm.templates.tab.basic') },
|
||||
{
|
||||
@ -493,12 +416,6 @@ const templateTabs = computed(() => [
|
||||
},
|
||||
])
|
||||
|
||||
function dimensionTip(type: string) {
|
||||
const key = `system.dataPerm.templates.form.tip.${type}`
|
||||
const text = t(key)
|
||||
return text === key ? '' : text
|
||||
}
|
||||
|
||||
const templateTreeData = computed(() =>
|
||||
buildFlatLeftMenuTree('category-templates', t('system.dataPerm.templates.title'), dataSource.value, {
|
||||
titleKey: 'name',
|
||||
@ -524,6 +441,21 @@ function newItemRow(formModel?: TemplateItemFormModel): TemplateItemRow {
|
||||
}
|
||||
}
|
||||
|
||||
function dimensionLabel(type: string) {
|
||||
return dimensionOptions.value.find((o) => o.value === type)?.label || type
|
||||
}
|
||||
|
||||
function dimensionColor(type: string) {
|
||||
const map: Record<string, string> = {
|
||||
org: 'blue',
|
||||
data_scope: 'green',
|
||||
biz: 'orange',
|
||||
secret: 'red',
|
||||
filter: 'purple',
|
||||
}
|
||||
return map[type] || 'gray'
|
||||
}
|
||||
|
||||
function convertOrgTree(data: OrganizationTreeDto[]): typeof orgTree.value {
|
||||
return data.map((item) => ({
|
||||
key: item.id!,
|
||||
@ -888,298 +820,30 @@ onMounted(async () => {
|
||||
@import '@/components/RbacSplit/rbac-split-layout.less';
|
||||
|
||||
.built-in-tag {
|
||||
display: inline-block;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
font-size: 11px;
|
||||
font-weight: 400;
|
||||
line-height: 1.2;
|
||||
color: var(--color-text-3);
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
:deep(.node-second-row-extra-cell),
|
||||
:deep(.node-second-row-extra-slot) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tpl-flow {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px 16px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.tpl-flow__item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.tpl-flow__num {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-3);
|
||||
background: var(--color-fill-2);
|
||||
border: 1px solid var(--color-border-2);
|
||||
}
|
||||
|
||||
.tpl-flow__text {
|
||||
font-size: 12px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.tpl-flow__item.is-active {
|
||||
color: var(--color-text-1);
|
||||
|
||||
.tpl-flow__num {
|
||||
color: #fff;
|
||||
background: var(--app-primary-color, #1d7281);
|
||||
border-color: var(--app-primary-color, #1d7281);
|
||||
}
|
||||
|
||||
.tpl-flow__text {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.tpl-flow__item.is-done:not(.is-active) {
|
||||
color: var(--color-text-2);
|
||||
|
||||
.tpl-flow__num {
|
||||
color: var(--app-primary-color, #1d7281);
|
||||
background: color-mix(in srgb, var(--app-primary-color, #1d7281) 12%, #fff);
|
||||
border-color: color-mix(in srgb, var(--app-primary-color, #1d7281) 28%, #fff);
|
||||
}
|
||||
}
|
||||
|
||||
.tpl-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
max-width: 960px;
|
||||
}
|
||||
|
||||
.tpl-card {
|
||||
border: 1px solid var(--color-border-2);
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tpl-card__head {
|
||||
padding: 14px 18px 0;
|
||||
}
|
||||
|
||||
.tpl-card__title {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.tpl-card__hint {
|
||||
margin: 6px 0 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.tpl-card__body {
|
||||
padding: 12px 18px 16px;
|
||||
}
|
||||
|
||||
.tpl-next-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 14px 18px;
|
||||
border-radius: 10px;
|
||||
border: 1px dashed color-mix(in srgb, var(--app-primary-color, #1d7281) 35%, #fff);
|
||||
background: color-mix(in srgb, var(--app-primary-color, #1d7281) 6%, #fff);
|
||||
}
|
||||
|
||||
.tpl-next-card__main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tpl-next-card__title {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.tpl-next-card__desc {
|
||||
margin: 4px 0 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.tpl-apply-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.35fr;
|
||||
gap: 12px;
|
||||
margin-top: 4px;
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.tpl-apply-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
padding: 14px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--color-border-2);
|
||||
background: var(--color-fill-1);
|
||||
}
|
||||
|
||||
.tpl-apply-block__head {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.tpl-apply-block__badge {
|
||||
flex-shrink: 0;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 6px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: var(--app-primary-color, #1d7281);
|
||||
background: color-mix(in srgb, var(--app-primary-color, #1d7281) 12%, #fff);
|
||||
}
|
||||
|
||||
.tpl-apply-block__title {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.tpl-apply-block__desc {
|
||||
margin: 4px 0 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.tpl-mode-chips {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tpl-mode-chip {
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--color-border-2);
|
||||
background: #fff;
|
||||
color: var(--color-text-2);
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, color 0.15s, background 0.15s;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--app-primary-color, #1d7281);
|
||||
color: var(--app-primary-color, #1d7281);
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
border-color: var(--app-primary-color, #1d7281);
|
||||
color: var(--app-primary-color, #1d7281);
|
||||
background: color-mix(in srgb, var(--app-primary-color, #1d7281) 8%, #fff);
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.tpl-warn {
|
||||
margin-top: 4px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: #ad6800;
|
||||
background: #fff7e6;
|
||||
border: 1px solid #ffe58f;
|
||||
}
|
||||
|
||||
.tpl-items-guide {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--color-border-2);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.tpl-items-guide__title {
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.tpl-items-guide__desc {
|
||||
margin: 4px 0 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
color: var(--color-text-3);
|
||||
max-width: 560px;
|
||||
}
|
||||
|
||||
.tpl-items-empty {
|
||||
text-align: center;
|
||||
padding: 48px 24px;
|
||||
border-radius: 10px;
|
||||
border: 1px dashed var(--color-border-3);
|
||||
background: var(--color-fill-1);
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.tpl-items-empty__title {
|
||||
margin: 0 0 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-2);
|
||||
}
|
||||
|
||||
.tpl-items-empty__desc {
|
||||
margin: 0 0 18px;
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
font-size: 10px;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.template-form {
|
||||
max-width: 960px;
|
||||
}
|
||||
|
||||
.items-hint {
|
||||
font-size: 13px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.items-empty {
|
||||
text-align: center;
|
||||
padding: 40px 24px;
|
||||
color: var(--color-text-3);
|
||||
|
||||
p {
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.preset-option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -1198,12 +862,9 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.item-card {
|
||||
margin-bottom: 0;
|
||||
margin-bottom: 12px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--color-border-2);
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.item-card-head {
|
||||
@ -1229,28 +890,52 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.item-card-body {
|
||||
padding: 14px 18px 16px;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.item-tip {
|
||||
.bind-alert {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.apply-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 12px 0;
|
||||
}
|
||||
|
||||
.apply-row-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.apply-row-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.apply-row-desc {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.apply-user-block {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.apply-block-title {
|
||||
margin: 0 0 4px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.apply-block-desc {
|
||||
margin: 0 0 12px;
|
||||
padding: 8px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--color-text-2);
|
||||
background: var(--color-fill-1);
|
||||
border: 1px solid var(--color-border-2);
|
||||
}
|
||||
|
||||
:deep(.shell-tabs .t-tabs) {
|
||||
width: auto;
|
||||
max-width: none;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
:deep(.shell-tabs .t-tab) {
|
||||
padding: 0 10px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
<template>
|
||||
<div class="openapi-cred-panel">
|
||||
<a-spin :loading="loading" class="openapi-cred-spin">
|
||||
<a-alert type="info" class="openapi-cred-alert">
|
||||
{{ t('user.customer.openapi.tip') }}
|
||||
</a-alert>
|
||||
|
||||
<template v-if="!credential.hasCredential">
|
||||
<p class="openapi-cred-empty">{{ t('user.customer.openapi.empty') }}</p>
|
||||
<a-button
|
||||
@ -17,15 +21,9 @@
|
||||
<a-form-item :label="t('user.customer.openapi.appKey')">
|
||||
<a-input-group>
|
||||
<a-input :model-value="credential.appKey || ''" readonly />
|
||||
<button
|
||||
type="button"
|
||||
class="openapi-icon-btn"
|
||||
:title="t('user.customer.openapi.copy')"
|
||||
:aria-label="t('user.customer.openapi.copy')"
|
||||
@click="copyText(credential.appKey)"
|
||||
>
|
||||
<icon-copy />
|
||||
</button>
|
||||
<a-button @click="copyText(credential.appKey)">
|
||||
{{ t('user.customer.openapi.copy') }}
|
||||
</a-button>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
|
||||
@ -36,16 +34,12 @@
|
||||
readonly
|
||||
:type="credential.secretRevealed ? 'text' : 'password'"
|
||||
/>
|
||||
<button
|
||||
<a-button
|
||||
v-if="credential.secretRevealed"
|
||||
type="button"
|
||||
class="openapi-icon-btn"
|
||||
:title="t('user.customer.openapi.copy')"
|
||||
:aria-label="t('user.customer.openapi.copy')"
|
||||
@click="copyText(credential.appSecret)"
|
||||
>
|
||||
<icon-copy />
|
||||
</button>
|
||||
{{ t('user.customer.openapi.copy') }}
|
||||
</a-button>
|
||||
</a-input-group>
|
||||
<template #extra>
|
||||
<span v-if="credential.secretRevealed" class="openapi-cred-warn">
|
||||
@ -77,6 +71,12 @@
|
||||
</span>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<a-space>
|
||||
<a-button :loading="busy" @click="handleReset">
|
||||
{{ t('user.customer.openapi.resetSecret') }}
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-spin>
|
||||
</div>
|
||||
@ -86,7 +86,6 @@
|
||||
import { onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Message, Modal } from '@arco-design/web-vue'
|
||||
import { IconCopy } from '@arco-design/web-vue/es/icon'
|
||||
import type { CustomerOpenApiCredentialDto } from '@/api/customer'
|
||||
import { CustomerService } from '@/api/customer'
|
||||
import { useRbacUserStore } from '@/store/modules/rbac-user'
|
||||
@ -98,10 +97,6 @@ const props = defineProps<{
|
||||
customerId?: string | number | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
status: [payload: { hasCredential: boolean; busy: boolean }]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const loading = ref(false)
|
||||
const busy = ref(false)
|
||||
@ -121,13 +116,6 @@ function canCallMineApi() {
|
||||
return rbacUserStore.isCustomerPortal()
|
||||
}
|
||||
|
||||
function emitStatus() {
|
||||
emit('status', {
|
||||
hasCredential: Boolean(credential.hasCredential),
|
||||
busy: busy.value,
|
||||
})
|
||||
}
|
||||
|
||||
function applyCredential(data: CustomerOpenApiCredentialDto) {
|
||||
Object.assign(credential, {
|
||||
id: data.id,
|
||||
@ -142,7 +130,6 @@ function applyCredential(data: CustomerOpenApiCredentialDto) {
|
||||
createTime: data.createTime,
|
||||
updateTime: data.updateTime,
|
||||
})
|
||||
emitStatus()
|
||||
}
|
||||
|
||||
async function load() {
|
||||
@ -238,14 +225,11 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
watch(busy, () => emitStatus())
|
||||
|
||||
onMounted(() => {
|
||||
emitStatus()
|
||||
void load()
|
||||
})
|
||||
|
||||
defineExpose({ reload: load, resetSecret: handleReset })
|
||||
defineExpose({ reload: load })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@ -257,41 +241,17 @@ defineExpose({ reload: load, resetSecret: handleReset })
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.openapi-cred-alert {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.openapi-cred-empty {
|
||||
margin: 0 0 12px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.openapi-cred-form {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.openapi-icon-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--color-text-3);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s ease, background 0.15s ease;
|
||||
}
|
||||
|
||||
.openapi-icon-btn:hover:not(:disabled) {
|
||||
color: var(--app-primary-color);
|
||||
background: color-mix(in srgb, var(--app-primary-color) 8%, transparent);
|
||||
}
|
||||
|
||||
.openapi-icon-btn:disabled {
|
||||
opacity: 0.55;
|
||||
cursor: not-allowed;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.openapi-cred-warn {
|
||||
|
||||
@ -9,20 +9,6 @@
|
||||
<div class="level-main">
|
||||
<p class="level-eyebrow">{{ t('marketing.portal.member.currentLevel') }}</p>
|
||||
<div class="level-name-row">
|
||||
<img
|
||||
v-if="memberLevel.iconUrl"
|
||||
class="level-icon"
|
||||
:src="memberLevel.iconUrl"
|
||||
alt=""
|
||||
draggable="false"
|
||||
/>
|
||||
<span
|
||||
v-else-if="levelGlyphHtml"
|
||||
class="level-icon"
|
||||
:style="{ color: levelTone?.color }"
|
||||
aria-hidden="true"
|
||||
v-html="levelGlyphHtml"
|
||||
/>
|
||||
<h2 class="level-name">{{ levelName }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
@ -132,11 +118,6 @@ import { useRouter } from 'vue-router'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import PaginationPro from '@/components/PaginationPro/index.vue'
|
||||
import { getMemberOverview, getMemberPointsFlow } from '@/api/marketing/portal'
|
||||
import {
|
||||
getMemberLevelTone,
|
||||
pickMemberLevelFromOverview,
|
||||
renderMemberLevelGlyphSvg,
|
||||
} from '@/utils/memberLevelIcon'
|
||||
import PortalHowTo from '../components/PortalHowTo.vue'
|
||||
import {
|
||||
PORTAL_DRAWER_KEY,
|
||||
@ -179,15 +160,10 @@ const howSteps = computed(() => [
|
||||
},
|
||||
])
|
||||
|
||||
const memberLevel = computed(() => pickMemberLevelFromOverview(overview.value))
|
||||
const levelName = computed(
|
||||
() => memberLevel.value.name || t('marketing.portal.member.defaultLevel'),
|
||||
)
|
||||
const levelTone = computed(() =>
|
||||
getMemberLevelTone(memberLevel.value.code, levelName.value),
|
||||
)
|
||||
const levelGlyphHtml = computed(() =>
|
||||
levelTone.value ? renderMemberLevelGlyphSvg(levelTone.value.key, 22) : '',
|
||||
() =>
|
||||
String(overview.value?.memberLevelName ?? overview.value?.MemberLevelName ?? '') ||
|
||||
t('marketing.portal.member.defaultLevel'),
|
||||
)
|
||||
const rollingPoints = computed(() =>
|
||||
Number(
|
||||
@ -342,22 +318,6 @@ onMounted(async () => {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.level-icon {
|
||||
flex-shrink: 0;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
object-fit: contain;
|
||||
|
||||
:deep(svg) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.level-name {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
|
||||
@ -19,66 +19,29 @@
|
||||
<h2 class="identity-name">
|
||||
{{ profile.customerName || profile.wechatNickname || t('profile.defaultName') }}
|
||||
</h2>
|
||||
<MemberLevelBadge
|
||||
v-if="memberLevel.name"
|
||||
:level-code="memberLevel.code"
|
||||
:level-name="memberLevel.name"
|
||||
:icon-url="memberLevel.iconUrl"
|
||||
size="sm"
|
||||
/>
|
||||
<span v-else-if="customerGroupLabel" class="vip-badge">{{ customerGroupLabel }}</span>
|
||||
<span v-if="customerGroupLabel" class="vip-badge">{{ customerGroupLabel }}</span>
|
||||
</div>
|
||||
<button
|
||||
v-if="profile.customerCode"
|
||||
type="button"
|
||||
class="code-row"
|
||||
:title="profile.customerCode"
|
||||
@click="copyText(profile.customerCode)"
|
||||
>
|
||||
<p class="welcome-title">{{ greetingText }}</p>
|
||||
<div v-if="profile.customerCode" class="code-row">
|
||||
<span>{{ profile.customerCode }}</span>
|
||||
<icon-copy class="code-row__icon" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="profile-hero__tools">
|
||||
<button
|
||||
type="button"
|
||||
class="profile-icon-btn"
|
||||
:title="t('profile.edit')"
|
||||
:aria-label="t('profile.edit')"
|
||||
@click="openEditModal"
|
||||
>
|
||||
<icon-edit />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="profile-icon-btn"
|
||||
:title="t('profile.resetPassword')"
|
||||
:aria-label="t('profile.resetPassword')"
|
||||
@click="openResetPasswordModal"
|
||||
>
|
||||
<icon-lock />
|
||||
</button>
|
||||
<a-button type="text" size="mini" class="copy-btn" @click="copyText(profile.customerCode)">
|
||||
<icon-copy />
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="balance-focus"
|
||||
:class="{ 'is-low': isLowBalance }"
|
||||
:title="t('nav.balance.recharge')"
|
||||
@click="goRecharge"
|
||||
>
|
||||
<div class="balance-focus__main">
|
||||
<span class="balance-focus__label">{{ t('user.customer.fields.accountBalance') }}</span>
|
||||
<strong class="balance-focus__value">
|
||||
¥{{ formatMoney(profile.accountBalance ?? profile.balance) }}
|
||||
</strong>
|
||||
<span v-if="isLowBalance" class="balance-focus__tip">{{ t('profile.lowBalanceTip') }}</span>
|
||||
</div>
|
||||
<span class="profile-icon-btn profile-icon-btn--accent" aria-hidden="true">
|
||||
<icon-plus />
|
||||
</span>
|
||||
</button>
|
||||
<div class="metric-block metric-block--clickable" @click="goRecharge">
|
||||
<span class="metric-label">{{ t('user.customer.fields.accountBalance') }}</span>
|
||||
<span class="metric-value">¥{{ formatMoney(profile.accountBalance ?? profile.balance) }}</span>
|
||||
<a-button size="mini" type="outline" class="metric-action" @click.stop="goRecharge">
|
||||
{{ t('nav.balance.recharge') }}
|
||||
</a-button>
|
||||
<p v-if="isLowBalance" class="metric-tip">{{ t('profile.lowBalanceTip') }}</p>
|
||||
</div>
|
||||
<div class="identity-actions">
|
||||
<a-button size="small" type="primary" @click="openEditModal">{{ t('profile.edit') }}</a-button>
|
||||
<a-button size="small" type="outline" @click="openResetPasswordModal">{{ t('profile.resetPassword') }}</a-button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="profile-grid">
|
||||
@ -159,23 +122,9 @@
|
||||
<section v-if="showOpenApiCredential" class="profile-card profile-card--full">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">{{ t('user.customer.openapi.title') }}</h3>
|
||||
<button
|
||||
v-if="openapiHasCredential"
|
||||
type="button"
|
||||
class="profile-icon-btn"
|
||||
:disabled="openapiBusy"
|
||||
:title="t('user.customer.openapi.resetSecret')"
|
||||
:aria-label="t('user.customer.openapi.resetSecret')"
|
||||
@click="openapiPanelRef?.resetSecret()"
|
||||
>
|
||||
<icon-refresh />
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<CustomerOpenApiCredentialPanel
|
||||
ref="openapiPanelRef"
|
||||
@status="onOpenapiStatus"
|
||||
/>
|
||||
<CustomerOpenApiCredentialPanel />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@ -248,11 +197,7 @@ import { useI18n } from 'vue-i18n'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import {
|
||||
IconCopy,
|
||||
IconEdit,
|
||||
IconExclamationCircleFill,
|
||||
IconLock,
|
||||
IconPlus,
|
||||
IconRefresh,
|
||||
} from '@arco-design/web-vue/es/icon'
|
||||
import type { CustomerDto } from '@/api/customer'
|
||||
import { CustomerService } from '@/api/customer'
|
||||
@ -263,9 +208,6 @@ import { AuthService } from '@/api/rbac'
|
||||
import { resolveAvatarUrl } from '@/utils/avatar'
|
||||
import { getMapIdFromAccessToken } from '@/utils/jwt'
|
||||
import { resolvePostLogoutLogin } from '@/utils/login-portal'
|
||||
import { pickMemberLevelFromOverview } from '@/utils/memberLevelIcon'
|
||||
import { getMemberOverview } from '@/api/marketing/portal'
|
||||
import MemberLevelBadge from '@/components/MemberLevelBadge/index.vue'
|
||||
import {
|
||||
PORTAL_DRAWER_KEY,
|
||||
type PortalDrawerContext,
|
||||
@ -288,14 +230,6 @@ const rbacUserStore = useRbacUserStore()
|
||||
const profile = ref<CustomerDto>({})
|
||||
const loading = ref(true)
|
||||
const loadError = ref('')
|
||||
const openapiPanelRef = ref<{ resetSecret: () => void } | null>(null)
|
||||
const openapiHasCredential = ref(false)
|
||||
const openapiBusy = ref(false)
|
||||
|
||||
function onOpenapiStatus(payload: { hasCredential: boolean; busy: boolean }) {
|
||||
openapiHasCredential.value = payload.hasCredential
|
||||
openapiBusy.value = payload.busy
|
||||
}
|
||||
|
||||
const isCustomerPortal = computed(() => rbacUserStore.isCustomerPortal())
|
||||
|
||||
@ -329,6 +263,17 @@ const displayAvatar = computed(() =>
|
||||
resolveAvatarUrl(profile.value.wechatAvatarUrl ?? profile.value.wechatAvatar)
|
||||
)
|
||||
|
||||
const displayName = computed(
|
||||
() => profile.value.customerName || profile.value.wechatNickname || t('profile.defaultName')
|
||||
)
|
||||
|
||||
const greetingText = computed(() => {
|
||||
const hour = new Date().getHours()
|
||||
if (hour < 12) return t('profile.greetingMorning', { name: displayName.value })
|
||||
if (hour < 18) return t('profile.greetingAfternoon', { name: displayName.value })
|
||||
return t('profile.greetingEvening', { name: displayName.value })
|
||||
})
|
||||
|
||||
const customerGroupLabel = computed(() => {
|
||||
const g = profile.value.customerGroup
|
||||
if (g == null) return ''
|
||||
@ -337,8 +282,6 @@ const customerGroupLabel = computed(() => {
|
||||
return label !== key ? label : ''
|
||||
})
|
||||
|
||||
const memberLevel = reactive({ code: '', name: '', iconUrl: '' })
|
||||
|
||||
const memberSince = computed(() => profile.value.createdAt || profile.value.createTime || '')
|
||||
|
||||
const accountBalance = computed(() => Number(profile.value.accountBalance ?? profile.value.balance ?? 0))
|
||||
@ -490,20 +433,6 @@ async function handleResetPasswordSubmit() {
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMemberLevel() {
|
||||
try {
|
||||
const overview = (await getMemberOverview()) as Record<string, unknown>
|
||||
const next = pickMemberLevelFromOverview(overview)
|
||||
memberLevel.code = next.code
|
||||
memberLevel.name = next.name
|
||||
memberLevel.iconUrl = next.iconUrl
|
||||
} catch {
|
||||
memberLevel.code = ''
|
||||
memberLevel.name = ''
|
||||
memberLevel.iconUrl = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function loadProfile() {
|
||||
loading.value = true
|
||||
loadError.value = ''
|
||||
@ -515,7 +444,6 @@ async function loadProfile() {
|
||||
return
|
||||
}
|
||||
try {
|
||||
void loadMemberLevel()
|
||||
const data = await resolveCurrentCustomer({ mapId, account })
|
||||
if (!data) {
|
||||
throw new Error(t('profile.notFound'))
|
||||
@ -587,9 +515,9 @@ onMounted(() => {
|
||||
.profile-hero {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
gap: 14px;
|
||||
margin-bottom: 12px;
|
||||
padding: 14px 16px;
|
||||
padding: 16px;
|
||||
background: var(--app-card-bg);
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.78);
|
||||
@ -604,117 +532,13 @@ onMounted(() => {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.profile-hero__tools {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.profile-icon-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--color-text-3);
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s ease, background 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: var(--app-primary-color);
|
||||
background: color-mix(in srgb, var(--app-primary-color) 10%, transparent);
|
||||
}
|
||||
|
||||
&--accent {
|
||||
color: var(--app-primary-color);
|
||||
background: color-mix(in srgb, var(--app-primary-color) 12%, #fff);
|
||||
}
|
||||
}
|
||||
|
||||
.code-row {
|
||||
display: inline-flex;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin: 4px 0 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--app-primary-color);
|
||||
}
|
||||
|
||||
.code-row__icon {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.balance-focus {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 12px 14px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid color-mix(in srgb, var(--app-primary-color) 16%, var(--color-border-2));
|
||||
background: color-mix(in srgb, var(--app-primary-color) 7%, var(--app-card-bg));
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--app-primary-color);
|
||||
box-shadow: 0 2px 8px color-mix(in srgb, var(--app-primary-color) 14%, transparent);
|
||||
}
|
||||
|
||||
&.is-low {
|
||||
border-color: color-mix(in srgb, #ff7d00 35%, var(--color-border-2));
|
||||
background: color-mix(in srgb, #ff7d00 8%, var(--app-card-bg));
|
||||
}
|
||||
}
|
||||
|
||||
.balance-focus__main {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.balance-focus__label {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.balance-focus__value {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--color-text-1);
|
||||
}
|
||||
|
||||
.balance-focus.is-low .balance-focus__value {
|
||||
color: #d97808;
|
||||
}
|
||||
|
||||
.balance-focus__tip {
|
||||
margin-top: 2px;
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
color: #ff7d00;
|
||||
}
|
||||
|
||||
.profile-card {
|
||||
@ -731,10 +555,6 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 14px 16px;
|
||||
border-bottom: 1px solid var(--color-border-2);
|
||||
flex-shrink: 0;
|
||||
@ -767,12 +587,12 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.avatar-img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
border: 2px solid color-mix(in srgb, var(--app-primary-color) 18%, var(--app-card-bg));
|
||||
border: 3px solid color-mix(in srgb, var(--app-primary-color) 20%, var(--app-card-bg));
|
||||
}
|
||||
|
||||
.identity-brief {
|
||||
@ -780,6 +600,13 @@ onMounted(() => {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
margin: 4px 0 0;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--color-text-3);
|
||||
}
|
||||
|
||||
.identity-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -791,9 +618,8 @@ onMounted(() => {
|
||||
.identity-name {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
font-weight: 600;
|
||||
color: var(--color-text-1);
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
.identity-nickname {
|
||||
@ -816,6 +642,17 @@ onMounted(() => {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.identity-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: auto;
|
||||
padding-top: 4px;
|
||||
|
||||
:deep(.arco-btn) {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.account-body {
|
||||
gap: 12px;
|
||||
}
|
||||
@ -824,6 +661,20 @@ onMounted(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
|
||||
&--clickable {
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--app-primary-color) 8%, var(--app-card-bg));
|
||||
border: 1px solid color-mix(in srgb, var(--app-primary-color) 18%, var(--color-border-2));
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--app-primary-color);
|
||||
box-shadow: 0 2px 8px color-mix(in srgb, var(--app-primary-color) 16%, transparent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
@ -838,6 +689,11 @@ onMounted(() => {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.metric-action {
|
||||
align-self: flex-start;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.metric-tip {
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user