feat(settings): improve profile navigation and password reset UX
This commit is contained in:
@@ -184,7 +184,10 @@ const handleCommand = async (command) => {
|
||||
// 用户取消
|
||||
}
|
||||
} else if (command === 'profile') {
|
||||
ElMessage.info('个人信息功能开发中...')
|
||||
router.push({
|
||||
path: '/settings',
|
||||
query: { tab: 'profile' }
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -67,11 +67,29 @@
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</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="code" label="兑换码" min-width="180" />
|
||||
<el-table-column label="小程序" min-width="140">
|
||||
@@ -111,8 +129,10 @@
|
||||
{{ formatDateTime(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openDetailDialog(row)">详情</el-button>
|
||||
<el-button link @click="handleCopyCode(row)">复制</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 'active'"
|
||||
link
|
||||
@@ -181,13 +201,40 @@
|
||||
<el-button type="primary" :loading="creating" @click="handleCreate">生成</el-button>
|
||||
</template>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import dayjs from 'dayjs'
|
||||
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 {
|
||||
createMembershipRedeemCodes,
|
||||
@@ -201,8 +248,13 @@ const creating = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const miniProgramOptions = ref([])
|
||||
const tableRef = ref(null)
|
||||
const selectedRows = ref([])
|
||||
const batchDisabling = ref(false)
|
||||
const createDialogVisible = ref(false)
|
||||
const createFormRef = ref(null)
|
||||
const detailDialogVisible = ref(false)
|
||||
const detailRecord = ref(null)
|
||||
|
||||
const overview = reactive({
|
||||
total_members: 0,
|
||||
@@ -236,6 +288,9 @@ const createRules = {
|
||||
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) => {
|
||||
if (!value) return '-'
|
||||
return dayjs(value).format('YYYY-MM-DD HH:mm:ss')
|
||||
@@ -276,6 +331,7 @@ const loadData = async () => {
|
||||
const res = await getMembershipRedeemCodes(query)
|
||||
const payload = res.data || {}
|
||||
list.value = payload.list || payload.items || []
|
||||
selectedRows.value = []
|
||||
total.value = payload.total || 0
|
||||
query.page = payload.page || query.page
|
||||
query.page_size = payload.page_size || query.page_size
|
||||
@@ -301,10 +357,42 @@ const handleSizeChange = () => {
|
||||
loadData()
|
||||
}
|
||||
|
||||
const handleSelectionChange = (rows) => {
|
||||
selectedRows.value = rows || []
|
||||
}
|
||||
|
||||
const openCreateDialog = () => {
|
||||
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 () => {
|
||||
if (!createFormRef.value) return
|
||||
try {
|
||||
@@ -341,6 +429,43 @@ const handleDisable = async (row) => {
|
||||
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 () => {
|
||||
await Promise.all([loadMiniPrograms(), loadOverview(), loadData()])
|
||||
})
|
||||
@@ -370,6 +495,12 @@ onMounted(async () => {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.right-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -89,8 +89,10 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useUserStore } from '../../stores/user'
|
||||
import {
|
||||
getAdminSettings,
|
||||
getSystemConfig,
|
||||
@@ -99,6 +101,16 @@ import {
|
||||
updateSystemConfig
|
||||
} 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 profileFormRef = ref(null)
|
||||
const passwordFormRef = ref(null)
|
||||
@@ -150,7 +162,21 @@ const passwordRules = {
|
||||
old_password: [{ required: true, message: '请输入当前密码', trigger: 'blur' }],
|
||||
new_password: [
|
||||
{ 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' }]
|
||||
}
|
||||
@@ -212,10 +238,18 @@ const handleChangePassword = async () => {
|
||||
old_password: passwordForm.old_password,
|
||||
new_password: passwordForm.new_password
|
||||
})
|
||||
|
||||
passwordForm.old_password = ''
|
||||
passwordForm.new_password = ''
|
||||
passwordForm.confirm_password = ''
|
||||
ElMessage.success('密码更新成功,请使用新密码重新登录')
|
||||
|
||||
await ElMessageBox.alert('密码更新成功,请使用新密码重新登录。', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
type: 'success'
|
||||
})
|
||||
|
||||
userStore.logout()
|
||||
router.push('/login')
|
||||
} finally {
|
||||
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 () => {
|
||||
activeTab.value = normalizeTab(route.query.tab)
|
||||
await Promise.all([loadProfile(), loadSystemConfig()])
|
||||
})
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user