improve user dialog ui

This commit is contained in:
pa
2025-10-30 15:04:15 +09:00
committed by Natsumi
parent 0fdcd6d0b2
commit 6de8d7467e
6 changed files with 203 additions and 147 deletions

View File

@@ -1,13 +1,13 @@
<template>
<div @click="confirm" class="avatar-info">
<span style="margin-right: 5px">{{ avatarName }}</span>
<span v-if="avatarType" :class="color" style="margin-right: 5px">{{ avatarType }}</span>
<span v-if="avatarType" :class="color" style="margin-right: 5px"><i :class="avatarTypeIcons" /></span>
<span v-if="avatarTags" style="color: #909399; font-family: monospace; font-size: 12px">{{ avatarTags }}</span>
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
import { computed, ref, watch } from 'vue';
import { useAvatarStore } from '../stores';
@@ -27,6 +27,14 @@
const color = ref('');
let ownerId = '';
const avatarTypeIcons = computed(() => {
return avatarType.value === '(own)'
? 'ri-lock-line'
: avatarType.value === '(public)'
? 'ri-lock-unlock-line'
: '';
});
const parse = async () => {
ownerId = '';
avatarName.value = '';
@@ -35,7 +43,7 @@
avatarTags.value = '';
if (!props.imageurl) {
avatarName.value = '-';
avatarName.value = '';
} else if (props.hintownerid) {
avatarName.value = props.hintavatarname;
ownerId = props.hintownerid;

View File

@@ -0,0 +1,141 @@
<template>
<el-dialog
class="x-dialog"
:model-value="props.visible"
title="Edit Note And Memo"
:show-close="false"
top="30vh"
width="500px"
append-to-body
@close="cancel">
<span class="name">{{ t('dialog.user.info.note') }}</span>
<br />
<el-input
v-model="note"
class="extra"
type="textarea"
maxlength="256"
show-word-limit
:rows="6"
:autosize="{ minRows: 2, maxRows: 20 }"
:placeholder="t('dialog.user.info.note_placeholder')"
size="small"
resize="none"></el-input>
<span class="name">{{ t('dialog.user.info.memo') }}</span>
<br />
<el-input
v-model="memo"
class="extra"
type="textarea"
:rows="6"
:autosize="{ minRows: 2, maxRows: 20 }"
:placeholder="t('dialog.user.info.memo_placeholder')"
size="small"
resize="none"></el-input>
<template #footer>
<div class="dialog-footer">
<el-button @click="cancel">Cancel</el-button>
<el-button type="primary" @click="saveChanges"> Confirm </el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import { ref, watch } from 'vue';
import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
import { miscRequest, userRequest } from '../../../api';
import { replaceBioSymbols, saveUserMemo } from '../../../shared/utils';
import { useUserStore } from '../../../stores';
const { userDialog } = storeToRefs(useUserStore());
const { cachedUsers } = useUserStore();
const { t } = useI18n();
const props = defineProps({
visible: {
type: Boolean,
required: true
}
});
const emit = defineEmits(['update:visible']);
const note = ref('');
const memo = ref('');
watch(
() => props.visible,
(val) => {
if (!val) return;
note.value = userDialog.value.note;
memo.value = userDialog.value.memo;
}
);
function saveChanges() {
cleanNote(note.value);
checkNote(userDialog.value.ref, note.value);
onUserMemoChange();
emit('update:visible', false);
}
function cancel() {
emit('update:visible', false);
}
function checkNote(ref, note) {
if (ref.note !== note) {
addNote(ref.id, note);
}
}
async function addNote(userId, note) {
if (userDialog.value.id === userId) {
userDialog.value.noteSaving = true;
}
const args = await miscRequest.saveNote({
targetUserId: userId,
note
});
handleNoteChange(args);
}
function handleNoteChange(args) {
let _note = '';
let targetUserId = '';
if (typeof args.json !== 'undefined') {
_note = replaceBioSymbols(args.json.note);
}
if (typeof args.params !== 'undefined') {
targetUserId = args.params.targetUserId;
}
if (targetUserId === userDialog.value.id) {
if (_note === args.params.note) {
userDialog.value.noteSaving = false;
userDialog.value.note = _note;
} else {
// response is cached sadge :<
userRequest.getUser({ userId: targetUserId });
}
}
const ref = cachedUsers.get(targetUserId);
if (typeof ref !== 'undefined') {
ref.note = _note;
}
}
function onUserMemoChange() {
const D = userDialog.value;
saveUserMemo(D.id, memo.value);
}
function cleanNote(note) {
if (!note.value) return;
// remove newlines because they aren't supported
note.value = note.value?.replace(/[\r\n]/g, '');
}
</script>

View File

@@ -120,6 +120,7 @@
<el-dropdown-item :icon="Operation" command="Group Moderation">{{
t('dialog.user.actions.group_moderation')
}}</el-dropdown-item>
<el-dropdown-item :icon="Edit" command="Edit Note Memo"> Edit Note and Memo </el-dropdown-item>
<el-dropdown-item :icon="UserFilled" command="Show Avatar Author" divided>{{
t('dialog.user.actions.show_avatar_author')
}}</el-dropdown-item>

View File

@@ -4,7 +4,8 @@
class="x-dialog x-user-dialog"
v-model="userDialog.visible"
:show-close="false"
width="770px">
top="10vh"
width="940px">
<div v-loading="userDialog.loading">
<UserSummaryHeader
:get-user-state-text="getUserStateText"
@@ -99,53 +100,19 @@
</template>
<div class="x-friend-list" style="max-height: none">
<div v-if="!hideUserNotes" class="x-friend-item" style="width: 100%; cursor: default">
<div class="detail">
<div v-if="!hideUserNotes" class="x-friend-item" style="width: 100%; cursor: pointer">
<div class="detail" v-if="userDialog.note" @click="isEditNoteAndMemoDialogVisible = true">
<span class="name">{{ t('dialog.user.info.note') }}</span>
<el-input
v-model="userDialog.note"
class="extra"
type="textarea"
maxlength="256"
:rows="2"
:autosize="{ minRows: 1, maxRows: 20 }"
:placeholder="t('dialog.user.info.note_placeholder')"
size="small"
resize="none"
@change="checkNote(userDialog.ref, userDialog.note)"
@input="cleanNote(userDialog.note)"></el-input>
<div style="float: right">
<el-icon v-if="userDialog.noteSaving" class="is-loading" style="margin-left: 5px"
><Loading
/></el-icon>
<el-icon
v-else-if="userDialog.note !== userDialog.ref.note"
style="margin-left: 5px"
><More
/></el-icon>
<el-button
v-if="userDialog.note"
type="text"
:icon="Delete"
size="small"
style="margin-left: 5px; padding: 0"
@click="deleteNote(userDialog.id)"></el-button>
</div>
<div>{{ userDialog.note }}</div>
</div>
</div>
<div v-if="!hideUserMemos" class="x-friend-item" style="width: 100%; cursor: default">
<div class="detail">
<div
v-if="userDialog.memo && !hideUserMemos"
class="x-friend-item"
style="width: 100%; cursor: pointer">
<div class="detail" @click="isEditNoteAndMemoDialogVisible = true">
<span class="name">{{ t('dialog.user.info.memo') }}</span>
<el-input
v-model="userDialog.memo"
class="extra"
type="textarea"
:rows="2"
:autosize="{ minRows: 1, maxRows: 20 }"
:placeholder="t('dialog.user.info.memo_placeholder')"
size="small"
resize="none"
@change="onUserMemoChange"></el-input>
<div>{{ userDialog.memo }}</div>
</div>
</div>
<div class="x-friend-item" style="width: 100%; cursor: default">
@@ -236,7 +203,7 @@
font-size: 12px;
white-space: pre-wrap;
margin: 0 0.5em 0 0;
max-height: 40vh;
max-height: 210px;
overflow-y: auto;
"
>{{ userDialog.ref.bio || '-' }}</pre
@@ -1216,15 +1183,14 @@
v-model:sendInviteDialog="sendInviteDialog"
@closeInviteDialog="closeInviteDialog" />
<PreviousInstancesUserDialog v-model:previous-instances-user-dialog="previousInstancesUserDialog" />
<template v-if="userDialog.visible">
<SocialStatusDialog
:social-status-dialog="socialStatusDialog"
:social-status-history-table="socialStatusHistoryTable" />
<LanguageDialog />
<BioDialog :bio-dialog="bioDialog" />
<PronounsDialog :pronouns-dialog="pronounsDialog" />
<ModerateGroupDialog
/></template>
<SocialStatusDialog
:social-status-dialog="socialStatusDialog"
:social-status-history-table="socialStatusHistoryTable" />
<LanguageDialog />
<BioDialog :bio-dialog="bioDialog" />
<PronounsDialog :pronouns-dialog="pronounsDialog" />
<ModerateGroupDialog />
<EditNoteAndMemoDialog v-model:visible="isEditNoteAndMemoDialogVisible" />
</el-dialog>
</template>
@@ -1240,7 +1206,6 @@
Download,
Edit,
Loading,
More,
MoreFilled,
Refresh,
Setting,
@@ -1265,8 +1230,6 @@
openExternalLink,
parseLocation,
refreshInstancePlayerCount,
replaceBioSymbols,
saveUserMemo,
timeToText,
userImage,
userOnlineFor,
@@ -1315,6 +1278,7 @@
const SendInviteRequestDialog = defineAsyncComponent(() => import('./SendInviteRequestDialog.vue'));
const SocialStatusDialog = defineAsyncComponent(() => import('./SocialStatusDialog.vue'));
const ModerateGroupDialog = defineAsyncComponent(() => import('../ModerateGroupDialog.vue'));
const EditNoteAndMemoDialog = defineAsyncComponent(() => import('./EditNoteAndMemoDialog.vue'));
const { t } = useI18n();
@@ -1343,7 +1307,6 @@
leaveGroup,
leaveGroupPrompt,
setGroupVisibility,
setGroupSubscription,
handleGroupList,
showModerateGroupDialog
} = useGroupStore();
@@ -1442,6 +1405,8 @@
showingTranslated: false
});
const isEditNoteAndMemoDialogVisible = ref(false);
const userDialogAvatars = computed(() => {
const { avatars, avatarReleaseStatus } = userDialog.value;
if (avatarReleaseStatus === 'public' || avatarReleaseStatus === 'private') {
@@ -1493,11 +1458,6 @@
return t('dialog.user.status.offline');
}
function cleanNote(note) {
// remove newlines because they aren't supported
userDialog.value.note = note.replace(/[\r\n]/g, '');
}
function handleUserDialogTab(tabName) {
const userId = userDialog.value.id;
if (tabName === 'Groups') {
@@ -1799,6 +1759,8 @@
} else {
setPlayerModeration(D.id, 5);
}
} else if (command === 'Edit Note Memo') {
isEditNoteAndMemoDialogVisible.value = true;
} else {
const i18nPreFix = 'dialog.user.actions.';
const formattedCommand = command.toLowerCase().replace(/ /g, '_');
@@ -2228,63 +2190,6 @@
userDialog.value.isFavoriteWorldsLoading = false;
}
function checkNote(ref, note) {
if (ref.note !== note) {
addNote(ref.id, note);
}
}
async function addNote(userId, note) {
if (userDialog.value.id === userId) {
userDialog.value.noteSaving = true;
}
const args = await miscRequest.saveNote({
targetUserId: userId,
note
});
handleNoteChange(args);
}
function handleNoteChange(args) {
let _note = '';
let targetUserId = '';
if (typeof args.json !== 'undefined') {
_note = replaceBioSymbols(args.json.note);
}
if (typeof args.params !== 'undefined') {
targetUserId = args.params.targetUserId;
}
if (targetUserId === userDialog.value.id) {
if (_note === args.params.note) {
userDialog.value.noteSaving = false;
userDialog.value.note = _note;
} else {
// response is cached sadge :<
userRequest.getUser({ userId: targetUserId });
}
}
const ref = cachedUsers.get(targetUserId);
if (typeof ref !== 'undefined') {
ref.note = _note;
}
}
async function deleteNote(userId) {
if (userDialog.value.id === userId) {
userDialog.value.noteSaving = true;
}
const args = await miscRequest.saveNote({
targetUserId: userId,
note: ''
});
handleNoteChange(args);
}
function onUserMemoChange() {
const D = userDialog.value;
saveUserMemo(D.id, D.memo);
}
function showBioDialog() {
const D = bioDialog.value;
D.bio = currentUser.value.bio;

View File

@@ -82,6 +82,20 @@
style="margin-right: 5px; margin-top: 5px">
{{ userDialog.ref.$trustLevel }}
</el-tag>
<el-tag
v-if="userDialog.ref.ageVerified && userDialog.ref.ageVerificationStatus"
type="info"
effect="plain"
size="small"
class="x-tag-age-verification"
style="margin-right: 5px; margin-top: 5px">
<template v-if="userDialog.ref.ageVerificationStatus === '18+'">
<i class="ri-info-card-line"></i> 18+
</template>
<template v-else>
<i class="ri-info-card-line"></i>
</template>
</el-tag>
<el-tag
v-if="userDialog.isFriend && userDialog.friend"
type="info"
@@ -89,11 +103,8 @@
size="small"
class="x-tag-friend"
style="margin-right: 5px; margin-top: 5px">
{{
t('dialog.user.tags.friend_no', {
number: userDialog.ref.$friendNumber ? userDialog.ref.$friendNumber : ''
})
}}
<i class="ri-user-add-line"></i>
{{ userDialog.ref.$friendNumber ? userDialog.ref.$friendNumber : '' }}
</el-tag>
<el-tag
v-if="userDialog.ref.$isTroll"
@@ -122,6 +133,7 @@
style="margin-right: 5px; margin-top: 5px">
{{ t('dialog.user.tags.vrchat_team') }}
</el-tag>
<el-tag
v-if="userDialog.ref.$platform === 'standalonewindows'"
type="info"
@@ -129,7 +141,7 @@
size="small"
class="x-tag-platform-pc"
style="margin-right: 5px; margin-top: 5px">
PC
<i class="ri-computer-line"></i>
</el-tag>
<el-tag
v-else-if="userDialog.ref.$platform === 'android'"
@@ -138,7 +150,7 @@
size="small"
class="x-tag-platform-quest"
style="margin-right: 5px; margin-top: 5px">
Android
<i class="ri-android-line"></i>
</el-tag>
<el-tag
v-else-if="userDialog.ref.$platform === 'ios'"
@@ -147,8 +159,8 @@
size="small"
class="x-tag-platform-ios"
style="margin-right: 5px; margin-top: 5px"
>iOS</el-tag
>
><i class="ri-apple-line"></i
></el-tag>
<el-tag
v-else-if="userDialog.ref.$platform"
type="info"
@@ -158,20 +170,7 @@
style="margin-right: 5px; margin-top: 5px">
{{ userDialog.ref.$platform }}
</el-tag>
<el-tag
v-if="userDialog.ref.ageVerified && userDialog.ref.ageVerificationStatus"
type="info"
effect="plain"
size="small"
class="x-tag-age-verification"
style="margin-right: 5px; margin-top: 5px">
<template v-if="userDialog.ref.ageVerificationStatus === '18+'">
{{ t('dialog.user.tags.18_plus_verified') }}
</template>
<template v-else>
{{ t('dialog.user.tags.age_verified') }}
</template>
</el-tag>
<el-tag
v-if="userDialog.ref.$customTag"
type="info"

View File

@@ -1,5 +1,5 @@
import { useFriendStore, useUserStore } from '../../stores';
import { database } from '../../service/database.js';
import { useFriendStore } from '../../stores';
/**
* @returns {Promise<void>}
@@ -43,6 +43,7 @@ async function getUserMemo(userId) {
*/
async function saveUserMemo(id, memo) {
const friendStore = useFriendStore();
const userStore = useUserStore();
if (memo) {
await database.setUserMemo({
userId: id,
@@ -61,6 +62,7 @@ async function saveUserMemo(id, memo) {
} else {
ref.$nickName = '';
}
userStore.userDialog.memo = memo;
}
}