feat(settings): improve profile navigation and password reset UX

This commit is contained in:
root
2026-03-10 16:40:56 +08:00
parent 4d8901d1af
commit 9e0d321177
3 changed files with 200 additions and 9 deletions
+4 -1
View File
@@ -184,7 +184,10 @@ const handleCommand = async (command) => {
// 用户取消 // 用户取消
} }
} else if (command === 'profile') { } else if (command === 'profile') {
ElMessage.info('个人信息功能开发中...') router.push({
path: '/settings',
query: { tab: 'profile' }
})
} }
} }
</script> </script>
+134 -3
View File
@@ -67,11 +67,29 @@
<el-button type="primary" @click="handleSearch">搜索</el-button> <el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="resetSearch">重置</el-button> <el-button @click="resetSearch">重置</el-button>
</div> </div>
<div class="right-actions">
<el-button
:disabled="selectedActiveCount === 0"
:loading="batchDisabling"
type="warning"
@click="handleBatchDisable"
>
批量停用{{ selectedActiveCount > 0 ? `(${selectedActiveCount})` : '' }}
</el-button>
<el-button type="primary" @click="openCreateDialog">生成兑换码</el-button> <el-button type="primary" @click="openCreateDialog">生成兑换码</el-button>
</div> </div>
</div>
</template> </template>
<el-table v-loading="loading" :data="list" stripe> <el-table
ref="tableRef"
v-loading="loading"
:data="list"
row-key="id"
stripe
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="48" reserve-selection />
<el-table-column prop="id" label="ID" width="80" /> <el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="code" label="兑换码" min-width="180" /> <el-table-column prop="code" label="兑换码" min-width="180" />
<el-table-column label="小程序" min-width="140"> <el-table-column label="小程序" min-width="140">
@@ -111,8 +129,10 @@
{{ formatDateTime(row.created_at) }} {{ formatDateTime(row.created_at) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="120" fixed="right"> <el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }"> <template #default="{ row }">
<el-button link type="primary" @click="openDetailDialog(row)">详情</el-button>
<el-button link @click="handleCopyCode(row)">复制</el-button>
<el-button <el-button
v-if="row.status === 'active'" v-if="row.status === 'active'"
link link
@@ -181,13 +201,40 @@
<el-button type="primary" :loading="creating" @click="handleCreate">生成</el-button> <el-button type="primary" :loading="creating" @click="handleCreate">生成</el-button>
</template> </template>
</el-dialog> </el-dialog>
<el-dialog v-model="detailDialogVisible" title="兑换码详情" width="560px">
<el-descriptions v-if="detailRecord" :column="1" border>
<el-descriptions-item label="ID">{{ detailRecord.id || '-' }}</el-descriptions-item>
<el-descriptions-item label="兑换码">{{ detailRecord.code || '-' }}</el-descriptions-item>
<el-descriptions-item label="状态">{{ statusText(detailRecord.status) }}</el-descriptions-item>
<el-descriptions-item label="所属小程序">
{{ detailRecord.mini_program_name || detailRecord.mini_program?.name || '-' }}
</el-descriptions-item>
<el-descriptions-item label="套餐类型">
{{ detailRecord.package_type || detailRecord.plan_name || '-' }}
</el-descriptions-item>
<el-descriptions-item label="时长(天)">{{ detailRecord.duration_days || '-' }}</el-descriptions-item>
<el-descriptions-item label="可用次数">
{{ detailRecord.max_uses || 1 }}
</el-descriptions-item>
<el-descriptions-item label="已使用次数">
{{ detailRecord.used_count || 0 }}
</el-descriptions-item>
<el-descriptions-item label="过期时间">
{{ formatDateTime(detailRecord.expires_at) }}
</el-descriptions-item>
<el-descriptions-item label="创建时间">
{{ formatDateTime(detailRecord.created_at) }}
</el-descriptions-item>
</el-descriptions>
</el-dialog>
</div> </div>
</template> </template>
<script setup> <script setup>
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { ElMessage, ElMessageBox } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { onMounted, reactive, ref } from 'vue' import { computed, onMounted, reactive, ref } from 'vue'
import { getMiniPrograms } from '../../api/miniProgram' import { getMiniPrograms } from '../../api/miniProgram'
import { import {
createMembershipRedeemCodes, createMembershipRedeemCodes,
@@ -201,8 +248,13 @@ const creating = ref(false)
const list = ref([]) const list = ref([])
const total = ref(0) const total = ref(0)
const miniProgramOptions = ref([]) const miniProgramOptions = ref([])
const tableRef = ref(null)
const selectedRows = ref([])
const batchDisabling = ref(false)
const createDialogVisible = ref(false) const createDialogVisible = ref(false)
const createFormRef = ref(null) const createFormRef = ref(null)
const detailDialogVisible = ref(false)
const detailRecord = ref(null)
const overview = reactive({ const overview = reactive({
total_members: 0, total_members: 0,
@@ -236,6 +288,9 @@ const createRules = {
max_uses: [{ required: true, message: '请输入可用次数', trigger: 'blur' }] max_uses: [{ required: true, message: '请输入可用次数', trigger: 'blur' }]
} }
const selectedActiveRows = computed(() => selectedRows.value.filter((item) => item.status === 'active'))
const selectedActiveCount = computed(() => selectedActiveRows.value.length)
const formatDateTime = (value) => { const formatDateTime = (value) => {
if (!value) return '-' if (!value) return '-'
return dayjs(value).format('YYYY-MM-DD HH:mm:ss') return dayjs(value).format('YYYY-MM-DD HH:mm:ss')
@@ -276,6 +331,7 @@ const loadData = async () => {
const res = await getMembershipRedeemCodes(query) const res = await getMembershipRedeemCodes(query)
const payload = res.data || {} const payload = res.data || {}
list.value = payload.list || payload.items || [] list.value = payload.list || payload.items || []
selectedRows.value = []
total.value = payload.total || 0 total.value = payload.total || 0
query.page = payload.page || query.page query.page = payload.page || query.page
query.page_size = payload.page_size || query.page_size query.page_size = payload.page_size || query.page_size
@@ -301,10 +357,42 @@ const handleSizeChange = () => {
loadData() loadData()
} }
const handleSelectionChange = (rows) => {
selectedRows.value = rows || []
}
const openCreateDialog = () => { const openCreateDialog = () => {
createDialogVisible.value = true createDialogVisible.value = true
} }
const openDetailDialog = (row) => {
detailRecord.value = row
detailDialogVisible.value = true
}
const handleCopyCode = async (row) => {
if (!row?.code) {
ElMessage.warning('兑换码为空,无法复制')
return
}
try {
if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(row.code)
} else {
const input = document.createElement('input')
input.value = row.code
document.body.appendChild(input)
input.select()
document.execCommand('copy')
document.body.removeChild(input)
}
ElMessage.success('兑换码已复制')
} catch (error) {
ElMessage.error('复制失败,请手动复制')
}
}
const handleCreate = async () => { const handleCreate = async () => {
if (!createFormRef.value) return if (!createFormRef.value) return
try { try {
@@ -341,6 +429,43 @@ const handleDisable = async (row) => {
loadData() loadData()
} }
const handleBatchDisable = async () => {
if (selectedActiveRows.value.length === 0) {
ElMessage.warning('请先选择可用状态的兑换码')
return
}
const activeCount = selectedActiveRows.value.length
await ElMessageBox.confirm(`确认批量停用 ${activeCount} 条兑换码吗?`, '批量停用', {
type: 'warning',
confirmButtonText: '确定',
cancelButtonText: '取消'
})
batchDisabling.value = true
try {
const results = await Promise.allSettled(
selectedActiveRows.value.map((item) =>
updateMembershipRedeemCodeStatus(item.id, { status: 'disabled' })
)
)
const successCount = results.filter((item) => item.status === 'fulfilled').length
const failCount = activeCount - successCount
if (successCount > 0) {
ElMessage.success(`批量停用完成:成功 ${successCount}${failCount > 0 ? `,失败 ${failCount}` : ''}`)
} else {
ElMessage.error('批量停用失败,请稍后重试')
}
tableRef.value?.clearSelection()
await loadData()
} finally {
batchDisabling.value = false
}
}
onMounted(async () => { onMounted(async () => {
await Promise.all([loadMiniPrograms(), loadOverview(), loadData()]) await Promise.all([loadMiniPrograms(), loadOverview(), loadData()])
}) })
@@ -370,6 +495,12 @@ onMounted(async () => {
gap: 12px; gap: 12px;
} }
.right-actions {
display: flex;
align-items: center;
gap: 8px;
}
.filters { .filters {
display: flex; display: flex;
align-items: center; align-items: center;
+61 -4
View File
@@ -89,8 +89,10 @@
</template> </template>
<script setup> <script setup>
import { ElMessage } from 'element-plus' import { ElMessage, ElMessageBox } from 'element-plus'
import { onMounted, reactive, ref } from 'vue' import { onMounted, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useUserStore } from '../../stores/user'
import { import {
getAdminSettings, getAdminSettings,
getSystemConfig, getSystemConfig,
@@ -99,6 +101,16 @@ import {
updateSystemConfig updateSystemConfig
} from '../../api/settings' } from '../../api/settings'
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
const validTabs = ['profile', 'security', 'system']
const normalizeTab = (tab) => {
if (typeof tab !== 'string') return 'profile'
return validTabs.includes(tab) ? tab : 'profile'
}
const activeTab = ref('profile') const activeTab = ref('profile')
const profileFormRef = ref(null) const profileFormRef = ref(null)
const passwordFormRef = ref(null) const passwordFormRef = ref(null)
@@ -150,7 +162,21 @@ const passwordRules = {
old_password: [{ required: true, message: '请输入当前密码', trigger: 'blur' }], old_password: [{ required: true, message: '请输入当前密码', trigger: 'blur' }],
new_password: [ new_password: [
{ required: true, message: '请输入新密码', trigger: 'blur' }, { required: true, message: '请输入新密码', trigger: 'blur' },
{ min: 6, message: '新密码至少 6 位', trigger: 'blur' } { min: 6, message: '新密码至少 6 位', trigger: 'blur' },
{
validator: (_, value, callback) => {
if (!value) {
callback()
return
}
if (value === passwordForm.old_password) {
callback(new Error('新密码不能与当前密码相同'))
return
}
callback()
},
trigger: 'blur'
}
], ],
confirm_password: [{ validator: confirmPasswordValidator, trigger: 'blur' }] confirm_password: [{ validator: confirmPasswordValidator, trigger: 'blur' }]
} }
@@ -212,10 +238,18 @@ const handleChangePassword = async () => {
old_password: passwordForm.old_password, old_password: passwordForm.old_password,
new_password: passwordForm.new_password new_password: passwordForm.new_password
}) })
passwordForm.old_password = '' passwordForm.old_password = ''
passwordForm.new_password = '' passwordForm.new_password = ''
passwordForm.confirm_password = '' passwordForm.confirm_password = ''
ElMessage.success('密码更新成功,请使用新密码重新登录')
await ElMessageBox.alert('密码更新成功,请使用新密码重新登录。', '提示', {
confirmButtonText: '确定',
type: 'success'
})
userStore.logout()
router.push('/login')
} finally { } finally {
passwordSaving.value = false passwordSaving.value = false
} }
@@ -231,7 +265,30 @@ const handleSaveSystem = async () => {
} }
} }
watch(
() => route.query.tab,
(tab) => {
const normalizedTab = normalizeTab(tab)
if (activeTab.value !== normalizedTab) {
activeTab.value = normalizedTab
}
},
{ immediate: true }
)
watch(activeTab, (tab) => {
if (route.query.tab === tab) return
router.replace({
path: route.path,
query: {
...route.query,
tab
}
}).catch(() => {})
})
onMounted(async () => { onMounted(async () => {
activeTab.value = normalizeTab(route.query.tab)
await Promise.all([loadProfile(), loadSystemConfig()]) await Promise.all([loadProfile(), loadSystemConfig()])
}) })
</script> </script>