diff --git a/mpuniapp/pages/owner/profile/index.uvue b/mpuniapp/pages/owner/profile/index.uvue index 92be888..e0f6eed 100644 --- a/mpuniapp/pages/owner/profile/index.uvue +++ b/mpuniapp/pages/owner/profile/index.uvue @@ -1,76 +1,209 @@ @@ -152,8 +312,9 @@ import { resolveProfileText, previewAccountAvatar, removeAccountAvatar, + calculateQuotaPercent, } from './useProfile.uts'; -import { OmniLocale } from '../../../common/locale.uts'; +import { OmniLocale, isRtl } from '../../../common/locale.uts'; import { fetchCardQuota } from '../../../common/api.uts'; import { getTenantId, clearAuthToken } from '../../../common/request.uts'; @@ -165,6 +326,34 @@ type LangOption = { const state = reactive(loadProfileSettings()); const quotaLine = ref('--'); const quotaExceeded = ref(false); +const quotaPercent = ref(20); +const languageDrawerVisible = ref(false); + +const pageRtlClass = computed((): string => { + return isRtl(state.locale) ? 'page-rtl' : ''; +}); + +const currentLanguageLabel = computed((): string => { + for (let i = 0; i < languageList.length; i++) { + if (languageList[i].code === state.locale) { + return languageList[i].label; + } + } + return 'English'; +}); + +function openLanguageDrawer(): void { + languageDrawerVisible.value = true; +} + +function closeLanguageDrawer(): void { + languageDrawerVisible.value = false; +} + +function onSelectLocaleFromDrawer(code: OmniLocale): void { + onSelectLocale(code); + closeLanguageDrawer(); +} function onAvatarClick(): void { if (state.account.avatar !== '') { @@ -214,15 +403,64 @@ function onLeadNoticeChange(e: UniSwitchChangeEvent): void { toggleLeadNotice(state, e.detail.value); } -function onSelectLocale(code: OmniLocale): void { - setPreferredLocale(state, code); - saveProfileSettings(state); +const myCardsSubText = computed((): string => { + return state.locale === 'zh-CN' ? '查看名片与在线出示' : 'View & share'; +}); + +const cardEditSubText = computed((): string => { + return state.locale === 'zh-CN' ? '修改联系与社交矩阵' : 'Edit profile'; +}); + +const productCasesSubText = computed((): string => { + return state.locale === 'zh-CN' ? '画廊相册与量化成效' : 'Showcases & metrics'; +}); + +const articleDynamicsSubText = computed((): string => { + return state.locale === 'zh-CN' ? '发布行业博文与动态' : 'Publish insights'; +}); + +const sharePosterSubText = computed((): string => { + return state.locale === 'zh-CN' ? '展会专属扫码展台' : 'Digital QR poster'; +}); + +const exchangeHallSubText = computed((): string => { + return state.locale === 'zh-CN' ? '面对面扫码碰卡互换' : 'Peer exchange'; +}); + +function onEditProfileClick(): void { + openPage('/pages/owner/card/edit'); +} + +function onQuotaClick(): void { + const isZh = state.locale === 'zh-CN'; + uni.showModal({ + title: isZh ? '企业商业配额' : 'Commercial Quota', + content: isZh + ? `当前用量:${quotaLine.value} 张名片。\n如需扩容或升级企业旗舰版,请前往管理后台或联系您的企业管理员。` + : `Current Usage: ${quotaLine.value} cards.\nTo upgrade or expand quota, please visit the admin console.`, + showCancel: false, + confirmText: isZh ? '知道了' : 'OK', + }); } function openPage(url: string): void { - uni.navigateTo({ - url, - }); + const tabPages = [ + '/pages/portal/index', + '/pages/owner/card/index', + '/pages/owner/radar', + '/pages/owner/leads', + '/pages/owner/profile/index', + ]; + const isTab = tabPages.some((tab: string): boolean => url.startsWith(tab)); + if (isTab) { + uni.switchTab({ + url, + }); + } else { + uni.navigateTo({ + url, + }); + } } async function loadQuota(): Promise { @@ -231,9 +469,11 @@ async function loadQuota(): Promise { const quota = await fetchCardQuota(tenantId !== '' ? tenantId : 1); quotaLine.value = `${quota.used} / ${quota.limit}`; quotaExceeded.value = quota.exceeded === true; + quotaPercent.value = calculateQuotaPercent(quota.used, quota.limit); } catch (_e) { quotaLine.value = '1 / 5'; quotaExceeded.value = false; + quotaPercent.value = calculateQuotaPercent(1, 5); } } @@ -272,11 +512,25 @@ onShow(() => { diff --git a/mpuniapp/pages/owner/profile/useProfile.uts b/mpuniapp/pages/owner/profile/useProfile.uts index d2c65e9..bdd4f28 100644 --- a/mpuniapp/pages/owner/profile/useProfile.uts +++ b/mpuniapp/pages/owner/profile/useProfile.uts @@ -392,7 +392,7 @@ export function resolveProfileText(key: string): string { case 'tierBadge': return 'PRO'; case 'previewPortal': - return isZh ? '预览官网' : 'Preview Site'; + return isZh ? '预览微官网' : 'Preview site'; case 'metricsCards': return isZh ? '活跃名片' : 'Active Cards'; case 'metricsVisitors': @@ -416,7 +416,23 @@ export function resolveProfileText(key: string): string { case 'logout': return isZh ? '退出登录' : 'Log out'; case 'workspaceSection': - return isZh ? '出海工作台' : 'Workspace'; + return isZh ? '智能硬件与团队' : 'Hardware & Team'; + case 'cardStudioSection': + return isZh ? '名片与微官网工作室' : 'Digital Card Studio'; + case 'myCards': + return isZh ? '我的数字名片' : 'Digital Cards'; + case 'cardEdit': + return isZh ? '极速资料维护' : 'Quick Profile'; + case 'productCases': + return isZh ? '产品相册与案例' : 'Portfolio & Cases'; + case 'articleDynamics': + return isZh ? '文章与商务动态' : 'Insights & News'; + case 'sharePoster': + return isZh ? '分享海报' : 'Share Poster'; + case 'exchangeHall': + return isZh ? '互换大厅' : 'Exchange Hall'; + case 'editProfile': + return isZh ? '编辑资料' : 'Edit'; case 'nfcSection': return isZh ? 'NFC 智能硬件' : 'NFC Fleet'; case 'nfcScan': diff --git a/mpuniapp/tests/owner-profile.test.ts b/mpuniapp/tests/owner-profile.test.ts new file mode 100644 index 0000000..0b72d90 --- /dev/null +++ b/mpuniapp/tests/owner-profile.test.ts @@ -0,0 +1,54 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import path from 'node:path'; + +const MP_ROOT = path.resolve('m:/2026项目/CRMmp/mpuniapp'); +const profileUtsPath = path.join(MP_ROOT, 'pages/owner/profile/useProfile.uts'); +const profileUvuePath = path.join(MP_ROOT, 'pages/owner/profile/index.uvue'); + +test('1. useProfile.uts 必须包含名片工作室所有核心文案键', () => { + assert.ok(fs.existsSync(profileUtsPath), '必须存在 useProfile.uts'); + const content = fs.readFileSync(profileUtsPath, 'utf-8'); + + assert.ok(content.includes('cardStudioSection'), '必须包含 cardStudioSection'); + assert.ok(content.includes('myCards'), '必须包含 myCards'); + assert.ok(content.includes('cardEdit'), '必须包含 cardEdit'); + assert.ok(content.includes('productCases'), '必须包含 productCases'); + assert.ok(content.includes('articleDynamics'), '必须包含 articleDynamics'); + assert.ok(content.includes('sharePoster'), '必须包含 sharePoster'); + assert.ok(content.includes('exchangeHall'), '必须包含 exchangeHall'); + assert.ok(content.includes('editProfile'), '必须包含 editProfile'); +}); + +test('2. profile/index.uvue 零 Emoji 违规审查', () => { + const emojiRegex = /[\u{1F300}-\u{1F9FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/u; + const content = fs.readFileSync(profileUvuePath, 'utf-8'); + assert.equal(emojiRegex.test(content), false, 'profile/index.uvue 严禁含有 Emoji'); +}); + +test('3. profile/index.uvue 必须完整挂载数字名片工作室 6 大业务入口', () => { + const content = fs.readFileSync(profileUvuePath, 'utf-8'); + + assert.ok(content.includes('/pages/owner/card/index'), '必须包含我的名片列表入口'); + assert.ok(content.includes('/pages/owner/card/edit'), '必须包含名片极速维护入口'); + assert.ok(content.includes('/pages/owner/card/cases'), '必须包含产品相册与案例入口'); + assert.ok(content.includes('/pages/owner/card/articles'), '必须包含文章与商务动态入口'); + assert.ok(content.includes('/pages/owner/card/qrcode'), '必须包含分享海报入口'); + assert.ok(content.includes('/pages/owner/hall/index'), '必须包含名片互换大厅入口'); +}); + +test('4. openPage 跳转逻辑必须支持 TabBar 页面使用 uni.switchTab 防止死链', () => { + const content = fs.readFileSync(profileUvuePath, 'utf-8'); + + assert.ok(content.includes('uni.switchTab'), 'TabBar 页面必须调用 uni.switchTab 跳转'); + assert.ok(content.includes('uni.navigateTo'), '常规页面调用 uni.navigateTo 跳转'); + assert.ok(content.includes('/pages/owner/card/index'), 'TabBar 判断列表必须包含 /pages/owner/card/index'); +}); + +test('5. 个人资料顶部身份区域与配额进度条具备点击交互响应', () => { + const content = fs.readFileSync(profileUvuePath, 'utf-8'); + + assert.ok(content.includes('onEditProfileClick'), '顶部身份卡片必须具备 onEditProfileClick'); + assert.ok(content.includes('onQuotaClick'), '配额进度条必须具备 onQuotaClick 交互反馈'); +});