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') {
|
} else if (command === 'profile') {
|
||||||
ElMessage.info('个人信息功能开发中...')
|
router.push({
|
||||||
|
path: '/settings',
|
||||||
|
query: { tab: 'profile' }
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -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>
|
||||||
<el-button type="primary" @click="openCreateDialog">生成兑换码</el-button>
|
<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>
|
</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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user