Files
geMoldInsight/frontend/src/modules/users/UsersView.vue
T
cjw 0e6b3b1811 后端设计治理:批次 0-4 全部完成(安全/部署/一致性/结构/架构)
按 ROADMAP §3.1 治理批次推进的后端设计审查整改:

- 批次 0(安全):/api/status/{task_id} 补 JWT 鉴权与任务归属校验;
  pythonocc_available 真实探测;bcrypt 超 72 字节显式拒绝;
  SECRET_KEY/RUSTFS_* 惰性校验,代码侧弱默认移除
- 批次 1(部署正确性):主处理链路改走 RustFS(分派入参 stp_file_id 化,
  worker 按 object_key 下载);AUTO_MIGRATE 开关 + 迁移目录 alembic/→migrations/
  修复包遮蔽(自动迁移此前从未真正生效);OCC 镜像改 conda 原生执行 +
  基础镜像 tag 锁定;compose 关键项改 ${VAR:?} 强制显式配置
- 批次 2(任务一致性):删除 Redis 进程内存回退,PG 为任务状态单一事实源;
  批量元数据入库(processing_tasks.batch_id,迁移 a3f8c2d91e47);
  型腔失败任务标 failed 不再静默 completed;事务边界收口
  (数据本体写 flush-only、失败先回滚再置 failed、进度更新保留即时 commit)
- 批次 3(API 与代码结构):592 行 advanced_router 拆为 design/cost/machining/
  export 四子路由,请求体全量 Pydantic 化;ROUTE_MODULES + route_registry
  (/api/health 呈现 degraded,DEBUG fail fast);纯计算端点统一 to_thread;
  StorageIntegrationService 按职责三拆;MAX_FILE_SIZE 接线生效、
  celery 复用 Settings.redis_url;管理员重置密码改 JSON body(端到端断裂修复);
  openapi.json 重导出(76 paths)+ 前端 gen:api
- 批次 4(架构演进):共享 ORM 按模块拆分(shared/models/base.py + identity.py、
  moldinsight/models/、inventory/models/,删除三条无使用方的跨模块
  relationship,跨模块桥接收敛为裸 FK 硬规则,无兼容 facade);
  OCC executor 重建补 cancel_futures=True(消除旧队列被慢恢复线程
  并行消化的数据竞争);OCC 吞吐方案设计先行
  (docs/topics/performance/OCC_THROUGHPUT.md);顺手清偿 D15
  (vite.config.ts 未用参数致 npm run build 失败)

测试基线:125 passed, 2 skipped(pytest + sqlite+aiosqlite;归属边界、
路由契约、配置治理、鉴权回归等随批新增)
文档同步:STATUS / TECH_DEBT / ROADMAP / ARCHITECTURE / API_CONTRACT /
OPERATIONS / AGENTS

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-17 16:15:49 +08:00

261 lines
8.0 KiB
Vue

