feat(mobile): adapt dark theme

This commit is contained in:
Clover You
2026-01-30 23:35:30 +08:00
committed by Dawn
parent ff1f9b892a
commit f6ebc7d87c
37 changed files with 1171 additions and 1082 deletions

2
public/icon.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -49,6 +49,7 @@
"barcode-scanner:allow-vibrate",
"mic-recorder:allow-start-recording",
"mic-recorder:allow-stop-recording",
"core:app:allow-set-app-theme",
{
"identifier": "shell:allow-execute",
"allow": [

File diff suppressed because one or more lines are too long

View File

@@ -628,6 +628,7 @@ onMounted(() => {
isDesktop() && import('@/styles/scss/global/desktop.scss')
// 判断是否是移动端,移动端需要加载安全区域适配样式
isMobile() && import('@/styles/scss/global/mobile.scss')
import(`@/styles/scss/theme/${themes.value.versatile}.scss`)
if (!settingStore.themes.content) {
// 首次运行使用跟随系统,保持既有体验
@@ -734,6 +735,8 @@ watch(
watch(
() => themes.value.versatile,
async (val, oldVal) => {
console.log(val)
await import(`@/styles/scss/theme/${val}.scss`)
// 然后给最顶层的div设置val的类样式
const app = document.querySelector('#app')?.classList as DOMTokenList

View File

@@ -67,11 +67,12 @@ const prefers = matchMedia('(prefers-color-scheme: dark)')
// 定义不需要显示消息提示的窗口
const noMessageWindows = ['tray', 'notify', 'capture', 'update', 'checkupdate']
const isValidContent = (theme?: string) => theme === ThemeEnum.DARK || theme === ThemeEnum.LIGHT
const isValidContent = (theme?: string): theme is ThemeEnum => theme === ThemeEnum.DARK || theme === ThemeEnum.LIGHT
const applyThemeContent = (theme: ThemeEnum) => {
globalTheme.value = theme === ThemeEnum.DARK ? darkTheme : lightTheme
document.documentElement.dataset.theme = theme
console.log(globalTheme.value)
}
const syncOsTheme = () => {
@@ -117,7 +118,7 @@ watch(
settingStore.normalizeThemeState()
return
}
applyThemeContent(content as ThemeEnum)
applyThemeContent(content)
},
{ immediate: true }
)

View File

@@ -45,9 +45,10 @@
@compositionend="updateSelectionRange"
@keydown.exact.ctrl.enter="handleEnterKey"
:data-placeholder="t('editor.placeholder')"
class="n-input"
:class="
isMobile()
? 'empty:before:content-[attr(data-placeholder)] before:text-(12px #777) p-2 min-h-2rem ps-10px! text-14px! bg-white! rounded-10px! max-h-8rem! flex items-center'
? 'empty:before:content-[attr(data-placeholder)] before:text-(12px #777) p-2 min-h-2rem ps-10px! text-14px! rounded-10px! max-h-8rem! flex items-center'
: 'empty:before:content-[attr(data-placeholder)] before:text-(12px #777) p-2'
"></div>
</n-scrollbar>
@@ -57,62 +58,58 @@
<div
v-if="!isMobile()"
class="flex-shrink-0 max-h-52px p-4px pr-12px border-t border-gray-200/50 flex justify-end mb-4px">
<n-config-provider :theme="lightTheme">
<n-button-group size="small">
<n-button
color="#13987f"
:disabled="props.isAIMode && props.isAIStreaming ? false : disabledSend"
class="w-65px"
@click="handleDesktopSend">
{{ props.isAIMode && props.isAIStreaming ? '停止思考' : t('editor.send') }}
</n-button>
<n-button color="#13987f" class="p-[0_6px]">
<template #icon>
<n-config-provider :theme="themes.content === ThemeEnum.DARK ? darkTheme : lightTheme">
<n-popselect
v-model:show="arrow"
v-model:value="chatKey"
:options="sendOptions"
trigger="click"
placement="top-end">
<svg @click="arrow = true" v-if="!arrow" class="w-22px h-22px mt-2px outline-none">
<use href="#down"></use>
</svg>
<svg @click="arrow = false" v-else class="w-22px h-22px mt-2px outline-none">
<use href="#up"></use>
</svg>
<template #action>
<n-flex
justify="center"
align="center"
:size="4"
class="text-(12px #777) cursor-default tracking-1 select-none">
<i18n-t keypath="editor.send_or_newline">
<template #send>
<span v-if="chatKey !== 'Enter'">
{{ isMac() ? MacOsKeyEnum[''] : WinKeyEnum.CTRL }}
</span>
<svg class="size-12px">
<use href="#Enter"></use>
</svg>
</template>
<template #newline>
<n-flex align="center" :size="0">
{{ isMac() ? MacOsKeyEnum['⇧'] : WinKeyEnum.SHIFT }}
<svg class="size-12px">
<use href="#Enter"></use>
</svg>
</n-flex>
</template>
</i18n-t>
</n-flex>
</template>
</n-popselect>
</n-config-provider>
</template>
</n-button>
</n-button-group>
</n-config-provider>
<n-button-group size="small">
<n-button
color="#13987f"
:disabled="props.isAIMode && props.isAIStreaming ? false : disabledSend"
class="w-65px"
@click="handleDesktopSend">
{{ props.isAIMode && props.isAIStreaming ? '停止思考' : t('editor.send') }}
</n-button>
<n-button color="#13987f" class="p-[0_6px]">
<template #icon>
<n-popselect
v-model:show="arrow"
v-model:value="chatKey"
:options="sendOptions"
trigger="click"
placement="top-end">
<svg @click="arrow = true" v-if="!arrow" class="w-22px h-22px mt-2px outline-none">
<use href="#down"></use>
</svg>
<svg @click="arrow = false" v-else class="w-22px h-22px mt-2px outline-none">
<use href="#up"></use>
</svg>
<template #action>
<n-flex
justify="center"
align="center"
:size="4"
class="text-(12px #777) cursor-default tracking-1 select-none">
<i18n-t keypath="editor.send_or_newline">
<template #send>
<span v-if="chatKey !== 'Enter'">
{{ isMac() ? MacOsKeyEnum[''] : WinKeyEnum.CTRL }}
</span>
<svg class="size-12px">
<use href="#Enter"></use>
</svg>
</template>
<template #newline>
<n-flex align="center" :size="0">
{{ isMac() ? MacOsKeyEnum['⇧'] : WinKeyEnum.SHIFT }}
<svg class="size-12px">
<use href="#Enter"></use>
</svg>
</n-flex>
</template>
</i18n-t>
</n-flex>
</template>
</n-popselect>
</template>
</n-button>
</n-button-group>
</div>
<!-- @提及框 -->
@@ -197,17 +194,15 @@
<div
v-if="msgInput"
class="flex-shrink-0 max-h-62px h-full border-t border-gray-200/50 flex items-center justify-end">
<n-config-provider class="h-full" :theme="lightTheme">
<n-button-group size="small" :class="isMobile() ? 'h-full' : 'pr-20px'">
<n-button
color="#13987f"
:disabled="props.isAIMode && props.isAIStreaming ? false : disabledSend"
class="w-3rem h-full"
@click="handleMobileSend">
{{ props.isAIMode && props.isAIStreaming ? '停止思考' : t('editor.send') }}
</n-button>
</n-button-group>
</n-config-provider>
<n-button-group size="small" :class="isMobile() ? 'h-full' : 'pr-20px'">
<n-button
color="#13987f"
:disabled="props.isAIMode && props.isAIStreaming ? false : disabledSend"
class="w-3rem h-full"
@click="handleMobileSend">
{{ props.isAIMode && props.isAIStreaming ? '停止思考' : t('editor.send') }}
</n-button>
</n-button-group>
</div>
<div v-if="!msgInput" class="flex items-center justify-start h-full">
<svg
@@ -233,7 +228,7 @@
import { emit } from '@tauri-apps/api/event'
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
import { onKeyStroke } from '@vueuse/core'
import { darkTheme, lightTheme, type VirtualListInst } from 'naive-ui'
import { type VirtualListInst } from 'naive-ui'
import { storeToRefs } from 'pinia'
import type { Ref } from 'vue'
import { MacOsKeyEnum, MittEnum, RoomTypeEnum, ThemeEnum, WinKeyEnum } from '@/enums'

View File

@@ -14,11 +14,7 @@
<!-- 置顶公告提示 -->
<Transition name="announcement" mode="out-in">
<div
v-if="isGroup && topAnnouncement"
key="announcement"
:class="{ 'bg-#eee': isMobile() }"
class="p-[6px_12px_0_12px]">
<div v-if="isGroup && topAnnouncement" key="announcement" class="p-[6px_12px_0_12px]">
<div
class="custom-announcement"
:class="{ 'announcement-hover': isAnnouncementHover }"
@@ -42,7 +38,7 @@
</Transition>
<!-- 聊天内容 -->
<div :class="{ 'bg-#eee': isMobile() }" class="flex flex-col flex-1 min-h-0">
<div class="flex flex-col flex-1 min-h-0">
<div
id="image-chat-main"
ref="scrollContainer"

View File

@@ -1,6 +1,5 @@
<template>
<n-config-provider
:theme="lightTheme"
<div
class="h-full flex flex-col box-border"
:class="{
'bg-cover bg-center bg-no-repeat': props.backgroundImage
@@ -16,7 +15,7 @@
<!-- 底部安全区域 -->
<div :class="[{ 'safe-area-bottom': safeAreaBottom }, props.bottomSafeAreaClass]" />
</n-config-provider>
</div>
</template>
<script setup lang="ts">
@@ -35,7 +34,6 @@ import { audioManager } from '@/utils/AudioManager'
import { isMobile, isWindows } from '@/utils/PlatformConstants'
import { invokeSilently } from '@/utils/TauriInvokeHandler'
import { useRoute } from 'vue-router'
import { lightTheme } from 'naive-ui'
interface MobileLayoutProps {
/** 是否应用顶部安全区域 */
safeAreaTop?: boolean

View File

@@ -11,9 +11,7 @@
</div>
</div>
<div class="bg-#FAFAFA">
<slot name="footer"></slot>
</div>
<slot name="footer"></slot>
</div>
</template>

View File

@@ -7,7 +7,7 @@
spellcheck="false"
style="position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; white-space: pre-wrap"></div>
<div class="w-full min-h-20px bg-#FAFAFA flex flex-col z-2 footer-bar-shadow">
<div class="w-full min-h-20px flex flex-col z-2 footer-bar-shadow">
<div class="flex-1 min-h-0">
<chat-footer :detail-id="globalStore.currentSession?.detailId"></chat-footer>
</div>

View File

@@ -1,7 +1,9 @@
<template>
<div class="w-full h-[56px] grid grid-cols-[100px_1fr_100px] z-2">
<div class="w-full h-[56px] grid grid-cols-[100px_1fr_100px] z-2 bg-background text-foreground">
<div @click="handleBack" class="w-full h-full flex items-center">
<svg class="iconpark-icon w-24px h-24px ms-16px p-5px"><use href="#fanhui"></use></svg>
<svg class="iconpark-icon w-24px h-24px ms-16px p-5px">
<use href="#fanhui" class="text-foreground"></use>
</svg>
<div
v-show="props.msgCount ? (props.msgCount > 0 ? true : false) : false"
class="rounded-15px flex items-center bg-#C7DBD9 px-7px text-14px min-h-20px">
@@ -10,7 +12,7 @@
</div>
<div class="w-full h-full overflow-hidden flex items-center justify-center">
<div @click="handleRoomNameClick" :class="props.isOfficial ? ['chat-room-name-official'] : ['chat-room-name']">
<div class="truncate whitespace-nowrap overflow-hidden text-ellipsis w-full text-center">
<div class="truncate whitespace-nowrap overflow-hidden text-foreground text-ellipsis w-full text-center">
{{ props.roomName }}
</div>
<svg v-if="props.isOfficial" class="w-18px h-18px iconpark-icon text-#1A9B83"><use href="#auth"></use></svg>
@@ -23,6 +25,8 @@
</div>
</div>
</div>
<n-divider v-if="props.border" class="m-0!" />
</template>
<script setup lang="ts">
@@ -35,6 +39,7 @@ export interface HeaderBarProps {
enableDefaultBackground?: boolean
enableShadow?: boolean
roomName?: string | false
border?: boolean
}
const props = withDefaults(defineProps<HeaderBarProps>(), {
@@ -42,7 +47,8 @@ const props = withDefaults(defineProps<HeaderBarProps>(), {
hiddenRight: false,
enableDefaultBackground: true,
enableShadow: true,
roomName: false
roomName: false,
border: false
})
const emits = defineEmits<(e: 'roomNameClick', payload: HeaderBarProps) => void>()

View File

@@ -9,9 +9,9 @@
<!-- 中间两行内容 -->
<div class="truncate pl-4 flex gap-10px flex-col">
<div class="text-14px leading-tight font-bold flex-1 truncate text-#333 flex items-center gap-2">
<n-text class="text-14px leading-tight font-bold flex-1 truncate flex items-center gap-2">
<span>{{ userName }}</span>
</div>
</n-text>
<div class="text-12px text-#666 truncate">{{ formatTime(feedItem.createTime) }}</div>
</div>
</div>
@@ -22,9 +22,9 @@
<div></div>
<div class="flex flex-col gap-2 text-14px">
<!-- 文本内容 -->
<div class="text-#333 leading-relaxed whitespace-pre-wrap break-words">
<n-text depth="3" class="leading-relaxed whitespace-pre-wrap break-words">
{{ feedItem.content }}
</div>
</n-text>
<!-- 图片网格 - 根据图片数量动态调整 -->
<div v-if="feedItem.urls && feedItem.urls.length > 0" :class="getImageGridClass(feedItem.urls.length)">
@@ -52,7 +52,7 @@
</div>
<!-- 底部操作栏 -->
<div class="w-full flex justify-end mt-5px gap-5 items-center text-12px text-#666">
<div class="w-full flex justify-end mt-5px gap-5 items-center text-12px text-#666 dark:invert">
<!-- 分享 -->
<div class="flex items-center gap-1 cursor-pointer active:opacity-60" @click="handleShare">
<svg class="iconpark-icon w-20px h-20px"><use href="#fenxiang"></use></svg>

View File

@@ -14,7 +14,7 @@
<div ref="infoBox" class="pl-2 flex gap-8px flex-col transition-transform duration-300 ease-in-out">
<!-- 名字与在线状态 -->
<div class="flex flex-warp gap-4 items-center">
<span class="font-bold text-20px text-#373838">{{ userDetailInfo!.name }}</span>
<n-text class="font-bold text-20px">{{ userDetailInfo!.name }}</n-text>
<div
v-show="hasUserOnlineState"
class="bg-#E7EFE6 flex flex-wrap ps-2 px-8px items-center rounded-full gap-1 h-24px">
@@ -34,7 +34,7 @@
<div class="flex flex-warp gap-2 items-center">
<span class="text-bold-style">{{ t('mobile_personal_info.account') }}:{{ userDetailInfo!.account }}</span>
<span v-if="isMyPage" @click="toMyQRCode" class="pe-15px">
<img class="w-14px h-14px" src="@/assets/mobile/my/qr-code.webp" alt="" />
<img class="w-14px h-14px dark:invert" src="@/assets/mobile/my/qr-code.webp" alt="" />
</span>
</div>
<Transition name="medal-fade">
@@ -76,56 +76,65 @@
<div class="flex flex-wrap justify-around mt-4">
<div class="flex flex-warp gap-2 items-center">
<div class="min-w-10 flex flex-col items-center">
<div class="fans-number">920.13W</div>
<n-text class="fans-number">920.13W</n-text>
<div class="fans-title">{{ t('mobile_personal_info.follower') }}</div>
</div>
<div class="h-20px w-1px bg-gray-300"></div>
<n-divider vertical class="h-20px" />
<div class="min-w-10 flex flex-col items-center">
<div class="fans-number">120</div>
<n-text class="fans-number">120</n-text>
<div class="fans-title">{{ t('mobile_personal_info.follow') }}</div>
</div>
<div class="h-20px w-1px bg-gray-300"></div>
<n-divider vertical class="h-20px" />
<div class="min-w-10 flex flex-col items-center">
<div class="fans-number">43.15W</div>
<n-text class="fans-number">43.15W</n-text>
<div class="fans-title">{{ t('mobile_personal_info.like') }}</div>
</div>
</div>
<div class="flex-1 justify-end flex items-center gap-3">
<n-button
:disabled="loading"
@click="toEditProfile"
v-if="props.isMyPage && !isBotUser(uid)"
class="font-bold px-4 py-10px bg-#EEF4F3 text-#373838 rounded-full text-12px">
{{ t('mobile_personal_info.edit_profile') }}
</n-button>
<n-button
:loading="loading"
:disabled="loading"
@click="handleDelete"
:color="'#d5304f'"
v-if="!props.isMyPage && isMyFriend && !isBotUser(uid)"
class="px-5 py-10px font-bold text-center rounded-full text-12px">
{{ t('mobile_personal_info.remove_user') }}
</n-button>
</div>
<div class="justify-end flex items-center gap-3 mt-30px">
<n-button
:disabled="loading"
@click="toEditProfile"
strong
secondary
round
v-if="props.isMyPage && !isBotUser(uid)"
class="font-bold px-4 py-10px rounded-full text-12px">
{{ t('mobile_personal_info.edit_profile') }}
</n-button>
<n-button
:loading="loading"
:disabled="loading"
strong
secondary
@click="handleDelete"
:color="'#d5304f'"
v-if="!props.isMyPage && isMyFriend && !isBotUser(uid)"
class="px-5 py-10px font-bold text-center rounded-full text-12px">
{{ t('mobile_personal_info.remove_user') }}
</n-button>
<n-button
type="primary"
:disabled="loading"
@click="handleAddFriend"
v-if="!props.isMyPage && !isMyFriend && !isBotUser(uid)"
class="px-5 py-10px font-bold text-center rounded-full text-12px">
+&nbsp;
{{ t('mobile_personal_info.add_friend') }}
</n-button>
<n-button
type="primary"
@click="toChatRoom"
:disabled="loading"
v-if="!props.isMyPage && isMyFriend"
class="px-5 py-10px text-center font-bold rounded-full text-12px">
{{ isBotUser(uid) ? t('mobile_personal_info.open_bot') : t('mobile_personal_info.chat') }}
</n-button>
</div>
<n-button
type="primary"
:disabled="loading"
strong
secondary
@click="handleAddFriend"
v-if="!props.isMyPage && !isMyFriend && !isBotUser(uid)"
class="px-5 py-10px font-bold text-center rounded-full text-12px">
+&nbsp;
{{ t('mobile_personal_info.add_friend') }}
</n-button>
<n-button
type="primary"
strong
secondary
@click="toChatRoom"
:disabled="loading"
v-if="!props.isMyPage && isMyFriend"
class="px-5 py-10px text-center font-bold rounded-full text-12px">
{{ isBotUser(uid) ? t('mobile_personal_info.open_bot') : t('mobile_personal_info.chat') }}
</n-button>
</div>
</div>
</Transition>

View File

@@ -1,5 +1,6 @@
<template>
<div class="grid grid-cols-[32px_32px_32px_32px] px-16px gap-12px z-1 h-32px items-center justify-end mt-10px">
<div
class="grid grid-cols-[32px_32px_32px_32px] px-16px gap-12px z-1 h-32px items-center justify-end mt-10px dark:invert">
<div @click="toMyMessages" class="h-32px w-32px flex items-center justify-center">
<n-badge :max="99" :value="unreadApplyCount" :show="unreadApplyCount > 0">
<svg class="iconpark-icon h-24px w-24px block"><use href="#remind"></use></svg>

View File

@@ -1,11 +1,12 @@
<template>
<n-divider class="p-0! m-0!" />
<div class="tab-bar flex justify-around items-end pt-3">
<RouterLink
v-for="item in navItems"
:key="item.path"
:to="item.path"
class="tab-item flex flex-col flex-1 items-center no-underline relative"
:class="route.path === item.path ? 'color-[--tab-bar-icon-color]' : 'text-#000'">
:class="route.path === item.path ? 'color-[--tab-bar-icon-color]' : 'text-#000 dark:text-white/80'">
<n-badge
class="flex flex-col w-55% flex-1 relative items-center"
:offset="[-6, 6]"
@@ -85,7 +86,6 @@ const navItems: NavItem[] = [
<style scoped lang="scss">
.tab-bar {
border-top: 0.5px solid #e3e3e3;
-webkit-backdrop-filter: blur(12px);
backdrop-filter: blur(12px);
}

View File

@@ -6,163 +6,160 @@
:enable-default-background="false"
:enable-shadow="false"
:room-name="t('mobile_forget_code.title')" />
<n-config-provider :theme="lightTheme" class="bg-#fff rounded-8px select-none cursor-default">
<n-flex vertical class="w-full size-full">
<!-- 步骤条 -->
<n-steps size="small" class="w-full px-40px mt-20px" :current="currentStep" :status="stepStatus">
<n-step :title="t('mobile_forget_code.steps.verify_email')" description="" />
<n-step :title="t('mobile_forget_code.steps.set_new_password')" description="" />
<n-step :title="t('mobile_forget_code.steps.done')" description="" />
</n-steps>
<n-flex vertical class="w-full size-full">
<!-- 步骤条 -->
<n-steps size="small" class="w-full px-40px mt-20px" :current="currentStep" :status="stepStatus">
<n-step :title="t('mobile_forget_code.steps.verify_email')" description="" />
<n-step :title="t('mobile_forget_code.steps.set_new_password')" description="" />
<n-step :title="t('mobile_forget_code.steps.done')" description="" />
</n-steps>
<!-- 第一步验证邮箱 -->
<div v-if="currentStep === 1" class="w-full max-w-300px mx-auto mt-30px">
<n-form ref="formRef" :model="formData" :rules="emailRules">
<!-- 邮箱输入 -->
<n-form-item path="email" :label="t('mobile_forget_code.input.label.email')">
<!-- 第一步验证邮箱 -->
<div v-if="currentStep === 1" class="w-full max-w-300px mx-auto mt-30px">
<n-form ref="formRef" :model="formData" :rules="emailRules">
<!-- 邮箱输入 -->
<n-form-item path="email" :label="t('mobile_forget_code.input.label.email')">
<n-input
:allow-input="noSideSpace"
class="border-(1px solid #90909080)"
v-model:value="formData.email"
:placeholder="t('mobile_forget_code.input.email')"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
clearable />
</n-form-item>
<!-- 邮箱验证码 -->
<n-form-item path="emailCode" :label="t('mobile_forget_code.input.label.email_verification_code')">
<n-flex :size="8">
<n-input
:allow-input="noSideSpace"
class="border-(1px solid #90909080)"
v-model:value="formData.email"
:placeholder="t('mobile_forget_code.input.email')"
v-model:value="formData.emailCode"
:placeholder="t('mobile_forget_code.input.email_code')"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
clearable />
</n-form-item>
<!-- 邮箱验证码 -->
<n-form-item path="emailCode" :label="t('mobile_forget_code.input.label.email_verification_code')">
<n-flex :size="8">
<n-input
:allow-input="noSideSpace"
class="border-(1px solid #90909080)"
v-model:value="formData.emailCode"
:placeholder="t('mobile_forget_code.input.email_code')"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
maxlength="6" />
<n-button
color="#13987f"
ghost
:disabled="sendBtnDisabled"
:loading="sendingEmailCode"
@click="sendEmailCode"
class="min-w-100px w-fit h-34px">
{{ emailCodeBtnText }}
</n-button>
</n-flex>
</n-form-item>
<n-button
:loading="verifyLoading"
:disabled="nextDisabled"
tertiary
style="color: #fff"
@click="verifyEmail"
class="mt-10px w-full gradient-button">
{{ t('mobile_forget_code.button.next') }}
</n-button>
</n-form>
</div>
<!-- 第二步设置新密码 -->
<div v-if="currentStep === 2" class="w-full max-w-300px mx-auto mt-30px">
<n-form ref="passwordFormRef" :model="passwordForm" :rules="passwordRules">
<!-- 新密码 -->
<n-form-item path="password" :label="t('mobile_forget_code.input.label.new_pass')">
<n-flex vertical :size="8" class="w-full">
<n-input
:allow-input="noSideSpace"
class="border-(1px solid #90909080) w-full"
v-model:value="passwordForm.password"
type="password"
show-password-on="click"
:placeholder="t('mobile_forget_code.input.new_pass', { len: '6-16' })"
maxlength="16"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
minlength="6" />
<n-flex vertical :size="4" class="space-y-4px">
<Validation
:value="passwordForm.password"
:message="t('mobile_forget_code.validation.minlength', { len: '6-16' })"
:validator="validateMinLength" />
<Validation
:value="passwordForm.password"
:message="t('mobile_forget_code.validation.valid_characters')"
:validator="validateAlphaNumeric" />
<Validation
:value="passwordForm.password"
:message="t('mobile_forget_code.validation.must_special_char')"
:validator="validateSpecialChar" />
</n-flex>
</n-flex>
</n-form-item>
<!-- 确认密码 -->
<n-form-item path="confirmPassword" :label="t('mobile_forget_code.input.label.confirm_password')">
<n-flex vertical :size="8" class="w-full">
<n-input
:allow-input="noSideSpace"
class="border-(1px solid #90909080) w-full"
v-model:value="passwordForm.confirmPassword"
type="password"
show-password-on="click"
:placeholder="t('mobile_forget_code.input.confirm_password')"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
maxlength="16"
minlength="6" />
<n-flex vertical :size="4">
<Validation
:value="passwordForm.confirmPassword"
:message="t('mobile_forget_code.validation.passwords_match')"
:validator="(value: string) => value === passwordForm.password && value !== ''" />
</n-flex>
</n-flex>
</n-form-item>
<n-flex :size="16" class="mt-30px">
<n-button @click="goBack" class="flex-1">{{ t('mobile_forget_code.button.go_back_setp') }}</n-button>
maxlength="6" />
<n-button
:loading="submitLoading"
tertiary
style="color: #fff"
@click="submitNewPassword"
class="flex-1 gradient-button">
{{ t('mobile_forget_code.button.submit') }}
color="#13987f"
ghost
:disabled="sendBtnDisabled"
:loading="sendingEmailCode"
@click="sendEmailCode"
class="min-w-100px w-fit h-34px">
{{ emailCodeBtnText }}
</n-button>
</n-flex>
</n-form>
</div>
</n-form-item>
<!-- 第三步完成 -->
<div v-if="currentStep === 3" class="w-full max-w-300px mx-auto mt-100px text-center">
<!-- <n-icon size="64" class="text-#13987f">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="currentColor" d="M9,20.42L2.79,14.21L5.62,11.38L9,14.77L18.88,4.88L21.71,7.71L9,20.42Z" />
</svg>
</n-icon> -->
<img class="size-98px" src="/emoji/party-popper.webp" alt="" />
<n-button
:loading="verifyLoading"
:disabled="nextDisabled"
tertiary
style="color: #fff"
@click="verifyEmail"
class="mt-10px w-full gradient-button">
{{ t('mobile_forget_code.button.next') }}
</n-button>
</n-form>
</div>
<div class="mt-16px text-18px">{{ t('mobile_forget_code.password_reset_success') }}</div>
<div class="mt-16px text-14px text-#666">{{ t('mobile_forget_code.password_reset_success_desc') }}</div>
</div>
</n-flex>
</n-config-provider>
<!-- 第二步设置新密码 -->
<div v-if="currentStep === 2" class="w-full max-w-300px mx-auto mt-30px">
<n-form ref="passwordFormRef" :model="passwordForm" :rules="passwordRules">
<!-- 新密码 -->
<n-form-item path="password" :label="t('mobile_forget_code.input.label.new_pass')">
<n-flex vertical :size="8" class="w-full">
<n-input
:allow-input="noSideSpace"
class="border-(1px solid #90909080) w-full"
v-model:value="passwordForm.password"
type="password"
show-password-on="click"
:placeholder="t('mobile_forget_code.input.new_pass', { len: '6-16' })"
maxlength="16"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
minlength="6" />
<n-flex vertical :size="4" class="space-y-4px">
<Validation
:value="passwordForm.password"
:message="t('mobile_forget_code.validation.minlength', { len: '6-16' })"
:validator="validateMinLength" />
<Validation
:value="passwordForm.password"
:message="t('mobile_forget_code.validation.valid_characters')"
:validator="validateAlphaNumeric" />
<Validation
:value="passwordForm.password"
:message="t('mobile_forget_code.validation.must_special_char')"
:validator="validateSpecialChar" />
</n-flex>
</n-flex>
</n-form-item>
<!-- 确认密码 -->
<n-form-item path="confirmPassword" :label="t('mobile_forget_code.input.label.confirm_password')">
<n-flex vertical :size="8" class="w-full">
<n-input
:allow-input="noSideSpace"
class="border-(1px solid #90909080) w-full"
v-model:value="passwordForm.confirmPassword"
type="password"
show-password-on="click"
:placeholder="t('mobile_forget_code.input.confirm_password')"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
maxlength="16"
minlength="6" />
<n-flex vertical :size="4">
<Validation
:value="passwordForm.confirmPassword"
:message="t('mobile_forget_code.validation.passwords_match')"
:validator="(value: string) => value === passwordForm.password && value !== ''" />
</n-flex>
</n-flex>
</n-form-item>
<n-flex :size="16" class="mt-30px">
<n-button @click="goBack" class="flex-1">{{ t('mobile_forget_code.button.go_back_setp') }}</n-button>
<n-button
:loading="submitLoading"
tertiary
style="color: #fff"
@click="submitNewPassword"
class="flex-1 gradient-button">
{{ t('mobile_forget_code.button.submit') }}
</n-button>
</n-flex>
</n-form>
</div>
<!-- 第三步完成 -->
<div v-if="currentStep === 3" class="w-full max-w-300px mx-auto mt-100px text-center">
<!-- <n-icon size="64" class="text-#13987f">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path fill="currentColor" d="M9,20.42L2.79,14.21L5.62,11.38L9,14.77L18.88,4.88L21.71,7.71L9,20.42Z" />
</svg>
</n-icon> -->
<img class="size-98px" src="/emoji/party-popper.webp" alt="" />
<div class="mt-16px text-18px">{{ t('mobile_forget_code.password_reset_success') }}</div>
<div class="mt-16px text-14px text-#666">{{ t('mobile_forget_code.password_reset_success_desc') }}</div>
</div>
</n-flex>
</MobileLayout>
</template>
<script setup lang="ts">
import { lightTheme } from 'naive-ui'
import Validation from '@/components/common/Validation.vue'
import { forgetPassword, getCaptcha, sendCaptcha } from '@/utils/ImRequestUtils'
import { validateAlphaNumeric, validateSpecialChar } from '@/utils/Validate'

View File

@@ -1,49 +1,60 @@
<template>
<div class="h-100vh flex flex-col bg-gray-100">
<img src="@/assets/mobile/chat-home/background.webp" class="w-100% absolute top-0 z-1" alt="hula" />
<AutoFixHeightPage :show-footer="false" class="dark:bg-red">
<template #header>
<div class="h-100px"></div>
<HeaderBar
:isOfficial="false"
:hidden-right="true"
:enable-default-background="false"
:enable-shadow="false"
:room-name="t('mobile_personal_info_qr.title')" />
</template>
<HeaderBar
:isOfficial="false"
:hidden-right="true"
:enable-default-background="false"
:enable-shadow="false"
:room-name="t('mobile_personal_info_qr.title')" />
<template #container>
<div class="flex flex-col overflow-auto h-full relative">
<img
src="@/assets/mobile/chat-home/background.webp"
class="absolute fixed top-0 left-0 w-full h-full z-0 dark:opacity-20" />
<!-- 页面全部内容 -->
<div class="flex flex-col flex-1 items-center p-15px z-2 my-15">
<div class="flex flex-col rounded-15px bg-white py-10">
<div class="flex flex-1 flex-col px-5 gap-10px">
<div class="flex flex-wrap ps-3 gap-10px">
<div class="flex h-auto">
<n-avatar round :size="60" :src="AvatarUtils.getAvatarUrl(userStore.userInfo?.avatar || '')" />
<!-- 页面全部内容 -->
<div class="flex flex-col flex-1 items-center p-15px z-2 my-15">
<n-card class="flex flex-col rounded-15px py-10">
<div class="flex flex-1 flex-col px-5 gap-10px">
<div class="flex flex-wrap ps-3 gap-10px">
<div class="flex h-auto">
<n-avatar round :size="60" :src="AvatarUtils.getAvatarUrl(userStore.userInfo?.avatar || '')" />
</div>
<div
class="flex flex-col text-#4e4e4e h-auto gap-8px overflow-hidden justify-center text-18px whitespace-normal break-words max-w-46">
<n-text class="font-bold">{{ userInfo?.name }}</n-text>
<n-text depth="3" class="text-16px">
{{ t('mobile_personal_info_qr.account') }}:{{ userInfo?.account }}
</n-text>
</div>
</div>
<div class="flex w-auto justify-center">
<n-qr-code
:size="250"
class="rounded-12px"
:value="qrCodeValue"
color="#14997E"
:bg-color="qrCodeBgColor"
:type="qrCodeType"
:icon-src="AvatarUtils.getAvatarUrl(userStore.userInfo?.avatar ?? '')"
:icon-size="60"
:icon-margin="2"
:error-correction-level="qrErrorCorrectionLevel" />
</div>
<div class="flex justify-center text-gray">{{ t('mobile_personal_info_qr.scan_to_add') }}</div>
</div>
<div
class="flex flex-col text-#4e4e4e h-auto gap-8px overflow-hidden justify-center text-18px whitespace-normal break-words max-w-46">
<div class="font-bold">{{ userInfo?.name }}</div>
<div class="text-16px">{{ t('mobile_personal_info_qr.account') }}:{{ userInfo?.account }}</div>
</div>
</div>
<div class="flex w-auto justify-center">
<n-qr-code
:size="250"
class="rounded-12px"
:value="qrCodeValue"
color="#14997E"
:bg-color="qrCodeBgColor"
:type="qrCodeType"
:icon-src="AvatarUtils.getAvatarUrl(userStore.userInfo?.avatar || '')"
:icon-size="60"
:icon-margin="2"
:error-correction-level="qrErrorCorrectionLevel" />
</div>
<div class="flex justify-center text-gray">{{ t('mobile_personal_info_qr.scan_to_add') }}</div>
</n-card>
</div>
</div>
</div>
</div>
</template>
</AutoFixHeightPage>
</template>
<script setup lang="ts">

View File

@@ -3,78 +3,83 @@
<template #header>
<HeaderBar
:isOfficial="false"
style="border-bottom: 1px solid; border-color: #dfdfdf"
border
:hidden-right="true"
:room-name="t('mobile_chat_setting.title', { t: title })" />
</template>
<template #container>
<div
class="bg-[url('@/assets/mobile/chat-home/background.webp')] bg-cover bg-center flex flex-col overflow-auto h-full">
<div class="flex flex-col gap-15px py-15px px-20px flex-1 min-h-0">
<div class="flex shadow py-10px bg-white rounded-10px w-full items-center gap-10px" @click="clickInfo">
<!-- 群头像 -->
<div class="flex justify-center">
<div class="rounded-full relative bg-white w-38px h-38px overflow-hidden" style="margin-left: 10px">
<n-avatar
class="absolute"
:size="38"
:src="AvatarUtils.getAvatarUrl(activeItem?.avatar || '')"
fallback-src="/logo.png"
:style="{
'object-fit': 'cover',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)'
}"
round />
<div class="flex flex-col overflow-auto h-full relative">
<img
src="@/assets/mobile/chat-home/background.webp"
class="absolute fixed top-0 left-0 w-full h-full z-0 dark:opacity-20" />
<div class="flex flex-col gap-15px py-15px px-20px flex-1 min-h-0 z-1">
<n-card size="small" embedded class="rounded-10px p-0" content-class="p-0!">
<div class="flex py-10px rounded-10px w-full items-center gap-10px" @click="clickInfo">
<!-- 群头像 -->
<div class="flex justify-center">
<div class="rounded-full relative bg-white w-38px h-38px overflow-hidden" style="margin-left: 10px">
<n-avatar
class="absolute"
:size="38"
:src="AvatarUtils.getAvatarUrl(activeItem?.avatar || '')"
fallback-src="/logo.png"
:style="{
'object-fit': 'cover',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)'
}"
round />
</div>
<input
v-if="isGroup"
ref="fileInput"
type="file"
accept="image/jpeg,image/png,image/webp"
class="hidden"
@change="handleFileChange" />
<AvatarCropper
ref="cropperRef"
v-model:show="showCropper"
:image-url="localImageUrl"
@crop="handleCrop" />
</div>
<input
v-if="isGroup"
ref="fileInput"
type="file"
accept="image/jpeg,image/png,image/webp"
class="hidden"
@change="handleFileChange" />
<AvatarCropper
ref="cropperRef"
v-model:show="showCropper"
:image-url="localImageUrl"
@crop="handleCrop" />
</div>
<div class="text-14px flex items-center h-full gap-5px">
<span>
{{ activeItem?.name || '' }}
</span>
<span v-if="activeItem?.hotFlag === 1">
<svg class="w-18px h-18px iconpark-icon text-#1A9B83">
<use href="#auth"></use>
</svg>
</span>
<div class="text-14px flex items-center h-full gap-5px">
<span>
{{ activeItem?.name || '' }}
</span>
<span v-if="activeItem?.hotFlag === 1">
<svg class="w-18px h-18px iconpark-icon text-#1A9B83">
<use href="#auth"></use>
</svg>
</span>
</div>
</div>
</div>
</n-card>
<!-- 群成员 -->
<div v-if="isGroup" class="bg-white rounded-10px max-w-full p-[5px_10px_5x_10px] shadow">
<div class="p-[15px_15px_0px_15px] flex flex-col">
<!-- 群号 -->
<div class="flex justify-between items-center">
<div class="text-14px">{{ t('mobile_chat_setting.group_members_title') }}</div>
<div @click="toGroupChatMember" class="text-12px text-#6E6E6E flex flex-wrap gap-10px items-center">
<i18n-t keypath="mobile_chat_setting.member_count">
<template #count>
<span class="text-#398D7E">{{ groupStore.countInfo?.memberNum || 0 }}</span>
</template>
</i18n-t>
<n-card
v-if="isGroup"
class="rounded-10px"
content-class="p-[15px_15px_0px_15px]!"
header-class="text-14px! p-[15px_15px_0px_15px]!"
:title="t('mobile_chat_setting.group_members_title')">
<template #header-extra>
<div @click="toGroupChatMember" class="text-12px text-#6E6E6E flex flex-wrap gap-10px items-center">
<i18n-t keypath="mobile_chat_setting.member_count">
<template #count>
<span class="text-#398D7E">{{ groupStore.countInfo?.memberNum || 0 }}</span>
</template>
</i18n-t>
<div>
<svg class="w-14px h-14px iconpark-icon">
<use href="#right"></use>
</svg>
</div>
<div>
<svg class="w-14px h-14px iconpark-icon">
<use href="#right"></use>
</svg>
</div>
</div>
</div>
</template>
<div class="py-15px px-5px grid grid-cols-5 gap-15px text-12px">
<div
@click="toFriendInfo(i.uid)"
@@ -90,19 +95,16 @@
</div>
<div class="truncate max-w-full text-#707070">{{ i.name }}</div>
</div>
<div
@click="toInviteGroupMember"
class="flex flex-col justify-center items-center gap-5px cursor-pointer">
<div
class="rounded-full bg-#E5EFEE w-36px h-36px flex items-center justify-center hover:bg-#D5E5E0 transition-colors">
<svg class="iconpark-icon h-25px w-25px">
<div class="flex flex-col justify-center items-center gap-5px cursor-pointer">
<n-button strong secondary circle @click="toInviteGroupMember">
<svg class="iconpark-icon h-25px w-25px dark:opacity-40">
<use href="#plus"></use>
</svg>
</div>
</n-button>
<div>{{ t('mobile_chat_setting.group_invite_member') }}</div>
</div>
</div>
</div>
</n-card>
<!-- 管理群成员 -->
<div
@@ -112,19 +114,17 @@
{{ t('mobile_chat_setting.manage_group_members') }}
</div>
<div
class="bg-white p-15px rounded-10px shadow text-14px flex cursor-pointer"
@click="handleSearchChatContent">
<n-card class="rounded-10px" content-class="p-15px!" @click="handleSearchChatContent">
{{ t('mobile_chat_setting.search_history') }}
</div>
</n-card>
<!-- 群公告 -->
<div class="flex bg-white rounded-10px w-full h-auto shadow">
<div class="px-15px flex flex-col w-full">
<!-- 群号 -->
<div
style="border-bottom: 1px solid; border-color: #ebebeb"
@click="handleCopy(activeItem?.account || '')"
class="flex justify-between py-15px items-center">
<n-card
class="rounded-10px"
header-class="text-14px! p-15px!"
:segmented="{ content: true, footer: 'soft' }"
content-class="p-15px!">
<template #header>
<div @click="handleCopy(activeItem?.account || '')" class="flex justify-between items-center">
<div class="text-14px">
{{
t('mobile_chat_setting.id_card.qr_code_label', {
@@ -143,98 +143,89 @@
</div>
</div>
</div>
<!-- 公告内容 -->
<div @click="goToNotice" v-if="isGroup" class="pt-15px flex flex-col text-14px gap-10px">
<div>{{ t('mobile_chat_setting.group_notice.title') }}</div>
<div class="text-#707070 line-clamp-2 text-12px line-height-20px">
{{ announList.length > 0 ? announList[0]?.content : '' }}
</div>
</div>
<!-- v-if="isGroup && groupStore.isAdminOrLord()" -->
<div class="flex justify-between py-15px items-center">
<div class="text-14px">{{ t('mobile_chat_setting.group_name') }}</div>
<div class="text-12px text-#6E6E6E flex flex-wrap gap-10px items-center">
<input
style="
height: 17px;
border: none;
text-align: right;
outline: none;
font-size: 14px;
text-align: right;
"
v-model="nameValue"
@blur="handleGroupInfoUpdate"
:placeholder="t('mobile_chat_setting.input.group_name')" />
</div>
</div>
<div v-if="isGroup" class="flex justify-between py-15px items-center">
<div class="text-14px">{{ t('mobile_chat_setting.group_alias') }}</div>
<div class="text-12px text-#6E6E6E flex flex-wrap gap-10px items-center">
<input
style="
height: 17px;
border: none;
text-align: right;
outline: none;
font-size: 14px;
text-align: right;
"
v-model="nicknameValue"
@blur="handleInfoUpdate"
:placeholder="t('mobile_chat_setting.input.group_alias')" />
</div>
</template>
<!-- 公告内容 -->
<div @click="goToNotice" v-if="isGroup" class="flex flex-col text-14px gap-10px">
<div>{{ t('mobile_chat_setting.group_notice.title') }}</div>
<div class="text-#707070 line-clamp-2 text-12px line-height-20px">
{{ announList.length > 0 ? announList[0]?.content : '' }}
</div>
</div>
</div>
<!-- v-if="isGroup && groupStore.isAdminOrLord()" -->
<div class="flex justify-between py-15px items-center">
<div class="text-14px">{{ t('mobile_chat_setting.group_name') }}</div>
<div class="text-12px text-#6E6E6E flex flex-wrap gap-10px items-center">
<n-input
size="small"
style="border: none; text-align: right; outline: none; text-align: right"
v-model:value="nameValue"
@blur="handleGroupInfoUpdate"
:placeholder="t('mobile_chat_setting.input.group_name')" />
</div>
</div>
<div v-if="isGroup" class="flex justify-between py-15px items-center">
<div class="text-14px">{{ t('mobile_chat_setting.group_alias') }}</div>
<div class="text-12px text-#6E6E6E flex flex-wrap gap-10px items-center">
<n-input
size="small"
style="border: none; text-align: right; outline: none; text-align: right"
v-model="nicknameValue"
@blur="handleInfoUpdate"
:placeholder="t('mobile_chat_setting.input.group_alias')" />
</div>
</div>
</n-card>
<!-- 备注 -->
<div class="w-full flex flex-col gap-15px rounded-10px">
<div class="ps-15px text-14px">
<span>{{ t('mobile_chat_setting.remark') }}</span>
<span class="dark:text-white">{{ t('mobile_chat_setting.remark') }}</span>
<span class="text-#6E6E6E ml-1">{{ t('mobile_chat_setting.remar_kprivate_visible') }}</span>
</div>
<div class="rounded-10px flex w-full bg-white shadow">
<div class="w-full px-15px">
<input
v-model="remarkValue"
class="h-50px w-full"
style="border: none; outline: none; font-size: 14px"
:placeholder="t('mobile_chat_setting.input.remark')"
@blur="handleInfoUpdate" />
</div>
<n-input
v-model="remarkValue"
size="large"
:placeholder="t('mobile_chat_setting.input.remark')"
@blur="handleInfoUpdate" />
</div>
<n-card
class="rounded-10px"
content-class="p-15px!"
header-class="p-15px! text-14px!"
:title="t('mobile_chat_setting.setting_type', { t: title })">
<div class="flex justify-between items-center">
<div class="text-14px">{{ t('mobile_chat_setting.pintop') }}</div>
<n-switch :value="!!activeItem?.top" @update:value="handleTop" />
</div>
</div>
<div class="flex bg-white rounded-10px w-full h-auto shadow">
<div class="px-15px flex flex-col w-full">
<div class="pt-15px text-14px text-#6E6E6E">
{{ t('mobile_chat_setting.setting_type', { t: title }) }}
</div>
<!-- 群号 -->
<div
style="border-bottom: 1px solid; border-color: #ebebeb"
class="flex justify-between py-12px items-center">
<div class="text-14px">{{ t('mobile_chat_setting.pintop') }}</div>
<n-switch :value="!!activeItem?.top" @update:value="handleTop" />
</div>
<div
style="border-bottom: 1px solid; border-color: #ebebeb"
class="flex justify-between py-12px items-center">
<div class="text-14px">{{ t('mobile_chat_setting.silent') }}</div>
<n-switch
@update:value="handleNotification"
:value="activeItem?.muteNotification === NotificationTypeEnum.NOT_DISTURB" />
</div>
<n-divider />
<div class="flex justify-between py-12px items-center">
<div class="text-14px">{{ t('mobile_chat_setting.silent') }}</div>
<n-switch
@update:value="handleNotification"
:value="activeItem?.muteNotification === NotificationTypeEnum.NOT_DISTURB" />
</div>
</div>
<div class="shadow bg-white cursor-pointer text-red text-14px rounded-10px w-full mb-20px">
<div class="p-15px">{{ t('mobile_chat_setting.delete_chat_history') }}</div>
</div>
</n-card>
<n-button
strong
secondary
circle
size="large"
class="cursor-pointer text-red text-14px rounded-10px w-full mb-20px">
{{ t('mobile_chat_setting.delete_chat_history') }}
</n-button>
<div class="mt-auto flex justify-center mb-20px">
<!-- 解散群聊退出群聊删除好友按钮 -->
<n-button v-if="isGroup && globalStore.currentSessionRoomId !== '1'" type="error" @click="handleExit">
<n-button
class="w-full"
v-if="isGroup && globalStore.currentSessionRoomId !== '1'"
strong
secondary
round
type="error"
size="large"
@click="handleExit">
{{
isGroup
? isLord

View File

@@ -1,11 +1,7 @@
<template>
<AutoFixHeightPage :show-footer="false">
<template #header>
<HeaderBar
:isOfficial="false"
style="border-bottom: 1px solid; border-color: #dfdfdf"
:hidden-right="true"
room-name="群成员" />
<HeaderBar :isOfficial="false" border :hidden-right="true" room-name="群成员" />
</template>
<template #container>
@@ -14,10 +10,11 @@
<!-- 搜索表单 -->
<n-form @submit="handleSubmit" class="flex flex-wrap gap-10px">
<div class="flex flex-1">
<input
v-model="formData.keyword"
<n-input
v-model:value="formData.keyword"
placeholder="搜索"
class="bg-gray-100 text-center border-none w-full rounded-10px h-30px" />
size="medium"
class="text-center border-none w-full rounded-10px" />
</div>
</n-form>
@@ -25,7 +22,6 @@
<div ref="measure" class="flex absolute w-full h-full top-0 left-0 z-1"></div>
<div class="absolute z-10 w-full">
<div v-if="filteredList.length === 0" class="flex w-full justify-center mt-20px">无数据</div>
<n-virtual-list
v-else
:style="{ height: virtualScrollerHeight + 'px', width: '100%' }"
@@ -39,9 +35,9 @@
:src="AvatarUtils.getAvatarUrl(item.avatar)"
fallback-src="/logo.png"
round />
<div class="line-clamp-1">
<n-text class="line-clamp-1">
{{ item.name }}
</div>
</n-text>
</div>
</div>
</template>

View File

@@ -13,7 +13,7 @@
<div class="flex-1 py-5px shrink-0">
<n-input
v-model:value="keyword"
class="rounded-10px w-full bg-gray-100 relative text-14px"
class="rounded-10px w-full relative text-14px"
placeholder="搜索联系人~"
clearable
spellCheck="false"
@@ -44,7 +44,9 @@
class="w-full flex items-center px-5px"
:class="[
'cursor-pointer select-none transition-colors duration-150',
selectedList.includes(item.uid) ? 'bg-blue-50 border-blue-300' : 'hover:bg-gray-50'
selectedList.includes(item.uid)
? 'bg-blue-100 text-blue-700 dark:bg-blue-500/20 dark:text-blue-300'
: 'data-[active=true]:hover:bg-blue-100 dark:data-[active=true]:hover:bg-blue-500/20'
]">
<template #default>
<div class="flex items-center gap-10px px-8px py-10px">

View File

@@ -1,8 +1,8 @@
<template>
<div>
<div class="flex gap-2 mx-2">
<input class="flex-1" type="text" placeholder="请输入聊天内容" />
<n-button type="primary" @click="() => router.back()">取消</n-button>
<n-input type="text" placeholder="请输入聊天内容" />
<n-button strong secondary type="primary" @click="() => router.back()">取消</n-button>
</div>
<div class="flex flex-col items-center mt-10">

View File

@@ -1,74 +1,60 @@
<template>
<AutoFixHeightPage :show-footer="false">
<template #header>
<HeaderBar
:isOfficial="false"
class="bg-white"
style="border-bottom: 1px solid; border-color: #dfdfdf"
:hidden-right="true"
room-name="公告详情" />
<HeaderBar :isOfficial="false" class="bg-white" border :hidden-right="true" room-name="公告详情" />
</template>
<template #container>
<div
class="bg-[url('@/assets/mobile/chat-home/background.webp')] bg-cover bg-center flex flex-col overflow-auto h-full">
<div class="flex flex-col overflow-auto h-full relative">
<img
src="@/assets/mobile/chat-home/background.webp"
class="absolute t-0 l-0 w-full h-full z-0 dark:opacity-20" />
<div class="flex flex-col flex-1 gap-15px py-15px px-20px">
<div v-if="loading" class="flex justify-center items-center h-200px">
<n-spin size="large" />
</div>
<div v-else-if="announcement" class="bg-white flex flex-col shadow p-10px gap-15px text-14px rounded-15px">
<!-- 公告头部信息 -->
<div
style="border-bottom: 1px solid; border-color: #ebebeb"
class="grid grid-cols-[2.2rem_1fr_4rem] items-start px-2 py-3 gap-1">
<!-- 头像 -->
<div class="self-center h-38px">
<n-badge>
<n-avatar :size="40" :src="publisherAvatar" :fallback-src="getFallbackAvatar()" round />
</n-badge>
</div>
<!-- 发布人信息 -->
<div class="truncate pl-4 flex gap-10px flex-col">
<div class="text-14px leading-tight font-bold flex-1 truncate text-#333">
{{ publisherName }}
<n-card
v-else-if="announcement"
class="rounded-15px"
header-class="p-10px!"
:segmented="{ content: true, footer: 'soft' }">
<template #header>
<div class="grid grid-cols-[2.2rem_1fr_4rem] items-start px-2 py-3 gap-1">
<div class="self-center h-38px">
<n-badge>
<n-avatar :size="40" :src="publisherAvatar" :fallback-src="getFallbackAvatar()" round />
</n-badge>
</div>
<div class="text-12px text-#333">
{{ formatTimestamp(announcement.createTime) }}
<!-- 发布人信息 -->
<div class="truncate pl-4 flex gap-10px flex-col">
<div class="text-14px leading-tight font-bold flex-1 truncate">
{{ publisherName }}
</div>
<n-text depth="3" class="text-12px">
{{ formatTimestamp(announcement.createTime) }}
</n-text>
</div>
</div>
</template>
<!-- 阅读统计 -->
<template #header-extra>
<div class="justify-self-end self-center text-12px text-right flex gap-1 items-center">
<span class="text-#13987F">{{ announcement.readCount || 0 }}人已读</span>
</div>
</div>
</template>
<!-- 公告内容 -->
<div class="announcement-content whitespace-pre-wrap break-words text-14px leading-6 text-#333">
<n-text>
{{ announcement.content }}
</div>
</n-text>
<!-- 编辑按钮仅管理员/群主可见 -->
<div v-if="canEdit" class="flex justify-center mb-10px">
<div
@click="goToNoticeEdit"
style="
background: linear-gradient(145deg, #7eb7ac, #6fb0a4, #5fa89c);
border-radius: 30px;
padding: 10px 30px;
color: white;
font-weight: 500;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
text-align: center;
display: inline-block;
cursor: pointer;
">
编辑公告
<template #action v-if="canEdit">
<div class="flex justify-center">
<n-button strong secondary type="primary" class="m-auto" @click="goToNoticeEdit">编辑公告</n-button>
</div>
</div>
</div>
</template>
</n-card>
<div v-else class="flex justify-center items-center h-200px text-#909090">公告不存在或已被删除</div>
</div>

View File

@@ -4,50 +4,36 @@
<HeaderBar
:isOfficial="false"
class="bg-white"
style="border-bottom: 1px solid; border-color: #dfdfdf"
border
:hidden-right="true"
:room-name="isEditMode ? '编辑群公告' : '新增群公告'" />
</template>
<template #container>
<div
class="bg-[url('@/assets/mobile/chat-home/background.webp')] bg-cover bg-center flex flex-col overflow-auto h-full">
<div class="flex flex-col flex-1 gap-20px py-15px px-20px">
<!-- 公告内容编辑区域 -->
<div class="bg-white rounded-15px p-15px shadow">
<n-form label-placement="top" size="medium">
<n-form-item label="公告内容" required>
<n-input
v-model:value="announcementContent"
type="textarea"
placeholder="请输入公告内容..."
class="w-full"
:autosize="announcementAutosize"
:maxlength="1000"
:show-count="true" />
</n-form-item>
<n-card :bordered="false" class="h-full" content-class="p-10px!">
<n-input
v-model:value="announcementContent"
type="textarea"
placeholder="请输入公告内容..."
class="w-full p-0! m-0!"
:autosize="announcementAutosize"
:maxlength="1000"
:show-count="true" />
<n-form-item label="上传图片(暂不支持)">
<div class="upload-image-container">
<n-upload
action="https://www.mocky.io/v2/5e4bafc63100007100d8b70f"
list-type="image-card"
:max="4"
disabled>
<div class="upload-trigger">
<svg class="size-24px text-#999">
<use href="#plus"></use>
</svg>
<span class="text-12px text-#999 mt-5px">点击上传</span>
</div>
</n-upload>
</div>
</n-form-item>
</n-form>
</div>
<div class="upload-image-container pt-10px">
<n-upload action="https://www.mocky.io/v2/5e4bafc63100007100d8b70f" list-type="image-card" :max="4" disabled>
<div class="upload-trigger">
<svg class="size-24px text-#999">
<use href="#plus"></use>
</svg>
<span class="text-12px text-#999 mt-5px">点击上传</span>
</div>
</n-upload>
</div>
<!-- 置顶设置区域 -->
<div class="bg-white rounded-15px shadow">
<template #action>
<div class="pb-10px">
<!-- 置顶设置区域 -->
<div class="flex flex-col w-full">
<div class="flex justify-between py-15px px-15px items-center border-b border-gray-200">
<div class="flex flex-col">
@@ -57,17 +43,27 @@
<n-switch v-model:value="top" />
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="flex justify-center gap-15px">
<n-button type="default" class="w-40%" size="large" @click="handleCancel">取消</n-button>
<n-button type="primary" class="w-40%" size="large" @click="handleSubmit" :loading="submitting">
保存
</n-button>
<!-- 操作按钮 -->
<div class="flex justify-center gap-15px">
<n-button type="default" class="w-40%" strong secondary round size="large" @click="handleCancel">
取消
</n-button>
<n-button
type="primary"
class="w-40%"
strong
secondary
round
size="large"
@click="handleSubmit"
:loading="submitting">
保存
</n-button>
</div>
</div>
</div>
</div>
</template>
</n-card>
</template>
</AutoFixHeightPage>
</template>
@@ -172,22 +168,8 @@ onMounted(() => {
</script>
<style scoped>
/* 按钮样式优化 */
.n-button {
border-radius: 30px;
font-weight: 500;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
transition: all 0.3s;
}
.n-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
}
.n-button--primary {
background: linear-gradient(145deg, #7eb7ac, #6fb0a4, #5fa89c);
border: none;
:deep(.n-card > .n-card__action) {
padding: 0 !important;
}
/* 上传图片组件样式优化 */

View File

@@ -1,47 +1,46 @@
<template>
<AutoFixHeightPage :show-footer="false">
<template #header>
<HeaderBar
:isOfficial="false"
class="bg-white"
style="border-bottom: 1px solid; border-color: #dfdfdf"
:hidden-right="true"
room-name="群公告" />
<HeaderBar :isOfficial="false" class="bg-white" border :hidden-right="true" room-name="群公告" />
</template>
<template #container>
<div
class="bg-[url('@/assets/mobile/chat-home/background.webp')] bg-cover bg-center flex flex-col overflow-auto h-full relative">
<div class="flex flex-col overflow-auto h-full relative">
<img
src="@/assets/mobile/chat-home/background.webp"
class="absolute t-0 l-0 w-full h-full z-0 dark:opacity-20" />
<div class="flex flex-col flex-1 gap-15px py-15px px-20px">
<RecycleScroller :items="announList" :item-size="15" key-field="id" class="flex flex-col gap-15px">
<template #default="{ item }">
<!-- 公告内容块 -->
<div @click="goToNoticeDetail(item.id)" class="shadow flex p-15px bg-white rounded-10px">
<div class="flex flex-col w-full gap-10px">
<!-- 时间/阅读人数 -->
<div class="flex items-center justify-between text-14px">
<span class="flex gap-5px">
<span class="text-#717171">发布人:</span>
<span class="text-black">{{ groupStore.getUserInfo(item.uid)?.name }}</span>
</span>
<span
v-if="item.isTop"
class="text-#13987F rounded-15px px-7px py-5px text-12px"
style="border: 1px solid; border-color: #13987f">
置顶
</span>
</div>
<!-- 公告内容 -->
<div class="text-14px line-clamp-3 line-height-20px text-#717171 max-h-60px">
{{ item.content }}
</div>
<n-card content-class="p-15px!" class="rounded-10px">
<div @click="goToNoticeDetail(item.id)">
<div class="flex flex-col w-full gap-10px">
<!-- 时间/阅读人数 -->
<div class="flex items-center justify-between text-14px">
<span class="flex gap-5px">
<span class="text-#717171">发布人:</span>
<span class="text-black dark:text-white/80">{{ groupStore.getUserInfo(item.uid)?.name }}</span>
</span>
<span
v-if="item.isTop"
class="text-#13987F rounded-15px px-7px py-5px text-12px"
style="border: 1px solid; border-color: #13987f">
置顶
</span>
</div>
<!-- 公告内容 -->
<div class="text-14px line-clamp-3 line-height-20px text-#717171 max-h-60px">
{{ item.content }}
</div>
<div class="flex items-center justify-between text-12px">
<span class="flex gap-5px text-#717171">{{ formatTimestamp(item.createTime) }}</span>
<span class="text-#13987F">128人已读</span>
<div class="flex items-center justify-between text-12px">
<span class="flex gap-5px text-#717171">{{ formatTimestamp(item.createTime) }}</span>
<span class="text-#13987F">128人已读</span>
</div>
</div>
</div>
</div>
</n-card>
</template>
</RecycleScroller>
</div>

View File

@@ -1,17 +1,16 @@
<template>
<div class="flex flex-col h-full flex-1 bg-white">
<div class="flex flex-col h-full flex-1">
<!-- 固定顶部区域 -->
<Transition name="header-fade">
<div
v-if="isHeaderFixed"
class="fixed backdrop-blur-md left-0 right-0 z-100 flex items-center justify-between"
class="fixed backdrop-blur-md left-0 right-0 z-100 flex items-center justify-between bg-[rgba(255,255,255,0.8)] dark:bg-[rgba(20,20,20,0.8)]"
:style="{
height: 'env(safe-area-inset-top, 0px)',
paddingTop: '36px',
background: 'rgba(220, 220, 220, 0.6)'
paddingTop: '36px'
}">
<div class="pl-24px pt-16px">
<n-button text @click="openNotificationPopup" class="relative text-#303030">
<n-button text @click="openNotificationPopup" class="relative text-#303030 dark:invert">
<template #icon>
<div class="relative">
<svg class="size-22px" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -31,7 +30,7 @@
</template>
</n-button>
</div>
<div class="flex-1 flex justify-center pt-12px text-16px font-600 text-#303030">
<div class="flex-1 flex justify-center pt-12px text-16px font-600 text-#303030 dark:text-white">
{{ t('dynamic.page.mobile_title') }}
</div>
<div class="pr-24px opacity-0 pointer-events-none">

View File

@@ -1,86 +1,97 @@
<template>
<div class="flex flex-col h-full">
<HeaderBar
:isOfficial="false"
:hidden-right="true"
:enable-default-background="false"
:enable-shadow="false"
room-name="用户资料" />
<AutoFixHeightPage :show-footer="false">
<template #header>
<HeaderBar
:isOfficial="false"
:hidden-right="true"
:enable-default-background="false"
:enable-shadow="false"
room-name="用户资料" />
</template>
<img src="@/assets/mobile/chat-home/background.webp" class="w-100% fixed top-0" alt="hula" />
<template #container>
<div class="flex flex-col overflow-auto h-full relative">
<img
src="@/assets/mobile/chat-home/background.webp"
class="absolute fixed top-0 left-0 w-full h-full z-0 dark:opacity-20" />
<PersonalInfo :is-my-page="isMyPage" :is-show="isShow"></PersonalInfo>
<PersonalInfo :is-my-page="isMyPage" :is-show="isShow"></PersonalInfo>
<div class="relative top-0 flex-1 flex">
<div ref="measureRef" class="h-full w-full absolute top-0 z-0"></div>
<div class="top-0 flex-1 flex w-full border-#13987F border-1">
<div ref="measureRef" class="h-full w-full absolute top-0 z-0"></div>
<div ref="scrollContainer" :style="{ height: tabHeight + 'px' }" class="z-1 overflow-y-auto absolute z-3">
<div class="custom-rounded flex px-24px flex-col gap-4 z-1 p-10px mt-4 shadow">
<CommunityTab
:style="{ height: tabHeight - 10 + 'px' }"
:custom-height="tabHeight - 10"
@scroll="handleScroll"
@update="onUpdate"
:options="tabOptions"
active-tab-name="find">
<template #find>
<!-- 加载状态 -->
<div
v-if="feedOptions.isLoading && feedList.length === 0"
class="flex justify-center items-center py-20px">
<n-spin size="large" />
</div>
<div
ref="scrollContainer"
:style="{ height: tabHeight + 'px' }"
class="w-full z-1 overflow-y-auto absolute z-3">
<div class="custom-rounded flex px-24px flex-col gap-4 z-1 p-10px mt-4 shadow">
<CommunityTab
:style="{ height: tabHeight - 10 + 'px' }"
:custom-height="tabHeight - 10"
@scroll="handleScroll"
@update="onUpdate"
:options="tabOptions"
active-tab-name="find">
<template #find>
<!-- 加载状态 -->
<div
v-if="feedOptions.isLoading && feedList.length === 0"
class="flex justify-center items-center py-20px">
<n-spin size="large" />
</div>
<!-- 空状态 -->
<div v-else-if="feedList.length === 0" class="flex justify-center items-center py-40px text-gray-500">
暂无动态
</div>
<!-- 空状态 -->
<div v-else-if="feedList.length === 0" class="flex justify-center items-center py-40px text-gray-500">
暂无动态
</div>
<!-- 动态列表 -->
<template v-else>
<CommunityContent v-for="item in feedList" :key="item.id" :feed-item="item" />
<!-- 动态列表 -->
<template v-else>
<CommunityContent v-for="item in feedList" :key="item.id" :feed-item="item" />
<!-- 加载更多 -->
<div v-if="!feedOptions.isLast" class="flex justify-center py-15px">
<n-button :loading="feedOptions.isLoading" @click="loadMore" type="primary" text size="small">
{{ feedOptions.isLoading ? '加载中...' : '加载更多' }}
</n-button>
</div>
<!-- 加载更多 -->
<div v-if="!feedOptions.isLast" class="flex justify-center py-15px">
<n-button :loading="feedOptions.isLoading" @click="loadMore" type="primary" text size="small">
{{ feedOptions.isLoading ? '加载中...' : '加载更多' }}
</n-button>
</div>
<!-- 已加载全部 -->
<div v-else class="flex justify-center py-15px text-12px text-gray-400">已加载全部</div>
</template>
</template>
<!-- 已加载全部 -->
<div v-else class="flex justify-center py-15px text-12px text-gray-400">已加载全部</div>
</template>
</template>
<template #follow>
<!-- 赞过的动态 -->
<div
v-if="feedOptions.isLoading && feedList.length === 0"
class="flex justify-center items-center py-20px">
<n-spin size="large" />
</div>
<template #follow>
<!-- 赞过的动态 -->
<div
v-if="feedOptions.isLoading && feedList.length === 0"
class="flex justify-center items-center py-20px">
<n-spin size="large" />
</div>
<div v-else-if="feedList.length === 0" class="flex justify-center items-center py-40px text-gray-500">
暂无赞过的动态
</div>
<div v-else-if="feedList.length === 0" class="flex justify-center items-center py-40px text-gray-500">
暂无赞过的动态
</div>
<template v-else>
<CommunityContent v-for="item in feedList" :key="item.id" :feed-item="item" />
<template v-else>
<CommunityContent v-for="item in feedList" :key="item.id" :feed-item="item" />
<div v-if="!feedOptions.isLast" class="flex justify-center py-15px">
<n-button :loading="feedOptions.isLoading" @click="loadMore" type="primary" text size="small">
{{ feedOptions.isLoading ? '加载中...' : '加载更多' }}
</n-button>
</div>
<div v-if="!feedOptions.isLast" class="flex justify-center py-15px">
<n-button :loading="feedOptions.isLoading" @click="loadMore" type="primary" text size="small">
{{ feedOptions.isLoading ? '加载中...' : '加载更多' }}
</n-button>
</div>
<div v-else class="flex justify-center py-15px text-12px text-gray-400">已加载全部</div>
</template>
</template>
</CommunityTab>
<div v-else class="flex justify-center py-15px text-12px text-gray-400">已加载全部</div>
</template>
</template>
</CommunityTab>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
</AutoFixHeightPage>
</template>
<script setup lang="ts">
import { storeToRefs } from 'pinia'

View File

@@ -1,7 +1,8 @@
<template>
<div class="flex flex-col h-full flex-1">
<img src="@/assets/mobile/chat-home/background.webp" class="w-100% fixed top-0" alt="hula" />
<div class="flex flex-col overflow-auto h-full relative">
<img
src="@/assets/mobile/chat-home/background.webp"
class="absolute fixed top-0 left-0 w-full h-full z-0 dark:opacity-20" />
<!-- 页面蒙板 -->
<div
v-if="showMask"
@@ -11,7 +12,9 @@
<!-- 导航条 -->
<NavBar>
<template #center>{{ t('mobile_contact.title') }}</template>
<template #center>
<n-text>{{ t('mobile_contact.title') }}</n-text>
</template>
<template #right>
<n-dropdown
@on-clickoutside="addIconHandler.clickOutside"
@@ -19,9 +22,13 @@
trigger="click"
:show-arrow="true"
:options="uiViewsData.addOptions">
<svg @click="addIconHandler.open" class="size-22px bg-white p-5px rounded-8px">
<use href="#plus"></use>
</svg>
<n-button round strong secondary @click="addIconHandler.open">
<template #icon>
<n-icon>
<svg><use href="#plus"></use></svg>
</n-icon>
</template>
</n-button>
</n-dropdown>
</template>
</NavBar>
@@ -30,7 +37,7 @@
<div class="px-16px mt-2 mb-12px z-1">
<n-input
id="search"
class="rounded-6px w-full bg-white relative text-12px"
class="rounded-6px w-full relative text-12px"
:maxlength="20"
clearable
spellCheck="false"
@@ -44,11 +51,13 @@
</n-input>
</div>
<div class="flex flex-1 gap-2 flex-col bg-white z-1 custom-rounded">
<!-- 我的消息条 -->
<div
class="grid grid-cols-[4rem_1fr_24px] items-center gap-8px py-15px px-16px border-b-[1px] border-b-solid border-b-[#e5e7eb]">
<div class="h-full flex items-center text-14px">{{ t('mobile_contact.my_chat') }}</div>
<n-card
:segmented="{ content: true, footer: 'soft' }"
:bordered="false"
class="custom-rounded flex-1"
header-class="py-15px! px-16px! text-14px!"
:title="t('mobile_contact.my_chat')">
<template #header-extra>
<div @click="toMessage" class="h-full flex items-center justify-end">
<span
v-if="contactUnreadCount > 0"
@@ -57,9 +66,9 @@
</span>
</div>
<div @click="toMessage" class="h-full flex justify-end items-center">
<img src="@/assets/mobile/friend/right-arrow.webp" class="block h-20px" alt="" />
<img src="@/assets/mobile/friend/right-arrow.webp" class="block h-20px dark:invert" alt="" />
</div>
</div>
</template>
<n-tabs type="segment" animated class="mt-4px p-[4px_10px_0px_8px]">
<n-tab-pane name="1" :tab="t('mobile_contact.tab.contacts')">
@@ -146,7 +155,7 @@
</n-collapse>
</n-tab-pane>
</n-tabs>
</div>
</n-card>
</div>
</template>
<style scoped>
@@ -182,7 +191,8 @@ const renderImgIcon = (src: string) => {
return () =>
h('img', {
src,
style: 'display:block; width: 24px; height: 24px; vertical-align: middle'
style: 'display:block; width: 24px; height: 24px; vertical-align: middle',
class: 'dark:invert'
})
}

View File

@@ -1,6 +1,8 @@
<template>
<div class="flex flex-col h-full">
<img src="@/assets/mobile/chat-home/background.webp" class="w-100% fixed top-0" alt="hula" />
<div class="flex flex-col overflow-auto h-full relative">
<img
src="@/assets/mobile/chat-home/background.webp"
class="absolute fixed top-0 l-0 w-full h-full z-0 dark:opacity-20" />
<!-- 页面蒙板 -->
<div
@@ -53,9 +55,16 @@
trigger="click"
:show-arrow="true"
:options="uiViewsData.addOptions">
<svg @click="addIconHandler.open" class="size-22px bg-white p-5px rounded-8px">
<n-button round strong secondary @click="addIconHandler.open">
<template #icon>
<n-icon>
<svg><use href="#plus"></use></svg>
</n-icon>
</template>
</n-button>
<!-- <svg @click="addIconHandler.open" class="size-22px p-5px rounded-8px">
<use href="#plus"></use>
</svg>
</svg> -->
</n-dropdown>
</template>
</NavBar>
@@ -64,7 +73,7 @@
<div class="py-5px shrink-0">
<n-input
id="search"
class="rounded-6px w-full bg-white relative text-12px"
class="rounded-6px w-full relative text-12px"
:maxlength="20"
clearable
spellCheck="false"
@@ -81,7 +90,7 @@
</template>
</n-input>
</div>
<div class="border-b-1 border-solid color-gray-200 px-18px mt-5px"></div>
<n-divider class="m-0! p-0! mt-10px!" />
</div>
<van-pull-refresh
@@ -115,8 +124,8 @@
</div>
<!-- 中间两行内容 -->
<div class="truncate pl-7 flex pt-5px gap-10px leading-tight flex-col">
<div class="text-16px font-bold flex-1 truncate text-#333 truncate">{{ item.name }}</div>
<div class="text-13px text-#555 truncate">
<n-text class="text-16px font-bold flex-1 truncate">{{ item.name }}</n-text>
<div class="text-13px text-gray-600 dark:text-gray-400 truncate">
{{ item.text }}
</div>
</div>
@@ -129,12 +138,12 @@
<use href="#auth"></use>
</svg>
</span>
<span class="text-#555 whitespace-nowrap">
<span class="text-gray-600 whitespace-nowrap">
{{ formatTimestamp(item?.activeTime) }}
</span>
</div>
<div v-if="item.muteNotification === NotificationTypeEnum.NOT_DISTURB">
<svg class="size-14px z-100 color-#909090">
<svg class="size-14px z-100 color-gray-500/90">
<use href="#close-remind"></use>
</svg>
</div>
@@ -447,7 +456,8 @@ const renderImgIcon = (src: string) => {
return () =>
h('img', {
src,
style: 'display:block; width: 26px; height: 26px; vertical-align: middle;'
style: 'display:block; width: 26px; height: 26px; vertical-align: middle;',
class: 'dark:invert'
})
}

View File

@@ -1,147 +1,220 @@
<template>
<div class="flex flex-1 flex-col">
<img src="@/assets/mobile/chat-home/background.webp" class="w-100% fixed z-0 top-0" alt="hula" />
<AutoFixHeightPage :show-footer="false" class="z-1">
<template #header>
<HeaderBar
:isOfficial="false"
:hidden-right="true"
:enable-default-background="false"
:enable-shadow="false"
:room-name="t('mobile_edit_profile.title')" />
</template>
<template #container>
<div class="flex flex-col gap-1 overflow-auto h-full">
<div class="flex flex-col p-[0px_20px_20px_20px] gap-15px">
<!-- 头像 -->
<div class="flex justify-center">
<div class="rounded-full relative bg-white w-86px h-86px overflow-hidden" @click="openAvatarCropper">
<n-avatar
class="absolute"
:size="86"
:src="AvatarUtils.getAvatarUrl(localUserInfo.avatar!)"
fallback-src="/logo.png"
round />
<div
class="absolute h-50% w-full bottom-0 bg-[rgb(50,50,50)] bg-clip-padding backdrop-filter backdrop-blur-sm bg-opacity-15 backdrop-saturate-100 backdrop-contrast-100"></div>
<div class="absolute bottom-25% text-center w-full text-12px text-white">
{{ t('mobile_edit_profile.change_avatar') }}
</div>
<AutoFixHeightPage :show-footer="false" class="z-1">
<template #header>
<HeaderBar
:isOfficial="false"
:hidden-right="true"
:enable-default-background="false"
:enable-shadow="false"
:room-name="t('mobile_edit_profile.title')" />
</template>
<template #container>
<div class="flex flex-col overflow-auto h-full relative">
<img
src="@/assets/mobile/chat-home/background.webp"
class="absolute fixed top-0 left-0 w-full h-full z-0 dark:opacity-20" />
<div class="p-20px">
<!-- 头像 -->
<div class="flex justify-center mb-50px">
<div class="rounded-full relative bg-white w-86px h-86px overflow-hidden" @click="openAvatarCropper">
<n-avatar
class="absolute"
:size="86"
:src="AvatarUtils.getAvatarUrl(localUserInfo.avatar!)"
fallback-src="/logo.png"
round />
<div
class="absolute h-50% w-full bottom-0 bg-[rgb(50,50,50)] bg-clip-padding backdrop-filter backdrop-blur-sm bg-opacity-15 backdrop-saturate-100 backdrop-contrast-100"></div>
<div class="absolute bottom-25% text-center w-full text-12px text-white">
{{ t('mobile_edit_profile.change_avatar') }}
</div>
<input
ref="fileInput"
type="file"
accept="image/jpeg,image/png,image/webp"
class="hidden"
@change="handleFileChange" />
<AvatarCropper
ref="cropperRef"
v-model:show="showCropper"
:image-url="localImageUrl"
@crop="handleCrop" />
</div>
<!-- 个人信息 -->
<van-form @submit="saveEditInfo">
<van-cell-group class="shadow" inset>
<!-- 昵称 -->
<van-field
:disabled="true"
v-model="localUserInfo.name"
name="昵称"
:label="t('mobile_edit_profile.nickname')"
<input
ref="fileInput"
type="file"
accept="image/jpeg,image/png,image/webp"
class="hidden"
@change="handleFileChange" />
<AvatarCropper ref="cropperRef" v-model:show="showCropper" :image-url="localImageUrl" @crop="handleCrop" />
</div>
<!-- 个人信息 -->
<n-card class="p-0! rounded-16px">
<n-form @submit="saveEditInfo" label-placement="left" label-align="left" :label-width="80">
<n-form-item :label="t('mobile_edit_profile.nickname')">
<n-input
readonly
v-model:value="localUserInfo.name"
:placeholder="t('mobile_edit_profile.placeholder.nickname')"
:rules="[{ required: true, message: t('mobile_edit_profile.placeholder.nickname') }]" />
class="bg-transparent!" />
</n-form-item>
<!-- 性别 -->
<van-field
v-model="genderText"
is-link
<n-divider class="my-3! p-0!" />
<n-form-item
:label="t('mobile_edit_profile.gender')"
:placeholder="t('mobile_edit_profile.placeholder.gender')">
<n-input
@click="pickerState.gender = true"
v-model:value="genderText"
readonly
name="picker"
:label="t('mobile_edit_profile.gender')"
:placeholder="t('mobile_edit_profile.placeholder.gender')"
@click="pickerState.gender = true" />
class="bg-transparent!" />
</n-form-item>
<van-popup v-model:show="pickerState.gender" position="bottom">
<van-picker
:columns="pickerColumn.gender"
@confirm="pickerConfirm.gender"
@cancel="pickerState.gender = false" />
</van-popup>
<n-divider class="my-3! p-0!" />
<!-- 生日 -->
<van-field
v-model="birthday"
:name="t('mobile_edit_profile.brithday')"
:label="t('mobile_edit_profile.brithday')"
:placeholder="t('mobile_edit_profile.placeholder.brithday')"
is-link
<n-form-item
:label="t('mobile_edit_profile.brithday')"
:placeholder="t('mobile_edit_profile.placeholder.brithday')">
<n-input
@click="toEditBirthday"
v-model:value="birthday"
readonly
@click="toEditBirthday" />
<!-- 地区 -->
<van-field
v-model="region"
is-link
readonly
name="area"
:label="t('mobile_edit_profile.brithday')"
:placeholder="t('mobile_edit_profile.placeholder.brithday')"
@click="pickerState.region = true" />
<van-popup v-model:show="pickerState.region" position="bottom">
<van-area
:area-list="areaList"
@confirm="pickerConfirm.region"
@cancel="pickerState.region = false" />
</van-popup>
class="bg-transparent!" />
</n-form-item>
<!-- 手机号 -->
<van-field
:disabled="true"
v-model="localUserInfo.phone"
type="tel"
name="手机号"
:label="t('mobile_edit_profile.phone')"
<n-divider class="my-3! p-0!" />
<n-form-item label="地区" :placeholder="t('mobile_edit_profile.placeholder.brithday')">
<n-input
@click="pickerState.region = true"
v-model:value="region"
readonly
:placeholder="t('mobile_edit_profile.placeholder.brithday')"
class="bg-transparent!" />
</n-form-item>
<n-divider class="my-3! p-0!" />
<n-form-item
disabled
:label="t('mobile_edit_profile.phone')"
:placeholder="t('mobile_edit_profile.placeholder.phone')">
<n-input
disabled
readonly
v-model:value="localUserInfo.phone"
:placeholder="t('mobile_edit_profile.placeholder.phone')"
:rules="[{ required: false, message: '请填写手机号' }]" />
class="bg-transparent!" />
</n-form-item>
<!-- 简介 -->
<van-field
v-model="localUserInfo.resume"
name="简介"
:label="t('mobile_edit_profile.bio')"
:placeholder="t('mobile_edit_profile.placeholder.bio')"
<n-divider class="my-3! p-0!" />
<n-form-item
disabled
:label="t('mobile_edit_profile.bio')"
:placeholder="t('mobile_edit_profile.placeholder.bio')">
<n-input
type="textarea"
rows="3"
autosize
@click="toEditBio" />
</van-cell-group>
v-model:value="localUserInfo.resume"
:placeholder="t('mobile_edit_profile.placeholder.bio')"
class="bg-transparent!"
@click="toEditBio"
readonly />
</n-form-item>
<!-- <van-cell-group class="shadow" inset> -->
<!-- 昵称 -->
<!-- <van-field
:disabled="true"
v-model="localUserInfo.name"
name="昵称"
:label="t('mobile_edit_profile.nickname')"
:placeholder="t('mobile_edit_profile.placeholder.nickname')"
:rules="[{ required: true, message: t('mobile_edit_profile.placeholder.nickname') }]" /> -->
<!-- 性别 -->
<!-- <van-field
v-model="genderText"
is-link
readonly
name="picker"
:label="t('mobile_edit_profile.gender')"
:placeholder="t('mobile_edit_profile.placeholder.gender')"
@click="pickerState.gender = true" /> -->
<n-drawer
v-model:show="pickerState.gender"
class="rounded-t-20px! overflow-hidden"
position="bottom"
round
placement="bottom"
default-height="300px">
<van-picker
:columns="pickerColumn.gender"
@confirm="pickerConfirm.gender"
@cancel="pickerState.gender = false" />
</n-drawer>
<!-- <van-popup v-model:show="pickerState.gender" position="bottom">
<van-picker
:columns="pickerColumn.gender"
@confirm="pickerConfirm.gender"
@cancel="pickerState.gender = false" />
</van-popup> -->
<!-- 生日 -->
<!-- <van-field
v-model="birthday"
:name="t('mobile_edit_profile.brithday')"
:label="t('mobile_edit_profile.brithday')"
:placeholder="t('mobile_edit_profile.placeholder.brithday')"
is-link
readonly
@click="toEditBirthday" /> -->
<!-- 地区 -->
<!-- <van-field
v-model="region"
is-link
readonly
name="area"
:label="t('mobile_edit_profile.brithday')"
:placeholder="t('mobile_edit_profile.placeholder.brithday')"
@click="pickerState.region = true" />
-->
<n-drawer
v-model:show="pickerState.region"
class="rounded-t-20px! overflow-hidden"
position="bottom"
round
placement="bottom"
default-height="300px">
<van-area :area-list="areaList" @confirm="pickerConfirm.region" @cancel="pickerState.region = false" />
</n-drawer>
<!-- 手机号 -->
<!-- <van-field
:disabled="true"
v-model="localUserInfo.phone"
type="tel"
name="手机号"
:label="t('mobile_edit_profile.phone')"
:placeholder="t('mobile_edit_profile.placeholder.phone')"
:rules="[{ required: false, message: '请填写手机号' }]" /> -->
<!-- 简介 -->
<!-- <van-field
v-model="localUserInfo.resume"
name="简介"
:label="t('mobile_edit_profile.bio')"
:placeholder="t('mobile_edit_profile.placeholder.bio')"
type="textarea"
rows="3"
autosize
@click="toEditBio" /> -->
<!-- </van-cell-group> -->
<div class="flex justify-center mt-20px">
<button
class=""
style="
background: linear-gradient(145deg, #7eb7ac, #6fb0a4, #5fa89c);
border-radius: 30px;
padding: 10px 30px;
color: white;
font-weight: 500;
border: none;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
text-align: center;
display: inline-block;
"
type="submit">
<n-button block attr-type="submit" type="primary" strong secondary round>
{{ t('mobile_edit_profile.save_btn') }}
</button>
</n-button>
</div>
</van-form>
</div>
</n-form>
</n-card>
</div>
</template>
</AutoFixHeightPage>
</div>
</div>
</template>
</AutoFixHeightPage>
</template>
<script setup lang="ts">

View File

@@ -1,12 +1,7 @@
<template>
<AutoFixHeightPage :show-footer="false">
<template #header>
<HeaderBar
:isOfficial="false"
class="bg-white"
style="border-bottom: 1px solid; border-color: #dfdfdf"
:hidden-right="true"
:room-name="t('mobile_setting.title')" />
<HeaderBar border :isOfficial="false" :hidden-right="true" :room-name="t('mobile_setting.title')" />
</template>
<template #container>
@@ -17,7 +12,7 @@
<div
v-for="item in settings"
:key="item.key"
class="flex justify-between items-center bg-white p-12px rounded-lg shadow-sm">
class="flex justify-between items-center bg-card text-card-foreground ring-1 p-12px rounded-lg shadow-sm">
<div class="text-base">{{ item.label }}</div>
<div>
<!-- 根据 type 渲染对应组件 -->

View File

@@ -1,16 +1,11 @@
<template>
<AutoFixHeightPage :show-footer="false">
<template #header>
<HeaderBar
:isOfficial="false"
class="bg-white"
style="border-bottom: 1px solid; border-color: #dfdfdf"
:hidden-right="true"
:room-name="t('mobile_mymessage.title')" />
<HeaderBar :isOfficial="false" border :hidden-right="true" :room-name="t('mobile_mymessage.title')" />
</template>
<template #container>
<div class="flex flex-col bg-#fefefe gap-1 overflow-auto h-full">
<div class="flex flex-col overflow-auto h-full relative">
<div class="flex flex-col p-[10px_20px_0px_20px] gap-20px">
<CommunityTab @update="onUpdate" :options="tabOptions" active-tab-name="friend-message">
<template #friend-message>

View File

@@ -1,20 +1,18 @@
<template>
<AutoFixHeightPage :show-footer="false">
<template #header>
<HeaderBar
:isOfficial="false"
class="bg-white"
style="border-bottom: 1px solid; border-color: #dfdfdf"
:hidden-right="true"
:room-name="t('mobile_post.title')" />
<HeaderBar :isOfficial="false" border :hidden-right="true" :room-name="t('mobile_post.title')" />
</template>
<template #container>
<div class="flex flex-col gap-1 overflow-auto h-full bg-#f5f5f5">
<div class="flex flex-col gap-1 overflow-auto h-full">
<div class="flex flex-col p-16px gap-12px">
<!-- 动态内容输入 -->
<div class="bg-white rounded-12px p-16px">
<div class="text-14px text-#333 mb-8px font-500">{{ t('mobile_post.content.label') }}</div>
<n-card
class="rounded-12px p-0!"
content-class="p-16px!"
header-class="text-14px! pb-0!"
:title="t('mobile_post.content.label')">
<!-- 动态内容输入 -->
<van-field
v-model="feedContent"
type="textarea"
@@ -22,177 +20,171 @@
:maxlength="500"
show-word-limit
:rows="8"
class="bg-transparent!"
:autosize="feedAutosize" />
</div>
</n-card>
<!-- 媒体类型提示暂时禁用 -->
<div class="bg-white rounded-12px p-16px">
<div class="text-14px text-#333 mb-8px font-500">{{ t('mobile_post.media_type.label') }}</div>
<div class="text-13px text-#999">
<div class="flex items-center gap-8px mb-6px">
<span class="text-#c8c9cc">📷</span>
<span class="text-#c8c9cc">{{ t('mobile_post.media_type.option_image') }}</span>
</div>
<div class="flex items-center gap-8px">
<span class="text-#c8c9cc">🎬</span>
<span class="text-#c8c9cc">{{ t('mobile_post.media_type.option_video') }}</span>
</div>
<!-- <div class="bg-white rounded-12px p-16px"> -->
<n-card
class="p-0! rounded-12px"
content-class="p-16px!"
header-class="text-14px! pb-0!"
:title="t('mobile_post.media_type.label')">
<div class="flex items-center gap-8px mb-6px">
<n-text depth="3" s>📷</n-text>
<n-text depth="3">{{ t('mobile_post.media_type.option_image') }}</n-text>
</div>
</div>
<div class="flex items-center gap-8px">
<n-text depth="3">🎬</n-text>
<n-text depth="3">{{ t('mobile_post.media_type.option_video') }}</n-text>
</div>
</n-card>
<!-- </div> -->
<!-- 权限选择 -->
<div class="bg-white rounded-12px p-16px">
<div class="text-14px text-#333 mb-12px font-500">{{ t('mobile_post.visibility.label') }}</div>
<van-radio-group v-model="permission" direction="vertical" @change="handlePermissionChange">
<van-radio name="open" icon-size="18px" class="mb-12px">
<template #icon="props">
<div
:class="[
'w-20px h-20px rounded-full border-2 flex items-center justify-center transition-all',
props.checked ? 'border-#13987f bg-#13987f' : 'border-#c8c9cc'
]">
<div v-if="props.checked" class="w-8px h-8px rounded-full bg-white"></div>
</div>
</template>
<span class="ml-8px text-14px">{{ t('mobile_post.visibility.public') }}</span>
</van-radio>
<van-radio name="partVisible" icon-size="18px" class="mb-12px">
<template #icon="props">
<div
:class="[
'w-20px h-20px rounded-full border-2 flex items-center justify-center transition-all',
props.checked ? 'border-#13987f bg-#13987f' : 'border-#c8c9cc'
]">
<div v-if="props.checked" class="w-8px h-8px rounded-full bg-white"></div>
</div>
</template>
<span class="ml-8px text-14px">{{ t('mobile_post.visibility.selected') }}</span>
</van-radio>
<van-radio name="notAnyone" icon-size="18px">
<template #icon="props">
<div
:class="[
'w-20px h-20px rounded-full border-2 flex items-center justify-center transition-all',
props.checked ? 'border-#13987f bg-#13987f' : 'border-#c8c9cc'
]">
<div v-if="props.checked" class="w-8px h-8px rounded-full bg-white"></div>
</div>
</template>
<span class="ml-8px text-14px">{{ t('mobile_post.visibility.exclude') }}</span>
</van-radio>
</van-radio-group>
</div>
<n-card
class="p-0! rounded-12px"
content-class="p-16px"
header-class="text-14px! pb-0!"
:title="t('mobile_post.visibility.label')">
<n-radio-group
:value="permission"
@update:value="
(e) => {
permission = e
handlePermissionChange(e)
}
">
<n-radio class="w-full" value="open">{{ t('mobile_post.visibility.public') }}</n-radio>
<n-radio class="w-full" value="partVisible">{{ t('mobile_post.visibility.selected') }}</n-radio>
<n-radio class="w-full" value="notAnyone">{{ t('mobile_post.visibility.exclude') }}</n-radio>
</n-radio-group>
</n-card>
<!-- 选择用户 -->
<div v-if="permission === 'partVisible' || permission === 'notAnyone'" class="bg-white rounded-12px p-16px">
<div class="text-14px text-#333 mb-12px font-500">
{{
permission === 'partVisible'
? t('mobile_post.visibility_selected_btn_label')
: t('mobile_post.visibility_exclude_btn_label')
}}
</div>
<van-button
type="primary"
<n-card
v-if="permission === 'partVisible' || permission === 'notAnyone'"
class="rounded-12px p-0!"
content-class="p-16px!"
header-class="text-14px! pb-0!"
:title="
permission === 'partVisible'
? t('mobile_post.visibility_selected_btn_label')
: t('mobile_post.visibility_exclude_btn_label')
">
<n-button
size="small"
strong
secondary
type="primary"
plain
@click="showUserSelectPopup = true"
class="w-full"
:style="{ borderColor: '#13987f', color: '#13987f' }">
{{ t('mobile_post.visibility_select_btn', { count: selectedUsers.length }) }}
</van-button>
</n-button>
<div v-if="selectedUsers.length > 0" class="mt-12px flex flex-wrap gap-8px">
<van-tag
<n-tag
v-for="user in selectedUsers"
:key="user.uid"
closeable
size="medium"
color="#e8f5f4"
type="success"
size="small"
round
closable
text-color="#13987f"
@close="removeSelectedUser(user.uid)">
{{ getUserName(user) }}
</van-tag>
</n-tag>
</div>
</div>
</n-card>
<!-- strong secondary round -->
<!-- 发布按钮 -->
<div class="flex gap-12px mt-8px pb-20px">
<van-button block plain @click="goBack" :style="{ borderColor: '#c8c9cc', color: '#666' }">
{{ t('mobile_post.btn.cancel') }}
</van-button>
<van-button
block
type="primary"
:loading="isPublishing"
:disabled="!isPublishValid"
@click="handlePublish"
:style="{ background: '#13987f', borderColor: '#13987f' }">
{{ t('mobile_post.btn.publish') }}
</van-button>
</div>
<n-grid :cols="2" x-gap="12">
<n-gi>
<n-button
block
secondary
round
size="large"
@click="goBack"
:style="{ borderColor: '#c8c9cc', color: '#666' }">
{{ t('mobile_post.btn.cancel') }}
</n-button>
</n-gi>
<n-gi>
<n-button
strong
secondary
round
block
size="large"
:loading="isPublishing"
:disabled="!isPublishValid"
@click="handlePublish"
:style="{ background: '#13987f', borderColor: '#13987f' }">
{{ t('mobile_post.btn.publish') }}
</n-button>
</n-gi>
</n-grid>
</div>
</div>
</template>
</AutoFixHeightPage>
<!-- 用户选择弹窗 -->
<van-popup v-model:show="showUserSelectPopup" position="bottom" :style="{ height: '70%' }" round>
<div class="flex flex-col h-full">
<!-- 弹窗标题 -->
<div class="flex items-center justify-between p-16px border-b border-#eee">
<span class="text-16px font-500 text-#333">{{ t('mobile_post.select_users.title') }}</span>
<van-button type="primary" size="small" @click="confirmUserSelection" :style="{ background: '#13987f' }">
{{ t('mobile_post.select_users.btn.done') }}
</van-button>
</div>
<n-drawer
v-model:show="showUserSelectPopup"
class="rounded-t-20px!"
:style="{ background: themeVars.cardColor }"
position="bottom"
round
placement="bottom"
default-height="80%">
<n-drawer-content>
<template #header>
<n-flex justify="space-between">
<n-h3>{{ t('mobile_post.select_users.title') }}</n-h3>
<n-button strong secondary type="primary" size="small" @click="confirmUserSelection">
{{ t('mobile_post.select_users.btn.done') }}
</n-button>
</n-flex>
</template>
<n-space vertical :size="16">
<!-- 搜索框 -->
<n-input
size="large"
v-model:value="userSearchKeyword"
:placeholder="t('mobile_post.select_users.search_placeholder')" />
<!-- 搜索框 -->
<div class="p-12px border-b border-#f5f5f5">
<van-search
v-model="userSearchKeyword"
:placeholder="t('mobile_post.select_users.search_placeholder')"
shape="round" />
</div>
<!-- 用户列表 -->
<div class="flex-1 overflow-y-auto">
<n-checkbox-group v-model:value="selectedUserIds">
<n-list hoverable clickable class="bg-transparent" :bordered="false">
<n-list-item v-for="user in filteredContactsList" :key="user.uid" @click="toggleUser(user.uid)">
<n-flex class="flex items-center justify-between w-full" :gap="10">
<n-avatar size="medium" class="shrink-0" :src="getUserAvatar(user)" round />
<!-- 用户列表 -->
<div class="flex-1 overflow-y-auto">
<van-checkbox-group v-model="selectedUserIds">
<van-cell-group>
<van-cell
v-for="user in filteredContactsList"
:key="user.uid"
clickable
@click="toggleUser(user.uid)"
class="user-item">
<template #title>
<div class="flex items-center gap-12px">
<van-image
:src="getUserAvatar(user)"
round
width="40"
height="40"
fit="cover"
:style="{ flexShrink: 0 }" />
<div class="flex-1 min-w-0">
<div class="text-14px text-#333 font-500 truncate">
<div class="grow overflow-hidden">
<n-text class="truncate block">
{{ getUserName(user) }}
</div>
<div v-if="user.remark" class="text-12px text-#999 truncate mt-2px">{{ user.remark }}</div>
</n-text>
<n-text depth="3" v-if="user.remark" class="text-12px">{{ user.remark }}</n-text>
</div>
</div>
</template>
<template #right-icon>
<van-checkbox :name="user.uid" @click.stop ref="checkboxes" />
</template>
</van-cell>
</van-cell-group>
</van-checkbox-group>
<!-- 空状态 -->
<van-empty v-if="filteredContactsList.length === 0" :description="t('mobile_post.empty')" />
</div>
</div>
</van-popup>
<n-checkbox :value="user.uid" class="shrink-0" @click.stop />
</n-flex>
</n-list-item>
</n-list>
</n-checkbox-group>
<!-- 空状态 -->
<van-empty v-if="filteredContactsList.length === 0" :description="t('mobile_post.empty')" />
</div>
</n-space>
</n-drawer-content>
</n-drawer>
</template>
<script setup lang="ts">
@@ -205,6 +197,9 @@ import { AvatarUtils } from '@/utils/AvatarUtils'
import type { FriendItem } from '@/services/types'
import 'vant/lib/index.css' // Vant UI 样式
import { useI18n } from 'vue-i18n'
import { useThemeVars } from 'naive-ui'
const themeVars = useThemeVars()
const { t, getLocaleMessage } = useI18n()
console.log(getLocaleMessage('en'))
@@ -367,4 +362,10 @@ onMounted(async () => {
.user-item:active {
background-color: #f5f5f5;
}
.custom-rounded {
border-top-left-radius: 20px !important; /* 左上角 */
border-top-right-radius: 20px !important;
overflow: hidden;
}
</style>

View File

@@ -18,27 +18,25 @@
<svg @click="handleBack" class="w-32px h-32px iconpark-icon"><use href="#right"></use></svg>
</div>
</div>
<div class="flex shadow bg-white w-full rounded-lg items-center">
<div class="px-24px py-20px flex w-full flex-wrap gap-20px">
<n-avatar
:size="74"
:src="AvatarUtils.getAvatarUrl(userStore.userInfo!.avatar!)"
fallback-src="/logo.png"
round />
<n-card size="small" class="rounded-lg" content-class="flex gap-20px items-center">
<n-avatar
:size="74"
:src="AvatarUtils.getAvatarUrl(userStore.userInfo!.avatar!)"
fallback-src="/logo.png"
round />
<div @click="toMyInfo" class="flex flex-col flex-1 py-10px">
<div class="font-bold text-18px text-#373838">{{ userStore.userInfo!.name }}</div>
<div class="mt-2 text-bold-style line-height-22px line-clamp-2">
{{ userStore.userInfo!.resume || t('mobile_my.default_bio') }}
</div>
</div>
<div @click="toMyInfo" class="flex items-center justify-end">
<svg @click="handleBack" class="w-24px text-gray h-24px iconpark-icon"><use href="#right"></use></svg>
<div @click="toMyInfo" class="flex flex-col flex-1 py-10px">
<div class="font-bold text-18px">{{ userStore.userInfo!.name }}</div>
<div class="mt-2 text-bold-style line-height-22px line-clamp-2">
{{ userStore.userInfo!.resume || t('mobile_my.default_bio') }}
</div>
</div>
</div>
<div class="flex flex-col w-full bg-white rounded-lg flex-1">
<div @click="toMyInfo" class="flex items-center justify-end">
<svg @click="handleBack" class="w-24px text-gray h-24px iconpark-icon"><use href="#right"></use></svg>
</div>
</n-card>
<n-card size="small" content-class="flex flex-col w-full flex-1" class="rounded-lg">
<div
v-for="item in options"
:key="item.label"
@@ -52,7 +50,7 @@
<svg class="w-20px text-gray h-20px iconpark-icon"><use href="#right"></use></svg>
</div>
</div>
</div>
</n-card>
</div>
</div>
</template>

View File

@@ -1,7 +1,8 @@
<template>
<div class="flex flex-col h-full">
<img src="@/assets/mobile/chat-home/background.webp" class="w-100% fixed z-0 top-0" alt="hula" />
<div class="flex flex-col overflow-auto h-full relative">
<img
src="@/assets/mobile/chat-home/background.webp"
class="absolute fixed top-0 left-0 w-full h-full z-0 dark:opacity-20" />
<!-- 设置区 -->
<Settings />
@@ -14,7 +15,7 @@
ref="scrollContainer"
:style="{ height: tabHeight + 'px' }"
class="z-1 overflow-hidden mt-2 absolute z-3 w-full">
<div class="custom-rounded bg-white flex px-24px flex-col gap-4 z-1 p-10px mt-4">
<n-card class="custom-rounded" content-class="flex flex-col gap-4 z-1 p-10px mt-4 p-15px!">
<n-scrollbar
ref="scrollbarRef"
:style="{ height: tabHeight + 'px' }"
@@ -31,7 +32,7 @@
@item-click="handleItemClick" />
</div>
</n-scrollbar>
</div>
</n-card>
</div>
</div>

View File

@@ -11,6 +11,14 @@
--left-active-icon-color: #13987f;
--left-win-icon-color: rgba(19, 152, 127, 0.6);
// 通用背景色
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--border: oklch(0.92 0.004 286.32);
--card: oklch(1 0 0);
--card-foreground: oklch(0.141 0.005 285.823);
--input: oklch(0.92 0.004 286.32);
// 包含边框的选中样式
--border-active-color: #13987f;
@@ -50,6 +58,13 @@ html[data-theme='dark'] {
--left-active-icon-color: #13987f;
--left-win-icon-color: rgba(19, 152, 127, 0.6);
--background: oklch(27, 27, 27);
--foreground: oklch(0.985 0 0);
--border: oklch(1 0 0 / 10%);
--card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.985 0 0);
--input: oklch(1 0 0 / 15%);
// 包含边框的选中样式
--border-active-color: #13987f;

View File

@@ -44,40 +44,38 @@
<p class="text-(24px [--chat-text-color]) font-500">{{ userStore.userInfo!.name }}</p>
<!-- 密码输入框 -->
<n-config-provider :theme="lightTheme">
<n-input
v-if="!isLogining && !isWrongPassword"
ref="inputInstRef"
style="
width: 320px;
border: 2px solid rgba(255, 255, 255, 0.1);
border-bottom-color: rgba(19, 152, 127, 1);
background-color: #404040;
color: #fff;
"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
:placeholder="t('message.lock_screen.password_placeholder')"
show-password-on="click"
type="password"
@keyup.enter.prevent="unlock"
v-model:value="password">
<template #suffix>
<n-popover trigger="hover">
<template #trigger>
<svg
@click.stop="unlock"
class="size-16px color-#e3e3e3 mr-6px p-[4px_6px] rounded-8px cursor-pointer transition-all duration-300 ease-in-out hover:bg-#13987fe6">
<use href="#arrow-right"></use>
</svg>
</template>
<p>{{ t('message.lock_screen.enter_system_tooltip') }}</p>
</n-popover>
</template>
</n-input>
</n-config-provider>
<n-input
v-if="!isLogining && !isWrongPassword"
ref="inputInstRef"
style="
width: 320px;
border: 2px solid rgba(255, 255, 255, 0.1);
border-bottom-color: rgba(19, 152, 127, 1);
background-color: #404040;
color: #fff;
"
spellCheck="false"
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
:placeholder="t('message.lock_screen.password_placeholder')"
show-password-on="click"
type="password"
@keyup.enter.prevent="unlock"
v-model:value="password">
<template #suffix>
<n-popover trigger="hover">
<template #trigger>
<svg
@click.stop="unlock"
class="size-16px color-#e3e3e3 mr-6px p-[4px_6px] rounded-8px cursor-pointer transition-all duration-300 ease-in-out hover:bg-#13987fe6">
<use href="#arrow-right"></use>
</svg>
</template>
<p>{{ t('message.lock_screen.enter_system_tooltip') }}</p>
</n-popover>
</template>
</n-input>
<!-- 登录时显示的文字 -->
<n-flex vertical align="center" justify="center" :size="30" v-if="isLogining && !isWrongPassword">
@@ -115,7 +113,7 @@
import { WebviewWindow } from '@tauri-apps/api/webviewWindow'
import { onKeyStroke } from '@vueuse/core'
import dayjs from 'dayjs'
import { type InputInst, lightTheme } from 'naive-ui'
import { type InputInst } from 'naive-ui'
import { useLogin } from '@/hooks/useLogin.ts'
import { useSettingStore } from '@/stores/setting.ts'
import { useUserStore } from '@/stores/user.ts'

View File

@@ -56,5 +56,16 @@ export default defineConfig({
'fixed-rt': 'fixed right-0 top-0',
'fixed-rb': 'fixed right-0 bottom-0',
'fixed-center': 'fixed-lt flex-center size-full'
},
theme: {
colors: {
background: 'var(--background)',
foreground: 'var(--foreground)',
border: 'var(--border)',
card: 'var(--card)',
'card-foreground': 'var(--card-foreground)',
input: 'var(--input)'
}
}
})