feat: 完成会员/保质期/系统设置/去水印管理页面开发
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
export function getExpirySummary() {
|
||||
return request({
|
||||
url: '/api/admin/expiry/summary',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getExpiryItems(params) {
|
||||
return request({
|
||||
url: '/api/admin/expiry/items',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function createExpiryItem(data) {
|
||||
return request({
|
||||
url: '/api/admin/expiry/items',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateExpiryItem(id, data) {
|
||||
return request({
|
||||
url: `/api/admin/expiry/items/${id}`,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteExpiryItem(id) {
|
||||
return request({
|
||||
url: `/api/admin/expiry/items/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
export function updateExpiryItemStatus(id, status) {
|
||||
return request({
|
||||
url: `/api/admin/expiry/items/${id}/status`,
|
||||
method: 'post',
|
||||
data: { status }
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
export function getMembershipOverview() {
|
||||
return request({
|
||||
url: '/api/admin/memberships/overview',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getMembershipRedeemCodes(params) {
|
||||
return request({
|
||||
url: '/api/admin/memberships/redeem-codes',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function createMembershipRedeemCodes(data) {
|
||||
return request({
|
||||
url: '/api/admin/memberships/redeem-codes',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateMembershipRedeemCodeStatus(id, data) {
|
||||
return request({
|
||||
url: `/api/admin/memberships/redeem-codes/${id}/status`,
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
export function getAdminSettings() {
|
||||
return request({
|
||||
url: '/api/admin/settings',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function updateAdminProfile(data) {
|
||||
return request({
|
||||
url: '/api/admin/settings/profile',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function updateAdminPassword(data) {
|
||||
return request({
|
||||
url: '/api/admin/settings/password',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function getSystemConfig() {
|
||||
return request({
|
||||
url: '/api/admin/settings/system',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function updateSystemConfig(data) {
|
||||
return request({
|
||||
url: '/api/admin/settings/system',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import request from '../utils/request'
|
||||
|
||||
export function getWatermarkOverview() {
|
||||
return request({
|
||||
url: '/api/admin/watermark/overview',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
export function getWatermarkTasks(params) {
|
||||
return request({
|
||||
url: '/api/admin/watermark/tasks',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function retryWatermarkTask(id) {
|
||||
return request({
|
||||
url: `/api/admin/watermark/tasks/${id}/retry`,
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteWatermarkTask(id) {
|
||||
return request({
|
||||
url: `/api/admin/watermark/tasks/${id}`,
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
+474
-6
@@ -1,8 +1,476 @@
|
||||
<template>
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>保质期管理</span>
|
||||
</template>
|
||||
<el-empty description="开发中,下一步对接保质期后台接口" />
|
||||
</el-card>
|
||||
<div class="expiry-page">
|
||||
<el-row :gutter="16" class="cards">
|
||||
<el-col :xs="12" :sm="12" :md="4">
|
||||
<el-card shadow="hover">
|
||||
<div class="card-value">{{ summary.total_items || 0 }}</div>
|
||||
<div class="card-label">物品总数</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="12" :md="4">
|
||||
<el-card shadow="hover">
|
||||
<div class="card-value warning">{{ summary.expiring_soon || 0 }}</div>
|
||||
<div class="card-label">即将过期</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="12" :md="4">
|
||||
<el-card shadow="hover">
|
||||
<div class="card-value danger">{{ summary.expired || 0 }}</div>
|
||||
<div class="card-label">已过期</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="12" :md="4">
|
||||
<el-card shadow="hover">
|
||||
<div class="card-value success">{{ summary.normal || 0 }}</div>
|
||||
<div class="card-label">正常</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="12" :md="4">
|
||||
<el-card shadow="hover">
|
||||
<div class="card-value">{{ summary.used || 0 }}</div>
|
||||
<div class="card-label">已使用</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="12" :md="4">
|
||||
<el-card shadow="hover">
|
||||
<div class="card-value">{{ summary.discarded || 0 }}</div>
|
||||
<div class="card-label">已丢弃</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="header-row">
|
||||
<div class="filters">
|
||||
<el-input
|
||||
v-model="query.keyword"
|
||||
placeholder="搜索物品名称"
|
||||
clearable
|
||||
style="width: 220px"
|
||||
@keyup.enter="handleSearch"
|
||||
@clear="handleSearch"
|
||||
/>
|
||||
<el-select
|
||||
v-model="query.status"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
placeholder="全部状态"
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option label="正常" value="normal" />
|
||||
<el-option label="即将过期" value="expiring" />
|
||||
<el-option label="已过期" value="expired" />
|
||||
<el-option label="已使用" value="used" />
|
||||
<el-option label="已丢弃" value="discarded" />
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="query.category"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
placeholder="全部分类"
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option label="食品" value="food" />
|
||||
<el-option label="药品" value="medicine" />
|
||||
<el-option label="化妆品" value="cosmetic" />
|
||||
<el-option label="其他" value="other" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
<el-button type="primary" @click="openCreateDialog">新增物品</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" :data="list" stripe>
|
||||
<el-table-column prop="id" label="ID" width="70" />
|
||||
<el-table-column prop="name" label="物品名称" min-width="160" />
|
||||
<el-table-column label="分类" width="110">
|
||||
<template #default="{ row }">
|
||||
{{ categoryText(row.category) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="过期日期" width="130">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.expiry_date) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="剩余天数" width="100">
|
||||
<template #default="{ row }">
|
||||
<span :class="daysClass(row.days_left)">{{ row.days_left ?? '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="数量" width="80" prop="quantity" />
|
||||
<el-table-column prop="location" label="位置" min-width="120" />
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusTagType(row.status)">{{ statusText(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="170">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="220" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button link type="primary" @click="openEditDialog(row)">编辑</el-button>
|
||||
<el-button
|
||||
v-if="!['used', 'discarded'].includes(row.status)"
|
||||
link
|
||||
type="success"
|
||||
@click="handleMarkStatus(row, 'used')"
|
||||
>
|
||||
标记已使用
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="!['used', 'discarded'].includes(row.status)"
|
||||
link
|
||||
type="warning"
|
||||
@click="handleMarkStatus(row, 'discarded')"
|
||||
>
|
||||
标记丢弃
|
||||
</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="query.page"
|
||||
v-model:page-size="query.page_size"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
@current-change="loadData"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑物品' : '新增物品'" width="560px">
|
||||
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
||||
<el-form-item label="物品名称" prop="name">
|
||||
<el-input v-model="form.name" placeholder="请输入名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="分类" prop="category">
|
||||
<el-select v-model="form.category" style="width: 100%">
|
||||
<el-option label="食品" value="food" />
|
||||
<el-option label="药品" value="medicine" />
|
||||
<el-option label="化妆品" value="cosmetic" />
|
||||
<el-option label="其他" value="other" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="过期日期" prop="expiry_date">
|
||||
<el-date-picker
|
||||
v-model="form.expiry_date"
|
||||
type="date"
|
||||
value-format="YYYY-MM-DD"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="数量" prop="quantity">
|
||||
<el-input-number v-model="form.quantity" :min="1" :max="999" />
|
||||
</el-form-item>
|
||||
<el-form-item label="存放位置">
|
||||
<el-input v-model="form.location" placeholder="可选" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="form.remark" type="textarea" :rows="3" placeholder="可选" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="dialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="handleSubmit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import dayjs from 'dayjs'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import {
|
||||
createExpiryItem,
|
||||
deleteExpiryItem,
|
||||
getExpiryItems,
|
||||
getExpirySummary,
|
||||
updateExpiryItem,
|
||||
updateExpiryItemStatus
|
||||
} from '../../api/expiry'
|
||||
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const dialogVisible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const formRef = ref(null)
|
||||
|
||||
const summary = reactive({
|
||||
total_items: 0,
|
||||
expiring_soon: 0,
|
||||
expired: 0,
|
||||
normal: 0,
|
||||
used: 0,
|
||||
discarded: 0
|
||||
})
|
||||
|
||||
const query = reactive({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
keyword: '',
|
||||
status: undefined,
|
||||
category: undefined
|
||||
})
|
||||
|
||||
const form = reactive({
|
||||
id: undefined,
|
||||
name: '',
|
||||
category: 'food',
|
||||
expiry_date: '',
|
||||
quantity: 1,
|
||||
location: '',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入物品名称', trigger: 'blur' }],
|
||||
category: [{ required: true, message: '请选择分类', trigger: 'change' }],
|
||||
expiry_date: [{ required: true, message: '请选择过期日期', trigger: 'change' }],
|
||||
quantity: [{ required: true, message: '请输入数量', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const formatDateTime = (value) => {
|
||||
if (!value) return '-'
|
||||
return dayjs(value).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
const formatDate = (value) => {
|
||||
if (!value) return '-'
|
||||
return dayjs(value).format('YYYY-MM-DD')
|
||||
}
|
||||
|
||||
const categoryText = (category) => {
|
||||
if (category === 'food') return '食品'
|
||||
if (category === 'medicine') return '药品'
|
||||
if (category === 'cosmetic') return '化妆品'
|
||||
if (category === 'other') return '其他'
|
||||
return category || '-'
|
||||
}
|
||||
|
||||
const statusText = (status) => {
|
||||
if (status === 'normal') return '正常'
|
||||
if (status === 'expiring') return '即将过期'
|
||||
if (status === 'expired') return '已过期'
|
||||
if (status === 'used') return '已使用'
|
||||
if (status === 'discarded') return '已丢弃'
|
||||
return status || '-'
|
||||
}
|
||||
|
||||
const statusTagType = (status) => {
|
||||
if (status === 'normal') return 'success'
|
||||
if (status === 'expiring') return 'warning'
|
||||
if (status === 'expired') return 'danger'
|
||||
if (status === 'used') return 'info'
|
||||
if (status === 'discarded') return ''
|
||||
return 'info'
|
||||
}
|
||||
|
||||
const daysClass = (daysLeft) => {
|
||||
if (daysLeft == null) return ''
|
||||
if (daysLeft < 0) return 'danger'
|
||||
if (daysLeft <= 7) return 'warning'
|
||||
return 'success'
|
||||
}
|
||||
|
||||
const loadSummary = async () => {
|
||||
const res = await getExpirySummary()
|
||||
const payload = res.data || {}
|
||||
summary.total_items = payload.total_items || 0
|
||||
summary.expiring_soon = payload.expiring_soon || 0
|
||||
summary.expired = payload.expired || 0
|
||||
summary.normal = payload.normal || 0
|
||||
summary.used = payload.used || 0
|
||||
summary.discarded = payload.discarded || 0
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getExpiryItems(query)
|
||||
const payload = res.data || {}
|
||||
list.value = payload.list || payload.items || []
|
||||
total.value = payload.total || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
query.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
const resetSearch = () => {
|
||||
query.keyword = ''
|
||||
query.status = undefined
|
||||
query.category = undefined
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
const handleSizeChange = () => {
|
||||
query.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
const openCreateDialog = () => {
|
||||
isEdit.value = false
|
||||
form.id = undefined
|
||||
form.name = ''
|
||||
form.category = 'food'
|
||||
form.expiry_date = ''
|
||||
form.quantity = 1
|
||||
form.location = ''
|
||||
form.remark = ''
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const openEditDialog = (row) => {
|
||||
isEdit.value = true
|
||||
form.id = row.id
|
||||
form.name = row.name || ''
|
||||
form.category = row.category || 'food'
|
||||
form.expiry_date = row.expiry_date ? dayjs(row.expiry_date).format('YYYY-MM-DD') : ''
|
||||
form.quantity = row.quantity || 1
|
||||
form.location = row.location || ''
|
||||
form.remark = row.remark || ''
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) return
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
} catch (error) {
|
||||
return
|
||||
}
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const payload = {
|
||||
name: form.name,
|
||||
category: form.category,
|
||||
expiry_date: form.expiry_date,
|
||||
quantity: form.quantity,
|
||||
location: form.location,
|
||||
remark: form.remark
|
||||
}
|
||||
|
||||
if (isEdit.value && form.id) {
|
||||
await updateExpiryItem(form.id, payload)
|
||||
ElMessage.success('更新成功')
|
||||
} else {
|
||||
await createExpiryItem(payload)
|
||||
ElMessage.success('创建成功')
|
||||
}
|
||||
|
||||
dialogVisible.value = false
|
||||
await Promise.all([loadSummary(), loadData()])
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleMarkStatus = async (row, status) => {
|
||||
const actionText = status === 'used' ? '标记为已使用' : '标记为已丢弃'
|
||||
await ElMessageBox.confirm(`确认${actionText}吗?`, '提示', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消'
|
||||
})
|
||||
await updateExpiryItemStatus(row.id, status)
|
||||
ElMessage.success('更新成功')
|
||||
await Promise.all([loadSummary(), loadData()])
|
||||
}
|
||||
|
||||
const handleDelete = async (row) => {
|
||||
await ElMessageBox.confirm('删除后不可恢复,确认删除该物品?', '提示', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消'
|
||||
})
|
||||
await deleteExpiryItem(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
await Promise.all([loadSummary(), loadData()])
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadSummary(), loadData()])
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cards {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.card-value.warning {
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
.card-value.danger {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.card-value.success {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
margin-top: 8px;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.danger {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: #67c23a;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,385 @@
|
||||
<template>
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>会员管理</span>
|
||||
</template>
|
||||
<el-empty description="开发中,下一步对接会员管理接口" />
|
||||
</el-card>
|
||||
<div class="memberships-page">
|
||||
<el-row :gutter="16" class="cards">
|
||||
<el-col :xs="12" :sm="12" :md="6">
|
||||
<el-card shadow="hover">
|
||||
<div class="card-value">{{ overview.total_members || 0 }}</div>
|
||||
<div class="card-label">会员总数</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="12" :md="6">
|
||||
<el-card shadow="hover">
|
||||
<div class="card-value">{{ overview.active_members || 0 }}</div>
|
||||
<div class="card-label">有效会员</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="12" :md="6">
|
||||
<el-card shadow="hover">
|
||||
<div class="card-value">{{ overview.expiring_soon || 0 }}</div>
|
||||
<div class="card-label">7天内到期</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="12" :md="6">
|
||||
<el-card shadow="hover">
|
||||
<div class="card-value">{{ overview.today_redeemed || 0 }}</div>
|
||||
<div class="card-label">今日兑换</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="header-row">
|
||||
<div class="filters">
|
||||
<el-input
|
||||
v-model="query.keyword"
|
||||
placeholder="搜索兑换码"
|
||||
clearable
|
||||
style="width: 200px"
|
||||
@keyup.enter="handleSearch"
|
||||
@clear="handleSearch"
|
||||
/>
|
||||
<el-select
|
||||
v-model="query.mini_program_id"
|
||||
placeholder="全部小程序"
|
||||
clearable
|
||||
style="width: 180px"
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in miniProgramOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="query.status"
|
||||
placeholder="全部状态"
|
||||
clearable
|
||||
style="width: 140px"
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option label="可用" value="active" />
|
||||
<el-option label="已停用" value="disabled" />
|
||||
<el-option label="已过期" value="expired" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
<el-button type="primary" @click="openCreateDialog">生成兑换码</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" :data="list" stripe>
|
||||
<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">
|
||||
<template #default="{ row }">
|
||||
{{ row.mini_program_name || row.mini_program?.name || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="套餐类型" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ row.package_type || row.plan_name || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="时长(天)" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.duration_days || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="使用次数" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.used_count || 0 }}/{{ row.max_uses || 1 }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusTagType(row.status)">
|
||||
{{ statusText(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="过期时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.expires_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="120" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.status === 'active'"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDisable(row)"
|
||||
>
|
||||
停用
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="query.page"
|
||||
v-model:page-size="query.page_size"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
@current-change="loadData"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="createDialogVisible" title="生成兑换码" width="520px">
|
||||
<el-form ref="createFormRef" :model="createForm" :rules="createRules" label-width="110px">
|
||||
<el-form-item label="所属小程序" prop="mini_program_id">
|
||||
<el-select v-model="createForm.mini_program_id" placeholder="请选择小程序" style="width: 100%">
|
||||
<el-option
|
||||
v-for="item in miniProgramOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="套餐类型" prop="package_type">
|
||||
<el-select v-model="createForm.package_type" style="width: 100%">
|
||||
<el-option label="月卡" value="month" />
|
||||
<el-option label="季卡" value="quarter" />
|
||||
<el-option label="年卡" value="year" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="时长(天)" prop="duration_days">
|
||||
<el-input-number v-model="createForm.duration_days" :min="1" :max="3650" />
|
||||
</el-form-item>
|
||||
<el-form-item label="生成数量" prop="quantity">
|
||||
<el-input-number v-model="createForm.quantity" :min="1" :max="200" />
|
||||
</el-form-item>
|
||||
<el-form-item label="每码可用次数" prop="max_uses">
|
||||
<el-input-number v-model="createForm.max_uses" :min="1" :max="100" />
|
||||
</el-form-item>
|
||||
<el-form-item label="到期时间">
|
||||
<el-date-picker
|
||||
v-model="createForm.expires_at"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
placeholder="可选,不填则长期有效"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="createDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="creating" @click="handleCreate">生成</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import dayjs from 'dayjs'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { getMiniPrograms } from '../../api/miniProgram'
|
||||
import {
|
||||
createMembershipRedeemCodes,
|
||||
getMembershipOverview,
|
||||
getMembershipRedeemCodes,
|
||||
updateMembershipRedeemCodeStatus
|
||||
} from '../../api/membership'
|
||||
|
||||
const loading = ref(false)
|
||||
const creating = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const miniProgramOptions = ref([])
|
||||
const createDialogVisible = ref(false)
|
||||
const createFormRef = ref(null)
|
||||
|
||||
const overview = reactive({
|
||||
total_members: 0,
|
||||
active_members: 0,
|
||||
expiring_soon: 0,
|
||||
today_redeemed: 0
|
||||
})
|
||||
|
||||
const query = reactive({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
mini_program_id: undefined,
|
||||
status: undefined,
|
||||
keyword: ''
|
||||
})
|
||||
|
||||
const createForm = reactive({
|
||||
mini_program_id: undefined,
|
||||
package_type: 'month',
|
||||
duration_days: 30,
|
||||
quantity: 10,
|
||||
max_uses: 1,
|
||||
expires_at: ''
|
||||
})
|
||||
|
||||
const createRules = {
|
||||
mini_program_id: [{ required: true, message: '请选择小程序', trigger: 'change' }],
|
||||
package_type: [{ required: true, message: '请选择套餐类型', trigger: 'change' }],
|
||||
duration_days: [{ required: true, message: '请输入时长', trigger: 'blur' }],
|
||||
quantity: [{ required: true, message: '请输入生成数量', trigger: 'blur' }],
|
||||
max_uses: [{ required: true, message: '请输入可用次数', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const formatDateTime = (value) => {
|
||||
if (!value) return '-'
|
||||
return dayjs(value).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
const statusText = (status) => {
|
||||
if (status === 'active') return '可用'
|
||||
if (status === 'disabled') return '已停用'
|
||||
if (status === 'expired') return '已过期'
|
||||
return status || '-'
|
||||
}
|
||||
|
||||
const statusTagType = (status) => {
|
||||
if (status === 'active') return 'success'
|
||||
if (status === 'disabled') return 'info'
|
||||
if (status === 'expired') return 'danger'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
const loadMiniPrograms = async () => {
|
||||
const res = await getMiniPrograms({ page: 1, page_size: 200 })
|
||||
const payload = res.data || {}
|
||||
miniProgramOptions.value = payload.list || []
|
||||
}
|
||||
|
||||
const loadOverview = async () => {
|
||||
const res = await getMembershipOverview()
|
||||
const payload = res.data || {}
|
||||
overview.total_members = payload.total_members || 0
|
||||
overview.active_members = payload.active_members || 0
|
||||
overview.expiring_soon = payload.expiring_soon || 0
|
||||
overview.today_redeemed = payload.today_redeemed || 0
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getMembershipRedeemCodes(query)
|
||||
const payload = res.data || {}
|
||||
list.value = payload.list || payload.items || []
|
||||
total.value = payload.total || 0
|
||||
query.page = payload.page || query.page
|
||||
query.page_size = payload.page_size || query.page_size
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
query.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
const resetSearch = () => {
|
||||
query.keyword = ''
|
||||
query.mini_program_id = undefined
|
||||
query.status = undefined
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
const handleSizeChange = () => {
|
||||
query.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
const openCreateDialog = () => {
|
||||
createDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!createFormRef.value) return
|
||||
try {
|
||||
await createFormRef.value.validate()
|
||||
} catch (error) {
|
||||
return
|
||||
}
|
||||
|
||||
creating.value = true
|
||||
try {
|
||||
const res = await createMembershipRedeemCodes(createForm)
|
||||
const codes = res?.data?.codes || []
|
||||
ElMessage.success(`生成成功,共 ${createForm.quantity} 个`)
|
||||
if (codes.length > 0) {
|
||||
await ElMessageBox.alert(codes.join('\n'), '新生成兑换码', {
|
||||
confirmButtonText: '确定'
|
||||
})
|
||||
}
|
||||
createDialogVisible.value = false
|
||||
loadData()
|
||||
} finally {
|
||||
creating.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleDisable = async (row) => {
|
||||
await ElMessageBox.confirm('停用后该兑换码将无法再使用,是否继续?', '提示', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消'
|
||||
})
|
||||
await updateMembershipRedeemCodeStatus(row.id, { status: 'disabled' })
|
||||
ElMessage.success('已停用')
|
||||
loadData()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadMiniPrograms(), loadOverview(), loadData()])
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cards {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
margin-top: 8px;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,6 +3,235 @@
|
||||
<template #header>
|
||||
<span>系统设置</span>
|
||||
</template>
|
||||
<el-empty description="开发中,下一步补充密码修改和日志查询" />
|
||||
|
||||
<el-tabs v-model="activeTab">
|
||||
<el-tab-pane label="个人资料" name="profile">
|
||||
<el-form
|
||||
ref="profileFormRef"
|
||||
:model="profileForm"
|
||||
:rules="profileRules"
|
||||
label-width="120px"
|
||||
style="max-width: 680px"
|
||||
>
|
||||
<el-form-item label="用户名" prop="username">
|
||||
<el-input v-model="profileForm.username" disabled />
|
||||
</el-form-item>
|
||||
<el-form-item label="显示名称" prop="display_name">
|
||||
<el-input v-model="profileForm.display_name" placeholder="请输入显示名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="profileForm.email" placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="profileForm.phone" placeholder="请输入手机号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="时区">
|
||||
<el-select v-model="profileForm.timezone" style="width: 100%">
|
||||
<el-option label="Asia/Shanghai" value="Asia/Shanghai" />
|
||||
<el-option label="UTC" value="UTC" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="profileSaving" @click="handleSaveProfile">保存资料</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="安全设置" name="security">
|
||||
<el-form
|
||||
ref="passwordFormRef"
|
||||
:model="passwordForm"
|
||||
:rules="passwordRules"
|
||||
label-width="120px"
|
||||
style="max-width: 560px"
|
||||
>
|
||||
<el-form-item label="当前密码" prop="old_password">
|
||||
<el-input v-model="passwordForm.old_password" type="password" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item label="新密码" prop="new_password">
|
||||
<el-input v-model="passwordForm.new_password" type="password" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item label="确认新密码" prop="confirm_password">
|
||||
<el-input v-model="passwordForm.confirm_password" type="password" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="passwordSaving" @click="handleChangePassword">
|
||||
更新密码
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
|
||||
<el-tab-pane label="系统参数" name="system">
|
||||
<el-form label-width="180px" style="max-width: 720px">
|
||||
<el-form-item label="后台名称">
|
||||
<el-input v-model="systemForm.site_name" placeholder="系统名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="允许注册">
|
||||
<el-switch v-model="systemForm.allow_register" />
|
||||
</el-form-item>
|
||||
<el-form-item label="登录失败锁定阈值">
|
||||
<el-input-number v-model="systemForm.login_fail_limit" :min="3" :max="20" />
|
||||
</el-form-item>
|
||||
<el-form-item label="默认分页大小">
|
||||
<el-input-number v-model="systemForm.default_page_size" :min="10" :max="200" />
|
||||
</el-form-item>
|
||||
<el-form-item label="操作日志保留天数">
|
||||
<el-input-number v-model="systemForm.audit_log_retention_days" :min="7" :max="3650" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="systemSaving" @click="handleSaveSystem">保存参数</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import {
|
||||
getAdminSettings,
|
||||
getSystemConfig,
|
||||
updateAdminPassword,
|
||||
updateAdminProfile,
|
||||
updateSystemConfig
|
||||
} from '../../api/settings'
|
||||
|
||||
const activeTab = ref('profile')
|
||||
const profileFormRef = ref(null)
|
||||
const passwordFormRef = ref(null)
|
||||
|
||||
const profileSaving = ref(false)
|
||||
const passwordSaving = ref(false)
|
||||
const systemSaving = ref(false)
|
||||
|
||||
const profileForm = reactive({
|
||||
username: '',
|
||||
display_name: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
timezone: 'Asia/Shanghai'
|
||||
})
|
||||
|
||||
const passwordForm = reactive({
|
||||
old_password: '',
|
||||
new_password: '',
|
||||
confirm_password: ''
|
||||
})
|
||||
|
||||
const systemForm = reactive({
|
||||
site_name: '多小程序管理后台',
|
||||
allow_register: false,
|
||||
login_fail_limit: 5,
|
||||
default_page_size: 20,
|
||||
audit_log_retention_days: 180
|
||||
})
|
||||
|
||||
const profileRules = {
|
||||
display_name: [{ required: true, message: '请输入显示名称', trigger: 'blur' }],
|
||||
email: [{ type: 'email', message: '邮箱格式不正确', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const confirmPasswordValidator = (rule, value, callback) => {
|
||||
if (!value) {
|
||||
callback(new Error('请再次输入新密码'))
|
||||
return
|
||||
}
|
||||
if (value !== passwordForm.new_password) {
|
||||
callback(new Error('两次输入密码不一致'))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
}
|
||||
|
||||
const passwordRules = {
|
||||
old_password: [{ required: true, message: '请输入当前密码', trigger: 'blur' }],
|
||||
new_password: [
|
||||
{ required: true, message: '请输入新密码', trigger: 'blur' },
|
||||
{ min: 6, message: '新密码至少 6 位', trigger: 'blur' }
|
||||
],
|
||||
confirm_password: [{ validator: confirmPasswordValidator, trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const loadProfile = async () => {
|
||||
const res = await getAdminSettings()
|
||||
const payload = res.data || {}
|
||||
profileForm.username = payload.username || ''
|
||||
profileForm.display_name = payload.display_name || payload.nickname || payload.username || ''
|
||||
profileForm.email = payload.email || ''
|
||||
profileForm.phone = payload.phone || ''
|
||||
profileForm.timezone = payload.timezone || 'Asia/Shanghai'
|
||||
}
|
||||
|
||||
const loadSystemConfig = async () => {
|
||||
const res = await getSystemConfig()
|
||||
const payload = res.data || {}
|
||||
systemForm.site_name = payload.site_name || systemForm.site_name
|
||||
systemForm.allow_register = payload.allow_register ?? systemForm.allow_register
|
||||
systemForm.login_fail_limit = payload.login_fail_limit || systemForm.login_fail_limit
|
||||
systemForm.default_page_size = payload.default_page_size || systemForm.default_page_size
|
||||
systemForm.audit_log_retention_days =
|
||||
payload.audit_log_retention_days || systemForm.audit_log_retention_days
|
||||
}
|
||||
|
||||
const handleSaveProfile = async () => {
|
||||
if (!profileFormRef.value) return
|
||||
try {
|
||||
await profileFormRef.value.validate()
|
||||
} catch (error) {
|
||||
return
|
||||
}
|
||||
|
||||
profileSaving.value = true
|
||||
try {
|
||||
await updateAdminProfile({
|
||||
display_name: profileForm.display_name,
|
||||
email: profileForm.email,
|
||||
phone: profileForm.phone,
|
||||
timezone: profileForm.timezone
|
||||
})
|
||||
ElMessage.success('资料保存成功')
|
||||
} finally {
|
||||
profileSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleChangePassword = async () => {
|
||||
if (!passwordFormRef.value) return
|
||||
try {
|
||||
await passwordFormRef.value.validate()
|
||||
} catch (error) {
|
||||
return
|
||||
}
|
||||
|
||||
passwordSaving.value = true
|
||||
try {
|
||||
await updateAdminPassword({
|
||||
old_password: passwordForm.old_password,
|
||||
new_password: passwordForm.new_password
|
||||
})
|
||||
passwordForm.old_password = ''
|
||||
passwordForm.new_password = ''
|
||||
passwordForm.confirm_password = ''
|
||||
ElMessage.success('密码更新成功,请使用新密码重新登录')
|
||||
} finally {
|
||||
passwordSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleSaveSystem = async () => {
|
||||
systemSaving.value = true
|
||||
try {
|
||||
await updateSystemConfig(systemForm)
|
||||
ElMessage.success('系统参数已保存')
|
||||
} finally {
|
||||
systemSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadProfile(), loadSystemConfig()])
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,8 +1,326 @@
|
||||
<template>
|
||||
<el-card>
|
||||
<template #header>
|
||||
<span>去水印管理</span>
|
||||
</template>
|
||||
<el-empty description="开发中,下一步对接去水印任务管理接口" />
|
||||
</el-card>
|
||||
<div class="watermark-page">
|
||||
<el-row :gutter="16" class="cards">
|
||||
<el-col :xs="12" :sm="12" :md="6">
|
||||
<el-card shadow="hover">
|
||||
<div class="card-value">{{ overview.total_tasks || 0 }}</div>
|
||||
<div class="card-label">总任务数</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="12" :md="6">
|
||||
<el-card shadow="hover">
|
||||
<div class="card-value success">{{ overview.success_tasks || 0 }}</div>
|
||||
<div class="card-label">成功任务</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="12" :md="6">
|
||||
<el-card shadow="hover">
|
||||
<div class="card-value warning">{{ overview.processing_tasks || 0 }}</div>
|
||||
<div class="card-label">处理中</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :xs="12" :sm="12" :md="6">
|
||||
<el-card shadow="hover">
|
||||
<div class="card-value danger">{{ overview.failed_tasks || 0 }}</div>
|
||||
<div class="card-label">失败任务</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-card>
|
||||
<template #header>
|
||||
<div class="header-row">
|
||||
<div class="filters">
|
||||
<el-input
|
||||
v-model="query.keyword"
|
||||
clearable
|
||||
style="width: 220px"
|
||||
placeholder="搜索视频链接/用户ID"
|
||||
@keyup.enter="handleSearch"
|
||||
@clear="handleSearch"
|
||||
/>
|
||||
<el-select
|
||||
v-model="query.mini_program_id"
|
||||
clearable
|
||||
style="width: 180px"
|
||||
placeholder="全部小程序"
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in miniProgramOptions"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="query.status"
|
||||
clearable
|
||||
style="width: 150px"
|
||||
placeholder="全部状态"
|
||||
@change="handleSearch"
|
||||
>
|
||||
<el-option label="成功" value="success" />
|
||||
<el-option label="处理中" value="processing" />
|
||||
<el-option label="失败" value="failed" />
|
||||
</el-select>
|
||||
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||
<el-button @click="resetSearch">重置</el-button>
|
||||
</div>
|
||||
<el-button @click="refreshAll">刷新</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-table v-loading="loading" :data="list" stripe>
|
||||
<el-table-column prop="id" label="ID" width="80" />
|
||||
<el-table-column label="小程序" min-width="140">
|
||||
<template #default="{ row }">
|
||||
{{ row.mini_program_name || row.mini_program?.name || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="平台" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.platform || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="原始链接" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<el-link :href="row.source_url" target="_blank" type="primary" :underline="false">
|
||||
{{ row.source_url || '-' }}
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="解析结果" min-width="220">
|
||||
<template #default="{ row }">
|
||||
<el-link
|
||||
v-if="row.cleaned_url || row.result_url"
|
||||
:href="row.cleaned_url || row.result_url"
|
||||
target="_blank"
|
||||
type="success"
|
||||
:underline="false"
|
||||
>
|
||||
查看无水印链接
|
||||
</el-link>
|
||||
<span v-else>-</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="耗时(ms)" width="100">
|
||||
<template #default="{ row }">
|
||||
{{ row.duration_ms || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="statusTagType(row.status)">{{ statusText(row.status) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="失败原因" min-width="160">
|
||||
<template #default="{ row }">
|
||||
{{ row.error_message || row.fail_reason || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="创建时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatDateTime(row.created_at) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.status === 'failed'"
|
||||
link
|
||||
type="primary"
|
||||
@click="handleRetry(row)"
|
||||
>
|
||||
重试
|
||||
</el-button>
|
||||
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="query.page"
|
||||
v-model:page-size="query.page_size"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
@current-change="loadData"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import dayjs from 'dayjs'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { getMiniPrograms } from '../../api/miniProgram'
|
||||
import {
|
||||
deleteWatermarkTask,
|
||||
getWatermarkOverview,
|
||||
getWatermarkTasks,
|
||||
retryWatermarkTask
|
||||
} from '../../api/watermark'
|
||||
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
const miniProgramOptions = ref([])
|
||||
|
||||
const overview = reactive({
|
||||
total_tasks: 0,
|
||||
success_tasks: 0,
|
||||
processing_tasks: 0,
|
||||
failed_tasks: 0
|
||||
})
|
||||
|
||||
const query = reactive({
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
mini_program_id: undefined,
|
||||
status: undefined,
|
||||
keyword: ''
|
||||
})
|
||||
|
||||
const formatDateTime = (value) => {
|
||||
if (!value) return '-'
|
||||
return dayjs(value).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
|
||||
const statusText = (status) => {
|
||||
if (status === 'success') return '成功'
|
||||
if (status === 'processing') return '处理中'
|
||||
if (status === 'failed') return '失败'
|
||||
return status || '-'
|
||||
}
|
||||
|
||||
const statusTagType = (status) => {
|
||||
if (status === 'success') return 'success'
|
||||
if (status === 'processing') return 'warning'
|
||||
if (status === 'failed') return 'danger'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
const loadMiniPrograms = async () => {
|
||||
const res = await getMiniPrograms({ page: 1, page_size: 200 })
|
||||
const payload = res.data || {}
|
||||
miniProgramOptions.value = payload.list || []
|
||||
}
|
||||
|
||||
const loadOverview = async () => {
|
||||
const res = await getWatermarkOverview()
|
||||
const payload = res.data || {}
|
||||
overview.total_tasks = payload.total_tasks || 0
|
||||
overview.success_tasks = payload.success_tasks || 0
|
||||
overview.processing_tasks = payload.processing_tasks || 0
|
||||
overview.failed_tasks = payload.failed_tasks || 0
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await getWatermarkTasks(query)
|
||||
const payload = res.data || {}
|
||||
list.value = payload.list || payload.items || []
|
||||
total.value = payload.total || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const refreshAll = async () => {
|
||||
await Promise.all([loadOverview(), loadData()])
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
query.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
const resetSearch = () => {
|
||||
query.keyword = ''
|
||||
query.status = undefined
|
||||
query.mini_program_id = undefined
|
||||
handleSearch()
|
||||
}
|
||||
|
||||
const handleSizeChange = () => {
|
||||
query.page = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
const handleRetry = async (row) => {
|
||||
await retryWatermarkTask(row.id)
|
||||
ElMessage.success('已提交重试')
|
||||
refreshAll()
|
||||
}
|
||||
|
||||
const handleDelete = async (row) => {
|
||||
await ElMessageBox.confirm('确认删除该任务记录吗?', '提示', {
|
||||
type: 'warning',
|
||||
confirmButtonText: '删除',
|
||||
cancelButtonText: '取消'
|
||||
})
|
||||
await deleteWatermarkTask(row.id)
|
||||
ElMessage.success('删除成功')
|
||||
refreshAll()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadMiniPrograms(), loadOverview(), loadData()])
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cards {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 26px;
|
||||
font-weight: 600;
|
||||
color: #303133;
|
||||
}
|
||||
|
||||
.card-value.success {
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
.card-value.warning {
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
.card-value.danger {
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.card-label {
|
||||
margin-top: 8px;
|
||||
color: #909399;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.header-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user