<template>
<div class="page-container">
<div class="page-header">
<div>
<h1>用户管理</h1>
<p>管理系统用户和权限</p>
</div>
<t-button theme="primary" @click="openUserModal()">+ 添加用户</t-button>
</div>
<t-loading :loading="state.loading" size="large">
<t-table :data="state.users" row-key="id" stripe>
<t-table-column colKey="username" title="用户名" />
<t-table-column colKey="email" title="邮箱" />
<t-table-column colKey="full_name" title="姓名">
<template #cell="{ row }">{{ row.full_name || '-' }}</template>
</t-table-column>
<t-table-column colKey="is_active" title="状态">
<template #cell="{ row }">
<t-tag :theme="row.is_active ? 'success' : 'danger'">
{{ row.is_active ? '正常' : '禁用' }}
</t-tag>
</template>
</t-table-column>
<t-table-column colKey="roles" title="角色">
<template #cell="{ row }">
<t-tag v-for="role in row.roles" :key="role" theme="primary" variant="light" style="margin-right: 4px;">
{{ role }}
</t-tag>
</template>
</t-table-column>
<t-table-column colKey="created_at" title="注册时间">
<template #cell="{ row }">{{ formatDateTime(row.created_at) }}</template>
</t-table-column>
<t-table-column colKey="actions" title="操作">
<template #cell="{ row }">
<t-space :size="4">
<t-button theme="primary" size="small" @click="openUserModal(row)">编辑</t-button>
<t-button theme="warning" size="small" @click="resetPassword(row)">重置密码</t-button>
<t-button v-if="row.id !== storeUser?.id" theme="danger" size="small" @click="deleteUser(row)">删除</t-button>
</t-space>
</template>
</t-table-column>
</t-table>
<t-empty v-if="!state.loading && state.users.length === 0" description="暂无用户数据" />
</t-loading>
<t-dialog
v-model:visible="state.showUserModal"
:header="state.editingUser ? '编辑用户' : '添加用户'"
:close-on-overlay-click="true"
width="520px"
>
<t-form label-width="80px">
<t-form-item label="用户名">
<t-input v-model="state.userForm.username" :disabled="!!state.editingUser" />
</t-form-item>
<t-form-item label="邮箱">
<t-input v-model="state.userForm.email" type="email" />
</t-form-item>
<t-form-item v-if="!state.editingUser" label="密码">
<t-input v-model="state.userForm.password" type="password" />
</t-form-item>
<t-form-item label="姓名">
<t-input v-model="state.userForm.full_name" />
</t-form-item>
<t-form-item label="角色">
<div style="display: flex; flex-direction: column; gap: 8px;">
<label v-for="role in state.roles" :key="role.id" style="display: flex; align-items: center; gap: 6px; cursor: pointer;">
<input type="checkbox" :value="role.id" v-model="state.userForm.role_ids" />
{{ role.name }}
</label>
</div>
</t-form-item>
</t-form>
<template #footer>
<t-button theme="default" @click="state.showUserModal = false">取消</t-button>
<t-button theme="primary" @click="saveUser">保存</t-button>
</template>
</t-dialog>
</div>
</template>
<script setup lang="ts">
import { reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { DialogPlugin } from 'tdesign-vue-next'
import { apiRequest } from '@/shared/api'
import { useAppStore } from '@/stores/app'
import { addNotification } from '@/shared/notification'
import { formatDateTime } from '@/shared/utils'
const router = useRouter()
const store = useAppStore()
const storeUser = store.user
interface UserItem {
id: number
username: string
email: string
full_name: string | null
is_active: boolean
roles: string[]
created_at: string
}
interface RoleItem {
id: number
code: string
name: string
}
const state = reactive({
users: [] as UserItem[],
roles: [] as RoleItem[],
loading: true,
showUserModal: false,
editingUser: null as UserItem | null,
userForm: {
username: '',
email: '',
password: '',
full_name: '',
role_ids: [] as number[],
},
})
function confirmDialog(header: string, body: string, theme: string, confirmText: string, cancelText: string): Promise<boolean> {
return new Promise((resolve) => {
const dlg = DialogPlugin.confirm({
header,
body,
theme: theme as any,
confirmBtn: confirmText,
cancelBtn: cancelText,
onConfirm: () => { dlg.hide(); resolve(true) },
onCancel: () => { dlg.hide(); resolve(false) },
onClose: () => { resolve(false) },
})
})
}
const loadUsers = async () => {
try {
state.users = await apiRequest<UserItem[]>('/api/auth/users')
} catch (e) {
const err = e as Error
addNotification(err.message || '加载用户列表失败', 'error')
} finally {
state.loading = false
}
}
const loadRoles = async () => {
try {
state.roles = await apiRequest<RoleItem[]>('/api/auth/roles')
} catch (e) {
const err = e as Error
addNotification(err.message || '加载角色列表失败', 'error')
}
}
const openUserModal = (user: UserItem | null = null) => {
state.editingUser = user
if (user) {
state.userForm = {
username: user.username,
email: user.email,
password: '',
full_name: user.full_name || '',
role_ids: user.roles
.map(r => {
const role = state.roles.find(role => role.code === r)
return role ? role.id : null
})
.filter(id => id !== null) as number[],
}
} else {
state.userForm = { username: '', email: '', password: '', full_name: '', role_ids: [] }
}
state.showUserModal = true
}
const saveUser = async () => {
if (!state.userForm.username || !state.userForm.email) {
addNotification('请填写用户名和邮箱', 'error')
return
}
if (!state.editingUser && !state.userForm.password) {
addNotification('请填写密码', 'error')
return
}
try {
if (state.editingUser) {
await apiRequest(`/api/auth/users/${(state.editingUser as UserItem).id}`, {
method: 'PUT',
body: JSON.stringify({
email: state.userForm.email,
full_name: state.userForm.full_name || null,
role_ids: state.userForm.role_ids,
}),
})
addNotification('用户更新成功', 'success')
} else {
await apiRequest('/api/auth/users', {
method: 'POST',
body: JSON.stringify(state.userForm),
})
addNotification('用户创建成功', 'success')
}
state.showUserModal = false
loadUsers()
} catch (e) {
const err = e as Error
addNotification(err.message || '保存用户失败', 'error')
}
}
const deleteUser = async (user: UserItem) => {
if (!await confirmDialog('删除确认', `确定要删除用户 ${user.username} 吗?`, 'warning', '删除', '取消')) return
try {
await apiRequest(`/api/auth/users/${user.id}`, { method: 'DELETE' })
addNotification('用户已删除', 'success')
loadUsers()
} catch (e) {
const err = e as Error
addNotification(err.message || '删除用户失败', 'error')
}
}
const resetPassword = async (user: UserItem) => {
const newPassword = prompt(`请输入 ${user.username} 的新密码:`)
if (!newPassword || newPassword.length < 6) {
addNotification('密码长度至少6位', 'error')
return
}
try {
await apiRequest(`/api/auth/users/${user.id}/reset-password`, {
method: 'PUT',
body: JSON.stringify({ new_password: newPassword }),
})
addNotification('密码已重置', 'success')
} catch (e) {
const err = e as Error
addNotification(err.message || '重置密码失败', 'error')
}
}
onMounted(async () => {
if (!store.user?.is_superuser) {
router.push('/')
return
}
await loadRoles()
loadUsers()
})
</script>