This commit is contained in:
2025-09-29 21:38:32 +08:00
parent ed26244cdb
commit 19117de6c8
182 changed files with 11590 additions and 2156 deletions

View File

@@ -2,6 +2,7 @@ import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import Shell from '../views/Shell.vue'
const routes: RouteRecordRaw[] = [
{ path: '/admin/login', component: () => import('../views/admin/Login.vue') },
{
path: '/',
component: Shell,
@@ -15,8 +16,9 @@ const routes: RouteRecordRaw[] = [
{ path: 'parts/templates', component: () => import('../views/parts/Templates.vue') },
{ path: 'consult', component: () => import('../views/consult/ConsultList.vue') }
,{ path: 'notice/list', component: () => import('../views/notice/NoticeList.vue') }
,{ path: 'dict/units', component: () => import('../views/dict/Units.vue') }
,{ path: 'dict/categories', component: () => import('../views/dict/Categories.vue') }
,{ path: 'normal-admin/applications', component: () => import('../views/normal-admin/Applications.vue') }
]
}
]

View File

@@ -14,8 +14,8 @@
<el-menu-item index="/parts/templates"><i class="el-icon-collection"></i><span>模板管理</span></el-menu-item>
<el-menu-item index="/consult"><i class="el-icon-message"></i><span>咨询回复</span></el-menu-item>
<el-menu-item index="/notice/list"><i class="el-icon-notebook-1"></i><span>公告管理</span></el-menu-item>
<el-menu-item index="/dict/units"><i class="el-icon-collection"></i><span>主单位</span></el-menu-item>
<el-menu-item index="/dict/categories"><i class="el-icon-collection-tag"></i><span>主类别</span></el-menu-item>
<el-menu-item index="/normal-admin/applications"><i class="el-icon-user"></i><span>普通管理员审批</span></el-menu-item>
</el-menu>
</aside>
<main class="main">

View File

