refactor(developer-config): 移除开发者配置页面相关代码和文档

- 删除应用配置页面及相关组件,重构路由为 /developer/config/[id].vue
- 移除开发者文档页面及其导航与样式实现
- 清理开发者侧功能完善工作日志文件
- 删除全局.gitignore配置文件,清理无用忽略规则
- 优化应用配置页面的参数读取和路由结构,解决刷新404问题
- 解决数据库配置唯一键冲突,调整保存逻辑避免重复插入
- 移除对后端配置加密字段的 secret 标记,修正加密异常问题
This commit is contained in:
2026-04-09 07:35:34 +08:00
parent 3209d92cc5
commit f9e1286ab1
130 changed files with 18656 additions and 22143 deletions

View File

@@ -1,89 +1,157 @@
<template>
<div class="admin-home">
<!-- 欢迎横幅 -->
<div class="welcome-banner">
<div class="welcome-left">
<h2 class="welcome-title">🎛 平台管理中心</h2>
<p class="welcome-sub">欢迎回来{{ adminName }}今日数据已更新</p>
</div>
<div class="welcome-right">
<a-space>
<a-tag color="red" style="font-size:13px;padding:4px 12px">超级管理员</a-tag>
<a-button size="small" @click="loadStats" :loading="loadingStats">
<template #icon><ReloadOutlined /></template>
刷新数据
</a-button>
</a-space>
</div>
</div>
<script setup lang="ts">
definePageMeta({ layout: 'admin' })
<!-- 核心数据统计 -->
<a-row :gutter="[16, 16]">
<a-col :xs="12" :sm="12" :md="6" v-for="stat in coreStats" :key="stat.label">
<div class="stat-block" :class="stat.color" @click="navigateTo(stat.to)" :style="{ cursor: stat.to ? 'pointer' : 'default' }">
<div class="stat-block-header">
<span class="stat-block-icon">{{ stat.icon }}</span>
<span class="stat-block-label">{{ stat.label }}</span>
const { activeTab } = useNav()
const stats = ref([
{ label: '生产工单', value: 128, unit: '单', change: '+12%', up: true, icon: '📋', color: '#6366f1' },
{ label: '在制品数量', value: 3, unit: '万', change: '+8%', up: true, icon: '🏭', color: '#10b981' },
{ label: '设备利用率', value: 87, unit: '%', change: '+3%', up: true, icon: '⚙️', color: '#f59e0b' },
{ label: '订单交付率', value: 96, unit: '%', change: '+1%', up: true, icon: '🚚', color: '#3b82f6' },
])
const quickActions = ref([
{ label: '计划排程', icon: '📅', color: 'from-indigo-500 to-purple-500', path: '/admin/production/schedule' },
{ label: '生产管控', icon: '🎛️', color: 'from-emerald-500 to-teal-500', path: '/admin/production/control' },
{ label: '质量检测', icon: '🔍', color: 'from-amber-500 to-orange-500', path: '/admin/production/quality' },
{ label: '设备监控', icon: '📊', color: 'from-blue-500 to-cyan-500', path: '/admin/production/equipment' },
{ label: '采购申请', icon: '🛒', color: 'from-pink-500 to-rose-500', path: '/admin/supply/purchase' },
{ label: '库存查询', icon: '📦', color: 'from-violet-500 to-purple-500', path: '/admin/supply/warehouse' },
])
const recentOrders = ref([
{ id: 'WO2026040901', product: '精密轴承组件 A型', quantity: 500, status: '生产中', progress: 65, startDate: '2026-04-09' },
{ id: 'WO2026040802', product: '液压缸体 B型', quantity: 200, status: '待排产', progress: 0, startDate: '2026-04-08' },
{ id: 'WO2026040801', product: '传动齿轮组 C型', quantity: 1000, status: '已完成', progress: 100, startDate: '2026-04-07' },
{ id: 'WO2026040703', product: '密封圈组件 D型', quantity: 3000, status: '已完成', progress: 100, startDate: '2026-04-06' },
{ id: 'WO2026040702', product: '弹簧组件 E型', quantity: 800, status: '已取消', progress: 30, startDate: '2026-04-05' },
])
const qualityAlerts = ref([
{ level: 'warning', title: '质检异常:批次 B-20260408-03', desc: '尺寸超出公差范围,已自动隔离', time: '10分钟前' },
{ level: 'info', title: '质检报告生成', desc: '批次 A-20260409-01 质检完成', time: '30分钟前' },
{ level: 'warning', title: '设备报警CNC-03', desc: '主轴温度异常,请及时处理', time: '1小时前' },
])
const equipmentStatus = ref([
{ name: 'CNC-01', status: '运行中', utilization: 92 },
{ name: 'CNC-02', status: '运行中', utilization: 88 },
{ name: 'CNC-03', status: '告警', utilization: 0 },
{ name: '铣床-01', status: '待机', utilization: 0 },
{ name: '铣床-02', status: '运行中', utilization: 75 },
])
const statusMap: Record<string, string> = {
'生产中': 'processing',
'待排产': 'warning',
'已完成': 'success',
'已取消': 'default',
}
const equipStatusMap: Record<string, string> = {
'运行中': 'success',
'告警': 'error',
'待机': 'default',
}
</script>
<template>
<div class="dashboard">
<!-- 统计卡片 -->
<a-row :gutter="[20, 20]" class="mb-6">
<a-col :xs="24" :sm="12" :xl="6" v-for="stat in stats" :key="stat.label">
<div class="stat-card" :style="{ '--accent': stat.color }">
<div class="stat-header">
<span class="stat-icon">{{ stat.icon }}</span>
<span :class="['stat-change', stat.up ? 'up' : 'down']">{{ stat.change }}</span>
</div>
<div class="stat-block-value">
<template v-if="loadingStats">
<a-skeleton-input :active="true" size="small" style="width:60px" />
</template>
<template v-else>{{ stat.value }}</template>
</div>
<div class="stat-block-desc">{{ stat.desc }}</div>
<div class="stat-value">{{ stat.value }}<span class="stat-unit">{{ stat.unit }}</span></div>
<div class="stat-label">{{ stat.label }}</div>
</div>
</a-col>
</a-row>
<!-- 待办事项 + 快速入口 -->
<a-row :gutter="[16, 16]">
<!-- 待处理事项 -->
<a-col :xs="24" :md="12">
<div class="panel">
<div class="panel-header">
<span class="panel-title">🔔 待处理事项</span>
</div>
<div class="todo-list">
<div
v-for="todo in todoItems"
:key="todo.label"
class="todo-item"
:class="{ 'todo-item-urgent': todo.urgent }"
@click="navigateTo(todo.to)"
<a-row :gutter="[20, 20]">
<!-- 左侧主体 -->
<a-col :xs="24" :xl="16">
<!-- 快捷入口 -->
<div class="card mb-6">
<div class="card-title">快捷入口</div>
<div class="quick-grid">
<NuxtLink
v-for="action in quickActions"
:key="action.label"
:to="action.path"
class="quick-item"
>
<div class="todo-dot" :class="todo.dotColor"></div>
<div class="todo-content">
<span class="todo-label">{{ todo.label }}</span>
<a-tag :color="todo.tagColor" style="margin-left:8px">
<template v-if="loadingStats">...</template>
<template v-else>{{ todo.value }}</template>
</a-tag>
</div>
<RightOutlined class="todo-arrow" />
</div>
<div v-if="!loadingStats && todoItems.every(t => t.value === 0)" class="todo-empty">
🎉 暂无待处理事项一切正常
</div>
<div class="quick-icon" :class="action.color">{{ action.icon }}</div>
<span class="quick-label">{{ action.label }}</span>
</NuxtLink>
</div>
</div>
<!-- 工单列表 -->
<div class="card">
<div class="card-title">近期工单</div>
<a-table
:dataSource="recentOrders"
:pagination="false"
size="small"
rowKey="id"
:scroll="{ x: 600 }"
>
<a-table-column title="工单编号" dataIndex="id" width="140" />
<a-table-column title="产品名称" dataIndex="product" />
<a-table-column title="数量" dataIndex="quantity" width="80" align="center" />
<a-table-column title="状态" dataIndex="status" width="100" align="center">
<template #default="{ text }">
<a-tag :color="statusMap[text] === 'success' ? 'success' : statusMap[text] === 'processing' ? 'processing' : statusMap[text] === 'warning' ? 'warning' : 'default'">
{{ text }}
</a-tag>
</template>
</a-table-column>
<a-table-column title="进度" dataIndex="progress" width="140" align="center">
<template #default="{ record }">
<a-progress
:percent="record.progress"
:status="record.progress === 100 ? 'success' : 'active'"
:showInfo="false"
size="small"
/>
<span class="text-xs text-gray-400 ml-2">{{ record.progress }}%</span>
</template>
</a-table-column>
<a-table-column title="开始日期" dataIndex="startDate" width="120" />
</a-table>
</div>
</a-col>
<!-- 快速导航 -->
<a-col :xs="24" :md="12">
<div class="panel">
<div class="panel-header">
<span class="panel-title"> 快速入口</span>
<!-- 右侧 -->
<a-col :xs="24" :xl="8">
<!-- 质量告警 -->
<div class="card mb-6">
<div class="card-title">质量告警</div>
<div class="alert-list">
<div v-for="(alert, idx) in qualityAlerts" :key="idx" class="alert-item" :class="alert.level">
<div class="alert-header">
<span class="alert-dot" :class="alert.level"></span>
<span class="alert-title">{{ alert.title }}</span>
</div>
<div class="alert-desc">{{ alert.desc }}</div>
<div class="alert-time">{{ alert.time }}</div>
</div>
</div>
<div class="quick-grid">
<div
v-for="item in quickLinks"
:key="item.to"
class="quick-card"
@click="navigateTo(item.to)"
>
<div class="quick-icon" :style="{ background: item.bg }">{{ item.icon }}</div>
<div class="quick-label">{{ item.label }}</div>
</div>
<!-- 设备状态 -->
<div class="card">
<div class="card-title">设备状态</div>
<div class="equip-list">
<div v-for="equip in equipmentStatus" :key="equip.name" class="equip-item">
<span class="equip-name">{{ equip.name }}</span>
<a-tag :color="equipStatusMap[equip.status] === 'success' ? 'success' : equipStatusMap[equip.status] === 'error' ? 'error' : 'default'" size="small">
{{ equip.status }}
</a-tag>
<span class="equip-util">{{ equip.utilization }}%</span>
</div>
</div>
</div>
@@ -92,180 +160,220 @@
</div>
</template>
<script setup lang="ts">
import { ReloadOutlined, RightOutlined } from '@ant-design/icons-vue'
import { getUserInfo } from '@/api/layout'
import { getToken } from '@/utils/token-util'
import { pageAppProductAll } from '@/api/app/appProduct'
import { pageUsers } from '@/api/system/user/index'
import { listAppArticle as listCmsArticle } from '@/api/app/article'
import { pageGitAccounts } from '@/api/developer'
definePageMeta({ layout: 'admin' })
useHead({ title: '平台管理首页' })
const adminName = ref('管理员')
const loadingStats = ref(false)
const coreStats = reactive([
{ icon: '📦', label: '应用总数', value: 0, desc: '全平台应用', color: 'blue', to: '/admin/apps' },
{ icon: '👥', label: '用户总数', value: 0, desc: '注册用户', color: 'green', to: '/admin/users' },
{ icon: '⏳', label: '待审核应用', value: 0, desc: '等待审核中', color: 'orange', to: '/admin/app-review' },
{ icon: '🛒', label: '上架应用', value: 0, desc: '市场在售', color: 'purple', to: '/admin/market' },
])
const todoItems = reactive([
{ label: '待审核应用', value: 0, to: '/admin/app-review', tagColor: 'orange', dotColor: 'dot-orange', urgent: false },
{ label: '待审核Git账号', value: 0, to: '/admin/git-review', tagColor: 'cyan', dotColor: 'dot-cyan', urgent: false },
{ label: '草稿文章', value: 0, to: '/admin/articles', tagColor: 'blue', dotColor: 'dot-blue', urgent: false },
{ label: '冻结用户', value: 0, to: '/admin/users', tagColor: 'red', dotColor: 'dot-red', urgent: false },
])
const ANNOUNCE_MODEL = 'announcement'
const quickLinks = [
{ to: '/admin/app-review', icon: '🔍', label: '应用审核', bg: '#fff7ed' },
{ to: '/admin/git-review', icon: '🔧', label: 'Git 审核', bg: '#ecfdf5' },
{ to: '/admin/apps', icon: '📦', label: '应用管理', bg: '#eff6ff' },
{ to: '/admin/market', icon: '🛒', label: '应用市场', bg: '#faf5ff' },
{ to: '/admin/users', icon: '👥', label: '用户管理', bg: '#f0fdf4' },
{ to: '/admin/developers', icon: '🧑‍💻', label: '开发者', bg: '#f0f9ff' },
{ to: '/admin/tickets', icon: '🎫', label: '工单处理', bg: '#fdf4ff' },
{ to: '/admin/articles', icon: '📝', label: '文章管理', bg: '#fefce8' },
{ to: '/admin/article-categories', icon: '🗂️', label: '文章分类', bg: '#ecfeff' },
{ to: '/admin/announcements', icon: '📢', label: '公告管理', bg: '#fff1f2' },
{ to: '/admin/settings', icon: '⚙️', label: '平台设置', bg: '#f9fafb' },
]
async function loadStats() {
loadingStats.value = true
try {
const [appsRes, usersRes, pendingRes, marketRes, draftRes, frozenRes, gitPendingRes] = await Promise.allSettled([
pageAppProductAll({ current: 1, size: 1 }),
pageUsers({ page: 1, limit: 1 }),
pageAppProductAll({ current: 1, size: 1, publishStatus: 'pending_review' }),
pageAppProductAll({ current: 1, size: 1, publishStatus: 'published' }),
listCmsArticle({ status: 1 }),
pageUsers({ page: 1, limit: 1, status: 1 }),
pageGitAccounts({ page: 1, size: 1, status: 'pending' }),
])
coreStats[0].value = appsRes.status === 'fulfilled' ? appsRes.value?.count || 0 : 0
coreStats[1].value = usersRes.status === 'fulfilled' ? usersRes.value?.count || 0 : 0
coreStats[2].value = pendingRes.status === 'fulfilled' ? pendingRes.value?.count || 0 : 0
coreStats[3].value = marketRes.status === 'fulfilled' ? marketRes.value?.count || 0 : 0
const pendingCount = pendingRes.status === 'fulfilled' ? pendingRes.value?.count || 0 : 0
const draftCount = draftRes.status === 'fulfilled'
? (draftRes.value || []).filter(item => (item.model || '').trim() !== ANNOUNCE_MODEL).length
: 0
const frozenCount = frozenRes.status === 'fulfilled' ? frozenRes.value?.count || 0 : 0
const gitPendingCount = gitPendingRes.status === 'fulfilled' ? (gitPendingRes.value as any)?.data?.data?.total || 0 : 0
todoItems[0].value = pendingCount
todoItems[0].urgent = pendingCount > 0
todoItems[1].value = gitPendingCount
todoItems[1].urgent = gitPendingCount > 0
todoItems[2].value = draftCount
todoItems[3].value = frozenCount
} catch { /* ignore */ } finally {
loadingStats.value = false
}
}
onMounted(async () => {
const token = getToken()
if (!token) return
// 并发加载用户信息和统计数据
Promise.allSettled([
getUserInfo().then(me => {
adminName.value = me?.nickname?.trim() || me?.username?.trim() || '管理员'
}),
loadStats(),
])
})
</script>
<style scoped>
.admin-home {
display: flex;
flex-direction: column;
gap: 20px;
.dashboard {
padding: 24px;
}
/* 欢迎横幅 */
.welcome-banner {
display: flex;
align-items: center;
justify-content: space-between;
background: linear-gradient(135deg, #1a0f0f 0%, #3d1515 100%);
border-radius: 14px;
padding: 24px 28px;
color: #fff;
flex-wrap: wrap;
gap: 12px;
}
.welcome-title { font-size: 20px; font-weight: 700; color: #fff; margin: 0 0 6px; }
.welcome-sub { font-size: 14px; color: rgba(255,255,255,0.7); margin: 0; }
/* 核心统计块 */
.stat-block {
padding: 18px 20px;
.stat-card {
background: white;
border-radius: 12px;
border: 2px solid transparent;
transition: all 0.2s;
padding: 20px;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
border: 1px solid #f0f0f0;
position: relative;
overflow: hidden;
}
.stat-block:hover { transform: translateY(-2px); box-shadow: 0 6px 16px rgba(0,0,0,0.08); }
.stat-block.blue { background: #eff6ff; border-color: #dbeafe; }
.stat-block.green { background: #f0fdf4; border-color: #bbf7d0; }
.stat-block.orange { background: #fff7ed; border-color: #fed7aa; }
.stat-block.purple { background: #faf5ff; border-color: #e9d5ff; }
.stat-block-header { display: flex; align-items: center; gap: 6px; margin-bottom: 10px; }
.stat-block-icon { font-size: 18px; }
.stat-block-label { font-size: 13px; color: rgba(0,0,0,0.55); }
.stat-block-value { font-size: 32px; font-weight: 800; color: rgba(0,0,0,0.85); line-height: 1.1; margin-bottom: 4px; }
.stat-block-desc { font-size: 12px; color: rgba(0,0,0,0.4); }
/* Panel */
.panel { background: #fff; border: 1px solid #f0f0f0; border-radius: 12px; overflow: hidden; }
.panel-header { padding: 14px 18px; border-bottom: 1px solid #f5f5f5; }
.panel-title { font-size: 14px; font-weight: 600; color: rgba(0,0,0,0.85); }
/* 待办 */
.todo-list { padding: 8px 0; }
.todo-item {
display: flex; align-items: center; gap: 12px;
padding: 12px 18px; cursor: pointer; transition: background 0.15s;
.stat-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 4px;
height: 100%;
background: var(--accent);
}
.stat-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.stat-icon {
font-size: 24px;
}
.stat-change {
font-size: 12px;
padding: 2px 6px;
border-radius: 4px;
}
.stat-change.up {
color: #10b981;
background: #ecfdf5;
}
.stat-change.down {
color: #ef4444;
background: #fef2f2;
}
.stat-value {
font-size: 32px;
font-weight: 700;
color: #1f2937;
margin-bottom: 4px;
}
.stat-unit {
font-size: 14px;
font-weight: 400;
color: #6b7280;
margin-left: 4px;
}
.stat-label {
font-size: 13px;
color: #9ca3af;
}
.card {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 1px 3px rgba(0,0,0,0.06);
border: 1px solid #f0f0f0;
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #1f2937;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #f0f0f0;
}
.todo-item:hover { background: #f9fafb; }
.todo-item-urgent .todo-label { font-weight: 600; }
.todo-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.dot-orange { background: #f97316; }
.dot-blue { background: #3b82f6; }
.dot-cyan { background: #06b6d4; }
.dot-red { background: #ef4444; }
.todo-content { flex: 1; display: flex; align-items: center; }
.todo-label { font-size: 14px; color: rgba(0,0,0,0.75); }
.todo-arrow { font-size: 11px; color: rgba(0,0,0,0.3); }
.todo-empty { text-align: center; padding: 20px 0; color: rgba(0,0,0,0.4); font-size: 14px; }
/* 快速入口九宫格 */
.quick-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1px;
background: #f5f5f5;
gap: 12px;
}
.quick-card {
display: flex; flex-direction: column; align-items: center;
gap: 8px; padding: 18px 12px; background: #fff;
cursor: pointer; transition: background 0.15s;
.quick-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 16px 8px;
border-radius: 10px;
background: #fafafa;
text-decoration: none;
transition: all 0.2s;
}
.quick-card:hover { background: #f9fafb; }
.quick-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
}
.quick-icon {
width: 44px; height: 44px; border-radius: 10px;
display: flex; align-items: center; justify-content: center; font-size: 22px;
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
margin-bottom: 8px;
background: linear-gradient(135deg, rgba(99,102,241,0.1), rgba(168,85,247,0.1));
}
.quick-label {
font-size: 13px;
color: #374151;
font-weight: 500;
}
.alert-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.alert-item {
padding: 12px;
border-radius: 8px;
background: #fafafa;
}
.alert-item.warning {
background: #fffbeb;
border-left: 3px solid #f59e0b;
}
.alert-item.info {
background: #eff6ff;
border-left: 3px solid #3b82f6;
}
.alert-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.alert-dot {
width: 6px;
height: 6px;
border-radius: 50%;
}
.alert-dot.warning {
background: #f59e0b;
}
.alert-dot.info {
background: #3b82f6;
}
.alert-title {
font-size: 13px;
font-weight: 600;
color: #1f2937;
}
.alert-desc {
font-size: 12px;
color: #6b7280;
margin-bottom: 4px;
}
.alert-time {
font-size: 11px;
color: #9ca3af;
}
.equip-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.equip-item {
display: flex;
align-items: center;
padding: 8px 12px;
background: #fafafa;
border-radius: 8px;
gap: 8px;
}
.equip-name {
font-size: 13px;
font-weight: 500;
color: #374151;
flex: 1;
}
.equip-util {
font-size: 12px;
color: #6b7280;
width: 40px;
text-align: right;
}
.quick-label { font-size: 13px; color: rgba(0,0,0,0.75); font-weight: 500; }
</style>