chore(web): 收口前几轮后台改动(品牌 / 媒资上传 / 轮播图 / 类型管理 / 操作日志 / App 用户)
为什么:这些改动在前几轮就已完成并在 :3005 验证过,但一直留在工作区没有入库。 下一步 3-4 的四个页面依赖其中的公共样式入口(src/styles/business/index.css)与 api/admin 的公共类型(PageResult 等),如果不先收口,新页面的提交就会夹带旧改动, 既说不清 diff 边界,也让 index.css 这类多轮共用的文件长期处于半提交状态。 本提交只做归档,不改行为: - 品牌与接口基址改成 FruitShorts / 短剧服务(.env.*、README、vite 代理) - 去掉前端 mock 数据目录(src/data、src/permissions、stores/auth),改为走真实接口 - 新增 api/admin 下的接口封装与 src/utils/uploader.ts(媒资分片直传) - 新增/改造:轮播图管理、类型管理、操作日志、App 用户管理、后台布局与公共样式
This commit is contained in:
1 parent
40546bfd01
commit
44c2c46a68
59 files changed
+4082
-982
No files matched your search
@@ -0,0 +1,229 @@
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { getOperationLogApi, pageOperationLogsApi } from '@/api/admin/log'
|
||||
import PageHeading from '@/views/components/PageHeading.vue'
|
||||
|
||||
const loading = ref(false)
|
||||
const list = ref([])
|
||||
const total = ref(0)
|
||||
|
||||
const filters = reactive({
|
||||
username: '',
|
||||
module: '',
|
||||
status: '',
|
||||
range: null
|
||||
})
|
||||
const pageIndex = ref(1)
|
||||
const pageSize = ref(10)
|
||||
|
||||
const detailVisible = ref(false)
|
||||
const detailLoading = ref(false)
|
||||
const detail = ref(null)
|
||||
|
||||
const moduleOptions = [
|
||||
{ value: '认证', label: '认证' },
|
||||
{ value: '内容管理', label: '内容管理' },
|
||||
{ value: '后台管理', label: '后台管理' }
|
||||
]
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await pageOperationLogsApi({
|
||||
pageIndex: pageIndex.value,
|
||||
pageSize: pageSize.value,
|
||||
username: filters.username.trim() || undefined,
|
||||
module: filters.module || undefined,
|
||||
status: filters.status || undefined,
|
||||
startTime: filters.range?.[0] || undefined,
|
||||
endTime: filters.range?.[1] || undefined
|
||||
})
|
||||
list.value = res.data?.list || []
|
||||
total.value = res.data?.total || 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function search() {
|
||||
pageIndex.value = 1
|
||||
load()
|
||||
}
|
||||
|
||||
function resetFilters() {
|
||||
filters.username = ''
|
||||
filters.module = ''
|
||||
filters.status = ''
|
||||
filters.range = null
|
||||
search()
|
||||
}
|
||||
|
||||
function handleSizeChange() {
|
||||
pageIndex.value = 1
|
||||
load()
|
||||
}
|
||||
|
||||
async function openDetail(row) {
|
||||
detailVisible.value = true
|
||||
detailLoading.value = true
|
||||
detail.value = null
|
||||
try {
|
||||
const res = await getOperationLogApi(row.id)
|
||||
detail.value = res.data
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 请求/响应体按 JSON 缩进显示,解析失败就原样展示 */
|
||||
function pretty(text) {
|
||||
if (!text) return ''
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(text), null, 2)
|
||||
} catch {
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageHeading
|
||||
eyebrow="后台管理"
|
||||
title="操作日志"
|
||||
description="记录后台的写操作,包含操作人、时间、请求与响应内容。"
|
||||
/>
|
||||
|
||||
<section class="panel">
|
||||
<div class="library-toolbar">
|
||||
<div class="drama-filters">
|
||||
<el-input
|
||||
v-model="filters.username"
|
||||
:prefix-icon="Search"
|
||||
clearable
|
||||
placeholder="操作人"
|
||||
@keyup.enter="search"
|
||||
@clear="search"
|
||||
/>
|
||||
<el-select v-model="filters.module" clearable placeholder="全部模块" @change="search">
|
||||
<el-option
|
||||
v-for="item in moduleOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select v-model="filters.status" clearable placeholder="全部结果" @change="search">
|
||||
<el-option label="成功" value="SUCCESS" />
|
||||
<el-option label="失败" value="FAIL" />
|
||||
</el-select>
|
||||
<el-date-picker
|
||||
v-model="filters.range"
|
||||
type="datetimerange"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
@change="search"
|
||||
/>
|
||||
<el-button @click="resetFilters">重置</el-button>
|
||||
</div>
|
||||
<span class="table-result-count">共 {{ total }} 条</span>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
v-loading="loading"
|
||||
:data="list"
|
||||
row-key="id"
|
||||
empty-text="暂无操作记录"
|
||||
class="drama-el-table"
|
||||
>
|
||||
<el-table-column prop="createdAt" label="操作时间" width="170" />
|
||||
<el-table-column label="操作人" width="140">
|
||||
<template #default="{ row }">
|
||||
{{ row.username || '(未登录)' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="module" label="模块" width="110" />
|
||||
<el-table-column prop="action" label="操作" min-width="150" />
|
||||
<el-table-column label="请求" min-width="260">
|
||||
<template #default="{ row }">
|
||||
<div class="episode-table-title">
|
||||
<b>{{ row.requestMethod }} {{ row.requestUrl }}</b>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="结果" width="90">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.status === 'SUCCESS' ? 'success' : 'danger'" effect="light">
|
||||
{{ row.statusText }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="耗时" width="90">
|
||||
<template #default="{ row }">{{ row.durationMs }} ms</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="ip" label="IP" width="130" />
|
||||
<el-table-column label="操作" width="90" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" size="small" @click="openDetail(row)">详情</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="pagination">
|
||||
<el-pagination
|
||||
v-model:current-page="pageIndex"
|
||||
v-model:page-size="pageSize"
|
||||
:page-sizes="[10, 20, 50, 100]"
|
||||
:total="total"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
background
|
||||
@current-change="load"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<el-dialog v-model="detailVisible" title="操作详情" width="760px">
|
||||
<div v-loading="detailLoading" class="log-detail">
|
||||
<el-descriptions v-if="detail" :column="2" border>
|
||||
<el-descriptions-item label="操作人">{{
|
||||
detail.username || '(未登录)'
|
||||
}}</el-descriptions-item>
|
||||
<el-descriptions-item label="操作时间">{{ detail.createdAt }}</el-descriptions-item>
|
||||
<el-descriptions-item label="模块">{{ detail.module }}</el-descriptions-item>
|
||||
<el-descriptions-item label="操作">{{ detail.action }}</el-descriptions-item>
|
||||
<el-descriptions-item label="请求方式">{{ detail.requestMethod }}</el-descriptions-item>
|
||||
<el-descriptions-item label="接口地址">{{ detail.requestUrl }}</el-descriptions-item>
|
||||
<el-descriptions-item label="结果">
|
||||
<el-tag :type="detail.status === 'SUCCESS' ? 'success' : 'danger'" effect="light">
|
||||
{{ detail.statusText }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="耗时">{{ detail.durationMs }} ms</el-descriptions-item>
|
||||
<el-descriptions-item label="IP">{{ detail.ip }}</el-descriptions-item>
|
||||
<el-descriptions-item label="查询参数">{{
|
||||
detail.requestParams || '—'
|
||||
}}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<template v-if="detail">
|
||||
<div class="log-block">
|
||||
<h4>请求内容</h4>
|
||||
<pre class="log-json">{{ pretty(detail.requestBody) || '—' }}</pre>
|
||||
</div>
|
||||
<div class="log-block">
|
||||
<h4>响应内容</h4>
|
||||
<pre class="log-json">{{ pretty(detail.responseBody) || '—' }}</pre>
|
||||
</div>
|
||||
<div v-if="detail.errorMsg" class="log-block">
|
||||
<h4>错误信息</h4>
|
||||
<pre class="log-json">{{ detail.errorMsg }}</pre>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -1,17 +1,68 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { listConfigsApi, updateConfigsApi } from '@/api/admin/config'
|
||||
import { useRbac } from '@/stores/rbac'
|
||||
|
||||
const { can } = useRbac()
|
||||
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const workspaceName = ref('')
|
||||
const defaultStatus = ref('draft')
|
||||
|
||||
const statusOptions = [
|
||||
{ code: 'draft', text: '草稿' },
|
||||
{ code: 'reviewing', text: '待审核' }
|
||||
]
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await listConfigsApi()
|
||||
const map = Object.fromEntries((res.data || []).map((item) => [item.key, item.value]))
|
||||
workspaceName.value = map['workspace.name'] || ''
|
||||
defaultStatus.value = map['content.default-status'] || 'draft'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function save() {
|
||||
saving.value = true
|
||||
try {
|
||||
await updateConfigsApi({
|
||||
'workspace.name': workspaceName.value,
|
||||
'content.default-status': defaultStatus.value
|
||||
})
|
||||
ElMessage.success('设置已保存')
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page-heading"
|
||||
><div><div class="eyebrow">系统管理</div><h1>系统设置</h1><p>工作空间基础设置。</p></div></div
|
||||
>
|
||||
<section class="panel settings-panel"
|
||||
<section v-loading="loading" class="panel settings-panel"
|
||||
><h2>工作空间</h2
|
||||
><el-form label-position="top" class="settings-form"
|
||||
><el-form-item label="空间名称"><el-input model-value="ReelShort Studio" /></el-form-item
|
||||
><el-form-item label="空间名称"
|
||||
><el-input v-model="workspaceName" placeholder="请输入工作空间名称" /></el-form-item
|
||||
><el-form-item label="内容默认状态"
|
||||
><el-select model-value="草稿"
|
||||
><el-option label="草稿" value="草稿" /><el-option
|
||||
label="待审核"
|
||||
value="待审核" /></el-select></el-form-item
|
||||
><el-button type="primary" disabled>保存设置</el-button></el-form
|
||||
><el-select v-model="defaultStatus"
|
||||
><el-option
|
||||
v-for="item in statusOptions"
|
||||
:key="item.code"
|
||||
:label="item.text"
|
||||
:value="item.code" /></el-select></el-form-item
|
||||
><el-button type="primary" :loading="saving" :disabled="!can('system:role')" @click="save"
|
||||
>保存设置</el-button
|
||||
></el-form
|
||||
></section
|
||||
>
|
||||
</template>
|
||||
Reference in new issue
Block a user