@@ -0,0 +1,59 @@
<template>
<div class="login-wrap">
<el-card class="login-card">
<div class="title">平台管理员登录</div>
<el-form :model="form" @keyup.enter.native="onLogin">
<el-form-item label="用户名/手机号">
<el-input v-model="form.account" placeholder="用户名或手机号" />
</el-form-item>
<el-form-item label="密码">
<el-input v-model="form.password" type="password" placeholder="密码" />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="loading" @click="onLogin">登录</el-button>
</el-form-item>
</el-form>
<div class="tips">说明登录成功后将把 ADMIN_ID/TOKEN 写入本地用于授权</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { http } from '../../api/http'
import { useRouter } from 'vue-router'
const router = useRouter()
const form = reactive({ account: '', password: '' })
const loading = ref(false)
async function onLogin(){
if (!form.account.trim() || !form.password) return ElMessage.warning('请输入账号与密码')
loading.value = true
try {
const body:any = form.account.includes('@') || /^\d{5,}$/.test(form.account)
? { phone: form.account, password: form.password }
: { username: form.account, password: form.password }
const resp = await http.post('/api/admin/auth/login', body)
const admin = resp?.admin || {}
try {
if (admin?.adminId) localStorage.setItem('ADMIN_ID', String(admin.adminId))
if (resp?.token) localStorage.setItem('ADMIN_TOKEN', String(resp.token))
} catch {}
ElMessage.success('登录成功')
router.replace('/vip/system')
} catch (e:any) {
ElMessage.error(e.message || '登录失败')
} finally { loading.value=false }
}
</script>
<style scoped>
.login-wrap { height: 100vh; display:flex; align-items:center; justify-content:center; padding: 16px; }
.login-card { width: 380px; }
.title { font-weight: 800; font-size: 18px; margin-bottom: 12px; }
.tips { color:#888; font-size:12px; margin-top:8px; }
</style>

View File

@@ -0,0 +1,96 @@
<template>
<div class="page">
<div class="header">
<h2>普通管理员申请</h2>
</div>
<div class="panel" style="padding:12px; margin-bottom:12px;">
<el-form :inline="true" :model="query">
<el-form-item label="关键词">
<el-input v-model="query.kw" placeholder="姓名/手机号/邮箱" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="load">查询</el-button>
<el-button @click="reset">重置</el-button>
</el-form-item>
</el-form>
</div>
<el-table :data="rows" style="width:100%" :loading="loading" stripe>
<el-table-column type="index" width="60" label="#" />
<el-table-column prop="name" label="姓名" width="140" />
<el-table-column prop="phone" label="手机号" width="140" />
<el-table-column prop="email" label="邮箱" width="200" />
<el-table-column prop="shopId" label="店铺ID" width="100" />
<el-table-column prop="remark" label="备注" />
<el-table-column prop="createdAt" label="申请时间" width="180">
<template #default="{row}">{{ formatDate(row.createdAt) }}</template>
</el-table-column>
<el-table-column label="操作" width="220" fixed="right">
<template #default="{row}">
<el-button type="primary" size="small" @click="approve(row.userId)">通过</el-button>
<el-button type="danger" size="small" @click="openReject(row.userId)">驳回</el-button>
</template>
</el-table-column>
</el-table>
<div class="pager">
<el-pagination
background
layout="prev, pager, next, jumper, ->, total"
:total="total"
:page-size="query.size"
:current-page="query.page"
@current-change="onPage"
/>
</div>
<el-dialog v-model="reject.visible" title="驳回原因" width="420px">
<el-input type="textarea" v-model="reject.reason" placeholder="请输入驳回原因" :rows="4" />
<template #footer>
<el-button @click="reject.visible=false">取消</el-button>
<el-button type="danger" @click="doReject" :loading="reject.loading">确认驳回</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { get, post } from '../../api/http'
const query = reactive({ kw: '', page: 1, size: 20 })
const rows = ref<any[]>([])
const total = ref(0)
const loading = ref(false)
const reject = reactive({ visible: false, userId: 0, reason: '', loading: false })
function formatDate(v?: string) { if (!v) return '-'; try { const d = new Date(v); if (Number.isNaN(d.getTime())) return '-'; return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')} ${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}` } catch { return '-' } }
async function load(){
loading.value = true
try {
const res:any = await get('/api/admin/normal-admin/applications', { kw: query.kw, page: query.page, size: query.size })
rows.value = Array.isArray(res?.list) ? res.list : []
total.value = Number(res?.total || rows.value.length)
} catch (e:any) { ElMessage.error(e.message || '加载失败') } finally { loading.value=false }
}
function reset(){ query.kw=''; query.page=1; load() }
function onPage(p:number){ query.page=p; load() }
async function approve(userId:number){
try { await post(`/api/admin/normal-admin/applications/${userId}/approve`, {}) ; ElMessage.success('已通过'); load() } catch(e:any){ ElMessage.error(e.message||'操作失败') }
}
function openReject(userId:number){ reject.visible=true; reject.userId=userId; reject.reason='' }
async function doReject(){ if (!reject.reason.trim()) return ElMessage.warning('请输入驳回原因'); reject.loading=true; try { await post(`/api/admin/normal-admin/applications/${reject.userId}/reject`, { remark: reject.reason }); reject.visible=false; ElMessage.success('已驳回'); load() } catch(e:any){ ElMessage.error(e.message||'操作失败') } finally { reject.loading=false } }
onMounted(load)
</script>
<style scoped>
.page { padding: 16px; }
.header { display:flex; align-items:center; justify-content: space-between; margin-bottom: 12px; }
.pager { padding: 12px; display:flex; justify-content:flex-end; }
</style>

View File

@@ -23,31 +23,37 @@
<el-tag :type="row.status===1?'success':'info'">{{ row.status===1?'启用':'停用' }}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200">
<el-table-column label="操作" width="220">
<template #default="{row}">
<el-button type="primary" text @click="openEdit(row)">编辑</el-button>
<el-button type="primary" text @click="openView(row)">查看</el-button>
<el-divider direction="vertical" />
<el-popconfirm title="确认删除该模板?此操作不可恢复" @confirm="doDelete(row)">
<template #reference>
<el-button type="danger" text>删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<el-dialog v-model="dlg.visible" :title="dlg.id? '编辑模板':'新建模板'" width="720">
<el-dialog v-model="dlg.visible" :title="dlg.id? '查看模板' : '新建模板'" width="720">
<el-form :model="form" label-width="100px">
<el-form-item label="分类">
<el-select v-model="form.categoryId" placeholder="选择分类">
<el-select v-model="form.categoryId" placeholder="选择分类" :disabled="!!dlg.id">
<el-option v-for="c in categories" :key="c.id" :label="c.name" :value="c.id" />
</el-select>
</el-form-item>
<el-form-item label="模板名">
<el-input v-model="form.name" maxlength="120" />
<el-input v-model="form.name" maxlength="120" :disabled="!!dlg.id" />
</el-form-item>
<el-form-item label="型号规则">
<el-input v-model="form.modelRule" placeholder="可填备注或正则" />
<el-input v-model="form.modelRule" placeholder="可填备注或正则" :disabled="!!dlg.id" />
</el-form-item>
<el-form-item label="状态">
<el-switch v-model="form.status" :active-value="1" :inactive-value="0" />
<el-switch v-model="form.status" :active-value="1" :inactive-value="0" :disabled="!!dlg.id" />
</el-form-item>
<el-divider>参数字段</el-divider>
<div>
<div v-if="!dlg.id">
<el-button size="small" @click="addParam()">新增字段</el-button>
</div>
<el-table :data="form.params" size="small" style="width:100%;margin-top:10px">
@@ -55,14 +61,14 @@
<template #default="{ $index }">{{ $index+1 }}</template>
</el-table-column>
<el-table-column label="键">
<template #default="{row}"><el-input v-model="row.fieldKey" placeholder="英文字母/下划线" /></template>
<template #default="{row}"><el-input v-model="row.fieldKey" placeholder="英文字母/下划线" :disabled="!!dlg.id" /></template>
</el-table-column>
<el-table-column label="名称">
<template #default="{row}"><el-input v-model="row.fieldLabel" /></template>
<template #default="{row}"><el-input v-model="row.fieldLabel" :disabled="!!dlg.id" /></template>
</el-table-column>
<el-table-column label="类型" width="120">
<template #default="{row}">
<el-select v-model="row.type" style="width:110px">
<el-select v-model="row.type" style="width:110px" :disabled="!!dlg.id">
<el-option label="string" value="string" />
<el-option label="number" value="number" />
<el-option label="boolean" value="boolean" />
@@ -72,28 +78,31 @@
</template>
</el-table-column>
<el-table-column label="必填" width="80">
<template #default="{row}"><el-switch v-model="row.required" /></template>
<template #default="{row}"><el-switch v-model="row.required" :disabled="!!dlg.id" /></template>
</el-table-column>
<el-table-column label="单位" width="120">
<template #default="{row}"><el-input v-model="row.unit" /></template>
<template #default="{row}"><el-input v-model="row.unit" :disabled="!!dlg.id" /></template>
</el-table-column>
<el-table-column label="枚举项">
<template #default="{row}"><el-input v-model="row.enumOptionsText" placeholder="逗号分隔type=enum" /></template>
<template #default="{row}"><el-input v-model="row.enumOptionsText" placeholder="逗号分隔type=enum" :disabled="!!dlg.id" /></template>
</el-table-column>
<el-table-column label="检索" width="80">
<template #default="{row}"><el-switch v-model="row.searchable" /></template>
<template #default="{row}"><el-switch v-model="row.searchable" :disabled="!!dlg.id" /></template>
</el-table-column>
<el-table-column label="去重" width="80">
<template #default="{row}"><el-switch v-model="row.dedupeParticipate" /></template>
<template #default="{row}"><el-switch v-model="row.dedupeParticipate" :disabled="!!dlg.id" /></template>
</el-table-column>
<el-table-column label="操作" width="90">
<template #default="{ $index }"><el-button text type="danger" @click="removeParam($index)">删除</el-button></template>
<template #default="{ $index }">
<el-button v-if="!dlg.id" text type="danger" @click="removeParam($index)">删除</el-button>
<span v-else></span>
</template>
</el-table-column>
</el-table>
</el-form>
<template #footer>
<el-button @click="dlg.visible=false">取消</el-button>
<el-button type="primary" @click="save()">保存</el-button>
<el-button @click="dlg.visible=false">{{ dlg.id? '关闭':'取消' }}</el-button>
<el-button v-if="!dlg.id" type="primary" @click="save()">保存</el-button>
</template>
</el-dialog>
</div>
@@ -122,11 +131,7 @@ function loadCategories() {
})
}
function openCreate() {
dlg.visible = true; dlg.id = 0
Object.assign(form, { id:0, categoryId: undefined, name:'', modelRule:'', status:1, params:[] })
}
function openEdit(row:any) {
function openView(row:any) {
dlg.visible = true; dlg.id = row.id
http.get(`/api/admin/part-templates/${row.id}`).then(res => {
const d = res.data
@@ -135,14 +140,19 @@ function openEdit(row:any) {
})
}
function openCreate() {
dlg.visible = true; dlg.id = 0
Object.assign(form, { id: 0, categoryId: undefined, name: '', modelRule: '', status: 1, params: [] })
}
function addParam() {
form.params.push({ fieldKey:'', fieldLabel:'', type:'string', required:false, unit:'', enumOptionsText:'', searchable:false, dedupeParticipate:false, sortOrder:0 })
}
function removeParam(i:number) {
form.params.splice(i,1)
}
function removeParam(i:number) { form.params.splice(i,1) }
function save() {
// 前端校验fieldKey/fieldLabel/类型/重复
// 校验
const seen = new Set<string>()
for (const p of form.params) {
const key = String(p.fieldKey||'').trim()
@@ -157,9 +167,19 @@ function save() {
const payload:any = { categoryId: form.categoryId, name: form.name, modelRule: form.modelRule, status: form.status,
params: form.params.map((p:any)=>({ fieldKey:p.fieldKey, fieldLabel:p.fieldLabel, type:p.type, required:p.required, unit:p.unit,
enumOptions: (p.enumOptionsText||'').split(',').map((s:string)=>s.trim()).filter((s:string)=>s), searchable:p.searchable, dedupeParticipate:p.dedupeParticipate, sortOrder:p.sortOrder })) }
const req = dlg.id ? http.put(`/api/admin/part-templates/${dlg.id}`, { ...payload, deleteAllRelatedProductsAndSubmissions: true })
: http.post('/api/admin/part-templates', payload)
req.then(()=>{ ElMessage.success('保存成功'); dlg.visible=false; load() })
http.post('/api/admin/part-templates', payload).then(()=>{
ElMessage.success('创建成功'); dlg.visible=false; load()
})
}
function doDelete(row:any) {
// 默认非强制删除;如需强制,可加 { params: { force: true } }
http.delete(`/api/admin/part-templates/${row.id}`).then(()=>{
ElMessage.success('已隐藏(可通过后台强制删除彻底清理)')
load()
}).catch((err:any)=>{
ElMessage.error(err?.message || '删除失败')
})
}
onMounted(()=>{ loadCategories(); load() })