feat: 资料更新

This commit is contained in:
2026-05-27 17:29:35 +08:00
parent 3e64912804
commit a4861da21a
13 changed files with 794 additions and 521 deletions

View File

@@ -27,7 +27,7 @@
<div class="region-row">
<div class="security-label">{{ t('Settings.region.region') }}</div>
<div v-show="!isEditing" class="security-static field-box">
{{ displayRegionLabel }}
{{ t(`area.${displayRegionLabel}`) }}
</div>
<div v-show="isEditing" class="outlined-field select-field">
<el-select
@@ -38,7 +38,7 @@
<el-option
v-for="item in regionOptions"
:key="item.value"
:label="item.label"
:label="t(`area.${item.key}`)"
:value="item.value"
/>
</el-select>

View File

@@ -23,14 +23,14 @@
:placeholder="t('Settings.security.newEmailPlaceholder')"
@update:model-value="emit('update:newEmail', String($event))"
/>
<button
<!-- <button
type="button"
class="verify-btn"
:class="{ verified: isEmailVerified }"
@click="emit('verify-email')"
>
{{ isEmailVerified ? t('Settings.security.verified') : t('Settings.security.verify') }}
</button>
</button> -->
</div>
<div v-if="isEmailVerified" class="security-tip verified-tip">
{{ t('Settings.security.verifiedTip') }}

View File

