chore: 整合部署文件与管理端源码入仓
- deploy/server.Dockerfile、deploy/client-h5.Dockerfile(自 ai-art 仓库迁出,去重) - docker-compose.yml 迁入并修正路径(admin 上下文 ./admin),新增可选 h5 服务(profile 隔离) - admin/:管理前端完整源码(Vite+Vue3,自 workspace/dimension-admin 合并)
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<div>
|
||||
<h2>数据看板</h2>
|
||||
<div class="cards">
|
||||
<el-card><div class="num">{{ s.newUsers ?? '-' }}</div><div class="lbl">今日新增</div></el-card>
|
||||
<el-card><div class="num">{{ s.activeUsers ?? '-' }}</div><div class="lbl">今日活跃</div></el-card>
|
||||
<el-card><div class="num">{{ s.newOrders ?? '-' }}</div><div class="lbl">今日订单</div></el-card>
|
||||
<el-card><div class="num ok">{{ s.aiChatToday ?? '-' }}</div><div class="lbl">问答总数</div></el-card>
|
||||
<el-card><div class="num">{{ s.aiChatUsers ?? '-' }}</div><div class="lbl">问答用户</div></el-card>
|
||||
<el-card><div class="num">{{ s.aiChatShares ?? '-' }}</div><div class="lbl">分享次数</div></el-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { apiGet } from '../api'
|
||||
const s = ref({})
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const d = await apiGet('/api/admin/dashboard')
|
||||
const stats = await apiGet('/api/admin/ai-chat/stats').catch(() => ({}))
|
||||
s.value = {
|
||||
newUsers: d?.today?.newUsers ?? '-',
|
||||
activeUsers: d?.today?.activeUsers ?? '-',
|
||||
newOrders: d?.today?.newOrders ?? '-',
|
||||
aiChatToday: stats?.totalQuestions ?? '-',
|
||||
aiChatUsers: stats?.totalUsers ?? '-',
|
||||
aiChatShares: stats?.totalShares ?? '-',
|
||||
}
|
||||
} catch (e) { console.error(e) }
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cards { display:grid; grid-template-columns:repeat(auto-fit,minmax(140px,1fr)); gap:12px; }
|
||||
.num { font-size:26px; font-weight:700; color:#1f2333; }
|
||||
.num.ok { color:#18a058; }
|
||||
.lbl { font-size:12px; color:#8a8fa3; margin-top:4px; }
|
||||
</style>
|
||||
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="head"><h2>反馈管理</h2></div>
|
||||
<el-table :data="list">
|
||||
<el-table-column prop="content" label="内容" show-overflow-tooltip />
|
||||
<el-table-column prop="user" label="用户" width="120" />
|
||||
<el-table-column label="状态" width="90">
|
||||
<template #default="{row}"><el-tag :type="row.status==='resolved'?'success':'info'" size="small">{{ row.status==='resolved'?'已处理':'待处理' }}</el-tag></template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="时间" width="160"><template #default="{row}">{{ row.createdAt?.slice(0,10) }}</template></el-table-column>
|
||||
<el-table-column label="操作" width="100">
|
||||
<template #default="{row}"><el-button size="small" type="danger" plain @click="del(row)">删除</el-button></template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { apiGet } from '../api'
|
||||
const list = ref([])
|
||||
async function load() { const r = await apiGet('/api/admin/feedback'); list.value = r.feedbacks || r.data || [] }
|
||||
async function del(row) { await ElMessageBox.confirm('确认删除?'); await apiDelete(`/api/admin/feedback/${row._id}`); load() }
|
||||
async function apiDelete(url) { await fetch(url, { method: 'DELETE', headers: { Authorization: `Bearer ${localStorage.getItem('dim_admin_token')}` } }) }
|
||||
onMounted(load)
|
||||
</script>
|
||||
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="head"><h2>问答额度管理</h2>
|
||||
<el-button type="primary" @click="addQuota">新增额度</el-button>
|
||||
</div>
|
||||
<el-table :data="list">
|
||||
<el-table-column prop="user" label="用户" width="160" />
|
||||
<el-table-column prop="total" label="总额度" width="90" />
|
||||
<el-table-column prop="used" label="已用" width="80" />
|
||||
<el-table-column prop="remaining" label="剩余" width="80" />
|
||||
<el-table-column prop="date" label="日期" width="120" />
|
||||
</el-table>
|
||||
<el-dialog v-model="dialog" title="增加额度" width="400">
|
||||
<el-form label-width="80">
|
||||
<el-form-item label="用户ID"><el-input v-model="form.userId" /></el-form-item>
|
||||
<el-form-item label="增加次数"><el-input-number v-model="form.amount" :min="1" :max="100" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer><el-button @click="dialog=false">取消</el-button><el-button type="primary" @click="save">确认</el-button></template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, ref, reactive } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { apiGet, apiPost } from '../api'
|
||||
const list = ref([]); const dialog = ref(false)
|
||||
const form = reactive({ userId: '', amount: 5 })
|
||||
async function load() { const r = await apiGet('/api/admin/ai-chat/quota-list'); list.value = r.list || r.data || [] }
|
||||
function addQuota() { dialog.value = true }
|
||||
async function save() {
|
||||
try { await apiPost('/api/admin/ai-chat/add-quota', { userId: form.userId, amount: form.amount }); ElMessage.success('已增加'); dialog.value = false; load() }
|
||||
catch (e) { ElMessage.error(e.message) }
|
||||
}
|
||||
onMounted(load)
|
||||
</script>
|
||||
<style scoped>.head{display:flex;justify-content:space-between;align-items:center}</style>
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="head"><h2>趋势管理</h2><el-button type="primary" @click="dialog=true">新增趋势</el-button></div>
|
||||
<el-table :data="list">
|
||||
<el-table-column prop="title" label="标题" show-overflow-tooltip />
|
||||
<el-table-column prop="source" label="来源" width="100" />
|
||||
<el-table-column prop="category" label="分类" width="80" />
|
||||
<el-table-column prop="hot" label="热门" width="70"><template #default="{row}"><el-tag :type="row.hot?'danger':'info'" size="small">{{row.hot?'热':'普通'}}</el-tag></template></el-table-column>
|
||||
<el-table-column prop="createdAt" label="创建时间" width="160"><template #default="{row}">{{ row.createdAt?.slice(0,10) }}</template></el-table-column>
|
||||
<el-table-column label="操作" width="100"><template #default="{row}"><el-button size="small" type="danger" plain @click="del(row)">删除</el-button></template></el-table-column>
|
||||
</el-table>
|
||||
<el-dialog v-model="dialog" title="新增趋势" width="480">
|
||||
<el-form label-width="70">
|
||||
<el-form-item label="标题"><el-input v-model="form.title" /></el-form-item>
|
||||
<el-form-item label="来源"><el-input v-model="form.source" placeholder="来源名称" /></el-form-item>
|
||||
<el-form-item label="分类"><el-select v-model="form.category"><el-option v-for="c in ['产品','论文','工具','公司','政策','其他']" :key="c" :value="c" :label="c" /></el-select></el-form-item>
|
||||
<el-form-item label="摘要"><el-input v-model="form.summary" type="textarea" :rows="3" /></el-form-item>
|
||||
</el-form>
|
||||
<template #footer><el-button @click="dialog=false">取消</el-button><el-button type="primary" @click="save">保存</el-button></template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
|
||||
const list = ref([]); const dialog = ref(false)
|
||||
const form = reactive({ title: '', source: '', category: '其他', summary: '' })
|
||||
async function load() { /* TODO */ }
|
||||
async function save() {
|
||||
try { await apiPost('/api/admin/trends', { ...form }); ElMessage.success('已保存'); dialog.value = false; load() }
|
||||
catch (e) { ElMessage.error(e.message) }
|
||||
}
|
||||
async function del(row) {
|
||||
await ElMessageBox.confirm('确认删除?'); await apiDelete(`/api/admin/trends/${row._id}`); ElMessage.success('已删除'); load()
|
||||
}
|
||||
function apiDelete(url) { return fetch(url, { method: 'DELETE', headers: { Authorization: `Bearer ${localStorage.getItem('dim_admin_token')}` } }) }
|
||||
function apiPost(url, body) { return fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem('dim_admin_token')}` }, body: JSON.stringify(body) }).then(r => r.json()) }
|
||||
function apiGet(path) { return fetch(path, { headers: { Authorization: `Bearer ${localStorage.getItem('dim_admin_token')}` } }).then(r => r.json()) }
|
||||
onMounted(load)
|
||||
</script>
|
||||
<style scoped>.head{display:flex;justify-content:space-between;align-items:center}</style>
|
||||
@@ -0,0 +1,23 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="head"><h2>用户管理</h2><el-input v-model="q" placeholder="搜索昵称" style="width:200px" clearable @change="page=1;load()" /></div>
|
||||
<el-table :data="list">
|
||||
<el-table-column prop="nickname" label="昵称" width="140" />
|
||||
<el-table-column label="手机号" width="130"><template #default="{row}">{{ row.phone || '-' }}</template></el-table-column>
|
||||
<el-table-column label="注册时间" width="160"><template #default="{row}">{{ row.createdAt?.slice(0,10) }}</template></el-table-column>
|
||||
<el-table-column label="操作"><template #default="{row}"><el-tag size="small">{{ row._id?.slice(-6) }}</el-tag></template></el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-model:current-page="page" layout="prev,pager,next,total" :total="total" :page-size="20" style="margin-top:14px" @current-change="load" />
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { apiGet } from '../api'
|
||||
const list = ref([]); const total = ref(0); const page = ref(1); const q = ref('')
|
||||
async function load() {
|
||||
const r = await apiGet(`/api/admin/users?q=${q.value}&page=${page.value}`)
|
||||
list.value = r.users || []; total.value = r.total || 0
|
||||
}
|
||||
onMounted(load)
|
||||
</script>
|
||||
<style scoped>.head{display:flex;justify-content:space-between;align-items:center}</style>
|
||||
Reference in New Issue
Block a user