release: freeze V1.2 demo acceptance 2026-10-25
CI/CD Pipeline / Test Shell Guard (Phase 23 Gate) (push) Waiting to run
CI/CD Pipeline / Backend Build & Test (push) Waiting to run
CI/CD Pipeline / Admin Web Build (push) Waiting to run
CI/CD Pipeline / Mini Program Compile Check (push) Waiting to run
CI/CD Pipeline / Migration Validation (push) Blocked by required conditions
CI/CD Pipeline / UI Contracts (source-structure gates) (push) Waiting to run
CI/CD Pipeline / E2E (live stack) (push) Blocked by required conditions
CI/CD Pipeline / Test Shell Guard (Phase 23 Gate) (push) Waiting to run
CI/CD Pipeline / Backend Build & Test (push) Waiting to run
CI/CD Pipeline / Admin Web Build (push) Waiting to run
CI/CD Pipeline / Mini Program Compile Check (push) Waiting to run
CI/CD Pipeline / Migration Validation (push) Blocked by required conditions
CI/CD Pipeline / UI Contracts (source-structure gates) (push) Waiting to run
CI/CD Pipeline / E2E (live stack) (push) Blocked by required conditions
This commit is contained in:
@@ -217,6 +217,7 @@ const navigation: NavigationItem[] = [
|
||||
{ label: '组织人员', path: '/organizations', permissions: ['organization.manage'], icon: UserFilled },
|
||||
{ label: '操作日志', path: '/audit', permissions: ['user.view'], icon: Clock },
|
||||
{ label: '系统设置', path: '/operators', permissions: ['permission.manage'], icon: Setting },
|
||||
{ label: '红娘团队管理', path: '/matchmaker-team', permissions: ['matchmaker.role.manage'], icon: Connection },
|
||||
]
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<el-card shadow="never">
|
||||
<template #header>
|
||||
<div class="header-row">
|
||||
<span>红娘团队管理</span>
|
||||
<div class="header-actions">
|
||||
<el-select v-model="status" clearable placeholder="全部状态" style="width:120px" @change="load">
|
||||
<el-option label="正常" value="normal" /><el-option label="停用" value="suspended" />
|
||||
</el-select>
|
||||
<el-input v-model="keyword" clearable placeholder="姓名或用户名" style="width:180px" @keyup.enter="load" />
|
||||
<el-button type="primary" @click="openCreate">新增红娘</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-alert v-if="error" type="error" :closable="false" :title="error" />
|
||||
<el-table v-loading="loading" :data="items" border stripe empty-text="暂无红娘团队成员">
|
||||
<el-table-column prop="username" label="用户名" width="150" />
|
||||
<el-table-column prop="name" label="姓名" width="120" />
|
||||
<el-table-column label="角色" width="130"><template #default="{ row }">{{ roleName(row.roles?.[0]) }}</template></el-table-column>
|
||||
<el-table-column label="数据范围" width="110"><template #default="{ row }">{{ scopeName(row.scope) }}</template></el-table-column>
|
||||
<el-table-column label="所属组织" min-width="180"><template #default="{ row }">{{ orgMap[row.organizationId] || '未绑定' }}</template></el-table-column>
|
||||
<el-table-column label="状态" width="90"><template #default="{ row }"><el-tag :type="row.status === 'normal' ? 'success' : 'danger'">{{ row.status === 'normal' ? '正常' : '停用' }}</el-tag></template></el-table-column>
|
||||
<el-table-column label="操作" width="300" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button size="small" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button size="small" @click="resetPassword(row)">重置密码</el-button>
|
||||
<el-button size="small" :type="row.status === 'normal' ? 'warning' : 'success'" @click="toggleStatus(row)">{{ row.status === 'normal' ? '停用' : '启用' }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-pagination v-if="total" v-model:current-page="page" style="margin-top:16px;text-align:right" layout="total, prev, pager, next" :page-size="pageSize" :total="total" @current-change="load" />
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="editing ? '编辑红娘团队成员' : '新增红娘团队成员'" width="520px">
|
||||
<el-form label-width="92px">
|
||||
<el-form-item label="姓名"><el-input v-model="form.name" /></el-form-item>
|
||||
<el-form-item label="角色"><el-radio-group v-model="form.roleCode"><el-radio value="matchmaker">普通红娘</el-radio><el-radio value="matchmaker_leader">红娘组长</el-radio></el-radio-group></el-form-item>
|
||||
<el-form-item label="所属组织" required><el-select v-model="form.organizationId" clearable style="width:100%"><el-option v-for="org in organizations" :key="org.id" :label="org.name" :value="org.id" /></el-select></el-form-item>
|
||||
<el-form-item v-if="!editing" label="初始密码"><el-input v-model="form.password" type="password" show-password /></el-form-item>
|
||||
<el-alert type="info" :closable="false" :title="`数据范围:${scopeName(form.roleCode === 'matchmaker_leader' ? 'TEAM' : 'SELF')}`" />
|
||||
</el-form>
|
||||
<template #footer><el-button @click="dialogVisible = false">取消</el-button><el-button type="primary" :loading="saving" @click="save">保存</el-button></template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { get, post, put } from '@/api/client'
|
||||
|
||||
const loading = ref(false)
|
||||
const saving = ref(false)
|
||||
const error = ref('')
|
||||
const items = ref<any[]>([])
|
||||
const organizations = ref<any[]>([])
|
||||
const total = ref(0)
|
||||
const page = ref(1)
|
||||
const pageSize = 20
|
||||
const keyword = ref('')
|
||||
const status = ref('')
|
||||
const dialogVisible = ref(false)
|
||||
const editing = ref<any | null>(null)
|
||||
const form = ref({ name: '', roleCode: 'matchmaker', organizationId: '', password: '' })
|
||||
const orgMap = computed(() => Object.fromEntries(organizations.value.map((org) => [org.id, org.name])))
|
||||
|
||||
function roleName(role: string) { return role === 'matchmaker_leader' ? '红娘组长' : '普通红娘' }
|
||||
function scopeName(scope: string) { return scope === 'TEAM' ? '团队范围' : scope === 'ALL' ? '全部范围' : '本人范围' }
|
||||
async function load() {
|
||||
loading.value = true; error.value = ''
|
||||
try {
|
||||
const res: any = await get('/operator/matchmaker-team/members', { page: page.value - 1, pageSize, keyword: keyword.value || undefined, status: status.value || undefined })
|
||||
items.value = res.data?.items || []; total.value = res.data?.total || 0
|
||||
} catch (e: any) { error.value = e?.response?.data?.message || '红娘团队数据加载失败' } finally { loading.value = false }
|
||||
}
|
||||
async function loadOrganizations() {
|
||||
const res: any = await get('/operator/operators/organizations')
|
||||
organizations.value = res.data || []
|
||||
}
|
||||
function openCreate() { editing.value = null; form.value = { name: '', roleCode: 'matchmaker', organizationId: '', password: '' }; dialogVisible.value = true }
|
||||
function openEdit(row: any) { editing.value = row; form.value = { name: row.name, roleCode: row.roles?.[0] || 'matchmaker', organizationId: row.organizationId || '', password: '' }; dialogVisible.value = true }
|
||||
async function save() {
|
||||
if (!form.value.name.trim()) return ElMessage.warning('请填写红娘姓名')
|
||||
if (form.value.roleCode === 'matchmaker_leader' && !form.value.organizationId) return ElMessage.warning('红娘组长必须选择所属组织')
|
||||
if (!editing.value && form.value.password.length < 8) return ElMessage.warning('初始密码至少8位')
|
||||
saving.value = true
|
||||
try {
|
||||
const payload: any = { name: form.value.name, roleCode: form.value.roleCode, organizationId: form.value.organizationId || null }
|
||||
if (!editing.value) Object.assign(payload, { password: form.value.password })
|
||||
const res: any = editing.value ? await put(`/operator/matchmaker-team/members/${editing.value.id}`, payload) : await post('/operator/matchmaker-team/members', payload)
|
||||
if (!res.success) throw new Error(res.message || '保存失败')
|
||||
dialogVisible.value = false; ElMessage.success('保存成功'); await load()
|
||||
} catch (e: any) { ElMessage.error(e?.response?.data?.message || e.message || '保存失败') } finally { saving.value = false }
|
||||
}
|
||||
async function toggleStatus(row: any) {
|
||||
await put(`/operator/matchmaker-team/members/${row.id}/${row.status === 'normal' ? 'disable' : 'enable'}`)
|
||||
ElMessage.success('状态已更新'); await load()
|
||||
}
|
||||
async function resetPassword(row: any) {
|
||||
const value = await ElMessageBox.prompt('请输入新密码(至少8位)', '重置密码', { inputType: 'password', inputValidator: (v) => v.length >= 8 || '密码至少8位' })
|
||||
await put(`/operator/matchmaker-team/members/${row.id}/reset-password`, { newPassword: value.value })
|
||||
ElMessage.success('密码已重置')
|
||||
}
|
||||
onMounted(async () => { await loadOrganizations(); await load() })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.header-row,.header-actions{display:flex;align-items:center}.header-row{justify-content:space-between}.header-actions{gap:8px}
|
||||
</style>
|
||||
@@ -12,6 +12,14 @@
|
||||
<el-table-column prop="username" label="用户名" width="140" />
|
||||
<el-table-column prop="displayId" label="人员编号" width="150" />
|
||||
<el-table-column prop="name" label="姓名" width="120" />
|
||||
<el-table-column label="角色" min-width="190">
|
||||
<template #default="{row}">
|
||||
<el-tag v-for="role in row.roles || []" :key="role" size="small" style="margin-right:4px">{{ roleName(role) }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="数据范围" width="110">
|
||||
<template #default="{row}">{{ scopeName(row.scope) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="organizationId" label="所属机构" width="160">
|
||||
<template #default="{row}">{{ orgMap[row.organizationId] || '-' }}</template>
|
||||
</el-table-column>
|
||||
@@ -26,8 +34,9 @@
|
||||
<el-table-column prop="createdAt" label="创建时间" width="180">
|
||||
<template #default="{row}">{{ formatTime(row.createdAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="200">
|
||||
<el-table-column label="操作" width="270">
|
||||
<template #default="{row}">
|
||||
<el-button size="small" @click="openEdit(row)">角色权限</el-button>
|
||||
<el-button size="small" @click="resetPwd(row.id)">重置密码</el-button>
|
||||
<el-button size="small" :type="row.status==='normal'?'warning':'success'"
|
||||
@click="toggleStatus(row)">{{ row.status==='normal'?'停用':'启用' }}</el-button>
|
||||
@@ -41,15 +50,23 @@
|
||||
</el-card>
|
||||
|
||||
<!-- 新增对话框 -->
|
||||
<el-dialog v-model="formVisible" title="新增Operator" width="480px">
|
||||
<el-dialog v-model="formVisible" title="新增工作人员" width="560px">
|
||||
<el-form :model="form" label-width="100px">
|
||||
<el-form-item label="用户名"><el-input v-model="form.username" /></el-form-item>
|
||||
<el-form-item label="姓名"><el-input v-model="form.name" /></el-form-item>
|
||||
<el-form-item label="所属机构">
|
||||
<el-select v-model="form.organizationId" style="width:100%">
|
||||
<el-select v-model="form.organizationId" clearable style="width:100%">
|
||||
<el-option v-for="o in orgList" :key="o.id" :label="o.name" :value="o.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色">
|
||||
<el-checkbox-group v-model="form.roleCodes">
|
||||
<el-checkbox v-for="role in roleTemplates" :key="role.code" :label="role.code">{{ role.name }}</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
<el-alert v-if="selectedPermissions.length" type="info" :closable="false" title="将继承以下权限">
|
||||
{{ selectedPermissions.join('、') }}
|
||||
</el-alert>
|
||||
<el-form-item label="密码">
|
||||
<el-input v-model="form.password" type="password" show-password />
|
||||
</el-form-item>
|
||||
@@ -59,12 +76,35 @@
|
||||
<el-button type="primary" :loading="saving" @click="save">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="editVisible" title="调整角色与数据范围" width="560px">
|
||||
<el-form :model="editForm" label-width="100px">
|
||||
<el-form-item label="账号">{{ editForm.name }}({{ editForm.username }})</el-form-item>
|
||||
<el-form-item label="所属机构">
|
||||
<el-select v-model="editForm.organizationId" clearable style="width:100%">
|
||||
<el-option v-for="o in orgList" :key="o.id" :label="o.name" :value="o.id" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色">
|
||||
<el-checkbox-group v-model="editForm.roleCodes">
|
||||
<el-checkbox v-for="role in roleTemplates" :key="role.code" :label="role.code">{{ role.name }}</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
<el-alert v-if="editPermissions.length" type="info" :closable="false" title="将继承以下权限">
|
||||
{{ editPermissions.join('、') }}
|
||||
</el-alert>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="editVisible=false">取消</el-button>
|
||||
<el-button type="primary" :loading="saving" @click="saveEdit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getAccessToken } from '@/utils/token'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { get, post, put } from '@/api/client'
|
||||
|
||||
const loading = ref(false)
|
||||
@@ -74,7 +114,10 @@ const page = ref(0)
|
||||
const pageSize = ref(20)
|
||||
const formVisible = ref(false)
|
||||
const saving = ref(false)
|
||||
const form = ref({ username: '', name: '', organizationId: '', password: '' })
|
||||
const form = ref({ username: '', name: '', organizationId: '', password: '', roleCodes: [] as string[] })
|
||||
const editVisible = ref(false)
|
||||
const editForm = ref({ id: '', username: '', name: '', organizationId: '', roleCodes: [] as string[] })
|
||||
const roleTemplates = ref<any[]>([])
|
||||
const orgList = ref<any[]>([])
|
||||
const orgMap = ref<Record<string, string>>({})
|
||||
|
||||
@@ -83,12 +126,20 @@ async function loadData() {
|
||||
try {
|
||||
const token = getAccessToken()
|
||||
const res: any = await get('/operator/operators', { page: page.value, pageSize: pageSize.value }, { Authorization: `Bearer ${token}` })
|
||||
tableData.value = res.data?.list || []
|
||||
tableData.value = res.data?.items || res.data?.list || []
|
||||
total.value = res.data?.total || 0
|
||||
} catch (e) { console.error(e) }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
|
||||
async function loadRoles() {
|
||||
try {
|
||||
const token = getAccessToken()
|
||||
const res: any = await get('/operator/operators/roles', {}, { Authorization: `Bearer ${token}` })
|
||||
roleTemplates.value = res.data || []
|
||||
} catch (e) { console.error(e) }
|
||||
}
|
||||
|
||||
async function loadOrgs() {
|
||||
try {
|
||||
const token = getAccessToken()
|
||||
@@ -99,7 +150,7 @@ async function loadOrgs() {
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
form.value = { username: '', name: '', organizationId: '', password: '' }
|
||||
form.value = { username: '', name: '', organizationId: '', password: '', roleCodes: ['matchmaker'] }
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
@@ -108,6 +159,8 @@ async function save() {
|
||||
saving.value = true
|
||||
try {
|
||||
const token = getAccessToken()
|
||||
if (!form.value.roleCodes.length) return
|
||||
if (form.value.roleCodes.includes('matchmaker_leader') && !form.value.organizationId) return
|
||||
await post('/operator/operators', form.value, { Authorization: `Bearer ${token}` })
|
||||
formVisible.value = false
|
||||
loadData()
|
||||
@@ -115,6 +168,29 @@ async function save() {
|
||||
finally { saving.value = false }
|
||||
}
|
||||
|
||||
function openEdit(row: any) {
|
||||
editForm.value = { id: row.id, username: row.username, name: row.name, organizationId: row.organizationId || '', roleCodes: [...(row.roles || [])] }
|
||||
editVisible.value = true
|
||||
}
|
||||
|
||||
async function saveEdit() {
|
||||
if (!editForm.value.roleCodes.length) return
|
||||
if (editForm.value.roleCodes.includes('matchmaker_leader') && !editForm.value.organizationId) return
|
||||
saving.value = true
|
||||
try {
|
||||
const token = getAccessToken()
|
||||
await put(`/operator/operators/${editForm.value.id}/roles`, { roleCodes: editForm.value.roleCodes, organizationId: editForm.value.organizationId || null }, { Authorization: `Bearer ${token}` })
|
||||
editVisible.value = false
|
||||
await loadData()
|
||||
} catch (e) { console.error(e) }
|
||||
finally { saving.value = false }
|
||||
}
|
||||
|
||||
function roleName(code: string) { return roleTemplates.value.find(r => r.code === code)?.name || code }
|
||||
function scopeName(scope: string) { return scope === 'ALL' ? '全平台' : scope === 'TEAM' ? '本组织' : '本人' }
|
||||
const selectedPermissions = computed(() => [...new Set(form.value.roleCodes.flatMap(code => roleTemplates.value.find(r => r.code === code)?.permissions || []))])
|
||||
const editPermissions = computed(() => [...new Set(editForm.value.roleCodes.flatMap(code => roleTemplates.value.find(r => r.code === code)?.permissions || []))])
|
||||
|
||||
async function toggleStatus(row: any) {
|
||||
try {
|
||||
const token = getAccessToken()
|
||||
@@ -136,7 +212,7 @@ async function resetPwd(id: string) {
|
||||
function handlePageChange(p: number) { page.value = p - 1; loadData() }
|
||||
function formatTime(t?: string) { return t ? new Date(t).toLocaleString('zh-CN') : '-' }
|
||||
|
||||
onMounted(() => { loadData(); loadOrgs() })
|
||||
onMounted(() => { loadData(); loadOrgs(); loadRoles() })
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<el-table-column prop="contactName" label="联系人" width="120" />
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{row}">
|
||||
<el-tag :type="row.status==='normal'?'success':'danger'">{{ row.status==='normal'?'正常':'已停用' }}</el-tag>
|
||||
<el-tag :type="row.status==='active'?'success':'danger'">{{ row.status==='active'?'正常':'已停用' }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="createdAt" label="创建时间" width="180">
|
||||
@@ -25,8 +25,8 @@
|
||||
<el-table-column label="操作" width="180">
|
||||
<template #default="{row}">
|
||||
<el-button size="small" @click="openEdit(row)">编辑</el-button>
|
||||
<el-button size="small" :type="row.status==='normal'?'warning':'success'"
|
||||
@click="toggleStatus(row)">{{ row.status==='normal'?'停用':'启用' }}</el-button>
|
||||
<el-button size="small" :type="row.status==='active'?'warning':'success'"
|
||||
@click="toggleStatus(row)">{{ row.status==='active'?'停用':'启用' }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
@@ -40,10 +40,11 @@
|
||||
</el-form-item>
|
||||
<el-form-item label="机构类型">
|
||||
<el-select v-model="form.type" style="width:100%">
|
||||
<el-option label="妇联" value="fulian" />
|
||||
<el-option label="工会" value="gonghui" />
|
||||
<el-option label="公益服务中心" value="public_service" />
|
||||
<el-option label="志愿者团队" value="volunteer" />
|
||||
<el-option label="妇联" value="women_federation" />
|
||||
<el-option label="工会" value="trade_union" />
|
||||
<el-option label="公益服务中心" value="charity_center" />
|
||||
<el-option label="志愿者团队" value="volunteer_team" />
|
||||
<el-option label="民政合作单位" value="civil_affairs_partner" />
|
||||
<el-option label="其他合作机构" value="other" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
@@ -72,8 +73,8 @@ const form = ref({ name: '', type: '', contactName: '' })
|
||||
const saving = ref(false)
|
||||
|
||||
const ORG_TYPE_TEXT: Record<string, string> = {
|
||||
fulian: '妇联', gonghui: '工会', public_service: '公益服务中心',
|
||||
volunteer: '志愿者团队', other: '其他合作机构'
|
||||
women_federation: '妇联', trade_union: '工会', charity_center: '公益服务中心',
|
||||
volunteer_team: '志愿者团队', civil_affairs_partner: '民政合作单位', other: '其他合作机构'
|
||||
}
|
||||
|
||||
async function loadData() {
|
||||
@@ -88,7 +89,7 @@ async function loadData() {
|
||||
|
||||
function openCreate() {
|
||||
editId.value = ''
|
||||
form.value = { name: '', type: 'fulian', contactName: '' }
|
||||
form.value = { name: '', type: 'women_federation', contactName: '' }
|
||||
formVisible.value = true
|
||||
}
|
||||
|
||||
@@ -121,7 +122,7 @@ async function save() {
|
||||
async function toggleStatus(row: any) {
|
||||
try {
|
||||
const token = getAccessToken()
|
||||
const action = row.status === 'normal' ? 'disable' : 'enable'
|
||||
const action = row.status === 'active' ? 'disable' : 'enable'
|
||||
await put(`/operator/organizations/${row.id}/${action}`, {}, {
|
||||
Authorization: `Bearer ${token}`
|
||||
})
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div class="page-container page-shell">
|
||||
<header class="page-heading"><div><h1>后台建档</h1><p>建立草稿档案,手机号核验和资料审核仍需独立完成。</p></div></header>
|
||||
<section class="surface-card form-card">
|
||||
<el-form label-position="top" @submit.prevent="submit">
|
||||
<el-form-item label="姓名"><el-input v-model="form.name" /></el-form-item>
|
||||
<el-form-item label="所属组织"><el-input v-model="form.organization" /></el-form-item>
|
||||
<el-form-item label="组织联系电话"><el-input v-model="form.organizationPhone" /></el-form-item>
|
||||
<el-form-item label="手机号"><el-input v-model="form.phone" /></el-form-item>
|
||||
<el-button type="primary" :loading="loading" @click="submit">创建草稿</el-button>
|
||||
</el-form>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { post } from '@/api/client'
|
||||
const loading = ref(false)
|
||||
const form = reactive({ name: '', organization: '', organizationPhone: '', phone: '' })
|
||||
async function submit() {
|
||||
loading.value = true
|
||||
try { await post('/operator/users', form); ElMessage.success('草稿档案已创建'); Object.assign(form, { name: '', organization: '', organizationPhone: '', phone: '' }) }
|
||||
finally { loading.value = false }
|
||||
}
|
||||
</script>
|
||||
<style scoped>.page-container{padding:28px}.form-card{max-width:620px;padding:24px}</style>
|
||||
@@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<div class="page-container page-shell">
|
||||
<header class="page-heading"><div><h1>CSV 批量建档</h1><p>仅支持 UTF-8 CSV;预览不会写入用户表。</p></div></header>
|
||||
<section class="surface-card form-card">
|
||||
<input type="file" accept=".csv,text/csv" @change="onFile" />
|
||||
<el-button type="primary" :loading="loading" :disabled="!fileText" @click="preview">预览</el-button>
|
||||
<el-alert v-if="error" :title="error" type="error" :closable="false" />
|
||||
<el-table v-if="result" :data="result.rows" class="preview-table"><el-table-column prop="rowNumber" label="行号" width="80" /><el-table-column prop="status" label="状态" /><el-table-column prop="error" label="错误" /></el-table>
|
||||
<el-button v-if="result && result.validRows" type="success" :loading="committing" @click="commit">提交 {{ result.validRows }} 行</el-button>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { post } from '@/api/client'
|
||||
const fileText = ref(''); const result = ref<any>(null); const error = ref(''); const loading = ref(false); const committing = ref(false)
|
||||
function onFile(event: Event) { const file = (event.target as HTMLInputElement).files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => { fileText.value = String(reader.result || '') }; reader.readAsText(file, 'UTF-8') }
|
||||
async function preview() { loading.value = true; error.value = ''; try { const response: any = await post('/operator/users/import/preview', fileText.value, { 'Content-Type': 'text/csv' }); result.value = response.data } catch (e: any) { error.value = e.response?.data?.message || '预览失败' } finally { loading.value = false } }
|
||||
async function commit() { if (!result.value?.batchId) return; committing.value = true; try { await post('/operator/users/import/commit', { batchId: result.value.batchId, idempotencyKey: crypto.randomUUID() }); ElMessage.success('导入提交成功'); result.value = null } finally { committing.value = false } }
|
||||
</script>
|
||||
<style scoped>.page-container{padding:28px}.form-card{padding:24px;display:grid;gap:16px}.preview-table{margin-top:12px}</style>
|
||||
@@ -6,7 +6,7 @@
|
||||
<h1>用户管理</h1>
|
||||
<p>仅按当前角色权限查询用户,手机号等敏感信息默认脱敏。</p>
|
||||
</div>
|
||||
<el-tag type="info" effect="plain">共 {{ total }} 人</el-tag>
|
||||
<div class="heading-actions"><el-button v-if="canCreate" type="primary" @click="router.push('/users/create')">后台建档</el-button><el-button v-if="canCreate" @click="router.push('/users/import')">CSV导入</el-button><el-tag type="info" effect="plain">共 {{ total }} 人</el-tag></div>
|
||||
</header>
|
||||
|
||||
<section class="surface-card filter-panel" aria-label="用户筛选">
|
||||
@@ -101,12 +101,15 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { get } from '@/api/client'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const canCreate = computed(() => authStore.permissions.includes('user.create'))
|
||||
const loading = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const keyword = ref('')
|
||||
|
||||
@@ -25,9 +25,12 @@ const routes: RouteRecordRaw[] = [
|
||||
{ path: 'risk/reports', name: 'ReportCenter', component: () => import('@/modules/risk/ReportCenterView.vue'), meta: { title: '举报中心', permissions: ['risk.report.view'] } },
|
||||
{ path: 'appeals', name: 'AppealCenter', component: () => import('@/modules/appeal/AppealCenterView.vue'), meta: { title: '申诉管理', permissions: ['risk.report.process'] } },
|
||||
{ path: 'users', name: 'UserManagement', component: () => import('@/modules/user/UserManagementView.vue'), meta: { title: '用户管理', permissions: ['user.view'] } },
|
||||
{ path: 'users/create', name: 'UserCreate', component: () => import('@/modules/user/UserCreateView.vue'), meta: { title: '后台建档', permissions: ['user.create'] } },
|
||||
{ path: 'users/import', name: 'UserImport', component: () => import('@/modules/user/UserImportView.vue'), meta: { title: '批量建档', permissions: ['user.create'] } },
|
||||
{ path: 'users/detail', name: 'UserDetail', component: () => import('@/modules/user/UserDetailView.vue'), meta: { title: '用户详情', permissions: ['user.view', 'user.sensitive.view'] } },
|
||||
{ path: 'organizations', name: 'OrganizationManagement', component: () => import('@/modules/organization/OrganizationView.vue'), meta: { title: '组织管理', permissions: ['organization.manage'] } },
|
||||
{ path: 'operators', name: 'OperatorManagement', component: () => import('@/modules/operator/OperatorManagementView.vue'), meta: { title: '工作人员管理', permissions: ['permission.manage'] } },
|
||||
{ path: 'matchmaker-team', name: 'MatchmakerTeamManagement', component: () => import('@/modules/operator/MatchmakerTeamManagementView.vue'), meta: { title: '红娘团队管理', permissions: ['matchmaker.role.manage'] } },
|
||||
{ path: 'analytics', name: 'Analytics', component: () => import('@/modules/analytics/AnalyticsView.vue'), meta: { title: '数据统计', permissions: ['analytics.view'] } },
|
||||
{ path: 'audit', name: 'AuditLog', component: () => import('@/modules/audit/AuditLogView.vue'), meta: { title: '操作日志', permissions: ['user.view'] } },
|
||||
// V1.1 新增模块
|
||||
@@ -61,6 +64,10 @@ router.beforeEach(async (to) => {
|
||||
}
|
||||
|
||||
// 2. 权限检查
|
||||
if ((to.path === '/referral' || to.path === '/matchmaker') && authStore.permissions.includes('permission.manage')) {
|
||||
ElMessage.info('管理员请在工作台查看红娘业务统计')
|
||||
return { path: '/dashboard' }
|
||||
}
|
||||
if (requiredPerms.length > 0 && !hasPermission(requiredPerms)) {
|
||||
ElMessage.warning('您没有访问该页面的权限')
|
||||
return { path: '/dashboard' }
|
||||
|
||||
@@ -18,6 +18,8 @@ export default defineConfig(({ command }) => ({
|
||||
server: {
|
||||
// 本机开发端口。避开 3000/8080/8081/8085 等被其他项目占用的常用端口。
|
||||
port: 13520,
|
||||
// 允许同一局域网的浏览器访问开发页面。
|
||||
host: '0.0.0.0',
|
||||
// 端口被占用时直接失败,而不是静默 +1 退到 13521。
|
||||
// 静默漂移会让「文档写 13520、实际跑在别的端口」这种不一致无法被发现。
|
||||
strictPort: true,
|
||||
|
||||
Reference in New Issue
Block a user