@@ -1,159 +1,165 @@
<template>
<div class="setting-wrapper mini-scrollbar">
<div class="banner">
<div class="title">{{ t('Settings.title') }}</div>
<div class="slogan">{{ t('Settings.slogan') }}</div>
</div>
<div class="setting-wrapper mini-scrollbar">
<div class="banner">
<div class="title">{{ t('Settings.title') }}</div>
<div class="slogan">{{ t('Settings.slogan') }}</div>
</div>
<div class="setting-content">
<ProfileSection
v-model:first-name="draftData.firstName"
v-model:last-name="draftData.lastName"
v-model:username="draftData.username"
v-model:role-model="roleModel"
:display-data="displayData"
:full-name="fullName"
:is-editing="isEditing"
:role-options="roleList"
/>
<div class="setting-content">
<ProfileSection
v-model:first-name="draftData.firstName"
v-model:last-name="draftData.lastName"
v-model:username="draftData.username"
v-model:role-model="roleModel"
:display-data="displayData"
:full-name="fullName"
:is-editing="isEditing"
:role-options="roleList"
/>
<div class="gap" />
<div class="gap" />
<SecuritySection
v-model:new-email="securityDraft.newEmail"
v-model:new-password="securityDraft.newPassword"
v-model:current-password="securityDraft.currentPassword"
:email="displayData.email"
:is-editing="isEditing"
:is-email-verified="isEmailVerified"
@reset-email="resetSecurityEmail"
@reset-password="resetSecurityPassword"
@verify-email="handleVerifyEmail"
/>
<SecuritySection
v-model:new-email="securityDraft.newEmail"
v-model:new-password="securityDraft.newPassword"
v-model:current-password="securityDraft.currentPassword"
:email="displayData.email"
:is-editing="isEditing"
:is-email-verified="isEmailVerified"
@reset-email="resetSecurityEmail"
@reset-password="resetSecurityPassword"
@verify-email="handleVerifyEmail"
/>
<div class="gap" />
<div class="gap" />
<RegionSection
v-model:language="draftData.language"
v-model:region="draftData.region"
:display-language-label="displayLanguageLabel"
:display-region-label="displayRegionLabel"
:is-editing="isEditing"
:language-options="languageList"
:region-options="regionList"
/>
<RegionSection
v-model:language="draftData.language"
v-model:region="draftData.region"
:display-language-label="displayLanguageLabel"
:display-region-label="displayRegionLabel"
:is-editing="isEditing"
:language-options="languageList"
:region-options="regionList"
/>
<div class="gap bottom-gap" />
<div class="gap bottom-gap" />
<SettingsActions
:is-editing="isEditing"
:saving="saving"
@edit="handleEdit"
@save="handleSave"
@discard="handleDiscard"
/>
</div>
<SettingsActions
:is-editing="isEditing"
:saving="saving"
@edit="handleEdit"
@save="handleSave"
@discard="handleDiscard"
/>
</div>
<Footer />
<Footer />
<EmailVerificationDialog
:visible="isVerificationDialogVisible"
:email="verificationTargetEmail"
:saving="saving"
@close="closeVerificationDialog"
@resend="handleSendVerifyCode"
@submit="handleVerificationSubmit"
/>
</div>
<EmailVerificationDialog
:visible="isVerificationDialogVisible"
:email="verificationTargetEmail"
:saving="saving"
@close="closeVerificationDialog"
@resend="handleSendVerifyCode"
@submit="handleVerificationSubmit"
/>
</div>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import EmailVerificationDialog from './components/EmailVerificationDialog.vue'
import ProfileSection from './components/ProfileSection.vue'
import RegionSection from './components/RegionSection.vue'
import SecuritySection from './components/SecuritySection.vue'
import SettingsActions from './components/SettingsActions.vue'
import { useSettingsForm } from './useSettingsForm'
import { ref, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import EmailVerificationDialog from './components/EmailVerificationDialog.vue'
import ProfileSection from './components/ProfileSection.vue'
import RegionSection from './components/RegionSection.vue'
import SecuritySection from './components/SecuritySection.vue'
import SettingsActions from './components/SettingsActions.vue'
import { useSettingsForm } from './useSettingsForm'
const { t, locale } = useI18n({ useScope: 'global' })
const { t, locale } = useI18n({ useScope: 'global' })
const {
draftData,
securityDraft,
isEditing,
saving,
isVerificationDialogVisible,
verificationTargetEmail,
roleList,
languageList,
regionList,
displayData,
isEmailVerified,
displayLanguageLabel,
displayRegionLabel,
fullName,
roleModel,
handleEdit,
handleDiscard,
handleSave,
resetSecurityEmail,
resetSecurityPassword,
handleVerifyEmail,
handleSendVerifyCode,
handleVerificationSubmit,
closeVerificationDialog
} = useSettingsForm({ t, locale })
const {
draftData,
securityDraft,
isEditing,
saving,
isVerificationDialogVisible,
verificationTargetEmail,
roleList,
languageList,
regionList,
displayData,
isEmailVerified,
displayLanguageLabel,
displayRegionLabel,
fullName,
roleModel,
handleEdit,
handleDiscard,
handleSave,
resetSecurityEmail,
resetSecurityPassword,
handleVerifyEmail,
handleSendVerifyCode,
handleVerificationSubmit,
closeVerificationDialog,
loadUserProfile
} = useSettingsForm({ t, locale })
onMounted(() => {
loadUserProfile()
})
</script>
<style lang="less" scoped>
.setting-wrapper {
height: 100%;
overflow-y: auto;
background: #ffffff;
}
.setting-wrapper {
height: 100%;
overflow-y: auto;
background: #ffffff;
}
.banner {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 14.8rem;
row-gap: 1.2rem;
background: linear-gradient(rgba(255, 255, 255, 0.91), rgba(255, 255, 255, 0.91)),
linear-gradient(90deg, #f2eee8 0%, #fbfaf8 40%, #f1ede7 100%);
}
.banner {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 14.8rem;
row-gap: 1.2rem;
background: linear-gradient(rgba(255, 255, 255, 0.91), rgba(255, 255, 255, 0.91)),
linear-gradient(90deg, #f2eee8 0%, #fbfaf8 40%, #f1ede7 100%);
}
.title {
font-family: 'KaiseiOpti-Bold';
font-size: 4rem;
line-height: 3.6rem;
color: #232323;
}
.title {
font-family: 'KaiseiOpti-Bold';
font-size: 4rem;
line-height: 3.6rem;
color: #232323;
}
.slogan {
font-family: 'KaiseiOpti-Regular';
font-size: 1.6rem;
line-height: 2.4rem;
color: #585858;
}
.slogan {
font-family: 'KaiseiOpti-Regular';
font-size: 1.6rem;
line-height: 2.4rem;
color: #585858;
}
.setting-content {
padding: 4rem 18rem 7rem;
}
.setting-content {
padding: 4rem 18rem 7rem;
}
.gap {
height: 0.05rem;
margin-top: 6rem;
margin-bottom: 4rem;
background-color: #c4c4c4;
.gap {
height: 0.05rem;
margin-top: 6rem;
margin-bottom: 4rem;
background-color: #c4c4c4;
&.bottom-gap {
margin-top: 4rem;
}
}
&.bottom-gap {
margin-top: 4rem;
}
}
:deep(.el-select-dropdown__item) {
padding: 0 2rem !important;
}
:deep(.el-select-dropdown__item) {
padding: 0 2rem !important;
}
</style>

View File

@@ -13,20 +13,19 @@ export const roleValues = [
] as const
export const languageValues = ['english', 'chinese'] as const
export const regionValues = ['hongKongSar', 'mainlandChina', 'singapore', 'unitedKingdom'] as const
export type RoleValue = (typeof roleValues)[number]
export type LanguageValue = (typeof languageValues)[number]
export type RegionValue = (typeof regionValues)[number]
export interface SettingsData {
firstName: string
lastName: string
email: string
username: string
role: RoleValue[]
language: LanguageValue
region: RegionValue
roles: RoleValue[]
language: LanguageValue | ''
region: string | ''
}
export interface SecurityDraft {

View File

@@ -1,288 +1,335 @@
import { computed, ref, shallowRef, watch, type Ref } from 'vue'
import { ElMessage } from 'element-plus'
import { fetchUserProfile, type UserProfile, updateUserProfile } from '@/api/user'
import regionList from '@/utils/area'
import {
languageValues,
regionValues,
roleValues,
type LanguageValue,
type RegionValue,
type RoleValue,
type SecurityDraft,
type SettingsData
languageValues,
roleValues,
type LanguageValue,
type RoleValue,
type SecurityDraft,
type SettingsData
} from './types'
type Translate = (key: string, ...args: unknown[]) => string
interface UseSettingsFormOptions {
t: Translate
locale: Ref<string>
t: Translate
locale: Ref<string>
}
const languageLocaleMap: Record<LanguageValue, 'ENGLISH' | 'CHINESE_SIMPLIFIED'> = {
english: 'ENGLISH',
chinese: 'CHINESE_SIMPLIFIED'
english: 'ENGLISH',
chinese: 'CHINESE_SIMPLIFIED'
}
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
const createDefaultData = (): SettingsData => ({
firstName: 'Alexandra',
lastName: 'Chen',
email: 'alex.chen@gmail.com',
username: '@alexandra_chen',
role: ['student', 'graphicDesigner'],
language: 'english',
region: 'hongKongSar'
firstName: '',
lastName: '',
email: '',
username: '',
roles: [] as RoleValue[],
language: '',
region: ''
})
const cloneSettingsData = (data: SettingsData): SettingsData => ({
firstName: data.firstName,
lastName: data.lastName,
email: data.email,
username: data.username,
role: [...data.role],
language: data.language,
region: data.region
firstName: data.firstName,
lastName: data.lastName,
email: data.email,
username: data.username,
roles: [...data.roles],
language: data.language,
region: data.region
})
const normalizeLanguage = (language: string | null | undefined): LanguageValue => {
if (!language) {
return '' as LanguageValue
}
const normalized = language.trim().toLowerCase()
return normalized.includes('chinese') ? 'chinese' : 'english'
}
const buildSettingsDataFromProfile = (profile: Partial<UserProfile>): SettingsData => ({
firstName: profile.firstName || '',
lastName: profile.lastName || '',
username: profile.username || '',
email: profile.email || '',
roles: profile.roles || [],
language: normalizeLanguage(profile.language),
region: profile.region
})
const createEmptySecurityDraft = (): SecurityDraft => ({
newEmail: '',
newPassword: '',
currentPassword: ''
newEmail: '',
newPassword: '',
currentPassword: ''
})
export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
const sourceData = ref<SettingsData>(createDefaultData())
const draftData = ref<SettingsData>(cloneSettingsData(sourceData.value))
const securityDraft = ref<SecurityDraft>(createEmptySecurityDraft())
const isEditing = shallowRef(false)
const saving = shallowRef(false)
const isVerificationDialogVisible = shallowRef(false)
const verificationTargetEmail = shallowRef('')
const verifiedEmail = shallowRef('')
const sourceData = ref<SettingsData>(createDefaultData())
const draftData = ref<SettingsData>(cloneSettingsData(sourceData.value))
const securityDraft = ref<SecurityDraft>(createEmptySecurityDraft())
const isEditing = shallowRef(false)
const saving = shallowRef(false)
const isVerificationDialogVisible = shallowRef(false)
const verificationTargetEmail = shallowRef('')
const verifiedEmail = shallowRef('')
const roleList = computed(() =>
roleValues.map((value) => ({
name: t(`Settings.roles.${value}`),
value
}))
)
const roleList = computed(() =>
roleValues.map((value) => ({
name: t(`Settings.roles.${value}`),
value
}))
)
const languageList = computed(() =>
languageValues.map((value) => ({
label: t(`Settings.languages.${value}`),
value
}))
)
const languageList = computed(() =>
languageValues.map((value) => ({
label: t(`Settings.languages.${value}`),
value
}))
)
const regionList = computed(() =>
regionValues.map((value) => ({
label: t(`Settings.regions.${value}`),
value
}))
)
const displayData = computed(() => (isEditing.value ? draftData.value : sourceData.value))
const normalizedNewEmail = computed(() => securityDraft.value.newEmail.trim())
const hasNewEmailChange = computed(
() =>
normalizedNewEmail.value.length > 0 &&
normalizedNewEmail.value !== sourceData.value.email
)
const isEmailVerified = computed(
() => hasNewEmailChange.value && verifiedEmail.value === normalizedNewEmail.value
)
const displayLanguageLabel = computed(() =>
displayData.value.language ? t(`Settings.languages.${displayData.value.language}`) : ''
)
const displayRegionLabel = computed(() => {
if (displayData.value.region) {
const regionItem = regionList.find((item) => item.value === displayData.value.region)
return regionItem.key
} else {
return ''
}
})
const displayData = computed(() => (isEditing.value ? draftData.value : sourceData.value))
const normalizedNewEmail = computed(() => securityDraft.value.newEmail.trim())
const hasNewEmailChange = computed(
() => normalizedNewEmail.value.length > 0 && normalizedNewEmail.value !== sourceData.value.email
)
const isEmailVerified = computed(
() => hasNewEmailChange.value && verifiedEmail.value === normalizedNewEmail.value
)
const displayLanguageLabel = computed(() => t(`Settings.languages.${displayData.value.language}`))
const displayRegionLabel = computed(() => t(`Settings.regions.${displayData.value.region}`))
const fullName = computed(() => {
const data = displayData.value
return `${data.firstName} ${data.lastName}`.trim()
})
const fullName = computed(() => {
const data = displayData.value
return `${data.firstName} ${data.lastName}`.trim()
})
const roleModel = computed<RoleValue[]>({
get: () => displayData.value.roles,
set: (value) => {
if (isEditing.value) {
draftData.value.roles = value
return
}
const roleModel = computed<RoleValue[]>({
get: () => displayData.value.role,
set: (value) => {
if (isEditing.value) {
draftData.value.role = value
return
}
sourceData.value.roles = value
}
})
sourceData.value.role = value
}
})
const resetEmailVerificationState = () => {
isVerificationDialogVisible.value = false
verificationTargetEmail.value = ''
verifiedEmail.value = ''
}
const resetEmailVerificationState = () => {
isVerificationDialogVisible.value = false
verificationTargetEmail.value = ''
verifiedEmail.value = ''
}
const syncAppLanguage = (language: LanguageValue) => {
const nextLocale = languageLocaleMap[language]
locale.value = nextLocale
localStorage.setItem('language', nextLocale)
}
const syncAppLanguage = (language: LanguageValue) => {
const nextLocale = languageLocaleMap[language]
locale.value = nextLocale
localStorage.setItem('language', nextLocale)
}
const loadUserProfile = async () => {
try {
const profile = (await fetchUserProfile()) as Partial<UserProfile>
const nextData = buildSettingsDataFromProfile(profile)
sourceData.value = cloneSettingsData(nextData)
draftData.value = cloneSettingsData(sourceData.value)
const resetDraftState = () => {
draftData.value = cloneSettingsData(sourceData.value)
securityDraft.value = createEmptySecurityDraft()
resetEmailVerificationState()
}
if (sourceData.value.language) {
syncAppLanguage(sourceData.value.language as LanguageValue)
}
} catch (error) {
console.warn(error)
}
}
const handleEdit = () => {
resetDraftState()
isEditing.value = true
}
const resetDraftState = () => {
draftData.value = cloneSettingsData(sourceData.value)
securityDraft.value = createEmptySecurityDraft()
resetEmailVerificationState()
}
const resetSecurityEmail = () => {
securityDraft.value.newEmail = ''
resetEmailVerificationState()
}
const handleEdit = () => {
resetDraftState()
isEditing.value = true
}
const resetSecurityPassword = () => {
securityDraft.value.newPassword = ''
securityDraft.value.currentPassword = ''
}
const resetSecurityEmail = () => {
securityDraft.value.newEmail = ''
resetEmailVerificationState()
}
const handleDiscard = () => {
resetDraftState()
isEditing.value = false
}
const resetSecurityPassword = () => {
securityDraft.value.newPassword = ''
securityDraft.value.currentPassword = ''
}
const closeVerificationDialog = () => {
isVerificationDialogVisible.value = false
verificationTargetEmail.value = ''
}
const handleDiscard = () => {
resetDraftState()
isEditing.value = false
}
const handleVerifyEmail = () => {
const nextEmail = normalizedNewEmail.value
const closeVerificationDialog = () => {
isVerificationDialogVisible.value = false
verificationTargetEmail.value = ''
}
if (!nextEmail) {
ElMessage.warning(t('Settings.messages.enterNewEmailFirst'))
return
}
const handleVerifyEmail = () => {
const nextEmail = normalizedNewEmail.value
if (!emailPattern.test(nextEmail)) {
ElMessage.warning(t('Settings.messages.invalidEmail'))
return
}
if (!nextEmail) {
ElMessage.warning(t('Settings.messages.enterNewEmailFirst'))
return
}
if (nextEmail === sourceData.value.email) {
ElMessage.warning(t('Settings.messages.sameEmail'))
return
}
if (!emailPattern.test(nextEmail)) {
ElMessage.warning(t('Settings.messages.invalidEmail'))
return
}
if (verifiedEmail.value === nextEmail) {
ElMessage.success(t('Settings.messages.alreadyVerified'))
return
}
if (nextEmail === sourceData.value.email) {
ElMessage.warning(t('Settings.messages.sameEmail'))
return
}
verificationTargetEmail.value = nextEmail
handleSendVerifyCode()
isVerificationDialogVisible.value = true
}
if (verifiedEmail.value === nextEmail) {
ElMessage.success(t('Settings.messages.alreadyVerified'))
return
}
const handleSendVerifyCode = () => {
ElMessage.success(t('Settings.messages.verificationCodeSent'))
}
verificationTargetEmail.value = nextEmail
handleSendVerifyCode()
}
const handleVerificationSubmit = (code: string) => {
if (code.length !== 6) {
ElMessage.warning(t('Settings.messages.enterVerificationCode'))
return
}
const handleSendVerifyCode = () => {
// AccountSendVerifyCode({
// email: verificationTargetEmail.value,
// operationType: 'FORGET_PWD'
// }).then((res) => {
// console.log(res)
// ElMessage.success(t('Settings.messages.verificationCodeSent'))
// isVerificationDialogVisible.value = true
// })
}
verifiedEmail.value = verificationTargetEmail.value
closeVerificationDialog()
ElMessage.success(t('Settings.messages.verificationCompleted'))
}
const handleVerificationSubmit = (code: string) => {
if (code.length !== 6) {
ElMessage.warning(t('Settings.messages.enterVerificationCode'))
return
}
const buildNextData = (): SettingsData => {
const nextEmail = securityDraft.value.newEmail.trim() || draftData.value.email
verifiedEmail.value = verificationTargetEmail.value
closeVerificationDialog()
ElMessage.success(t('Settings.messages.verificationCompleted'))
}
return {
firstName: draftData.value.firstName.trim(),
lastName: draftData.value.lastName.trim(),
username: draftData.value.username.trim(),
email: nextEmail,
role: [...draftData.value.role],
language: draftData.value.language,
region: draftData.value.region
}
}
const buildNextData = (): SettingsData => {
const nextEmail = securityDraft.value.newEmail.trim() || draftData.value.email
const handleSave = async () => {
if (hasNewEmailChange.value && !isEmailVerified.value) {
ElMessage.warning(t('Settings.messages.verifyEmailBeforeSave'))
return
}
return {
firstName: draftData.value.firstName.trim(),
lastName: draftData.value.lastName.trim(),
username: draftData.value.username.trim(),
email: nextEmail,
roles: [...draftData.value.roles],
language: draftData.value.language,
region: draftData.value.region
}
}
const nextData = buildNextData()
const previousLanguage = sourceData.value.language
saving.value = true
const handleSave = async () => {
if (hasNewEmailChange.value && !isEmailVerified.value) {
ElMessage.warning(t('Settings.messages.verifyEmailBeforeSave'))
return
}
try {
sourceData.value = cloneSettingsData(nextData)
const nextData = buildNextData()
const previousLanguage = sourceData.value.language
saving.value = true
if (nextData.language !== previousLanguage) {
syncAppLanguage(nextData.language)
}
try {
await updateUserProfile(nextData)
sourceData.value = cloneSettingsData(nextData)
console.log(nextData)
draftData.value = cloneSettingsData(sourceData.value)
securityDraft.value = createEmptySecurityDraft()
resetEmailVerificationState()
isEditing.value = false
ElMessage.success(t('Settings.messages.settingsUpdated'))
} catch (error) {
console.warn(error)
} finally {
saving.value = false
}
}
if (nextData.language && nextData.language !== previousLanguage) {
syncAppLanguage(nextData.language as LanguageValue)
}
watch(
() => securityDraft.value.newEmail,
(value) => {
const trimmedValue = value.trim()
draftData.value = cloneSettingsData(sourceData.value)
securityDraft.value = createEmptySecurityDraft()
resetEmailVerificationState()
isEditing.value = false
ElMessage.success(t('Settings.messages.settingsUpdated'))
} catch (error) {
console.warn(error)
} finally {
saving.value = false
}
}
if (verifiedEmail.value && trimmedValue !== verifiedEmail.value) {
verifiedEmail.value = ''
}
watch(
() => securityDraft.value.newEmail,
(value) => {
const trimmedValue = value.trim()
if (
isVerificationDialogVisible.value &&
verificationTargetEmail.value &&
trimmedValue !== verificationTargetEmail.value
) {
closeVerificationDialog()
}
}
)
if (verifiedEmail.value && trimmedValue !== verifiedEmail.value) {
verifiedEmail.value = ''
}
return {
sourceData,
draftData,
securityDraft,
isEditing,
saving,
isVerificationDialogVisible,
verificationTargetEmail,
roleList,
languageList,
regionList,
displayData,
isEmailVerified,
displayLanguageLabel,
displayRegionLabel,
fullName,
roleModel,
handleEdit,
handleDiscard,
handleSave,
resetSecurityEmail,
resetSecurityPassword,
handleVerifyEmail,
handleSendVerifyCode,
handleVerificationSubmit,
closeVerificationDialog
}
if (
isVerificationDialogVisible.value &&
verificationTargetEmail.value &&
trimmedValue !== verificationTargetEmail.value
) {
closeVerificationDialog()
}
}
)
return {
sourceData,
draftData,
securityDraft,
isEditing,
saving,
isVerificationDialogVisible,
verificationTargetEmail,
roleList,
languageList,
regionList,
displayData,
isEmailVerified,
displayLanguageLabel,
displayRegionLabel,
fullName,
roleModel,
handleEdit,
handleDiscard,
handleSave,
resetSecurityEmail,
resetSecurityPassword,
handleVerifyEmail,
handleSendVerifyCode,
handleVerificationSubmit,
closeVerificationDialog,
loadUserProfile
}
}

View File

@@ -34,6 +34,7 @@
<div class="amount" v-show="!disabled">${{ info.amount }}<span> HKD</span></div>
<SvgIcon
v-if="showDownload"
@click.stop="emit('download', info)"
class="download"
name="download"
size="32"
@@ -52,7 +53,7 @@
import { FormatDate } from '@/utils/tools'
import { useRouter } from 'vue-router'
const router = useRouter()
const emit = defineEmits(['remove'])
const emit = defineEmits(['remove', 'download'])
const props = defineProps({
showTags: { type: Boolean, default: true },
showDate: { type: Boolean, default: true },

View File

@@ -31,6 +31,7 @@
<div
class="assets-toolbar__download flex flex-center"
:class="{ disabled: selectedCount < 1 }"
v-loading="downloadingSelected"
@click="handleDownloadSelected"
>
<SvgIcon name="downloadBtn" color="#fff" />
@@ -66,6 +67,7 @@
download
:url="item.thumbnailUrl"
:name="item.listingName"
@download.stop="handleDownloadSelected(item)"
:showPrice="false"
></CommodityItem>
</div>
@@ -92,7 +94,7 @@
import { useClothesCategories } from '@/utils/ClothesCategory'
import Empty from './Empty.vue'
import FilterSidebar from './FilterSidebar.vue'
import { fetchMyWardrobe } from '@/api/user'
import { fetchMyWardrobe, fetchDownloadItemsByGet } from '@/api/user'
import { useUserInfoStore } from '@/stores'
import { debounce } from 'lodash-es'
@@ -320,9 +322,37 @@
}
}
const handleDownloadSelected = () => {
const items = dataList.value.filter((item) => item.checked)
console.log(items)
const downloadingSelected = ref(false)
const handleDownloadSelected = (assets) => {
const items = assets ? [assets] : dataList.value.filter((item) => item.checked)
downloadingSelected.value = true
const ids = items.map((item) => item.listingId)
fetchDownloadItemsByGet({ ids })
.then((res) => {
const disposition = res.headers['content-disposition']
const fileName =
disposition?.split('filename=')[1]?.replace(/"/g, '') || 'download.zip'
const blob = res.data
const url = window.URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
const timestamp = new Date().getTime()
link.download = fileName || `wardrobe_download_${timestamp}.zip`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
})
.catch((error) => {
console.error('Download failed:', error)
}).finally(() => {
downloadingSelected.value = false
})
}
const handleAssetsScroll = () => {

View File

@@ -84,6 +84,7 @@
:show-brand="false"
is-order
order-actions-layout
@download="handleDownload(order)"
/>
</div>
</article>
@@ -96,7 +97,7 @@
import { computed, onMounted, ref, shallowRef } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter } from 'vue-router'
import { fetchMyOrders } from '@/api/user'
import { fetchMyOrders ,fetchDownloadItemsByGet} from '@/api/user'
import ScItem from '@/views/shoppingCart/sc-item.vue'
import Empty from './Empty.vue'
@@ -181,14 +182,10 @@
return t('Wardrobe.orders.actions.buyAgain')
}
const getOrderStatusValue = (status: unknown): ActualOrderStatus => {
if (status === 0 || status === '0' || status === 'unpaid') return 'unpaid'
if (status === 2 || status === '2' || status === 'cancelled') return 'cancelled'
return 'paid'
}
const getOrderStatus = (order: OrderRecord) => {
return getOrderStatusValue(order.status)
if (Number(order.status) === 0) return 'unpaid'
if (Number(order.status) === 2) return 'cancelled'
if (Number(order.status) === 1) return 'paid'
}
const formatOrderUpdateTime = (dateStr: string) => {
@@ -221,7 +218,7 @@
}
const getOrderItemInfo = (item: OrderItem, order: OrderRecord) => ({
status: item.status,
status: order.status,
title: item.listingName,
brand: order.shopName,
tags: item.productCategory,
@@ -230,6 +227,43 @@
cover: item.thumbnailUrl
})
const resetOrders = () => {
ordersRequestId.value += 1
orders.value = []
expandedOrderId.value = ''
isLoadingOrders.value = false
hasMoreOrders.value = true
orderParams.value.page = 1
if (ordersScrollRef.value) {
ordersScrollRef.value.scrollTop = 0
}
}
const setActiveStatus = (status: OrderStatus) => {
activeStatus.value = status
resetOrders()
fetchAllOrders()
}
const handleOrdersScroll = () => {
const el = ordersScrollRef.value
if (!el) return
const reachBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 120
if (reachBottom) {
fetchAllOrders()
}
}
const handleDownload = (order) => {
console.log(order)
}
const handleRouteBrand = (order: OrderRecord) => {
ROUTER.push(`/brand/${order.sellerId}`)
}
const fetchAllOrders = async () => {
if (isLoadingOrders.value || !hasMoreOrders.value) return
@@ -267,39 +301,6 @@
}
}
const resetOrders = () => {
ordersRequestId.value += 1
orders.value = []
expandedOrderId.value = ''
isLoadingOrders.value = false
hasMoreOrders.value = true
orderParams.value.page = 1
if (ordersScrollRef.value) {
ordersScrollRef.value.scrollTop = 0
}
}
const setActiveStatus = (status: OrderStatus) => {
activeStatus.value = status
resetOrders()
fetchAllOrders()
}
const handleOrdersScroll = () => {
const el = ordersScrollRef.value
if (!el) return
const reachBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 120
if (reachBottom) {
fetchAllOrders()
}
}
const handleRouteBrand = (order: OrderRecord) => {
ROUTER.push(`/brand/${order.sellerId}`)
}
onMounted(() => {
fetchAllOrders()
})